public final void notify(final String upstreamDependency, String downstreamDependency) {
    if (downstreamDependency == null) {
      notifyForGenericListener(upstreamDependency);
      return;
    }

    // Handle if the downstream dependency is "class level",  meaning we need to figure out the
    // specific downstream MID this metadata provider wants to update/refresh.
    if (MetadataIdentificationUtils.isIdentifyingClass(downstreamDependency)) {
      // We have not identified an instance-specific downstream MID, so we'll need to calculate an
      // instance-specific downstream MID to retrieve.
      downstreamDependency = resolveDownstreamDependencyIdentifier(upstreamDependency);

      // We skip if the resolution method returns null, as it doesn't want to continue for some
      // reason
      if (downstreamDependency == null) {
        return;
      }

      Assert.isTrue(
          MetadataIdentificationUtils.isIdentifyingInstance(downstreamDependency),
          "An instance-specific downstream MID was required by '"
              + getClass().getName()
              + "' (not '"
              + downstreamDependency
              + "')");

      // We only need to proceed if the downstream dependency relationship is not already
      // registered.
      // It is unusual to register a direct downstream relationship given it costs dependency
      // registration memory and class-level notifications will always occur anyway.
      if (metadataDependencyRegistry
          .getDownstream(upstreamDependency)
          .contains(downstreamDependency)) {
        return;
      }
    }

    // We should now have an instance-specific "downstream dependency" that can be processed by this
    // class
    Assert.isTrue(
        MetadataIdentificationUtils.getMetadataClass(downstreamDependency)
            .equals(MetadataIdentificationUtils.getMetadataClass(getProvidesType())),
        "Unexpected downstream notification for '"
            + downstreamDependency
            + "' to this provider (which uses '"
            + getProvidesType()
            + "'");

    // We no longer notify downstreams here, as the "get" operation with eviction will ensure the
    // main get(String) method below will be fired and it
    // directly notified downstreams as part of that method (BPA 10 Dec 2010)
    metadataService.get(downstreamDependency, true);
  }
 public final String getIdForPhysicalJavaType(final String physicalJavaTypeIdentifier) {
   Assert.isTrue(
       MetadataIdentificationUtils.getMetadataClass(physicalJavaTypeIdentifier)
           .equals(
               MetadataIdentificationUtils.getMetadataClass(
                   PhysicalTypeIdentifier.getMetadataIdentiferType())),
       "Expected a valid physical Java type instance identifier (not '"
           + physicalJavaTypeIdentifier
           + "')");
   JavaType javaType = PhysicalTypeIdentifier.getJavaType(physicalJavaTypeIdentifier);
   ContextualPath path = PhysicalTypeIdentifier.getPath(physicalJavaTypeIdentifier);
   return createLocalIdentifier(javaType, path);
 }
  public Set<String> getUpstream(final String downstreamDependency) {
    Validate.isTrue(
        MetadataIdentificationUtils.isValid(downstreamDependency),
        "Downstream dependency is an invalid metadata identification string ('%s')",
        downstreamDependency);

    final Set<String> upstream = downstreamKeyed.get(downstreamDependency);
    if (upstream == null) {
      return new HashSet<String>();
    }

    return Collections.unmodifiableSet(upstream);
  }
  public Set<String> getDownstream(final String upstreamDependency) {
    Validate.isTrue(
        MetadataIdentificationUtils.isValid(upstreamDependency),
        "Upstream dependency is an invalid metadata identification string ('%s')",
        upstreamDependency);

    final Set<String> downstream = upstreamKeyed.get(upstreamDependency);
    if (downstream == null) {
      return new HashSet<String>();
    }

    return Collections.unmodifiableSet(new CopyOnWriteArraySet<String>(downstream));
  }
  /**
   * Assists creating a local metadata identification string (MID) from any presented {@link
   * MemberHoldingTypeDetails} implementation. This is achieved by extracting the {@link
   * IdentifiableJavaStructure#getDeclaredByMetadataId()} and converting it into a {@link JavaType}
   * and {@link Path}, then calling {@link #createLocalIdentifier(JavaType, Path)}.
   *
   * @param memberHoldingTypeDetails the member holder from which the declaring type information
   *     should be extracted (required)
   * @return a MID produced by {@link #createLocalIdentifier(JavaType, Path)} for the extracted Java
   *     type in the extract Path (never null)
   */
  protected String getLocalMid(final MemberHoldingTypeDetails memberHoldingTypeDetails) {
    JavaType governorType = memberHoldingTypeDetails.getName();

    // Extract out the metadata provider class (we need this later to extract just the Path it is
    // located in)
    String providesType =
        MetadataIdentificationUtils.getMetadataClass(
            memberHoldingTypeDetails.getDeclaredByMetadataId());
    ContextualPath path =
        PhysicalTypeIdentifierNamingUtils.getPath(
            providesType, memberHoldingTypeDetails.getDeclaredByMetadataId());
    // Produce the local MID we're going to use to make the request
    return createLocalIdentifier(governorType, path);
  }
  public void deregisterDependency(
      final String upstreamDependency, final String downstreamDependency) {
    Validate.isTrue(
        MetadataIdentificationUtils.isValid(upstreamDependency),
        "Upstream dependency is an invalid metadata identification string ('%s')",
        upstreamDependency);
    Validate.isTrue(
        MetadataIdentificationUtils.isValid(downstreamDependency),
        "Downstream dependency is an invalid metadata identification string ('%s')",
        downstreamDependency);

    // Maintain the upstream-keyed map, if it even exists
    final Set<String> downstream = upstreamKeyed.get(upstreamDependency);
    if (downstream != null) {
      downstream.remove(downstreamDependency);
    }

    // Maintain the downstream-keyed map, if it even exists
    final Set<String> upstream = downstreamKeyed.get(downstreamDependency);
    if (upstream != null) {
      upstream.remove(upstreamDependency);
    }
  }
  public boolean isValidDependency(
      final String upstreamDependency, final String downstreamDependency) {
    Validate.isTrue(
        MetadataIdentificationUtils.isValid(upstreamDependency),
        "Upstream dependency is an invalid metadata identification string ('%s')",
        upstreamDependency);
    Validate.isTrue(
        MetadataIdentificationUtils.isValid(downstreamDependency),
        "Downstream dependency is an invalid metadata identification string ('%s')",
        downstreamDependency);
    Validate.isTrue(
        !upstreamDependency.equals(downstreamDependency),
        "Upstream dependency cannot be the same as the downstream dependency ('%s')",
        downstreamDependency);

    // The simplest possible outcome is the relationship already exists, so
    // quickly return in that case
    Set<String> downstream = upstreamKeyed.get(upstreamDependency);
    if (downstream != null && downstream.contains(downstreamDependency)) {
      return true;
    }
    // Don't need the variable anymore, as we don't care about the other
    // downstream dependencies
    downstream = null;

    // Need to walk the upstream dependency's parent dependency graph,
    // verifying no presence of the proposed downstream dependency

    // Need to build a set representing every eventual upstream dependency
    // of the indicated upstream dependency
    final Set<String> allUpstreams = new HashSet<String>();
    buildSetOfAllUpstreamDependencies(allUpstreams, upstreamDependency);

    // The dependency is valid if none of the upstreams depend on the
    // proposed downstream
    return !allUpstreams.contains(downstreamDependency);
  }
  public void deregisterDependencies(final String downstreamDependency) {
    Validate.isTrue(
        MetadataIdentificationUtils.isValid(downstreamDependency),
        "Downstream dependency is an invalid metadata identification string ('%s')",
        downstreamDependency);

    // Acquire the keys to delete
    final Set<String> upstream = downstreamKeyed.get(downstreamDependency);
    if (upstream == null) {
      return;
    }

    final Set<String> upstreamToDelete = new HashSet<String>(upstream);

    // Delete them normally
    for (final String deleteUpstream : upstreamToDelete) {
      deregisterDependency(deleteUpstream, downstreamDependency);
    }
  }
Example #9
0
/**
 * This type produces metadata for a new ITD. It uses an {@link ItdTypeDetailsBuilder} provided by
 * {@link AbstractItdTypeDetailsProvidingMetadataItem} to register a field in the ITD and a new
 * method.
 *
 * @author jccastrejon
 */
public class GraphMetadata extends AbstractItdTypeDetailsProvidingMetadataItem {

  // Constants
  private static final String PROVIDES_TYPE_STRING = GraphMetadata.class.getName();
  private static final String PROVIDES_TYPE =
      MetadataIdentificationUtils.create(PROVIDES_TYPE_STRING);

  public static final String getMetadataIdentiferType() {
    return PROVIDES_TYPE;
  }

  public static final String createIdentifier(JavaType javaType, LogicalPath path) {
    return PhysicalTypeIdentifierNamingUtils.createIdentifier(PROVIDES_TYPE_STRING, javaType, path);
  }

  public static final JavaType getJavaType(String metadataIdentificationString) {
    return PhysicalTypeIdentifierNamingUtils.getJavaType(
        PROVIDES_TYPE_STRING, metadataIdentificationString);
  }

  public static final LogicalPath getPath(String metadataIdentificationString) {
    return PhysicalTypeIdentifierNamingUtils.getPath(
        PROVIDES_TYPE_STRING, metadataIdentificationString);
  }

  public static boolean isValid(String metadataIdentificationString) {
    return PhysicalTypeIdentifierNamingUtils.isValid(
        PROVIDES_TYPE_STRING, metadataIdentificationString);
  }

  public GraphMetadata(
      String identifier, JavaType aspectName, PhysicalTypeMetadata governorPhysicalTypeMetadata) {
    super(identifier, aspectName, governorPhysicalTypeMetadata);
    Validate.isTrue(
        isValid(identifier),
        "Metadata identification string '" + identifier + "' does not appear to be a valid");

    // Adding a new sample field definition
    builder.addField(getSampleField());

    // Adding a new sample method definition
    builder.addMethod(getSampleMethod());

    // Create a representation of the desired output ITD
    itdTypeDetails = builder.build();
  }

  /**
   * Create metadata for a field definition.
   *
   * @return a FieldMetadata object
   */
  private FieldMetadata getSampleField() {
    // Note private fields are private to the ITD, not the target type, this
    // is undesirable if a dependent method is pushed in to the target type
    int modifier = 0;

    // Using the FieldMetadataBuilder to create the field definition.
    final FieldMetadataBuilder fieldBuilder =
        new FieldMetadataBuilder(
            getId(), // Metadata
            // ID
            // provided
            // by
            // supertype
            modifier, // Using package protection rather than private
            new ArrayList<AnnotationMetadataBuilder>(), // No annotations
            // for this field
            new JavaSymbolName("sampleField"), // Field name
            JavaType.STRING); // Field type

    return fieldBuilder.build(); // Build and return a FieldMetadata
    // instance
  }

  private MethodMetadata getSampleMethod() {
    // Specify the desired method name
    JavaSymbolName methodName = new JavaSymbolName("sampleMethod");

    // Check if a method with the same signature already exists in the
    // target type
    final MethodMetadata method = methodExists(methodName, new ArrayList<AnnotatedJavaType>());
    if (method != null) {
      // If it already exists, just return the method and omit its
      // generation via the ITD
      return method;
    }

    // Define method annotations (none in this case)
    List<AnnotationMetadataBuilder> annotations = new ArrayList<AnnotationMetadataBuilder>();

    // Define method throws types (none in this case)
    List<JavaType> throwsTypes = new ArrayList<JavaType>();

    // Define method parameter types (none in this case)
    List<AnnotatedJavaType> parameterTypes = new ArrayList<AnnotatedJavaType>();

    // Define method parameter names (none in this case)
    List<JavaSymbolName> parameterNames = new ArrayList<JavaSymbolName>();

    // Create the method body
    InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder();
    bodyBuilder.appendFormalLine("System.out.println(\"Hello World\");");

    // Use the MethodMetadataBuilder for easy creation of MethodMetadata
    MethodMetadataBuilder methodBuilder =
        new MethodMetadataBuilder(
            getId(),
            Modifier.PUBLIC,
            methodName,
            JavaType.VOID_PRIMITIVE,
            parameterTypes,
            parameterNames,
            bodyBuilder);
    methodBuilder.setAnnotations(annotations);
    methodBuilder.setThrowsTypes(throwsTypes);

    return methodBuilder.build(); // Build and return a MethodMetadata
    // instance
  }

  private MethodMetadata methodExists(
      JavaSymbolName methodName, List<AnnotatedJavaType> paramTypes) {
    // We have no access to method parameter information, so we scan by name
    // alone and treat any match as authoritative
    // We do not scan the superclass, as the caller is expected to know
    // we'll only scan the current class
    for (MethodMetadata method : governorTypeDetails.getDeclaredMethods()) {
      if (method.getMethodName().equals(methodName)
          && method.getParameterTypes().equals(paramTypes)) {
        // Found a method of the expected name; we won't check method
        // parameters though
        return method;
      }
    }
    return null;
  }

  // Typically, no changes are required beyond this point

  public String toString() {
    final ToStringBuilder builder = new ToStringBuilder(this);
    builder.append("identifier", getId());
    builder.append("valid", valid);
    builder.append("aspectName", aspectName);
    builder.append("destinationType", destination);
    builder.append("governor", governorPhysicalTypeMetadata.getId());
    builder.append("itdTypeDetails", itdTypeDetails);
    return builder.toString();
  }
}
/**
 * Metadata for {@link RooToString}.
 *
 * @author Ben Alex
 * @since 1.0
 */
public class ToStringMetadata extends AbstractItdTypeDetailsProvidingMetadataItem {

  // Constants
  private static final String PROVIDES_TYPE_STRING = ToStringMetadata.class.getName();
  private static final String PROVIDES_TYPE =
      MetadataIdentificationUtils.create(PROVIDES_TYPE_STRING);

  // Fields
  private final List<MethodMetadata> locatedAccessors;

  // From annotation
  @AutoPopulate private String toStringMethod = "toString";
  @AutoPopulate private String[] excludeFields;

  /**
   * Constructor
   *
   * @param identifier
   * @param aspectName
   * @param governorPhysicalTypeMetadata
   * @param locatedAccessors
   */
  public ToStringMetadata(
      String identifier,
      JavaType aspectName,
      PhysicalTypeMetadata governorPhysicalTypeMetadata,
      List<MethodMetadata> locatedAccessors) {
    super(identifier, aspectName, governorPhysicalTypeMetadata);
    Assert.isTrue(
        isValid(identifier),
        "Metadata identification string '" + identifier + "' does not appear to be a valid");
    Assert.notNull(locatedAccessors, "Public accessors required");

    this.locatedAccessors = locatedAccessors;

    // Process values from the annotation, if present
    AnnotationMetadata annotation = governorTypeDetails.getAnnotation(RooJavaType.ROO_TO_STRING);
    if (annotation != null) {
      AutoPopulationUtils.populate(this, annotation);
    }

    // Generate the toString
    builder.addMethod(getToStringMethod());

    // Create a representation of the desired output ITD
    itdTypeDetails = builder.build();
  }

