package org.springframework.roo.addon.jpa.addon.dod;
import static org.springframework.roo.model.JpaJavaType.ENTITY;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
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.addon.jpa.addon.entity.JpaEntityMetadata;
import org.springframework.roo.addon.jpa.addon.entity.JpaEntityMetadata.RelationInfo;
import org.springframework.roo.addon.test.providers.DataOnDemandCreatorProvider;
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.annotations.AnnotationAttributeValue;
import org.springframework.roo.classpath.details.annotations.AnnotationMetadata;
import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder;
import org.springframework.roo.classpath.details.annotations.ClassAttributeValue;
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.JavaSymbolName;
import org.springframework.roo.model.JavaType;
import org.springframework.roo.model.RooJavaType;
import org.springframework.roo.project.Dependency;
import org.springframework.roo.project.DependencyScope;
import org.springframework.roo.project.DependencyType;
import org.springframework.roo.project.LogicalPath;
import org.springframework.roo.project.Path;
import org.springframework.roo.project.Plugin;
import org.springframework.roo.project.ProjectOperations;
import org.springframework.roo.project.maven.Pom;
import org.springframework.roo.support.util.XmlUtils;
import org.w3c.dom.Element;
/**
* Implementation of {@link DataOnDemandOperations}, based on old
* DataOnDemandOperationsImpl.
*
* @author Alan Stewart
* @author Sergio Clares
* @since 2.0
*/
@Component
@Service
public class JpaDataOnDemandCreator implements DataOnDemandCreatorProvider {
private static final Dependency VALIDATION_API_DEPENDENCY = new Dependency("javax.validation",
"validation-api", null);
private static final Dependency SPRING_BOOT_TEST_DEPENDENCY = new Dependency(
"org.springframework.boot", "spring-boot-test", null, DependencyType.JAR,
DependencyScope.TEST);
private static final String MAVEN_JAR_PLUGIN = "maven-jar-plugin";
@Reference
private MemberDetailsScanner memberDetailsScanner;
@Reference
private MetadataService metadataService;
@Reference
private ProjectOperations projectOperations;
@Reference
private TypeLocationService typeLocationService;
@Reference
private TypeManagementService typeManagementService;
@Override
public boolean isValid(JavaType javaType) {
ClassOrInterfaceTypeDetails cid = typeLocationService.getTypeDetails(javaType);
if (cid.getAnnotation(RooJavaType.ROO_JPA_ENTITY) != null) {
return true;
}
return false;
}
@Override
public JavaType createDataOnDemand(JavaType entity) {
Validate.notNull(entity, "Entity to produce a data on demand provider for is required");
JavaType dodClass = getDataOnDemand(entity);
if (dodClass != null) {
return dodClass;
}
// Add plugin to generate test jar
addMavenJarPlugin(entity.getModule());
// Create the JavaType for DoD class
JavaType name =
new JavaType(entity.getPackage().getFullyQualifiedPackageName().concat(".dod.")
.concat(entity.getSimpleTypeName()).concat("DataOnDemand"), entity.getModule());
// Obatain test path for the module of the new class
final LogicalPath path = LogicalPath.getInstance(Path.SRC_TEST_JAVA, name.getModule());
Validate.notNull(path, "Location of the new data on demand provider is required");
// Create DoD configuration class
createDataOnDemandConfiguration(entity.getModule());
// Create entity factories for the given entity and its related entities
createEntityFactory(entity);
// Create data on demand class
return newDataOnDemandClass(entity, name);
}
@Override
public JavaType createDataOnDemandConfiguration(String moduleName) {
// Check if alreafy exists
JavaType dodConfig = getDataOnDemandConfiguration();
if (dodConfig != null) {
return dodConfig;
}
// Add spring-boot-test dependency with test scope
projectOperations.addDependency(moduleName, SPRING_BOOT_TEST_DEPENDENCY);
// Get Pom
final Pom module = projectOperations.getPomFromModuleName(moduleName);
// Get test Path for module
final LogicalPath path = LogicalPath.getInstance(Path.SRC_TEST_JAVA, moduleName);
// Create the JavaType for the configuration class
JavaType dodConfigurationClass =
new JavaType(String.format("%s.dod.DataOnDemandConfiguration",
typeLocationService.getTopLevelPackageForModule(module), moduleName));
final String declaredByMetadataId =
PhysicalTypeIdentifier.createIdentifier(dodConfigurationClass, path);
if (metadataService.get(declaredByMetadataId) != null) {
// The file already exists
return new ClassOrInterfaceTypeDetailsBuilder(declaredByMetadataId).getName();
}
// Create the CID builder
ClassOrInterfaceTypeDetailsBuilder cidBuilder =
new ClassOrInterfaceTypeDetailsBuilder(declaredByMetadataId, Modifier.PUBLIC,
dodConfigurationClass, PhysicalTypeCategory.CLASS);
cidBuilder.addAnnotation(new AnnotationMetadataBuilder(
RooJavaType.ROO_JPA_DATA_ON_DEMAND_CONFIGURATION));
// Write changes to disk
final ClassOrInterfaceTypeDetails configDodCid = cidBuilder.build();
typeManagementService.createOrUpdateTypeOnDisk(configDodCid);
return configDodCid.getName();
}
@Override
public JavaType createEntityFactory(JavaType currentEntity) {
Validate.notNull(currentEntity, "Entity to produce a data on demand provider for is required");
// Verify the requested entity actually exists as a class and is not
// abstract
final ClassOrInterfaceTypeDetails cid = getEntityDetails(currentEntity);
Validate.isTrue(cid.getPhysicalTypeCategory() == PhysicalTypeCategory.CLASS,
"Type %s is not a class", currentEntity.getFullyQualifiedTypeName());
Validate.isTrue(!Modifier.isAbstract(cid.getModifier()), "Type %s is abstract",
currentEntity.getFullyQualifiedTypeName());
// Check if the requested entity is a JPA @Entity
final MemberDetails memberDetails =
memberDetailsScanner.getMemberDetails(JpaDataOnDemandCreator.class.getName(), cid);
final AnnotationMetadata entityAnnotation = memberDetails.getAnnotation(ENTITY);
Validate.isTrue(entityAnnotation != null, "Type %s must be a JPA entity type",
currentEntity.getFullyQualifiedTypeName());
// Get related entities
List<JavaType> entities = getEntityAndRelatedEntitiesList(currentEntity);
// Get test Path for module
final LogicalPath path = LogicalPath.getInstance(Path.SRC_TEST_JAVA, currentEntity.getModule());
JavaType currentEntityFactory = null;
for (JavaType entity : entities) {
// Create the JavaType for the configuration class
JavaType factoryClass =
new JavaType(String.format("%s.dod.%sFactory", entity.getPackage()
.getFullyQualifiedPackageName(), entity.getSimpleTypeName()), entity.getModule());
final String declaredByMetadataId =
PhysicalTypeIdentifier.createIdentifier(factoryClass, path);
if (metadataService.get(declaredByMetadataId) != null) {
// The file already exists
continue;
}
// Create the CID builder
ClassOrInterfaceTypeDetailsBuilder cidBuilder =
new ClassOrInterfaceTypeDetailsBuilder(declaredByMetadataId, Modifier.PUBLIC,
factoryClass, PhysicalTypeCategory.CLASS);
// Add @RooEntityFactory annotation
AnnotationMetadataBuilder entityFactoryAnnotation =
new AnnotationMetadataBuilder(RooJavaType.ROO_JPA_ENTITY_FACTORY);
entityFactoryAnnotation.addClassAttribute("entity", entity);
cidBuilder.addAnnotation(entityFactoryAnnotation);
// Write changes to disk
typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build());
// First entity is current entity
if (currentEntityFactory == null) {
currentEntityFactory = cidBuilder.getName();
}
}
return currentEntityFactory;
}
@Override
public JavaType getDataOnDemand(JavaType entity) {
Set<ClassOrInterfaceTypeDetails> dataOnDemandCids =
typeLocationService
.findClassesOrInterfaceDetailsWithAnnotation(RooJavaType.ROO_JPA_DATA_ON_DEMAND);
JavaType typeToReturn = null;
for (ClassOrInterfaceTypeDetails cid : dataOnDemandCids) {
if (entity.equals((JavaType) cid.getAnnotation(RooJavaType.ROO_JPA_DATA_ON_DEMAND)
.getAttribute("entity").getValue())) {
typeToReturn = cid.getName();
break;
}
}
return typeToReturn;
}
@Override
public JavaType getDataOnDemandConfiguration() {
Set<JavaType> dodConfigurationTypes =
typeLocationService
.findTypesWithAnnotation(RooJavaType.ROO_JPA_DATA_ON_DEMAND_CONFIGURATION);
if (!dodConfigurationTypes.isEmpty()) {
return dodConfigurationTypes.iterator().next();
}
return null;
}
@Override
public JavaType getDataOnDemandConfiguration(String moduleName) {
Set<JavaType> dodConfigurationTypes =
typeLocationService
.findTypesWithAnnotation(RooJavaType.ROO_JPA_DATA_ON_DEMAND_CONFIGURATION);
Iterator<JavaType> it = dodConfigurationTypes.iterator();
while (it.hasNext()) {
JavaType dodConfigType = it.next();
if (dodConfigType.getModule().equals(moduleName)) {
return dodConfigType;
}
}
return null;
}
@Override
public JavaType getEntityFactory(JavaType entity) {
Set<ClassOrInterfaceTypeDetails> dataOnDemandCids =
typeLocationService
.findClassesOrInterfaceDetailsWithAnnotation(RooJavaType.ROO_JPA_ENTITY_FACTORY);
JavaType typeToReturn = null;
for (ClassOrInterfaceTypeDetails cid : dataOnDemandCids) {
if (entity.equals((JavaType) cid.getAnnotation(RooJavaType.ROO_JPA_ENTITY_FACTORY)
.getAttribute("entity").getValue())) {
typeToReturn = cid.getName();
break;
}
}
return typeToReturn;
}
/**
* Add maven-jar-plugin to provided module.
*
* @param moduleName the name of the module.
*/
private void addMavenJarPlugin(String moduleName) {
// Add plugin maven-jar-plugin
Pom module = projectOperations.getPomFromModuleName(moduleName);
// Stop if the plugin is already installed
for (final Plugin plugin : module.getBuildPlugins()) {
if (plugin.getArtifactId().equals(MAVEN_JAR_PLUGIN)) {
return;
}
}
final Element configuration = XmlUtils.getConfiguration(getClass());
final Element plugin = XmlUtils.findFirstElement("/configuration/plugin", configuration);
// Now install the plugin itself
if (plugin != null) {
projectOperations.addBuildPlugin(moduleName, new Plugin(plugin), false);
}
}
/**
* Creates a new data-on-demand provider for an entity. Silently returns
* if the DoD class already exists.
*
* @param entity to produce a DoD provider for
* @param name the name of the new DoD class
*/
private JavaType newDataOnDemandClass(JavaType entity, JavaType name) {
Validate.notNull(entity, "Entity to produce a data on demand provider for is required");
Validate.notNull(name, "Name of the new data on demand provider is required");
final LogicalPath path = LogicalPath.getInstance(Path.SRC_TEST_JAVA, name.getModule());
Validate.notNull(path, "Location of the new data on demand provider is required");
// Add javax validation dependency
projectOperations.addDependency(name.getModule(), VALIDATION_API_DEPENDENCY);
// Verify the requested entity actually exists as a class and is not
// abstract
final ClassOrInterfaceTypeDetails cid = getEntityDetails(entity);
Validate.isTrue(cid.getPhysicalTypeCategory() == PhysicalTypeCategory.CLASS,
"Type %s is not a class", entity.getFullyQualifiedTypeName());
Validate.isTrue(!Modifier.isAbstract(cid.getModifier()), "Type %s is abstract",
entity.getFullyQualifiedTypeName());
// Check if the requested entity is a JPA @Entity
final MemberDetails memberDetails =
memberDetailsScanner.getMemberDetails(JpaDataOnDemandCreator.class.getName(), cid);
final AnnotationMetadata entityAnnotation = memberDetails.getAnnotation(ENTITY);
Validate.isTrue(entityAnnotation != null, "Type %s must be a JPA entity type",
entity.getFullyQualifiedTypeName());
// Everything is OK to proceed
final String declaredByMetadataId = PhysicalTypeIdentifier.createIdentifier(name, path);
if (metadataService.get(declaredByMetadataId) != null) {
// The file already exists
return new ClassOrInterfaceTypeDetailsBuilder(declaredByMetadataId).getName();
}
final List<AnnotationMetadataBuilder> annotations = new ArrayList<AnnotationMetadataBuilder>();
final List<AnnotationAttributeValue<?>> dodConfig =
new ArrayList<AnnotationAttributeValue<?>>();
dodConfig.add(new ClassAttributeValue(new JavaSymbolName("entity"), entity));
annotations.add(new AnnotationMetadataBuilder(RooJavaType.ROO_JPA_DATA_ON_DEMAND, dodConfig));
final ClassOrInterfaceTypeDetailsBuilder cidBuilder =
new ClassOrInterfaceTypeDetailsBuilder(declaredByMetadataId, Modifier.PUBLIC, name,
PhysicalTypeCategory.CLASS);
cidBuilder.setAnnotations(annotations);
// Write changes on disk
final ClassOrInterfaceTypeDetails dodClassCid = cidBuilder.build();
typeManagementService.createOrUpdateTypeOnDisk(dodClassCid);
return cid.getName();
}
/**
* Searches the related entities of provided entity and returns a
* {@link List} with all the related entities plus the provided entity.
*
* @param entity
* the entity JavaType to search for its related entities.
* @return a List with all the related entities.
*/
private List<JavaType> getEntityAndRelatedEntitiesList(JavaType entity) {
ClassOrInterfaceTypeDetails entityDetails = getEntityDetails(entity);
JpaEntityMetadata entityMetadata =
metadataService.get(JpaEntityMetadata.createIdentifier(entityDetails));
List<JavaType> entitiesToCreateFactories = new ArrayList<JavaType>();
entitiesToCreateFactories.add(entity);
// Get related child entities
for (RelationInfo info : entityMetadata.getRelationInfos().values()) {
// Add to list
if (!entitiesToCreateFactories.contains(info.childType)) {
entitiesToCreateFactories.add(info.childType);
}
}
return entitiesToCreateFactories;
}
/**
* Returns the {@link ClassOrInterfaceTypeDetails} for the provided entity.
*
* @param entity
* the entity to lookup required
* @return the ClassOrInterfaceTypeDetails type details (never null; throws
* an exception if it cannot be obtained or parsed)
*/
private ClassOrInterfaceTypeDetails getEntityDetails(final JavaType entity) {
final ClassOrInterfaceTypeDetails cid = typeLocationService.getTypeDetails(entity);
Validate.notNull(cid, "Java source code details unavailable for type '%s'", entity);
return cid;
}
}