package org.springframework.roo.addon.layers.repository.jpa.addon;
import static java.lang.reflect.Modifier.PUBLIC;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.tuple.Pair;
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.addon.jpa.addon.JpaOperations;
import org.springframework.roo.addon.jpa.addon.entity.JpaEntityMetadata.RelationInfo;
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.FieldMetadata;
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.operations.Cardinality;
import org.springframework.roo.classpath.scanner.MemberDetailsScanner;
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.model.RooJavaType;
import org.springframework.roo.model.SpringJavaType;
import org.springframework.roo.process.manager.FileManager;
import org.springframework.roo.project.Dependency;
import org.springframework.roo.project.FeatureNames;
import org.springframework.roo.project.Path;
import org.springframework.roo.project.PathResolver;
import org.springframework.roo.project.Plugin;
import org.springframework.roo.project.ProjectOperations;
import org.springframework.roo.project.Repository;
import org.springframework.roo.project.maven.Pom;
import org.springframework.roo.support.logging.HandlerUtils;
import org.springframework.roo.support.osgi.ServiceInstaceManager;
import org.springframework.roo.support.util.FileUtils;
import org.springframework.roo.support.util.XmlUtils;
import org.w3c.dom.Element;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Modifier;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* The {@link RepositoryJpaOperations} implementation.
*
* @author Stefan Schmidt
* @author Juan Carlos García
* @author Sergio Clares
* @author Jose Manuel Vivó
* @since 1.2.0
*/
@Component
@Service
public class RepositoryJpaOperationsImpl implements RepositoryJpaOperations {
protected final static Logger LOGGER = HandlerUtils.getLogger(RepositoryJpaOperationsImpl.class);
// ------------ OSGi component attributes ----------------
private BundleContext context;
private ServiceInstaceManager serviceInstaceManager = new ServiceInstaceManager();
protected void activate(final ComponentContext context) {
this.context = context.getBundleContext();
serviceInstaceManager.activate(this.context);
}
@Override
public boolean isRepositoryInstallationPossible() {
return getProjectOperations().isFeatureInstalled(FeatureNames.JPA);
}
@Override
public void generateAllRepositories(JavaPackage repositoriesPackage) {
// Getting all project entities
Set<ClassOrInterfaceTypeDetails> entities =
getTypeLocationService().findClassesOrInterfaceDetailsWithAnnotation(
RooJavaType.ROO_JPA_ENTITY);
Iterator<ClassOrInterfaceTypeDetails> it = entities.iterator();
while (it.hasNext()) {
ClassOrInterfaceTypeDetails entity = it.next();
// Ignore abstract classes
if (entity.isAbstract()) {
continue;
}
// Generating new interface type using entity
JavaType interfaceType =
new JavaType(repositoriesPackage.getFullyQualifiedPackageName().concat(".")
.concat(entity.getType().getSimpleTypeName()).concat("Repository"),
repositoriesPackage.getModule());
// Delegate on simple add repository method
addRepository(interfaceType, entity.getType(), null, false);
}
}
@Override
public void addRepository(JavaType interfaceType, final JavaType domainType,
JavaType defaultReturnType, boolean failOnComposition) {
Validate.notNull(domainType, "ERROR: You must specify a valid Entity. ");
if (getProjectOperations().isMultimoduleProject()) {
Validate.notNull(interfaceType,
"ERROR: You must specify an interface repository type on multimodule projects.");
Validate.notNull(interfaceType.getModule(),
"ERROR: interfaceType module is required on multimodule projects.");
} else if (interfaceType == null) {
interfaceType =
new JavaType(String.format("%s.repository.%sRepository", getProjectOperations()
.getFocusedTopLevelPackage(), domainType.getSimpleTypeName()), "");
}
// Check if entity provided type is annotated with @RooJpaEntity
ClassOrInterfaceTypeDetails entityDetails = getTypeLocationService().getTypeDetails(domainType);
AnnotationMetadata entityAnnotation = entityDetails.getAnnotation(RooJavaType.ROO_JPA_ENTITY);
// Show an error indicating that entity should be annotated with
// @RooJpaEntity
Validate.notNull(entityAnnotation,
"ERROR: Provided entity should be annotated with @RooJpaEntity");
if (!shouldGenerateRepository(entityDetails)) {
if (failOnComposition) {
throw new IllegalArgumentException(
"%s is child part of a composition relation. Can't create repository (entity should be handle in parent part)");
} else {
// Nothing to do: silently exit
return;
}
}
if (defaultReturnType != null) {
ClassOrInterfaceTypeDetails defaultReturnTypeDetails =
getTypeLocationService().getTypeDetails(defaultReturnType);
AnnotationMetadata defaultReturnTypeAnnotation =
defaultReturnTypeDetails.getAnnotation(RooJavaType.ROO_ENTITY_PROJECTION);
// Show an error indicating that defaultReturnType should be annotated with
// @RooEntityProjection
Validate.notNull(defaultReturnTypeAnnotation,
"ERROR: Provided defaultReturnType should be annotated with @RooEntityProjection");
}
// Check if the new interface to be created already exists
final String interfaceIdentifier =
getPathResolver().getCanonicalPath(interfaceType.getModule(), Path.SRC_MAIN_JAVA,
interfaceType);
if (getFileManager().exists(interfaceIdentifier)) {
// Type already exists - return.
LOGGER.log(
Level.INFO,
String.format("INFO: The repository '%s' already exists.",
interfaceType.getSimpleTypeName()));
return;
}
// Check if already exists a repository that manage current entity
// Only one repository per entity is allowed
Set<ClassOrInterfaceTypeDetails> existingRepositories =
getTypeLocationService().findClassesOrInterfaceDetailsWithAnnotation(
RooJavaType.ROO_REPOSITORY_JPA);
for (ClassOrInterfaceTypeDetails existingRepository : existingRepositories) {
AnnotationAttributeValue<Object> relatedEntity =
existingRepository.getAnnotation(RooJavaType.ROO_REPOSITORY_JPA).getAttribute("entity");
if (relatedEntity.getValue().equals(domainType)) {
LOGGER
.log(
Level.INFO,
String
.format(
"INFO: Already exists a repository associated to the entity '%s'. Only one repository per entity is allowed.",
domainType.getSimpleTypeName()));
return;
}
}
// Add Springlets base repository class
addRepositoryConfigurationClass();
// Check if current entity is defined as "readOnly".
AnnotationAttributeValue<Boolean> readOnlyAttr =
entityDetails.getAnnotation(RooJavaType.ROO_JPA_ENTITY).getAttribute("readOnly");
boolean readOnly = readOnlyAttr != null && readOnlyAttr.getValue() ? true : false;
if (readOnly) {
// If is readOnly entity, generates common ReadOnlyRepository interface
generateReadOnlyRepository(interfaceType.getPackage());
}
// Generates repository interface
addRepositoryInterface(interfaceType, domainType, entityDetails, interfaceIdentifier,
defaultReturnType);
// By default, generate RepositoryCustom interface and its
// implementation that allow developers to include its dynamic queries
// using QueryDSL
addRepositoryCustom(domainType, interfaceType, interfaceType.getPackage());
// Add dependencies between modules
getProjectOperations().addModuleDependency(interfaceType.getModule(), domainType.getModule());
// Add dependencies and plugins
generateConfiguration(interfaceType, domainType);
}
/**
* Checks for all the application modules in project and adds a repository
* configuration class, which uses the Springlets base repository class if
* none is already specified.
*
*/
private void addRepositoryConfigurationClass() {
Set<ClassOrInterfaceTypeDetails> applicationCids =
getTypeLocationService().findClassesOrInterfaceDetailsWithAnnotation(
SpringJavaType.SPRING_BOOT_APPLICATION);
for (ClassOrInterfaceTypeDetails applicationCid : applicationCids) {
// Obtain main application config class and its module
Pom module =
getProjectOperations().getPomFromModuleName(applicationCid.getType().getModule());
// Create or update SpringDataJpaDetachableRepositoryConfiguration
JavaType repositoryConfigurationClass =
new JavaType(String.format("%s.config.SpringDataJpaDetachableRepositoryConfiguration",
getTypeLocationService().getTopLevelPackageForModule(module)), module.getModuleName());
Validate.notNull(repositoryConfigurationClass.getModule(),
"ERROR: Module name is required to generate a valid JavaType");
// Checks if new service interface already exists.
final String repositoryConfigurationClassIdentifier =
getPathResolver().getCanonicalPath(repositoryConfigurationClass.getModule(),
Path.SRC_MAIN_JAVA, repositoryConfigurationClass);
final String mid =
PhysicalTypeIdentifier.createIdentifier(repositoryConfigurationClass, getPathResolver()
.getPath(repositoryConfigurationClassIdentifier));
if (!getFileManager().exists(repositoryConfigurationClassIdentifier)) {
// Repository config class doesn't exist. Create class builder
final ClassOrInterfaceTypeDetailsBuilder typeBuilder =
new ClassOrInterfaceTypeDetailsBuilder(mid, PUBLIC, repositoryConfigurationClass,
PhysicalTypeCategory.CLASS);
// Add @RooJpaRepositoryConfiguration
AnnotationMetadataBuilder repositoryCondigurationAnnotation =
new AnnotationMetadataBuilder(RooJavaType.ROO_JPA_REPOSITORY_CONFIGURATION);
typeBuilder.addAnnotation(repositoryCondigurationAnnotation);
// Write new class disk
getTypeManagementService().createOrUpdateTypeOnDisk(typeBuilder.build());
}
}
}
/**
* Method that generates RepositoryCustom interface and its implementation
* for an specific entity
*
* @param domainType
* @param repositoryType
* @param repositoryPackage
* @param defaultReturnType
*
* @return JavaType with new RepositoryCustom interface.
*/
private JavaType addRepositoryCustom(JavaType domainType, JavaType repositoryType,
JavaPackage repositoryPackage) {
// Getting RepositoryCustom interface JavaTYpe
JavaType interfaceType =
new JavaType(repositoryPackage.getFullyQualifiedPackageName().concat(".")
.concat(repositoryType.getSimpleTypeName()).concat("Custom"),
repositoryType.getModule());
// Check if new interface exists yet
final String interfaceIdentifier =
getPathResolver().getCanonicalPath(interfaceType.getModule(), Path.SRC_MAIN_JAVA,
interfaceType);
if (getFileManager().exists(interfaceIdentifier)) {
// Type already exists - return
return interfaceType;
}
final String interfaceMdId =
PhysicalTypeIdentifier.createIdentifier(interfaceType,
getPathResolver().getPath(interfaceIdentifier));
final ClassOrInterfaceTypeDetailsBuilder interfaceBuilder =
new ClassOrInterfaceTypeDetailsBuilder(interfaceMdId, Modifier.PUBLIC, interfaceType,
PhysicalTypeCategory.INTERFACE);
// Generates @RooJpaRepositoryCustom annotation with referenced entity value
final AnnotationMetadataBuilder repositoryCustomAnnotationMetadata =
new AnnotationMetadataBuilder(RooJavaType.ROO_REPOSITORY_JPA_CUSTOM);
repositoryCustomAnnotationMetadata.addAttribute(new ClassAttributeValue(new JavaSymbolName(
"entity"), domainType));
interfaceBuilder.addAnnotation(repositoryCustomAnnotationMetadata);
// Save RepositoryCustom interface and its implementation on disk
getTypeManagementService().createOrUpdateTypeOnDisk(interfaceBuilder.build());
generateRepositoryCustomImpl(interfaceType, repositoryType, domainType);
return interfaceType;
}
/**
* Method that generates the repository interface. This method takes in mind
* if entity is defined as readOnly or not.
*
* @param interfaceType
* @param domainType
* @param entityDetails
* @param interfaceIdentifier
*/
private void addRepositoryInterface(JavaType interfaceType, JavaType domainType,
ClassOrInterfaceTypeDetails entityDetails, String interfaceIdentifier,
JavaType defaultReturnType) {
// Generates @RooJpaRepository annotation with referenced entity value
// and repository custom associated to this repository
final AnnotationMetadataBuilder interfaceAnnotationMetadata =
new AnnotationMetadataBuilder(RooJavaType.ROO_REPOSITORY_JPA);
interfaceAnnotationMetadata.addAttribute(new ClassAttributeValue(new JavaSymbolName("entity"),
domainType));
if (defaultReturnType != null) {
interfaceAnnotationMetadata.addAttribute(new ClassAttributeValue(new JavaSymbolName(
"defaultReturnType"), defaultReturnType));
// Add dependencies between modules
getProjectOperations().addModuleDependency(interfaceType.getModule(),
defaultReturnType.getModule());
}
// Generating interface
final String interfaceMdId =
PhysicalTypeIdentifier.createIdentifier(interfaceType,
getPathResolver().getPath(interfaceIdentifier));
final ClassOrInterfaceTypeDetailsBuilder cidBuilder =
new ClassOrInterfaceTypeDetailsBuilder(interfaceMdId, Modifier.PUBLIC, interfaceType,
PhysicalTypeCategory.INTERFACE);
// Annotate repository interface
cidBuilder.addAnnotation(interfaceAnnotationMetadata.build());
// Save new repository on disk
getTypeManagementService().createOrUpdateTypeOnDisk(cidBuilder.build());
}
@SuppressWarnings({"unchecked", "rawtypes"})
private void generateConfiguration(JavaType interfaceType, JavaType domainType) {
final Element configuration = XmlUtils.getConfiguration(getClass());
// Add querydsl dependency
final List<Element> dependencies;
final List<Element> plugins;
if (getProjectOperations().isMultimoduleProject()) {
dependencies =
XmlUtils
.findElements("/configuration/multimodule/dependencies/dependency", configuration);
plugins = XmlUtils.findElements("/configuration/multimodule/plugins/plugin", configuration);
// Add database test dependency
getJpaOperations().addDatabaseDependencyWithTestScope(interfaceType.getModule(), null, null);
} else {
dependencies =
XmlUtils.findElements("/configuration/monomodule/dependencies/dependency", configuration);
plugins = XmlUtils.findElements("/configuration/monomodule/plugins/plugin", configuration);
}
for (final Element dependencyElement : dependencies) {
getProjectOperations().addDependency(interfaceType.getModule(),
new Dependency(dependencyElement));
}
// Add querydsl plugin
Plugin queryDslPlugin = null;
for (final Element pluginElement : plugins) {
Plugin plugin = new Plugin(pluginElement);
if (plugin.getArtifactId().equals("querydsl-maven-plugin")) {
queryDslPlugin = plugin;
}
getProjectOperations().addBuildPlugin(interfaceType.getModule(), plugin);
}
if (getProjectOperations().isMultimoduleProject()) {
if (queryDslPlugin == null) {
throw new RuntimeException("Error: Missing QueryDSL plugin");
}
// Add entity package to find Q classes.
Set<String> packages = new HashSet();
for (ClassOrInterfaceTypeDetails cid : getTypeLocationService()
.findClassesOrInterfaceDetailsWithAnnotation(RooJavaType.ROO_REPOSITORY_JPA)) {
if (cid.getType().getModule().equals(interfaceType.getModule())) {
JavaType relatedEntity =
(JavaType) cid.getAnnotation(RooJavaType.ROO_REPOSITORY_JPA).getAttribute("entity")
.getValue();
String module =
getTypeLocationService().getTypeDetails(relatedEntity).getType().getModule();
if (!packages.contains(module)) {
packages.add(module);
getProjectOperations().addPackageToPluginExecution(interfaceType.getModule(),
queryDslPlugin, "generate-qtypes",
getProjectOperations().getTopLevelPackage(module).getFullyQualifiedPackageName());
}
}
}
getProjectOperations().addPackageToPluginExecution(
interfaceType.getModule(),
queryDslPlugin,
"generate-qtypes",
getProjectOperations().getTopLevelPackage(domainType.getModule())
.getFullyQualifiedPackageName());
} else {
// Add querydsl processor repository
List<Element> repositories =
XmlUtils.findElements("/configuration/monomodule/repositories/repository", configuration);
for (final Element repositoryElement : repositories) {
getProjectOperations().addRepository(interfaceType.getModule(),
new Repository(repositoryElement));
}
}
}
/**
* Method that generates ReadOnlyRepository interface on current package. If
* ReadOnlyRepository already exists in this or other package, will not be
* generated.
*
* @param repositoryPackage Package where ReadOnlyRepository should be
* generated
* @return JavaType with existing or new ReadOnlyRepository
*/
private JavaType generateReadOnlyRepository(JavaPackage repositoryPackage) {
// First of all, check if already exists a @RooReadOnlyRepository
// interface on current project
Set<JavaType> readOnlyRepositories =
getTypeLocationService().findTypesWithAnnotation(RooJavaType.ROO_READ_ONLY_REPOSITORY);
if (!readOnlyRepositories.isEmpty()) {
Iterator<JavaType> it = readOnlyRepositories.iterator();
while (it.hasNext()) {
return it.next();
}
}
final JavaType javaType =
new JavaType(String.format("%s.ReadOnlyRepository", repositoryPackage),
repositoryPackage.getModule());
final String physicalPath =
getPathResolver().getCanonicalPath(javaType.getModule(), Path.SRC_MAIN_JAVA, javaType);
// Including ReadOnlyRepository interface
InputStream inputStream = null;
try {
// Use defined template
inputStream = FileUtils.getInputStream(getClass(), "ReadOnlyRepository-template._java");
String input = IOUtils.toString(inputStream);
// Replacing package
input = input.replace("__PACKAGE__", repositoryPackage.getFullyQualifiedPackageName());
// Creating ReadOnlyRepository interface
getFileManager().createOrUpdateTextFileIfRequired(physicalPath, input, true);
} catch (final IOException e) {
throw new IllegalStateException(String.format("Unable to create '%s'", physicalPath), e);
} finally {
IOUtils.closeQuietly(inputStream);
}
return javaType;
}
/**
* Method that generates RepositoryCustom implementation on current package.
* If this RepositoryCustom implementation already exists in this or other
* package, will not be generated.
*
* @param interfaceType
* @param repository
* @param entity
* @return JavaType with existing or new RepositoryCustom implementation
*/
private JavaType generateRepositoryCustomImpl(JavaType interfaceType, JavaType repository,
JavaType entity) {
// Getting RepositoryCustomImpl JavaType
JavaType implType =
new JavaType(repository.getFullyQualifiedTypeName().concat("Impl"), repository.getModule());
// Check if new class exists yet
final String implIdentifier =
getPathResolver().getCanonicalPath(implType.getModule(), Path.SRC_MAIN_JAVA, implType);
if (getFileManager().exists(implIdentifier)) {
// Type already exists - return
return implType;
}
// Check if already exists some class annotated with
// @RooJpaRepositoryCustomImpl
// that implements the same repositoryCustom interface.
Set<JavaType> repositoriesCustomImpl =
getTypeLocationService()
.findTypesWithAnnotation(RooJavaType.ROO_REPOSITORY_JPA_CUSTOM_IMPL);
if (!repositoriesCustomImpl.isEmpty()) {
Iterator<JavaType> it = repositoriesCustomImpl.iterator();
while (it.hasNext()) {
JavaType repositoryCustom = it.next();
ClassOrInterfaceTypeDetails repositoryDetails =
getTypeLocationService().getTypeDetails(repositoryCustom);
AnnotationMetadata annotation =
repositoryDetails.getAnnotation(RooJavaType.ROO_REPOSITORY_JPA_CUSTOM_IMPL);
AnnotationAttributeValue<JavaType> repositoryType = annotation.getAttribute("repository");
if (repositoryType.getValue().equals(interfaceType)) {
return repositoryType.getValue();
}
}
}
// If not, continue creating new RepositoryCustomImpl
InputStream inputStream = null;
try {
// Use defined template
inputStream = FileUtils.getInputStream(getClass(), "RepositoryCustomImpl-template._java");
String input = IOUtils.toString(inputStream);
// Replacing package
input = input.replace("__PACKAGE__", implType.getPackage().getFullyQualifiedPackageName());
// Replacing entity import
input = input.replace("__ENTITY_IMPORT__", entity.getFullyQualifiedTypeName());
// Replacing interface .class
input = input.replace("__REPOSITORY_CUSTOM_INTERFACE__", interfaceType.getSimpleTypeName());
// Replacing class name
input = input.replaceAll("__REPOSITORY_CUSTOM_IMPL__", implType.getSimpleTypeName());
// Replacing entity name
input = input.replace("__ENTITY_NAME__", entity.getSimpleTypeName());
// Creating RepositoryCustomImpl class
getFileManager().createOrUpdateTextFileIfRequired(implIdentifier, input, false);
} catch (final IOException e) {
throw new IllegalStateException(String.format("Unable to create '%s'", implIdentifier), e);
} finally {
IOUtils.closeQuietly(inputStream);
}
return implType;
}
private FileManager getFileManager() {
return serviceInstaceManager.getServiceInstance(this, FileManager.class);
}
private PathResolver getPathResolver() {
return serviceInstaceManager.getServiceInstance(this, PathResolver.class);
}
private ProjectOperations getProjectOperations() {
return serviceInstaceManager.getServiceInstance(this, ProjectOperations.class);
}
public TypeManagementService getTypeManagementService() {
return serviceInstaceManager.getServiceInstance(this, TypeManagementService.class);
}
public TypeLocationService getTypeLocationService() {
return serviceInstaceManager.getServiceInstance(this, TypeLocationService.class);
}
public MemberDetailsScanner getMemberDetailsScanner() {
return serviceInstaceManager.getServiceInstance(this, MemberDetailsScanner.class);
}
public MetadataService getMetadataService() {
return serviceInstaceManager.getServiceInstance(this, MetadataService.class);
}
/**
* Method to get JpaOperations Service implementation
*
* @return
*/
public JpaOperations getJpaOperations() {
return serviceInstaceManager.getServiceInstance(this, JpaOperations.class);
}
@Override
public boolean shouldGenerateRepository(JavaType domainType) {
ClassOrInterfaceTypeDetails entityDetails = getTypeLocationService().getTypeDetails(domainType);
return shouldGenerateRepository(entityDetails);
}
private boolean shouldGenerateRepository(ClassOrInterfaceTypeDetails entity) {
Pair<FieldMetadata, RelationInfo> compositionRelation =
getJpaOperations().getFieldChildPartOfCompositionRelation(entity);
if (compositionRelation == null) {
return true;
}
return compositionRelation.getRight().cardinality != Cardinality.ONE_TO_ONE;
}
}