  /**
   * Obtains the "toString" method for this type, if available.
   *
   * <p>If the user provided a non-default name for "toString", that method will be returned.
   *
   * @return the "toString" method declared on this type or that will be introduced (or null if
   *     undeclared and not introduced)
   */
  public MethodMetadata getToStringMethod() {
    // Compute the relevant toString method name
    JavaSymbolName methodName = new JavaSymbolName("toString");
    if (!this.toStringMethod.equals("")) {
      methodName = new JavaSymbolName(this.toStringMethod);
    }

    // See if the type itself declared the method
    MethodMetadata result =
        MemberFindingUtils.getDeclaredMethod(governorTypeDetails, methodName, null);
    if (result != null) {
      return result;
    }

    // Decide whether we need to produce the toString method
    if (this.toStringMethod.equals("")) {
      return null;
    }

    InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder();
    bodyBuilder.appendFormalLine("StringBuilder sb = new StringBuilder();");

    /** Key: field name, Value: accessor name */
    Map<String, String> map = new LinkedHashMap<String, String>();

    /** Field names */
    List<String> order = new ArrayList<String>();

    Set<String> excludeFieldsSet = new LinkedHashSet<String>();
    if (excludeFields != null && excludeFields.length > 0) {
      Collections.addAll(excludeFieldsSet, excludeFields);
    }

    for (MethodMetadata accessor : locatedAccessors) {
      String accessorName = accessor.getMethodName().getSymbolName();
      String fieldName = BeanInfoUtils.getPropertyNameForJavaBeanMethod(accessor).getSymbolName();
      if (!excludeFieldsSet.contains(StringUtils.uncapitalize(fieldName))
          && !map.containsKey(fieldName)) {
        String accessorText = accessorName + "()";
        if (accessor.getReturnType().isCommonCollectionType()) {
          accessorText = accessorName + "() == null ? \"null\" : " + accessorName + "().size()";
        } else if (accessor.getReturnType().isArray()) {
          accessorText = "java.util.Arrays.toString(" + accessorName + "())";
        } else if (Calendar.class
            .getName()
            .equals(accessor.getReturnType().getFullyQualifiedTypeName())) {
          accessorText = accessorName + "() == null ? \"null\" : " + accessorName + "().getTime()";
        }
        map.put(fieldName, accessorText);
        order.add(fieldName);
      }
    }

    if (!order.isEmpty()) {
      int index = 0;
      int size = map.keySet().size();
      for (String fieldName : order) {
        index++;
        String accessorText = map.get(fieldName);
        StringBuilder string = new StringBuilder();
        string
            .append("sb.append(\"")
            .append(fieldName)
            .append(": \").append(")
            .append(accessorText)
            .append(")");
        if (index < size) {
          string.append(".append(\", \")");
        }
        string.append(";");
        bodyBuilder.appendFormalLine(string.toString());
      }

      bodyBuilder.appendFormalLine("return sb.toString();");

      MethodMetadataBuilder methodBuilder =
          new MethodMetadataBuilder(getId(), Modifier.PUBLIC, methodName, STRING, bodyBuilder);
      result = methodBuilder.build();
    }

    return result;
  }

  public String toString() {
    ToStringCreator tsc = new ToStringCreator(this);
    tsc.append("identifier", getId());
    tsc.append("valid", valid);
    tsc.append("aspectName", aspectName);
    tsc.append("destinationType", destination);
    tsc.append("governor", governorPhysicalTypeMetadata.getId());
    tsc.append("itdTypeDetails", itdTypeDetails);
    return tsc.toString();
  }

  public static String getMetadataIdentiferType() {
    return PROVIDES_TYPE;
  }

  public static String createIdentifier(JavaType javaType, Path path) {
    return PhysicalTypeIdentifierNamingUtils.createIdentifier(PROVIDES_TYPE_STRING, javaType, path);
  }

  public static JavaType getJavaType(String metadataIdentificationString) {
    return PhysicalTypeIdentifierNamingUtils.getJavaType(
        PROVIDES_TYPE_STRING, metadataIdentificationString);
  }

  public static Path getPath(String metadataIdentificationString) {
    return PhysicalTypeIdentifierNamingUtils.getPath(
        PROVIDES_TYPE_STRING, metadataIdentificationString);
  }

  public static boolean isValid(String metadataIdentificationString) {
    return PhysicalTypeIdentifierNamingUtils.isValid(
        PROVIDES_TYPE_STRING, metadataIdentificationString);
  }
}
/**
 * Metadata for {@link RooIntegrationTest}.
 *
 * @author Ben Alex
 * @since 1.0
 */
public class IntegrationTestMetadata extends AbstractItdTypeDetailsProvidingMetadataItem {

  // Constants
  private static final String PROVIDES_TYPE_STRING = IntegrationTestMetadata.class.getName();
  private static final String PROVIDES_TYPE =
      MetadataIdentificationUtils.create(PROVIDES_TYPE_STRING);

  private static final JavaType AFTER_CLASS = new JavaType("org.junit.AfterClass");
  private static final JavaType ASSERT = new JavaType("org.junit.Assert");
  private static final JavaType BEFORE_CLASS = new JavaType("org.junit.BeforeClass");
  private static final JavaType RUN_WITH = new JavaType("org.junit.runner.RunWith");
  private static final JavaType TEST = new JavaType("org.junit.Test");
  private static final JavaType[] SETUP_PARAMETERS = {};
  private static final JavaType[] TEARDOWN_PARAMETERS = {};

  // Fields
  private IntegrationTestAnnotationValues annotationValues;
  private DataOnDemandMetadata dataOnDemandMetadata;
  private boolean isGaeSupported = false;
  private String transactionManager;
  private boolean hasEmbeddedIdentifier;
  private boolean entityHasSuperclass;

  public IntegrationTestMetadata(
      final String identifier,
      final JavaType aspectName,
      final PhysicalTypeMetadata governorPhysicalTypeMetadata,
      final IntegrationTestAnnotationValues annotationValues,
      final DataOnDemandMetadata dataOnDemandMetadata,
      final MethodMetadata identifierAccessorMethod,
      final MethodMetadata versionAccessorMethod,
      final MemberTypeAdditions countMethod,
      final MemberTypeAdditions findMethod,
      final MemberTypeAdditions findAllMethod,
      final MemberTypeAdditions findEntriesMethod,
      final MemberTypeAdditions flushMethod,
      final MemberTypeAdditions mergeMethod,
      final MemberTypeAdditions persistMethod,
      final MemberTypeAdditions removeMethod,
      final String transactionManager,
      final boolean hasEmbeddedIdentifier,
      final boolean entityHasSuperclass,
      final boolean isGaeEnabled) {
    super(identifier, aspectName, governorPhysicalTypeMetadata);
    Assert.isTrue(
        isValid(identifier),
        "Metadata identification string '" + identifier + "' does not appear to be a valid");
    Assert.notNull(annotationValues, "Annotation values required");
    Assert.notNull(dataOnDemandMetadata, "Data on demand metadata required");

    if (!isValid()) {
      return;
    }

    this.annotationValues = annotationValues;
    this.dataOnDemandMetadata = dataOnDemandMetadata;
    this.transactionManager = transactionManager;
    this.hasEmbeddedIdentifier = hasEmbeddedIdentifier;
    this.entityHasSuperclass = entityHasSuperclass;

    addRequiredIntegrationTestClassIntroductions(
        DataOnDemandMetadata.getJavaType(dataOnDemandMetadata.getId()));

    // Add GAE LocalServiceTestHelper instance and @BeforeClass/@AfterClass
    // methods if GAE is enabled
    if (isGaeEnabled) {
      isGaeSupported = true;
      addOptionalIntegrationTestClassIntroductions();
    }

    builder.addMethod(getCountMethodTest(countMethod));
    builder.addMethod(getFindMethodTest(findMethod, identifierAccessorMethod));
    builder.addMethod(getFindAllMethodTest(findAllMethod, countMethod));
    builder.addMethod(getFindEntriesMethodTest(countMethod, findEntriesMethod));
    if (flushMethod != null) {
      builder.addMethod(
          getFlushMethodTest(
              versionAccessorMethod, identifierAccessorMethod, flushMethod, findMethod));
    }
    builder.addMethod(
        getMergeMethodTest(
            mergeMethod, findMethod, flushMethod, versionAccessorMethod, identifierAccessorMethod));
    builder.addMethod(getPersistMethodTest(persistMethod, flushMethod, identifierAccessorMethod));
    builder.addMethod(
        getRemoveMethodTest(removeMethod, findMethod, flushMethod, identifierAccessorMethod));

    itdTypeDetails = builder.build();
  }

  /** Adds the JUnit and Spring type level annotations if needed */
  private void addRequiredIntegrationTestClassIntroductions(final JavaType dodGovernor) {
    // Add an @RunWith(SpringJunit4ClassRunner) annotation to the type, if
    // the user did not define it on the governor directly
    if (MemberFindingUtils.getAnnotationOfType(governorTypeDetails.getAnnotations(), RUN_WITH)
        == null) {
      AnnotationMetadataBuilder runWithBuilder = new AnnotationMetadataBuilder(RUN_WITH);
      runWithBuilder.addClassAttribute(
          "value", "org.springframework.test.context.junit4.SpringJUnit4ClassRunner");
      builder.addAnnotation(runWithBuilder);
    }

    // Add an @ContextConfiguration("classpath:/applicationContext.xml")
    // annotation to the type, if the user did not define it on the governor
    // directly
    if (MemberFindingUtils.getAnnotationOfType(
            governorTypeDetails.getAnnotations(), CONTEXT_CONFIGURATION)
        == null) {
      AnnotationMetadataBuilder contextConfigurationBuilder =
          new AnnotationMetadataBuilder(CONTEXT_CONFIGURATION);
      contextConfigurationBuilder.addStringAttribute(
          "locations", "classpath:/META-INF/spring/applicationContext*.xml");
      builder.addAnnotation(contextConfigurationBuilder);
    }

    // Add an @Transactional, if the user did not define it on the governor
    // directly
    if (annotationValues.isTransactional()
        && MemberFindingUtils.getAnnotationOfType(
                governorTypeDetails.getAnnotations(), TRANSACTIONAL)
            == null) {
      AnnotationMetadataBuilder transactionalBuilder = new AnnotationMetadataBuilder(TRANSACTIONAL);
      if (StringUtils.hasText(transactionManager)
          && !"transactionManager".equals(transactionManager)) {
        transactionalBuilder.addStringAttribute("value", transactionManager);
      }
      builder.addAnnotation(transactionalBuilder);
    }

    // Add the data on demand field if the user did not define it on the
    // governor directly
    FieldMetadata field = governorTypeDetails.getField(new JavaSymbolName("dod"));
    if (field != null) {
      Assert.isTrue(
          field.getFieldType().equals(dodGovernor),
          "Field 'dod' on '"
              + destination.getFullyQualifiedTypeName()
              + "' must be of type '"
              + dodGovernor.getFullyQualifiedTypeName()
              + "'");
      Assert.notNull(
          MemberFindingUtils.getAnnotationOfType(field.getAnnotations(), AUTOWIRED),
          "Field 'dod' on '"
              + destination.getFullyQualifiedTypeName()
              + "' must be annotated with @Autowired");
    } else {
      // Add the field via the ITD
      List<AnnotationMetadataBuilder> annotations = new ArrayList<AnnotationMetadataBuilder>();
      annotations.add(new AnnotationMetadataBuilder(AUTOWIRED));
      FieldMetadataBuilder fieldBuilder =
          new FieldMetadataBuilder(
              getId(), Modifier.PRIVATE, annotations, new JavaSymbolName("dod"), dodGovernor);
      builder.addField(fieldBuilder);
    }

    builder.getImportRegistrationResolver().addImport(ASSERT);
  }

  private void addOptionalIntegrationTestClassIntroductions() {
    // Add the GAE test helper field if the user did not define it on the
    // governor directly
    final JavaType helperType = GAE_LOCAL_SERVICE_TEST_HELPER;
    FieldMetadata helperField = governorTypeDetails.getField(new JavaSymbolName("helper"));
    if (helperField != null) {
      Assert.isTrue(
          helperField
              .getFieldType()
              .getFullyQualifiedTypeName()
              .equals(helperType.getFullyQualifiedTypeName()),
          "Field 'helper' on '"
              + destination.getFullyQualifiedTypeName()
              + "' must be of type '"
              + helperType.getFullyQualifiedTypeName()
              + "'");
    } else {
      // Add the field via the ITD
      String initializer =
          "new LocalServiceTestHelper(new com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig())";
      FieldMetadataBuilder fieldBuilder =
          new FieldMetadataBuilder(
              getId(),
              Modifier.PRIVATE | Modifier.STATIC | Modifier.FINAL,
              new JavaSymbolName("helper"),
              helperType,
              initializer);
      builder.addField(fieldBuilder);
    }

    // Prepare setUp method signature
    JavaSymbolName setUpMethodName = new JavaSymbolName("setUp");
    MethodMetadata setUpMethod = getGovernorMethod(setUpMethodName, SETUP_PARAMETERS);
    if (setUpMethod != null) {
      Assert.notNull(
          MemberFindingUtils.getAnnotationOfType(setUpMethod.getAnnotations(), BEFORE_CLASS),
          "Method 'setUp' on '"
              + destination.getFullyQualifiedTypeName()
              + "' must be annotated with @BeforeClass");
    } else {
      // Add the method via the ITD
      List<AnnotationMetadataBuilder> annotations = new ArrayList<AnnotationMetadataBuilder>();
      annotations.add(new AnnotationMetadataBuilder(BEFORE_CLASS));

      InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder();
      bodyBuilder.appendFormalLine("helper.setUp();");

      MethodMetadataBuilder methodBuilder =
          new MethodMetadataBuilder(
              getId(),
              Modifier.PUBLIC | Modifier.STATIC,
              setUpMethodName,
              JavaType.VOID_PRIMITIVE,
              AnnotatedJavaType.convertFromJavaTypes(SETUP_PARAMETERS),
              new ArrayList<JavaSymbolName>(),
              bodyBuilder);
      methodBuilder.setAnnotations(annotations);
      builder.addMethod(methodBuilder);
    }

    // Prepare tearDown method signature
    JavaSymbolName tearDownMethodName = new JavaSymbolName("tearDown");
    MethodMetadata tearDownMethod = getGovernorMethod(tearDownMethodName, TEARDOWN_PARAMETERS);
    if (tearDownMethod != null) {
      Assert.notNull(
          MemberFindingUtils.getAnnotationOfType(tearDownMethod.getAnnotations(), AFTER_CLASS),
          "Method 'tearDown' on '"
              + destination.getFullyQualifiedTypeName()
              + "' must be annotated with @AfterClass");
    } else {
      // Add the method via the ITD
      List<AnnotationMetadataBuilder> annotations = new ArrayList<AnnotationMetadataBuilder>();
      annotations.add(new AnnotationMetadataBuilder(AFTER_CLASS));

      InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder();
      bodyBuilder.appendFormalLine("helper.tearDown();");

      MethodMetadataBuilder methodBuilder =
          new MethodMetadataBuilder(
              getId(),
              Modifier.PUBLIC | Modifier.STATIC,
              tearDownMethodName,
              JavaType.VOID_PRIMITIVE,
              AnnotatedJavaType.convertFromJavaTypes(TEARDOWN_PARAMETERS),
              new ArrayList<JavaSymbolName>(),
              bodyBuilder);
      methodBuilder.setAnnotations(annotations);
      builder.addMethod(methodBuilder);
    }
  }

  /** @return a test for the count method, if available and requested (may return null) */
  private MethodMetadataBuilder getCountMethodTest(final MemberTypeAdditions countMethod) {
    if (!annotationValues.isCount() || countMethod == null) {
      // User does not want this method
      return null;
    }

    // Prepare method signature
    JavaSymbolName methodName =
        new JavaSymbolName("test" + StringUtils.capitalize(countMethod.getMethodName()));
    if (governorHasMethod(methodName)) {
      return null;
    }

    List<AnnotationMetadataBuilder> annotations = new ArrayList<AnnotationMetadataBuilder>();
    annotations.add(new AnnotationMetadataBuilder(TEST));

    final String entityName = annotationValues.getEntity().getSimpleTypeName();

    InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder();
    bodyBuilder.appendFormalLine(
        "Assert.assertNotNull(\"Data on demand for '"
            + entityName
            + "' failed to initialize correctly\", dod."
            + dataOnDemandMetadata.getRandomPersistentEntityMethod().getMethodName().getSymbolName()
            + "());");
    bodyBuilder.appendFormalLine("long count = " + countMethod.getMethodCall() + ";");
    bodyBuilder.appendFormalLine(
        "Assert.assertTrue(\"Counter for '"
            + entityName
            + "' incorrectly reported there were no entries\", count > 0);");

    countMethod.copyAdditionsTo(builder, governorTypeDetails);

    MethodMetadataBuilder methodBuilder =
        new MethodMetadataBuilder(
            getId(), Modifier.PUBLIC, methodName, JavaType.VOID_PRIMITIVE, bodyBuilder);
    methodBuilder.setAnnotations(annotations);
    return methodBuilder;
  }

