package org.springframework.roo.addon.pushin; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; 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.felix.scr.annotations.Component; 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.ConstructorMetadata; import org.springframework.roo.classpath.details.FieldMetadata; import org.springframework.roo.classpath.details.FieldMetadataBuilder; import org.springframework.roo.classpath.details.ImportMetadata; import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; 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.details.annotations.AnnotationMetadata; import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; import org.springframework.roo.classpath.scanner.MemberDetails; import org.springframework.roo.classpath.scanner.MemberDetailsScanner; import org.springframework.roo.model.JavaPackage; import org.springframework.roo.model.JavaSymbolName; import org.springframework.roo.model.JavaType; import org.springframework.roo.model.RooJavaType; import org.springframework.roo.process.manager.FileManager; import org.springframework.roo.project.Path; import org.springframework.roo.project.PathResolver; import org.springframework.roo.project.ProjectOperations; import org.springframework.roo.support.logging.HandlerUtils; import org.springframework.roo.support.osgi.ServiceInstaceManager; /** * Operations for the 'push-in' add-on. * * @author Juan Carlos GarcĂ­a * @since 2.0 */ @Component @Service public class PushInOperationsImpl implements PushInOperations { // ------------ OSGi component attributes ---------------- private BundleContext context; protected void activate(final ComponentContext cContext) { this.context = cContext.getBundleContext(); serviceManager.activate(this.context); } private static final Logger LOGGER = HandlerUtils.getLogger(PushInOperationsImpl.class); private ServiceInstaceManager serviceManager = new ServiceInstaceManager(); @Override public boolean isPushInCommandAvailable() { return getProjectOperations().isFocusedProjectAvailable(); } @Override public List<Object> pushInAll(boolean writeOnDisk, boolean force) { List<Object> pushedElements = new ArrayList<Object>(); List<JavaPackage> projectPackages = new ArrayList<JavaPackage>(); // Getting all JavaTypes on current project for (String moduleName : getProjectOperations().getModuleNames()) { // ROO-3833: Push-in all following a specific order to avoid // metadata dependencies errors List<JavaPackage> packagesForModule = getTypeLocationService().getPackagesForModule( getProjectOperations().getPomFromModuleName(moduleName)); for (JavaPackage modulePackage : packagesForModule) { projectPackages.add(modulePackage); } Collection<JavaType> allDeclaredTypes = getTypeLocationService().getTypesForModule( getProjectOperations().getPomFromModuleName(moduleName)); if (!force) { for (JavaType declaredType : allDeclaredTypes) { // Push-in all content from .aj files to .java files pushedElements.addAll(pushInClass(declaredType, writeOnDisk, force)); } } else { for (JavaPackage modulePackage : packagesForModule) { pushIn(modulePackage, null, null, writeOnDisk); getFileManager().scan(); } } } if (!force) { LOGGER .log( Level.INFO, "All these changes will be applied. Execute your previous push-in command using --force parameter to apply them."); } return pushedElements; } @Override public List<Object> pushIn(JavaPackage specifiedPackage, JavaType klass, String method, boolean writeOnDisk) { List<Object> pushedElements = new ArrayList<Object>(); // Getting all JavaTypes on current project Collection<JavaType> allDeclaredTypes = new ArrayList<JavaType>(); for (String moduleName : getProjectOperations().getModuleNames()) { allDeclaredTypes.addAll(getTypeLocationService().getTypesForModule( getProjectOperations().getPomFromModuleName(moduleName))); } // Checking current class if (klass != null) { ClassOrInterfaceTypeDetails classDetails = getTypeLocationService().getTypeDetails(klass); Validate.notNull( classDetails, String.format("ERROR: Provided class '%s' doesn't exist on current project.", klass.getSimpleTypeName())); // If --class parameter is provided, --package will be ignored specifiedPackage = klass.getPackage(); // If --method is provided, method should exist on the provided // class if (method != null) { boolean methodExists = false; MemberDetails classMemberDetails = getMemberDetailsScanner().getMemberDetails(getClass().getName(), classDetails); for (MethodMetadata classMethod : classMemberDetails.getMethods()) { if (methodMatch(classMethod, method)) { pushedElements.addAll(pushInMethod(klass, classMethod, writeOnDisk)); methodExists = true; } } Validate.isTrue(methodExists, String.format( "ERROR: No methods found on class '%s' that matches with '%s' expression.", klass.getSimpleTypeName(), method)); } else { // If method is not specified, push-in entire class elements pushedElements.addAll(pushInClass(klass, writeOnDisk, true)); } } else if (specifiedPackage != null && method != null) { // Check method on specified package boolean methodExists = false; for (JavaType declaredType : allDeclaredTypes) { // Check only classes on specified package if (declaredType.getPackage().equals(specifiedPackage)) { ClassOrInterfaceTypeDetails classDetails = getTypeLocationService().getTypeDetails(declaredType); MemberDetails classMemberDetails = getMemberDetailsScanner().getMemberDetails(getClass().getName(), classDetails); for (MethodMetadata classMethod : classMemberDetails.getMethods()) { if (methodMatch(classMethod, method)) { pushedElements.addAll(pushInMethod(declaredType, classMethod, writeOnDisk)); methodExists = true; } } } } Validate.isTrue(methodExists, String.format( "ERROR: No methods found on package '%s' that matches with '%s' expression.", specifiedPackage.getFullyQualifiedPackageName(), method)); } else if (method != null) { // Check that exists some method that match with provided method boolean methodExists = false; for (JavaType declaredType : allDeclaredTypes) { ClassOrInterfaceTypeDetails classDetails = getTypeLocationService().getTypeDetails(declaredType); MemberDetails classMemberDetails = getMemberDetailsScanner().getMemberDetails(getClass().getName(), classDetails); for (MethodMetadata classMethod : classMemberDetails.getMethods()) { if (methodMatch(classMethod, method)) { pushedElements.addAll(pushInMethod(declaredType, classMethod, writeOnDisk)); methodExists = true; } } // Scan and update files status getFileManager().scan(); } Validate.isTrue(methodExists, String.format( "ERROR: No methods found on entire project that matches with '%s' expression.", method)); } else if (specifiedPackage != null) { for (JavaType declaredType : allDeclaredTypes) { if (declaredType.getPackage().equals(specifiedPackage)) { pushedElements.addAll(pushInClass(declaredType, writeOnDisk, true)); } } } else { LOGGER.log(Level.WARNING, "ERROR: You must specify at least one parameter. "); } return pushedElements; } /** * Makes push-in of all items defined on a provided class * * @param klass * class to make the push-in operation * @param weiteOnDisk * indicates if pushed elements should be writed on .java file * @param force * if some operation will produce several changes, this parameter * should be true. * * @return list of objects with all the pushed elements. */ public List<Object> pushInClass(JavaType klass, boolean writeOnDisk, boolean force) { List<Object> pushedElements = new ArrayList<Object>(); // Check if current klass exists Validate .notNull(klass, "ERROR: You must specify a valid class to continue with push-in action"); // Getting class details ClassOrInterfaceTypeDetails classDetails = getTypeLocationService().getTypeDetails(klass); Validate .notNull(klass, "ERROR: You must specify a valid class to continue with push-in action"); // String builder where changes will be registered StringBuilder changesToApply = new StringBuilder(); // Getting member details MemberDetails memberDetails = getMemberDetailsScanner().getMemberDetails(getClass().getName(), classDetails); List<MemberHoldingTypeDetails> memberHoldingTypes = memberDetails.getDetails(); // Return if the class has not associated ITD's if (memberHoldingTypes.size() == 1 && memberHoldingTypes.get(0).getPhysicalTypeCategory() != PhysicalTypeCategory.ITD) { return pushedElements; } // Check if the provided class is a test to be able to select valid // class path Path path = classDetails.getAnnotation(RooJavaType.ROO_JPA_UNIT_TEST) == null ? Path.SRC_MAIN_JAVA : Path.SRC_TEST_JAVA; // Getting current class .java file metadata ID final String declaredByMetadataId = PhysicalTypeIdentifier.createIdentifier(klass, getPathResolver().getPath(klass.getModule(), path)); // Getting detailsBuilder ClassOrInterfaceTypeDetailsBuilder detailsBuilder = new ClassOrInterfaceTypeDetailsBuilder(classDetails); // Getting all details for (final MemberHoldingTypeDetails memberHoldingTypeDetails : memberDetails.getDetails()) { // Prevent that details from inheritance classes could be include on // this .java file if (!memberHoldingTypeDetails.getType().equals(classDetails.getType())) { continue; } // Getting all declared methods (including declared on ITDs // and .java files) List<MethodMetadata> allDeclaredMethods = memberHoldingTypeDetails.getMethods(); // Checking if is necessary to make push-in for all declared methods for (MethodMetadata method : allDeclaredMethods) { // If method exists on .aj file, add it! if (method.getDeclaredByMetadataId().split("\\?").length > 1 && method.getDeclaredByMetadataId().split("\\?")[1].equals(klass .getFullyQualifiedTypeName()) && !method.getDeclaredByMetadataId().equals(declaredByMetadataId)) { // Add method to .java file MethodMetadata newMethod = getNewMethod(declaredByMetadataId, method); detailsBuilder.addMethod(newMethod); // Save changes on pushed elements list pushedElements.add(newMethod); changesToApply.append(String.format("Method '%s' will be pushed on '%s.java' class. \n", method.getMethodName(), klass.getSimpleTypeName())); } } // Getting all declared fields (including declared on ITDs // and .java files) List<? extends FieldMetadata> allDeclaredFields = memberHoldingTypeDetails.getDeclaredFields(); // Checking if is necessary to make push-in for all declared fields for (FieldMetadata field : allDeclaredFields) { // If field exists on .aj file, add it! if (field.getDeclaredByMetadataId().split("\\?").length > 1 && field.getDeclaredByMetadataId().split("\\?")[1].equals(klass .getFullyQualifiedTypeName()) && !field.getDeclaredByMetadataId().equals(declaredByMetadataId)) { // Add field to .java file FieldMetadata newField = getNewField(declaredByMetadataId, field); detailsBuilder.addField(newField); // Save changes on pushed elements list pushedElements.add(newField); changesToApply.append(String.format("Field '%s' will be pushed on '%s.java' class. \n", field.getFieldName(), klass.getSimpleTypeName())); } } // Getting all declared constructors (including declared on ITDs and // .java files) List<? extends ConstructorMetadata> allDeclaredConstructors = memberHoldingTypeDetails.getDeclaredConstructors(); // Checking if is necessary to make push-in for all declared // constructors for (ConstructorMetadata constructor : allDeclaredConstructors) { // Check if current constructor exists on .java file classDetails = getTypeLocationService().getTypeDetails(detailsBuilder.build().getType()); List<JavaType> parameterTypes = new ArrayList<JavaType>(); for (AnnotatedJavaType type : constructor.getParameterTypes()) { parameterTypes.add(type.getJavaType()); } ConstructorMetadata javaDeclaredConstructor = classDetails.getDeclaredConstructor(parameterTypes); // If not exists, add it! if (javaDeclaredConstructor == null) { // Add constructor to .java file detailsBuilder.addConstructor(constructor); // Save changes on pushed elements list pushedElements.add(constructor); String constructorParametersNames = ""; for (JavaSymbolName paramName : constructor.getParameterNames()) { constructorParametersNames = constructorParametersNames.concat(paramName.getSymbolName()).concat(", "); changesToApply.append(String.format( "Constructor with parameters '%s' will be pushed on '%s.java' class. \n", constructorParametersNames.substring(0, constructorParametersNames.length() - 2), klass.getSimpleTypeName())); } } } // Getting all declared annotations (including declared on ITDs // and .java files) List<AnnotationMetadata> allDeclaredAnnotations = memberHoldingTypeDetails.getAnnotations(); for (AnnotationMetadata annotation : allDeclaredAnnotations) { // Check if current annotation exists on .java file classDetails = getTypeLocationService().getTypeDetails(detailsBuilder.build().getType()); List<AnnotationMetadata> javaDeclaredAnnotations = classDetails.getAnnotations(); boolean annotationExists = false; for (AnnotationMetadata javaAnnotation : javaDeclaredAnnotations) { if (javaAnnotation.getAnnotationType().getFullyQualifiedTypeName() .equals(annotation.getAnnotationType().getFullyQualifiedTypeName())) { annotationExists = true; } } // If not exists, add it! if (!annotationExists) { // Add annotation to .java file detailsBuilder.addAnnotation(annotation); // Save changes on pushed elements list pushedElements.add(annotation); changesToApply.append(String.format( "Annotation '%s' will be pushed on '%s.java' class. \n", annotation .getAnnotationType().getSimpleTypeName(), klass.getSimpleTypeName())); } } // Getting all extends registered on .aj file to move to .java file List<JavaType> allExtendsTypes = memberHoldingTypeDetails.getExtendsTypes(); for (JavaType extendsType : allExtendsTypes) { // If extends exists on .aj file, add it! if (!detailsBuilder.getExtendsTypes().contains(extendsType)) { detailsBuilder.addExtendsTypes(extendsType); // Save changes on pushed elements list pushedElements.add(extendsType); changesToApply.append(String.format( "Extends type '%s' will be pushed on '%s.java' class. \n", extendsType.getSimpleTypeName(), klass.getSimpleTypeName())); } } // Getting all implements registered on .aj file to move to .java // file List<JavaType> allImplementsTypes = memberHoldingTypeDetails.getImplementsTypes(); for (JavaType implementsType : allImplementsTypes) { if (!detailsBuilder.getImplementsTypes().contains(implementsType)) { detailsBuilder.addImplementsType(implementsType); // Save changes on pushed elements list pushedElements.add(implementsType); changesToApply.append(String.format( "Implements type '%s' will be pushed on '%s.java' class. \n", implementsType.getSimpleTypeName(), klass.getSimpleTypeName())); } } // Getting all imports registered on .aj file to move to .java file Set<ImportMetadata> allRegisteredImports = memberHoldingTypeDetails.getImports(); detailsBuilder.addImports(allRegisteredImports); // Save changes on pushed elements list pushedElements.add(allRegisteredImports); } // Updating .java file if (!force) { // Show message to be able to know which changes will be applied if (changesToApply.length() > 0) { LOGGER.log(Level.INFO, changesToApply.toString()); } } else if (writeOnDisk) { getTypeManagementService().createOrUpdateTypeOnDisk(detailsBuilder.build()); } return pushedElements; } /** * Makes push-in of a method defined on a provided class * * @param klass * class to make the push-in operation * @param weiteOnDisk * indicates if pushed elements should be writed on .java file * * @return list of objects with all the pushed elements. */ public List<Object> pushInMethod(JavaType klass, MethodMetadata method, boolean writeOnDisk) { List<Object> pushedElements = new ArrayList<Object>(); // Check if current klass exists Validate .notNull(klass, "ERROR: You must specify a valid class to continue with push-in action"); // Getting class details ClassOrInterfaceTypeDetails classDetails = getTypeLocationService().getTypeDetails(klass); Validate .notNull(klass, "ERROR: You must specify a valid class to continue with push-in action"); Validate.notNull(method, "ERROR: You must provide a valid method"); // Getting member details MemberDetails memberDetails = getMemberDetailsScanner().getMemberDetails(getClass().getName(), classDetails); // Check if the provided class is a test to be able to select valid // class path Path path = classDetails.getAnnotation(RooJavaType.ROO_JPA_UNIT_TEST) == null ? Path.SRC_MAIN_JAVA : Path.SRC_TEST_JAVA; // Getting current class .java file metadata ID final String declaredByMetadataId = PhysicalTypeIdentifier.createIdentifier(klass, getPathResolver().getPath(klass.getModule(), path)); // Getting detailsBuilder ClassOrInterfaceTypeDetailsBuilder detailsBuilder = new ClassOrInterfaceTypeDetailsBuilder(classDetails); // Getting all details for (final MemberHoldingTypeDetails memberHoldingTypeDetails : memberDetails.getDetails()) { // Prevent that details from inheritance classes could be include on // this .java file if (!memberHoldingTypeDetails.getType().equals(classDetails.getType())) { continue; } // Getting all declared methods (including declared on ITDs // and .java files) List<MethodMetadata> allDeclaredMethods = memberHoldingTypeDetails.getMethods(); // Checking if is necessary to make push-in for all declared methods for (MethodMetadata declaredMethod : allDeclaredMethods) { // If method exists on .aj file, add it! if (!method.getDeclaredByMetadataId().equals(declaredByMetadataId) && declaredMethod.equals(method)) { // Add method to .java file MethodMetadata newMethod = getNewMethod(declaredByMetadataId, method); detailsBuilder.addMethod(newMethod); // Save changes to be pushed pushedElements.add(newMethod); } } // Getting all imports registered on .aj file to move to .java file Set<ImportMetadata> allRegisteredImports = memberHoldingTypeDetails.getImports(); detailsBuilder.addImports(allRegisteredImports); // Save imports to be pushed only if some method has been pushed if (!pushedElements.isEmpty()) { pushedElements.addAll(allRegisteredImports); } } // Updating .java file if write on disdk if (writeOnDisk) { getTypeManagementService().createOrUpdateTypeOnDisk(detailsBuilder.build()); } return pushedElements; } /** * Method that obtains all declared fields and returns a list with * the private ones. * * @param memberDetails * @return list with the private fields */ private List<? extends FieldMetadata> getPrivateFields(MemberDetails memberDetails) { List<FieldMetadata> privateFields = new ArrayList<FieldMetadata>(); // Checking all registered fields in ITDs and .java files for (FieldMetadata field : memberDetails.getFields()) { if (field.getModifier() == Modifier.PRIVATE || field.getModifier() == Modifier.PRIVATE + Modifier.FINAL) { privateFields.add(field); } } return privateFields; } /** * This method generates new method instance using an existing * methodMetadata * * @param declaredByMetadataId * @param method * * @return */ private MethodMetadata getNewMethod(String declaredByMetadataId, MethodMetadata method) { // Create bodyBuilder InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); bodyBuilder.appendFormalLine(method.getBody()); // Use the MethodMetadataBuilder for easy creation of MethodMetadata // based on existing method MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder(declaredByMetadataId, method.getModifier(), method.getMethodName(), method.getReturnType(), method.getParameterTypes(), method.getParameterNames(), bodyBuilder); methodBuilder.setAnnotations(method.getAnnotations()); // ROO-3834: Including default comment structure during push-in methodBuilder.setCommentStructure(method.getCommentStructure()); return methodBuilder.build(); } /** * This method generates new field instance using an existing FieldMetadata * * @param declaredByMetadataId * @param field * * @return */ private FieldMetadata getNewField(String declaredByMetadataId, FieldMetadata field) { // Use the FieldMetadataBuilder for easy creation of FieldMetadata // based on existing field FieldMetadataBuilder fieldBuilder = new FieldMetadataBuilder(declaredByMetadataId, field.getModifier(), field.getFieldName(), field.getFieldType(), field.getFieldInitializer()); fieldBuilder.setAnnotations(field.getAnnotations()); return fieldBuilder.build(); } /** * This method checks if the provided methodName matches with the provided * regular expression * * @param methodName * @param regEx * @return */ private boolean methodMatch(MethodMetadata method, String regEx) { // Create regular expression using provided text Pattern pattern = Pattern.compile(regEx); Matcher matcher = pattern.matcher(method.getMethodName().getSymbolName()); boolean matches = matcher.matches(); // If not matches, maybe is not a regEx, so is necessary to check it // manually if (!matches && regEx.split("\\(").length > 1) { // Getting method name and parameter types String name = regEx.split("\\(")[0]; String[] parameterTypes = regEx.split("\\(")[1].replaceAll("\\)", "").split(","); // Prevent errors with empty regular expressions if (StringUtils.isEmpty(name)) { return false; } if (method.getMethodName().equals(new JavaSymbolName(name))) { List<AnnotatedJavaType> methodParams = method.getParameterTypes(); boolean sameParameterTypes = false; if (methodParams.size() == parameterTypes.length) { sameParameterTypes = true; for (int i = 0; i < methodParams.size(); i++) { if (!methodParams.get(i).getJavaType().getSimpleTypeName().equals(parameterTypes[i])) { sameParameterTypes = false; break; } } } // If the same method as provided, return true if (sameParameterTypes) { return true; } } } return matches; } /** * Method to obtain ProjectOperation service implementation * * @return */ public ProjectOperations getProjectOperations() { return serviceManager.getServiceInstance(this, ProjectOperations.class); } /** * Method to obtain TypeLocationService service implementation * * @return */ public TypeLocationService getTypeLocationService() { return serviceManager.getServiceInstance(this, TypeLocationService.class); } /** * Method to obtain MemberDetailsScanner service implementation * * @return */ public MemberDetailsScanner getMemberDetailsScanner() { return serviceManager.getServiceInstance(this, MemberDetailsScanner.class); } /** * Method to obtain TypeManagementService service implementation * * @return */ public TypeManagementService getTypeManagementService() { return serviceManager.getServiceInstance(this, TypeManagementService.class); } /** * Method to obtain PathResolver service implementation * * @return */ public PathResolver getPathResolver() { return serviceManager.getServiceInstance(this, PathResolver.class); } /** * Method to obtain FileManager service implementation * * @return */ public FileManager getFileManager() { return serviceManager.getServiceInstance(this, FileManager.class); } }