Commit 6d810d65 authored by Matija Obreza's avatar Matija Obreza
Browse files

Added support for inverse (excluding) filters

parent 4ede2380
......@@ -226,17 +226,25 @@ public class DirectMysqlQuery {
} else {
sb.append(" and ");
}
// Opening
if (appliedFilter.isInverse()) {
sb.append(" NOT ");
}
sb.append(" ( ");
// A filter value can be (a) explicit value or (b) an operation
int toHandle=filterValues.size();
if (appliedFilter.getWithNull()) {
toHandle++;
}
// (a) explicit values are handled by =? or by IN (?,?,..)
int handledCount = handleExplicitValues(sb, dbName, filterValues, params);
// do we have more?
if (handledCount > 0 && filterValues.size() > handledCount) {
if (handledCount > 0 && toHandle > handledCount) {
sb.append(" OR ");
}
......@@ -244,7 +252,7 @@ public class DirectMysqlQuery {
handledCount += handledCountNull;
// do we have more?
if (handledCountNull > 0 && handledCount > 0 && filterValues.size() > handledCount) {
if (handledCountNull > 0 && handledCount > 0 && toHandle > handledCount) {
sb.append(" OR ");
}
......
......@@ -255,7 +255,11 @@ public class ElasticsearchSearchServiceImpl implements ElasticService, Initializ
}
}
filterBuilder.add(orFilter);
if (appliedFilter.isInverse()) {
filterBuilder.add(FilterBuilders.notFilter(orFilter));
} else {
filterBuilder.add(orFilter);
}
}
return filterBuilder;
......
......@@ -194,7 +194,7 @@ public class FilterHandler {
public void serialize(AppliedFilters filters, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
jgen.writeStartObject();
for (AppliedFilter filter : filters) {
jgen.writeArrayFieldStart(filter.getFilterName());
jgen.writeArrayFieldStart(filter.getKey());
for (FilterValue fv : filter.getValues())
jgen.writeObject(fv);
if (filter.withNull)
......@@ -225,7 +225,12 @@ public class FilterHandler {
while (jp.getCurrentToken() == JsonToken.FIELD_NAME) {
AppliedFilter af = new AppliedFilter();
appliedFilters.add(af);
af.setFilterName(jp.getCurrentName());
String name = jp.getCurrentName();
if (name.startsWith("-")) {
af.setInverse(true);
name = name.substring(1);
}
af.setFilterName(name);
jp.nextToken();
if (jp.getCurrentToken() == JsonToken.START_ARRAY) {
......@@ -391,6 +396,7 @@ public class FilterHandler {
private Set<FilterValue> values = new HashSet<FilterValue>();
private boolean withNull = false;
private String filterName;
private boolean inverse = false;
public String getFilterName() {
return this.filterName;
......@@ -411,6 +417,29 @@ public class FilterHandler {
return this;
}
/**
* When inverse, selected values are excluded instead of included
*/
public boolean isInverse() {
return inverse;
}
/**
* When inverse, selected values are excluded instead of included
*/
public boolean getInverse() {
return isInverse();
}
/**
* Set the filter to be inverse (i.e. excluding) selected values
*
* @param inverse
*/
public void setInverse(boolean inverse) {
this.inverse = inverse;
}
public boolean getWithNull() {
return this.withNull;
}
......@@ -424,6 +453,7 @@ public class FilterHandler {
final int prime = 31;
int result = 1;
result = prime * result + ((filterName == null) ? 0 : filterName.hashCode());
result = prime * result + (inverse ? 1231 : 1237);
result = prime * result + ((values == null) ? 0 : values.hashCode());
result = prime * result + (withNull ? 1231 : 1237);
return result;
......@@ -443,6 +473,8 @@ public class FilterHandler {
return false;
} else if (!filterName.equals(other.filterName))
return false;
if (inverse != other.inverse)
return false;
if (values == null) {
if (other.values != null)
return false;
......@@ -456,6 +488,18 @@ public class FilterHandler {
public int size() {
return values.size();
}
/**
* Return the filter key used in JSON
*
* @return "filterName" or "-filterName" for negative filters
*/
public String getKey() {
if (inverse) {
return "-" + this.filterName;
}
return this.filterName;
}
}
@JsonSerialize(using = LiteralValueFilter.Serializer.class)
......
......@@ -180,6 +180,7 @@ public class ExplorerController extends BaseController {
model.addAttribute("crops", cropService.list(getLocale()));
model.addAttribute("pagedData", accessions);
model.addAttribute("appliedFilters", appliedFilters);
model.addAttribute("currentFilters", currentFilters);
model.addAttribute("availableFilters", availableFilters);
......@@ -221,7 +222,7 @@ public class ExplorerController extends BaseController {
model.addAttribute("crop", crop);
// JSP works with JsonObject
// JSP works with JsonObject // TODO Handle -filter.key!!
final Map<?, ?> filters = mapper.readValue(appliedFilters.toString(), Map.class);
model.addAttribute("filters", filters);
......@@ -239,6 +240,7 @@ public class ExplorerController extends BaseController {
model.addAttribute("crops", cropService.list(getLocale()));
model.addAttribute("pagedData", accessions);
model.addAttribute("appliedFilters", appliedFilters);
model.addAttribute("currentFilters", currentFilters);
model.addAttribute("availableFilters", availableFilters);
......@@ -447,6 +449,7 @@ public class ExplorerController extends BaseController {
AppliedFilters appliedFilters = mapper.readValue(jsonFilter, AppliedFilters.class);
String[] selectedFilters = appliedFilters.getFilterNames();
final List<GenesysFilter> currentFilters = filterHandler.selectFilters(selectedFilters);
model.addAttribute("appliedFilters", appliedFilters);
model.addAttribute("currentFilters", currentFilters);
// JSP works with JsonObject
......
......@@ -367,6 +367,8 @@ filter.coll.collMissId=Collecting mission ID
filter.storage=Type of Germplasm storage
filter.string.equals=Equals
filter.string.like=Starts with
filter.inverse=Excluding
filter.set-inverse=Exclude selected values
search.page.title=Full-text Search
......
......@@ -90,6 +90,7 @@
<c:forEach items="${currentFilters}" var="filter">
<c:set var="normalizedKey" value="${filter.key.replace('.', '-').replace(':', '_')}"/>
<c:set var="appliedFilter" value="${appliedFilters.get(filter.key)}" />
<div class="clearfix filter-block" id="<c:out value="${normalizedKey}" />_filter" norm-key="<c:out value="${normalizedKey}" />" i-key="<c:out value="${filter.key}" />">
<div class="col-lg-3 edit-fil">
......@@ -109,7 +110,7 @@
<c:forEach items="${filter.options}" var="option">
<div>
<label>
<input class="filter-list" id="<c:out value="${normalizedKey}${option.value}" />_input" ${fn:contains(filters[filter.key], option.value)?'checked':''} norm-key="<c:out value="${normalizedKey}" />" i-key="<c:out value="${filter.key}" />" type="checkbox" value="${option.value}" />
<input class="filter-list" id="<c:out value="${normalizedKey}${option.value}" />_input" ${fn:contains(filters[appliedFilter.key], option.value)?'checked':''} norm-key="<c:out value="${normalizedKey}" />" i-key="<c:out value="${filter.key}" />" type="checkbox" value="${option.value}" />
<spring:message code="${option.name}"/>
</label>
</div>
......@@ -121,7 +122,7 @@
<c:forEach items="${filter.options}" var="option">
<div>
<label>
<input class="filter-list" id="<c:out value="${normalizedKey}${option.value}" />_input" ${fn:contains(filters[filter.key], option.value)?'checked':''} norm-key="<c:out value="${normalizedKey}" />" i-key="<c:out value="${filter.key}" />" type="checkbox" value="${option.value}" />
<input class="filter-list" id="<c:out value="${normalizedKey}${option.value}" />_input" ${fn:contains(filters[appliedFilter.key], option.value)?'checked':''} norm-key="<c:out value="${normalizedKey}" />" i-key="<c:out value="${filter.key}" />" type="checkbox" value="${option.value}" />
<spring:message code="${option.name}"/>
</label>
</div>
......@@ -149,9 +150,9 @@
</c:when>
<c:when test="${filter.dataType=='BOOLEAN'}">
<div class="">
<div><label><input type="checkbox" ${fn:contains(filters[filter.key], 'true')?'checked':''} class="filter-bool" i-key="<c:out value="${filter.key}" />" id="<c:out value="${normalizedKey}" />" value="true"><spring:message code="boolean.true"/></label></div>
<div><label><input type="checkbox" ${fn:contains(filters[filter.key], 'false')?'checked':''} class="filter-bool" i-key="<c:out value="${filter.key}" />" id="<c:out value="${normalizedKey}" />" value="false"><spring:message code="boolean.false"/></label></div>
<div><label><input type="checkbox" ${fn:contains(filters[filter.key], 'null')?'checked':''} class="filter-bool" i-key="<c:out value="${filter.key}" />" id="<c:out value="${normalizedKey}" />" value="null"><spring:message code="boolean.null"/></label></div>
<div><label><input type="checkbox" ${fn:contains(filters[appliedFilter.key], 'true')?'checked':''} class="filter-bool" i-key="<c:out value="${filter.key}" />" id="<c:out value="${normalizedKey}" />" value="true"><spring:message code="boolean.true"/></label></div>
<div><label><input type="checkbox" ${fn:contains(filters[appliedFilter.key], 'false')?'checked':''} class="filter-bool" i-key="<c:out value="${filter.key}" />" id="<c:out value="${normalizedKey}" />" value="false"><spring:message code="boolean.false"/></label></div>
<div><label><input type="checkbox" ${fn:contains(filters[appliedFilter.key], 'null')?'checked':''} class="filter-bool" i-key="<c:out value="${filter.key}" />" id="<c:out value="${normalizedKey}" />" value="null"><spring:message code="boolean.null"/></label></div>
</div>
</c:when>
<c:when test="${filter.key=='crops'}">
......@@ -184,7 +185,10 @@
</div>
<div class="col-lg-9">
<div class="filter-values" id="<c:out value="${normalizedKey}" />_value">
<c:forEach items="${filters[filter.key]}" var="value">
<c:if test="${appliedFilter.inverse}">
<spring:message code="filter.inverse" />
</c:if>
<c:forEach items="${filters[appliedFilter.key]}" var="value">
<c:set var="string" value="${value}"/>
<c:if test="${fn:contains(value, 'range')}">
<c:set var="string" value="${fn:replace(value,'{range=[','Between ')}"/>
......
......@@ -29,6 +29,7 @@
<c:forEach items="${currentFilters}" var="filter">
<c:set var="normalizedKey" value="${filter.key.replace('.', '-').replace(':', '_')}"/>
<c:set var="appliedFilter" value="${appliedFilters.get(filter.key)}" />
<div class="clearfix filter-block" id="<c:out value="${normalizedKey}" />_filter" norm-key="<c:out value="${normalizedKey}" />" i-key="<c:out value="${filter.key}" />">
<div class="col-lg-3 edit-fil">
......@@ -43,7 +44,10 @@
</div>
<div class="col-lg-9">
<div class="filter-values" id="<c:out value="${normalizedKey}" />_value">
<c:forEach items="${filters[filter.key]}" var="value">
<c:if test="${appliedFilter.inverse}">
<spring:message code="filter.inverse" />
</c:if>
<c:forEach items="${filters[appliedFilter.key]}" var="value">
<c:set var="string" value="${value}"/>
<c:if test="${fn:contains(value, 'range')}">
<c:set var="string" value="${fn:replace(value,'{range=[','Between ')}"/>
......
package org.genesys2.server.model.filters;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.genesys2.server.service.impl.FilterHandler.AppliedFilters;
import org.genesys2.server.test.PropertyPlacholderInitializer;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = InverseFiltersTest.Config.class, initializers = PropertyPlacholderInitializer.class)
@ActiveProfiles("dev")
public class InverseFiltersTest {
public static final Log LOG = LogFactory.getLog(InverseFiltersTest.class);
public static class Config {
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
}
@Autowired
private ObjectMapper objectMapper;
@Test
public void testDeserializeNeg() throws JsonParseException, JsonMappingException, IOException {
String source = "{\"key\":[1,{\"min\":2},\"Test\"],\"-key2\":[{\"max\":2.1},{\"range\":[-1,3.0]}],\"key3\":[{\"like\":\"Te\"},null]}";
AppliedFilters afs = objectMapper.readValue(source, AppliedFilters.class);
String result = objectMapper.writeValueAsString(afs);
LOG.info(result);
assertTrue(source.equals(result));
assertTrue("-key2 must be a negative filter", afs.get("key2").isInverse());
assertFalse("key must not be a negative filter", afs.get("key").isInverse());
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment