package org.springframework.roo.addon.web.mvc.controller.converter; import static org.springframework.roo.model.SpringJavaType.CONFIGURABLE; import static org.springframework.roo.model.SpringJavaType.FORMATTER_REGISTRY; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.springframework.roo.addon.json.CustomDataJsonTags; 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.classpath.layers.MemberTypeAdditions; import org.springframework.roo.model.JavaSymbolName; import org.springframework.roo.model.JavaType; import org.springframework.roo.model.SpringJavaType; /** * Represents metadata for the application-wide conversion service. Generates * the following ITD methods: * <ul> * <li>afterPropertiesSet() - overrides InitializingBean lifecycle parent method * </li> * <li>installLabelConverters(FormatterRegistry registry) - registers all * converter methods</li> * <li>a converter method for all scaffolded domain types as well their * associations</li> * </ul> * * @author Rossen Stoyanchev * @author Stefan Schmidt * @since 1.1.1 */ public class ConversionServiceMetadata extends AbstractItdTypeDetailsProvidingMetadataItem { private static final JavaType BASE_64 = new JavaType( "org.apache.commons.codec.binary.Base64"); private static final String CONVERTER = "Converter"; private static final JavaSymbolName INSTALL_LABEL_CONVERTERS = new JavaSymbolName( "installLabelConverters"); private Map<JavaType, Map<Object, JavaSymbolName>> compositePrimaryKeyTypes; private Map<JavaType, MemberTypeAdditions> findMethods; private Map<JavaType, JavaType> idTypes; private Set<JavaType> relevantDomainTypes; private Map<JavaType, List<MethodMetadata>> toStringMethods; /** * Production constructor * * @param identifier * @param aspectName * @param governorPhysicalTypeMetadata * @param findMethods * @param idTypes the ID types of the domain types for which to generate * converters (required); must be one for each domain type * @param relevantDomainTypes the types for which to generate converters * (required) * @param compositePrimaryKeyTypes (required) */ public ConversionServiceMetadata( final String identifier, final JavaType aspectName, final PhysicalTypeMetadata governorPhysicalTypeMetadata, final Map<JavaType, MemberTypeAdditions> findMethods, final Map<JavaType, JavaType> idTypes, final Set<JavaType> relevantDomainTypes, final Map<JavaType, Map<Object, JavaSymbolName>> compositePrimaryKeyTypes, final Map<JavaType, List<MethodMetadata>> toStringMethods) { super(identifier, aspectName, governorPhysicalTypeMetadata); Validate.notNull(findMethods, "Find methods required"); Validate.notNull(compositePrimaryKeyTypes, "List of PK types required"); Validate.notNull(idTypes, "List of ID types required"); Validate.notNull(relevantDomainTypes, "List of relevant domain types required"); Validate.isTrue(relevantDomainTypes.size() == idTypes.size(), "Expected " + relevantDomainTypes.size() + " ID types, but was " + idTypes.size()); Validate.notNull(toStringMethods, "ToString methods required"); if (!isValid() || relevantDomainTypes.isEmpty() && compositePrimaryKeyTypes.isEmpty()) { valid = false; return; } this.findMethods = findMethods; this.compositePrimaryKeyTypes = compositePrimaryKeyTypes; this.idTypes = idTypes; this.relevantDomainTypes = relevantDomainTypes; this.toStringMethods = toStringMethods; builder.addAnnotation(getTypeAnnotation(CONFIGURABLE)); builder.addMethod(getInstallLabelConvertersMethod()); builder.addMethod(getAfterPropertiesSetMethod()); itdTypeDetails = builder.build(); } private MethodMetadataBuilder getAfterPropertiesSetMethod() { final JavaSymbolName methodName = new JavaSymbolName( "afterPropertiesSet"); if (governorHasMethod(methodName)) { return null; } final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); bodyBuilder.appendFormalLine("super.afterPropertiesSet();"); bodyBuilder.appendFormalLine(INSTALL_LABEL_CONVERTERS.getSymbolName() + "(getObject());"); return new MethodMetadataBuilder(getId(), Modifier.PUBLIC, methodName, JavaType.VOID_PRIMITIVE, bodyBuilder); } private MethodMetadataBuilder getInstallLabelConvertersMethod() { final List<JavaType> sortedRelevantDomainTypes = new ArrayList<JavaType>( relevantDomainTypes); Collections.sort(sortedRelevantDomainTypes); final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); final Set<String> methodNames = new HashSet<String>(); for (final JavaType formBackingObject : sortedRelevantDomainTypes) { String simpleName = formBackingObject.getSimpleTypeName(); while (methodNames.contains(simpleName)) { simpleName += "_"; } methodNames.add(simpleName); final JavaSymbolName toIdMethodName = new JavaSymbolName("get" + simpleName + "ToStringConverter"); builder.addMethod(getToStringConverterMethod(formBackingObject, toIdMethodName, toStringMethods.get(formBackingObject))); bodyBuilder.appendFormalLine("registry.addConverter(" + toIdMethodName.getSymbolName() + "());"); final JavaSymbolName toTypeMethodName = new JavaSymbolName( "getIdTo" + simpleName + CONVERTER); final MethodMetadataBuilder toTypeConverterMethod = getToTypeConverterMethod( formBackingObject, toTypeMethodName, findMethods.get(formBackingObject), idTypes.get(formBackingObject)); if (toTypeConverterMethod != null) { builder.addMethod(toTypeConverterMethod); bodyBuilder.appendFormalLine("registry.addConverter(" + toTypeMethodName.getSymbolName() + "());"); } // Only allow conversion if ID type is not String already. if (!idTypes.get(formBackingObject).equals(JavaType.STRING)) { final JavaSymbolName stringToTypeMethodName = new JavaSymbolName( "getStringTo" + simpleName + CONVERTER); builder.addMethod(getStringToTypeConverterMethod( formBackingObject, stringToTypeMethodName, idTypes.get(formBackingObject))); bodyBuilder.appendFormalLine("registry.addConverter(" + stringToTypeMethodName.getSymbolName() + "());"); } } for (final Entry<JavaType, Map<Object, JavaSymbolName>> entry : compositePrimaryKeyTypes .entrySet()) { final JavaType targetType = entry.getKey(); final Map<Object, JavaSymbolName> jsonMethodNames = entry .getValue(); final MethodMetadataBuilder jsonToConverterMethod = getJsonToConverterMethod( targetType, jsonMethodNames.get(CustomDataJsonTags.FROM_JSON_METHOD)); if (jsonToConverterMethod != null) { builder.addMethod(jsonToConverterMethod); bodyBuilder.appendFormalLine("registry.addConverter(" + jsonToConverterMethod.getMethodName().getSymbolName() + "());"); } final MethodMetadataBuilder toJsonConverterMethod = getToJsonConverterMethod( targetType, jsonMethodNames.get(CustomDataJsonTags.TO_JSON_METHOD)); if (toJsonConverterMethod != null) { builder.addMethod(toJsonConverterMethod); bodyBuilder.appendFormalLine("registry.addConverter(" + toJsonConverterMethod.getMethodName().getSymbolName() + "());"); } } final JavaType parameterType = FORMATTER_REGISTRY; if (governorHasMethod(INSTALL_LABEL_CONVERTERS, parameterType)) { return null; } final List<JavaSymbolName> parameterNames = Arrays .asList(new JavaSymbolName("registry")); builder.getImportRegistrationResolver().addImport(parameterType); return new MethodMetadataBuilder(getId(), Modifier.PUBLIC, INSTALL_LABEL_CONVERTERS, JavaType.VOID_PRIMITIVE, AnnotatedJavaType.convertFromJavaTypes(parameterType), parameterNames, bodyBuilder); } private MethodMetadataBuilder getJsonToConverterMethod( final JavaType targetType, final JavaSymbolName jsonMethodName) { final JavaSymbolName methodName = new JavaSymbolName("getJsonTo" + targetType.getSimpleTypeName() + CONVERTER); if (governorHasMethod(methodName)) { return null; } final JavaType converterJavaType = SpringJavaType.getConverterType( JavaType.STRING, targetType); final String base64Name = BASE_64.getNameIncludingTypeParameters(false, builder.getImportRegistrationResolver()); final String typeName = targetType.getNameIncludingTypeParameters( false, builder.getImportRegistrationResolver()); final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); bodyBuilder.appendFormalLine("return new " + converterJavaType.getNameIncludingTypeParameters() + "() {"); bodyBuilder.indent(); bodyBuilder.appendFormalLine("public " + targetType.getSimpleTypeName() + " convert(String encodedJson) {"); bodyBuilder.indent(); bodyBuilder.appendFormalLine("return " + typeName + "." + jsonMethodName.getSymbolName() + "(new String(" + base64Name + ".decodeBase64(encodedJson)));"); bodyBuilder.indentRemove(); bodyBuilder.appendFormalLine("}"); bodyBuilder.indentRemove(); bodyBuilder.appendFormalLine("};"); return new MethodMetadataBuilder(getId(), Modifier.PUBLIC, methodName, converterJavaType, bodyBuilder); } /** * Returns the "string to type" converter method to be generated, if any * * @param targetType the type being converted into (required) * @param methodName the name of the method to generate if necessary * (required) * @param idType the ID type of the given target type (required) * @return <code>null</code> if none is to be generated */ private MethodMetadataBuilder getStringToTypeConverterMethod( final JavaType targetType, final JavaSymbolName methodName, final JavaType idType) { if (governorHasMethod(methodName)) { return null; } final JavaType converterJavaType = SpringJavaType.getConverterType( JavaType.STRING, targetType); final String idTypeName = idType.getNameIncludingTypeParameters(false, builder.getImportRegistrationResolver()); final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); bodyBuilder.appendFormalLine("return new " + converterJavaType.getNameIncludingTypeParameters() + "() {"); bodyBuilder.indent(); bodyBuilder.appendFormalLine("public " + targetType.getFullyQualifiedTypeName() + " convert(String id) {"); bodyBuilder.indent(); bodyBuilder .appendFormalLine("return getObject().convert(getObject().convert(id, " + idTypeName + ".class), " + targetType.getSimpleTypeName() + ".class);"); bodyBuilder.indentRemove(); bodyBuilder.appendFormalLine("}"); bodyBuilder.indentRemove(); bodyBuilder.appendFormalLine("};"); return new MethodMetadataBuilder(getId(), Modifier.PUBLIC, methodName, converterJavaType, bodyBuilder); } private MethodMetadataBuilder getToJsonConverterMethod( final JavaType targetType, final JavaSymbolName jsonMethodName) { final JavaSymbolName methodName = new JavaSymbolName("get" + targetType.getSimpleTypeName() + "ToJsonConverter"); if (governorHasMethod(methodName)) { return null; } final JavaType converterJavaType = SpringJavaType.getConverterType( targetType, JavaType.STRING); final String base64Name = BASE_64.getNameIncludingTypeParameters(false, builder.getImportRegistrationResolver()); final String targetTypeName = StringUtils.uncapitalize(targetType .getSimpleTypeName()); final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); bodyBuilder.appendFormalLine("return new " + converterJavaType.getNameIncludingTypeParameters() + "() {"); bodyBuilder.indent(); bodyBuilder .appendFormalLine("public String convert(" + targetType.getSimpleTypeName() + " " + targetTypeName + ") {"); bodyBuilder.indent(); bodyBuilder.appendFormalLine("return " + base64Name + ".encodeBase64URLSafeString(" + targetTypeName + "." + jsonMethodName.getSymbolName() + "().getBytes());"); bodyBuilder.indentRemove(); bodyBuilder.appendFormalLine("}"); bodyBuilder.indentRemove(); bodyBuilder.appendFormalLine("};"); return new MethodMetadataBuilder(getId(), Modifier.PUBLIC, methodName, converterJavaType, bodyBuilder); } private MethodMetadataBuilder getToStringConverterMethod( final JavaType targetType, final JavaSymbolName methodName, final List<MethodMetadata> toStringMethods) { if (governorHasMethod(methodName)) { return null; } final JavaType converterJavaType = SpringJavaType.getConverterType( targetType, JavaType.STRING); final String targetTypeName = StringUtils.uncapitalize(targetType .getSimpleTypeName()); final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); bodyBuilder.appendFormalLine("return new " + converterJavaType.getNameIncludingTypeParameters() + "() {"); bodyBuilder.indent(); bodyBuilder .appendFormalLine("public String convert(" + targetType.getSimpleTypeName() + " " + targetTypeName + ") {"); bodyBuilder.indent(); bodyBuilder.appendFormalLine(getTypeToStringLine(targetType, targetTypeName, toStringMethods)); bodyBuilder.indentRemove(); bodyBuilder.appendFormalLine("}"); bodyBuilder.indentRemove(); bodyBuilder.appendFormalLine("};"); return new MethodMetadataBuilder(getId(), Modifier.PUBLIC, methodName, converterJavaType, bodyBuilder); } private String getTypeToStringLine(final JavaType targetType, final String targetTypeName, final List<MethodMetadata> toStringMethods) { if (toStringMethods.isEmpty()) { return "return \"(no displayable fields)\";"; } final StringBuilder sb = new StringBuilder("return new StringBuilder()"); for (int i = 0; i < toStringMethods.size(); i++) { if (i > 0) { sb.append(".append(' ')"); } sb.append(".append("); sb.append(targetTypeName); sb.append("."); sb.append(toStringMethods.get(i).getMethodName().getSymbolName()); sb.append("())"); } sb.append(".toString();"); return sb.toString(); } private MethodMetadataBuilder getToTypeConverterMethod( final JavaType targetType, final JavaSymbolName methodName, final MemberTypeAdditions findMethod, final JavaType idType) { final MethodMetadata toTypeConverterMethod = getGovernorMethod(methodName); if (findMethod == null) { return null; } if (toTypeConverterMethod != null) { return new MethodMetadataBuilder(toTypeConverterMethod); } findMethod.copyAdditionsTo(builder, governorTypeDetails); final JavaType converterJavaType = SpringJavaType.getConverterType( idType, targetType); final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); bodyBuilder.appendFormalLine("return new " + converterJavaType.getNameIncludingTypeParameters() + "() {"); bodyBuilder.indent(); bodyBuilder.appendFormalLine("public " + targetType.getFullyQualifiedTypeName() + " convert(" + idType + " id) {"); bodyBuilder.indent(); bodyBuilder.appendFormalLine("return " + findMethod.getMethodCall() + ";"); bodyBuilder.indentRemove(); bodyBuilder.appendFormalLine("}"); bodyBuilder.indentRemove(); bodyBuilder.appendFormalLine("};"); return new MethodMetadataBuilder(getId(), Modifier.PUBLIC, methodName, converterJavaType, bodyBuilder); } }