  /** @return a test for the find (by ID) method, if available and requested (may return null) */
  private MethodMetadataBuilder getFindMethodTest(
      final MemberTypeAdditions findMethod, final MethodMetadata identifierAccessorMethod) {
    if (!annotationValues.isFind() || findMethod == null || identifierAccessorMethod == null) {
      // User does not want this method
      return null;
    }

    // Prepare method signature
    JavaSymbolName methodName =
        new JavaSymbolName("test" + StringUtils.capitalize(findMethod.getMethodName()));
    if (governorHasMethod(methodName)) {
      return null;
    }

    builder.getImportRegistrationResolver().addImport(identifierAccessorMethod.getReturnType());

    List<AnnotationMetadataBuilder> annotations = new ArrayList<AnnotationMetadataBuilder>();
    annotations.add(new AnnotationMetadataBuilder(TEST));

    final String entityName = annotationValues.getEntity().getSimpleTypeName();

    InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder();
    bodyBuilder.appendFormalLine(
        entityName
            + " obj = dod."
            + dataOnDemandMetadata.getRandomPersistentEntityMethod().getMethodName().getSymbolName()
            + "();");
    bodyBuilder.appendFormalLine(
        "Assert.assertNotNull(\"Data on demand for '"
            + entityName
            + "' failed to initialize correctly\", obj);");
    bodyBuilder.appendFormalLine(
        identifierAccessorMethod.getReturnType().getSimpleTypeName()
            + " id = obj."
            + identifierAccessorMethod.getMethodName().getSymbolName()
            + "();");
    bodyBuilder.appendFormalLine(
        "Assert.assertNotNull(\"Data on demand for '"
            + entityName
            + "' failed to provide an identifier\", id);");
    bodyBuilder.appendFormalLine("obj = " + findMethod.getMethodCall() + ";");
    bodyBuilder.appendFormalLine(
        "Assert.assertNotNull(\"Find method for '"
            + entityName
            + "' illegally returned null for id '\" + id + \"'\", obj);");
    bodyBuilder.appendFormalLine(
        "Assert.assertEquals(\"Find method for '"
            + entityName
            + "' returned the incorrect identifier\", id, obj."
            + identifierAccessorMethod.getMethodName().getSymbolName()
            + "());");

    findMethod.copyAdditionsTo(builder, governorTypeDetails);

    MethodMetadataBuilder methodBuilder =
        new MethodMetadataBuilder(
            getId(), Modifier.PUBLIC, methodName, JavaType.VOID_PRIMITIVE, bodyBuilder);
    methodBuilder.setAnnotations(annotations);
    return methodBuilder;
  }

  /** @return a test for the find all method, if available and requested (may return null) */
  private MethodMetadataBuilder getFindAllMethodTest(
      final MemberTypeAdditions findAllMethod, final MemberTypeAdditions countMethod) {
    if (!annotationValues.isFindAll() || findAllMethod == null || countMethod == null) {
      // User does not want this method, or core dependencies are missing
      return null;
    }

    // Prepare method signature
    JavaSymbolName methodName =
        new JavaSymbolName("test" + StringUtils.capitalize(findAllMethod.getMethodName()));
    if (governorHasMethod(methodName)) {
      return null;
    }

    builder.getImportRegistrationResolver().addImport(LIST);

    List<AnnotationMetadataBuilder> annotations = new ArrayList<AnnotationMetadataBuilder>();
    annotations.add(new AnnotationMetadataBuilder(TEST));

    final String entityName = annotationValues.getEntity().getSimpleTypeName();

    InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder();
    bodyBuilder.appendFormalLine(
        "Assert.assertNotNull(\"Data on demand for '"
            + entityName
            + "' failed to initialize correctly\", dod."
            + dataOnDemandMetadata.getRandomPersistentEntityMethod().getMethodName().getSymbolName()
            + "());");
    bodyBuilder.appendFormalLine("long count = " + countMethod.getMethodCall() + ";");
    bodyBuilder.appendFormalLine(
        "Assert.assertTrue(\"Too expensive to perform a find all test for '"
            + entityName
            + "', as there are \" + count + \" entries; set the findAllMaximum to exceed this value or set findAll=false on the integration test annotation to disable the test\", count < "
            + annotationValues.getFindAllMaximum()
            + ");");
    bodyBuilder.appendFormalLine(
        "List<" + entityName + "> result = " + findAllMethod.getMethodCall() + ";");
    bodyBuilder.appendFormalLine(
        "Assert.assertNotNull(\"Find all method for '"
            + entityName
            + "' illegally returned null\", result);");
    bodyBuilder.appendFormalLine(
        "Assert.assertTrue(\"Find all method for '"
            + entityName
            + "' failed to return any data\", result.size() > 0);");

    findAllMethod.copyAdditionsTo(builder, governorTypeDetails);
    countMethod.copyAdditionsTo(builder, governorTypeDetails);

    MethodMetadataBuilder methodBuilder =
        new MethodMetadataBuilder(
            getId(), Modifier.PUBLIC, methodName, JavaType.VOID_PRIMITIVE, bodyBuilder);
    methodBuilder.setAnnotations(annotations);
    return methodBuilder;
  }

  /** @return a test for the find entries method, if available and requested (may return null) */
  private MethodMetadataBuilder getFindEntriesMethodTest(
      final MemberTypeAdditions countMethod, final MemberTypeAdditions findEntriesMethod) {
    if (!annotationValues.isFindEntries() || countMethod == null || findEntriesMethod == null) {
      // User does not want this method, or core dependencies are missing
      return null;
    }

    // Prepare method signature
    JavaSymbolName methodName =
        new JavaSymbolName("test" + StringUtils.capitalize(findEntriesMethod.getMethodName()));
    if (governorHasMethod(methodName)) {
      return null;
    }

    builder.getImportRegistrationResolver().addImport(LIST);

    List<AnnotationMetadataBuilder> annotations = new ArrayList<AnnotationMetadataBuilder>();
    annotations.add(new AnnotationMetadataBuilder(TEST));

    final String entityName = annotationValues.getEntity().getSimpleTypeName();

    InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder();
    bodyBuilder.appendFormalLine(
        "Assert.assertNotNull(\"Data on demand for '"
            + entityName
            + "' failed to initialize correctly\", dod."
            + dataOnDemandMetadata.getRandomPersistentEntityMethod().getMethodName().getSymbolName()
            + "());");
    bodyBuilder.appendFormalLine("long count = " + countMethod.getMethodCall() + ";");
    bodyBuilder.appendFormalLine("if (count > 20) count = 20;");
    bodyBuilder.appendFormalLine("int firstResult = 0;");
    bodyBuilder.appendFormalLine("int maxResults = (int) count;");
    bodyBuilder.appendFormalLine(
        "List<" + entityName + "> result = " + findEntriesMethod.getMethodCall() + ";");
    bodyBuilder.appendFormalLine(
        "Assert.assertNotNull(\"Find entries method for '"
            + entityName
            + "' illegally returned null\", result);");
    bodyBuilder.appendFormalLine(
        "Assert.assertEquals(\"Find entries method for '"
            + entityName
            + "' returned an incorrect number of entries\", count, result.size());");

    findEntriesMethod.copyAdditionsTo(builder, governorTypeDetails);
    countMethod.copyAdditionsTo(builder, governorTypeDetails);

    MethodMetadataBuilder methodBuilder =
        new MethodMetadataBuilder(
            getId(), Modifier.PUBLIC, methodName, JavaType.VOID_PRIMITIVE, bodyBuilder);
    methodBuilder.setAnnotations(annotations);
    return methodBuilder;
  }

  /** @return a test for the flush method, if available and requested (may return null) */
  private MethodMetadataBuilder getFlushMethodTest(
      final MethodMetadata versionAccessorMethod,
      final MethodMetadata identifierAccessorMethod,
      final MemberTypeAdditions flushMethod,
      final MemberTypeAdditions findMethod) {
    if (!annotationValues.isFlush()
        || versionAccessorMethod == null
        || identifierAccessorMethod == null
        || flushMethod == null
        || findMethod == null) {
      // User does not want this method, or core dependencies are missing
      return null;
    }

    // Prepare method signature
    JavaSymbolName methodName =
        new JavaSymbolName("test" + StringUtils.capitalize(flushMethod.getMethodName()));
    if (governorHasMethod(methodName)) {
      return null;
    }

    JavaType versionType = versionAccessorMethod.getReturnType();
    builder
        .getImportRegistrationResolver()
        .addImports(identifierAccessorMethod.getReturnType(), versionType);

    List<AnnotationMetadataBuilder> annotations = new ArrayList<AnnotationMetadataBuilder>();
    annotations.add(new AnnotationMetadataBuilder(TEST));

    final String entityName = annotationValues.getEntity().getSimpleTypeName();

    InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder();
    bodyBuilder.appendFormalLine(
        entityName
            + " obj = dod."
            + dataOnDemandMetadata.getRandomPersistentEntityMethod().getMethodName().getSymbolName()
            + "();");
    bodyBuilder.appendFormalLine(
        "Assert.assertNotNull(\"Data on demand for '"
            + entityName
            + "' failed to initialize correctly\", obj);");
    bodyBuilder.appendFormalLine(
        identifierAccessorMethod.getReturnType().getSimpleTypeName()
            + " id = obj."
            + identifierAccessorMethod.getMethodName().getSymbolName()
            + "();");
    bodyBuilder.appendFormalLine(
        "Assert.assertNotNull(\"Data on demand for '"
            + entityName
            + "' failed to provide an identifier\", id);");
    bodyBuilder.appendFormalLine("obj = " + findMethod.getMethodCall() + ";");
    bodyBuilder.appendFormalLine(
        "Assert.assertNotNull(\"Find method for '"
            + entityName
            + "' illegally returned null for id '\" + id + \"'\", obj);");
    bodyBuilder.appendFormalLine(
        "boolean modified =  dod."
            + dataOnDemandMetadata.getModifyMethod().getMethodName().getSymbolName()
            + "(obj);");

    bodyBuilder.appendFormalLine(
        versionAccessorMethod.getReturnType().getSimpleTypeName()
            + " currentVersion = obj."
            + versionAccessorMethod.getMethodName().getSymbolName()
            + "();");
    bodyBuilder.appendFormalLine(flushMethod.getMethodCall() + ";");
    if (JdkJavaType.isDateField(versionType)) {
      bodyBuilder.appendFormalLine(
          "Assert.assertTrue(\"Version for '"
              + entityName
              + "' failed to increment on flush directive\", (currentVersion != null && obj."
              + versionAccessorMethod.getMethodName().getSymbolName()
              + "().after(currentVersion)) || !modified);");
    } else {
      bodyBuilder.appendFormalLine(
          "Assert.assertTrue(\"Version for '"
              + entityName
              + "' failed to increment on flush directive\", (currentVersion != null && obj."
              + versionAccessorMethod.getMethodName().getSymbolName()
              + "() > currentVersion) || !modified);");
    }
    flushMethod.copyAdditionsTo(builder, governorTypeDetails);
    findMethod.copyAdditionsTo(builder, governorTypeDetails);

    MethodMetadataBuilder methodBuilder =
        new MethodMetadataBuilder(
            getId(), Modifier.PUBLIC, methodName, JavaType.VOID_PRIMITIVE, bodyBuilder);
    methodBuilder.setAnnotations(annotations);
    return methodBuilder;
  }

  /** @return a test for the merge method, if available and requested (may return null) */
  private MethodMetadataBuilder getMergeMethodTest(
      final MemberTypeAdditions mergeMethod,
      final MemberTypeAdditions findMethod,
      final MemberTypeAdditions flushMethod,
      final MethodMetadata versionAccessorMethod,
      final MethodMetadata identifierAccessorMethod) {
    if (!annotationValues.isMerge()
        || mergeMethod == null
        || versionAccessorMethod == null
        || findMethod == null
        || identifierAccessorMethod == null) {
      // User does not want this method, or core dependencies are missing
      return null;
    }

    // Prepare method signature
    JavaSymbolName methodName =
        new JavaSymbolName("test" + StringUtils.capitalize(mergeMethod.getMethodName()) + "Update");
    if (governorHasMethod(methodName)) {
      return null;
    }

    JavaType versionType = versionAccessorMethod.getReturnType();
    builder
        .getImportRegistrationResolver()
        .addImports(identifierAccessorMethod.getReturnType(), versionType);

    List<AnnotationMetadataBuilder> annotations = new ArrayList<AnnotationMetadataBuilder>();
    annotations.add(new AnnotationMetadataBuilder(TEST));

    String entityName = annotationValues.getEntity().getSimpleTypeName();

    InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder();
    bodyBuilder.appendFormalLine(
        entityName
            + " obj = dod."
            + dataOnDemandMetadata.getRandomPersistentEntityMethod().getMethodName().getSymbolName()
            + "();");
    bodyBuilder.appendFormalLine(
        "Assert.assertNotNull(\"Data on demand for '"
            + entityName
            + "' failed to initialize correctly\", obj);");
    bodyBuilder.appendFormalLine(
        identifierAccessorMethod.getReturnType().getSimpleTypeName()
            + " id = obj."
            + identifierAccessorMethod.getMethodName().getSymbolName()
            + "();");
    bodyBuilder.appendFormalLine(
        "Assert.assertNotNull(\"Data on demand for '"
            + entityName
            + "' failed to provide an identifier\", id);");
    bodyBuilder.appendFormalLine("obj = " + findMethod.getMethodCall() + ";");
    bodyBuilder.appendFormalLine(
        "boolean modified =  dod."
            + dataOnDemandMetadata.getModifyMethod().getMethodName().getSymbolName()
            + "(obj);");

    bodyBuilder.appendFormalLine(
        versionAccessorMethod.getReturnType().getSimpleTypeName()
            + " currentVersion = obj."
            + versionAccessorMethod.getMethodName().getSymbolName()
            + "();");

    String castStr = entityHasSuperclass ? "(" + entityName + ")" : "";
    bodyBuilder.appendFormalLine(
        entityName + " merged = " + castStr + mergeMethod.getMethodCall() + ";");

    if (flushMethod != null) {
      bodyBuilder.appendFormalLine(flushMethod.getMethodCall() + ";");
      flushMethod.copyAdditionsTo(builder, governorTypeDetails);
    }

    bodyBuilder.appendFormalLine(
        "Assert.assertEquals(\"Identifier of merged object not the same as identifier of original object\", merged."
            + identifierAccessorMethod.getMethodName().getSymbolName()
            + "(), id);");
    if (JdkJavaType.isDateField(versionType)) {
      bodyBuilder.appendFormalLine(
          "Assert.assertTrue(\"Version for '"
              + entityName
              + "' failed to increment on merge and flush directive\", (currentVersion != null && obj."
              + versionAccessorMethod.getMethodName().getSymbolName()
              + "().after(currentVersion)) || !modified);");
    } else {
      bodyBuilder.appendFormalLine(
          "Assert.assertTrue(\"Version for '"
              + entityName
              + "' failed to increment on merge and flush directive\", (currentVersion != null && obj."
              + versionAccessorMethod.getMethodName().getSymbolName()
              + "() > currentVersion) || !modified);");
    }
    mergeMethod.copyAdditionsTo(builder, governorTypeDetails);
    findMethod.copyAdditionsTo(builder, governorTypeDetails);

    MethodMetadataBuilder methodBuilder =
        new MethodMetadataBuilder(
            getId(), Modifier.PUBLIC, methodName, JavaType.VOID_PRIMITIVE, bodyBuilder);
    methodBuilder.setAnnotations(annotations);
    return methodBuilder;
  }

