OverviewHelper.java
/*
* 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.gringlobal.service.impl;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.ParameterizedType;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.genesys.blocks.model.EmptyModel;
import org.genesys.blocks.model.filters.EmptyModelFilter;
import org.gringlobal.api.exception.InvalidApiUsageException;
import org.gringlobal.service.filter.AccessionFilter;
import org.gringlobal.service.filter.IFullTextFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ResolvableType;
import org.springframework.stereotype.Component;
import com.querydsl.core.Tuple;
import com.querydsl.core.types.CollectionExpression;
import com.querydsl.core.types.ExpressionUtils;
import com.querydsl.core.types.Path;
import com.querydsl.core.types.Predicate;
import com.querydsl.core.types.dsl.BeanPath;
import com.querydsl.core.types.dsl.CollectionPathBase;
import com.querydsl.core.types.dsl.EntityPathBase;
import com.querydsl.core.types.dsl.NumberExpression;
import com.querydsl.core.types.dsl.PathBuilder;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.util.ReflectionUtils;
/**
* Utility to generate overview queries.
*
* @author Matija Obreza
*/
@Component
@Slf4j
public class OverviewHelper {
@Autowired
protected JPAQueryFactory jpaQueryFactory;
public <E extends EmptyModel, P extends EntityPathBase<E>> Map<Object, Number> getOverview(Class<E> entityClass, P qdslPath, NumberExpression<?> counter, String group,
EmptyModelFilter<?, E> filter) {
if (filter instanceof IFullTextFilter) {
if (((IFullTextFilter) filter).isFulltextQuery()) {
// ES filters not supported
return null;
}
}
JPAQuery<?> query = jpaQueryFactory.from(qdslPath);
PathBuilder<Object> groupBy;
// fields
String[] groups = group.strip().split("\\.");
Object root = qdslPath;
Object currentFilter = filter;
PathBuilder<?> sourceProp = new PathBuilder<>(entityClass, qdslPath.getMetadata());
String currentPath = "";
String lookupField = group;
for (int i = 0; i < groups.length - 1; i++) { // except the last path element
String fieldName = groups[i];
currentPath += fieldName + ".";
log.debug("Looking for {} currentPath={}", fieldName, currentPath);
try {
Class<?> propType;
Object propValue;
Field field;
field = ReflectionUtils.findField(root.getClass(), fieldName);
if (field != null) {
ReflectionUtils.makeAccessible(field);
log.debug("Found {} {}", field.getType(), field.getName());
propType = field.getType();
propValue = field.get(root);
} else {
log.debug("Field not found {} {}", root.getClass(), fieldName);
// Look for generator/accessor method: fieldName()
var accessor = ReflectionUtils.findMethod(root.getClass(), fieldName);
if (accessor == null) {
throw new NoSuchMethodException(fieldName);
}
ReflectionUtils.makeAccessible(accessor);
log.debug("Found {} {}()", accessor.getReturnType(), accessor.getName());
propType = accessor.getReturnType();
propValue = accessor.invoke(root);
}
if (BeanPath.class.isAssignableFrom(propType)) {
log.debug("{} is an entity", fieldName);
// No join, but change root
root = propValue;
// Must not change the lookup path!
} else if (CollectionPathBase.class.isAssignableFrom(propType) && field != null) {
// CollectionPathBase doesn't have an accessor method, so it must have a field
log.debug("{} is a collection type, must join", fieldName);
// Figure out what it is
var fieldType = field.getGenericType();
var typeArguments = ((ParameterizedType) fieldType).getActualTypeArguments();
final int entityTypeIndex = 0;
final int qdslTypeIndex = 1;
Class<?> entityType = (Class<?>) typeArguments[entityTypeIndex]; // entity type
Class<?> qdslType = (Class<?>) typeArguments[qdslTypeIndex]; // it's querydsl counterpart
log.debug("Got {} and {} for {}", entityType.getName(), qdslType.getName(), fieldName);
// Join
Path<?> path = (Path<?>) qdslType.getDeclaredConstructor(String.class).newInstance("joined" + i);
joinQuery(query, (CollectionExpression<?, ?>) propValue, path);
// Change root
root = path;
// Apply any filters on the join path, not on query root
// Is there a filter field for this entity?
Field pathFilterField = findPathFilter(fieldName, entityType);
// We found a matching filter, now apply filters on join path
if (pathFilterField != null) {
// If filter has values, skip otherwise
Object filterObj = pathFilterField.get(currentFilter);
if (filterObj != null) {
// We have an entity filter with some values
EmptyModelFilter<?, ?> emf = EmptyModelFilter.class.cast(filterObj);
log.debug("Found the filter for {} with values! {}", pathFilterField, emf.getClass().getName());
// Apply filter on path
java.lang.reflect.Method collectPredicates = emf.getClass().getMethod("collectPredicates", qdslType);
// Invoke the `collectPredicates(Q...)` method on join path
if (collectPredicates != null) {
Collection<Predicate> predicates = (Collection<Predicate>) collectPredicates.invoke(emf, path);
log.trace("Got {}", predicates);
query.where(ExpressionUtils.allOf(predicates));
// Clear accession filter for this property
pathFilterField.set(currentFilter, null);
} else {
log.warn("No collectPredicates method for {} on {}", qdslType.getName(), emf.getClass().getName());
}
} else {
log.trace("Found the filter for {} but it's null. {}", pathFilterField, entityType.getName());
}
}
// update lookup
sourceProp = new PathBuilder<>(entityType, "joined" + i);
log.debug("Changing lookup from {} to {}", lookupField, group.substring(currentPath.length()));
lookupField = group.substring(currentPath.length());
} else {
log.debug("{} is a regular path", fieldName);
// Must not change the lookup path!
// Should never reach here.
throw new InvalidApiUsageException("Invalid field '" + fieldName + "' in path " + group);
}
} catch (SecurityException | IllegalArgumentException | IllegalAccessException | InstantiationException | InvocationTargetException
| NoSuchMethodException e) {
log.error("Field '{}' not found in {} for path {}: {}", fieldName, root.getClass().getName(), group, e.getMessage(), e);
throw new InvalidApiUsageException("Field '" + fieldName + "' not found in " + root.getClass().getName() + " for path " + group, e);
}
}
log.debug("Using field {} for {}", lookupField, group);
groupBy = sourceProp.get(lookupField);
if (filter != null) {
query.where(filter.buildPredicate());
}
JPAQuery<Tuple> select = query.select(groupBy, counter).groupBy(groupBy);
Map<Object, Number> results = new HashMap<>();
select.fetch().forEach(t -> {
Object key = t.get(0, Object.class);
results.put(key == null ? "NULL" : key, t.get(1, Number.class));
});
return results;
}
/**
* Find the entity filter field of specified type in AccessionFilter
*/
private Field findPathFilter(String fieldName, Class<?> entityType) throws IllegalArgumentException {
for (Field filterField : AccessionFilter.class.getFields()) {
// Must be a model filter
if (EmptyModelFilter.class.isAssignableFrom(filterField.getType())) {
ResolvableType filterType = ResolvableType.forClass(EmptyModelFilter.class, filterField.getType());
Class<?> gen0 = filterType.resolveGeneric(0);
Class<?> gen1 = filterType.resolveGeneric(1);
log.debug("Looking at {} as subfilter {} {}", filterField, gen0, gen1);
// If the filter is for the specified type
if (gen1 != null && gen1.equals(entityType)) {
if (!filterField.getName().equalsIgnoreCase(fieldName)) {
log.warn("Potential filter mismatch between {} and {}!", filterField.getName(), fieldName);
// TODO We have multiple SiteFilters in AccessionFilter. Need the right one!
}
return filterField;
}
} else {
// LOG.trace("Not a sub-filter {} {}", filterField,
// filterField.getType().getName());
}
}
return null;
}
@SuppressWarnings("unchecked")
private <E> void joinQuery(JPAQuery<?> query, CollectionExpression<?, ?> collectionPath, Path<?> alias)
throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {
query.join((CollectionExpression<?, E>) collectionPath, (Path<E>) alias);
}
}