package org.springframework.roo.classpath; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; 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.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.Reference; import org.apache.felix.scr.annotations.Service; import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; import org.springframework.roo.file.monitor.FileMonitorService; import org.springframework.roo.file.monitor.event.FileDetails; import org.springframework.roo.metadata.MetadataIdentificationUtils; import org.springframework.roo.metadata.MetadataService; import org.springframework.roo.model.JavaPackage; import org.springframework.roo.model.JavaSymbolName; import org.springframework.roo.model.JavaType; import org.springframework.roo.process.manager.FileManager; import org.springframework.roo.project.LogicalPath; import org.springframework.roo.project.PhysicalPath; import org.springframework.roo.project.ProjectOperations; import org.springframework.roo.project.maven.Pom; import org.springframework.roo.shell.NaturalOrderComparator; import org.springframework.roo.support.util.FileUtils; /** * Implementation of {@link TypeLocationService}. * <p> * For performance reasons automatically caches the queries. The cache is * invalidated on changes to the file system. * * @author Alan Stewart * @author Ben Alex * @author Stefan Schmidt * @author James Tyrrell * @since 1.1 */ @Component(immediate = true) @Service public class TypeLocationServiceImpl implements TypeLocationService { private static final Comparator<String> LENGTH_COMPARATOR = new Comparator<String>() { public int compare(final String key1, final String key2) { return Integer.valueOf(key1.length()).compareTo(key2.length()); } }; private static final String JAVA_FILES_ANT_PATH = "**" + File.separatorChar + "*.java"; /** * Returns all packages leading up to the given package, e.g. if the given * package is "com.foo.bar", returns ["com", "com.foo", "com.foo.bar"]. * * @param leafPackage the fully-qualified package to parse (required) * @return a non-<code>null</code> iterable */ static Set<String> getAllPackages(final String leafPackage) { final Set<String> discoveredPackages = new HashSet<String>(); final String[] typeSegments = leafPackage.split("\\."); final StringBuilder sb = new StringBuilder(); for (int i = 0; i < typeSegments.length; i++) { final String typeSegment = typeSegments[i]; if (i > 0) { sb.append("."); } sb.append(typeSegment); discoveredPackages.add(sb.toString()); } return discoveredPackages; } /** * Returns the lowest-level package that contains the given number of types. * * @param typeCount the number of types to look for * @param typesByPackage maps package names to type names; a given type will * appear under all of its parent packages (required) * @return <code>null</code> if no package contains the given number of * types */ static String getLowestCommonPackage(final int typeCount, final Map<String, Collection<String>> typesByPackage) { final Map<String, Collection<String>> typesBySortedPackage = sortByKeyLength(typesByPackage); int longestPackage = 0; String topLevelPackage = null; for (final Entry<String, Collection<String>> entry : typesBySortedPackage .entrySet()) { final String thisPackage = entry.getKey(); final Collection<String> typesInPackage = entry.getValue(); if (typesInPackage.size() == typeCount && thisPackage.length() > longestPackage) { longestPackage = thisPackage.length(); topLevelPackage = thisPackage; } } return topLevelPackage; } static String getPackageFromType(final String typeName) { return typeName.substring(0, typeName.lastIndexOf('.')); } /** * Sorts the given string-keyed map by the length of its keys. * * @param <V> the type of the map's values * @param mapToSort the map to sort (not changed) * @return the sorted version of the map */ static <V> Map<String, V> sortByKeyLength(final Map<String, V> mapToSort) { final List<String> keys = new ArrayList<String>(mapToSort.keySet()); Collections.sort(keys, LENGTH_COMPARATOR); final Map<String, V> sortedMap = new LinkedHashMap<String, V>(); for (final String key : keys) { sortedMap.put(key, mapToSort.get(key)); } return sortedMap; } @Reference private FileManager fileManager; @Reference private FileMonitorService fileMonitorService; @Reference private MetadataService metadataService; @Reference private ProjectOperations projectOperations; @Reference private TypeCache typeCache; @Reference private TypeResolutionService typeResolutionService; private final Map<JavaType, Set<String>> annotationToMidMap = new HashMap<JavaType, Set<String>>(); private final Map<String, Set<String>> changeMap = new HashMap<String, Set<String>>(); private final Set<String> dirtyFiles = new HashSet<String>(); private final Set<String> discoveredTypes = new HashSet<String>(); private final Map<String, Set<Object>> typeCustomDataMap = new HashMap<String, Set<Object>>(); private final Map<Object, Set<String>> tagToMidMap = new HashMap<Object, Set<String>>(); private final Map<String, Set<JavaType>> typeAnnotationMap = new HashMap<String, Set<JavaType>>(); private void cacheType(final String fileCanonicalPath) { Validate.notBlank(fileCanonicalPath, "File canonical path required"); if (doesPathIndicateJavaType(fileCanonicalPath)) { final String id = getPhysicalTypeIdentifier(fileCanonicalPath); if (id != null && PhysicalTypeIdentifier.isValid(id)) { // Change to Java, so drop the cache final ClassOrInterfaceTypeDetails cid = lookupClassOrInterfaceTypeDetails(id); if (cid == null) { if (!fileManager.exists(fileCanonicalPath)) { typeCache.removeType(id); final JavaType type = typeCache.getTypeDetails(id) .getName(); updateChanges(type.getFullyQualifiedTypeName(), true); } return; } typeCache.cacheType(fileCanonicalPath, cid); updateAttributeCache(cid); updateChanges(cid.getName().getFullyQualifiedTypeName(), false); } } } private Set<String> discoverTypes() { // Retrieve a list of paths that have been discovered or modified since // the last invocation by this class for (final String change : fileMonitorService .getDirtyFiles(TypeLocationServiceImpl.class.getName())) { if (doesPathIndicateJavaType(change)) { discoveredTypes.add(change); dirtyFiles.add(change); } } return discoveredTypes; } private boolean doesPathIndicateJavaType(final String fileCanonicalPath) { Validate.notBlank(fileCanonicalPath, "File canonical path required"); return fileCanonicalPath.endsWith(".java") && !fileCanonicalPath.endsWith("package-info.java") && JavaSymbolName .isLegalJavaName(getProposedJavaType(fileCanonicalPath)); } public Set<ClassOrInterfaceTypeDetails> findClassesOrInterfaceDetailsWithAnnotation( final JavaType... annotationsToDetect) { final List<ClassOrInterfaceTypeDetails> types = new ArrayList<ClassOrInterfaceTypeDetails>(); processTypesWithAnnotation(Arrays.asList(annotationsToDetect), new LocatedTypeCallback() { public void process( final ClassOrInterfaceTypeDetails located) { if (located != null) { types.add(located); } } }); Collections.sort(types, new NaturalOrderComparator<ClassOrInterfaceTypeDetails>() { @Override protected String stringify( final ClassOrInterfaceTypeDetails object) { return object.getName().getSimpleTypeName(); } }); return Collections .unmodifiableSet(new LinkedHashSet<ClassOrInterfaceTypeDetails>( types)); } public Set<ClassOrInterfaceTypeDetails> findClassesOrInterfaceDetailsWithTag( final Object tag) { Validate.notNull(tag, "Tag required"); final Set<ClassOrInterfaceTypeDetails> types = new LinkedHashSet<ClassOrInterfaceTypeDetails>(); processTypesWithTag(tag, new LocatedTypeCallback() { public void process(final ClassOrInterfaceTypeDetails located) { if (located != null) { types.add(located); } } }); return Collections.unmodifiableSet(types); } public Set<JavaType> findTypesWithAnnotation( final JavaType... annotationsToDetect) { return findTypesWithAnnotation(Arrays.asList(annotationsToDetect)); } public Set<JavaType> findTypesWithAnnotation( final List<JavaType> annotationsToDetect) { Validate.notNull(annotationsToDetect, "Annotations to detect required"); final Set<JavaType> types = new LinkedHashSet<JavaType>(); processTypesWithAnnotation(annotationsToDetect, new LocatedTypeCallback() { public void process( final ClassOrInterfaceTypeDetails located) { if (located != null) { types.add(located.getName()); } } }); return Collections.unmodifiableSet(types); } private String getParentPath(final JavaType javaType) { final String relativePath = javaType.getRelativeFileName(); for (final String typePath : discoverTypes()) { if (typePath.endsWith(relativePath)) { return StringUtils.removeEnd(typePath, relativePath); } } return null; } private PhysicalPath getPhysicalPath(final JavaType javaType) { Validate.notNull(javaType, "Java type required"); final String parentPath = getParentPath(javaType); if (parentPath == null) { return null; } for (final Pom pom : projectOperations.getPoms()) { for (final PhysicalPath physicalPath : pom.getPhysicalPaths()) { if (physicalPath.isSource()) { final String pathLocation = FileUtils .ensureTrailingSeparator(physicalPath .getLocationPath()); if (pathLocation.startsWith(parentPath)) { typeCache.cacheTypeAgainstModule(pom, javaType); return physicalPath; } } } } return null; } public String getPhysicalTypeCanonicalPath(final JavaType javaType, final LogicalPath path) { return getPhysicalTypeCanonicalPath(PhysicalTypeIdentifier .createIdentifier(javaType, path)); } public String getPhysicalTypeCanonicalPath(final String physicalTypeId) { final LogicalPath logicalPath = PhysicalTypeIdentifier .getPath(physicalTypeId); final JavaType javaType = PhysicalTypeIdentifier .getJavaType(physicalTypeId); final Pom pom = projectOperations.getPomFromModuleName(logicalPath .getModule()); final String canonicalFilePath = pom.getPathLocation(logicalPath .getPath()) + javaType.getRelativeFileName(); if (fileManager.exists(canonicalFilePath)) { typeCache.cacheTypeAgainstModule(pom, javaType); typeCache.cacheFilePathAgainstTypeIdentifier(canonicalFilePath, physicalTypeId); } return canonicalFilePath; } public String getPhysicalTypeIdentifier(final JavaType type) { final PhysicalPath containingPhysicalPath = getPhysicalPath(type); if (containingPhysicalPath == null) { return null; } // N.B. as a side-effect, we make the currently focused module depend on // the given type's module final LogicalPath logicalPath = containingPhysicalPath.getLogicalPath(); projectOperations.addModuleDependency(logicalPath.getModule()); return PhysicalTypeIdentifier.createIdentifier(type, logicalPath); } public String getPhysicalTypeIdentifier(final String fileCanonicalPath) { Validate.notBlank(fileCanonicalPath, "File canonical path required"); if (!doesPathIndicateJavaType(fileCanonicalPath)) { return null; } String physicalTypeIdentifier = typeCache .getTypeIdFromTypeFilePath(fileCanonicalPath); if (physicalTypeIdentifier != null) { return physicalTypeIdentifier; } final String typeDirectory = FileUtils .getFirstDirectory(fileCanonicalPath); final String simpleTypeName = StringUtils.replace(fileCanonicalPath, typeDirectory + File.separator, "", 1).replace(".java", ""); final JavaPackage javaPackage = typeResolutionService .getPackage(fileCanonicalPath); if (javaPackage == null) { return null; } final JavaType javaType = new JavaType( javaPackage.getFullyQualifiedPackageName() + "." + simpleTypeName); final Pom module = projectOperations .getModuleForFileIdentifier(fileCanonicalPath); Validate.notNull(module, "The module for the file '" + fileCanonicalPath + "' could not be located"); typeCache.cacheTypeAgainstModule(module, javaType); String reducedPath = fileCanonicalPath.replace( javaType.getRelativeFileName(), ""); reducedPath = StringUtils.stripEnd(reducedPath, File.separator); for (final PhysicalPath physicalPath : module.getPhysicalPaths()) { if (physicalPath.getLocationPath().startsWith(reducedPath)) { final LogicalPath path = physicalPath.getLogicalPath(); physicalTypeIdentifier = MetadataIdentificationUtils.create( PhysicalTypeIdentifier.class.getName(), path.getName() + "?" + javaType.getFullyQualifiedTypeName()); break; } } typeCache.cacheFilePathAgainstTypeIdentifier(fileCanonicalPath, physicalTypeIdentifier); return physicalTypeIdentifier; } public List<String> getPotentialTopLevelPackagesForModule(final Pom module) { Validate.notNull(module, "Module required"); final Map<String, Set<String>> packageMap = new HashMap<String, Set<String>>(); final Set<String> moduleTypes = getTypesForModule(module.getPath()); final List<String> topLevelPackages = new ArrayList<String>(); if (moduleTypes.isEmpty()) { topLevelPackages.add(module.getGroupId()); return topLevelPackages; } for (final String typeName : moduleTypes) { final StringBuilder sb = new StringBuilder(); final String type = getPackageFromType(typeName); final String[] typeSegments = type.split("\\."); final Set<String> discoveredPackages = new HashSet<String>(); for (int i = 0; i < typeSegments.length; i++) { final String typeSegment = typeSegments[i]; if (i > 0) { sb.append("."); } sb.append(typeSegment); discoveredPackages.add(sb.toString()); } for (final String discoveredPackage : discoveredPackages) { if (!packageMap.containsKey(discoveredPackage)) { packageMap.put(discoveredPackage, new HashSet<String>()); } packageMap.get(discoveredPackage).add(typeName); } } int longestPackage = 0; for (final Map.Entry<String, Set<String>> entry : packageMap.entrySet()) { if (entry.getValue().size() == moduleTypes.size()) { topLevelPackages.add(entry.getKey()); if (entry.getKey().length() > longestPackage) { longestPackage = entry.getKey().length(); } } } return topLevelPackages; } private String getProposedJavaType(final String fileCanonicalPath) { Validate.notBlank(fileCanonicalPath, "File canonical path required"); // Determine the JavaType for this file String relativePath = ""; final Pom moduleForFileIdentifier = projectOperations .getModuleForFileIdentifier(fileCanonicalPath); if (moduleForFileIdentifier == null) { return relativePath; } for (final PhysicalPath physicalPath : moduleForFileIdentifier .getPhysicalPaths()) { final String moduleCanonicalPath = FileUtils .ensureTrailingSeparator(FileUtils .getCanonicalPath(physicalPath.getLocation())); if (fileCanonicalPath.startsWith(moduleCanonicalPath)) { relativePath = File.separator + StringUtils.replace(fileCanonicalPath, moduleCanonicalPath, "", 1); break; } } Validate.notBlank(relativePath, "Could not determine compilation unit name for file '" + fileCanonicalPath + "'"); Validate.isTrue(relativePath.startsWith(File.separator), "Relative path unexpectedly dropped the '" + File.separator + "' prefix (received '" + relativePath + "' from '" + fileCanonicalPath + "'"); relativePath = relativePath.substring(1); Validate.isTrue(relativePath.endsWith(".java"), "The relative path unexpectedly dropped the .java extension for file '" + fileCanonicalPath + "'"); relativePath = relativePath.substring(0, relativePath.lastIndexOf(".java")); return relativePath.replace(File.separatorChar, '.'); } public String getTopLevelPackageForModule(final Pom module) { return module.getGroupId(); } /** * Builds a map of a module's packages and all types anywhere within them, * e.g. if there's two types com.foo.bar.A and com.foo.baz.B, the map will * look like this: "com": {"com.foo.bar.A", "com.foo.baz.B"} "com.foo": * {"com.foo.bar.A", "com.foo.baz.B"}, "com.foo.bar": {"com.foo.bar.A"}, * "com.foo.baz": {"com.foo.baz.B"} All packages that directly contain a * type are added to the given set. * * @param typesInModule the Java types within the module in question * @param typePackages the set to which to add each type's package * @return a non-<code>null</code> map laid out as above */ private Map<String, Collection<String>> getTypesByPackage( final Iterable<String> typesInModule, final Set<String> typePackages) { final Map<String, Collection<String>> typesByPackage = new HashMap<String, Collection<String>>(); for (final String typeName : typesInModule) { final String typePackage = getPackageFromType(typeName); typePackages.add(typePackage); for (final String discoveredPackage : getAllPackages(typePackage)) { if (typesByPackage.get(discoveredPackage) == null) { typesByPackage .put(discoveredPackage, new HashSet<String>()); } typesByPackage.get(discoveredPackage).add(typeName); } } return typesByPackage; } public ClassOrInterfaceTypeDetails getTypeDetails(final JavaType type) { return getTypeDetails(getPhysicalTypeIdentifier(type)); } // -------------------------- Private methods ------------------------------ public ClassOrInterfaceTypeDetails getTypeDetails( final String physicalTypeId) { if (StringUtils.isBlank(physicalTypeId)) { return null; } Validate.isTrue(PhysicalTypeIdentifier.isValid(physicalTypeId), "Metadata id '" + physicalTypeId + "' is not a valid physical type id"); updateTypeCache(); final ClassOrInterfaceTypeDetails cachedDetails = typeCache .getTypeDetails(physicalTypeId); if (cachedDetails != null) { return cachedDetails; } final PhysicalTypeMetadata physicalTypeMetadata = (PhysicalTypeMetadata) metadataService .get(physicalTypeId); if (physicalTypeMetadata == null) { return null; } return physicalTypeMetadata.getMemberHoldingTypeDetails(); } public LogicalPath getTypePath(final JavaType javaType) { final String physicalTypeId = getPhysicalTypeIdentifier(javaType); if (StringUtils.isBlank(physicalTypeId)) { return null; } return PhysicalTypeIdentifier.getPath(physicalTypeId); } public Collection<JavaType> getTypesForModule(final Pom module) { if ("pom".equals(module.getPackaging())) { return Collections.emptySet(); } final Set<String> typeNames = getTypesForModule(module.getPath()); final Collection<JavaType> javaTypes = new ArrayList<JavaType>(); for (final String typeName : typeNames) { javaTypes.add(new JavaType(typeName)); } return javaTypes; } public Set<String> getTypesForModule(final String modulePath) { Validate.notNull(modulePath, "Module path required"); return typeCache.getTypeNamesForModuleFilePath(modulePath); } public boolean hasTypeChanged(final String requestingClass, final JavaType javaType) { Validate.notNull(requestingClass, "Requesting class required"); Validate.notNull(javaType, "Java type required"); updateTypeCache(); Set<String> changesSinceLastRequest = changeMap.get(requestingClass); if (changesSinceLastRequest == null) { changesSinceLastRequest = new LinkedHashSet<String>(); for (final String typeIdentifier : typeCache .getAllTypeIdentifiers()) { changesSinceLastRequest.add(typeCache .getTypeDetails(typeIdentifier).getName() .getFullyQualifiedTypeName()); } changeMap.put(requestingClass, changesSinceLastRequest); } for (final String changedId : changesSinceLastRequest) { if (changedId.equals(javaType.getFullyQualifiedTypeName())) { changesSinceLastRequest.remove(changedId); return true; } } return false; } private void initTypeMap() { for (final Pom pom : projectOperations.getPoms()) { for (final PhysicalPath path : pom.getPhysicalPaths()) { if (path.isSource()) { final String allJavaFiles = FileUtils .ensureTrailingSeparator(path.getLocationPath()) + JAVA_FILES_ANT_PATH; for (final FileDetails file : fileManager .findMatchingAntPath(allJavaFiles)) { cacheType(file.getCanonicalPath()); } } } } } public boolean isInProject(final JavaType javaType) { return javaType != null && !javaType.isCoreType() && getPhysicalPath(javaType) != null; } /** * Obtains the a fresh copy of the {@link ClassOrInterfaceTypeDetails} for * the given physical type. * * @param physicalTypeIdentifier to lookup (required) * @return the requested details (or <code>null</code> if unavailable) */ private ClassOrInterfaceTypeDetails lookupClassOrInterfaceTypeDetails( final String physicalTypeIdentifier) { final PhysicalTypeMetadata physicalTypeMetadata = (PhysicalTypeMetadata) metadataService .evictAndGet(physicalTypeIdentifier); if (physicalTypeMetadata == null) { return null; } return physicalTypeMetadata.getMemberHoldingTypeDetails(); } public void processTypesWithAnnotation( final List<JavaType> annotationsToDetect, final LocatedTypeCallback callback) { Validate.notNull(annotationsToDetect, "Annotations to detect required"); Validate.notNull(callback, "Callback required"); // If the cache doesn't yet contain the annotation to be found it should // be added for (final JavaType annotationType : annotationsToDetect) { if (!annotationToMidMap.containsKey(annotationType)) { annotationToMidMap.put(annotationType, new HashSet<String>()); } } // Before processing the call any changes to the project should be // processed and the cache updated accordingly updateTypeCache(); for (final JavaType annotationType : annotationsToDetect) { for (final String locatedMid : annotationToMidMap .get(annotationType)) { final ClassOrInterfaceTypeDetails located = typeCache .getTypeDetails(locatedMid); callback.process(located); } } } private void processTypesWithTag(final Object tag, final LocatedTypeCallback callback) { Validate.notNull(tag, "Tag required"); Validate.notNull(callback, "Callback required"); // If the cache doesn't yet contain the tag it should be added if (!tagToMidMap.containsKey(tag)) { tagToMidMap.put(tag, new HashSet<String>()); } // Before processing the call any changes to the project should be // processed and the cache updated accordingly updateTypeCache(); for (final String locatedMid : tagToMidMap.get(tag)) { final ClassOrInterfaceTypeDetails located = typeCache .getTypeDetails(locatedMid); callback.process(located); } } private void updateAttributeCache(final MemberHoldingTypeDetails cid) { Validate.notNull(cid, "Member holding type details required"); if (!typeAnnotationMap.containsKey(cid.getDeclaredByMetadataId())) { typeAnnotationMap.put(cid.getDeclaredByMetadataId(), new HashSet<JavaType>()); } if (!typeCustomDataMap.containsKey(cid.getDeclaredByMetadataId())) { typeCustomDataMap.put(cid.getDeclaredByMetadataId(), new HashSet<Object>()); } final Set<JavaType> previousAnnotations = typeAnnotationMap.get(cid .getDeclaredByMetadataId()); for (final JavaType previousAnnotation : previousAnnotations) { final Set<String> midSet = annotationToMidMap .get(previousAnnotation); if (midSet != null) { midSet.remove(cid.getDeclaredByMetadataId()); } } previousAnnotations.clear(); for (final AnnotationMetadata annotationMetadata : cid.getAnnotations()) { if (!annotationToMidMap.containsKey(annotationMetadata .getAnnotationType())) { annotationToMidMap.put(annotationMetadata.getAnnotationType(), new HashSet<String>()); } previousAnnotations.add(annotationMetadata.getAnnotationType()); annotationToMidMap.get(annotationMetadata.getAnnotationType()).add( cid.getDeclaredByMetadataId()); } final Set<Object> previousCustomDataSet = typeCustomDataMap.get(cid .getDeclaredByMetadataId()); for (final Object previousCustomData : previousCustomDataSet) { final Set<String> midSet = tagToMidMap.get(previousCustomData); if (midSet != null) { midSet.remove(cid.getDeclaredByMetadataId()); } } previousCustomDataSet.clear(); for (final Object customData : cid.getCustomData().keySet()) { if (!tagToMidMap.containsKey(customData)) { tagToMidMap.put(customData, new HashSet<String>()); } previousCustomDataSet.add(customData); tagToMidMap.get(customData).add(cid.getDeclaredByMetadataId()); } } private void updateChanges(final String typeName, final boolean remove) { Validate.notNull(typeName, "Type name required"); for (final String requestingClass : changeMap.keySet()) { if (remove) { changeMap.get(requestingClass).remove(typeName); } else { changeMap.get(requestingClass).add(typeName); } } } private void updateTypeCache() { if (typeCache.getAllTypeIdentifiers().isEmpty()) { initTypeMap(); } discoverTypes(); // Update the type cache for (final String change : dirtyFiles) { cacheType(change); } dirtyFiles.clear(); } }