PageRenderTime 39ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 1ms

/addon-layers-repository-jpa/addon/src/main/java/org/springframework/roo/addon/layers/repository/jpa/addon/finder/parser/PartTree.java

http://github.com/SpringSource/spring-roo
Java | 517 lines | 235 code | 89 blank | 193 comment | 87 complexity | 6cc5b0fdaa20e02d88e9175c66e78581 MD5 | raw file
  1. package org.springframework.roo.addon.layers.repository.jpa.addon.finder.parser;
  2. import java.lang.reflect.Modifier;
  3. import java.util.ArrayList;
  4. import java.util.Arrays;
  5. import java.util.List;
  6. import java.util.Stack;
  7. import java.util.regex.Matcher;
  8. import java.util.regex.Pattern;
  9. import org.apache.commons.lang3.StringUtils;
  10. import org.apache.commons.lang3.Validate;
  11. import org.apache.commons.lang3.tuple.Pair;
  12. import org.springframework.roo.classpath.details.FieldMetadata;
  13. import org.springframework.roo.classpath.details.MemberHoldingTypeDetails;
  14. import org.springframework.roo.classpath.scanner.MemberDetails;
  15. import org.springframework.roo.model.DataType;
  16. import org.springframework.roo.model.JavaType;
  17. import org.springframework.roo.model.JpaJavaType;
  18. /**
  19. * This class is based on PartTree.java class from Spring Data commons project.
  20. *
  21. * It has some little changes to be able to work properly on Spring Roo project
  22. * and make easy Spring Data query parser.
  23. *
  24. * Get more information about original class on:
  25. *
  26. * https://github.com/spring-projects/spring-data-commons/blob/master/src/main/java/org/springframework/data/repository/query/parser/PartTree.java
  27. *
  28. * Class to parse a {@link String} into a {@link Subject} and a {@link Predicate}.
  29. * Takes a entity details to extract the
  30. * properties of the domain class. The {@link PartTree} can then be used to
  31. * build queries based on its API instead of parsing the method name for each
  32. * query execution.
  33. *
  34. * @author Paula Navarro
  35. * @author Juan Carlos GarcĂ­a
  36. * @since 2.0
  37. */
  38. public class PartTree {
  39. private static final String KEYWORD_TEMPLATE = "(%s)(?=(\\p{Lu}|\\z))";
  40. private static final Pattern PREFIX_TEMPLATE = Pattern.compile("^(" + Subject.QUERY_PATTERN + "|"
  41. + Subject.COUNT_PATTERN + ")((\\p{Lu}.*?))??By");
  42. /**
  43. * Subject is delimited by a prefix (find, read , query or count) and {@literal By} delimiter, for
  44. * example "findDistinctUserByNameOrderByAge" would have the subject
  45. * "DistinctUser".
  46. */
  47. private final Subject subject;
  48. /**
  49. * Predicate contains conditions, and optionally order clause subject. E.g. "findDistinctUserByNameOrderByAge" would have
  50. * the predicate "NameOrderByAge".
  51. */
  52. private final Predicate predicate;
  53. /**
  54. * Query used to generate the Subject and Predicate
  55. */
  56. private final String originalQuery;
  57. /**
  58. * Interface that provides operations to obtain useful information during finder autocomplete
  59. */
  60. private final FinderAutocomplete finderAutocomplete;
  61. /**
  62. * Return type of generated finder
  63. */
  64. private JavaType returnType;
  65. /**
  66. * Return type provided in constructor when it is different from target entity. Can be null.
  67. */
  68. private JavaType providedReturnType;
  69. /**
  70. * Parameters of generated finder
  71. */
  72. List<FinderParameter> finderParameters;
  73. /**
  74. * Creates a new {@link PartTree} by parsing the given {@link String}.
  75. *
  76. * @param source
  77. * the {@link String} to parse
  78. * @param memberDetails
  79. * the member details of the entity class to extract the fields
  80. * to expose them as options.
  81. * @param finderAutocomplete interface that provides operations to obtain useful information during autocomplete
  82. */
  83. public PartTree(String source, MemberDetails memberDetails,
  84. FinderAutocomplete finderAutocomplete, JavaType providedReturnType) {
  85. Validate.notNull(source, "Source must not be null");
  86. Validate.notNull(memberDetails, "MemberDetails must not be null");
  87. this.originalQuery = source;
  88. this.finderAutocomplete = finderAutocomplete;
  89. // Extracts entity fields removing persistence fields and list type
  90. // fields
  91. List<FieldMetadata> fields = getValidProperties(memberDetails.getFields());
  92. Matcher matcher = PREFIX_TEMPLATE.matcher(source);
  93. if (!matcher.find()) {
  94. this.subject = new Subject(this, source, fields);
  95. this.predicate = new Predicate(this, "", fields);
  96. } else {
  97. this.subject = new Subject(this, matcher.group(0), fields);
  98. this.predicate = new Predicate(this, source.substring(matcher.group().length()), fields);
  99. }
  100. this.providedReturnType = providedReturnType;
  101. this.returnType = extractReturnType(memberDetails);
  102. this.finderParameters = predicate.getParameters();
  103. }
  104. public PartTree(String source, MemberDetails memberDetails, FinderAutocomplete finderAutocomplete) {
  105. this(source, memberDetails, finderAutocomplete, null);
  106. }
  107. /**
  108. * Extracts the java type of the results to be returned by the PartTree query
  109. *
  110. * @param entityDetails the entity details to extract the object to return by default
  111. * @return
  112. */
  113. private JavaType extractReturnType(MemberDetails entityDetails) {
  114. Integer maxResults = subject.getMaxResults();
  115. Pair<Stack<FieldMetadata>, String> property = subject.getProperty();
  116. JavaType type = null;
  117. // Count subject returns Long
  118. if (subject.isCountProjection()) {
  119. return JavaType.LONG_OBJECT;
  120. }
  121. if (property != null && property.getLeft() != null) {
  122. // Returns the property type if it is specified
  123. type = property.getLeft().peek().getFieldType();
  124. } else if (providedReturnType != null) {
  125. type = providedReturnType;
  126. } else {
  127. // By default returns entity type
  128. List<MemberHoldingTypeDetails> details = entityDetails.getDetails();
  129. for (MemberHoldingTypeDetails detail : details) {
  130. if (finderAutocomplete != null
  131. && finderAutocomplete.getEntityDetails(detail.getType()).equals(entityDetails)) {
  132. type = detail.getType();
  133. break;
  134. } else {
  135. type = detail.getType();
  136. }
  137. }
  138. }
  139. // Check number of results to return.
  140. if (maxResults != null && maxResults == 1) {
  141. // Unique result
  142. return type;
  143. }
  144. //If it is not an unique result, returns a list
  145. if (type.isPrimitive()) {
  146. // Lists cannot return primitive types, so primitive types are transformed into their wrapper class
  147. type =
  148. new JavaType(type.getFullyQualifiedTypeName(), type.getArray(), DataType.TYPE,
  149. type.getArgName(), type.getParameters(), type.getModule());
  150. }
  151. return new JavaType("org.springframework.data.domain.Page", 0, DataType.TYPE, null,
  152. Arrays.asList(type));
  153. }
  154. /**
  155. * Creates a new {@link PartTree} by parsing the given {@link String}.
  156. *
  157. * @param source
  158. * the {@link String} to parse
  159. * @param memberDetails
  160. * the member details of the entity class to extract the fields
  161. * to expose them as options.
  162. */
  163. public PartTree(String source, MemberDetails memberDetails) {
  164. this(source, memberDetails, null);
  165. }
  166. /**
  167. * Filters the entity properties that can be used to build Spring Data
  168. * expressions.
  169. *
  170. * Persistence version property, multivalued properties, static fields and
  171. * transient fields are excluded since Spring Data does not support
  172. * operations with them.
  173. *
  174. * @param memberDetails
  175. * @return entity properties which type is supported by SpringData
  176. */
  177. private List<FieldMetadata> getValidProperties(List<FieldMetadata> fields) {
  178. List<FieldMetadata> validProperties = new ArrayList<FieldMetadata>();
  179. for (FieldMetadata field : fields) {
  180. // Check if its type is List/Map/etc
  181. if (field.getFieldType().isMultiValued()) {
  182. continue;
  183. }
  184. // Check if it is annotated with @Version
  185. if (field.getAnnotation(new JavaType("javax.persistence.Version")) != null) {
  186. continue;
  187. }
  188. // Exclude static fields
  189. int staticFinal = Modifier.STATIC + Modifier.FINAL;
  190. int publicStatic = Modifier.PUBLIC + Modifier.STATIC;
  191. int publicStaticFinal = Modifier.PUBLIC + Modifier.STATIC + Modifier.FINAL;
  192. int privateStatic = Modifier.PRIVATE + Modifier.STATIC;
  193. int privateStaticFinal = Modifier.PRIVATE + Modifier.STATIC + Modifier.FINAL;
  194. if (field.getModifier() == Modifier.STATIC || field.getModifier() == staticFinal
  195. || field.getModifier() == publicStatic || field.getModifier() == publicStaticFinal
  196. || field.getModifier() == privateStatic || field.getModifier() == privateStaticFinal) {
  197. continue;
  198. }
  199. // Exclude transient fields and the fields annotated with @Transient
  200. int transientFinal = Modifier.TRANSIENT + Modifier.FINAL;
  201. int publicTransient = Modifier.PUBLIC + Modifier.TRANSIENT;
  202. int publicTransientFinal = Modifier.PUBLIC + Modifier.TRANSIENT + Modifier.FINAL;
  203. int privateTransient = Modifier.PRIVATE + Modifier.TRANSIENT;
  204. int privateTransientFinal = Modifier.PRIVATE + Modifier.TRANSIENT + Modifier.FINAL;
  205. if (field.getAnnotation(JpaJavaType.TRANSIENT) != null
  206. || field.getModifier() == Modifier.TRANSIENT || field.getModifier() == transientFinal
  207. || field.getModifier() == publicTransient || field.getModifier() == publicTransientFinal
  208. || field.getModifier() == privateTransient
  209. || field.getModifier() == privateTransientFinal) {
  210. continue;
  211. }
  212. validProperties.add(field);
  213. }
  214. return validProperties;
  215. }
  216. /**
  217. * Filters the entity properties of a javaType that can be used to build Spring Data
  218. * expressions. Persistence version field is excluded, and multivalued fields
  219. * are removed since Spring Data does not supports operations with them.
  220. *
  221. * @param javaType
  222. * @return entity properties which type is supported by SpringData
  223. */
  224. public List<FieldMetadata> getValidProperties(JavaType javaType) {
  225. if (finderAutocomplete != null) {
  226. final MemberDetails entityDetails = finderAutocomplete.getEntityDetails(javaType);
  227. if (entityDetails != null) {
  228. return getValidProperties((List<FieldMetadata>) entityDetails.getFields());
  229. }
  230. }
  231. return null;
  232. }
  233. /**
  234. * Extract entity property name from raw property and returns the property metadata and the property name.
  235. * If raw property references a property of a related entity, returns a Pair with the related entity property metadata and
  236. * a string composed by the reference property name and the related entity property name.
  237. * E.g. if raw property contains "petName" and current entity has a relation with Pet, it will return Pair(NameMetadata, "petName"))
  238. *
  239. * @param rawProperty the string that contains property name
  240. * @param fields entity properties
  241. * @return Pair that contains the path of property metadata and the property name.
  242. */
  243. public Pair<Stack<FieldMetadata>, String> extractValidProperty(String rawProperty,
  244. List<FieldMetadata> fields) {
  245. return extractValidProperty(rawProperty, fields, null);
  246. }
  247. /**
  248. * Extract entity property name from raw property and returns the property metadata and the property name.
  249. * If raw property references a property of a related entity, returns a Pair with the related entity property metadata and
  250. * a string composed by the reference property name and the related entity property name.
  251. * E.g. if raw property contains "petName" and current entity has a relation with Pet, it will return Pair({pet, name}, "petName"))
  252. *
  253. * @param rawProperty the string that contains property name
  254. * @param fields entity properties
  255. * @param path of previous entities
  256. * @return Pair that contains the path of property metadata and the property name.
  257. */
  258. public Pair<Stack<FieldMetadata>, String> extractValidProperty(String rawProperty,
  259. List<FieldMetadata> fields, Stack<FieldMetadata> path) {
  260. if (path == null) {
  261. path = new Stack<FieldMetadata>();
  262. }
  263. if (StringUtils.isBlank(rawProperty) || fields == null) {
  264. return null;
  265. }
  266. FieldMetadata tempField = null;
  267. rawProperty = StringUtils.uncapitalize(rawProperty);
  268. // 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
  269. for (FieldMetadata field : fields) {
  270. if (field.getFieldName().toString().compareTo(rawProperty) == 0) {
  271. tempField = field;
  272. break;
  273. }
  274. if (rawProperty.startsWith(field.getFieldName().toString())) {
  275. if (tempField == null
  276. || tempField.getFieldName().toString().length() < field.getFieldName().toString()
  277. .length())
  278. tempField = field;
  279. }
  280. }
  281. if (tempField == null) {
  282. return null;
  283. }
  284. path.push(tempField);
  285. // 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
  286. Pair<Stack<FieldMetadata>, String> related =
  287. extractRelatedEntityValidProperty(rawProperty, tempField, path);
  288. if (related != null) {
  289. return Pair.of(
  290. related.getLeft() == null ? path : related.getLeft(),
  291. StringUtils.capitalize(tempField.getFieldName().toString()).concat(
  292. StringUtils.capitalize(related.getRight())));
  293. }
  294. return Pair.of(path, StringUtils.capitalize(tempField.getFieldName().toString()));
  295. }
  296. /**
  297. * Gets the property of a related entity, using raw property information.
  298. *
  299. * @param rawProperty string that contains the definition of a property, which can be a property accessed by a relation.
  300. * @param referenceProperty property that represents a relation with other entity.
  301. * @return Pair that contains a property metadata and its name.
  302. */
  303. private Pair<Stack<FieldMetadata>, String> extractRelatedEntityValidProperty(
  304. String extractProperty, FieldMetadata referenceProperty, Stack<FieldMetadata> path) {
  305. if (StringUtils.isBlank(extractProperty) || referenceProperty == null) {
  306. return null;
  307. }
  308. // Extract the property of a related entity
  309. String property =
  310. StringUtils.substringAfter(extractProperty, referenceProperty.getFieldName().toString());
  311. if (StringUtils.isBlank(property)) {
  312. return null;
  313. }
  314. return extractValidProperty(property, getValidProperties(referenceProperty.getFieldType()),
  315. path);
  316. }
  317. /**
  318. * Returns the different queries that can be build based on the current defined query.
  319. * First it lists the subject expressions that can be build. Once it is completed, it returns the queries available to
  320. * define the predicate.
  321. *
  322. * @return
  323. */
  324. public List<String> getOptions() {
  325. if (!subject.isComplete()) {
  326. return subject.getOptions();
  327. } else if (!predicate.hasOrderClause()) {
  328. return predicate.getOptions(subject.toString());
  329. } else {
  330. return predicate.getOrderOptions(subject.toString());
  331. }
  332. }
  333. /**
  334. * Returns whether we indicate distinct lookup of entities.
  335. *
  336. * @return {@literal true} if distinct
  337. */
  338. public boolean isDistinct() {
  339. return subject.isDistinct();
  340. }
  341. /**
  342. * Returns whether a count projection shall be applied.
  343. *
  344. * @return
  345. */
  346. public Boolean isCountProjection() {
  347. return subject.isCountProjection();
  348. }
  349. @Override
  350. public String toString() {
  351. return subject.toString().concat(predicate.toString());
  352. }
  353. /**
  354. * Splits the given text at the given keyword. Expects camel-case style to
  355. * only match concrete keywords and not derivatives of it.
  356. *
  357. * @param text
  358. * the text to split
  359. * @param keyword
  360. * the keyword to split around
  361. * @param limit the limit controls the number of times the pattern is applied and therefore affects the length of the resulting array
  362. * @return an array of split items
  363. */
  364. static String[] split(String text, String keyword, int limit) {
  365. Pattern pattern = Pattern.compile(String.format(KEYWORD_TEMPLATE, keyword));
  366. return pattern.split(text, limit);
  367. }
  368. /**
  369. * Returns true if PartTree query is well-defined and the query generated is the same that the one used to build its structure.
  370. * @return
  371. */
  372. public boolean isValid() {
  373. return subject.isValid() && predicate.IsValid() && toString().equals(originalQuery);
  374. }
  375. /**
  376. * Returns true if query is well-defined, which means that subject and predicate have a correct structure.
  377. * However, it does not validate if entity properties exist.
  378. * @return
  379. */
  380. public static boolean isValid(String query) {
  381. Matcher matcher = PREFIX_TEMPLATE.matcher(query);
  382. if (!matcher.find()) {
  383. return Subject.isValid(query) && Predicate.IsValid("");
  384. } else {
  385. return Subject.isValid(matcher.group(0))
  386. && Predicate.IsValid(query.substring(matcher.group().length()));
  387. }
  388. }
  389. /**
  390. * Returns the number of maximal results to return or {@literal null} if
  391. * not restricted.
  392. *
  393. * @return
  394. */
  395. public Integer getMaxResults() {
  396. return subject.getMaxResults();
  397. }
  398. /**
  399. * Returns true if the query matches with the given {@link Pattern}. Otherwise, returns false.
  400. * If the query is null, returns false.
  401. *
  402. * @param query
  403. * @param pattern
  404. * @return
  405. */
  406. public final static boolean matches(String query, Pattern pattern) {
  407. return query == null ? false : pattern.matcher(query).find();
  408. }
  409. /**
  410. * Method that obtains the return type of current finder
  411. *
  412. * @return JavaType with return type
  413. */
  414. public JavaType getReturnType() {
  415. return returnType;
  416. }
  417. /**
  418. * Method that obtains the necessary parameters of current finder
  419. *
  420. * @return List that contains all necessary parameters
  421. */
  422. public List<FinderParameter> getParameters() {
  423. return finderParameters;
  424. }
  425. public String getOriginalQuery() {
  426. return originalQuery;
  427. }
  428. }