package org.springframework.roo.addon.finder; import static org.springframework.roo.model.JavaType.STRING; import static org.springframework.roo.model.JpaJavaType.ENTITY_MANAGER; import static org.springframework.roo.model.JpaJavaType.TYPED_QUERY; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.builder.ToStringBuilder; import org.springframework.roo.addon.jpa.activerecord.RooJpaActiveRecord; import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; import org.springframework.roo.classpath.PhysicalTypeMetadata; import org.springframework.roo.classpath.details.MethodMetadata; import org.springframework.roo.classpath.details.MethodMetadataBuilder; import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem; import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; import org.springframework.roo.metadata.MetadataIdentificationUtils; import org.springframework.roo.model.DataType; import org.springframework.roo.model.JavaSymbolName; import org.springframework.roo.model.JavaType; import org.springframework.roo.project.LogicalPath; /** * Metadata for {@link RooJpaActiveRecord#finders()}. * * @author Stefan Schmidt * @author Ben Alex * @since 1.0 */ public class FinderMetadata extends AbstractItdTypeDetailsProvidingMetadataItem { private static final String PROVIDES_TYPE_STRING = FinderMetadata.class .getName(); private static final String PROVIDES_TYPE = MetadataIdentificationUtils .create(PROVIDES_TYPE_STRING); public static String createIdentifier(final JavaType javaType, final LogicalPath path) { return PhysicalTypeIdentifierNamingUtils.createIdentifier( PROVIDES_TYPE_STRING, javaType, path); } public static JavaType getJavaType(final String metadataIdentificationString) { return PhysicalTypeIdentifierNamingUtils.getJavaType( PROVIDES_TYPE_STRING, metadataIdentificationString); } public static String getMetadataIdentiferType() { return PROVIDES_TYPE; } public static LogicalPath getPath(final String metadataIdentificationString) { return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, metadataIdentificationString); } public static boolean isValid(final String metadataIdentificationString) { return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, metadataIdentificationString); } private final List<MethodMetadata> dynamicFinderMethods = new ArrayList<MethodMetadata>(); private Map<JavaSymbolName, QueryHolder> queryHolders; public FinderMetadata(final String identifier, final JavaType aspectName, final PhysicalTypeMetadata governorPhysicalTypeMetadata, final MethodMetadata entityManagerMethod, final Map<JavaSymbolName, QueryHolder> queryHolders) { super(identifier, aspectName, governorPhysicalTypeMetadata); Validate.isTrue(isValid(identifier), "Metadata identification string '" + identifier + "' does not appear to be a valid"); Validate.isTrue(entityManagerMethod != null || queryHolders.isEmpty(), "EntityManager method required if any query holders are provided"); Validate.notNull(queryHolders, "Query holders required"); if (!isValid()) { return; } this.queryHolders = queryHolders; for (final JavaSymbolName finderName : queryHolders.keySet()) { final MethodMetadataBuilder methodBuilder = getDynamicFinderMethod( finderName, entityManagerMethod); builder.addMethod(methodBuilder); dynamicFinderMethods.add(methodBuilder.build()); } // Create a representation of the desired output ITD itdTypeDetails = builder.build(); } /** * Obtains all the currently-legal dynamic finders known to this metadata * instance. This may be a subset (or even completely empty) versus those * requested via the {@link RooJpaActiveRecord} annotation, as the user may * have made a typing error in representing the requested dynamic finder, * the field may have been deleted by the user, or an add-on which produces * the field (or its mutator) might not yet be loaded or in error or other * similar conditions. * * @return a non-null, immutable representation of currently-available * finder methods (never returns null, but may be empty) */ public List<MethodMetadata> getAllDynamicFinders() { return Collections.unmodifiableList(dynamicFinderMethods); } /** * Locates a dynamic finder method of the specified name, or creates one on * demand if not present. * <p> * It is required that the requested name was defined in the * {@link RooJpaActiveRecord#finders()}. If it is not present, an exception * is thrown. * * @param finderName the dynamic finder method name * @param entityManagerMethod required * @return the user-defined method, or an ITD-generated method (never * returns null) */ private MethodMetadataBuilder getDynamicFinderMethod( final JavaSymbolName finderName, final MethodMetadata entityManagerMethod) { Validate.notNull(finderName, "Dynamic finder method name is required"); Validate.isTrue(queryHolders.containsKey(finderName), "Undefined method name '" + finderName.getSymbolName() + "'"); // We have no access to method parameter information, so we scan by name // alone and treat any match as authoritative // We do not scan the superclass, as the caller is expected to know // we'll only scan the current class for (final MethodMetadata method : governorTypeDetails .getDeclaredMethods()) { if (method.getMethodName().equals(finderName)) { // Found a method of the expected name; we won't check method // parameters though return new MethodMetadataBuilder(method); } } // To get this far we need to create the method... final List<JavaType> parameters = new ArrayList<JavaType>(); parameters.add(destination); final JavaType typedQueryType = new JavaType( TYPED_QUERY.getFullyQualifiedTypeName(), 0, DataType.TYPE, null, parameters); final QueryHolder queryHolder = queryHolders.get(finderName); final String jpaQuery = queryHolder.getJpaQuery(); final List<JavaType> parameterTypes = queryHolder.getParameterTypes(); final List<JavaSymbolName> parameterNames = queryHolder .getParameterNames(); // We declared the field in this ITD, so produce a public accessor for // it final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); final String methodName = finderName.getSymbolName(); boolean containsCollectionType = false; for (int i = 0; i < parameterTypes.size(); i++) { final String name = parameterNames.get(i).getSymbolName(); final StringBuilder length = new StringBuilder(); if (parameterTypes.get(i).equals(STRING)) { length.append(" || ").append(parameterNames.get(i)) .append(".length() == 0"); } if (!parameterTypes.get(i).isPrimitive()) { bodyBuilder.appendFormalLine("if (" + name + " == null" + length.toString() + ") throw new IllegalArgumentException(\"The " + name + " argument is required\");"); } if (length.length() > 0 && methodName.substring( methodName.indexOf(parameterNames.get(i) .getSymbolNameCapitalisedFirstLetter()) + name.length()).startsWith("Like")) { bodyBuilder.appendFormalLine(name + " = " + name + ".replace('*', '%');"); bodyBuilder.appendFormalLine("if (" + name + ".charAt(0) != '%') {"); bodyBuilder.indent(); bodyBuilder.appendFormalLine(name + " = \"%\" + " + name + ";"); bodyBuilder.indentRemove(); bodyBuilder.appendFormalLine("}"); bodyBuilder.appendFormalLine("if (" + name + ".charAt(" + name + ".length() - 1) != '%') {"); bodyBuilder.indent(); bodyBuilder.appendFormalLine(name + " = " + name + " + \"%\";"); bodyBuilder.indentRemove(); bodyBuilder.appendFormalLine("}"); } if (parameterTypes.get(i).isCommonCollectionType()) { containsCollectionType = true; } } // Get the entityManager() method (as per ROO-216) bodyBuilder.appendFormalLine(ENTITY_MANAGER .getNameIncludingTypeParameters(false, builder.getImportRegistrationResolver()) + " em = " + destination.getSimpleTypeName() + "." + entityManagerMethod.getMethodName().getSymbolName() + "();"); final List<JavaSymbolName> collectionTypeNames = new ArrayList<JavaSymbolName>(); if (containsCollectionType) { bodyBuilder .appendFormalLine("StringBuilder queryBuilder = new StringBuilder(\"" + jpaQuery + "\");"); boolean jpaQueryComplete = false; for (int i = 0; i < parameterTypes.size(); i++) { if (!jpaQueryComplete && !jpaQuery.trim().endsWith("WHERE") && !jpaQuery.trim().endsWith("AND") && !jpaQuery.trim().endsWith("OR")) { bodyBuilder.appendFormalLine("queryBuilder.append(\"" + (methodName.substring( methodName.toLowerCase().indexOf( parameterNames.get(i) .getSymbolName() .toLowerCase()) + parameterNames.get(i) .getSymbolName().length()) .startsWith("And") ? " AND" : " OR") + "\");"); jpaQueryComplete = true; } if (parameterTypes.get(i).isCommonCollectionType()) { collectionTypeNames.add(parameterNames.get(i)); } } int position = 0; for (final JavaSymbolName name : collectionTypeNames) { bodyBuilder.appendFormalLine("for (int i = 0; i < " + name + ".size(); i++) {"); bodyBuilder.indent(); bodyBuilder .appendFormalLine("if (i > 0) queryBuilder.append(\" AND\");"); bodyBuilder.appendFormalLine("queryBuilder.append(\" :" + name + "_item\").append(i).append(\" MEMBER OF o." + name + "\");"); bodyBuilder.indentRemove(); bodyBuilder.appendFormalLine("}"); if (collectionTypeNames.size() > ++position) { bodyBuilder.appendFormalLine("queryBuilder.append(\"" + (methodName.substring( methodName.toLowerCase().indexOf( name.getSymbolName().toLowerCase()) + name.getSymbolName().length()) .startsWith("And") ? " AND" : " OR") + "\");"); } } bodyBuilder.appendFormalLine(typedQueryType .getNameIncludingTypeParameters(false, builder.getImportRegistrationResolver()) + " q = em.createQuery(queryBuilder.toString(), " + destination.getSimpleTypeName() + ".class);"); for (int i = 0; i < parameterTypes.size(); i++) { if (parameterTypes.get(i).isCommonCollectionType()) { bodyBuilder.appendFormalLine("int " + parameterNames.get(i) + "Index = 0;"); bodyBuilder.appendFormalLine("for (" + parameterTypes.get(i).getParameters().get(0) .getSimpleTypeName() + " _" + parameterTypes.get(i).getParameters().get(0) .getSimpleTypeName().toLowerCase() + ": " + parameterNames.get(i) + ") {"); bodyBuilder.indent(); bodyBuilder.appendFormalLine("q.setParameter(\"" + parameterNames.get(i) + "_item\" + " + parameterNames.get(i) + "Index++, _" + parameterTypes.get(i).getParameters().get(0) .getSimpleTypeName().toLowerCase() + ");"); bodyBuilder.indentRemove(); bodyBuilder.appendFormalLine("}"); } else { bodyBuilder.appendFormalLine("q.setParameter(\"" + parameterNames.get(i) + "\", " + parameterNames.get(i) + ");"); } } } else { bodyBuilder.appendFormalLine(typedQueryType .getNameIncludingTypeParameters(false, builder.getImportRegistrationResolver()) + " q = em.createQuery(\"" + jpaQuery + "\", " + destination.getSimpleTypeName() + ".class);"); for (final JavaSymbolName name : parameterNames) { bodyBuilder.appendFormalLine("q.setParameter(\"" + name + "\", " + name + ");"); } } bodyBuilder.appendFormalLine("return q;"); return new MethodMetadataBuilder(getId(), Modifier.PUBLIC | Modifier.STATIC, finderName, typedQueryType, AnnotatedJavaType.convertFromJavaTypes(parameterTypes), parameterNames, bodyBuilder); } @Override public String toString() { final ToStringBuilder builder = new ToStringBuilder(this); builder.append("identifier", getId()); builder.append("valid", valid); builder.append("aspectName", aspectName); builder.append("destinationType", destination); builder.append("governor", governorPhysicalTypeMetadata.getId()); builder.append("itdTypeDetails", itdTypeDetails); return builder.toString(); } }