  /** @return a test for the persist method, if available and requested (may return null) */
  private MethodMetadataBuilder getPersistMethodTest(
      final MemberTypeAdditions persistMethod,
      final MemberTypeAdditions flushMethod,
      final MethodMetadata identifierAccessorMethod) {
    if (!annotationValues.isPersist()
        || persistMethod == null
        || identifierAccessorMethod == null) {
      // User does not want this method
      return null;
    }

    // Prepare method signature
    JavaSymbolName methodName =
        new JavaSymbolName("test" + StringUtils.capitalize(persistMethod.getMethodName()));
    if (governorHasMethod(methodName)) {
      return null;
    }

    List<AnnotationMetadataBuilder> annotations = new ArrayList<AnnotationMetadataBuilder>();
    annotations.add(new AnnotationMetadataBuilder(TEST));

    final String entityName = annotationValues.getEntity().getSimpleTypeName();

    InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder();
    bodyBuilder.appendFormalLine(
        "Assert.assertNotNull(\"Data on demand for '"
            + entityName
            + "' failed to initialize correctly\", dod."
            + dataOnDemandMetadata.getRandomPersistentEntityMethod().getMethodName().getSymbolName()
            + "());");
    bodyBuilder.appendFormalLine(
        entityName
            + " obj = dod."
            + dataOnDemandMetadata.getNewTransientEntityMethod().getMethodName().getSymbolName()
            + "(Integer.MAX_VALUE);");
    bodyBuilder.appendFormalLine(
        "Assert.assertNotNull(\"Data on demand for '"
            + entityName
            + "' failed to provide a new transient entity\", obj);");

    if (!hasEmbeddedIdentifier) {
      bodyBuilder.appendFormalLine(
          "Assert.assertNull(\"Expected '"
              + entityName
              + "' identifier to be null\", obj."
              + identifierAccessorMethod.getMethodName().getSymbolName()
              + "());");
    }

    bodyBuilder.appendFormalLine(persistMethod.getMethodCall() + ";");
    if (flushMethod != null) {
      bodyBuilder.appendFormalLine(flushMethod.getMethodCall() + ";");
      flushMethod.copyAdditionsTo(builder, governorTypeDetails);
    }

    bodyBuilder.appendFormalLine(
        "Assert.assertNotNull(\"Expected '"
            + entityName
            + "' identifier to no longer be null\", obj."
            + identifierAccessorMethod.getMethodName().getSymbolName()
            + "());");

    persistMethod.copyAdditionsTo(builder, governorTypeDetails);

    MethodMetadataBuilder methodBuilder =
        new MethodMetadataBuilder(
            getId(), Modifier.PUBLIC, methodName, JavaType.VOID_PRIMITIVE, bodyBuilder);
    methodBuilder.setAnnotations(annotations);
    return methodBuilder;
  }

  /** @return a test for the persist method, if available and requested (may return null) */
  private MethodMetadataBuilder getRemoveMethodTest(
      final MemberTypeAdditions removeMethod,
      final MemberTypeAdditions findMethod,
      final MemberTypeAdditions flushMethod,
      final MethodMetadata identifierAccessorMethod) {
    if (!annotationValues.isRemove()
        || removeMethod == null
        || findMethod == null
        || identifierAccessorMethod == null) {
      // User does not want this method or one of its core dependencies
      return null;
    }

    // Prepare method signature
    JavaSymbolName methodName =
        new JavaSymbolName("test" + StringUtils.capitalize(removeMethod.getMethodName()));
    if (governorHasMethod(methodName)) {
      return null;
    }

    builder.getImportRegistrationResolver().addImport(identifierAccessorMethod.getReturnType());

    List<AnnotationMetadataBuilder> annotations = new ArrayList<AnnotationMetadataBuilder>();
    annotations.add(new AnnotationMetadataBuilder(TEST));
    if (isGaeSupported) {
      AnnotationMetadataBuilder transactionalBuilder = new AnnotationMetadataBuilder(TRANSACTIONAL);
      if (StringUtils.hasText(transactionManager)
          && !"transactionManager".equals(transactionManager)) {
        transactionalBuilder.addStringAttribute("value", transactionManager);
      }
      transactionalBuilder.addEnumAttribute(
          "propagation", new EnumDetails(PROPAGATION, new JavaSymbolName("SUPPORTS")));
      annotations.add(transactionalBuilder);
    }

    final String entityName = annotationValues.getEntity().getSimpleTypeName();

    InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder();
    bodyBuilder.appendFormalLine(
        entityName
            + " obj = dod."
            + dataOnDemandMetadata.getRandomPersistentEntityMethod().getMethodName().getSymbolName()
            + "();");
    bodyBuilder.appendFormalLine(
        "Assert.assertNotNull(\"Data on demand for '"
            + entityName
            + "' failed to initialize correctly\", obj);");
    bodyBuilder.appendFormalLine(
        identifierAccessorMethod.getReturnType().getSimpleTypeName()
            + " id = obj."
            + identifierAccessorMethod.getMethodName().getSymbolName()
            + "();");
    bodyBuilder.appendFormalLine(
        "Assert.assertNotNull(\"Data on demand for '"
            + entityName
            + "' failed to provide an identifier\", id);");
    bodyBuilder.appendFormalLine("obj = " + findMethod.getMethodCall() + ";");
    bodyBuilder.appendFormalLine(removeMethod.getMethodCall() + ";");

    if (flushMethod != null) {
      bodyBuilder.appendFormalLine(flushMethod.getMethodCall() + ";");
      flushMethod.copyAdditionsTo(builder, governorTypeDetails);
    }

    bodyBuilder.appendFormalLine(
        "Assert.assertNull(\"Failed to remove '"
            + entityName
            + "' with identifier '\" + id + \"'\", "
            + findMethod.getMethodCall()
            + ");");

    removeMethod.copyAdditionsTo(builder, governorTypeDetails);
    findMethod.copyAdditionsTo(builder, governorTypeDetails);

    MethodMetadataBuilder methodBuilder =
        new MethodMetadataBuilder(
            getId(), Modifier.PUBLIC, methodName, JavaType.VOID_PRIMITIVE, bodyBuilder);
    methodBuilder.setAnnotations(annotations);
    return methodBuilder;
  }

  @Override
  public String toString() {
    ToStringCreator tsc = new ToStringCreator(this);
    tsc.append("identifier", getId());
    tsc.append("valid", valid);
    tsc.append("aspectName", aspectName);
    tsc.append("destinationType", destination);
    tsc.append("governor", governorPhysicalTypeMetadata.getId());
    tsc.append("itdTypeDetails", itdTypeDetails);
    return tsc.toString();
  }

  public static String getMetadataIdentiferType() {
    return PROVIDES_TYPE;
  }

  public static String createIdentifier(final JavaType javaType, final LogicalPath path) {
    return PhysicalTypeIdentifierNamingUtils.createIdentifier(PROVIDES_TYPE_STRING, javaType, path);
  }

  public static JavaType getJavaType(final String metadataIdentificationString) {
    return PhysicalTypeIdentifierNamingUtils.getJavaType(
        PROVIDES_TYPE_STRING, metadataIdentificationString);
  }

  public static LogicalPath getPath(final String metadataIdentificationString) {
    return PhysicalTypeIdentifierNamingUtils.getPath(
        PROVIDES_TYPE_STRING, metadataIdentificationString);
  }

  public static boolean isValid(final String metadataIdentificationString) {
    return PhysicalTypeIdentifierNamingUtils.isValid(
        PROVIDES_TYPE_STRING, metadataIdentificationString);
  }
}
/** This type produces metadata for a new service ITD. */
public class ServiceMetadata extends AbstractItdTypeDetailsProvidingMetadataItem {

  private static final InvocableMemberBodyBuilder BODY = new InvocableMemberBodyBuilder();

  private static final int PUBLIC_ABSTRACT = Modifier.PUBLIC | Modifier.ABSTRACT;

  private static final String PROVIDES_TYPE_STRING = ServiceMetadata.class.getName();
  private static final String PROVIDES_TYPE =
      MetadataIdentificationUtils.create(PROVIDES_TYPE_STRING);

  public static final String getMetadataIdentiferType() {
    return PROVIDES_TYPE;
  }

  public static final String createIdentifier(JavaType javaType, LogicalPath path) {
    return PhysicalTypeIdentifierNamingUtils.createIdentifier(PROVIDES_TYPE_STRING, javaType, path);
  }

  public static final JavaType getJavaType(String metadataIdString) {
    return PhysicalTypeIdentifierNamingUtils.getJavaType(PROVIDES_TYPE_STRING, metadataIdString);
  }

