package org.springframework.roo.addon.gwt.request;
import static java.lang.reflect.Modifier.ABSTRACT;
import static java.lang.reflect.Modifier.STATIC;
import static org.springframework.roo.addon.gwt.GwtJavaType.INSTANCE_REQUEST;
import static org.springframework.roo.addon.gwt.GwtJavaType.OLD_REQUEST_CONTEXT;
import static org.springframework.roo.addon.gwt.GwtJavaType.REQUEST;
import static org.springframework.roo.addon.gwt.GwtJavaType.REQUEST_CONTEXT;
import static org.springframework.roo.addon.gwt.GwtJavaType.SERVICE_NAME;
import static org.springframework.roo.classpath.customdata.CustomDataKeys.COUNT_ALL_METHOD;
import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ALL_METHOD;
import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_ENTRIES_METHOD;
import static org.springframework.roo.classpath.customdata.CustomDataKeys.FIND_METHOD;
import static org.springframework.roo.classpath.customdata.CustomDataKeys.PERSIST_METHOD;
import static org.springframework.roo.classpath.customdata.CustomDataKeys.REMOVE_METHOD;
import static org.springframework.roo.model.JavaType.INT_PRIMITIVE;
import static org.springframework.roo.model.JavaType.LONG_PRIMITIVE;
import static org.springframework.roo.model.JavaType.VOID_PRIMITIVE;
import static org.springframework.roo.model.RooJavaType.ROO_GWT_REQUEST;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
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.osgi.service.component.ComponentContext;
import org.springframework.roo.addon.gwt.GwtFileManager;
import org.springframework.roo.addon.gwt.GwtTypeService;
import org.springframework.roo.addon.gwt.GwtUtils;
import org.springframework.roo.classpath.PhysicalTypeIdentifier;
import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils;
import org.springframework.roo.classpath.TypeLocationService;
import org.springframework.roo.classpath.customdata.tagkeys.MethodMetadataCustomDataKey;
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.MethodMetadata;
import org.springframework.roo.classpath.details.MethodMetadataBuilder;
import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType;
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.StringAttributeValue;
import org.springframework.roo.classpath.layers.LayerService;
import org.springframework.roo.classpath.layers.LayerType;
import org.springframework.roo.classpath.layers.MemberTypeAdditions;
import org.springframework.roo.classpath.layers.MethodParameter;
import org.springframework.roo.classpath.persistence.PersistenceMemberLocator;
import org.springframework.roo.classpath.scanner.MemberDetailsScanner;
import org.springframework.roo.metadata.AbstractHashCodeTrackingMetadataNotifier;
import org.springframework.roo.metadata.MetadataItem;
import org.springframework.roo.model.DataType;
import org.springframework.roo.model.JavaSymbolName;
import org.springframework.roo.model.JavaType;
import org.springframework.roo.project.LogicalPath;
import org.springframework.roo.project.ProjectMetadata;
import org.springframework.roo.project.ProjectOperations;
@Component(immediate = true)
@Service
public class GwtRequestMetadataProviderImpl extends
AbstractHashCodeTrackingMetadataNotifier implements
GwtRequestMetadataProvider {
private static final int LAYER_POSITION = LayerType.HIGHEST.getPosition();
@Reference GwtFileManager gwtFileManager;
@Reference GwtTypeService gwtTypeService;
@Reference LayerService layerService;
@Reference MemberDetailsScanner memberDetailsScanner;
@Reference PersistenceMemberLocator persistenceMemberLocator;
@Reference ProjectOperations projectOperations;
@Reference TypeLocationService typeLocationService;
protected void activate(final ComponentContext context) {
metadataDependencyRegistry.registerDependency(
PhysicalTypeIdentifier.getMetadataIdentiferType(),
getProvidesType());
}
protected void deactivate(final ComponentContext context) {
metadataDependencyRegistry.deregisterDependency(
PhysicalTypeIdentifier.getMetadataIdentiferType(),
getProvidesType());
}
public MetadataItem get(final String requestMetadataId) {
final ProjectMetadata projectMetadata = projectOperations
.getProjectMetadata(PhysicalTypeIdentifierNamingUtils
.getModule(requestMetadataId));
if (projectMetadata == null) {
return null;
}
final ClassOrInterfaceTypeDetails requestInterface = getGovernor(requestMetadataId);
if (requestInterface == null) {
return null;
}
final AnnotationMetadata gwtRequestAnnotation = requestInterface
.getAnnotation(ROO_GWT_REQUEST);
if (gwtRequestAnnotation == null) {
return null;
}
final JavaType entityType = new JavaType((String) gwtRequestAnnotation
.getAttribute("value").getValue());
// Get the methods to be invoked and the type(s) that provide them
// (should only be one such type, or null)
final Map<MethodMetadata, FieldMetadata> requestMethods = getRequestMethodsAndInvokedTypes(
entityType, requestMetadataId);
if (requestMethods == null) {
return null;
}
final JavaType invokedType = getInvokedType(requestMethods.values());
final String requestTypeContents = writeRequestInterface(
requestInterface, invokedType, requestMethods.keySet(),
entityType, requestMetadataId);
final GwtRequestMetadata gwtRequestMetadata = new GwtRequestMetadata(
requestMetadataId, requestTypeContents);
notifyIfRequired(gwtRequestMetadata);
return gwtRequestMetadata;
}
private ClassOrInterfaceTypeDetails getGovernor(
final String metadataIdentificationString) {
final JavaType governorTypeName = GwtRequestMetadata
.getJavaType(metadataIdentificationString);
final LogicalPath governorTypePath = GwtRequestMetadata
.getPath(metadataIdentificationString);
final String physicalTypeId = PhysicalTypeIdentifier.createIdentifier(
governorTypeName, governorTypePath);
return typeLocationService.getTypeDetails(physicalTypeId);
}
/**
* Returns the type on which the given request methods will be invoked
*
* @param invokedFields the autowired fields invoked by layer method calls
* (can include <code>null</code> elements for 'active record'
* calls)
* @return <code>null</code> if active record is being used, otherwise a
* layer component type
*/
private JavaType getInvokedType(
final Collection<FieldMetadata> invokedFields) {
final Collection<JavaType> distinctInvokedTypes = new HashSet<JavaType>();
for (final FieldMetadata invokedField : invokedFields) {
if (invokedField == null) {
distinctInvokedTypes.add(null);
}
else {
distinctInvokedTypes.add(invokedField.getFieldType());
}
}
Validate.isTrue(distinctInvokedTypes.size() == 1,
"Expected one invoked type but found: " + distinctInvokedTypes);
return distinctInvokedTypes.iterator().next();
}
public String getProvidesType() {
return GwtRequestMetadata.getMetadataIdentifierType();
}
private MethodMetadataBuilder getRequestMethod(
final ClassOrInterfaceTypeDetails request,
final MethodMetadata method, final JavaType returnType) {
final ClassOrInterfaceTypeDetails entity = gwtTypeService
.lookupEntityFromRequest(request);
if (entity == null) {
return null;
}
final List<AnnotatedJavaType> parameterTypes = new ArrayList<AnnotatedJavaType>();
for (final AnnotatedJavaType parameterType : method.getParameterTypes()) {
parameterTypes.add(new AnnotatedJavaType(gwtTypeService
.getGwtSideLeafType(parameterType.getJavaType(),
entity.getType(), true, false)));
}
return new MethodMetadataBuilder(request.getDeclaredByMetadataId(),
ABSTRACT, method.getMethodName(), returnType, parameterTypes,
method.getParameterNames(), null);
}
private MethodMetadataBuilder getRequestMethod(
final ClassOrInterfaceTypeDetails request,
final MethodMetadata method, final JavaType entityType,
final JavaType invokedType) {
final ClassOrInterfaceTypeDetails proxy = gwtTypeService
.lookupProxyFromRequest(request);
if (proxy == null) {
return null;
}
final JavaType methodReturnType = getRequestMethodReturnType(
invokedType, method, proxy.getType());
return getRequestMethod(request, method, methodReturnType);
}
private MethodMetadata getRequestMethod(final JavaType entity,
final MethodMetadataCustomDataKey methodKey,
final MemberTypeAdditions memberTypeAdditions,
final String declaredByMetadataId) {
final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder(
declaredByMetadataId); // wrong MID, but doesn't matter here
methodBuilder.setMethodName(new JavaSymbolName(memberTypeAdditions
.getMethodName()));
if (memberTypeAdditions.isStatic()) {
// OK to overwrite any other modifiers
methodBuilder.setModifier(STATIC);
}
/*
* TODO make sure the active record instance methods have the correct
* parameters
*
* expected: abstract
* InstanceRequest<com.example.gwtbug.client.proxy.ThingProxy,
* java.lang.Void> persist(); actual: abstract Request<java.lang.Void>
* persist(ThingProxy proxy);
*/
for (final MethodParameter methodParameter : memberTypeAdditions
.getMethodParameters()) {
methodBuilder.addParameter(methodParameter.getValue()
.getSymbolName(), methodParameter.getKey());
}
final JavaType returnType = getReturnType(methodKey, entity);
final JavaType gwtType = gwtTypeService.getGwtSideLeafType(returnType,
entity, true, true);
methodBuilder.setReturnType(gwtType);
return methodBuilder.build();
}
private JavaType getRequestMethodReturnType(final JavaType invokedType,
final MethodMetadata method, final JavaType proxyType) {
if (invokedType == null && !method.isStatic()) {
// Calling an active record method that's non-static (i.e. target is
// an entity instance)
final List<JavaType> methodReturnTypeArgs = Arrays.asList(
proxyType, method.getReturnType());
return new JavaType(INSTANCE_REQUEST.getFullyQualifiedTypeName(),
0, DataType.TYPE, null, methodReturnTypeArgs);
}
final List<JavaType> methodReturnTypeArgs = Collections
.singletonList(method.getReturnType());
return new JavaType(REQUEST.getFullyQualifiedTypeName(), 0,
DataType.TYPE, null, methodReturnTypeArgs);
}
private Map<MethodMetadata, FieldMetadata> getRequestMethodsAndInvokedTypes(
final JavaType entity, final String requestMetadataId) {
final JavaType idType = persistenceMemberLocator
.getIdentifierType(entity);
if (idType == null) {
return null;
}
final Map<MethodMetadata, FieldMetadata> requestMethods = new LinkedHashMap<MethodMetadata, FieldMetadata>();
for (final Entry<MethodMetadataCustomDataKey, Collection<MethodParameter>> methodSignature : getRequestMethodSignatures(
entity, idType).entrySet()) {
final String methodId = methodSignature.getKey().name();
final MemberTypeAdditions memberTypeAdditions = layerService
.getMemberTypeAdditions(requestMetadataId, methodId,
entity, idType, LAYER_POSITION,
methodSignature.getValue());
Validate.notNull(memberTypeAdditions, "No support for " + methodId
+ " method for domain type " + entity);
final MethodMetadata requestMethod = getRequestMethod(entity,
methodSignature.getKey(), memberTypeAdditions,
requestMetadataId);
requestMethods.put(requestMethod,
memberTypeAdditions.getInvokedField());
}
return requestMethods;
}
private Map<MethodMetadataCustomDataKey, Collection<MethodParameter>> getRequestMethodSignatures(
final JavaType domainType, final JavaType idType) {
final Map<MethodMetadataCustomDataKey, Collection<MethodParameter>> signatures = new LinkedHashMap<MethodMetadataCustomDataKey, Collection<MethodParameter>>();
final List<MethodParameter> noArgs = Arrays.asList();
signatures.put(COUNT_ALL_METHOD, noArgs);
signatures.put(FIND_ALL_METHOD, noArgs);
signatures.put(FIND_ENTRIES_METHOD, Arrays.asList(new MethodParameter(
INT_PRIMITIVE, "firstResult"), new MethodParameter(
INT_PRIMITIVE, "maxResults")));
signatures.put(FIND_METHOD,
Arrays.asList(new MethodParameter(idType, "id")));
final List<MethodParameter> proxyParameterAsList = Arrays
.asList(new MethodParameter(domainType, "proxy"));
signatures.put(PERSIST_METHOD, proxyParameterAsList);
signatures.put(REMOVE_METHOD, proxyParameterAsList);
return signatures;
}
private JavaType getReturnType(final MethodMetadataCustomDataKey methodKey,
final JavaType entity) {
if (COUNT_ALL_METHOD.equals(methodKey)) {
return LONG_PRIMITIVE;
}
if (FIND_ALL_METHOD.equals(methodKey)
|| FIND_ENTRIES_METHOD.equals(methodKey)) {
return JavaType.listOf(entity);
}
if (FIND_METHOD.equals(methodKey)) {
return entity;
}
if (PERSIST_METHOD.equals(methodKey) || REMOVE_METHOD.equals(methodKey)) {
return VOID_PRIMITIVE;
}
throw new IllegalStateException("Unexpected method key " + methodKey);
}
private AnnotationMetadata getServiceNameAnnotation(
final ClassOrInterfaceTypeDetails request,
final JavaType invokedType, final JavaType entityType,
final String requestMetadataId) {
final List<AnnotationAttributeValue<?>> serviceAttributeValues = new ArrayList<AnnotationAttributeValue<?>>();
if (invokedType == null) {
// Active record; specify the entity type as the invoked "service"
final StringAttributeValue stringAttributeValue = new StringAttributeValue(
new JavaSymbolName("value"),
entityType.getFullyQualifiedTypeName());
serviceAttributeValues.add(stringAttributeValue);
}
else {
// Layer component, e.g. repository or service; specify its type as
// the invoked "service"
final StringAttributeValue stringAttributeValue = new StringAttributeValue(
new JavaSymbolName("value"),
invokedType.getFullyQualifiedTypeName());
serviceAttributeValues.add(stringAttributeValue);
// Specify the locator that GWT will use to find it
final LogicalPath requestLogicalPath = PhysicalTypeIdentifier
.getPath(request.getDeclaredByMetadataId());
final JavaType serviceLocator = gwtTypeService
.getServiceLocator(requestLogicalPath.getModule());
final StringAttributeValue locatorAttributeValue = new StringAttributeValue(
new JavaSymbolName("locator"),
serviceLocator.getFullyQualifiedTypeName());
serviceAttributeValues.add(locatorAttributeValue);
}
return new AnnotationMetadataBuilder(SERVICE_NAME,
serviceAttributeValues).build();
}
/**
* Creates or updates the entity-specific request interface with
*
* @param request
* @param requestMethods the methods to declare in the interface, mapped to
* the injected field type on which they are invoked (required)
* @param entityType
* @param requestMetadataId
* @return the Java source code for the request interface
*/
private String writeRequestInterface(
final ClassOrInterfaceTypeDetails request,
final JavaType invokedType,
final Iterable<MethodMetadata> requestMethods,
final JavaType entityType, final String requestMetadataId) {
final ClassOrInterfaceTypeDetailsBuilder typeDetailsBuilder = new ClassOrInterfaceTypeDetailsBuilder(
request);
// Service name annotation (@RooGwtRequest was already applied by
// GwtOperationsImpl#createRequestInterface)
typeDetailsBuilder.removeAnnotation(SERVICE_NAME);
typeDetailsBuilder.addAnnotation(getServiceNameAnnotation(request,
invokedType, entityType, requestMetadataId));
// Super-interface
typeDetailsBuilder.removeExtendsTypes(OLD_REQUEST_CONTEXT);
if (!typeDetailsBuilder.getExtendsTypes().contains(REQUEST_CONTEXT)) {
typeDetailsBuilder.addExtendsTypes(REQUEST_CONTEXT);
}
typeDetailsBuilder.clearDeclaredMethods();
for (final MethodMetadata method : requestMethods) {
typeDetailsBuilder.addMethod(getRequestMethod(request, method,
entityType, invokedType));
}
return gwtFileManager.write(typeDetailsBuilder.build(),
GwtUtils.PROXY_REQUEST_WARNING);
}
}