Commit 2edabb5b authored by Maxym Borodenko's avatar Maxym Borodenko

Introduce SuperModelFilter

parent 1fdb381d
/*
* Copyright 2019 Global Crop Diversity Trust
* Copyright 2020 Global Crop Diversity Trust
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
......@@ -15,41 +15,16 @@
*/
package org.genesys.blocks.model.filters;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import org.apache.commons.collections4.CollectionUtils;
import org.genesys.blocks.model.BasicModel;
import org.genesys.blocks.model.QBasicModel;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.querydsl.core.types.ExpressionUtils;
import com.querydsl.core.types.Predicate;
import com.querydsl.core.types.dsl.CollectionPathBase;
import com.querydsl.core.types.dsl.DslExpression;
import com.querydsl.core.types.dsl.EntityPathBase;
import com.querydsl.core.types.dsl.PathBuilder;
import com.querydsl.core.types.dsl.SimpleExpression;
/**
* {@link BasicModel} match by sample filters.
......@@ -57,45 +32,11 @@ import com.querydsl.core.types.dsl.SimpleExpression;
* @param <T> the generic type
* @param <R> the generic type
*/
public abstract class BasicModelFilter<T extends BasicModelFilter<T, R>, R extends BasicModel> {
private static final ObjectMapper jsonizer = new ObjectMapper();
private static final ObjectMapper nonDefault = new ObjectMapper();
private static final ObjectMapper defaultMapper = new ObjectMapper();
static {
// Any objectMapper configuration goes here
jsonizer.setSerializationInclusion(Include.NON_NULL);
nonDefault.setSerializationInclusion(Include.NON_DEFAULT);
defaultMapper.setSerializationInclusion(Include.USE_DEFAULTS);
}
public abstract class BasicModelFilter<T extends BasicModelFilter<T, R>, R extends BasicModel> extends SuperModelFilter<T, R> {
/** The id. */
public Set<Long> id;
/** The negative filters, but don't de-/serialize it's own NOT-properties. */
@JsonIgnoreProperties({ "NOT", "NULL", "NOTNULL" })
@JsonSerialize(using = NoDefaultValuesSerializer.class)
@JsonDeserialize(using = NonDefaultDeserializer.class)
public T NOT;
/** Names of properties to test with .isNull() */
public Set<String> NULL;
/** Names of properties to test with .isNotNull() */
public Set<String> NOTNULL;
public abstract List<Predicate> collectPredicates();
/**
* Builds the DSL predicate.
*
* @return the predicate
*/
public Predicate buildPredicate() {
return ExpressionUtils.allOf(collectPredicates());
}
/**
* Collects list of filter predicates
*
......@@ -104,154 +45,13 @@ public abstract class BasicModelFilter<T extends BasicModelFilter<T, R>, R exten
* @return list of predicates
*/
protected List<Predicate> collectPredicates(final EntityPathBase<R> instance, final QBasicModel basicModel) {
List<Predicate> predicates = new ArrayList<>();
List<Predicate> predicates = super.collectPredicates(instance);
if (CollectionUtils.isNotEmpty(id)) {
predicates.add(basicModel.id.in(id));
}
if (NULL != null && !NULL.isEmpty()) {
final Class<?> clazz = instance.getClass();
NULL.forEach(nullProp -> {
DslExpression<?> expression = getProperty(instance, clazz, nullProp);
if (expression instanceof SimpleExpression) {
predicates.add(((SimpleExpression<?>) expression).isNull());
} else if (expression instanceof CollectionPathBase) {
predicates.add(((CollectionPathBase<?, ?, ?>) expression).size().eq(0));
}
});
}
if (NOTNULL != null && !NOTNULL.isEmpty()) {
final Class<?> clazz = instance.getClass();
NOTNULL.forEach(notNullProp -> {
DslExpression<?> expression = getProperty(instance, clazz, notNullProp);
if (expression instanceof SimpleExpression) {
predicates.add(((SimpleExpression<?>) expression).isNotNull());
} else if (expression instanceof CollectionPathBase) {
predicates.add(((CollectionPathBase<?, ?, ?>) expression).size().gt(0));
}
});
}
if (NOT != null) {
predicates.add(ExpressionUtils.anyOf(NOT.collectPredicates()).not());
}
return predicates;
}
public void clearFilter(String jsonPath) throws NoSuchFieldException, IllegalAccessException {
this.clearFilter(jsonPath, true);
}
public void clearFilter(String jsonPath, boolean clearNullAndNotNull) throws NoSuchFieldException, IllegalAccessException {
String[] paths = jsonPath.split("\\.");
Object toClear = this;
Class<?> clazz = this.getClass();
Field field = clazz.getField(paths[0]);
for (int i = 1; i < paths.length; i++) {
clazz = field.getType();
if (!BasicModelFilter.class.isAssignableFrom(clazz))
break;
toClear = field.get(toClear);
if (toClear == null)
return;
field = clazz.getField(paths[i]);
}
if (clearNullAndNotNull) {
if (this.NULL != null) {
this.NULL.remove(jsonPath);
}
if (this.NOTNULL != null) {
this.NOTNULL.remove(jsonPath);
}
}
field.set(toClear, null);
}
/**
* Find the property of DSL-generated type.
*
* @param instance the DSL-generated type
* @param clazz type of instance
* @param nullProp property name
* @return
*/
private DslExpression<?> getProperty(final EntityPathBase<R> instance, final Class<?> clazz, final String nullProp) {
try {
// build path for nesting filters
if (nullProp.contains(".")) {
String paths[] = nullProp.split("\\.");
PathBuilder<?> pathBuilder = new PathBuilder<>(instance.getType(), instance.getMetadata());
Class<?> clazzToCheck = clazz;
for (String path : paths) {
Field field = clazzToCheck.getField(path);
if (CollectionPathBase.class.isAssignableFrom(field.getType())) {
pathBuilder.getSet(path, field.getDeclaringClass());
} else {
pathBuilder.getSimple(path, field.getDeclaringClass());
}
clazzToCheck = field.getType();
}
return pathBuilder.getSimple(nullProp, clazzToCheck);
} else {
final Field prop = clazz.getField(nullProp);
if (SimpleExpression.class.isAssignableFrom(prop.getType())) {
return (SimpleExpression<?>) prop.get(instance);
} if (CollectionPathBase.class.isAssignableFrom(prop.getType())) {
return(CollectionPathBase<?, ?, ?>) prop.get(instance);
} else {
throw new NoSuchFieldException("Property " + nullProp + " is not a SimpleExpression");
}
}
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
// test if nested objects have required property
// will test only the first level of nesting
if (e instanceof NoSuchFieldException) {
Field fields[] = clazz.getDeclaredFields();
for (Field field : fields) {
Class<?> superClazz = field.getType().getSuperclass();
if (superClazz != null && EntityPathBase.class.isAssignableFrom(superClazz)) {
try {
Field result = field.getType().getField(nullProp);
PathBuilder<?> pathBuilder = new PathBuilder<>(instance.getType(), instance.getMetadata());
if (CollectionPathBase.class.isAssignableFrom(result.getType())) {
return pathBuilder.getSet(field.getName() + "." + nullProp, result.getDeclaringClass());
}
return pathBuilder.getSimple(field.getName() + "." + nullProp, result.getDeclaringClass());
} catch (NoSuchFieldException e1) {
}
}
}
}
throw new RuntimeException("Error accessing field " + nullProp + " for isNull() in " + instance.getClass());
}
}
/**
* Copy by serializing to JSON and de-serializing to specified type.
*
* @param <X> the generic type
* @param targetType the target type
* @return the x
* @throws IOException Signals that an I/O exception has occurred.
*/
public <X> X copy(final Class<X> targetType) throws IOException {
return defaultMapper.readValue(defaultMapper.writeValueAsString(this), targetType);
}
@Override
public String toString() {
try {
return jsonizer.writeValueAsString(this);
} catch (final JsonProcessingException e) {
throw new RuntimeException("Could not serialize to JSON: " + e.getMessage(), e);
}
}
/**
* Id.
*
......@@ -264,107 +64,4 @@ public abstract class BasicModelFilter<T extends BasicModelFilter<T, R>, R exten
return id;
}
/**
* Checks if is null.
*
* @return the sets the
*/
public synchronized Set<String> isNull() {
if (NULL == null) {
NULL = new HashSet<>();
}
return NULL;
}
/**
* Not null.
*
* @return the sets the
*/
public synchronized Set<String> notNull() {
if (NOTNULL == null) {
NOTNULL = new HashSet<>();
}
return NOTNULL;
}
/**
* Prepare filter for use. NULL and NOTNULLs will clear any actual values
* provided for those properties.
*
* @param <Q> any BasicModelFilter subtype
* @param filter the filter
* @return
*/
public static <Q extends BasicModelFilter<?, ?>> Q normalize(final Q filter) {
Set<String> toClear = new HashSet<>();
if (filter.NULL != null) {
toClear.addAll(filter.NULL);
}
if (filter.NOTNULL != null) {
toClear.addAll(filter.NOTNULL);
}
for (String path : toClear) {
try {
filter.clearFilter(path, false);
} catch (NoSuchFieldException | IllegalAccessException e) {
System.err.println("Error while clearing filter: " + e.getMessage());
}
}
return filter;
}
/**
* Used to deserialize NOT filter without default values
*
* @param <Y> type of filter
*/
static class NonDefaultDeserializer<Y extends BasicModelFilter<?, ?>> extends JsonDeserializer<Y> implements ContextualDeserializer {
private Class<Y> targetClass;
@Override
public Y deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
Y parsed = (Y) p.getCodec().readValue(p, targetClass);
Y defaultFilter = null;
try {
defaultFilter = targetClass.newInstance();
for (Field f : targetClass.getDeclaredFields()) {
if (Modifier.isPublic(f.getModifiers()) && f.get(defaultFilter) != null && f.get(defaultFilter).equals(f.get(parsed))) {
f.set(parsed, null);
}
}
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException("Parsing of filter failed, e: " + e.getMessage());
}
return parsed;
}
@Override
public JsonDeserializer<?> createContextual(DeserializationContext deserializationContext, BeanProperty beanProperty) throws JsonMappingException {
final JavaType type;
if (beanProperty != null)
type = beanProperty.getType();
else {
type = deserializationContext.getContextualType();
}
this.targetClass = (Class<Y>) type.getRawClass();
return this;
}
}
/**
* Used to serialize and ignore default values of NOT filter
*
* @param <Y> type of filter
*/
static class NoDefaultValuesSerializer<Y extends BasicModelFilter<?, ?>> extends JsonSerializer<Y> {
@Override
public void serialize(Y value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeRawValue(BasicModelFilter.nonDefault.writeValueAsString(value));
}
}
}
/*
* Copyright 2020 Global Crop Diversity Trust
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.genesys.blocks.model.filters;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.querydsl.core.types.ExpressionUtils;
import com.querydsl.core.types.Predicate;
import com.querydsl.core.types.dsl.CollectionPathBase;
import com.querydsl.core.types.dsl.DslExpression;
import com.querydsl.core.types.dsl.EntityPathBase;
import com.querydsl.core.types.dsl.PathBuilder;
import com.querydsl.core.types.dsl.SimpleExpression;
/**
* @param <T> the generic type
* @param <R> the generic type
*/
public abstract class SuperModelFilter<T extends SuperModelFilter<T, R>, R> {
private static final ObjectMapper jsonizer = new ObjectMapper();
private static final ObjectMapper nonDefault = new ObjectMapper();
private static final ObjectMapper defaultMapper = new ObjectMapper();
static {
// Any objectMapper configuration goes here
jsonizer.setSerializationInclusion(JsonInclude.Include.NON_NULL);
nonDefault.setSerializationInclusion(JsonInclude.Include.NON_DEFAULT);
defaultMapper.setSerializationInclusion(JsonInclude.Include.USE_DEFAULTS);
}
/** The negative filters, but don't de-/serialize it's own NOT-properties. */
@JsonIgnoreProperties({ "NOT", "NULL", "NOTNULL" })
@JsonSerialize(using = SuperModelFilter.NoDefaultValuesSerializer.class)
@JsonDeserialize(using = SuperModelFilter.NonDefaultDeserializer.class)
public T NOT;
/** Names of properties to test with .isNull() */
public Set<String> NULL;
/** Names of properties to test with .isNotNull() */
public Set<String> NOTNULL;
public abstract List<Predicate> collectPredicates();
/**
* Builds the DSL predicate.
*
* @return the predicate
*/
public Predicate buildPredicate() {
return ExpressionUtils.allOf(collectPredicates());
}
/**
* Collects list of filter predicates
*
* @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 -> {
DslExpression<?> expression = getProperty(instance, clazz, nullProp);
if (expression instanceof SimpleExpression) {
predicates.add(((SimpleExpression<?>) expression).isNull());
} else if (expression instanceof CollectionPathBase) {
predicates.add(((CollectionPathBase<?, ?, ?>) expression).size().eq(0));
}
});
}
if (NOTNULL != null && !NOTNULL.isEmpty()) {
final Class<?> clazz = instance.getClass();
NOTNULL.forEach(notNullProp -> {
DslExpression<?> expression = getProperty(instance, clazz, notNullProp);
if (expression instanceof SimpleExpression) {
predicates.add(((SimpleExpression<?>) expression).isNotNull());
} else if (expression instanceof CollectionPathBase) {
predicates.add(((CollectionPathBase<?, ?, ?>) expression).size().gt(0));
}
});
}
if (NOT != null) {
predicates.add(ExpressionUtils.anyOf(NOT.collectPredicates()).not());
}
return predicates;
}
public void clearFilter(String jsonPath) throws NoSuchFieldException, IllegalAccessException {
this.clearFilter(jsonPath, true);
}
public void clearFilter(String jsonPath, boolean clearNullAndNotNull) throws NoSuchFieldException, IllegalAccessException {
String[] paths = jsonPath.split("\\.");
Object toClear = this;
Class<?> clazz = this.getClass();
Field field = clazz.getField(paths[0]);
for (int i = 1; i < paths.length; i++) {
clazz = field.getType();
if (!SuperModelFilter.class.isAssignableFrom(clazz))
break;
toClear = field.get(toClear);
if (toClear == null)
return;
field = clazz.getField(paths[i]);
}
if (clearNullAndNotNull) {
if (this.NULL != null) {
this.NULL.remove(jsonPath);
}
if (this.NOTNULL != null) {
this.NOTNULL.remove(jsonPath);
}
}
field.set(toClear, null);
}
/**
* Find the property of DSL-generated type.
*
* @param instance the DSL-generated type
* @param clazz type of instance
* @param nullProp property name
* @return
*/
private DslExpression<?> getProperty(final EntityPathBase<R> instance, final Class<?> clazz, final String nullProp) {
try {
// build path for nesting filters
if (nullProp.contains(".")) {
String paths[] = nullProp.split("\\.");
PathBuilder<?> pathBuilder = new PathBuilder<>(instance.getType(), instance.getMetadata());
Class<?> clazzToCheck = clazz;
for (String path : paths) {
Field field = clazzToCheck.getField(path);
if (CollectionPathBase.class.isAssignableFrom(field.getType())) {
pathBuilder.getSet(path, field.getDeclaringClass());
} else {
pathBuilder.getSimple(path, field.getDeclaringClass());
}
clazzToCheck = field.getType();
}
return pathBuilder.getSimple(nullProp, clazzToCheck);
} else {
final Field prop = clazz.getField(nullProp);
if (SimpleExpression.class.isAssignableFrom(prop.getType())) {
return (SimpleExpression<?>) prop.get(instance);
} if (CollectionPathBase.class.isAssignableFrom(prop.getType())) {
return(CollectionPathBase<?, ?, ?>) prop.get(instance);
} else {
throw new NoSuchFieldException("Property " + nullProp + " is not a SimpleExpression");
}
}
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
// test if nested objects have required property
// will test only the first level of nesting
if (e instanceof NoSuchFieldException) {
Field fields[] = clazz.getDeclaredFields();
for (Field field : fields) {
Class<?> superClazz = field.getType().getSuperclass();
if (superClazz != null && EntityPathBase.class.isAssignableFrom(superClazz)) {
try {
Field result = field.getType().getField(nullProp);
PathBuilder<?> pathBuilder = new PathBuilder<>(instance.getType(), instance.getMetadata());
if (CollectionPathBase.class.isAssignableFrom(result.getType())) {
return pathBuilder.getSet(field.getName() + "." + nullProp, result.getDeclaringClass());
}
return pathBuilder.getSimple(field.getName() + "." + nullProp, result.getDeclaringClass());
} catch (NoSuchFieldException e1) {
}
}
}
}
throw new RuntimeException("Error accessing field " + nullProp + " for isNull() in " + instance.getClass());
}
}
/**
* Copy by serializing to JSON and de-serializing to specified type.
*
* @param <X> the generic type
* @param targetType the target type
* @return the x
* @throws IOException Signals that an I/O exception has occurred.
*/
public <X> X copy(final Class<X> targetType) throws IOException {
return defaultMapper.readValue(defaultMapper.writeValueAsString(this), targetType);
}
@Override
public String toString() {
try {
return jsonizer.writeValueAsString(this);
} catch (final JsonProcessingException e) {
throw new RuntimeException("Could not serialize to JSON: " + e.getMessage(), e);
}
}
/**
* Checks if is null.
*
* @return the sets the
*/
public synchronized Set<String> isNull() {
if (NULL == null) {
NULL = new HashSet<>();
}
return NULL;
}
/**
* Not null.
*
* @return the sets the
*/
public synchronized Set<String> notNull() {
if (NOTNULL == null) {
NOTNULL = new HashSet<>();
}
return NOTNULL;
}
/**
* Prepare filter for use. NULL and NOTNULLs will clear any actual values
* provided for those properties.
*
* @param <Q> any SuperModelFilter subtype
* @param filter the filter
* @return
*/
public static <Q extends SuperModelFilter<?, ?>> Q normalize(final Q filter) {
Set<String> toClear = new HashSet<>();
if (filter.NULL != null) {
toClear.addAll(filter.NULL);
}
if (filter.NOTNULL != null) {
toClear.addAll(filter.NOTNULL);
}
for (String path : toClear) {
try {
filter.clearFilter(path, false);
} catch (NoSuchFieldException | IllegalAccessException e) {
System.err.println("Error while clearing filter: " + e.getMessage());
}
}
return filter;
}
/**
* Used to deserialize NOT filter without default values
*
* @param <Y> type of filter
*/
static class NonDefaultDeserializer<Y extends SuperModelFilter<?, ?>> extends JsonDeserializer<Y> implements ContextualDeserializer {
private Class<Y> targetClass;
@Override
public Y deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
Y parsed = (Y) p.getCodec().readValue(p, targetClass);
Y defaultFilter = null;
try {
defaultFilter = targetClass.newInstance();
for (Field f : targetClass.getDeclaredFields()) {
if (Modifier.isPublic(f.getModifiers()) && f.get(defaultFilter) != null && f.get(defaultFilter).equals(f.get(parsed))) {
f.set(parsed, null);
}
}