package org.springframework.roo.addon.layers.repository.jpa.addon;
import static org.springframework.roo.model.RooJavaType.ROO_READ_ONLY_REPOSITORY;
import static org.springframework.roo.model.RooJavaType.ROO_REPOSITORY_JPA;
import static org.springframework.roo.model.RooJavaType.ROO_REPOSITORY_JPA_CUSTOM;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import org.apache.commons.lang3.StringUtils;
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.service.component.ComponentContext;
import org.springframework.roo.addon.javabean.addon.JavaBeanMetadata;
import org.springframework.roo.addon.jpa.addon.JpaOperations;
import org.springframework.roo.addon.jpa.addon.entity.JpaEntityMetadata;
import org.springframework.roo.addon.jpa.addon.entity.JpaEntityMetadata.RelationInfo;
import org.springframework.roo.addon.layers.repository.jpa.addon.finder.parser.FinderAutocomplete;
import org.springframework.roo.addon.layers.repository.jpa.addon.finder.parser.FinderMethod;
import org.springframework.roo.addon.layers.repository.jpa.addon.finder.parser.FinderParameter;
import org.springframework.roo.addon.layers.repository.jpa.addon.finder.parser.PartTree;
import org.springframework.roo.addon.layers.repository.jpa.annotations.RooJpaRepository;
import org.springframework.roo.addon.layers.repository.jpa.annotations.finder.RooFinder;
import org.springframework.roo.classpath.PhysicalTypeIdentifier;
import org.springframework.roo.classpath.PhysicalTypeMetadata;
import org.springframework.roo.classpath.customdata.taggers.CustomDataKeyDecorator;
import org.springframework.roo.classpath.customdata.taggers.CustomDataKeyDecoratorTracker;
import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails;
import org.springframework.roo.classpath.details.FieldMetadata;
import org.springframework.roo.classpath.details.ItdTypeDetails;
import org.springframework.roo.classpath.details.MemberHoldingTypeDetails;
import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue;
import org.springframework.roo.classpath.details.annotations.AnnotationMetadata;
import org.springframework.roo.classpath.details.annotations.NestedAnnotationAttributeValue;
import org.springframework.roo.classpath.itd.AbstractMemberDiscoveringItdMetadataProvider;
import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem;
import org.springframework.roo.classpath.layers.LayerTypeMatcher;
import org.springframework.roo.classpath.scanner.MemberDetails;
import org.springframework.roo.metadata.MetadataDependencyRegistry;
import org.springframework.roo.metadata.MetadataIdentificationUtils;
import org.springframework.roo.metadata.internal.MetadataDependencyRegistryTracker;
import org.springframework.roo.model.JavaSymbolName;
import org.springframework.roo.model.JavaType;
import org.springframework.roo.model.RooJavaType;
import org.springframework.roo.project.LogicalPath;
import org.springframework.roo.support.logging.HandlerUtils;
/**
* Implementation of {@link RepositoryJpaMetadataProvider}.
*
* @author Stefan Schmidt
* @author Andrew Swan
* @author Enrique Ruiz at DISID Corporation S.L.
* @since 1.2.0
*/
@Component
@Service
public class RepositoryJpaMetadataProviderImpl extends AbstractMemberDiscoveringItdMetadataProvider
implements RepositoryJpaMetadataProvider, FinderAutocomplete {
protected final static Logger LOGGER = HandlerUtils
.getLogger(RepositoryJpaMetadataProviderImpl.class);
private final Map<JavaType, String> domainTypeToRepositoryMidMap =
new LinkedHashMap<JavaType, String>();
private final Map<String, JavaType> repositoryMidToDomainTypeMap =
new LinkedHashMap<String, JavaType>();
protected MetadataDependencyRegistryTracker registryTracker = null;
protected CustomDataKeyDecoratorTracker keyDecoratorTracker = null;
//Map where entity details will be cached
private Map<JavaType, MemberDetails> entitiesDetails = new HashMap<JavaType, MemberDetails>();
/**
* This service is being activated so setup it:
* <ul>
* <li>Create and open the {@link MetadataDependencyRegistryTracker}.</li>
* <li>Create and open the {@link CustomDataKeyDecoratorTracker}.</li>
* <li>Registers {@link RooJavaType#ROO_REPOSITORY_JPA} as additional
* JavaType that will trigger metadata registration.</li>
* <li>Set ensure the governor type details represent a class.</li>
* </ul>
*/
@Override
@SuppressWarnings("unchecked")
protected void activate(final ComponentContext cContext) {
super.activate(cContext);
super.setDependsOnGovernorBeingAClass(false);
this.registryTracker =
new MetadataDependencyRegistryTracker(cContext.getBundleContext(), this,
PhysicalTypeIdentifier.getMetadataIdentiferType(), getProvidesType());
this.registryTracker.open();
addMetadataTrigger(ROO_REPOSITORY_JPA);
this.keyDecoratorTracker =
new CustomDataKeyDecoratorTracker(cContext.getBundleContext(), getClass(),
new LayerTypeMatcher(ROO_REPOSITORY_JPA, new JavaSymbolName(
RooJpaRepository.ENTITY_ATTRIBUTE)));
this.keyDecoratorTracker.open();
}
/**
* This service is being deactivated so unregister upstream-downstream
* dependencies, triggers, matchers and listeners.
*
* @param context
*/
protected void deactivate(final ComponentContext context) {
MetadataDependencyRegistry registry = this.registryTracker.getService();
registry.removeNotificationListener(this);
registry.deregisterDependency(PhysicalTypeIdentifier.getMetadataIdentiferType(),
getProvidesType());
this.registryTracker.close();
removeMetadataTrigger(ROO_REPOSITORY_JPA);
CustomDataKeyDecorator keyDecorator = this.keyDecoratorTracker.getService();
keyDecorator.unregisterMatchers(getClass());
this.keyDecoratorTracker.close();
}
@Override
protected String createLocalIdentifier(final JavaType javaType, final LogicalPath path) {
return RepositoryJpaMetadata.createIdentifier(javaType, path);
}
@Override
protected String getGovernorPhysicalTypeIdentifier(final String metadataIdentificationString) {
final JavaType javaType = RepositoryJpaMetadata.getJavaType(metadataIdentificationString);
final LogicalPath path = RepositoryJpaMetadata.getPath(metadataIdentificationString);
return PhysicalTypeIdentifier.createIdentifier(javaType, path);
}
public String getItdUniquenessFilenameSuffix() {
return "Jpa_Repository";
}
@Override
protected String getLocalMidToRequest(final ItdTypeDetails itdTypeDetails) {
// Determine the governor for this ITD, and whether any metadata is even
// hoping to hear about changes to that JavaType and its ITDs
final JavaType governor = itdTypeDetails.getName();
final String localMid = domainTypeToRepositoryMidMap.get(governor);
if (localMid != null) {
return localMid;
}
final MemberHoldingTypeDetails memberHoldingTypeDetails =
getTypeLocationService().getTypeDetails(governor);
if (memberHoldingTypeDetails != null) {
for (final JavaType type : memberHoldingTypeDetails.getLayerEntities()) {
final String localMidType = domainTypeToRepositoryMidMap.get(type);
if (localMidType != null) {
return localMidType;
}
}
}
return null;
}
@Override
protected ItdTypeDetailsProvidingMetadataItem getMetadata(
final String metadataIdentificationString, final JavaType aspectName,
final PhysicalTypeMetadata governorPhysicalTypeMetadata, final String itdFilename) {
final RepositoryJpaAnnotationValues annotationValues =
new RepositoryJpaAnnotationValues(governorPhysicalTypeMetadata);
final JavaType domainType = annotationValues.getEntity();
// XXX clear entitiesDetails by security
entitiesDetails.clear();
// Remember that this entity JavaType matches up with this metadata
// identification string
// Start by clearing any previous association
final JavaType oldEntity = repositoryMidToDomainTypeMap.get(metadataIdentificationString);
if (oldEntity != null) {
domainTypeToRepositoryMidMap.remove(oldEntity);
}
domainTypeToRepositoryMidMap.put(domainType, metadataIdentificationString);
repositoryMidToDomainTypeMap.put(metadataIdentificationString, domainType);
// Getting associated entity
JavaType entity = annotationValues.getEntity();
ClassOrInterfaceTypeDetails entityDetails = getTypeLocationService().getTypeDetails(entity);
MemberDetails entityMemberDetails =
getMemberDetailsScanner().getMemberDetails(this.getClass().getName(), entityDetails);
final String entityMetadataId = JpaEntityMetadata.createIdentifier(entityDetails);
final JpaEntityMetadata entityMetadata = getMetadataService().get(entityMetadataId);
if (entityMetadata == null) {
return null;
}
// Getting java bean metadata
final String javaBeanMetadataKey = JavaBeanMetadata.createIdentifier(entityDetails);
// Register dependency between repositoryMetadata and jpaEntityMetadata
registerDependency(entityMetadataId, metadataIdentificationString);
// Create dependency between repository and java bean annotation
registerDependency(javaBeanMetadataKey, metadataIdentificationString);
// Check if related entity is setted as readOnly
JavaType readOnlyRepository = null;
if (entityMetadata.isReadOnly()) {
// Getting ReadOnlyRepository interface annotated with
// @RooReadOnlyRepository
Set<ClassOrInterfaceTypeDetails> readOnlyRepositories =
getTypeLocationService().findClassesOrInterfaceDetailsWithAnnotation(
ROO_READ_ONLY_REPOSITORY);
Validate
.notEmpty(
readOnlyRepositories,
"ERROR: You should define a ReadOnlyRepository interface annotated with @RooReadOnlyRepository to be able to generate repositories of readOnly entities.");
Iterator<ClassOrInterfaceTypeDetails> it = readOnlyRepositories.iterator();
while (it.hasNext()) {
ClassOrInterfaceTypeDetails readOnlyRepositoryDetails = it.next();
readOnlyRepository = readOnlyRepositoryDetails.getType();
break;
}
}
List<JavaType> repositoryCustomList = new ArrayList<JavaType>();
// Getting RepositoryCustom interface annotated with
// @RooJpaRepositoryCustom
Set<ClassOrInterfaceTypeDetails> customRepositories =
getTypeLocationService().findClassesOrInterfaceDetailsWithAnnotation(
ROO_REPOSITORY_JPA_CUSTOM);
Iterator<ClassOrInterfaceTypeDetails> customRepositoriesIt = customRepositories.iterator();
while (customRepositoriesIt.hasNext()) {
ClassOrInterfaceTypeDetails customRepository = customRepositoriesIt.next();
AnnotationMetadata annotation = customRepository.getAnnotation(ROO_REPOSITORY_JPA_CUSTOM);
if (annotation.getAttribute("entity").getValue().equals(entity)) {
repositoryCustomList.add(customRepository.getType());
}
}
Validate.notEmpty(repositoryCustomList,
"Can't find any interface annotated with @%s(entity=%s)",
ROO_REPOSITORY_JPA_CUSTOM.getSimpleTypeName(), entity.getSimpleTypeName());
Validate.isTrue(repositoryCustomList.size() == 1,
"More than one interface annotated with @%s(entity=%s): %s",
ROO_REPOSITORY_JPA_CUSTOM.getSimpleTypeName(), entity.getSimpleTypeName(),
StringUtils.join(repositoryCustomList, ","));
JavaType defaultReturnType;
final JavaType annotationDefaultReturnType = annotationValues.getDefaultReturnType();
// Get and check defaultReturnType
if (annotationDefaultReturnType == null || annotationDefaultReturnType.equals(JavaType.CLASS)
|| annotationDefaultReturnType.equals(entity)) {
defaultReturnType = entity;
} else {
// Validate that defaultReturnValue is projection of current entity
ClassOrInterfaceTypeDetails returnTypeCid =
getTypeLocationService().getTypeDetails(annotationDefaultReturnType);
AnnotationMetadata projectionAnnotation =
returnTypeCid.getAnnotation(RooJavaType.ROO_ENTITY_PROJECTION);
Validate.notNull(projectionAnnotation,
"ERROR: %s defined on %s.@%s.defaultReturnType must be annotated with @%s annotation",
annotationDefaultReturnType, governorPhysicalTypeMetadata.getType(),
RooJavaType.ROO_REPOSITORY_JPA, RooJavaType.ROO_ENTITY_PROJECTION);
Validate
.isTrue(
entity.equals(projectionAnnotation.getAttribute("entity").getValue()),
"ERROR: %s defined on %s.@%s.defaultReturnType must be annotated with @%s annotation and match the 'entity' attribute value",
annotationDefaultReturnType, governorPhysicalTypeMetadata.getType(),
RooJavaType.ROO_REPOSITORY_JPA, RooJavaType.ROO_ENTITY_PROJECTION);
defaultReturnType = annotationDefaultReturnType;
}
// Get field which entity is field part
List<Pair<FieldMetadata, RelationInfo>> relationsAsChild =
getJpaOperations().getFieldChildPartOfRelation(entityDetails);
// Get Annotation
ClassOrInterfaceTypeDetails cid = governorPhysicalTypeMetadata.getMemberHoldingTypeDetails();
AnnotationMetadata repositoryAnnotation = cid.getAnnotation(ROO_REPOSITORY_JPA);
// Create list of finder to add
List<FinderMethod> findersToAdd = new ArrayList<FinderMethod>();
// Create list of finder to add in RespositoryCustom
List<Pair<FinderMethod, PartTree>> findersToAddInCustom =
new ArrayList<Pair<FinderMethod, PartTree>>();
Map<JavaType, ClassOrInterfaceTypeDetails> detailsCache =
new HashMap<JavaType, ClassOrInterfaceTypeDetails>();
List<String> declaredFinderNames = new ArrayList<String>();
RooFinder[] findersAnnValue = annotationValues.getFinders();
// Get finders attributes
AnnotationAttributeValue<?> currentFinders = repositoryAnnotation.getAttribute("finders");
if (currentFinders != null) {
List<?> values = (List<?>) currentFinders.getValue();
Iterator<?> valuesIt = values.iterator();
while (valuesIt.hasNext()) {
NestedAnnotationAttributeValue finderAnnotation =
(NestedAnnotationAttributeValue) valuesIt.next();
if (finderAnnotation.getValue() != null
&& finderAnnotation.getValue().getAttribute("value") != null) {
// Get finder name
String finderName = null;
if (finderAnnotation.getValue().getAttribute("value").getValue() instanceof String) {
finderName = (String) finderAnnotation.getValue().getAttribute("value").getValue();
}
Validate.notNull(finderName, "'finder' attribute in @RooFinder must be a String");
declaredFinderNames.add(finderName);
// Get finder return type
JavaType returnType = getNestedAttributeValue(finderAnnotation, "returnType");
JavaType finderReturnType;
if (returnType == null || JavaType.CLASS.equals(returnType)) {
finderReturnType = defaultReturnType;
returnType = null;
} else {
finderReturnType = returnType;
}
// Get finder return type
JavaType formBean = getNestedAttributeValue(finderAnnotation, "formBean");
boolean isDeclaredFormBean = false;
if (JavaType.CLASS.equals(formBean)) {
formBean = null;
}
// Create FinderMethods
PartTree finder = new PartTree(finderName, entityMemberDetails, this, finderReturnType);
Validate
.notNull(
finder,
String
.format(
"ERROR: '%s' is not a valid finder. Use autocomplete feature (TAB or CTRL + Space) to include finder that follows Spring Data nomenclature.",
finderName));
FinderMethod finderMethod = new FinderMethod(finder);
// Add dependencies between modules
List<JavaType> types = new ArrayList<JavaType>();
types.add(finder.getReturnType());
types.addAll(finder.getReturnType().getParameters());
for (FinderParameter parameter : finder.getParameters()) {
types.add(parameter.getType());
types.addAll(parameter.getType().getParameters());
}
for (JavaType parameter : types) {
getTypeLocationService().addModuleDependency(
governorPhysicalTypeMetadata.getType().getModule(), parameter);
}
if (formBean == null && (returnType == null || entity.equals(returnType))) {
// Add to finder methods list
findersToAdd.add(finderMethod);
} else {
// If defaultReturnType is a Projection or formBean is a DTO, finder creation
// should be avoided here and let RepositoryJpaCustomMetadata create it on
// RepositoryCustom classes.
if (returnType != null && !returnType.equals(entity)) {
ClassOrInterfaceTypeDetails returnTypeDetails =
getDetailsFor(returnType, detailsCache);
Validate
.isTrue(
returnTypeDetails != null
&& returnTypeDetails.getAnnotation(RooJavaType.ROO_ENTITY_PROJECTION) != null,
"ERROR: finder '%s' declared 'returnType' (%s) is not annotated with @%s annotation",
finderName, returnType.getSimpleTypeName(),
RooJavaType.ROO_ENTITY_PROJECTION.getSimpleTypeName());
}
Validate.notNull(formBean,
"ERROR: finder '%s' requires 'formBean' value if 'defaultReturnType' is defined",
finderName);
ClassOrInterfaceTypeDetails formBeanDetails =
getTypeLocationService().getTypeDetails(formBean);
Validate.isTrue(
formBeanDetails != null
&& formBeanDetails.getAnnotation(RooJavaType.ROO_DTO) != null,
"ERROR: finder '%s' declared 'formBean' (%s) is not annotated with @%s annotation",
finderName, formBean.getSimpleTypeName(),
RooJavaType.ROO_ENTITY_PROJECTION.getSimpleTypeName());
checkDtoFieldsForFinder(formBeanDetails, finder, governorPhysicalTypeMetadata.getType());
if (returnType == null) {
returnType = entity;
}
finderMethod =
new FinderMethod(returnType, new JavaSymbolName(finderName),
Arrays.asList(new FinderParameter(formBean, new JavaSymbolName(StringUtils
.uncapitalize(formBean.getSimpleTypeName())))));
findersToAddInCustom.add(Pair.of(finderMethod, finder));
}
}
}
}
return new RepositoryJpaMetadata(metadataIdentificationString, aspectName,
governorPhysicalTypeMetadata, annotationValues, entityMetadata, readOnlyRepository,
repositoryCustomList.get(0), defaultReturnType, relationsAsChild, findersToAdd,
findersToAddInCustom, declaredFinderNames);
}
/**
* Validates that all dto fields matches with parameters on finder of a repository
*
* @param formBeanDetails
* @param finder
* @param repository
*/
private void checkDtoFieldsForFinder(ClassOrInterfaceTypeDetails formBeanDetails,
PartTree finder, JavaType repository) {
final JavaType dtoType = formBeanDetails.getName();
final String finderName = finder.getOriginalQuery();
FieldMetadata paramField, dtoField;
JavaType paramType, dtoFieldType;
for (FinderParameter param : finder.getParameters()) {
paramField = param.getPath().peek();
dtoField = formBeanDetails.getField(paramField.getFieldName());
Validate.notNull(dtoField, "Field '%s' not found on DTO %s for finder '%s' of %s",
paramField.getFieldName(), dtoType, finderName, repository);
paramType = paramField.getFieldType().getBaseType();
dtoFieldType = dtoField.getFieldType().getBaseType();
Validate.isTrue(paramType.equals(dtoFieldType),
"Type missmatch for field '%s' on DTO %s for finder '%s' of %s: excepted %s and got %s",
dtoField.getFieldName(), dtoType, finderName, repository, paramType, dtoFieldType);
}
}
/**
* Gets attributo of a nested annotation attribute value
*
* @param newstedAnnotationAttr
* @param attributeName
* @return
*/
private <T> T getNestedAttributeValue(NestedAnnotationAttributeValue newstedAnnotationAttr,
String attributeName) {
AnnotationMetadata annotationValue = newstedAnnotationAttr.getValue();
if (annotationValue == null) {
return null;
}
AnnotationAttributeValue<?> attribute = annotationValue.getAttribute(attributeName);
if (attribute == null) {
return null;
}
return (T) attribute.getValue();
}
private ClassOrInterfaceTypeDetails getDetailsFor(JavaType type,
Map<JavaType, ClassOrInterfaceTypeDetails> detailsCache) {
if (detailsCache.containsKey(type)) {
return detailsCache.get(type);
}
ClassOrInterfaceTypeDetails details = getTypeLocationService().getTypeDetails(type);
detailsCache.put(type, details);
return details;
}
private JpaOperations getJpaOperations() {
return getServiceManager().getServiceInstance(this, JpaOperations.class);
}
private RepositoryJpaLocator getRepositoryJpaLocator() {
return getServiceManager().getServiceInstance(this, RepositoryJpaLocator.class);
}
public String getProvidesType() {
return RepositoryJpaMetadata.getMetadataIdentiferType();
}
protected void registerDependency(final String upstreamDependency,
final String downStreamDependency) {
if (getMetadataDependencyRegistry() != null
&& StringUtils.isNotBlank(upstreamDependency)
&& StringUtils.isNotBlank(downStreamDependency)
&& !upstreamDependency.equals(downStreamDependency)
&& !MetadataIdentificationUtils.getMetadataClass(downStreamDependency).equals(
MetadataIdentificationUtils.getMetadataClass(upstreamDependency))) {
getMetadataDependencyRegistry().registerDependency(upstreamDependency, downStreamDependency);
}
}
@Override
public MemberDetails getEntityDetails(JavaType entity) {
Validate.notNull(entity, "ERROR: Entity should be provided");
if (entitiesDetails.containsKey(entity)) {
return entitiesDetails.get(entity);
}
// We know the file exists, as there's already entity metadata for it
final ClassOrInterfaceTypeDetails cid = getTypeLocationService().getTypeDetails(entity);
if (cid == null) {
return null;
}
if (cid.getAnnotation(RooJavaType.ROO_JPA_ENTITY) == null) {
return null;
}
entitiesDetails.put(entity,
getMemberDetailsScanner().getMemberDetails(getClass().getName(), cid));
return entitiesDetails.get(entity);
}
}