Skip to content

Instantly share code, notes, and snippets.

@emersonmoura
Last active September 9, 2018 22:38
Show Gist options
  • Save emersonmoura/f131ea12b947254d6b4a4029e844dd85 to your computer and use it in GitHub Desktop.
Save emersonmoura/f131ea12b947254d6b4a4029e844dd85 to your computer and use it in GitHub Desktop.
Search field Predicate creator
//############### This is a generic filter to use with Spring data framework.
//############### It possibilities only one abstraction to any user filter in your system
public class Filter<T> implements Specification<T> {
private static final Logger LOGGER = LoggerFactory.getLogger(Filter.class);
private Class<?> searchableType;
private Object fields;
private CriteriaBuilder cb;
private Root<T> root;
private static final int JOIN_LEVEL_ONE_SIZE = 2;
private static final int JOIN_LEVEL_TWO_SIZE = 3;
private static final int JOIN_LEVEL_TREE_SIZE = 4;
private static final int SOURCE_FIELD_INDEX = 0;
private static final int FIRST_JOIN_FIELD_INDEX = 1;
private static final int SECOND_JOIN_FIELD_INDEX = 2;
private static final int TREE_JOIN_FIELD_INDEX = 3;
private static final int MINIMUM_JOIN_SIZE = 2;
public Filter(Object fields){
this.searchableType = fields.getClass();
this.fields = fields;
}
private Filter(CriteriaBuilder cb, Root<T> root){
this.cb = cb;
this.root = root;
}
@Override
public Predicate toPredicate(Root<T> rootParam, CriteriaQuery<?> query, CriteriaBuilder cbParam) {
return new Filter<>(cbParam, rootParam).createValidPredicate(searchableFieldsToMap());
}
private Map<String, Object> searchableFieldsToMap() {
return Stream.of(searchableType.getDeclaredFields())
.filter(field -> field.isAnnotationPresent(SearchableField.class))
.filter(field -> Objects.nonNull(getFieldValue(field)))
.collect(Collectors.toMap(this::getFieldName, this::getFieldValue));
}
private <T> Predicate createValidPredicate(Map<String, Object> simpleFields) {
List<Predicate> predicates = simpleFields.entrySet().stream()
.filter(entry -> Objects.nonNull(entry.getValue()))
.map(this::createPredicate)
.collect(Collectors.toList());
return cb.and(predicates.toArray(new Predicate[predicates.size()]));
}
private Predicate createPredicate(Map.Entry<String, Object> pair){
String[] split = pair.getKey().split("\\.");
if(split.length >= MINIMUM_JOIN_SIZE){
return createJoinPredicate(pair,split);
}
return addOperator(root.get(pair.getKey()), pair.getValue());
}
private <T> Predicate createJoinPredicate(Map.Entry<String, Object> pair, String... fields) {
if(fields.length == JOIN_LEVEL_ONE_SIZE){
Join<T, Object> join = root.join(fields[SOURCE_FIELD_INDEX]);
return addOperator(join.get(fields[FIRST_JOIN_FIELD_INDEX]), pair.getValue());
}
if(fields.length == JOIN_LEVEL_TWO_SIZE){
Join<T, Object> firstJoin = root.join(fields[SOURCE_FIELD_INDEX]);
Join<T, Object> secondJoin = firstJoin.join(fields[FIRST_JOIN_FIELD_INDEX]);
return addOperator(secondJoin.get(fields[SECOND_JOIN_FIELD_INDEX]), pair.getValue());
}
if(fields.length == JOIN_LEVEL_TREE_SIZE){
Join<T, Object> firstJoin = root.join(fields[SOURCE_FIELD_INDEX]);
Join<T, Object> secondJoin = firstJoin.join(fields[FIRST_JOIN_FIELD_INDEX]);
Join<T, Object> treeJoin = secondJoin.join(fields[SECOND_JOIN_FIELD_INDEX]);
return addOperator(treeJoin.get(fields[TREE_JOIN_FIELD_INDEX]), pair.getValue());
}
throw new UnprocessableEntityException(String.format("Invalid filter join length: %s",fields.length));
}
private Predicate addOperator(Path key, Object value) {
if(value instanceof Period){
return createPeriodBetween(key, value);
}
if(value instanceof Enum || value instanceof Number){
return cb.equal(key, value);
}
if(value instanceof Collection && key instanceof PluralAttributePath){
return cb.isMember(value,key);
}
if(value instanceof Collection && key instanceof SingularAttributePath){
List<Predicate> predicates = ((Collection<Object>) value).stream()
.map(v -> cb.equal(key, v)).collect(Collectors.toList());
return cb.or(predicates.toArray(new Predicate[] {}));
}
return cb.like(cb.lower(key), ("%" + value + "%").toLowerCase());
}
@SneakyThrows
private <T> Predicate createPeriodBetween(Path key, Object value){
Period period = (Period) value;
if(period.getBegin() !=null && period.getEnd() == null)
return cb.greaterThanOrEqualTo(key,period.getBegin());
if(period.getEnd() !=null && period.getBegin() == null)
return cb.lessThanOrEqualTo(key,period.getEnd());
return cb.between(key,period.getBegin(),period.getEnd());
}
private String getFieldName(Field field){
Annotation annotation = field.getAnnotation(SearchableField.class);
SearchableField searchableField = (SearchableField) annotation;
return Strings.isNullOrEmpty(searchableField.field()) ? field.getName() : searchableField.field();
}
private Object getFieldValue(Field field){
try {
field.setAccessible(true);
return field.get(fields);
} catch (IllegalAccessException e) {
LOGGER.warn("could not get field value", e);
return null;
}
}
}
//############### To use, you only need to implements this interface
public interface GenericJpaSpecificationExecutor<T, F> extends JpaSpecificationExecutor {
default Page<T> findAll(F filter, Pageable pageable){
return findAll(new Filter<T>(filter), pageable);
}
default List<T> findAll(F spec){
return findAll(new Filter<T>(spec));
}
}
//############# after, you must use this annotations in a DTO object to receive informations in your controller
@Data
public class UserFilter {
@SearchableField
private String name;
@SearchableField
private String email;
@SearchableField(field = "groups.name")
private String groupName;
}
//############ This is only one test sample
def 'should return contracts by name using ignore case'() {
given:
Fixture.from(Contract.class).uses(jpaProcessor).gimme(3,"withReferences", new Rule(){{
add("name", uniqueRandom("JoSe", "fernanda", "joao"))
}})
def filter = new ContractFilter()
filter.with { name = findName }
when:
def result = repository.findAll(filter)
then:
that result, hasSize(1)
where:
findName | _
'JOse' | _
'jose' | _
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment