Commit 49d03f6d authored by Matija Obreza's avatar Matija Obreza
Browse files

Filtering: Added AND and OR support

- Removed @deprecated NumberFilter#between
parent fa0d73ad
......@@ -46,14 +46,6 @@ public class NumberFilter<T extends Number & Comparable<?>> implements Serializa
/** Less than or equal. */
public T le;
/**
* Between is array of two values [a, b].
*
* @deprecated Use {@link #ge} and {@link #le}
*/
@Deprecated
public T[] between;
/**
* Instantiates a new number filter.
*/
......@@ -99,10 +91,6 @@ public class NumberFilter<T extends Number & Comparable<?>> implements Serializa
and.and(val.isNotNull());
and.and(val.loe(le));
}
if (between != null && between.length == 2) {
and.and(val.isNotNull());
and.and(val.between(between[0], between[1]));
}
return and;
}
......
......@@ -28,6 +28,8 @@ import java.util.Set;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonProperty.Access;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
......@@ -81,6 +83,15 @@ public abstract class SuperModelFilter<T extends SuperModelFilter<T, R>, R> impl
/** Names of properties to test with .isNotNull() */
public Set<String> NOTNULL;
/** The AND filters, but not serialized. */
@JsonProperty(access = Access.READ_ONLY)
public T AND;
/** The OR filters, but not serialized. */
@JsonProperty(access = Access.READ_ONLY)
public T OR;
public abstract List<Predicate> collectPredicates();
/**
......@@ -104,19 +115,34 @@ public abstract class SuperModelFilter<T extends SuperModelFilter<T, R>, R> impl
*
* @return the predicate
*/
public Predicate buildPredicate() {
Predicate collected = ExpressionUtils.allOf(collectPredicates());
return collected == null ? new BooleanBuilder() : collected;
public BooleanBuilder buildPredicate() {
var builder = new BooleanBuilder(ExpressionUtils.allOf(collectPredicates()));
if (NOT != null) {
// This is not a regular NOT operation where not(A and B) = not(A) or not(B)
// Our filtering uses: not(A and B) = not(A) and not(B)
builder.and(ExpressionUtils.anyOf(NOT.collectPredicates()).not());
}
if (AND != null) {
builder.and(AND.buildPredicate());
}
if (OR != null) {
builder.or(OR.buildPredicate());
}
return builder;
}
/**
* Collects list of filter predicates
* Collects list of this filter predicates. Does not include NOT, OR and AND. Those are used in {@link #buildPredicate()}!
*
* @param instance the instance of Q-type of <em>R</em>
* @return list of predicates
*/
protected List<Predicate> collectPredicates(final EntityPathBase<R> instance) {
List<Predicate> predicates = new ArrayList<>();
if (NULL != null && !NULL.isEmpty()) {
final Class<?> clazz = instance.getClass();
NULL.forEach(nullProp -> {
......@@ -128,6 +154,7 @@ public abstract class SuperModelFilter<T extends SuperModelFilter<T, R>, R> impl
}
});
}
if (NOTNULL != null && !NOTNULL.isEmpty()) {
final Class<?> clazz = instance.getClass();
NOTNULL.forEach(notNullProp -> {
......@@ -139,9 +166,7 @@ public abstract class SuperModelFilter<T extends SuperModelFilter<T, R>, R> impl
}
});
}
if (NOT != null) {
predicates.add(ExpressionUtils.anyOf(NOT.collectPredicates()).not());
}
return predicates;
}
......@@ -292,11 +317,11 @@ public abstract class SuperModelFilter<T extends SuperModelFilter<T, R>, R> impl
* Prepare filter for use. NULL and NOTNULLs will clear any actual values
* provided for those properties.
*
* @param <Q> any SuperModelFilter subtype
* @param <Y> any SuperModelFilter subtype
* @param filter the filter
* @return the normalized valid filter
*/
public static <Q extends SuperModelFilter<?, ?>> Q normalize(final Q filter) {
public static <Y extends SuperModelFilter<Y, ?>> Y normalize(final Y filter) {
Set<String> toClear = new HashSet<>();
if (filter.NULL != null) {
toClear.addAll(filter.NULL);
......@@ -312,6 +337,15 @@ public abstract class SuperModelFilter<T extends SuperModelFilter<T, R>, R> impl
System.err.println("Error while clearing filter: " + e.getMessage());
}
}
if (filter.AND != null) {
filter.AND = normalize(filter.AND);
}
if (filter.OR != null) {
filter.OR = normalize(filter.OR);
}
return filter;
}
......@@ -320,7 +354,7 @@ public abstract class SuperModelFilter<T extends SuperModelFilter<T, R>, R> impl
*
* @param <Y> type of filter
*/
static class NonDefaultDeserializer<Y extends SuperModelFilter<?, ?>> extends JsonDeserializer<Y> implements ContextualDeserializer {
static class NonDefaultDeserializer<Y extends SuperModelFilter<Y, ?>> extends JsonDeserializer<Y> implements ContextualDeserializer {
private Class<Y> targetClass;
......@@ -361,7 +395,7 @@ public abstract class SuperModelFilter<T extends SuperModelFilter<T, R>, R> impl
*
* @param <Y> type of filter
*/
static class NoDefaultValuesSerializer<Y extends SuperModelFilter<?, ?>> extends JsonSerializer<Y> {
static class NoDefaultValuesSerializer<Y extends SuperModelFilter<Y, ?>> extends JsonSerializer<Y> {
@Override
public void serialize(Y value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeRawValue(SuperModelFilter.nonDefault.writeValueAsString(value));
......
......@@ -15,9 +15,12 @@
*/
package org.genesys.blocks.model;
import java.util.Date;
import java.util.Set;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import java.util.Date;
@Entity
public class TestingEntity extends BasicModel {
......@@ -29,6 +32,9 @@ public class TestingEntity extends BasicModel {
@Column
private Integer number;
@ElementCollection
private Set<String> tags;
public Date getDate() {
return date;
}
......@@ -52,4 +58,12 @@ public class TestingEntity extends BasicModel {
public void setNumber(Integer number) {
this.number = number;
}
public Set<String> getTags() {
return tags;
}
public void setTags(Set<String> tags) {
this.tags = tags;
}
}
......@@ -16,18 +16,24 @@
package org.genesys.blocks.model;
import java.util.List;
import java.util.Set;
import com.querydsl.core.types.Predicate;
import org.apache.commons.collections4.CollectionUtils;
import org.genesys.blocks.model.filters.BasicModelFilter;
import org.genesys.blocks.model.filters.DateFilter;
import org.genesys.blocks.model.filters.NumberFilter;
import com.querydsl.core.types.Predicate;
public class TestingEntityFilter extends BasicModelFilter<TestingEntityFilter, TestingEntity> {
private static final long serialVersionUID = 1L;
public DateFilter date;
public NumberFilter<Integer> number;
public Set<String> tags;
@Override
public List<Predicate> collectPredicates() {
......@@ -43,7 +49,17 @@ public class TestingEntityFilter extends BasicModelFilter<TestingEntityFilter, T
if (number != null) {
predicates.add(number.buildQuery(testingEntity.number));
}
if (CollectionUtils.isNotEmpty(tags)) {
predicates.add(testingEntity.tags.any().in(tags));
}
return predicates;
}
public NumberFilter<Integer> number() {
if (this.number == null) {
this.number = new NumberFilter<>();
}
return this.number;
}
}
......@@ -17,6 +17,8 @@ package org.genesys.blocks.model;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.ManyToOne;
/**
* The Class VersionedEntity.
......@@ -29,6 +31,9 @@ public class VersionedEntity extends VersionedModel {
@Column(nullable = false)
private String name;
@ManyToOne(cascade = {}, optional = true, fetch = FetchType.EAGER)
private TestingEntity testing;
/**
* Gets the name.
*
......@@ -46,4 +51,12 @@ public class VersionedEntity extends VersionedModel {
public void setName(String name) {
this.name = name;
}
public TestingEntity getTesting() {
return testing;
}
public void setTesting(TestingEntity testing) {
this.testing = testing;
}
}
......@@ -24,19 +24,27 @@ import com.querydsl.core.types.Predicate;
public class VersionedEntityFilter extends VersionedModelFilter<VersionedEntityFilter, VersionedEntity> {
private static final long serialVersionUID = 1L;
public StringFilter name;
public TestingEntityFilter testing;
public List<Predicate> collectPredicates() {
return collectPredicates(QVersionedEntity.versionedEntity);
}
public List<Predicate> collectPredicates(QVersionedEntity versionedEntity) {
final List<Predicate> predicates = super.collectPredicates(versionedEntity, versionedEntity._super);
var builder = super.collectPredicates(versionedEntity, versionedEntity._super);
if (name != null) {
predicates.add(name.buildQuery(versionedEntity.name));
builder.add(name.buildQuery(versionedEntity.name));
}
if (testing != null) {
builder.addAll(testing.collectPredicates(versionedEntity.testing));
}
return predicates;
return builder;
}
}
......@@ -19,6 +19,7 @@ import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.*;
import org.genesys.blocks.config.ApplicationConfig;
import org.genesys.blocks.persistence.TestingEntityRepository;
import org.genesys.blocks.persistence.VersionedEntityRepository;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
......@@ -48,6 +49,9 @@ public abstract class BaseTest {
@Autowired
protected VersionedEntityRepository versionedEntityRepository;
@Autowired
protected TestingEntityRepository testingEntityRepository;;
/**
* Cleanup.
*/
......@@ -57,5 +61,7 @@ public abstract class BaseTest {
LOG.trace("Deleting all from ...");
versionedEntityRepository.deleteAll();
assertThat(versionedEntityRepository.count(), is(0L));
testingEntityRepository.deleteAll();
assertThat(testingEntityRepository.count(), is(0L));
}
}
\ No newline at end of file
......@@ -15,11 +15,12 @@
*/
package org.genesys.blocks.tests.model;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import java.util.Calendar;
import java.util.Date;
import java.util.Set;
import org.genesys.blocks.model.TestingEntity;
import org.genesys.blocks.model.TestingEntityFilter;
......@@ -130,4 +131,128 @@ public class TestingEntityTest extends ServiceTest {
assertThat(page.getTotalElements(), is(2L));
}
@Test
public void testFiltering() throws Exception {
var entity1 = new TestingEntity(new Date(), 3);
entity1.setTags(Set.of("tag1", "tag2", "tag3"));
entity1 = repository.save(entity1);
var filter = new TestingEntityFilter();
filter.tags = Set.of("tag1", "tag5");
var page = repository.findAll(filter.buildPredicate(), PageRequest.of(0, 5));
assertThat(page.getTotalElements(), is(1L));
assertThat(page.getContent(), containsInAnyOrder(entity1));
var entity2 = new TestingEntity(new Date(), 30);
entity2.setTags(Set.of("tag1", "tag4", "tag5"));
entity2 = repository.save(entity2);
page = repository.findAll(filter.buildPredicate(), PageRequest.of(0, 5));
assertThat(page.getTotalElements(), is(2L));
assertThat(page.getContent(), containsInAnyOrder(entity1, entity2));
filter.AND = new TestingEntityFilter();
filter.AND.number().gt = 10;
page = repository.findAll(filter.buildPredicate(), PageRequest.of(0, 5));
assertThat(page.getTotalElements(), is(1L));
assertThat(page.getContent(), containsInAnyOrder(entity2));
filter.OR = new TestingEntityFilter();
filter.OR.number().lt = 10;
page = repository.findAll(filter.buildPredicate(), PageRequest.of(0, 5));
assertThat(page.getTotalElements(), is(2L));
assertThat(page.getContent(), containsInAnyOrder(entity1, entity2));
// Proper OR
filter = new TestingEntityFilter();
filter.tags = Set.of("tag1");
filter.number().ge = 1;
filter.number().le = 10;
page = repository.findAll(filter.buildPredicate(), PageRequest.of(0, 5));
assertThat(page.getContent(), containsInAnyOrder(entity1));
var filter2 = new TestingEntityFilter();
filter2.tags = Set.of("tag4");
filter2.number().ge = 20;
filter2.number().le = 100;
page = repository.findAll(filter2.buildPredicate(), PageRequest.of(0, 5));
assertThat(page.getContent(), containsInAnyOrder(entity2));
filter2.OR = filter;
page = repository.findAll(filter2.buildPredicate(), PageRequest.of(0, 5));
assertThat(page.getContent(), containsInAnyOrder(entity2, entity1));
// NOT
filter = new TestingEntityFilter();
filter.tags = Set.of("tag1");
filter.NOT = new TestingEntityFilter();
filter.NOT.number().eq = Set.of(3);
page = repository.findAll(filter.buildPredicate(), PageRequest.of(0, 5));
assertThat(page.getContent(), containsInAnyOrder(entity2));
filter.NOT.tags = Set.of("tag5");
page = repository.findAll(filter.buildPredicate(), PageRequest.of(0, 5));
assertThat(page.getContent(), hasSize(0));
}
@Test
public void testUnionIntersectionWithoutFilters() throws Exception {
var entity1 = new TestingEntity(new Date(), 3);
entity1.setTags(Set.of("tag1", "tag2", "tag3"));
entity1 = repository.save(entity1);
var entity2 = new TestingEntity(new Date(), 11);
entity2.setTags(Set.of("tag1", "tag4", "tag5"));
entity2 = repository.save(entity2);
var entity3 = new TestingEntity(new Date(), 17);
entity3.setTags(Set.of("tag2", "tag4", "tag7"));
entity3 = repository.save(entity3);
// On numbers
var filter1and2 = new TestingEntityFilter();
filter1and2.number().le = 11;
var page = repository.findAll(filter1and2.buildPredicate(), PageRequest.of(0, 5));
assertThat(page.getContent(), containsInAnyOrder(entity1, entity2));
var filter2and3 = new TestingEntityFilter();
filter2and3.number().ge = 10;
page = repository.findAll(filter2and3.buildPredicate(), PageRequest.of(0, 5));
assertThat(page.getContent(), containsInAnyOrder(entity2, entity3));
// Union
var filterUnion = filter1and2.copy(TestingEntityFilter.class);
filterUnion.OR = filter2and3;
page = repository.findAll(filterUnion.buildPredicate(), PageRequest.of(0, 5));
assertThat(page.getContent(), containsInAnyOrder(entity1, entity2, entity3));
// Union transitive
var filterUnionT = filter2and3.copy(TestingEntityFilter.class);
filterUnionT.OR = filter1and2;
page = repository.findAll(filterUnionT.buildPredicate(), PageRequest.of(0, 5));
assertThat(page.getContent(), containsInAnyOrder(entity1, entity2, entity3));
// Intersection
var filterIntersection = filter1and2.copy(TestingEntityFilter.class);
filterIntersection.AND = filter2and3;
page = repository.findAll(filterIntersection.buildPredicate(), PageRequest.of(0, 5));
assertThat(page.getContent(), containsInAnyOrder(entity2));
// Intersection transitive
var filterIntersectionT = filter2and3.copy(TestingEntityFilter.class);
filterIntersectionT.AND = filter1and2;
page = repository.findAll(filterIntersectionT.buildPredicate(), PageRequest.of(0, 5));
assertThat(page.getContent(), containsInAnyOrder(entity2));
// Without
var filterWithout = filter1and2.copy(TestingEntityFilter.class);
filterWithout.NOT = filter2and3;
page = repository.findAll(filterWithout.buildPredicate(), PageRequest.of(0, 5));
assertThat(page.getContent(), containsInAnyOrder(entity1));
// Without transitive
var filterWithoutT = filter2and3.copy(TestingEntityFilter.class);
filterWithoutT.NOT = filter1and2;
page = repository.findAll(filterWithoutT.buildPredicate(), PageRequest.of(0, 5));
assertThat(page.getContent(), containsInAnyOrder(entity3));
}
}
......@@ -15,11 +15,21 @@
*/
package org.genesys.blocks.tests.model;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import java.util.Date;
import org.genesys.blocks.model.TestingEntity;
import org.genesys.blocks.model.TestingEntityFilter;
import org.genesys.blocks.model.VersionedEntity;
import org.genesys.blocks.model.VersionedEntityFilter;
import org.genesys.blocks.model.filters.NumberFilter;
import org.genesys.blocks.model.filters.StringFilter;
import org.genesys.blocks.tests.ServiceTest;
import org.junit.Test;
......@@ -238,4 +248,66 @@ public class VersionedModelTest extends ServiceTest {
return entity;
}
@Test
public void testFiltering() throws Exception {
var entity = versionedEntityRepository.save(createEntity(ENTITY_NAME));
var entity2 = createEntity(ENTITY_NAME2);
entity2.setActive(false);
var testing = testingEntityRepository.save(new TestingEntity(new Date(), 7));
entity2.setTesting(testing);
entity2 = versionedEntityRepository.save(entity2);
var filter = new VersionedEntityFilter();
var page = versionedEntityRepository.findAll(filter.buildPredicate(), PageRequest.of(0, 10));
assertThat(page.getContent(), hasSize(2));
assertThat(page.getContent(), containsInAnyOrder(entity, entity2));
filter.name = new StringFilter().eq(ENTITY_NAME, ENTITY_NAME2);
page = versionedEntityRepository.findAll(filter.buildPredicate(), PageRequest.of(0, 10));
assertThat(page.getContent(), hasSize(2));
var filterAnd = new VersionedEntityFilter();
filterAnd.active = true;
filter.AND = filterAnd;
page = versionedEntityRepository.findAll(filter.buildPredicate(), PageRequest.of(0, 10));
assertThat(page.getContent(), hasSize(1));
assertThat(page.getContent(), containsInAnyOrder(entity));
filter.AND = null;
filter.OR = filterAnd;
page = versionedEntityRepository.findAll(filter.buildPredicate(), PageRequest.of(0, 10));
assertThat(page.getContent(), hasSize(2));
// Restart
filter = new VersionedEntityFilter();
filter.name = new StringFilter().sw(ENTITY_NAME);
page = versionedEntityRepository.findAll(filter.buildPredicate(), PageRequest.of(0, 10));
assertThat(page.getContent(), hasSize(2));
filter.NOT = new VersionedEntityFilter();
filter.NOT.active = true;
page = versionedEntityRepository.findAll(filter.buildPredicate(), PageRequest.of(0, 10));
assertThat(page.getContent(), hasSize(1));
assertThat(page.getContent(), containsInAnyOrder(entity2));
filter.NOT.testing = new TestingEntityFilter();
filter.NOT.testing.number = new NumberFilter<Integer>(6, 8);
page = versionedEntityRepository.findAll(filter.buildPredicate(), PageRequest.of(0, 10));
assertThat(page.getContent(), hasSize(0));
filter.AND = new VersionedEntityFilter();
filter.AND.testing = new TestingEntityFilter();
filter.AND.testing.number = new NumberFilter<Integer>(6, 8);
page = versionedEntityRepository.findAll(filter.buildPredicate(), PageRequest.of(0, 10));
assertThat(page.getContent(), hasSize(0));
filter.NOT.testing = null;
page = versionedEntityRepository.findAll(filter.buildPredicate(), PageRequest.of(0, 10));
assertThat(page.getContent(), hasSize(1));
assertThat(page.getContent(), containsInAnyOrder(entity2));
}
}
Supports Markdown
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