package org.springframework.roo.classpath.itd; import org.apache.commons.lang3.Validate; import org.apache.felix.scr.annotations.Component; import org.osgi.service.component.ComponentContext; import org.springframework.roo.classpath.ItdDiscoveryService; import org.springframework.roo.classpath.PhysicalTypeCategory; import org.springframework.roo.classpath.PhysicalTypeIdentifier; import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; import org.springframework.roo.classpath.PhysicalTypeMetadata; import org.springframework.roo.classpath.TypeLocationService; import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; import org.springframework.roo.classpath.details.IdentifiableJavaStructure; import org.springframework.roo.classpath.details.ItdTypeDetails; import org.springframework.roo.classpath.details.MemberHoldingTypeDetails; import org.springframework.roo.classpath.persistence.PersistenceMemberLocator; import org.springframework.roo.classpath.scanner.MemberDetails; import org.springframework.roo.classpath.scanner.MemberDetailsScanner; import org.springframework.roo.metadata.AbstractHashCodeTrackingMetadataNotifier; import org.springframework.roo.metadata.MetadataDependencyRegistry; import org.springframework.roo.metadata.MetadataIdentificationUtils; import org.springframework.roo.metadata.MetadataItem; import org.springframework.roo.metadata.MetadataNotificationListener; import org.springframework.roo.metadata.MetadataProvider; import org.springframework.roo.model.JavaType; import org.springframework.roo.process.manager.FileManager; import org.springframework.roo.project.LogicalPath; import org.springframework.roo.project.Path; import org.springframework.roo.support.logging.HandlerUtils; import java.util.ArrayList; import java.util.List; import java.util.logging.Logger; /** * Provides common functionality used by ITD-based generators. * <p> * This abstract class assumes: * <ul> * <li>There are one or more annotations which, if present on a physical type, * indicate metadata should be created (defined in {@link #metadataTriggers}</li> * <li>The default notification facilities offered by {@link MetadataProvider} * are sufficient</li> * <li>The only class-level notifications that can be processed will related to * {@link PhysicalTypeIdentifier}</li> * <li>Any instance-level notifications can be processed, but these must contain * a downstream identifier consistent with {@link #getProvidesType()}</li> * </ul> * <p> * Put differently, this abstract class assumes every ITD will have a * corresponding "governor". A "governor" is defined as the type which will * eventually receive the introduction. The abstract class assumes all metadata * identification strings represent the name of the governor, albeit with a * metadata class specific to the add-on. When an instance-specific metadata * identification request is received, the governor will be obtained and in turn * introspected for one of the trigger annotations. If these are detected, or if * there is already an ITD file of the same name as would normally be created * had a trigger annotation been found, the metadata will be created. The * metadata creation method is expected to create, update or delete the ITD file * as appropriate. * * @author Ben Alex * @since 1.0 */ @Component(componentAbstract = true) public abstract class AbstractItdMetadataProvider extends AbstractHashCodeTrackingMetadataNotifier implements ItdTriggerBasedMetadataProvider, MetadataNotificationListener { protected final static Logger LOGGER = HandlerUtils.getLogger(AbstractItdMetadataProvider.class); protected void activate(final ComponentContext cContext) { super.activate(cContext); } /** * Requires the governor to be a {@link PhysicalTypeCategory#CLASS} (as * opposed to an interface etc) */ // TODO change the type of this field to PhysicalTypeCategory and allow // subclasses to pass it via a new constructor private boolean dependsOnGovernorBeingAClass = true; /** * Cancel production if the governor type details are required, but aren't * available */ private boolean dependsOnGovernorTypeDetailAvailability = true; /** We don't care about trigger annotations; we always produce metadata */ private boolean ignoreTriggerAnnotations = false; /** * The annotations which, if present on a class or interface, will cause * metadata to be created */ private final List<JavaType> metadataTriggers = new ArrayList<JavaType>(); /** * Registers an additional {@link JavaType} that will trigger metadata * registration. * * @param javaType the type-level annotation to detect that will cause * metadata creation (required) */ public void addMetadataTrigger(final JavaType javaType) { Validate.notNull(javaType, "Java type required for metadata trigger registration"); metadataTriggers.add(javaType); } /** * Registers the given {@link JavaType}s as triggering metadata * registration. * * @param triggerTypes the type-level annotations to detect that will cause * metadata creation * @since 1.2.0 */ public void addMetadataTriggers(final JavaType... triggerTypes) { for (final JavaType triggerType : triggerTypes) { addMetadataTrigger(triggerType); } } /** * Called whenever there is a requirement to produce a local identifier (ie * an instance identifier consistent with {@link #getProvidesType()}) for * the indicated {@link JavaType} and {@link Path}. * * @param javaType the type (required) * @param path the path (required) * @return an instance-specific identifier that is compatible with * {@link #getProvidesType()} (never null or empty) */ protected abstract String createLocalIdentifier(JavaType javaType, LogicalPath path); /** * Called whenever there is a requirement to produce a local identifier (ie * an instance identifier consistent with {@link #getProvidesType()}) for * the indicated {@link ClassOrInterfaceTypeDetails}. * @param details * @return */ protected String createLocalIdentifier(ClassOrInterfaceTypeDetails details) { final LogicalPath logicalPath = PhysicalTypeIdentifier.getPath(details.getDeclaredByMetadataId()); return createLocalIdentifier(details.getType(), logicalPath); } /** * Deletes the given ITD, either now or later. * * @param metadataIdentificationString the ITD's metadata ID * @param itdFilename the ITD's filename * @param reason the reason for deletion; ignored if now is * <code>false</code> * @param now whether to delete the ITD immediately; <code>false</code> * schedules it for later deletion; this is preferable when it's * possible that the ITD might need to be re-created in the * meantime (e.g. because some ancestor metadata has changed to * that effect), otherwise there will be spurious console * messages about the ITD being deleted and created */ private void deleteItd(final String metadataIdentificationString, final String itdFilename, final String reason, final boolean now) { if (now) { getFileManager().delete(itdFilename, reason); } else { getFileManager().createOrUpdateTextFileIfRequired(itdFilename, "", false); } getItdDiscoveryService().removeItdTypeDetails(metadataIdentificationString); // TODO do we need to notify downstream dependencies that this ITD has // gone away? } public final MetadataItem get(final String metadataIdentificationString) { Validate.isTrue(MetadataIdentificationUtils.getMetadataClass(metadataIdentificationString) .equals(MetadataIdentificationUtils.getMetadataClass(getProvidesType())), "Unexpected request for '%s' to this provider (which uses '%s')", metadataIdentificationString, getProvidesType()); // Remove the upstream dependencies for this instance (we'll be // recreating them later, if needed) getMetadataDependencyRegistry().deregisterDependencies(metadataIdentificationString); // Compute the identifier for the Physical Type Metadata we're // correlated with final String governorPhysicalTypeIdentifier = getGovernorPhysicalTypeIdentifier(metadataIdentificationString); // Obtain the physical type final PhysicalTypeMetadata governorPhysicalTypeMetadata = (PhysicalTypeMetadata) getMetadataService().get(governorPhysicalTypeIdentifier); if (governorPhysicalTypeMetadata == null || !governorPhysicalTypeMetadata.isValid()) { // We can't get even basic information about the physical type, so // abort (the ITD will be deleted by ItdFileDeletionService) return null; } // Flag to indicate whether we'll even try to create this metadata boolean produceMetadata = false; // Determine if we should generate the metadata on the basis of it // containing a trigger annotation final ClassOrInterfaceTypeDetails cid = governorPhysicalTypeMetadata.getMemberHoldingTypeDetails(); if (cid != null) { // Only create metadata if the type is annotated with one of the // metadata triggers for (final JavaType trigger : metadataTriggers) { if (cid.getAnnotation(trigger) != null) { produceMetadata = true; break; } } } // Fall back to ignoring trigger annotations if (ignoreTriggerAnnotations) { produceMetadata = true; } // Cancel production if the governor type details are required, but // aren't available if (dependsOnGovernorTypeDetailAvailability && cid == null) { produceMetadata = false; } // Cancel production if the governor is not a class, and the subclass // only wants to know about classes if (cid != null && dependsOnGovernorBeingAClass && cid.getPhysicalTypeCategory() != PhysicalTypeCategory.CLASS) { produceMetadata = false; } final String itdFilename = governorPhysicalTypeMetadata.getItdCanonicalPath(this); if (!produceMetadata && isGovernor(cid) && getFileManager().exists(itdFilename)) { // We don't seem to want metadata anymore, yet the ITD physically // exists, so get rid of it // This might be because the trigger annotation has been removed, // the governor is missing a class declaration, etc. deleteItd(metadataIdentificationString, itdFilename, "not required for governor " + cid.getName(), true); return null; } if (produceMetadata) { // This type contains an annotation we were configured to detect, or // there is an ITD (which may need deletion), so we need to produce // the metadata final JavaType aspectName = governorPhysicalTypeMetadata.getItdJavaType(this); final ItdTypeDetailsProvidingMetadataItem metadata = getMetadata(metadataIdentificationString, aspectName, governorPhysicalTypeMetadata, itdFilename); // There is no requirement to register a direct connection with the // physical type and this metadata because changes will // trickle down via the class-level notification registered by // convention by AbstractItdMetadataProvider subclasses (BPA 10 Dec // 2010) if (metadata == null || !metadata.isValid()) { // The metadata couldn't be created properly deleteItd(metadataIdentificationString, itdFilename, "", false); return null; } // By this point we have a valid MetadataItem, but it might not // contain any members for the resulting ITD etc // Handle the management of the ITD file boolean deleteItdFile = false; final ItdTypeDetails itdTypeDetails = metadata.getMemberHoldingTypeDetails(); if (itdTypeDetails == null) { // The ITD has no members deleteItdFile = true; } if (!deleteItdFile) { // We have some members in the ITD, so decide if we're to write // something to disk final ItdSourceFileComposer itdSourceFileComposer = new ItdSourceFileComposer(metadata.getMemberHoldingTypeDetails()); // Decide whether the get an ITD on-disk based on whether there // is physical content to write if (itdSourceFileComposer.isContent()) { // We have content to write getItdDiscoveryService().addItdTypeDetails(itdTypeDetails); final String itd = itdSourceFileComposer.getOutput(); getFileManager().createOrUpdateTextFileIfRequired(itdFilename, itd, false); } else { // We don't have content to write deleteItdFile = true; } } if (deleteItdFile) { deleteItd(metadataIdentificationString, itdFilename, null, false); } // Eagerly notify that the metadata has been updated; this also // registers the metadata hash code in the superclass' cache to // avoid // unnecessary subsequent notifications if it hasn't changed notifyIfRequired(metadata); return metadata; } return null; } /** * Called whenever there is a requirement to convert a local metadata * identification string (ie an instance identifier consistent with * {@link #getProvidesType()}) into the corresponding governor physical type * identifier. * * @param metadataIdentificationString the local identifier (required) * @return the physical type identifier of the governor (required) */ protected abstract String getGovernorPhysicalTypeIdentifier(String metadataIdentificationString); public final String getIdForPhysicalJavaType(final String physicalJavaTypeIdentifier) { Validate.isTrue( MetadataIdentificationUtils.getMetadataClass(physicalJavaTypeIdentifier).equals( MetadataIdentificationUtils.getMetadataClass(PhysicalTypeIdentifier .getMetadataIdentiferType())), "Expected a valid physical Java type instance identifier (not '%s')", physicalJavaTypeIdentifier); final JavaType javaType = PhysicalTypeIdentifier.getJavaType(physicalJavaTypeIdentifier); final LogicalPath path = PhysicalTypeIdentifier.getPath(physicalJavaTypeIdentifier); return createLocalIdentifier(javaType, path); } /** * Assists creating a local metadata identification string (MID) from any * presented {@link MemberHoldingTypeDetails} implementation. This is * achieved by extracting the * {@link IdentifiableJavaStructure#getDeclaredByMetadataId()} and * converting it into a {@link JavaType} and {@link Path}, then calling * {@link #createLocalIdentifier(JavaType, Path)}. * * @param memberHoldingTypeDetails the member holder from which the * declaring type information should be extracted (required) * @return a MID produced by {@link #createLocalIdentifier(JavaType, Path)} * for the extracted Java type in the extract Path (never null) */ protected String getLocalMid(final MemberHoldingTypeDetails memberHoldingTypeDetails) { final JavaType governorType = memberHoldingTypeDetails.getName(); // Extract out the metadata provider class (we need this later to // extract just the Path it is located in) final String providesType = MetadataIdentificationUtils.getMetadataClass(memberHoldingTypeDetails .getDeclaredByMetadataId()); final LogicalPath path = PhysicalTypeIdentifierNamingUtils.getPath(providesType, memberHoldingTypeDetails.getDeclaredByMetadataId()); // Produce the local MID we're going to use to make the request return createLocalIdentifier(governorType, path); } /** * Returns details of the given class or interface type's members * * @param cid the physical type for which to get the members (can be * <code>null</code>) * @return <code>null</code> if the member details are unavailable */ protected MemberDetails getMemberDetails(final ClassOrInterfaceTypeDetails cid) { MemberDetailsScanner memberDetailsScanner = getMemberDetailsScanner(); Validate.notNull(memberDetailsScanner, "MemberDetailsScanner is required"); if (cid == null) { return null; } return memberDetailsScanner.getMemberDetails(getClass().getName(), cid); } /** * Returns details of the given Java type's members * * @param type the type for which to get the members (required) * @return <code>null</code> if the member details are unavailable */ protected MemberDetails getMemberDetails(final JavaType type) { TypeLocationService typeLocationService = getTypeLocationService(); Validate.notNull(typeLocationService, "TypeLocationService is required"); final String physicalTypeIdentifier = typeLocationService.getPhysicalTypeIdentifier(type); if (physicalTypeIdentifier == null) { return null; } // We need to lookup the metadata we depend on final PhysicalTypeMetadata physicalTypeMetadata = (PhysicalTypeMetadata) getMetadataService().get(physicalTypeIdentifier); return getMemberDetails(physicalTypeMetadata); } /** * Returns details of the given physical type's members * * @param physicalTypeMetadata the physical type for which to get the * members (can be <code>null</code>) * @return <code>null</code> if the member details are unavailable */ protected MemberDetails getMemberDetails(final PhysicalTypeMetadata physicalTypeMetadata) { MemberDetailsScanner memberDetailsScanner = getMemberDetailsScanner(); Validate.notNull(memberDetailsScanner, "MemberDetailsScanner is required"); // We need to abort if we couldn't find dependent metadata if (physicalTypeMetadata == null || !physicalTypeMetadata.isValid()) { return null; } final ClassOrInterfaceTypeDetails cid = physicalTypeMetadata.getMemberHoldingTypeDetails(); if (cid == null) { // Abort if the type's class details aren't available (parse error // etc) return null; } return memberDetailsScanner.getMemberDetails(getClass().getName(), cid); } /** * Called when it is time to create the actual metadata instance. * * @param metadataIdentificationString the local identifier (non-null and * consistent with {@link #getProvidesType()}) * @param aspectName the Java type name for the ITD (non-null and obtained * via * {@link PhysicalTypeMetadata#getItdJavaType(ItdMetadataProvider)} * ) * @param governorPhysicalTypeMetadata the governor metadata (non-null and * obtained via * {@link #getGovernorPhysicalTypeIdentifier(String)}) * @param itdFilename the canonical filename for the ITD (non-null and * obtained via * {@link PhysicalTypeMetadata#getItdCanoncialPath(ItdMetadataProvider)} * ) * @return the new metadata (may return null if there is a problem * processing) */ protected abstract ItdTypeDetailsProvidingMetadataItem getMetadata( String metadataIdentificationString, JavaType aspectName, PhysicalTypeMetadata governorPhysicalTypeMetadata, String itdFilename); /** * Looks up the given type's inheritance hierarchy for metadata of the given * type, starting with the given type's parent and going upwards until the * first such instance is found (i.e. lower level metadata takes priority * over higher level metadata) * * @param <T> the type of metadata to look for * @param child the child type whose parents to search (required) * @return <code>null</code> if there is no such metadata */ @SuppressWarnings("unchecked") protected <T extends MetadataItem> T getParentMetadata(final ClassOrInterfaceTypeDetails child) { T parentMetadata = null; ClassOrInterfaceTypeDetails superCid = child.getSuperclass(); while (parentMetadata == null && superCid != null) { final String superCidPhysicalTypeIdentifier = superCid.getDeclaredByMetadataId(); final LogicalPath path = PhysicalTypeIdentifier.getPath(superCidPhysicalTypeIdentifier); final String superCidLocalIdentifier = createLocalIdentifier(superCid.getName(), path); parentMetadata = (T) getMetadataService().get(superCidLocalIdentifier); superCid = superCid.getSuperclass(); } return parentMetadata; // Could be null } /** * Indicates whether the given type is the governor for this provider. This * implementation simply checks whether the given type is either a class or * an interface, based on the value of {@link #dependsOnGovernorBeingAClass} * . A more sophisticated implementation could check for the presence of * particular annotations or the implementation of particular interfaces. * * @param type can be <code>null</code> * @return <code>false</code> if the given type is <code>null</code> */ protected boolean isGovernor(final ClassOrInterfaceTypeDetails type) { if (type == null) { return false; } if (dependsOnGovernorBeingAClass) { return type.getPhysicalTypeCategory() == PhysicalTypeCategory.CLASS; } return type.getPhysicalTypeCategory() == PhysicalTypeCategory.INTERFACE; } protected boolean isIgnoreTriggerAnnotations() { return ignoreTriggerAnnotations; } private boolean isNotificationForJavaType(final String mid) { return MetadataIdentificationUtils.getMetadataClass(mid).equals( MetadataIdentificationUtils.getMetadataClass(PhysicalTypeIdentifier .getMetadataIdentiferType())); } public final void notify(final String upstreamDependency, String downstreamDependency) { if (downstreamDependency == null) { notifyForGenericListener(upstreamDependency); return; } // Handle if the downstream dependency is "class level", meaning we need // to figure out the specific downstream MID this metadata provider // wants to update/refresh. if (MetadataIdentificationUtils.isIdentifyingClass(downstreamDependency)) { // We have not identified an instance-specific downstream MID, so // we'll need to calculate an instance-specific downstream MID to // retrieve. downstreamDependency = resolveDownstreamDependencyIdentifier(upstreamDependency); // We skip if the resolution method returns null, as it doesn't want // to continue for some reason if (downstreamDependency == null) { return; } Validate.isTrue(MetadataIdentificationUtils.isIdentifyingInstance(downstreamDependency), "An instance-specific downstream MID was required by '%s' (not '%s')", getClass() .getName(), downstreamDependency); // We only need to proceed if the downstream dependency relationship // is not already registered. // It is unusual to register a direct downstream relationship given // it costs dependency registration memory and class-level // notifications will always occur anyway. if (getMetadataDependencyRegistry().getDownstream(upstreamDependency).contains( downstreamDependency)) { return; } } // We should now have an instance-specific "downstream dependency" that // can be processed by this class Validate.isTrue( MetadataIdentificationUtils.getMetadataClass(downstreamDependency).equals( MetadataIdentificationUtils.getMetadataClass(getProvidesType())), "Unexpected downstream notification for '%s' to this provider (which uses '%s')", downstreamDependency, getProvidesType()); // We no longer notify downstreams here, as the "get" operation with // eviction will ensure the main get(String) method below will be fired // and it // directly notified downstreams as part of that method (BPA 10 Dec // 2010) getMetadataService().evictAndGet(downstreamDependency); } /** * Designed to handle events originating from a * {@link MetadataDependencyRegistry#addNotificationListener(MetadataNotificationListener)} * registration. Such events are always presented with a non-null upstream * dependency indicator and a null downstream dependency indicator. These * events differ from events related to {@link PhysicalTypeIdentifier} * registrations, as in those cases the downstream dependency indicator will * be the class-level {@link #getProvidesType()}. * <p> * This method allows subclasses to specially handle generic * {@link MetadataDependencyRegistry} events. * * @param upstreamDependency the upstream which was modified (guaranteed to * be non-null, but could be class-level or instance-level) */ protected void notifyForGenericListener(final String upstreamDependency) {} /** * Removes a {@link JavaType} metadata trigger registration. If the type was * never registered, the method returns without an error. * * @param javaType to remove (required) */ public void removeMetadataTrigger(final JavaType javaType) { Validate.notNull(javaType, "Java type required for metadata trigger deregistration"); metadataTriggers.remove(javaType); } /** * Removes the given {@link JavaType}s as triggering metadata registration. * * @param triggerTypes the type-level annotations to remove as triggers * @since 1.2.0 */ public void removeMetadataTriggers(final JavaType... triggerTypes) { for (final JavaType triggerType : triggerTypes) { removeMetadataTrigger(triggerType); } } /** * Invoked whenever a "class-level" downstream dependency identifier is * presented in a metadata notification. An "instance-specific" downstream * dependency identifier is required so that a metadata request can * ultimately be made. This method is responsible for evaluating the * upstream dependency identifier and converting it into a valid downstream * dependency identifier. The downstream dependency identifier must be of * the same type as this metadata provider's {@link #getProvidesType()}. The * downstream dependency identifier must also be instance-specific. * <p> * The basic implementation offered in this class will only convert a * {@link PhysicalTypeIdentifier}. If a subclass registers a dependency on * an upstream (other than * {@link PhysicalTypeIdentifier#getMetadataIdentiferType()}) and presents * their {@link #getProvidesType()} as the downstream (thus meaning only * class-level downstream dependency identifiers will be presented), they * must override this method and appropriately handle instance-specific * downstream dependency identifier resolution. * <p> * This method may also return null if it wishes to abort processing of the * notification. This may be appropriate if a determination cannot be made * at this time for whatever reason (eg too early in a lifecycle etc). * * @param upstreamDependency the upstream (never null) * @return an instance-specific MID of type {@link #getProvidesType()} (or * null if the metadata notification should be aborted) */ protected String resolveDownstreamDependencyIdentifier(final String upstreamDependency) { // We only support analysis of a PhysicalTypeIdentifier upstream MID to // convert this to a downstream MID. // In any other case the downstream metadata should have registered an // instance-specific downstream dependency on a given upstream. Validate.isTrue(isNotificationForJavaType(upstreamDependency), "Expected class-level notifications only for physical Java types (not '" + upstreamDependency + "') for metadata provider " + getClass().getName()); // A physical Java type has changed, and determine what the // corresponding local metadata identification string would have been final JavaType javaType = PhysicalTypeIdentifier.getJavaType(upstreamDependency); final LogicalPath path = PhysicalTypeIdentifier.getPath(upstreamDependency); return createLocalIdentifier(javaType, path); } /** * If set to true (default is true), ensures the governor type details * represent a class. Note that * {@link #setDependsOnGovernorTypeDetailAvailability(boolean)} must also be * true to ensure this can be relied upon. * * @param dependsOnGovernorBeingAClass true means governor type detail must * represent a class */ public void setDependsOnGovernorBeingAClass(final boolean dependsOnGovernorBeingAClass) { this.dependsOnGovernorBeingAClass = dependsOnGovernorBeingAClass; } /** * If set to true (default is true), ensures subclass not called unless the * governor type details are available. * * @param dependsOnGovernorTypeDetailAvailability true means governor type * details must be available */ public void setDependsOnGovernorTypeDetailAvailability( final boolean dependsOnGovernorTypeDetailAvailability) { this.dependsOnGovernorTypeDetailAvailability = dependsOnGovernorTypeDetailAvailability; } protected void setIgnoreTriggerAnnotations(final boolean ignoreTriggerAnnotations) { this.ignoreTriggerAnnotations = ignoreTriggerAnnotations; } protected FileManager getFileManager() { return getServiceManager().getServiceInstance(this, FileManager.class); } protected ItdDiscoveryService getItdDiscoveryService() { return getServiceManager().getServiceInstance(this, ItdDiscoveryService.class); } protected MemberDetailsScanner getMemberDetailsScanner() { return getServiceManager().getServiceInstance(this, MemberDetailsScanner.class); } protected TypeLocationService getTypeLocationService() { return getServiceManager().getServiceInstance(this, TypeLocationService.class); } protected PersistenceMemberLocator getPersistenceMemberLocator() { return getServiceManager().getServiceInstance(this, PersistenceMemberLocator.class); } }