package org.springframework.roo.addon.layers.repository.jpa.addon.finder.parser;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.roo.classpath.details.FieldMetadata;
import org.springframework.roo.classpath.details.MemberHoldingTypeDetails;
import org.springframework.roo.classpath.scanner.MemberDetails;
import org.springframework.roo.model.DataType;
import org.springframework.roo.model.JavaType;
import org.springframework.roo.model.JpaJavaType;
/**
* This class is based on PartTree.java class from Spring Data commons project.
*
* It has some little changes to be able to work properly on Spring Roo project
* and make easy Spring Data query parser.
*
* Get more information about original class on:
*
* https://github.com/spring-projects/spring-data-commons/blob/master/src/main/java/org/springframework/data/repository/query/parser/PartTree.java
*
* Class to parse a {@link String} into a {@link Subject} and a {@link Predicate}.
* Takes a entity details to extract the
* properties of the domain class. The {@link PartTree} can then be used to
* build queries based on its API instead of parsing the method name for each
* query execution.
*
* @author Paula Navarro
* @author Juan Carlos GarcĂa
* @since 2.0
*/
public class PartTree {
private static final String KEYWORD_TEMPLATE = "(%s)(?=(\\p{Lu}|\\z))";
private static final Pattern PREFIX_TEMPLATE = Pattern.compile("^(" + Subject.QUERY_PATTERN + "|"
+ Subject.COUNT_PATTERN + ")((\\p{Lu}.*?))??By");
/**
* Subject is delimited by a prefix (find, read , query or count) and {@literal By} delimiter, for
* example "findDistinctUserByNameOrderByAge" would have the subject
* "DistinctUser".
*/
private final Subject subject;
/**
* Predicate contains conditions, and optionally order clause subject. E.g. "findDistinctUserByNameOrderByAge" would have
* the predicate "NameOrderByAge".
*/
private final Predicate predicate;
/**
* Query used to generate the Subject and Predicate
*/
private final String originalQuery;
/**
* Interface that provides operations to obtain useful information during finder autocomplete
*/
private final FinderAutocomplete finderAutocomplete;
/**
* Return type of generated finder
*/
private JavaType returnType;
/**
* Return type provided in constructor when it is different from target entity. Can be null.
*/
private JavaType providedReturnType;
/**
* Parameters of generated finder
*/
List<FinderParameter> finderParameters;
/**
* Creates a new {@link PartTree} by parsing the given {@link String}.
*
* @param source
* the {@link String} to parse
* @param memberDetails
* the member details of the entity class to extract the fields
* to expose them as options.
* @param finderAutocomplete interface that provides operations to obtain useful information during autocomplete
*/
public PartTree(String source, MemberDetails memberDetails,
FinderAutocomplete finderAutocomplete, JavaType providedReturnType) {
Validate.notNull(source, "Source must not be null");
Validate.notNull(memberDetails, "MemberDetails must not be null");
this.originalQuery = source;
this.finderAutocomplete = finderAutocomplete;
// Extracts entity fields removing persistence fields and list type
// fields
List<FieldMetadata> fields = getValidProperties(memberDetails.getFields());
Matcher matcher = PREFIX_TEMPLATE.matcher(source);
if (!matcher.find()) {
this.subject = new Subject(this, source, fields);
this.predicate = new Predicate(this, "", fields);
} else {
this.subject = new Subject(this, matcher.group(0), fields);
this.predicate = new Predicate(this, source.substring(matcher.group().length()), fields);
}
this.providedReturnType = providedReturnType;
this.returnType = extractReturnType(memberDetails);
this.finderParameters = predicate.getParameters();
}
public PartTree(String source, MemberDetails memberDetails, FinderAutocomplete finderAutocomplete) {
this(source, memberDetails, finderAutocomplete, null);
}
/**
* Extracts the java type of the results to be returned by the PartTree query
*
* @param entityDetails the entity details to extract the object to return by default
* @return
*/
private JavaType extractReturnType(MemberDetails entityDetails) {
Integer maxResults = subject.getMaxResults();
Pair<Stack<FieldMetadata>, String> property = subject.getProperty();
JavaType type = null;
// Count subject returns Long
if (subject.isCountProjection()) {
return JavaType.LONG_OBJECT;
}
if (property != null && property.getLeft() != null) {
// Returns the property type if it is specified
type = property.getLeft().peek().getFieldType();
} else if (providedReturnType != null) {
type = providedReturnType;
} else {
// By default returns entity type
List<MemberHoldingTypeDetails> details = entityDetails.getDetails();
for (MemberHoldingTypeDetails detail : details) {
if (finderAutocomplete != null
&& finderAutocomplete.getEntityDetails(detail.getType()).equals(entityDetails)) {
type = detail.getType();
break;
} else {
type = detail.getType();
}
}
}
// Check number of results to return.
if (maxResults != null && maxResults == 1) {
// Unique result
return type;
}
//If it is not an unique result, returns a list
if (type.isPrimitive()) {
// Lists cannot return primitive types, so primitive types are transformed into their wrapper class
type =
new JavaType(type.getFullyQualifiedTypeName(), type.getArray(), DataType.TYPE,
type.getArgName(), type.getParameters(), type.getModule());
}
return new JavaType("org.springframework.data.domain.Page", 0, DataType.TYPE, null,
Arrays.asList(type));
}
/**
* Creates a new {@link PartTree} by parsing the given {@link String}.
*
* @param source
* the {@link String} to parse
* @param memberDetails
* the member details of the entity class to extract the fields
* to expose them as options.
*/
public PartTree(String source, MemberDetails memberDetails) {
this(source, memberDetails, null);
}
/**
* Filters the entity properties that can be used to build Spring Data
* expressions.
*
* Persistence version property, multivalued properties, static fields and
* transient fields are excluded since Spring Data does not support
* operations with them.
*
* @param memberDetails
* @return entity properties which type is supported by SpringData
*/
private List<FieldMetadata> getValidProperties(List<FieldMetadata> fields) {
List<FieldMetadata> validProperties = new ArrayList<FieldMetadata>();
for (FieldMetadata field : fields) {
// Check if its type is List/Map/etc
if (field.getFieldType().isMultiValued()) {
continue;
}
// Check if it is annotated with @Version
if (field.getAnnotation(new JavaType("javax.persistence.Version")) != null) {
continue;
}
// Exclude static fields
int staticFinal = Modifier.STATIC + Modifier.FINAL;
int publicStatic = Modifier.PUBLIC + Modifier.STATIC;
int publicStaticFinal = Modifier.PUBLIC + Modifier.STATIC + Modifier.FINAL;
int privateStatic = Modifier.PRIVATE + Modifier.STATIC;
int privateStaticFinal = Modifier.PRIVATE + Modifier.STATIC + Modifier.FINAL;
if (field.getModifier() == Modifier.STATIC || field.getModifier() == staticFinal
|| field.getModifier() == publicStatic || field.getModifier() == publicStaticFinal
|| field.getModifier() == privateStatic || field.getModifier() == privateStaticFinal) {
continue;
}
// Exclude transient fields and the fields annotated with @Transient
int transientFinal = Modifier.TRANSIENT + Modifier.FINAL;
int publicTransient = Modifier.PUBLIC + Modifier.TRANSIENT;
int publicTransientFinal = Modifier.PUBLIC + Modifier.TRANSIENT + Modifier.FINAL;
int privateTransient = Modifier.PRIVATE + Modifier.TRANSIENT;
int privateTransientFinal = Modifier.PRIVATE + Modifier.TRANSIENT + Modifier.FINAL;
if (field.getAnnotation(JpaJavaType.TRANSIENT) != null
|| field.getModifier() == Modifier.TRANSIENT || field.getModifier() == transientFinal
|| field.getModifier() == publicTransient || field.getModifier() == publicTransientFinal
|| field.getModifier() == privateTransient
|| field.getModifier() == privateTransientFinal) {
continue;
}
validProperties.add(field);
}
return validProperties;
}
/**
* Filters the entity properties of a javaType that can be used to build Spring Data
* expressions. Persistence version field is excluded, and multivalued fields
* are removed since Spring Data does not supports operations with them.
*
* @param javaType
* @return entity properties which type is supported by SpringData
*/
public List<FieldMetadata> getValidProperties(JavaType javaType) {
if (finderAutocomplete != null) {
final MemberDetails entityDetails = finderAutocomplete.getEntityDetails(javaType);
if (entityDetails != null) {
return getValidProperties((List<FieldMetadata>) entityDetails.getFields());
}
}
return null;
}
/**
* Extract entity property name from raw property and returns the property metadata and the property name.
* If raw property references a property of a related entity, returns a Pair with the related entity property metadata and
* a string composed by the reference property name and the related entity property name.
* E.g. if raw property contains "petName" and current entity has a relation with Pet, it will return Pair(NameMetadata, "petName"))
*
* @param rawProperty the string that contains property name
* @param fields entity properties
* @return Pair that contains the path of property metadata and the property name.
*/
public Pair<Stack<FieldMetadata>, String> extractValidProperty(String rawProperty,
List<FieldMetadata> fields) {
return extractValidProperty(rawProperty, fields, null);
}
/**
* Extract entity property name from raw property and returns the property metadata and the property name.
* If raw property references a property of a related entity, returns a Pair with the related entity property metadata and
* a string composed by the reference property name and the related entity property name.
* E.g. if raw property contains "petName" and current entity has a relation with Pet, it will return Pair({pet, name}, "petName"))
*
* @param rawProperty the string that contains property name
* @param fields entity properties
* @param path of previous entities
* @return Pair that contains the path of property metadata and the property name.
*/
public Pair<Stack<FieldMetadata>, String> extractValidProperty(String rawProperty,
List<FieldMetadata> fields, Stack<FieldMetadata> path) {
if (path == null) {
path = new Stack<FieldMetadata>();
}
if (StringUtils.isBlank(rawProperty) || fields == null) {
return null;
}
FieldMetadata tempField = null;
rawProperty = StringUtils.uncapitalize(rawProperty);
// ExtractProperty can contain other information after property name. For that reason, it is necessary find the property that matches more letters with the property contained into extractProperty
for (FieldMetadata field : fields) {
if (field.getFieldName().toString().compareTo(rawProperty) == 0) {
tempField = field;
break;
}
if (rawProperty.startsWith(field.getFieldName().toString())) {
if (tempField == null
|| tempField.getFieldName().toString().length() < field.getFieldName().toString()
.length())
tempField = field;
}
}
if (tempField == null) {
return null;
}
path.push(tempField);
// If extracted property is a reference to other entity, the fields of this related entity are inspected to check if extractProperty contains information about them
Pair<Stack<FieldMetadata>, String> related =
extractRelatedEntityValidProperty(rawProperty, tempField, path);
if (related != null) {
return Pair.of(
related.getLeft() == null ? path : related.getLeft(),
StringUtils.capitalize(tempField.getFieldName().toString()).concat(
StringUtils.capitalize(related.getRight())));
}
return Pair.of(path, StringUtils.capitalize(tempField.getFieldName().toString()));
}
/**
* Gets the property of a related entity, using raw property information.
*
* @param rawProperty string that contains the definition of a property, which can be a property accessed by a relation.
* @param referenceProperty property that represents a relation with other entity.
* @return Pair that contains a property metadata and its name.
*/
private Pair<Stack<FieldMetadata>, String> extractRelatedEntityValidProperty(
String extractProperty, FieldMetadata referenceProperty, Stack<FieldMetadata> path) {
if (StringUtils.isBlank(extractProperty) || referenceProperty == null) {
return null;
}
// Extract the property of a related entity
String property =
StringUtils.substringAfter(extractProperty, referenceProperty.getFieldName().toString());
if (StringUtils.isBlank(property)) {
return null;
}
return extractValidProperty(property, getValidProperties(referenceProperty.getFieldType()),
path);
}
/**
* Returns the different queries that can be build based on the current defined query.
* First it lists the subject expressions that can be build. Once it is completed, it returns the queries available to
* define the predicate.
*
* @return
*/
public List<String> getOptions() {
if (!subject.isComplete()) {
return subject.getOptions();
} else if (!predicate.hasOrderClause()) {
return predicate.getOptions(subject.toString());
} else {
return predicate.getOrderOptions(subject.toString());
}
}
/**
* Returns whether we indicate distinct lookup of entities.
*
* @return {@literal true} if distinct
*/
public boolean isDistinct() {
return subject.isDistinct();
}
/**
* Returns whether a count projection shall be applied.
*
* @return
*/
public Boolean isCountProjection() {
return subject.isCountProjection();
}
@Override
public String toString() {
return subject.toString().concat(predicate.toString());
}
/**
* Splits the given text at the given keyword. Expects camel-case style to
* only match concrete keywords and not derivatives of it.
*
* @param text
* the text to split
* @param keyword
* the keyword to split around
* @param limit the limit controls the number of times the pattern is applied and therefore affects the length of the resulting array
* @return an array of split items
*/
static String[] split(String text, String keyword, int limit) {
Pattern pattern = Pattern.compile(String.format(KEYWORD_TEMPLATE, keyword));
return pattern.split(text, limit);
}
/**
* Returns true if PartTree query is well-defined and the query generated is the same that the one used to build its structure.
* @return
*/
public boolean isValid() {
return subject.isValid() && predicate.IsValid() && toString().equals(originalQuery);
}
/**
* Returns true if query is well-defined, which means that subject and predicate have a correct structure.
* However, it does not validate if entity properties exist.
* @return
*/
public static boolean isValid(String query) {
Matcher matcher = PREFIX_TEMPLATE.matcher(query);
if (!matcher.find()) {
return Subject.isValid(query) && Predicate.IsValid("");
} else {
return Subject.isValid(matcher.group(0))
&& Predicate.IsValid(query.substring(matcher.group().length()));
}
}
/**
* Returns the number of maximal results to return or {@literal null} if
* not restricted.
*
* @return
*/
public Integer getMaxResults() {
return subject.getMaxResults();
}
/**
* Returns true if the query matches with the given {@link Pattern}. Otherwise, returns false.
* If the query is null, returns false.
*
* @param query
* @param pattern
* @return
*/
public final static boolean matches(String query, Pattern pattern) {
return query == null ? false : pattern.matcher(query).find();
}
/**
* Method that obtains the return type of current finder
*
* @return JavaType with return type
*/
public JavaType getReturnType() {
return returnType;
}
/**
* Method that obtains the necessary parameters of current finder
*
* @return List that contains all necessary parameters
*/
public List<FinderParameter> getParameters() {
return finderParameters;
}
public String getOriginalQuery() {
return originalQuery;
}
}