  public static final LogicalPath getPath(String metadataIdString) {
    return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, metadataIdString);
  }

  public static boolean isValid(String metadataIdString) {
    return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, metadataIdString);
  }

  private final ServiceAnnotationValues annotationValues;

  private final JavaType domainType;
  private final String plural;
  private final FieldMetadata identifierField;
  private final FieldMetadata parentProperty;

  public ServiceMetadata(
      String identifier,
      JavaType aspectName,
      PhysicalTypeMetadata governorPhysicalTypeMetadata,
      ServiceAnnotationValues annotationValues,
      JavaType domainType,
      String plural,
      FieldMetadata idField,
      FieldMetadata parentProperty) {
    super(identifier, aspectName, governorPhysicalTypeMetadata);
    Validate.isTrue(
        isValid(identifier),
        "Metadata identification string '" + identifier + "' does not appear to be a valid");

    this.annotationValues = annotationValues;
    this.domainType = domainType;
    this.plural = plural;
    this.identifierField = idField;
    this.parentProperty = parentProperty;

    if (!isValid()) {
      return;
    }

    builder.addMethod(getFindEntriesByParentMethod());
    builder.addMethod(getFindByParentMethod());
    builder.addMethod(getCountByParentMethod());
    builder.addMethod(getFindByStringIdMethod());

    itdTypeDetails = builder.build();
  }

  private MethodMetadata getFindByStringIdMethod() {
    if (!KEY.equals(identifierField.getFieldType())) {
      return null;
    }

    final String idFieldName = identifierField.getFieldName().getSymbolName();
    final JavaSymbolName methodName =
        new JavaSymbolName("find" + destination.getSimpleTypeName() + "ByStringId");
    final JavaType parameterType = STRING;
    final List<JavaSymbolName> parameterNames = Arrays.asList(new JavaSymbolName(idFieldName));
    final JavaType returnType = destination;

    // Check if a method with the same signature already exists in the target type
    final MethodMetadata method = getGovernorMethod(methodName, parameterType);
    if (method != null) {
      // If it already exists, just return the method and omit its generation via the ITD
      return method;
    }

    final MethodMetadataBuilder methodBuilder =
        new MethodMetadataBuilder(
            getId(),
            PUBLIC_ABSTRACT,
            methodName,
            returnType,
            AnnotatedJavaType.convertFromJavaTypes(parameterType),
            parameterNames,
            BODY);

    return methodBuilder.build();
  }

  private MethodMetadata getFindEntriesByParentMethod() {
    if (parentProperty == null) {
      return null;
    }

    JavaSymbolName methodName =
        new JavaSymbolName("find" + domainType.getSimpleTypeName() + "EntriesByParentId");

    final JavaType idType =
        KEY.equals(identifierField.getFieldType()) ? STRING : identifierField.getFieldType();
    final JavaType[] parameterTypes = {idType, INT_PRIMITIVE, INT_PRIMITIVE};

    final MethodMetadata method = getGovernorMethod(methodName, parameterTypes);
    if (method != null) {
      return method;
    }

    final String idParamName =
        StringUtils.uncapitalize(parentProperty.getFieldType().getSimpleTypeName()) + "Id";
    final List<JavaSymbolName> parameterNames =
        Arrays.asList(
            new JavaSymbolName(idParamName),
            new JavaSymbolName("firstResult"),
            new JavaSymbolName("maxResults"));
    final JavaType returnType =
        new JavaType(
            LIST.getFullyQualifiedTypeName(), 0, DataType.TYPE, null, Arrays.asList(domainType));

    final MethodMetadataBuilder methodBuilder =
        new MethodMetadataBuilder(
            getId(),
            PUBLIC_ABSTRACT,
            methodName,
            returnType,
            AnnotatedJavaType.convertFromJavaTypes(parameterTypes),
            parameterNames,
            BODY);

    return methodBuilder.build();
  }

  private MethodMetadata getFindByParentMethod() {
    if (parentProperty == null) {
      return null;
    }

    JavaSymbolName methodName = new JavaSymbolName("find" + plural + "ByParentId");

    final JavaType idType =
        KEY.equals(identifierField.getFieldType()) ? STRING : identifierField.getFieldType();
    final JavaType[] parameterTypes = {idType};

    final MethodMetadata method = getGovernorMethod(methodName, parameterTypes);
    if (method != null) {
      return method;
    }

    final String idParamName =
        StringUtils.uncapitalize(parentProperty.getFieldType().getSimpleTypeName()) + "Id";
    final List<JavaSymbolName> parameterNames = Arrays.asList(new JavaSymbolName(idParamName));
    final JavaType returnType =
        new JavaType(
            LIST.getFullyQualifiedTypeName(), 0, DataType.TYPE, null, Arrays.asList(domainType));

    final MethodMetadataBuilder methodBuilder =
        new MethodMetadataBuilder(
            getId(),
            PUBLIC_ABSTRACT,
            methodName,
            returnType,
            AnnotatedJavaType.convertFromJavaTypes(parameterTypes),
            parameterNames,
            BODY);

    return methodBuilder.build();
  }

  private MethodMetadata getCountByParentMethod() {
    if (parentProperty == null) {
      return null;
    }

    JavaSymbolName methodName = new JavaSymbolName("count" + plural + "ByParentId");

    final JavaType idType =
        KEY.equals(identifierField.getFieldType()) ? STRING : identifierField.getFieldType();
    final JavaType[] parameterTypes = {idType};

    final MethodMetadata method = getGovernorMethod(methodName, parameterTypes);
    if (method != null) {
      return method;
    }

    final String idParamName =
        StringUtils.uncapitalize(parentProperty.getFieldType().getSimpleTypeName()) + "Id";
    final List<JavaSymbolName> parameterNames = Arrays.asList(new JavaSymbolName(idParamName));

    MethodMetadataBuilder methodBuilder =
        new MethodMetadataBuilder(
            getId(),
            PUBLIC_ABSTRACT,
            methodName,
            LONG_PRIMITIVE,
            AnnotatedJavaType.convertFromJavaTypes(parameterTypes),
            parameterNames,
            BODY);

    return methodBuilder.build();
  }

  public ServiceAnnotationValues getServiceAnnotationValues() {
    return annotationValues;
  }

  public String toString() {
    final ToStringBuilder builder = new ToStringBuilder(this);
    builder.append("identifier", getId());
    builder.append("valid", valid);
    builder.append("aspectName", aspectName);
    builder.append("destinationType", destination);
    builder.append("governor", governorPhysicalTypeMetadata.getId());
    builder.append("itdTypeDetails", itdTypeDetails);
    return builder.toString();
  }
}
  public void notify(String upstreamDependency, String downstreamDependency) {
    ProjectMetadata projectMetadata = projectOperations.getProjectMetadata();
    if (projectMetadata == null) {
      return;
    }

    if (MetadataIdentificationUtils.isIdentifyingClass(downstreamDependency)) {
      Assert.isTrue(
          MetadataIdentificationUtils.getMetadataClass(upstreamDependency)
              .equals(
                  MetadataIdentificationUtils.getMetadataClass(
                      PhysicalTypeIdentifier.getMetadataIdentiferType())),
          "Expected class-level notifications only for PhysicalTypeIdentifier (not '"
              + upstreamDependency
              + "')");

      ClassOrInterfaceTypeDetails cid =
          typeLocationService.getTypeForIdentifier(upstreamDependency);
      boolean processed = false;
      if (MemberFindingUtils.getAnnotationOfType(cid.getAnnotations(), RooJavaType.ROO_GWT_REQUEST)
          != null) {
        ClassOrInterfaceTypeDetails proxy = gwtTypeService.lookupProxyFromRequest(cid);
        if (proxy != null) {
          JavaType typeName = PhysicalTypeIdentifier.getJavaType(proxy.getDeclaredByMetadataId());
          Path typePath = PhysicalTypeIdentifier.getPath(proxy.getDeclaredByMetadataId());
          downstreamDependency = GwtLocatorMetadata.createIdentifier(typeName, typePath);
          processed = true;
        }
      }
      if (!processed
          && MemberFindingUtils.getAnnotationOfType(cid.getAnnotations(), RooJavaType.ROO_GWT_PROXY)
              == null) {
        boolean found = false;
        for (ClassOrInterfaceTypeDetails classOrInterfaceTypeDetails :
            typeLocationService.findClassesOrInterfaceDetailsWithAnnotation(
                RooJavaType.ROO_GWT_PROXY)) {
          AnnotationMetadata annotationMetadata =
              GwtUtils.getFirstAnnotation(
                  classOrInterfaceTypeDetails, GwtUtils.ROO_PROXY_REQUEST_ANNOTATIONS);
          if (annotationMetadata != null) {
            AnnotationAttributeValue<?> attributeValue = annotationMetadata.getAttribute("value");
            if (attributeValue != null) {
              String mirrorName = GwtUtils.getStringValue(attributeValue);
              if (mirrorName != null
                  && cid.getName().getFullyQualifiedTypeName().equals(attributeValue.getValue())) {
                found = true;
                JavaType typeName =
                    PhysicalTypeIdentifier.getJavaType(
                        classOrInterfaceTypeDetails.getDeclaredByMetadataId());
                Path typePath =
                    PhysicalTypeIdentifier.getPath(
                        classOrInterfaceTypeDetails.getDeclaredByMetadataId());
                downstreamDependency = GwtLocatorMetadata.createIdentifier(typeName, typePath);
                break;
              }
            }
          }
        }
        if (!found) {
          return;
        }
      } else if (!processed) {
        // A physical Java type has changed, and determine what the corresponding local metadata
        // identification string would have been
        JavaType typeName = PhysicalTypeIdentifier.getJavaType(upstreamDependency);
        Path typePath = PhysicalTypeIdentifier.getPath(upstreamDependency);
        downstreamDependency = GwtLocatorMetadata.createIdentifier(typeName, typePath);
      }

      // We only need to proceed if the downstream dependency relationship is not already registered
      // (if it's already registered, the event will be delivered directly later on)
      if (metadataDependencyRegistry
          .getDownstream(upstreamDependency)
          .contains(downstreamDependency)) {
        return;
      }
    }

    // We should now have an instance-specific "downstream dependency" that can be processed by this
    // class
    Assert.isTrue(
        MetadataIdentificationUtils.getMetadataClass(downstreamDependency)
            .equals(MetadataIdentificationUtils.getMetadataClass(getProvidesType())),
        "Unexpected downstream notification for '"
            + downstreamDependency
            + "' to this provider (which uses '"
            + getProvidesType()
            + "'");

    metadataService.get(downstreamDependency, true);
  }
 private boolean isNotificationForJavaType(final String mid) {
   return MetadataIdentificationUtils.getMetadataClass(mid)
       .equals(
           MetadataIdentificationUtils.getMetadataClass(
               PhysicalTypeIdentifier.getMetadataIdentiferType()));
 }
  public final MetadataItem get(final String metadataIdentificationString) {
    Assert.isTrue(
        MetadataIdentificationUtils.getMetadataClass(metadataIdentificationString)
            .equals(MetadataIdentificationUtils.getMetadataClass(getProvidesType())),
        "Unexpected request for '"
            + metadataIdentificationString
            + "' to this provider (which uses '"
            + getProvidesType()
            + "'");

    // Remove the upstream dependencies for this instance (we'll be recreating them later, if
    // needed)
    metadataDependencyRegistry.deregisterDependencies(metadataIdentificationString);

    // Compute the identifier for the Physical Type Metadata we're correlated with
    String governorPhysicalTypeIdentifier =
        getGovernorPhysicalTypeIdentifier(metadataIdentificationString);

    // Obtain the physical type
    PhysicalTypeMetadata governorPhysicalTypeMetadata =
        (PhysicalTypeMetadata) metadataService.get(governorPhysicalTypeIdentifier);
    if (governorPhysicalTypeMetadata == null || !governorPhysicalTypeMetadata.isValid()) {
      // We can't get even basic information about the physical type, so abort (the ITD will be
      // deleted by ItdFileDeletionService)
      return null;
    }

    // Flag to indicate whether we'll even try to create this metadata
    boolean produceMetadata = false;

    // Determine if we should generate the metadata on the basis of it containing a trigger
    // annotation
    ClassOrInterfaceTypeDetails cid = null;
    if (governorPhysicalTypeMetadata.getMemberHoldingTypeDetails()
        instanceof ClassOrInterfaceTypeDetails) {
      cid =
          (ClassOrInterfaceTypeDetails) governorPhysicalTypeMetadata.getMemberHoldingTypeDetails();
      // Only create metadata if the type is annotated with one of the metadata triggers
      for (JavaType trigger : metadataTriggers) {
        if (cid.getAnnotation(trigger) != null) {
          produceMetadata = true;
          break;
        }
      }
    }

    // Fall back to ignoring trigger annotations
    if (ignoreTriggerAnnotations) {
      produceMetadata = true;
    }

    // Cancel production if the governor type details are required, but aren't available
    if (dependsOnGovernorTypeDetailAvailability && cid == null) {
      produceMetadata = false;
    }

    // Cancel production if the governor is not a class, and the subclass only wants to know about
    // classes
    if (cid != null
        && dependsOnGovernorBeingAClass
        && cid.getPhysicalTypeCategory() != PhysicalTypeCategory.CLASS) {
      produceMetadata = false;
    }

    String itdFilename = governorPhysicalTypeMetadata.getItdCanonicalPath(this);
    if (!produceMetadata && isGovernor(cid) && fileManager.exists(itdFilename)) {
      // We don't seem to want metadata anymore, yet the ITD physically exists, so get rid of it
      // This might be because the trigger annotation has been removed, the governor is missing a
      // class declaration, etc.
      deleteItd(
          metadataIdentificationString,
          itdFilename,
          "not required for governor " + cid.getName(),
          true);
      return null;
    }

    if (produceMetadata) {
      // This type contains an annotation we were configured to detect, or there is an ITD (which
      // may need deletion), so we need to produce the metadata
      JavaType aspectName = governorPhysicalTypeMetadata.getItdJavaType(this);
      ItdTypeDetailsProvidingMetadataItem metadata =
          getMetadata(
              metadataIdentificationString, aspectName, governorPhysicalTypeMetadata, itdFilename);

      // There is no requirement to register a direct connection with the physical type and this
      // metadata because changes will
      // trickle down via the class-level notification registered by convention by
      // AbstractItdMetadataProvider subclasses (BPA 10 Dec 2010)

      if (metadata == null || !metadata.isValid()) {
        // The metadata couldn't be created properly
        deleteItd(metadataIdentificationString, itdFilename, "", false);
        return null;
      }

      // By this point we have a valid MetadataItem, but it might not contain any members for the
      // resulting ITD etc

      // Handle the management of the ITD file
      boolean deleteItdFile = false;
      ItdTypeDetails itdTypeDetails = metadata.getMemberHoldingTypeDetails();

      if (itdTypeDetails == null) {
        // The ITD has no members
        deleteItdFile = true;
      }

      if (!deleteItdFile) {
        // We have some members in the ITD, so decide if we're to write something to disk
        ItdSourceFileComposer itdSourceFileComposer =
            new ItdSourceFileComposer(metadata.getMemberHoldingTypeDetails());

        // Decide whether the get an ITD on-disk based on whether there is physical content to write
        if (itdSourceFileComposer.isContent()) {
          // We have content to write
          itdDiscoveryService.addItdTypeDetails(itdTypeDetails);
          String itd = itdSourceFileComposer.getOutput();
          fileManager.createOrUpdateTextFileIfRequired(itdFilename, itd, false);
        } else {
          // We don't have content to write
          deleteItdFile = true;
        }
      }

      if (deleteItdFile) {
        deleteItd(metadataIdentificationString, itdFilename, null, false);
      }

      // Eagerly notify that the metadata has been updated; this also registers the metadata hash
      // code in the superclass' cache to avoid
      // unnecessary subsequent notifications if it hasn't changed
      notifyIfRequired(metadata);

      return metadata;
    }
    return null;
  }
  public void notifyDownstream(final String upstreamDependency) {
    try {
      metadataLogger.startEvent();

      if (metadataService != null) {
        // First dispatch the fine-grained, instance-specific
        // dependencies.
        Set<String> notifiedDownstreams = new HashSet<String>();
        for (final String downstream : getDownstream(upstreamDependency)) {
          if (metadataLogger.getTraceLevel() > 0) {
            metadataLogger.log(upstreamDependency + " -> " + downstream);
          }
          // No need to ensure upstreamDependency is different from
          // downstream, as that's taken care of in the
          // isValidDependency() method
          try {
            final String responsibleClass =
                MetadataIdentificationUtils.getMetadataClass(downstream);
            metadataLogger.startTimer(responsibleClass);
            metadataService.notify(upstreamDependency, downstream);
          } finally {
            metadataLogger.stopTimer();
          }
          notifiedDownstreams.add(downstream);
        }

        // Next dispatch the coarse-grained, class-specific
        // dependencies.
        // We only do it if the upstream is not class specific, as
        // otherwise we'd have handled class-specific dispatch in
        // previous loop
        if (!MetadataIdentificationUtils.isIdentifyingClass(upstreamDependency)) {
          final String asClass = MetadataIdentificationUtils.getMetadataClassId(upstreamDependency);
          for (final String downstream : getDownstream(asClass)) {
            // We don't notify a downstream if it had a direct
            // instance-specific dependency and was already notified
            // in previous loop
            // We also don't notify if upstream is the same as
            // downstream, as it doesn't make sense to notify
            // yourself of an event
            // (such a condition is only possible if an instance
            // registered to receive class-specific notifications
            // and that instance
            // caused an event to fire)
            if (!notifiedDownstreams.contains(downstream)
                && !upstreamDependency.equals(downstream)) {
              if (metadataLogger.getTraceLevel() > 0) {
                metadataLogger.log(upstreamDependency + " -> " + downstream + " [via class]");
              }
              try {
                final String responsibleClass =
                    MetadataIdentificationUtils.getMetadataClass(downstream);
                metadataLogger.startTimer(responsibleClass);
                metadataService.notify(upstreamDependency, downstream);
              } finally {
                metadataLogger.stopTimer();
              }
            }
          }
        }

        notifiedDownstreams = null;
      }

      // Finally dispatch the general-purpose additional listeners
      for (final MetadataNotificationListener listener : listeners) {
        if (metadataLogger.getTraceLevel() > 1) {
          metadataLogger.log(
              upstreamDependency
                  + " -> "
                  + upstreamDependency
                  + " ["
                  + listener.getClass().getSimpleName()
                  + "]");
        }
        try {
          final String responsibleClass = listener.getClass().getName();
          metadataLogger.startTimer(responsibleClass);
          listener.notify(upstreamDependency, null);
        } finally {
          metadataLogger.stopTimer();
        }
      }
    } finally {
      metadataLogger.stopEvent();
    }
  }
/**
 * Metadata for a type annotated with {@link RooJpaActiveRecord}.
 *
 * @author Ben Alex
 * @author Stefan Schmidt
 * @author Alan Stewart
 * @since 1.0
 */
public class JpaActiveRecordMetadata extends AbstractItdTypeDetailsProvidingMetadataItem {

  // Constants
  private static final String ENTITY_MANAGER_METHOD_NAME = "entityManager";
  private static final String PROVIDES_TYPE_STRING = JpaActiveRecordMetadata.class.getName();
  private static final String PROVIDES_TYPE =
      MetadataIdentificationUtils.create(PROVIDES_TYPE_STRING);
  private static final JavaType COUNT_RETURN_TYPE = JavaType.LONG_PRIMITIVE;

  public static String getMetadataIdentifierType() {
    return PROVIDES_TYPE;
  }

  public static String createIdentifier(final JavaType javaType, final LogicalPath path) {
    return PhysicalTypeIdentifierNamingUtils.createIdentifier(PROVIDES_TYPE_STRING, javaType, path);
  }

  public static JavaType getJavaType(final String metadataIdentificationString) {
    return PhysicalTypeIdentifierNamingUtils.getJavaType(
        PROVIDES_TYPE_STRING, metadataIdentificationString);
  }

  public static LogicalPath getPath(final String metadataIdentificationString) {
    return PhysicalTypeIdentifierNamingUtils.getPath(
        PROVIDES_TYPE_STRING, metadataIdentificationString);
  }

  public static boolean isValid(final String metadataIdentificationString) {
    return PhysicalTypeIdentifierNamingUtils.isValid(
        PROVIDES_TYPE_STRING, metadataIdentificationString);
  }

  // Fields
  private boolean isGaeEnabled;
  private JpaActiveRecordMetadata parent;
  private FieldMetadata identifierField;
  private JpaCrudAnnotationValues crudAnnotationValues;
  private String entityName;
  private String plural;

  /**
   * Constructor
   *
   * @param metadataId (required)
   * @param aspectName (required)
   * @param governorPhysicalTypeMetadata (required)
   * @param parent can be <code>null</code>
   * @param projectMetadata (required)
   * @param crudAnnotationValues the CRUD-related annotation values (required)
   * @param plural the plural form of the entity (required)
   * @param identifierField the entity's identifier field (required)
   * @param entityName the JPA entity name (required)
   */
  public JpaActiveRecordMetadata(
      final String metadataId,
      final JavaType aspectName,
      final PhysicalTypeMetadata governorPhysicalTypeMetadata,
      final JpaActiveRecordMetadata parent,
      final JpaCrudAnnotationValues crudAnnotationValues,
      final String plural,
      final FieldMetadata identifierField,
      final String entityName,
      final boolean isGaeEnabled) {
    super(metadataId, aspectName, governorPhysicalTypeMetadata);
    Assert.isTrue(
        isValid(metadataId),
        "Metadata identification string '" + metadataId + "' does not appear to be a valid");
    Assert.notNull(crudAnnotationValues, "CRUD-related annotation values required");
    Assert.notNull(identifierField, "Identifier required for '" + metadataId + "'");
    Assert.hasText(entityName, "Entity name required for '" + metadataId + "'");
    Assert.hasText(plural, "Plural required for '" + metadataId + "'");

    if (!isValid()) {
      return;
    }

    this.crudAnnotationValues = crudAnnotationValues;
    this.entityName = entityName;
    this.identifierField = identifierField;
    this.isGaeEnabled = isGaeEnabled;
    this.parent = parent;
    this.plural = StringUtils.capitalize(plural);

    // Determine the entity's "entityManager" field, which is guaranteed to be accessible to the
    // ITD.
    builder.addField(getEntityManagerField());

    // Add helper methods
    builder.addMethod(getPersistMethod());
    builder.addMethod(getRemoveMethod());
    builder.addMethod(getFlushMethod());
    builder.addMethod(getClearMethod());
    builder.addMethod(getMergeMethod());

    // Add static methods
    builder.addMethod(getEntityManagerMethod());
    builder.addMethod(getCountMethod());
    builder.addMethod(getFindAllMethod());
    builder.addMethod(getFindMethod());
    builder.addMethod(getFindEntriesMethod());

    builder.putCustomData(CustomDataKeys.DYNAMIC_FINDER_NAMES, getDynamicFinders());

    // Create a representation of the desired output ITD
    itdTypeDetails = builder.build();
  }

  /**
   * Locates the entity manager field that should be used.
   *
   * <p>If a parent is defined, it must provide the field.
   *
   * <p>We generally expect the field to be named "entityManager" and be of type
   * javax.persistence.EntityManager. We also require it to be public or protected, and annotated
   * with @PersistenceContext. If there is an existing field which doesn't meet these latter
   * requirements, we add an underscore prefix to the "entityManager" name and try again, until such
   * time as we come up with a unique name that either meets the requirements or the name is not
   * used and we will create it.
   *
   * @return the entity manager field (never returns null)
   */
  public FieldMetadata getEntityManagerField() {
    if (parent != null) {
      // The parent is required to guarantee this is available
      return parent.getEntityManagerField();
    }

    // Need to locate it ourself
    int index = -1;
    while (true) {
      // Compute the required field name
      index++;
      final JavaSymbolName fieldSymbolName =
          new JavaSymbolName(StringUtils.repeat("_", index) + "entityManager");
      final FieldMetadata candidate = governorTypeDetails.getField(fieldSymbolName);
      if (candidate != null) {
        // Verify if candidate is suitable

        if (!Modifier.isPublic(candidate.getModifier())
            && !Modifier.isProtected(candidate.getModifier())
            && (Modifier.TRANSIENT != candidate.getModifier())) {
          // Candidate is not public and not protected and not simply a transient field (in which
          // case subclasses
          // will see the inherited field), so any subsequent subclasses won't be able to see it.
          // Give up!
          continue;
        }

        if (!candidate.getFieldType().equals(ENTITY_MANAGER)) {
          // Candidate isn't an EntityManager, so give up
          continue;
        }

        if (MemberFindingUtils.getAnnotationOfType(candidate.getAnnotations(), PERSISTENCE_CONTEXT)
            == null) {
          // Candidate doesn't have a PersistenceContext annotation, so give up
          continue;
        }

        // If we got this far, we found a valid candidate
        return candidate;
      }

      // Candidate not found, so let's create one
      final List<AnnotationMetadataBuilder> annotations =
          new ArrayList<AnnotationMetadataBuilder>();
      final AnnotationMetadataBuilder annotationBuilder =
          new AnnotationMetadataBuilder(PERSISTENCE_CONTEXT);
      if (StringUtils.hasText(crudAnnotationValues.getPersistenceUnit())) {
        annotationBuilder.addStringAttribute("unitName", crudAnnotationValues.getPersistenceUnit());
      }
      annotations.add(annotationBuilder);

      final FieldMetadataBuilder fieldBuilder =
          new FieldMetadataBuilder(
              getId(), Modifier.TRANSIENT, annotations, fieldSymbolName, ENTITY_MANAGER);
      return fieldBuilder.build();
    }
  }

