Last active
September 9, 2018 22:38
-
-
Save emersonmoura/f131ea12b947254d6b4a4029e844dd85 to your computer and use it in GitHub Desktop.
Search field Predicate creator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//############### 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