package org.springframework.roo.addon.dto.addon; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.Reference; import org.apache.felix.scr.annotations.Service; import org.osgi.framework.BundleContext; import org.osgi.service.component.ComponentContext; import org.springframework.roo.classpath.PhysicalTypeCategory; import org.springframework.roo.classpath.PhysicalTypeIdentifier; import org.springframework.roo.classpath.TypeLocationService; import org.springframework.roo.classpath.TypeManagementService; import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; import org.springframework.roo.classpath.details.FieldDetails; import org.springframework.roo.classpath.details.FieldMetadata; import org.springframework.roo.classpath.details.FieldMetadataBuilder; import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; import org.springframework.roo.classpath.details.annotations.ArrayAttributeValue; import org.springframework.roo.classpath.details.annotations.StringAttributeValue; import org.springframework.roo.classpath.operations.jsr303.CollectionField; import org.springframework.roo.classpath.operations.jsr303.DateField; import org.springframework.roo.classpath.operations.jsr303.ListField; import org.springframework.roo.classpath.operations.jsr303.SetField; import org.springframework.roo.classpath.persistence.PersistenceMemberLocator; import org.springframework.roo.classpath.scanner.MemberDetailsScanner; import org.springframework.roo.model.DataType; import org.springframework.roo.model.JavaSymbolName; import org.springframework.roo.model.JavaType; import org.springframework.roo.model.JdkJavaType; import org.springframework.roo.model.JpaJavaType; import org.springframework.roo.model.RooJavaType; import org.springframework.roo.process.manager.FileManager; import org.springframework.roo.project.Dependency; import org.springframework.roo.project.LogicalPath; import org.springframework.roo.project.Path; import org.springframework.roo.project.PathResolver; import org.springframework.roo.project.ProjectOperations; import org.springframework.roo.project.Property; import org.springframework.roo.shell.ShellContext; import org.springframework.roo.support.logging.HandlerUtils; import org.springframework.roo.support.osgi.ServiceInstaceManager; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.logging.Logger; /** * Implementation of {@link JpaOperations}. * * @author Sergio Clares * @since 2.0 */ @Component @Service public class DtoOperationsImpl implements DtoOperations { protected final static Logger LOGGER = HandlerUtils.getLogger(DtoOperationsImpl.class); private static final Property SPRINGLETS_VERSION_PROPERTY = new Property("springlets.version", "1.2.0.RC1"); private static final Dependency SPRINGLETS_CONTEXT_DEPENDENCY = new Dependency("io.springlets", "springlets-context", "${springlets.version}"); //------------ OSGi component attributes ---------------- private BundleContext context; private ServiceInstaceManager serviceInstaceManager = new ServiceInstaceManager(); protected void activate(final ComponentContext context) { this.context = context.getBundleContext(); serviceInstaceManager.activate(this.context); } @Reference private ProjectOperations projectOperations; @Reference private TypeManagementService typeManagementService; @Reference private TypeLocationService typeLocationService; @Reference private MemberDetailsScanner memberDetailsScanner; @Reference private PathResolver pathResolver; @Reference private FileManager fileManager; @Override public boolean isDtoCreationPossible() { return projectOperations.isFocusedProjectAvailable(); } @Override public boolean isEntityProjectionPossible() { return typeLocationService.findTypesWithAnnotation(RooJavaType.ROO_JPA_ENTITY).size() > 0; } @Override public void createDto(JavaType name, boolean immutable, boolean utilityMethods, boolean serializable, String formatExpression, String formatMessage) { Validate.isTrue(!JdkJavaType.isPartOfJavaLang(name.getSimpleTypeName()), "Class name '%s' is part of java.lang", name.getSimpleTypeName()); // Set focus on dto module projectOperations.setModule(projectOperations.getPomFromModuleName(name.getModule())); // Add springlets-context dependency projectOperations.addDependency(name.getModule(), SPRINGLETS_CONTEXT_DEPENDENCY); projectOperations.addProperty("", SPRINGLETS_VERSION_PROPERTY); // Create file final String declaredByMetadataId = PhysicalTypeIdentifier.createIdentifier(name, LogicalPath.getInstance(Path.SRC_MAIN_JAVA, name.getModule())); final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder(declaredByMetadataId, Modifier.PUBLIC, name, PhysicalTypeCategory.CLASS); // Add @RooDTO and @RooJavaBean AnnotationMetadataBuilder rooDtoAnnotation = new AnnotationMetadataBuilder(RooJavaType.ROO_DTO); AnnotationMetadataBuilder rooJavaBeanAnnotation = new AnnotationMetadataBuilder(RooJavaType.ROO_JAVA_BEAN); if (immutable) { rooDtoAnnotation.addBooleanAttribute("immutable", immutable); rooJavaBeanAnnotation.addBooleanAttribute("settersByDefault", false); } // ROO-3868: New entity visualization support using a DTO // Check for each attribute individually if (StringUtils.isNotBlank(formatExpression)) { rooDtoAnnotation.addStringAttribute("formatExpression", formatExpression); } if (StringUtils.isNotBlank(formatMessage)) { rooDtoAnnotation.addStringAttribute("formatMessage", formatMessage); } cidBuilder.addAnnotation(rooDtoAnnotation); cidBuilder.addAnnotation(rooJavaBeanAnnotation); // Add utility annotations if necessary if (utilityMethods) { AnnotationMetadataBuilder rooToStringAnnotation = new AnnotationMetadataBuilder(RooJavaType.ROO_TO_STRING); AnnotationMetadataBuilder rooEqualsAnnotation = new AnnotationMetadataBuilder(RooJavaType.ROO_EQUALS); cidBuilder.addAnnotation(rooToStringAnnotation); cidBuilder.addAnnotation(rooEqualsAnnotation); } // Add @RooSerializable if necessary if (serializable) { AnnotationMetadataBuilder rooSerializableAnnotation = new AnnotationMetadataBuilder(RooJavaType.ROO_SERIALIZABLE); cidBuilder.addAnnotation(rooSerializableAnnotation); } typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build()); } @Override public void createProjection(JavaType entity, JavaType name, String fields, String suffix, String formatExpression, String formatMessage) { Validate.notNull(name, "Use --class to select the name of the Projection."); // TODO: Validate fields for excluding entity collection, transient and // static fields from operations (already doing when comming from commands). // Set focus on projection module projectOperations.setModule(projectOperations.getPomFromModuleName(name.getModule())); // Add springlets-context dependency projectOperations.addDependency(name.getModule(), SPRINGLETS_CONTEXT_DEPENDENCY); projectOperations.addProperty("", SPRINGLETS_VERSION_PROPERTY); Map<String, FieldMetadata> fieldsToAdd = new LinkedHashMap<String, FieldMetadata>(); boolean onlyMainEntityFields = true; if (fields != null) { onlyMainEntityFields = false; // Check that id field has been included. If not, include it. fields = checkAndAddIdField(entity, fields); fieldsToAdd = buildFieldsFromString(fields, entity); } else { List<FieldMetadata> allFields = memberDetailsScanner.getMemberDetails(this.getClass().getName(), typeLocationService.getTypeDetails(entity)).getFields(); for (FieldMetadata field : allFields) { // Add only valid fields if (isFieldValidForProjection(field)) { fieldsToAdd.put(field.getFieldName().getSymbolName(), field); } } } // Create projection final String declaredByMetadataId = PhysicalTypeIdentifier.createIdentifier(name, LogicalPath.getInstance(Path.SRC_MAIN_JAVA, name.getModule())); final ClassOrInterfaceTypeDetailsBuilder projectionBuilder = new ClassOrInterfaceTypeDetailsBuilder(declaredByMetadataId, Modifier.PUBLIC, name, PhysicalTypeCategory.CLASS); // Add fields to projection addFieldsToProjection(projectionBuilder, fieldsToAdd); // @RooJavaBean, @RooToString and @RooEquals AnnotationMetadataBuilder rooJavaBeanAnnotation = new AnnotationMetadataBuilder(RooJavaType.ROO_JAVA_BEAN); rooJavaBeanAnnotation.addBooleanAttribute("settersByDefault", false); projectionBuilder.addAnnotation(rooJavaBeanAnnotation); AnnotationMetadataBuilder rooToStringAnnotation = new AnnotationMetadataBuilder(RooJavaType.ROO_TO_STRING); AnnotationMetadataBuilder rooEqualsAnnotation = new AnnotationMetadataBuilder(RooJavaType.ROO_EQUALS); projectionBuilder.addAnnotation(rooToStringAnnotation); projectionBuilder.addAnnotation(rooEqualsAnnotation); // Add @RooEntityProjection AnnotationMetadataBuilder projectionAnnotation = new AnnotationMetadataBuilder(RooJavaType.ROO_ENTITY_PROJECTION); projectionAnnotation.addClassAttribute("entity", entity); List<StringAttributeValue> fieldNames = new ArrayList<StringAttributeValue>(); if (onlyMainEntityFields) { // Should add all entity fields for (FieldMetadata field : fieldsToAdd.values()) { fieldNames.add(new StringAttributeValue(new JavaSymbolName("fields"), field.getFieldName() .getSymbolName())); } } else { // --fields option has been completed and validated, so build annotation 'fields' // param from selected fields String[] fieldsFromCommand = StringUtils.split(fields, ","); for (int i = 0; i < fieldsFromCommand.length; i++) { fieldNames .add(new StringAttributeValue(new JavaSymbolName("fields"), fieldsFromCommand[i])); } } projectionAnnotation.addAttribute(new ArrayAttributeValue<StringAttributeValue>( new JavaSymbolName("fields"), fieldNames)); // ROO-3868: New entity visualization support using a Projection // Check for each attribute individually if (StringUtils.isNotBlank(formatExpression)) { projectionAnnotation.addStringAttribute("formatExpression", formatExpression); } if (StringUtils.isNotBlank(formatMessage)) { projectionAnnotation.addStringAttribute("formatMessage", formatMessage); } projectionBuilder.addAnnotation(projectionAnnotation); // Build and save changes to disk typeManagementService.createOrUpdateTypeOnDisk(projectionBuilder.build()); } @Override public void createAllProjections(String suffix, ShellContext shellContext) { // Get all entities Set<ClassOrInterfaceTypeDetails> entities = typeLocationService.findClassesOrInterfaceDetailsWithAnnotation(RooJavaType.ROO_JPA_ENTITY, JpaJavaType.ENTITY); List<String> projectionNames = new ArrayList<String>(); // Get all Projections to check names Set<ClassOrInterfaceTypeDetails> currentProjections = typeLocationService.findClassesOrInterfaceDetailsWithAnnotation(RooJavaType.ROO_DTO); for (ClassOrInterfaceTypeDetails projection : currentProjections) { projectionNames.add(projection.getType().getFullyQualifiedTypeName()); } // Create Projection for each entity for (ClassOrInterfaceTypeDetails entity : entities) { String name = entity.getType().getFullyQualifiedTypeName().concat(suffix); if (projectionNames.contains(name) && !shellContext.isForce()) { throw new IllegalArgumentException( "One or more Projections with pre-generated names already " + "exist and cannot be created. Use --force parameter to overwrite them. Suffix for pre-generated names is " .concat(suffix)); } // Create Projection if doesn't exists, already JavaType projectionType = new JavaType(name, entity.getType().getModule()); final String entityFilePathIdentifier = pathResolver.getCanonicalPath(projectionType.getModule(), Path.SRC_MAIN_JAVA, projectionType); if (!fileManager.exists(entityFilePathIdentifier)) { createProjection(entity.getType(), projectionType, null, suffix, null, null); } } } /** * Returns the list of fields to include in Projection. * * @param fieldsString the fields provided by user. * @param entity the associated entity to use for searching the fields. * @return Map<String, FieldMetadata> with the field name to add in the Projection * and its metadata. */ public Map<String, FieldMetadata> buildFieldsFromString(String fieldsString, JavaType entity) { // Create Map for storing FieldMetadata and it's future new name Map<String, FieldMetadata> fieldsToAdd = new LinkedHashMap<String, FieldMetadata>(); // Create array of field names from command String fieldsString = fieldsString.trim(); String[] fields = fieldsString.split(","); ClassOrInterfaceTypeDetails cid = typeLocationService.getTypeDetails(entity); List<FieldMetadata> allFields = memberDetailsScanner.getMemberDetails(this.getClass().getName(), cid).getFields(); // Iterate over all specified fields for (int i = 0; i < fields.length; i++) { String fieldName = ""; boolean found = false; // Iterate over all entity fields for (FieldMetadata field : allFields) { if (field.getFieldName().getSymbolName().equals(fields[i])) { // If found, add field to returned map fieldsToAdd.put(field.getFieldName().getSymbolName(), field); found = true; break; } } if (!found) { // The field isn't in the entity, should be in one of its relations String[] splittedByDot = StringUtils.split(fields[i], "."); JavaType currentEntity = entity; if (fields[i].contains(".")) { // Search a matching relation field for (int t = 0; t < splittedByDot.length; t++) { ClassOrInterfaceTypeDetails currentEntityCid = typeLocationService.getTypeDetails(currentEntity); List<FieldMetadata> currentEntityFields = memberDetailsScanner.getMemberDetails(this.getClass().getName(), currentEntityCid) .getFields(); boolean relationFieldFound = false; // Iterate to build the field-levels of the relation field for (FieldMetadata field : currentEntityFields) { if (field.getFieldName().getSymbolName().equals(splittedByDot[t]) && t != splittedByDot.length - 1 && typeLocationService.getTypeDetails(field.getFieldType()) != null && typeLocationService.getTypeDetails(field.getFieldType()).getAnnotation( RooJavaType.ROO_JPA_ENTITY) != null) { // Field is an entity and we should look into its fields currentEntity = field.getFieldType(); found = true; relationFieldFound = true; if (t == 0) { fieldName = fieldName.concat(field.getFieldName().getSymbolName()); } else { fieldName = fieldName .concat(StringUtils.capitalize(field.getFieldName().getSymbolName())); } break; } else if (field.getFieldName().getSymbolName().equals(splittedByDot[t])) { // Add field to projection fields fieldName = fieldName.concat(StringUtils.capitalize(field.getFieldName().getSymbolName())); fieldsToAdd.put(fieldName, field); found = true; relationFieldFound = true; break; } } // If not found, relation field is bad written if (!relationFieldFound) { throw new IllegalArgumentException(String.format( "Field %s couldn't be located in %s. Please, be sure that it is well written.", splittedByDot[t], currentEntity.getFullyQualifiedTypeName())); } } } else { // Not written as a relation field throw new IllegalArgumentException(String.format( "Field %s couldn't be located in entity %s", fields[i], entity.getFullyQualifiedTypeName())); } } // If still not found, field is bad written if (!found) { throw new IllegalArgumentException(String.format( "Field %s couldn't be located. Please, be sure that it is well written.", fields[i])); } } return fieldsToAdd; } /** * Check if provided fields contain the related entity id field and otherwise * adds it to the Map. * * @param entity JavaType the Projection related entity. * @param fieldsString the String with the fields received from operation * @return the String with the fields to add to Projection. */ private String checkAndAddIdField(JavaType entity, String fieldsString) { List<FieldMetadata> identifierFields = getPersistenceMemberLocator().getIdentifierFields(entity); String[] fieldsProvided = fieldsString.split(","); for (FieldMetadata idField : identifierFields) { boolean fieldIsInProjection = false; for (String field : fieldsProvided) { if (field.equals(idField.getFieldName().getSymbolName())) { fieldIsInProjection = true; break; } } if (!fieldIsInProjection) { // Add to fields String, which will be used to create the annotation fieldsString = idField.getFieldName().getSymbolName().concat(",").concat(fieldsString); LOGGER.info(String.format( "INFO: You haven't included the identifier field/s of the entity '%s' in " + "your projection, which is necessary to be able to use this projection " + "in the view layer. But don't worry, Spring Roo has included it automatically.", entity.getSimpleTypeName())); } } return fieldsString; } /** * Removes persistence annotations of provided fields and adds them to a * ClassOrInterfaceTypeDetailsBuilder representing a Projection in construction. * Also adds final modifier to fields if required. * * @param projectionBuilder the ClassOrInterfaceTypeDetailsBuilder for building the * Projection class. * @param fieldsToAdd the List<FieldMetadata> to add. */ private void addFieldsToProjection(ClassOrInterfaceTypeDetailsBuilder projectionBuilder, Map<String, FieldMetadata> fieldsToAdd) { Iterator<Entry<String, FieldMetadata>> iterator = fieldsToAdd.entrySet().iterator(); while (iterator.hasNext()) { Entry<String, FieldMetadata> entry = iterator.next(); FieldMetadata field = entry.getValue(); // List and Set types require special management FieldMetadataBuilder fieldBuilder = null; FieldDetails fieldDetails = null; if (field.getFieldType().getFullyQualifiedTypeName().equals("java.util.Set")) { JavaType fieldType = field.getFieldType().getParameters().get(0); fieldDetails = new SetField(projectionBuilder.getDeclaredByMetadataId(), new JavaType( JdkJavaType.SET.getFullyQualifiedTypeName(), 0, DataType.TYPE, null, Arrays.asList(fieldType)), field.getFieldName(), fieldType, null, null, true); fieldBuilder = new FieldMetadataBuilder(fieldDetails); fieldBuilder.setModifier(field.getModifier()); fieldBuilder.setAnnotations(field.getAnnotations()); } else if (field.getFieldType().getFullyQualifiedTypeName().equals("java.util.List")) { JavaType fieldType = field.getFieldType().getParameters().get(0); fieldDetails = new ListField(projectionBuilder.getDeclaredByMetadataId(), new JavaType( JdkJavaType.LIST.getFullyQualifiedTypeName(), 0, DataType.TYPE, null, Arrays.asList(fieldType)), field.getFieldName(), fieldType, null, null, true); fieldBuilder = new FieldMetadataBuilder(fieldDetails); fieldBuilder.setModifier(field.getModifier()); fieldBuilder.setAnnotations(field.getAnnotations()); } else { // Can't just copy FieldMetadata because field.declaredByMetadataId will be the same fieldBuilder = new FieldMetadataBuilder(projectionBuilder.getDeclaredByMetadataId(), field); } // Add dependency between modules typeLocationService.addModuleDependency(projectionBuilder.getName().getModule(), field.getFieldType()); // Set new fieldName fieldBuilder.setFieldName(new JavaSymbolName(entry.getKey())); // If it is a CollectionField it needs an initializer String initializer = null; if (fieldDetails instanceof CollectionField) { final CollectionField collectionField = (CollectionField) fieldDetails; initializer = "new " + collectionField.getInitializer() + "()"; } else if (fieldDetails instanceof DateField && fieldDetails.getFieldName().getSymbolName().equals("created")) { initializer = "new Date()"; } fieldBuilder.setFieldInitializer(initializer); // Remove persistence annotations List<AnnotationMetadata> annotations = field.getAnnotations(); for (AnnotationMetadata annotation : annotations) { if (annotation.getAnnotationType().getFullyQualifiedTypeName() .contains("javax.persistence")) { fieldBuilder.removeAnnotation(annotation.getAnnotationType()); } else if (annotation.getAnnotationType().getFullyQualifiedTypeName() .startsWith("javax.validation")) { // Add validation dependency projectOperations.addDependency(projectionBuilder.getName().getModule(), new Dependency( "javax.validation", "validation-api", null)); } else if (annotation.getAnnotationType().equals(RooJavaType.ROO_JPA_RELATION)) { fieldBuilder.removeAnnotation(annotation.getAnnotationType()); } } fieldBuilder.setModifier(Modifier.PRIVATE); // Build field FieldMetadata projectionField = fieldBuilder.build(); // Add field to DTO projectionBuilder.addField(projectionField); } } private boolean isFieldValidForProjection(FieldMetadata field) { // Exclude static fields if (Modifier.isStatic(field.getModifier())) { return false; } // Exclude transient fields if (field.getAnnotation(JpaJavaType.TRANSIENT) != null) { return false; } // Exclude entity collection fields JavaType fieldType = field.getFieldType(); if (fieldType.isCommonCollectionType()) { boolean isEntityCollectionField = false; List<JavaType> parameters = fieldType.getParameters(); for (JavaType parameter : parameters) { if (typeLocationService.getTypeDetails(parameter) != null && typeLocationService.getTypeDetails(parameter).getAnnotation( RooJavaType.ROO_JPA_ENTITY) != null) { isEntityCollectionField = true; break; } } if (isEntityCollectionField) { return false; } } return true; } protected PersistenceMemberLocator getPersistenceMemberLocator() { return serviceInstaceManager.getServiceInstance(this, PersistenceMemberLocator.class); } }