  /** @return the persist method (may return null) */
  private MethodMetadata getPersistMethod() {
    if (parent != null) {
      final MethodMetadata found = parent.getPersistMethod();
      if (found != null) {
        return found;
      }
    }
    if ("".equals(crudAnnotationValues.getPersistMethod())) {
      return null;
    }
    return getDelegateMethod(
        new JavaSymbolName(crudAnnotationValues.getPersistMethod()), "persist");
  }

  /** @return the remove method (may return null) */
  private MethodMetadata getRemoveMethod() {
    if (parent != null) {
      final MethodMetadata found = parent.getRemoveMethod();
      if (found != null) {
        return found;
      }
    }
    if ("".equals(crudAnnotationValues.getRemoveMethod())) {
      return null;
    }
    return getDelegateMethod(new JavaSymbolName(crudAnnotationValues.getRemoveMethod()), "remove");
  }

  /** @return the flush method (never returns null) */
  private MethodMetadata getFlushMethod() {
    if (parent != null) {
      final MethodMetadata found = parent.getFlushMethod();
      if (found != null) {
        return found;
      }
    }
    if ("".equals(crudAnnotationValues.getFlushMethod())) {
      return null;
    }
    return getDelegateMethod(new JavaSymbolName(crudAnnotationValues.getFlushMethod()), "flush");
  }

  /** @return the clear method (never returns null) */
  private MethodMetadata getClearMethod() {
    if (parent != null) {
      final MethodMetadata found = parent.getClearMethod();
      if (found != null) {
        return found;
      }
    }
    if ("".equals(crudAnnotationValues.getClearMethod())) {
      return null;
    }
    return getDelegateMethod(new JavaSymbolName(crudAnnotationValues.getClearMethod()), "clear");
  }

  /** @return the merge method (never returns null) */
  private MethodMetadata getMergeMethod() {
    if ("".equals(crudAnnotationValues.getMergeMethod())) {
      return null;
    }
    return getDelegateMethod(new JavaSymbolName(crudAnnotationValues.getMergeMethod()), "merge");
  }

  private MethodMetadata getDelegateMethod(
      final JavaSymbolName methodName, final String methodDelegateName) {
    // Method definition to find or build
    final JavaType[] parameterTypes = {};

    // Locate user-defined method
    final MethodMetadata userMethod = getGovernorMethod(methodName, parameterTypes);
    if (userMethod != null) {
      return userMethod;
    }

    // Create the method
    final List<AnnotationMetadataBuilder> annotations = new ArrayList<AnnotationMetadataBuilder>();

    final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder();

    // Address non-injected entity manager field
    final MethodMetadata entityManagerMethod = getEntityManagerMethod();
    Assert.notNull(entityManagerMethod, "Entity manager method should not have returned null");

    // Use the getEntityManager() method to acquire an entity manager (the method will throw an
    // exception if it cannot be acquired)
    final String entityManagerFieldName = getEntityManagerField().getFieldName().getSymbolName();
    bodyBuilder.appendFormalLine(
        "if (this."
            + entityManagerFieldName
            + " == null) this."
            + entityManagerFieldName
            + " = "
            + entityManagerMethod.getMethodName().getSymbolName()
            + "();");

    JavaType returnType = JavaType.VOID_PRIMITIVE;
    if ("flush".equals(methodDelegateName)) {
      addTransactionalAnnotation(annotations);
      bodyBuilder.appendFormalLine("this." + entityManagerFieldName + ".flush();");
    } else if ("clear".equals(methodDelegateName)) {
      addTransactionalAnnotation(annotations);
      bodyBuilder.appendFormalLine("this." + entityManagerFieldName + ".clear();");
    } else if ("merge".equals(methodDelegateName)) {
      addTransactionalAnnotation(annotations);
      returnType = new JavaType(destination.getSimpleTypeName());
      bodyBuilder.appendFormalLine(
          destination.getSimpleTypeName()
              + " merged = this."
              + entityManagerFieldName
              + ".merge(this);");
      bodyBuilder.appendFormalLine("this." + entityManagerFieldName + ".flush();");
      bodyBuilder.appendFormalLine("return merged;");
    } else if ("remove".equals(methodDelegateName)) {
      addTransactionalAnnotation(annotations);
      bodyBuilder.appendFormalLine("if (this." + entityManagerFieldName + ".contains(this)) {");
      bodyBuilder.indent();
      bodyBuilder.appendFormalLine("this." + entityManagerFieldName + ".remove(this);");
      bodyBuilder.indentRemove();
      bodyBuilder.appendFormalLine("} else {");
      bodyBuilder.indent();
      bodyBuilder.appendFormalLine(
          destination.getSimpleTypeName()
              + " attached = "
              + destination.getSimpleTypeName()
              + "."
              + getFindMethod().getMethodName().getSymbolName()
              + "(this."
              + identifierField.getFieldName().getSymbolName()
              + ");");
      bodyBuilder.appendFormalLine("this." + entityManagerFieldName + ".remove(attached);");
      bodyBuilder.indentRemove();
      bodyBuilder.appendFormalLine("}");
    } else {
      // Persist
      addTransactionalAnnotation(annotations, true);
      bodyBuilder.appendFormalLine(
          "this." + entityManagerFieldName + "." + methodDelegateName + "(this);");
    }

    final MethodMetadataBuilder methodBuilder =
        new MethodMetadataBuilder(
            getId(),
            Modifier.PUBLIC,
            methodName,
            returnType,
            AnnotatedJavaType.convertFromJavaTypes(parameterTypes),
            new ArrayList<JavaSymbolName>(),
            bodyBuilder);
    methodBuilder.setAnnotations(annotations);
    return methodBuilder.build();
  }

  private void addTransactionalAnnotation(
      final List<AnnotationMetadataBuilder> annotations, final boolean isPersistMethod) {
    final AnnotationMetadataBuilder transactionalBuilder =
        new AnnotationMetadataBuilder(TRANSACTIONAL);
    if (StringUtils.hasText(crudAnnotationValues.getTransactionManager())) {
      transactionalBuilder.addStringAttribute(
          "value", crudAnnotationValues.getTransactionManager());
    }
    if (isGaeEnabled && isPersistMethod) {
      transactionalBuilder.addEnumAttribute(
          "propagation", new EnumDetails(PROPAGATION, new JavaSymbolName("REQUIRES_NEW")));
    }
    annotations.add(transactionalBuilder);
  }

  private void addTransactionalAnnotation(final List<AnnotationMetadataBuilder> annotations) {
    addTransactionalAnnotation(annotations, false);
  }

  /**
   * @return the static utility entityManager() method used by other methods to obtain entity
   *     manager and available as a utility for user code (never returns nulls)
   */
  public MethodMetadata getEntityManagerMethod() {
    if (parent != null) {
      // The parent is required to guarantee this is available
      return parent.getEntityManagerMethod();
    }

    // Method definition to find or build
    final JavaSymbolName methodName = new JavaSymbolName(ENTITY_MANAGER_METHOD_NAME);
    final JavaType[] parameterTypes = {};
    final JavaType returnType = ENTITY_MANAGER;

    // Locate user-defined method
    final MethodMetadata userMethod = getGovernorMethod(methodName, parameterTypes);
    if (userMethod != null) {
      Assert.isTrue(
          userMethod.getReturnType().equals(returnType),
          "Method '"
              + methodName
              + "' on '"
              + destination
              + "' must return '"
              + returnType.getNameIncludingTypeParameters()
              + "'");
      return userMethod;
    }

    // Create method
    final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder();

    if (Modifier.isAbstract(governorTypeDetails.getModifier())) {
      // Create an anonymous inner class that extends the abstract class (no-arg constructor is
      // available as this is a JPA entity)
      bodyBuilder.appendFormalLine(
          ENTITY_MANAGER.getNameIncludingTypeParameters(
                  false, builder.getImportRegistrationResolver())
              + " em = new "
              + destination.getSimpleTypeName()
              + "() {");
      // Handle any abstract methods in this class
      bodyBuilder.indent();
      for (final MethodMetadata method : governorTypeDetails.getMethods()) {
        if (Modifier.isAbstract(method.getModifier())) {
          final StringBuilder params = new StringBuilder();
          int i = -1;
          final List<AnnotatedJavaType> types = method.getParameterTypes();
          for (final JavaSymbolName name : method.getParameterNames()) {
            i++;
            if (i > 0) {
              params.append(", ");
            }
            final AnnotatedJavaType type = types.get(i);
            params.append(type.toString()).append(" ").append(name);
          }
          final int newModifier = method.getModifier() - Modifier.ABSTRACT;
          bodyBuilder.appendFormalLine(
              Modifier.toString(newModifier)
                  + " "
                  + method.getReturnType().getNameIncludingTypeParameters()
                  + " "
                  + method.getMethodName().getSymbolName()
                  + "("
                  + params.toString()
                  + ") {");
          bodyBuilder.indent();
          bodyBuilder.appendFormalLine("throw new UnsupportedOperationException();");
          bodyBuilder.indentRemove();
          bodyBuilder.appendFormalLine("}");
        }
      }
      bodyBuilder.indentRemove();
      bodyBuilder.appendFormalLine(
          "}." + getEntityManagerField().getFieldName().getSymbolName() + ";");
    } else {
      // Instantiate using the no-argument constructor (we know this is available as the entity must
      // comply with the JPA no-arg constructor requirement)
      bodyBuilder.appendFormalLine(
          ENTITY_MANAGER.getNameIncludingTypeParameters(
                  false, builder.getImportRegistrationResolver())
              + " em = new "
              + destination.getSimpleTypeName()
              + "()."
              + getEntityManagerField().getFieldName().getSymbolName()
              + ";");
    }

    bodyBuilder.appendFormalLine(
        "if (em == null) throw new IllegalStateException(\"Entity manager has not been injected (is the Spring Aspects JAR configured as an AJC/AJDT aspects library?)\");");
    bodyBuilder.appendFormalLine("return em;");
    final int modifier = Modifier.PUBLIC | Modifier.STATIC | Modifier.FINAL;

    final MethodMetadataBuilder methodBuilder =
        new MethodMetadataBuilder(
            getId(),
            modifier,
            methodName,
            returnType,
            AnnotatedJavaType.convertFromJavaTypes(parameterTypes),
            new ArrayList<JavaSymbolName>(),
            bodyBuilder);
    return methodBuilder.build();
  }

  /**
   * Finds (creating if necessary) the method that counts entities of this type
   *
   * @return the count method (never null)
   */
  private MethodMetadata getCountMethod() {
    // Method definition to find or build
    final JavaSymbolName methodName =
        new JavaSymbolName(crudAnnotationValues.getCountMethod() + plural);
    final JavaType[] parameterTypes = {};
    final List<JavaSymbolName> parameterNames = Collections.<JavaSymbolName>emptyList();

    // Locate user-defined method
    final MethodMetadata userMethod = getGovernorMethod(methodName, parameterTypes);
    if (userMethod != null) {
      Assert.isTrue(
          userMethod.getReturnType().equals(COUNT_RETURN_TYPE),
          "Method '"
              + methodName
              + "' on '"
              + destination
              + "' must return '"
              + COUNT_RETURN_TYPE.getNameIncludingTypeParameters()
              + "'");
      return userMethod;
    }

    // Create method
    final List<AnnotationMetadataBuilder> annotations = new ArrayList<AnnotationMetadataBuilder>();
    if (isGaeEnabled) {
      addTransactionalAnnotation(annotations);
    }

    final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder();
    bodyBuilder.appendFormalLine(
        "return "
            + ENTITY_MANAGER_METHOD_NAME
            + "().createQuery(\"SELECT COUNT(o) FROM "
            + entityName
            + " o\", Long.class).getSingleResult();");

    final MethodMetadataBuilder methodBuilder =
        new MethodMetadataBuilder(
            getId(),
            Modifier.PUBLIC | Modifier.STATIC,
            methodName,
            COUNT_RETURN_TYPE,
            AnnotatedJavaType.convertFromJavaTypes(parameterTypes),
            parameterNames,
            bodyBuilder);
    methodBuilder.setAnnotations(annotations);
    return methodBuilder.build();
  }

  /** @return the find all method (may return null) */
  private MethodMetadata getFindAllMethod() {
    if ("".equals(crudAnnotationValues.getFindAllMethod())) {
      return null;
    }

    // Method definition to find or build
    final JavaSymbolName methodName =
        new JavaSymbolName(crudAnnotationValues.getFindAllMethod() + plural);
    final JavaType[] parameterTypes = {};
    final List<JavaSymbolName> parameterNames = new ArrayList<JavaSymbolName>();
    final JavaType returnType =
        new JavaType(
            LIST.getFullyQualifiedTypeName(), 0, DataType.TYPE, null, Arrays.asList(destination));

    // Locate user-defined method
    final MethodMetadata userMethod = getGovernorMethod(methodName, parameterTypes);
    if (userMethod != null) {
      Assert.isTrue(
          userMethod.getReturnType().equals(returnType),
          "Method '"
              + methodName
              + "' on '"
              + destination
              + "' must return '"
              + returnType.getNameIncludingTypeParameters()
              + "'");
      return userMethod;
    }

    // Create method
    final List<AnnotationMetadataBuilder> annotations = new ArrayList<AnnotationMetadataBuilder>();
    if (isGaeEnabled) {
      addTransactionalAnnotation(annotations);
    }

    final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder();
    bodyBuilder.appendFormalLine(
        "return "
            + ENTITY_MANAGER_METHOD_NAME
            + "().createQuery(\"SELECT o FROM "
            + entityName
            + " o\", "
            + destination.getSimpleTypeName()
            + ".class).getResultList();");

    final MethodMetadataBuilder methodBuilder =
        new MethodMetadataBuilder(
            getId(),
            Modifier.PUBLIC | Modifier.STATIC,
            methodName,
            returnType,
            AnnotatedJavaType.convertFromJavaTypes(parameterTypes),
            parameterNames,
            bodyBuilder);
    methodBuilder.setAnnotations(annotations);
    return methodBuilder.build();
  }

  /** @return the find (by ID) method (may return null) */
  public MethodMetadata getFindMethod() {
    if ("".equals(crudAnnotationValues.getFindMethod())) {
      return null;
    }

    // Method definition to find or build
    final String idFieldName = identifierField.getFieldName().getSymbolName();
    final JavaSymbolName methodName =
        new JavaSymbolName(crudAnnotationValues.getFindMethod() + destination.getSimpleTypeName());
    final JavaType parameterType = identifierField.getFieldType();
    final List<JavaSymbolName> parameterNames = Arrays.asList(new JavaSymbolName(idFieldName));
    final JavaType returnType = destination;

    // Locate user-defined method
    final MethodMetadata userMethod = getGovernorMethod(methodName, parameterType);
    if (userMethod != null) {
      Assert.isTrue(
          userMethod.getReturnType().equals(returnType),
          "Method '"
              + methodName
              + "' on '"
              + returnType
              + "' must return '"
              + returnType.getNameIncludingTypeParameters()
              + "'");
      return userMethod;
    }

    // Create method
    final List<AnnotationMetadataBuilder> annotations = new ArrayList<AnnotationMetadataBuilder>();
    if (isGaeEnabled) {
      addTransactionalAnnotation(annotations);
    }

    final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder();

    if (JavaType.STRING.equals(identifierField.getFieldType())) {
      bodyBuilder.appendFormalLine(
          "if (" + idFieldName + " == null || " + idFieldName + ".length() == 0) return null;");
    } else if (!identifierField.getFieldType().isPrimitive()) {
      bodyBuilder.appendFormalLine("if (" + idFieldName + " == null) return null;");
    }

    bodyBuilder.appendFormalLine(
        "return "
            + ENTITY_MANAGER_METHOD_NAME
            + "().find("
            + returnType.getSimpleTypeName()
            + ".class, "
            + idFieldName
            + ");");

    final MethodMetadataBuilder methodBuilder =
        new MethodMetadataBuilder(
            getId(),
            Modifier.PUBLIC | Modifier.STATIC,
            methodName,
            returnType,
            AnnotatedJavaType.convertFromJavaTypes(parameterType),
            parameterNames,
            bodyBuilder);
    methodBuilder.setAnnotations(annotations);
    return methodBuilder.build();
  }

