/addon-layers-repository-jpa/addon/src/main/java/org/springframework/roo/addon/layers/repository/jpa/addon/finder/parser/PartTree.java
Java | 517 lines | 235 code | 89 blank | 193 comment | 87 complexity | 6cc5b0fdaa20e02d88e9175c66e78581 MD5 | raw file
- 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;
- }
- }