/* * gvNIX is an open source tool for rapid application development (RAD). * Copyright (C) 2010 Generalitat Valenciana * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see <http://www.gnu.org/licenses/>. */ package org.gvnix.addon.jpa.addon.audit; import java.io.File; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.logging.Logger; 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.ReferenceCardinality; import org.apache.felix.scr.annotations.ReferencePolicy; import org.apache.felix.scr.annotations.ReferenceStrategy; import org.apache.felix.scr.annotations.Service; import org.gvnix.addon.jpa.addon.JpaOperations; import org.gvnix.addon.jpa.addon.audit.providers.RevisionLogProvider; import org.gvnix.addon.jpa.addon.audit.providers.RevisionLogProviderId; import org.gvnix.addon.jpa.annotations.audit.GvNIXJpaAudit; import org.gvnix.addon.jpa.annotations.audit.GvNIXJpaAuditListener; import org.gvnix.addon.jpa.annotations.audit.GvNIXJpaAuditRevisionEntity; import org.gvnix.addon.jpa.annotations.audit.GvNIXJpaAuditUserService; import org.springframework.roo.classpath.PhysicalTypeCategory; import org.springframework.roo.classpath.PhysicalTypeIdentifier; import org.springframework.roo.classpath.PhysicalTypeMetadata; 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.MemberFindingUtils; import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; import org.springframework.roo.classpath.scanner.MemberDetails; import org.springframework.roo.classpath.scanner.MemberDetailsScanner; import org.springframework.roo.metadata.MetadataService; import org.springframework.roo.model.JavaPackage; import org.springframework.roo.model.JavaType; import org.springframework.roo.model.JdkJavaType; import org.springframework.roo.model.RooJavaType; import org.springframework.roo.project.FeatureNames; 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.support.logging.HandlerUtils; /** * Implementation of {@link JpaAuditOperations} * <p/> * For {@link #getUserServiceType()} this class implements a simple cache system * to store computed value. This is due to performance problems on * look-for-an-annotated-class mechanism. * * @author <a href="http://www.disid.com">DISID Corporation S.L.</a> made for <a * href="http://www.dgti.gva.es">General Directorate for Information * Technologies (DGTI)</a> * @since 1.3.0 */ @Component @Service @Reference(name = "provider", strategy = ReferenceStrategy.EVENT, policy = ReferencePolicy.DYNAMIC, referenceInterface = RevisionLogProvider.class, cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE) public class JpaAuditOperationsImpl implements JpaAuditOperations, JpaAuditOperationsMetadata, JpaAuditOperationsSPI { private static final JavaType AUDIT_ANNOTATION_TYPE = new JavaType( GvNIXJpaAudit.class.getName()); private static final JavaType AUDIT_USR_SERV_ANN_T = new JavaType( GvNIXJpaAuditUserService.class.getName()); private static final String DEFAULT_USER_SERVICE_NAME = "AuditUserService"; private static final Logger LOGGER = HandlerUtils .getLogger(JpaAuditOperationsImpl.class); private static final int EVICT_CACHE_MLSEC = 60 * 3 * 1000; @Reference private ProjectOperations projectOperations; @Reference private TypeLocationService typeLocationService; @Reference private TypeManagementService typeManagementService; @Reference private PathResolver pathResolver; @Reference private MemberDetailsScanner memberDetailsScanner; @Reference private MetadataService metadataService; // Cache user services private JavaType userServiceType = null; // User service cache timestamp private Long userServiceTypeTimestamp = null; // Cache revisionEntityJavaType services private JavaType revisionEntityJavaType; // revisionEntityJavaType cache timestamp private Long revEntJTypeTimestamp = null; /** * Registered providers */ private List<RevisionLogProvider> providers = new ArrayList<RevisionLogProvider>(); /** * Current active provider */ private RevisionLogProvider currentProvider = null; /** * Bind a provider * * @param provider */ protected void bindProvider(final RevisionLogProvider provider) { providers.add(provider); } /** * Unbind a provider * * @param provider */ protected void unbindProvider(final RevisionLogProvider provider) { providers.remove(provider); // Reset current provider currentProvider = null; } /** {@inheritDoc} */ @Override public boolean isSetupCommandAvailable() { return projectOperations .isFeatureInstalledInFocusedModule(JpaOperations.FEATURE_NAME_GVNIX_JPA) && !hasUserService(); } /** {@inheritDoc} */ public boolean isCommandAvailable() { // Check if gvNIX JPA dependencies installed return projectOperations .isFeatureInstalledInFocusedModule(JpaOperations.FEATURE_NAME_GVNIX_JPA) && hasUserService(); } /** * Clean the User service Cache data */ public void evictUserServiceInfoCache() { this.userServiceType = null; this.userServiceTypeTimestamp = null; } /** * Load userService and userType data if is needed (manage cache) * */ public void loadUserServiceData() { // Check cache if (userServiceTypeTimestamp != null) { // Check for valid cache if (System.currentTimeMillis() - userServiceTypeTimestamp.longValue() < EVICT_CACHE_MLSEC) { // return javaType cache return; } } // evict cache evictUserServiceInfoCache(); // Look for user service class Set<ClassOrInterfaceTypeDetails> classes = typeLocationService .findClassesOrInterfaceDetailsWithAnnotation(AUDIT_USR_SERV_ANN_T); if (classes != null && !classes.isEmpty()) { // Check for multiple classes if (classes.size() > 1) { LOGGER.severe(String.format( "Only one class can be annotated with %s: found %s", AUDIT_USR_SERV_ANN_T, classes.size())); } else { // load data from locate of userService ClassOrInterfaceTypeDetails cid = classes.iterator().next(); userServiceType = cid.getType(); userServiceTypeTimestamp = System.currentTimeMillis(); } } } /** * {@inheritDoc} * <p/> * In order to solve possible performance problems when it tries to look for * User-service class, a very simple cache mechanism has been implemented. * */ @Override public JavaType getUserServiceType() { loadUserServiceData(); return userServiceType; } private boolean hasUserService() { return getUserServiceType() != null; } /** * {@inheritDoc} */ public boolean isSpringSecurityInstalled() { return projectOperations .isFeatureInstalledInFocusedModule(FeatureNames.SECURITY); } /** * {@inheritDoc} */ @Override public boolean isProvidersAvailable() { if (providers.isEmpty()) { return false; } for (RevisionLogProvider provider : providers) { if (provider.isAvailable()) { return true; } } return false; } /** * Generates new a JavaType for entity listener class based on * <code>entity</code> class name. * * @param entity * @param targetPackage if null uses <code>entity</code> package * @return */ private JavaType generateListenerJavaType(JavaType entity, JavaPackage targetPackage) { if (targetPackage == null) { targetPackage = entity.getPackage(); } return new JavaType(String.format("%s.%sAuditListener", targetPackage.getFullyQualifiedPackageName(), entity.getSimpleTypeName())); } /** {@inheritDoc} */ public void createAll(JavaPackage targetPackage) { // Use the TypeLocationService to scan project for all types with a // specific annotation for (JavaType entity : typeLocationService .findTypesWithAnnotation(RooJavaType.ROO_JPA_ACTIVE_RECORD)) { JavaType finalType = null; if (targetPackage != null) { finalType = generateListenerJavaType(entity, targetPackage); } create(entity, finalType, false); } } /** {@inheritDoc} */ @Override public void create(JavaType entity, JavaType target) { create(entity, target, true); } /** {@inheritDoc} */ @Override public void create(JavaType entity, JavaType target, boolean failIfAlreadySet) { Validate.notNull(entity, "Entity required"); if (target == null) { target = generateListenerJavaType(entity, null); } Validate.isTrue( !JdkJavaType.isPartOfJavaLang(target.getSimpleTypeName()), "Target name '%s' must not be part of java.lang", target.getSimpleTypeName()); int modifier = Modifier.PUBLIC; final String declaredByMetadataId = PhysicalTypeIdentifier .createIdentifier(target, pathResolver.getFocusedPath(Path.SRC_MAIN_JAVA)); File targetFile = new File( typeLocationService .getPhysicalTypeCanonicalPath(declaredByMetadataId)); if (targetFile.exists()) { if (failIfAlreadySet) { Validate.isTrue(!targetFile.exists(), "Type '%s' already exists", target); } else { LOGGER.info(String.format( "Ignoring entity '%s': Type '%s' already exists", entity, target)); return; } } // Prepare class builder final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( declaredByMetadataId, modifier, target, PhysicalTypeCategory.CLASS); // Prepare annotations array List<AnnotationMetadataBuilder> annotations = new ArrayList<AnnotationMetadataBuilder>( 2); // Add @GvNIXJpaAuditListener annotation AnnotationMetadataBuilder jpaAuditListenerAnnotation = new AnnotationMetadataBuilder( new JavaType(GvNIXJpaAuditListener.class)); jpaAuditListenerAnnotation.addClassAttribute("entity", entity); annotations.add(jpaAuditListenerAnnotation); // Set annotations cidBuilder.setAnnotations(annotations); // Add GvNIXJpaAudit annotation to entity if (!annotateEntity(entity)) { // Already set annotation. Nothing to do LOGGER.info(String .format("Entity %s is already annotated with %s: ignore this entity.", entity.getFullyQualifiedTypeName(), GvNIXJpaAudit.class.getSimpleName())); return; } // Create Listener class typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build()); } /** * Annotated entity with {@link GvNIXJpaAudit} * <p/> * * @param entity * @return true if entity has been annotated or false if entity is already * annotated */ public boolean annotateEntity(final JavaType entity) { Validate.notNull(entity, "Java type required"); // get class details final ClassOrInterfaceTypeDetails cid = typeLocationService .getTypeDetails(entity); if (cid == null) { throw new IllegalArgumentException("Cannot locate source for '" .concat(entity.getFullyQualifiedTypeName()).concat("'")); } // Check for @GvNIXJpaAudit annotation if (MemberFindingUtils.getAnnotationOfType(cid.getAnnotations(), AUDIT_ANNOTATION_TYPE) == null) { // Add GvNIXJpaAudit annotation final AnnotationMetadataBuilder annotationBuilder = new AnnotationMetadataBuilder( AUDIT_ANNOTATION_TYPE); final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( cid); cidBuilder.addAnnotation(annotationBuilder); typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build()); return true; } else { // Already annotated return false; } } /** * {@inheritDoc} */ @Override public List<RevisionLogProvider> getAvailableRevisionLogProviders() { List<RevisionLogProvider> availables = new ArrayList<RevisionLogProvider>( providers.size()); if (!providers.isEmpty()) { for (RevisionLogProvider provider : providers) { if (provider.isAvailable()) { availables.add(provider); } } } return Collections.unmodifiableList(availables); } /** * {@inheritDoc} */ @Override public RevisionLogProvider getActiveRevisionLogProvider() { // Try to use cached provider (if any) if (currentProvider != null && currentProvider.isActive()) { return currentProvider; } if (providers.isEmpty()) { return null; } RevisionLogProvider active = null; for (RevisionLogProvider provider : providers) { if (!provider.isAvailable()) { continue; } if (provider.isActive()) { if (active != null) { throw new IllegalStateException(String.format( "Two active providers: %s and %s", active.getName(), provider.getName())); } active = provider; } } // Cache activated found currentProvider = active; return currentProvider; } /** * {@inheritDoc} */ @Override public RevisionLogProviderId getProviderIdByName(String value) { if (providers.isEmpty()) { return null; } for (RevisionLogProvider provider : providers) { if (provider.isAvailable() && StringUtils.equals(value, provider.getName())) { return new RevisionLogProviderId(provider); } } return null; } /** * {@inheritDoc} */ @Override public List<RevisionLogProviderId> getProvidersId() { List<RevisionLogProviderId> availables = new ArrayList<RevisionLogProviderId>( providers.size()); if (!providers.isEmpty()) { for (RevisionLogProvider provider : providers) { if (provider.isAvailable()) { availables.add(new RevisionLogProviderId(provider)); } } } return Collections.unmodifiableList(availables); } @Override public void activeRevisionLog(RevisionLogProviderId provider) { // Check current provider installed RevisionLogProvider currentActive = getActiveRevisionLogProvider(); if (currentActive != null) { // By now, we don't allow change provider throw new IllegalStateException("Provider ".concat( currentActive.getName()).concat(" is alredy configured.")); } // look for provider on available providers RevisionLogProvider toActive = null; List<RevisionLogProvider> availables = getAvailableRevisionLogProviders(); for (RevisionLogProvider toCheck : availables) { if (provider.is(toCheck)) { toActive = toCheck; break; } } // Provider not found if (toActive == null) { throw new IllegalArgumentException("Provider ".concat( provider.getId()).concat( " is not available for this project.")); } // Setup provider toActive.setup(this); } /** * Create the class for entity which will hold the revision information for * Hibernate Envers * <p/> * This use {@link #REVISION_LOG_ENTITY_NAME} as class name and look for * <em>the first package which contains a entity</em> to place it. * */ public void installRevisonEntity(JavaType revisionEntity) { PathResolver pathResolver = projectOperations.getPathResolver(); JavaType target; if (revisionEntity == null) { target = generateRevionEntityJavaType(); } else { target = revisionEntity; } int modifier = Modifier.PUBLIC; final String declaredByMetadataId = PhysicalTypeIdentifier .createIdentifier(target, pathResolver.getFocusedPath(Path.SRC_MAIN_JAVA)); File targetFile = new File( typeLocationService .getPhysicalTypeCanonicalPath(declaredByMetadataId)); if (targetFile.exists()) { Validate.isTrue(!targetFile.exists(), "Type '%s' already exists", target); } // Prepare class builder final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( declaredByMetadataId, modifier, target, PhysicalTypeCategory.CLASS); // Prepare annotations array List<AnnotationMetadataBuilder> annotations = new ArrayList<AnnotationMetadataBuilder>( 1); // Add @GvNIXJpaAuditListener annotation AnnotationMetadataBuilder jpaAuditRevEntAnn = new AnnotationMetadataBuilder( new JavaType(GvNIXJpaAuditRevisionEntity.class)); annotations.add(jpaAuditRevEntAnn); // Set annotations cidBuilder.setAnnotations(annotations); // Create Revision entity class typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build()); } /** * @return installed RevisonEntity JavaType */ public JavaType getRevisionEntityJavaType() { if (this.revisionEntityJavaType != null) { if (System.currentTimeMillis() - revEntJTypeTimestamp.longValue() < EVICT_CACHE_MLSEC) { return this.revisionEntityJavaType; } } Set<ClassOrInterfaceTypeDetails> found = typeLocationService .findClassesOrInterfaceDetailsWithAnnotation(JpaAuditOperationsSPI.GVNIX_REVION_ENTITY_ANNOTATION); if (found.isEmpty()) { throw new IllegalStateException(String.format( "Class with %s annotation is missing", JpaAuditOperationsSPI.GVNIX_REVION_ENTITY_ANNOTATION .getFullyQualifiedTypeName())); } else if (found.size() > 1) { throw new IllegalStateException(String.format( "More than 1 classes with %s annotation", JpaAuditOperationsSPI.GVNIX_REVION_ENTITY_ANNOTATION .getFullyQualifiedTypeName())); } this.revisionEntityJavaType = found.iterator().next().getType(); this.revEntJTypeTimestamp = System.currentTimeMillis(); return this.revisionEntityJavaType; } /** * Clean cached revision entity javaType */ void cleanRevisionEntityJavaType() { this.revisionEntityJavaType = null; } /** * Generates new a JavaType for revision log entity. * <p/> * Locates the early package which contains a entity and use it as domain * package. * * @return */ private JavaType generateRevionEntityJavaType() { JavaPackage targetPackage = getBaseDomainPackage(); if (targetPackage == null) { throw new IllegalStateException( "No entities found on project: Can't identify package for revision entity."); } // Create JavaType with locate package return new JavaType(targetPackage.getFullyQualifiedPackageName() .concat(".") .concat(JpaAuditOperationsSPI.REVISION_LOG_ENTITY_NAME)); } /** * {@inheritDoc} */ public JavaPackage getBaseDomainPackage() { // Use the TypeLocationService to scan project for all types with entity // annotation Set<JavaPackage> packages = new HashSet<JavaPackage>(); for (JavaType entity : typeLocationService .findTypesWithAnnotation(RooJavaType.ROO_JPA_ACTIVE_RECORD)) { packages.add(entity.getPackage()); } // Get the shorter (lowest deep level) package which contains an entity JavaPackage targetPackage = null; for (JavaPackage cur : packages) { if (targetPackage == null || cur.getElements().size() < targetPackage.getElements() .size()) { targetPackage = cur; } } return targetPackage; } @Override public void setup(JavaType serviceClass, JavaType userType) { // Check parameters: get defaults JavaType targetServiceClass; if (serviceClass == null) { targetServiceClass = generateUserServiceJavaType(); } else { targetServiceClass = serviceClass; } JavaType targetUserType; if (userType == null) { targetUserType = JavaType.STRING; } else { targetUserType = userType; } Validate.isTrue(!JdkJavaType.isPartOfJavaLang(targetServiceClass .getSimpleTypeName()), "Target service class '%s' must not be part of java.lang", targetServiceClass); int modifier = Modifier.PUBLIC; final String declaredByMetadataId = PhysicalTypeIdentifier .createIdentifier(targetServiceClass, pathResolver.getFocusedPath(Path.SRC_MAIN_JAVA)); File targetUserServiceFile = new File( typeLocationService .getPhysicalTypeCanonicalPath(declaredByMetadataId)); if (targetUserServiceFile.exists()) { Validate.isTrue(!targetUserServiceFile.exists(), "Type '%s' already exists", targetServiceClass); } // Prepare class builder final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder( declaredByMetadataId, modifier, targetServiceClass, PhysicalTypeCategory.CLASS); // Prepare annotations array List<AnnotationMetadataBuilder> annotations = new ArrayList<AnnotationMetadataBuilder>( 2); // Add @GvNIXJpaAuditUserService annotation AnnotationMetadataBuilder jpaAuditUserServiceAnnotation = new AnnotationMetadataBuilder( new JavaType(GvNIXJpaAuditUserService.class)); if (!JavaType.STRING.equals(targetUserType)) { jpaAuditUserServiceAnnotation.addClassAttribute("userType", targetUserType); } annotations.add(jpaAuditUserServiceAnnotation); // Set annotations cidBuilder.setAnnotations(annotations); // Create User Service class typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build()); refreshAuditedEntities(); PathResolver pathResolver = projectOperations.getPathResolver(); LogicalPath path = pathResolver.getFocusedPath(Path.SRC_MAIN_JAVA); String metadataId = JpaAuditUserServiceMetadata.createIdentifier( targetServiceClass, path); JpaAuditUserServiceMetadata metadata = (JpaAuditUserServiceMetadata) metadataService .get(metadataId); // Show warning about UserType if (isSpringSecurityInstalled()) { if (JavaType.STRING.equals(targetUserType)) { LOGGER.warning(String .format("Generating implemention of %s.%s() method which use UserDetails.getName() as user info. Customize it if is needed.", targetServiceClass, JpaAuditUserServiceMetadata.GET_USER_METHOD)); } else if (!(metadata.isUserTypeSpringSecUserDetails() && metadata .isUserTypeEntity())) { LOGGER.warning(String .format("You MUST customize %s.%s() method to provider user information for aduit. Addon can't identify how get %s instance.", targetServiceClass, JpaAuditUserServiceMetadata.GET_USER_METHOD, targetUserType)); } } else { LOGGER.warning(String .format("You MUST implement %s.%s() method to provider user information for aduit.", targetServiceClass, JpaAuditUserServiceMetadata.GET_USER_METHOD)); } } private JavaType generateUserServiceJavaType() { JavaPackage basePackage = getBaseDomainPackage(); return new JavaType(basePackage.getFullyQualifiedPackageName() .concat(".").concat(DEFAULT_USER_SERVICE_NAME)); } /** * 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 */ @SuppressWarnings("unused") private MemberDetails getMemberDetails(final JavaType type) { final String physicalTypeIdentifier = typeLocationService .getPhysicalTypeIdentifier(type); if (physicalTypeIdentifier == null) { return null; } // We need to lookup the metadata we depend on final PhysicalTypeMetadata physicalTypeMetadata = (PhysicalTypeMetadata) metadataService .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) { // 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); } /** * {@inheritDoc} */ public void refreshAuditedEntities() { // Use the TypeLocationService to scan project for all types with // jpaAudit and related annotation String metadataId; PathResolver pathResolver = projectOperations.getPathResolver(); LogicalPath path = pathResolver.getFocusedPath(Path.SRC_MAIN_JAVA); for (JavaType entity : typeLocationService .findTypesWithAnnotation(new JavaType(GvNIXJpaAudit.class))) { metadataId = JpaAuditMetadata.createIdentifier(entity, path); metadataService.evictAndGet(metadataId); } for (JavaType entity : typeLocationService .findTypesWithAnnotation(new JavaType( GvNIXJpaAuditListener.class))) { metadataId = JpaAuditListenerMetadata .createIdentifier(entity, path); metadataService.evictAndGet(metadataId); } } }