  /** @return the find entries method (may return null) */
  private MethodMetadata getFindEntriesMethod() {
    if ("".equals(crudAnnotationValues.getFindEntriesMethod())) {
      return null;
    }

    // Method definition to find or build
    final JavaSymbolName methodName =
        new JavaSymbolName(
            crudAnnotationValues.getFindEntriesMethod()
                + destination.getSimpleTypeName()
                + "Entries");
    final JavaType[] parameterTypes = {INT_PRIMITIVE, INT_PRIMITIVE};
    final List<JavaSymbolName> parameterNames =
        Arrays.asList(new JavaSymbolName("firstResult"), new JavaSymbolName("maxResults"));
    final JavaType returnType =
        new JavaType(
            LIST.getFullyQualifiedTypeName(), 0, DataType.TYPE, null, Arrays.asList(destination));

    // Locate user-defined method
    final MethodMetadata userMethod = getGovernorMethod(methodName, parameterTypes);
    if (userMethod != null) {
      Assert.isTrue(
          userMethod.getReturnType().equals(returnType),
          "Method '"
              + methodName
              + "' on '"
              + destination
              + "' must return '"
              + returnType.getNameIncludingTypeParameters()
              + "'");
      return userMethod;
    }

    // Create method
    final List<AnnotationMetadataBuilder> annotations = new ArrayList<AnnotationMetadataBuilder>();
    if (isGaeEnabled) {
      addTransactionalAnnotation(annotations);
    }

    final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder();
    bodyBuilder.appendFormalLine(
        "return "
            + ENTITY_MANAGER_METHOD_NAME
            + "().createQuery(\"SELECT o FROM "
            + entityName
            + " o\", "
            + destination.getSimpleTypeName()
            + ".class).setFirstResult(firstResult).setMaxResults(maxResults).getResultList();");

    final MethodMetadataBuilder methodBuilder =
        new MethodMetadataBuilder(
            getId(),
            Modifier.PUBLIC | Modifier.STATIC,
            methodName,
            returnType,
            AnnotatedJavaType.convertFromJavaTypes(parameterTypes),
            parameterNames,
            bodyBuilder);
    methodBuilder.setAnnotations(annotations);
    return methodBuilder.build();
  }

  /** @return the dynamic, custom finders (never returns null, but may return an empty list) */
  public List<String> getDynamicFinders() {
    if (crudAnnotationValues.getFinders() == null) {
      return Collections.emptyList();
    }
    return Arrays.asList(crudAnnotationValues.getFinders());
  }

  /** @return the pluralised name (never returns null or an empty string) */
  public String getPlural() {
    return plural;
  }

  @Override
  public String toString() {
    final ToStringCreator tsc = new ToStringCreator(this);
    tsc.append("identifier", getId());
    tsc.append("valid", valid);
    tsc.append("aspectName", aspectName);
    tsc.append("destinationType", destination);
    tsc.append("finders", crudAnnotationValues.getFinders());
    tsc.append("governor", governorPhysicalTypeMetadata.getId());
    tsc.append("itdTypeDetails", itdTypeDetails);
    return tsc.toString();
  }

  /**
   * Returns the JPA name of this entity.
   *
   * @return a non-<code>null</code> name (might be empty)
   */
  public String getEntityName() {
    return entityName;
  }
}
Example #18
0
/**
 * Metadata to be triggered by {@link RooJson} annotation
 *
 * @author Stefan Schmidt
 * @since 1.1
 */
public class JsonMetadata extends AbstractItdTypeDetailsProvidingMetadataItem {

  // Constants
  private static final String PROVIDES_TYPE_STRING = JsonMetadata.class.getName();
  private static final String PROVIDES_TYPE =
      MetadataIdentificationUtils.create(PROVIDES_TYPE_STRING);

  // Fields
  private JsonAnnotationValues annotationValues;
  private String typeNamePlural;

  public JsonMetadata(
      String identifier,
      JavaType aspectName,
      PhysicalTypeMetadata governorPhysicalTypeMetadata,
      String typeNamePlural,
      JsonAnnotationValues annotationValues) {
    super(identifier, aspectName, governorPhysicalTypeMetadata);
    Assert.notNull(annotationValues, "Annotation values required");
    Assert.hasText(typeNamePlural, "Plural of the target type required");
    Assert.isTrue(
        isValid(identifier),
        "Metadata identification string '" + identifier + "' does not appear to be a valid");

    if (!isValid()) {
      return;
    }

    this.annotationValues = annotationValues;
    this.typeNamePlural = typeNamePlural;

    builder.addMethod(getToJsonMethod());
    builder.addMethod(getFromJsonMethod());
    builder.addMethod(getToJsonArrayMethod());
    builder.addMethod(getFromJsonArrayMethod());

    // Create a representation of the desired output ITD
    itdTypeDetails = builder.build();
  }

  public JavaSymbolName getToJsonMethodName() {
    String methodLabel = annotationValues.getToJsonMethod();
    if (methodLabel == null || methodLabel.length() == 0) {
      return null;
    }
    return new JavaSymbolName(methodLabel);
  }

  private MethodMetadata getToJsonMethod() {
    // Compute the relevant method name
    JavaSymbolName methodName = getToJsonMethodName();
    if (methodName == null) {
      return null;
    }

    // See if the type itself declared the method
    MethodMetadata result =
        MemberFindingUtils.getDeclaredMethod(governorTypeDetails, methodName, null);
    if (result != null) {
      return result;
    }

    InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder();
    String serializer =
        new JavaType("flexjson.JSONSerializer")
            .getNameIncludingTypeParameters(false, builder.getImportRegistrationResolver());
    String root =
        annotationValues.getRootName() != null && annotationValues.getRootName().length() > 0
            ? ".rootName(\"" + annotationValues.getRootName() + "\")"
            : "";
    bodyBuilder.appendFormalLine(
        "return new "
            + serializer
            + "()"
            + root
            + ".exclude(\"*.class\")"
            + (annotationValues.isDeepSerialize() ? ".deepSerialize(this)" : ".serialize(this)")
            + ";");

    MethodMetadataBuilder methodBuilder =
        new MethodMetadataBuilder(
            getId(), Modifier.PUBLIC, methodName, new JavaType("java.lang.String"), bodyBuilder);
    methodBuilder.putCustomData(CustomDataJsonTags.TO_JSON_METHOD, null);
    return methodBuilder.build();
  }

  public JavaSymbolName getToJsonArrayMethodName() {
    String methodLabel = annotationValues.getToJsonArrayMethod();
    if (methodLabel == null || methodLabel.length() == 0) {
      return null;
    }
    return new JavaSymbolName(methodLabel);
  }

  private MethodMetadata getToJsonArrayMethod() {
    // Compute the relevant method name
    JavaSymbolName methodName = getToJsonArrayMethodName();
    if (methodName == null) {
      return null;
    }

    final JavaType parameterType =
        new JavaType(
            Collection.class.getName(), 0, DataType.TYPE, null, Arrays.asList(destination));

    // See if the type itself declared the method
    MethodMetadata result = getGovernorMethod(methodName, parameterType);
    if (result != null) {
      return result;
    }

    List<JavaSymbolName> parameterNames = Arrays.asList(new JavaSymbolName("collection"));

    InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder();
    String serializer =
        new JavaType("flexjson.JSONSerializer")
            .getNameIncludingTypeParameters(false, builder.getImportRegistrationResolver());
    String root =
        annotationValues.getRootName() != null && annotationValues.getRootName().length() > 0
            ? ".rootName(\"" + annotationValues.getRootName() + "\")"
            : "";
    bodyBuilder.appendFormalLine(
        "return new "
            + serializer
            + "()"
            + root
            + ".exclude(\"*.class\")"
            + (annotationValues.isDeepSerialize()
                ? ".deepSerialize(collection)"
                : ".serialize(collection)")
            + ";");

    MethodMetadataBuilder methodBuilder =
        new MethodMetadataBuilder(
            getId(),
            Modifier.PUBLIC | Modifier.STATIC,
            methodName,
            new JavaType("java.lang.String"),
            AnnotatedJavaType.convertFromJavaTypes(parameterType),
            parameterNames,
            bodyBuilder);
    methodBuilder.putCustomData(CustomDataJsonTags.TO_JSON_ARRAY_METHOD, null);
    return methodBuilder.build();
  }

  public JavaSymbolName getFromJsonArrayMethodName() {
    String methodLabel = annotationValues.getFromJsonArrayMethod();
    if (methodLabel == null || methodLabel.length() == 0) {
      return null;
    }

    return new JavaSymbolName(methodLabel.replace("<TypeNamePlural>", typeNamePlural));
  }

  private MethodMetadata getFromJsonArrayMethod() {
    // Compute the relevant method name
    JavaSymbolName methodName = getFromJsonArrayMethodName();
    if (methodName == null) {
      return null;
    }

    final JavaType parameterType = JavaType.STRING;
    MethodMetadata result = getGovernorMethod(methodName, parameterType);
    if (result != null) {
      return result;
    }

    String list =
        new JavaType("java.util.List")
            .getNameIncludingTypeParameters(false, builder.getImportRegistrationResolver());
    String arrayList =
        new JavaType("java.util.ArrayList")
            .getNameIncludingTypeParameters(false, builder.getImportRegistrationResolver());
    String bean = destination.getSimpleTypeName();

    InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder();
    String deserializer =
        new JavaType("flexjson.JSONDeserializer")
            .getNameIncludingTypeParameters(false, builder.getImportRegistrationResolver());
    bodyBuilder.appendFormalLine(
        "return new "
            + deserializer
            + "<"
            + list
            + "<"
            + bean
            + ">>().use(null, "
            + arrayList
            + ".class).use(\"values\", "
            + bean
            + ".class).deserialize(json);");

    List<JavaSymbolName> parameterNames = Arrays.asList(new JavaSymbolName("json"));

    JavaType collection =
        new JavaType("java.util.Collection", 0, DataType.TYPE, null, Arrays.asList(destination));

    MethodMetadataBuilder methodBuilder =
        new MethodMetadataBuilder(
            getId(),
            Modifier.PUBLIC | Modifier.STATIC,
            methodName,
            collection,
            AnnotatedJavaType.convertFromJavaTypes(parameterType),
            parameterNames,
            bodyBuilder);
    methodBuilder.putCustomData(CustomDataJsonTags.FROM_JSON_ARRAY_METHOD, null);
    return methodBuilder.build();
  }

  public JavaSymbolName getFromJsonMethodName() {
    String methodLabel = annotationValues.getFromJsonMethod();
    if (methodLabel == null || methodLabel.length() == 0) {
      return null;
    }

    // Compute the relevant method name
    return new JavaSymbolName(methodLabel.replace("<TypeName>", destination.getSimpleTypeName()));
  }

  private MethodMetadata getFromJsonMethod() {
    JavaSymbolName methodName = getFromJsonMethodName();
    if (methodName == null) {
      return null;
    }

    final JavaType parameterType = JavaType.STRING;
    MethodMetadata result = getGovernorMethod(methodName, parameterType);
    if (result != null) {
      return result;
    }

    InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder();
    String deserializer =
        new JavaType("flexjson.JSONDeserializer")
            .getNameIncludingTypeParameters(false, builder.getImportRegistrationResolver());
    bodyBuilder.appendFormalLine(
        "return new "
            + deserializer
            + "<"
            + destination.getSimpleTypeName()
            + ">().use(null, "
            + destination.getSimpleTypeName()
            + ".class).deserialize(json);");

    List<JavaSymbolName> parameterNames = Arrays.asList(new JavaSymbolName("json"));

    MethodMetadataBuilder methodBuilder =
        new MethodMetadataBuilder(
            getId(),
            Modifier.PUBLIC | Modifier.STATIC,
            methodName,
            destination,
            AnnotatedJavaType.convertFromJavaTypes(parameterType),
            parameterNames,
            bodyBuilder);
    methodBuilder.putCustomData(CustomDataJsonTags.FROM_JSON_METHOD, null);
    return methodBuilder.build();
  }

  public String toString() {
    ToStringCreator tsc = new ToStringCreator(this);
    tsc.append("identifier", getId());
    tsc.append("valid", valid);
    tsc.append("aspectName", aspectName);
    tsc.append("destinationType", destination);
    tsc.append("governor", governorPhysicalTypeMetadata.getId());
    tsc.append("itdTypeDetails", itdTypeDetails);
    return tsc.toString();
  }

  public static final String getMetadataIdentiferType() {
    return PROVIDES_TYPE;
  }

  public static final String createIdentifier(JavaType javaType, Path path) {
    return PhysicalTypeIdentifierNamingUtils.createIdentifier(PROVIDES_TYPE_STRING, javaType, path);
  }

  public static final JavaType getJavaType(String metadataIdentificationString) {
    return PhysicalTypeIdentifierNamingUtils.getJavaType(
        PROVIDES_TYPE_STRING, metadataIdentificationString);
  }

  public static final Path getPath(String metadataIdentificationString) {
    return PhysicalTypeIdentifierNamingUtils.getPath(
        PROVIDES_TYPE_STRING, metadataIdentificationString);
  }

  public static boolean isValid(String metadataIdentificationString) {
    return PhysicalTypeIdentifierNamingUtils.isValid(
        PROVIDES_TYPE_STRING, metadataIdentificationString);
  }
}
/**
 * Metadata for finder functionality provided via {@link RooWebScaffold}.
 *
 * @author Stefan Schmidt
 * @since 1.1.3
 */
public class WebFinderMetadata extends AbstractItdTypeDetailsProvidingMetadataItem {

  private static final String PROVIDES_TYPE_STRING = WebFinderMetadata.class.getName();
  private static final String PROVIDES_TYPE =
      MetadataIdentificationUtils.create(PROVIDES_TYPE_STRING);

  public static String createIdentifier(final JavaType javaType, final LogicalPath path) {
    return PhysicalTypeIdentifierNamingUtils.createIdentifier(PROVIDES_TYPE_STRING, javaType, path);
  }

  public static JavaType getJavaType(final String metadataIdentificationString) {
    return PhysicalTypeIdentifierNamingUtils.getJavaType(
        PROVIDES_TYPE_STRING, metadataIdentificationString);
  }

  public static String getMetadataIdentiferType() {
    return PROVIDES_TYPE;
  }

  public static LogicalPath getPath(final String metadataIdentificationString) {
    return PhysicalTypeIdentifierNamingUtils.getPath(
        PROVIDES_TYPE_STRING, metadataIdentificationString);
  }

  public static boolean isValid(final String metadataIdentificationString) {
    return PhysicalTypeIdentifierNamingUtils.isValid(
        PROVIDES_TYPE_STRING, metadataIdentificationString);
  }

  private WebScaffoldAnnotationValues annotationValues;
  private String controllerPath;
  private JavaType formBackingType;
  private JavaTypeMetadataDetails javaTypeMetadataHolder;
  private Map<JavaType, JavaTypeMetadataDetails> specialDomainTypes;

  private Map<JavaSymbolName, DateTimeFormatDetails> dateTypes;

  /**
   * Constructor
   *
   * @param identifier
   * @param aspectName
   * @param governorPhysicalTypeMetadata
   * @param annotationValues
   * @param specialDomainTypes
   * @param dynamicFinderMethods
   */
  public WebFinderMetadata(
      final String identifier,
      final JavaType aspectName,
      final PhysicalTypeMetadata governorPhysicalTypeMetadata,
      final WebScaffoldAnnotationValues annotationValues,
      final SortedMap<JavaType, JavaTypeMetadataDetails> specialDomainTypes,
      final Set<FinderMetadataDetails> dynamicFinderMethods,
      final Map<JavaSymbolName, DateTimeFormatDetails> dateTypes) {
    super(identifier, aspectName, governorPhysicalTypeMetadata);
    Validate.isTrue(
        isValid(identifier),
        "Metadata identification string '%s' does not appear to be a valid",
        identifier);
    Validate.notNull(annotationValues, "Annotation values required");
    Validate.notNull(specialDomainTypes, "Special domain type map required");
    Validate.notNull(dynamicFinderMethods, "Dynamoic finder methods required");

    this.dateTypes = dateTypes;

    if (!isValid()) {
      return;
    }

    this.annotationValues = annotationValues;
    controllerPath = annotationValues.getPath();
    formBackingType = annotationValues.getFormBackingObject();
    this.specialDomainTypes = specialDomainTypes;

    if (dynamicFinderMethods.isEmpty()) {
      valid = false;
      return;
    }

    javaTypeMetadataHolder = specialDomainTypes.get(formBackingType);
    Validate.notNull(
        javaTypeMetadataHolder,
        "Metadata holder required for form backing type %s",
        formBackingType);

    for (final FinderMetadataDetails finder : dynamicFinderMethods) {
      builder.addMethod(getFinderFormMethod(finder));
      builder.addMethod(getFinderMethod(finder));
    }

    itdTypeDetails = builder.build();
  }

  public WebScaffoldAnnotationValues getAnnotationValues() {
    return annotationValues;
  }

  private MethodMetadataBuilder getFinderFormMethod(final FinderMetadataDetails finder) {
    Validate.notNull(finder, "Method metadata required for finder");
    final JavaSymbolName finderFormMethodName =
        new JavaSymbolName(
            finder.getFinderMethodMetadata().getMethodName().getSymbolName() + "Form");

    final List<JavaType> methodParameterTypes = new ArrayList<JavaType>();
    final List<JavaSymbolName> methodParameterNames = new ArrayList<JavaSymbolName>();
    final List<JavaType> finderParameterTypes =
        AnnotatedJavaType.convertFromAnnotatedJavaTypes(
            finder.getFinderMethodMetadata().getParameterTypes());

    final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder();

    boolean needModel = false;
    for (final JavaType finderParameterType : finderParameterTypes) {
      JavaTypeMetadataDetails typeMd = specialDomainTypes.get(finderParameterType);
      JavaTypePersistenceMetadataDetails javaTypePersistenceMetadataHolder = null;
      if (finderParameterType.isCommonCollectionType()) {
        typeMd = specialDomainTypes.get(finderParameterType.getParameters().get(0));
        if (typeMd != null && typeMd.isApplicationType()) {
          javaTypePersistenceMetadataHolder = typeMd.getPersistenceDetails();
        }
      } else if (typeMd != null && typeMd.isEnumType()) {
        bodyBuilder.appendFormalLine(
            "uiModel.addAttribute(\""
                + typeMd.getPlural().toLowerCase()
                + "\", java.util.Arrays.asList("
                + getShortName(finderParameterType)
                + ".class.getEnumConstants()));");
      } else if (typeMd != null && typeMd.isApplicationType()) {
        javaTypePersistenceMetadataHolder = typeMd.getPersistenceDetails();
      }
      if (typeMd != null
          && javaTypePersistenceMetadataHolder != null
          && javaTypePersistenceMetadataHolder.getFindAllMethod() != null) {
        bodyBuilder.appendFormalLine(
            "uiModel.addAttribute(\""
                + typeMd.getPlural().toLowerCase()
                + "\", "
                + javaTypePersistenceMetadataHolder.getFindAllMethod().getMethodCall()
                + ");");
      }
      needModel = true;
    }
    if (finderParameterTypes.contains(DATE) || finderParameterTypes.contains(CALENDAR)) {
      bodyBuilder.appendFormalLine("addDateTimeFormatPatterns(uiModel);");
    }
    bodyBuilder.appendFormalLine(
        "return \""
            + controllerPath
            + "/"
            + finder.getFinderMethodMetadata().getMethodName().getSymbolName()
            + "\";");

    if (needModel) {
      methodParameterTypes.add(MODEL);
      methodParameterNames.add(new JavaSymbolName("uiModel"));
    }

    if (governorHasMethod(finderFormMethodName, methodParameterTypes)) {
      return null;
    }

    final List<AnnotationAttributeValue<?>> requestMappingAttributes =
        new ArrayList<AnnotationAttributeValue<?>>();
    final List<StringAttributeValue> arrayValues = new ArrayList<StringAttributeValue>();
    arrayValues.add(
        new StringAttributeValue(
            new JavaSymbolName("value"),
            "find="
                + finder
                    .getFinderMethodMetadata()
                    .getMethodName()
                    .getSymbolName()
                    .replaceFirst("find" + javaTypeMetadataHolder.getPlural(), "")));
    arrayValues.add(new StringAttributeValue(new JavaSymbolName("value"), "form"));
    requestMappingAttributes.add(
        new ArrayAttributeValue<StringAttributeValue>(new JavaSymbolName("params"), arrayValues));
    requestMappingAttributes.add(
        new EnumAttributeValue(
            new JavaSymbolName("method"),
            new EnumDetails(REQUEST_METHOD, new JavaSymbolName("GET"))));
    final AnnotationMetadataBuilder requestMapping =
        new AnnotationMetadataBuilder(REQUEST_MAPPING, requestMappingAttributes);
    final List<AnnotationMetadataBuilder> annotations = new ArrayList<AnnotationMetadataBuilder>();
    annotations.add(requestMapping);

    final MethodMetadataBuilder methodBuilder =
        new MethodMetadataBuilder(
            getId(),
            Modifier.PUBLIC,
            finderFormMethodName,
            JavaType.STRING,
            AnnotatedJavaType.convertFromJavaTypes(methodParameterTypes),
            methodParameterNames,
            bodyBuilder);
    methodBuilder.setAnnotations(annotations);
    return methodBuilder;
  }

  private MethodMetadataBuilder getFinderMethod(final FinderMetadataDetails finderMetadataDetails) {
    Validate.notNull(finderMetadataDetails, "Method metadata required for finder");
    final JavaSymbolName finderMethodName =
        new JavaSymbolName(
            finderMetadataDetails.getFinderMethodMetadata().getMethodName().getSymbolName());

    final List<AnnotatedJavaType> parameterTypes = new ArrayList<AnnotatedJavaType>();
    final List<JavaSymbolName> parameterNames = new ArrayList<JavaSymbolName>();

    final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder();
    final StringBuilder methodParams = new StringBuilder();

    boolean dateFieldPresent = !dateTypes.isEmpty();
    for (final FieldMetadata field : finderMetadataDetails.getFinderMethodParamFields()) {
      final JavaSymbolName fieldName = field.getFieldName();
      final List<AnnotationMetadata> annotations = new ArrayList<AnnotationMetadata>();
      final List<AnnotationAttributeValue<?>> attributes =
          new ArrayList<AnnotationAttributeValue<?>>();
      attributes.add(
          new StringAttributeValue(
              new JavaSymbolName("value"), uncapitalize(fieldName.getSymbolName())));
      if (field.getFieldType().equals(JavaType.BOOLEAN_PRIMITIVE)
          || field.getFieldType().equals(JavaType.BOOLEAN_OBJECT)) {
        attributes.add(new BooleanAttributeValue(new JavaSymbolName("required"), false));
      }
      final AnnotationMetadataBuilder requestParamAnnotation =
          new AnnotationMetadataBuilder(REQUEST_PARAM, attributes);
      annotations.add(requestParamAnnotation.build());
      if (field.getFieldType().equals(DATE) || field.getFieldType().equals(CALENDAR)) {
        dateFieldPresent = true;
        final AnnotationMetadata annotation =
            MemberFindingUtils.getAnnotationOfType(field.getAnnotations(), DATE_TIME_FORMAT);
        if (annotation != null) {
          getShortName(DATE_TIME_FORMAT);
          annotations.add(annotation);
        }
      }
      parameterNames.add(fieldName);
      parameterTypes.add(new AnnotatedJavaType(field.getFieldType(), annotations));

      if (field.getFieldType().equals(JavaType.BOOLEAN_OBJECT)) {
        methodParams.append(fieldName + " == null ? Boolean.FALSE : " + fieldName + ", ");
      } else {
        methodParams.append(fieldName + ", ");
      }
    }

    if (methodParams.length() > 0) {
      methodParams.delete(methodParams.length() - 2, methodParams.length());
    }

    final List<AnnotationAttributeValue<?>> firstResultAttributes =
        new ArrayList<AnnotationAttributeValue<?>>();
    firstResultAttributes.add(new StringAttributeValue(new JavaSymbolName("value"), "page"));
    firstResultAttributes.add(new BooleanAttributeValue(new JavaSymbolName("required"), false));
    final AnnotationMetadataBuilder firstResultAnnotation =
        new AnnotationMetadataBuilder(REQUEST_PARAM, firstResultAttributes);

    final List<AnnotationAttributeValue<?>> maxResultsAttributes =
        new ArrayList<AnnotationAttributeValue<?>>();
    maxResultsAttributes.add(new StringAttributeValue(new JavaSymbolName("value"), "size"));
    maxResultsAttributes.add(new BooleanAttributeValue(new JavaSymbolName("required"), false));
    final AnnotationMetadataBuilder maxResultAnnotation =
        new AnnotationMetadataBuilder(REQUEST_PARAM, maxResultsAttributes);

    final List<AnnotationAttributeValue<?>> sortFieldNameAttributes =
        new ArrayList<AnnotationAttributeValue<?>>();
    sortFieldNameAttributes.add(
        new StringAttributeValue(new JavaSymbolName("value"), "sortFieldName"));
    sortFieldNameAttributes.add(new BooleanAttributeValue(new JavaSymbolName("required"), false));
    final AnnotationMetadataBuilder sortFieldNameAnnotation =
        new AnnotationMetadataBuilder(REQUEST_PARAM, sortFieldNameAttributes);

    final List<AnnotationAttributeValue<?>> sortOrderAttributes =
        new ArrayList<AnnotationAttributeValue<?>>();
    sortOrderAttributes.add(new StringAttributeValue(new JavaSymbolName("value"), "sortOrder"));
    sortOrderAttributes.add(new BooleanAttributeValue(new JavaSymbolName("required"), false));
    final AnnotationMetadataBuilder sortOrderAnnotation =
        new AnnotationMetadataBuilder(REQUEST_PARAM, sortOrderAttributes);

    parameterTypes.add(
        new AnnotatedJavaType(
            new JavaType(Integer.class.getName()), firstResultAnnotation.build()));
    parameterTypes.add(
        new AnnotatedJavaType(new JavaType(Integer.class.getName()), maxResultAnnotation.build()));
    parameterTypes.add(
        new AnnotatedJavaType(
            new JavaType(String.class.getName()), sortFieldNameAnnotation.build()));
    parameterTypes.add(
        new AnnotatedJavaType(new JavaType(String.class.getName()), sortOrderAnnotation.build()));

    parameterTypes.add(new AnnotatedJavaType(MODEL));
    if (getGovernorMethod(
            finderMethodName, AnnotatedJavaType.convertFromAnnotatedJavaTypes(parameterTypes))
        != null) {
      return null;
    }

    final List<JavaSymbolName> newParamNames = new ArrayList<JavaSymbolName>();
    newParamNames.addAll(parameterNames);
    newParamNames.add(new JavaSymbolName("page"));
    newParamNames.add(new JavaSymbolName("size"));
    newParamNames.add(new JavaSymbolName("sortFieldName"));
    newParamNames.add(new JavaSymbolName("sortOrder"));
    newParamNames.add(new JavaSymbolName("uiModel"));

    final List<AnnotationAttributeValue<?>> requestMappingAttributes =
        new ArrayList<AnnotationAttributeValue<?>>();
    requestMappingAttributes.add(
        new StringAttributeValue(
            new JavaSymbolName("params"),
            "find="
                + finderMetadataDetails
                    .getFinderMethodMetadata()
                    .getMethodName()
                    .getSymbolName()
                    .replaceFirst("find" + javaTypeMetadataHolder.getPlural(), "")));
    requestMappingAttributes.add(
        new EnumAttributeValue(
            new JavaSymbolName("method"),
            new EnumDetails(REQUEST_METHOD, new JavaSymbolName("GET"))));
    final AnnotationMetadataBuilder requestMapping =
        new AnnotationMetadataBuilder(REQUEST_MAPPING, requestMappingAttributes);
    final List<AnnotationMetadataBuilder> annotations = new ArrayList<AnnotationMetadataBuilder>();
    annotations.add(requestMapping);

    bodyBuilder.appendFormalLine("if (page != null || size != null) {");
    bodyBuilder.indent();
    bodyBuilder.appendFormalLine("int sizeNo = size == null ? 10 : size.intValue();");
    bodyBuilder.appendFormalLine(
        "final int firstResult = page == null ? 0 : (page.intValue() - 1) * sizeNo;");
    String methodParamsString = methodParams.toString();
    if (StringUtils.isNotBlank(methodParamsString)) {
      methodParamsString.concat(", ");
    }
    bodyBuilder.appendFormalLine(
        "uiModel.addAttribute(\""
            + javaTypeMetadataHolder.getPlural().toLowerCase()
            + "\", "
            + getShortName(formBackingType)
            + "."
            + finderMetadataDetails.getFinderMethodMetadata().getMethodName().getSymbolName()
            + "("
            + methodParamsString
            + "sortFieldName, sortOrder).setFirstResult(firstResult).setMaxResults(sizeNo).getResultList());");

    char[] methodNameArray =
        finderMetadataDetails
            .getFinderMethodMetadata()
            .getMethodName()
            .getSymbolName()
            .toCharArray();
    methodNameArray[0] = Character.toUpperCase(methodNameArray[0]);
    String countMethodName = "count" + new String(methodNameArray);

    bodyBuilder.appendFormalLine(
        "float nrOfPages = (float) "
            + getShortName(formBackingType)
            + "."
            + countMethodName
            + "("
            + methodParamsString
            + ") / sizeNo;");
    bodyBuilder.appendFormalLine(
        "uiModel.addAttribute(\"maxPages\", (int) ((nrOfPages > (int) nrOfPages || nrOfPages == 0.0) ? nrOfPages + 1 : nrOfPages));");
    bodyBuilder.indentRemove();
    bodyBuilder.appendFormalLine("} else {");
    bodyBuilder.indent();
    bodyBuilder.appendFormalLine(
        "uiModel.addAttribute(\""
            + javaTypeMetadataHolder.getPlural().toLowerCase()
            + "\", "
            + getShortName(formBackingType)
            + "."
            + finderMetadataDetails.getFinderMethodMetadata().getMethodName().getSymbolName()
            + "("
            + methodParamsString
            + "sortFieldName, sortOrder).getResultList());");
    bodyBuilder.indentRemove();
    bodyBuilder.appendFormalLine("}");

    if (dateFieldPresent) {
      bodyBuilder.appendFormalLine("addDateTimeFormatPatterns(uiModel);");
    }
    bodyBuilder.appendFormalLine("return \"" + controllerPath + "/list\";");

    final MethodMetadataBuilder methodBuilder =
        new MethodMetadataBuilder(
            getId(),
            Modifier.PUBLIC,
            finderMethodName,
            JavaType.STRING,
            parameterTypes,
            newParamNames,
            bodyBuilder);
    methodBuilder.setAnnotations(annotations);
    return methodBuilder;
  }

  private String getShortName(final JavaType type) {
    return type.getNameIncludingTypeParameters(false, builder.getImportRegistrationResolver());
  }

  @Override
  public String toString() {
    final ToStringBuilder builder = new ToStringBuilder(this);
    builder.append("identifier", getId());
    builder.append("valid", valid);
    builder.append("aspectName", aspectName);
    builder.append("destinationType", destination);
    builder.append("governor", governorPhysicalTypeMetadata.getId());
    builder.append("itdTypeDetails", itdTypeDetails);
    return builder.toString();
  }

  private String uncapitalize(final String term) {
    // [ROO-1790] this is needed to adhere to the JavaBean naming
    // conventions (see JavaBean spec section 8.8)
    return Introspector.decapitalize(StringUtils.capitalize(term));
  }
}