package org.springframework.roo.addon.dbre; import java.io.File; import static org.springframework.roo.model.JdkJavaType.DATE; import static org.springframework.roo.model.JdkJavaType.SET; import static org.springframework.roo.model.JdkJavaType.TIMESTAMP; import static org.springframework.roo.model.JpaJavaType.CASCADE_TYPE; import static org.springframework.roo.model.JpaJavaType.COLUMN; import static org.springframework.roo.model.JpaJavaType.JOIN_COLUMN; import static org.springframework.roo.model.JpaJavaType.JOIN_COLUMNS; import static org.springframework.roo.model.JpaJavaType.JOIN_TABLE; import static org.springframework.roo.model.JpaJavaType.LOB; import static org.springframework.roo.model.JpaJavaType.MANY_TO_MANY; import static org.springframework.roo.model.JpaJavaType.MANY_TO_ONE; import static org.springframework.roo.model.JpaJavaType.ONE_TO_MANY; import static org.springframework.roo.model.JpaJavaType.ONE_TO_ONE; import static org.springframework.roo.model.JpaJavaType.MAPS_ID; import static org.springframework.roo.model.JpaJavaType.TEMPORAL; import static org.springframework.roo.model.JpaJavaType.TEMPORAL_TYPE; import static org.springframework.roo.model.Jsr303JavaType.NOT_NULL; import static org.springframework.roo.model.RooJavaType.ROO_TO_STRING; import static org.springframework.roo.model.SpringJavaType.DATE_TIME_FORMAT; import static org.springframework.roo.model.DomainRefType.DOMAIN_REF_TYPE; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import org.apache.commons.lang3.Validate; import org.jvnet.inflector.Noun; import org.springframework.roo.addon.dbre.model.CascadeAction; import org.springframework.roo.addon.dbre.model.Column; import org.springframework.roo.addon.dbre.model.Database; import org.springframework.roo.addon.dbre.model.ForeignKey; import org.springframework.roo.addon.dbre.model.Reference; import org.springframework.roo.addon.dbre.model.Table; import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; import org.springframework.roo.classpath.PhysicalTypeMetadata; import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; import org.springframework.roo.classpath.details.FieldMetadata; import org.springframework.roo.classpath.details.FieldMetadataBuilder; import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; import org.springframework.roo.classpath.details.annotations.ArrayAttributeValue; import org.springframework.roo.classpath.details.annotations.EnumAttributeValue; import org.springframework.roo.classpath.details.annotations.NestedAnnotationAttributeValue; import org.springframework.roo.classpath.details.annotations.StringAttributeValue; import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem; import org.springframework.roo.metadata.MetadataIdentificationUtils; import org.springframework.roo.model.DataType; import org.springframework.roo.model.DomainRefType; import org.springframework.roo.model.EnumDetails; import org.springframework.roo.model.JavaSymbolName; import org.springframework.roo.model.JavaType; import org.springframework.roo.model.JdkJavaType; import org.springframework.roo.project.LogicalPath; /** * Metadata for {@link RooDbManaged}. * <p> * Creates and manages entity relationships, such as many-valued and * single-valued associations. * <p> * One-to-many and one-to-one associations are created based on the following * laws: * <ul> * <li>Primary Key (PK) - Foreign Key (FK) LAW #1: If the foreign key column is * part of the primary key (or part of an index) then the relationship between * the tables will be one to many (1:M). * <li>Primary Key (PK) - Foreign Key (FK) LAW #2: If the foreign key column * represents the entire primary key (or the entire index) then the relationship * between the tables will be one to one (1:1). * </ul> * <p> * Many-to-many associations are created if a join table is detected. To be * identified as a many-to-many join table, the table must have have exactly two * primary keys and have exactly two foreign-keys pointing to other entity * tables and have no other columns. * * @author Alan Stewart * @since 1.1 */ public class DbreMetadata extends AbstractItdTypeDetailsProvidingMetadataItem { private static final String CREATED = "created"; private static final String MAPPED_BY = "mappedBy"; private static final String NAME = "name"; private static final String PROVIDES_TYPE_STRING = DbreMetadata.class .getName(); private static final String PROVIDES_TYPE = MetadataIdentificationUtils .create(PROVIDES_TYPE_STRING); private static final String VALUE = "value"; public static final String DOMAIN_TYPE_TERMINATOR = "Type"; private DbManagedAnnotationValues annotationValues; private Database database; private IdentifierHolder identifierHolder; private Iterable<ClassOrInterfaceTypeDetails> managedEntities; private ClassOrInterfaceTypeDetailsBuilder updatedGovernorBuilder; private FieldMetadata versionField; public DbreMetadata(final String identifier, final JavaType aspectName, final PhysicalTypeMetadata governorPhysicalTypeMetadata, final DbManagedAnnotationValues annotationValues, final IdentifierHolder identifierHolder, final FieldMetadata versionField, final Iterable<ClassOrInterfaceTypeDetails> managedEntities, final Database database) { 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(identifierHolder, "Identifier holder required"); Validate.notNull(managedEntities, "Managed entities required"); Validate.notNull(database, "Database required"); String aliasPropertyFilename = database.getAliasPropertiesFilename(); DbreTypeUtils.initAliasMappings(aliasPropertyFilename, database); this.annotationValues = annotationValues; this.identifierHolder = identifierHolder; this.versionField = versionField; this.managedEntities = managedEntities; this.database = database; final Table table = this.database.getTable( DbreTypeUtils.getTableName(governorTypeDetails), DbreTypeUtils.getSchemaName(governorTypeDetails)); if (table == null) { valid = false; return; } // Add fields for many-valued associations with many-to-many // multiplicity addManyToManyFields(table); // Add fields for single-valued associations to other entities that have // one-to-one multiplicity addOneToOneFields(table); // Add fields for many-valued associations with one-to-many multiplicity // addOneToManyFields(table); // Add fields for single-valued associations to other entities that have // many-to-one multiplicity // mcm - ignore for now // addManyToOneFields(table); // Add remaining fields from columns addOtherFields(table); // Create a representation of the desired output ITD itdTypeDetails = builder.build(); } 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); } public ClassOrInterfaceTypeDetails getUpdatedGovernor() { return updatedGovernorBuilder == null ? null : updatedGovernorBuilder .build(); } public boolean isAutomaticallyDelete() { return annotationValues.isAutomaticallyDelete(); } private void addCascadeType( final AnnotationMetadataBuilder annotationBuilder, final CascadeAction onUpdate, final CascadeAction onDelete) { final String attributeName = "cascade"; boolean hasCascadeType = true; if (onUpdate == CascadeAction.CASCADE && onDelete == CascadeAction.CASCADE) { annotationBuilder.addEnumAttribute(attributeName, CASCADE_TYPE, "ALL"); } else if (onUpdate == CascadeAction.CASCADE && onDelete != CascadeAction.CASCADE) { final List<EnumAttributeValue> arrayValues = new ArrayList<EnumAttributeValue>(); arrayValues.add(new EnumAttributeValue(new JavaSymbolName( attributeName), new EnumDetails(CASCADE_TYPE, new JavaSymbolName("PERSIST")))); arrayValues.add(new EnumAttributeValue(new JavaSymbolName( attributeName), new EnumDetails(CASCADE_TYPE, new JavaSymbolName("MERGE")))); annotationBuilder .addAttribute(new ArrayAttributeValue<EnumAttributeValue>( new JavaSymbolName(attributeName), arrayValues)); } else if (onUpdate != CascadeAction.CASCADE && onDelete == CascadeAction.CASCADE) { annotationBuilder.addEnumAttribute(attributeName, CASCADE_TYPE.getSimpleTypeName(), "REMOVE"); } else { hasCascadeType = false; } if (hasCascadeType) { builder.getImportRegistrationResolver().addImport(CASCADE_TYPE); } } private void addManyToManyFields(final Table table) { final Map<Table, Integer> owningSideTables = new LinkedHashMap<Table, Integer>(); final Map<JavaSymbolName, FieldMetadataBuilder> uniqueOwningSideFields = new LinkedHashMap<JavaSymbolName, FieldMetadataBuilder>(); final Map<JavaSymbolName, FieldMetadataBuilder> uniqueInverseSideFields = new LinkedHashMap<JavaSymbolName, FieldMetadataBuilder>(); for (final Table joinTable : database.getTables()) { if (!joinTable.isJoinTable()) { continue; } final String errMsg = "table in join table '" + joinTable.getName() + "' for many-to-many relationship could not be found. Note that table names are case sensitive in some databases such as MySQL."; final Iterator<ForeignKey> iter = joinTable.getImportedKeys() .iterator(); // First foreign key in set final ForeignKey foreignKey1 = iter.next(); // Second and last foreign key in set final ForeignKey foreignKey2 = iter.next(); final Table owningSideTable = foreignKey1.getForeignTable(); Validate.notNull(owningSideTable, "Owning-side " + errMsg); final Table inverseSideTable = foreignKey2.getForeignTable(); Validate.notNull(inverseSideTable, "Inverse-side " + errMsg); final Integer tableCount = owningSideTables .containsKey(owningSideTable) ? owningSideTables .get(owningSideTable) + 1 : 0; owningSideTables.put(owningSideTable, tableCount); final String fieldSuffix = owningSideTables.get(owningSideTable) > 0 ? String .valueOf(owningSideTables.get(owningSideTable)) : ""; final boolean sameTable = owningSideTable.equals(inverseSideTable); if (owningSideTable.equals(table)) { final JavaSymbolName fieldName = new JavaSymbolName( getInflectorPlural(DbreTypeUtils .suggestFieldName(inverseSideTable)) + (sameTable ? "1" : fieldSuffix)); final FieldMetadataBuilder fieldBuilder = getManyToManyOwningSideField( fieldName, joinTable, inverseSideTable, foreignKey1.getOnUpdate(), foreignKey1.getOnDelete()); uniqueOwningSideFields.put(fieldName, fieldBuilder); } if (inverseSideTable.equals(table)) { final JavaSymbolName fieldName = new JavaSymbolName( getInflectorPlural(DbreTypeUtils .suggestFieldName(owningSideTable)) + (sameTable ? "2" : fieldSuffix)); final JavaSymbolName mappedByFieldName = new JavaSymbolName( getInflectorPlural(DbreTypeUtils .suggestFieldName(inverseSideTable)) + (sameTable ? "1" : fieldSuffix)); final FieldMetadataBuilder fieldBuilder = getManyToManyInverseSideField( fieldName, mappedByFieldName, owningSideTable, foreignKey2.getOnUpdate(), foreignKey2.getOnDelete()); uniqueInverseSideFields.put(fieldName, fieldBuilder); } } // Add unique owning-side many-to-one fields for (final FieldMetadataBuilder fieldBuilder : uniqueOwningSideFields .values()) { addToBuilder(fieldBuilder); } // Add unique inverse-side many-to-one fields for (final FieldMetadataBuilder fieldBuilder : uniqueInverseSideFields .values()) { addToBuilder(fieldBuilder); } } private void addManyToOneFields(final Table table) { // Add unique many-to-one fields final Map<JavaSymbolName, FieldMetadataBuilder> uniqueFields = new LinkedHashMap<JavaSymbolName, FieldMetadataBuilder>(); for (final ForeignKey foreignKey : table.getImportedKeys()) { final Table foreignTable = foreignKey.getForeignTable(); if (foreignTable == null || isOneToOne(table, foreignKey)) { continue; } // Assume many-to-one multiplicity JavaSymbolName fieldName = null; final String foreignTableName = foreignTable.getName(); final String foreignSchemaName = foreignTable.getSchema().getName(); if (foreignKey.getReferenceCount() == 1) { final Reference reference = foreignKey.getReferences() .iterator().next(); fieldName = new JavaSymbolName(DbreTypeUtils.suggestFieldName( reference.getLocalColumnName(), table.getName(), true)); } else { final Short keySequence = foreignKey.getKeySequence(); final String fieldSuffix = keySequence != null && keySequence > 0 ? String.valueOf(keySequence) : ""; fieldName = new JavaSymbolName(DbreTypeUtils.suggestFieldName( foreignTableName, "") + fieldSuffix); } final JavaType fieldType = DbreTypeUtils.findTypeForTableName( managedEntities, foreignTableName, foreignSchemaName); Validate.notNull( fieldType, "Attempted to create many-to-one field '" + fieldName + "' in '" + destination.getFullyQualifiedTypeName() + "'" + getErrorMsg( foreignTable.getFullyQualifiedTableName(), table.getFullyQualifiedTableName())); FieldMetadataBuilder fieldBuilder = null; // mcm - needs to be externalized. boolean renderRelationshipAsEmbeddedType = false; if (renderRelationshipAsEmbeddedType) { // Fields are stored in a field-keyed map first before adding them // to the builder. // This ensures the fields from foreign keys with multiple columns // will only get created once. fieldBuilder = getOneToOneOrManyToOneField( fieldName, fieldType, foreignKey, MANY_TO_ONE, true); } else { fieldBuilder = getField(fieldName, foreignKey.getReferences().iterator().next().getLocalColumn(), table.getName(), table.isIncludeNonPortableAttributes()); if (fieldBuilder.getFieldType().equals(DATE) && fieldName.getSymbolName().equals(CREATED)) { fieldBuilder.setFieldInitializer("new Date()"); } } if (null != fieldBuilder) { uniqueFields.put(fieldName, fieldBuilder); } } for (final FieldMetadataBuilder fieldBuilder : uniqueFields.values()) { addToBuilder(fieldBuilder); } } private void addOneToManyFields(final Table table) { Validate.notNull(table, "Table required"); if (table.isJoinTable()) { return; } /** mcm Assume domain table and don't map any reverse relationships **/ if (table.getName().endsWith("_TYPE")) { return; } for (final ForeignKey exportedKey : table.getExportedKeys()) { final Table exportedKeyForeignTable = exportedKey.getForeignTable(); if (exportedKeyForeignTable == null) { System.out.println("Foreign key table for foreign key " + exportedKey.getName() + "' in table '" + table.getFullyQualifiedTableName() + "' does not exist. One-to-many relationship not created"); continue; } // Validate.notNull( // exportedKeyForeignTable, // "Foreign key table for foreign key '" // + exportedKey.getName() // + "' in table '" // + table.getFullyQualifiedTableName() // + "' does not exist. One-to-many relationship not created"); if (exportedKeyForeignTable.isJoinTable()) { continue; } final String foreignTableName = exportedKeyForeignTable.getName(); final String foreignSchemaName = exportedKeyForeignTable .getSchema().getName(); final Table foreignTable = database.getTable(foreignTableName, foreignSchemaName); Validate.notNull( foreignTable, "Related table '%s' could not be found but was referenced by table '%s'", exportedKeyForeignTable.getFullyQualifiedTableName(), table.getFullyQualifiedTableName() + "'"); if (isOneToOne(foreignTable, foreignTable.getImportedKey(exportedKey.getName()))) { continue; } final Short keySequence = exportedKey.getKeySequence(); final String fieldSuffix = keySequence != null && keySequence > 0 ? String .valueOf(keySequence) : ""; JavaSymbolName fieldName = new JavaSymbolName( getInflectorPlural(DbreTypeUtils.suggestFieldName( foreignTableName, "")) + fieldSuffix); JavaSymbolName mappedByFieldName = null; if (exportedKey.getReferenceCount() == 1) { final Reference reference = exportedKey.getReferences() .iterator().next(); mappedByFieldName = new JavaSymbolName( DbreTypeUtils.suggestFieldName( reference.getForeignColumnName(), "")); } else { mappedByFieldName = new JavaSymbolName( DbreTypeUtils.suggestFieldName(table) + fieldSuffix); } // Check for existence of same field - ROO-1691 while (true) { if (!hasFieldInItd(fieldName)) { break; } fieldName = new JavaSymbolName(fieldName.getSymbolName() + "_"); } final FieldMetadataBuilder fieldBuilder = getOneToManyMappedByField( fieldName, mappedByFieldName, foreignTableName, foreignSchemaName, exportedKey.getOnUpdate(), exportedKey.getOnDelete()); addToBuilder(fieldBuilder); } } private void addOneToOneFields(final Table table) { // Add unique one-to-one fields final Map<JavaSymbolName, FieldMetadataBuilder> uniqueFields = new LinkedHashMap<JavaSymbolName, FieldMetadataBuilder>(); for (final ForeignKey foreignKey : table.getImportedKeys()) { if (!isOneToOne(table, foreignKey)) { continue; } final Table importedKeyForeignTable = foreignKey.getForeignTable(); final String foreignTableName = importedKeyForeignTable.getName(); final String foreignSchemaName = importedKeyForeignTable .getSchema().getName(); final Short keySequence = foreignKey.getKeySequence(); final String fieldSuffix = keySequence != null && keySequence > 0 ? String .valueOf(keySequence) : ""; final JavaSymbolName fieldName = new JavaSymbolName( DbreTypeUtils.suggestFieldName(foreignTableName, "") + fieldSuffix); final JavaType fieldType = DbreTypeUtils.findTypeForTableName( managedEntities, foreignTableName, foreignSchemaName); Validate.notNull( fieldType, "Attempted to create one-to-one field '" + fieldName + "' in '" + destination.getFullyQualifiedTypeName() + "'" + getErrorMsg(importedKeyForeignTable .getFullyQualifiedTableName(), table .getFullyQualifiedTableName())); // Fields are stored in a field-keyed map first before adding them // to the builder. // This ensures the fields from foreign keys with multiple columns // will only get created once. final FieldMetadataBuilder fieldBuilder = getOneToOneOrManyToOneField( fieldName, fieldType, foreignKey, ONE_TO_ONE, false); if (null != fieldBuilder) { uniqueFields.put(fieldName, fieldBuilder); } } for (final FieldMetadataBuilder fieldBuilder : uniqueFields.values()) { addToBuilder(fieldBuilder); // Exclude these fields in @RooToString to avoid circular references // - ROO-1399 excludeFieldsInToStringAnnotation(fieldBuilder.getFieldName() .getSymbolName()); } // Add one-to-one mapped-by fields if (table.isJoinTable()) { return; } /** * mcm - temporary hack to make one-to-one relationships unidirectional * Need to make this configurable from the ROO command line. **/ boolean allowBiDirectionalOneToOne = false; if (allowBiDirectionalOneToOne) { for (final ForeignKey exportedKey : table.getExportedKeys()) { final Table exportedKeyForeignTable = exportedKey .getForeignTable(); Validate.notNull( exportedKeyForeignTable, "Foreign key table for foreign key '" + exportedKey.getName() + "' in table '" + table.getFullyQualifiedTableName() + "' does not exist. One-to-one relationship not created"); if (exportedKeyForeignTable.isJoinTable()) { continue; } final String foreignTableName = exportedKeyForeignTable .getName(); final String foreignSchemaName = exportedKeyForeignTable .getSchema().getName(); final Table foreignTable = database.getTable(foreignTableName, foreignSchemaName); Validate.notNull( foreignTable, "Related table '" + exportedKeyForeignTable .getFullyQualifiedTableName() + "' could not be found but has a foreign-key reference to table '" + table.getFullyQualifiedTableName() + "'"); if (!isOneToOne(foreignTable, foreignTable.getImportedKey(exportedKey.getName()))) { continue; } final Short keySequence = exportedKey.getKeySequence(); final String fieldSuffix = keySequence != null && keySequence > 0 ? String.valueOf(keySequence) : ""; JavaSymbolName fieldName = new JavaSymbolName( DbreTypeUtils.suggestFieldName(foreignTableName, "") + fieldSuffix); final JavaType fieldType = DbreTypeUtils.findTypeForTableName( managedEntities, foreignTableName, foreignSchemaName); Validate.notNull( fieldType, "Attempted to create one-to-one mapped-by field '" + fieldName + "' in '" + destination.getFullyQualifiedTypeName() + "'" + getErrorMsg(foreignTable .getFullyQualifiedTableName())); // Check for existence of same field - ROO-1691 while (true) { if (!hasFieldInItd(fieldName)) { break; } fieldName = new JavaSymbolName(fieldName.getSymbolName() + "_"); } final JavaSymbolName mappedByFieldName = new JavaSymbolName( DbreTypeUtils.suggestFieldName(table.getName(), table.getName()) + fieldSuffix); final FieldMetadataBuilder fieldBuilder = getOneToOneMappedByField( fieldName, fieldType, mappedByFieldName, exportedKey.getOnUpdate(), exportedKey.getOnDelete()); addToBuilder(fieldBuilder); } } } private void addOtherFields(final Table table) { final Map<JavaSymbolName, FieldMetadataBuilder> uniqueFields = new LinkedHashMap<JavaSymbolName, FieldMetadataBuilder>(); for (final Column column : table.getColumns()) { final String columnName = column.getName(); final ForeignKey foreignKey = table.findImportedKeyByLocalColumnName(columnName); boolean isForeignKey = foreignKey != null; // -- mcm - don't deal with ManyToOne for now -- isForeignKey = false; JavaSymbolName fieldName = new JavaSymbolName( DbreTypeUtils.suggestFieldName(columnName, table.getName(), isForeignKey || column.isPrimaryKey())); final boolean isIdField = isIdField(fieldName) || column.isPrimaryKey(); final boolean isVersionField = isVersionField(fieldName) || columnName.equals("version"); final boolean isCompositeKeyField = isCompositeKeyField(fieldName); final boolean isDiscriminatorField = columnName.equalsIgnoreCase("dtype"); if (isIdField || isVersionField || isCompositeKeyField || isForeignKey || isDiscriminatorField) { continue; } final boolean hasEmbeddedIdField = isEmbeddedIdField(fieldName) && !isCompositeKeyField; if (hasEmbeddedIdField) { fieldName = governorTypeDetails.getUniqueFieldName(fieldName .getSymbolName()); } final FieldMetadataBuilder fieldBuilder = getField(fieldName, column, table.getName(), table.isIncludeNonPortableAttributes()); if (foreignKey != null) { final Table importedKeyForeignTable = foreignKey.getForeignTable(); final String foreignTableName = importedKeyForeignTable.getName(); final String foreignSchemaName = importedKeyForeignTable.getSchema().getName(); final JavaType fieldType = DbreTypeUtils.findTypeForTableName(managedEntities, foreignTableName, foreignSchemaName); final AnnotationMetadataBuilder columnBuilder = new AnnotationMetadataBuilder( DOMAIN_REF_TYPE); columnBuilder.addStringAttribute(NAME, column.getEscapedName()); columnBuilder.addStringAttribute("foreignTableName", foreignKey.getForeignTable().getName()); columnBuilder.addStringAttribute("foreignFieldName", DbreTypeUtils.suggestFieldName(foreignKey.getForeignTable())); final String foreignEntityTypeName = fieldType.getSimpleTypeName(); columnBuilder.addStringAttribute("foreignFieldType", foreignEntityTypeName); if (foreignEntityTypeName.endsWith(DOMAIN_TYPE_TERMINATOR)) { columnBuilder.addStringAttribute("domainFieldType", foreignEntityTypeName); } fieldBuilder.addAnnotation(columnBuilder); } if (fieldBuilder.getFieldType().equals(DATE) && fieldName.getSymbolName().equals(CREATED)) { fieldBuilder.setFieldInitializer("new Date()"); } uniqueFields.put(fieldName, fieldBuilder); } for (final FieldMetadataBuilder fieldBuilder : uniqueFields.values()) { addToBuilder(fieldBuilder); } } private void addToBuilder(final FieldMetadataBuilder fieldBuilder) { final JavaSymbolName fieldName = fieldBuilder.getFieldName(); if (hasField(fieldName, fieldBuilder.buildAnnotations()) || hasFieldInItd(fieldName)) { return; } builder.addField(fieldBuilder); // Check for an existing accessor in the governor final JavaType fieldType = fieldBuilder.getFieldType(); builder.addMethod(getAccessorMethod(fieldName, fieldType)); // Check for an existing mutator in the governor builder.addMethod(getMutatorMethod(fieldName, fieldType)); } private void excludeFieldsInToStringAnnotation(final String fieldName) { final AnnotationMetadata toStringAnnotation = governorTypeDetails .getAnnotation(ROO_TO_STRING); if (toStringAnnotation == null) { return; } final List<AnnotationAttributeValue<?>> attributes = new ArrayList<AnnotationAttributeValue<?>>(); final List<StringAttributeValue> ignoreFields = new ArrayList<StringAttributeValue>(); // Copy the existing attributes, excluding the "ignoreFields" attribute boolean alreadyAdded = false; AnnotationAttributeValue<?> value = toStringAnnotation .getAttribute(new JavaSymbolName("excludeFields")); if (value == null) { value = new ArrayAttributeValue<StringAttributeValue>( new JavaSymbolName("excludeFields"), new ArrayList<StringAttributeValue>()); } // Ensure we have an array of strings final String errMsg = "@RooToString attribute 'excludeFields' must be an array of strings"; Validate.isInstanceOf(ArrayAttributeValue.class, value, errMsg); final ArrayAttributeValue<?> arrayVal = (ArrayAttributeValue<?>) value; for (final Object obj : arrayVal.getValue()) { Validate.isInstanceOf(StringAttributeValue.class, obj, errMsg); final StringAttributeValue sv = (StringAttributeValue) obj; if (sv.getValue().equals(fieldName)) { alreadyAdded = true; } ignoreFields.add(sv); } // Add the desired field to ignore to the end if (!alreadyAdded) { ignoreFields.add(new StringAttributeValue(new JavaSymbolName( "ignored"), fieldName)); } attributes.add(new ArrayAttributeValue<StringAttributeValue>( new JavaSymbolName("excludeFields"), ignoreFields)); final AnnotationMetadataBuilder toStringAnnotationBuilder = new AnnotationMetadataBuilder( ROO_TO_STRING, attributes); updatedGovernorBuilder = new ClassOrInterfaceTypeDetailsBuilder( governorTypeDetails); updatedGovernorBuilder.updateTypeAnnotation( toStringAnnotationBuilder.build(), new HashSet<JavaSymbolName>()); } private String getErrorMsg(final String tableName) { return " but type for table '" + tableName + "' could not be found or is not database managed (not annotated with @RooDbManaged)"; } private String getErrorMsg(final String foreignTableName, final String tableName) { return getErrorMsg(foreignTableName) + " and table '" + tableName + "' has a foreign-key reference to table '" + foreignTableName + "'"; } private FieldMetadataBuilder getField(final JavaSymbolName fieldName, final Column column, final String tableName, final boolean includeNonPortable) { JavaType fieldType = column.getJavaType(); Validate.notNull(fieldType, "Field type for column '" + column.getName() + "' in table '" + tableName + "' is null"); // Check if field is a Boolean object and is required, then change to // boolean primitive if (fieldType.equals(JavaType.BOOLEAN_OBJECT) && column.isRequired()) { fieldType = JavaType.BOOLEAN_PRIMITIVE; } // Add annotations to field final List<AnnotationMetadataBuilder> annotations = new ArrayList<AnnotationMetadataBuilder>(); // Add @Column annotation final AnnotationMetadataBuilder columnBuilder = new AnnotationMetadataBuilder( COLUMN); columnBuilder.addStringAttribute(NAME, column.getEscapedName()); if (includeNonPortable) { columnBuilder.addStringAttribute("columnDefinition", column.getTypeName()); } // Add length attribute for Strings if (column.getColumnSize() < 4000 && fieldType.equals(JavaType.STRING)) { columnBuilder.addIntegerAttribute("length", column.getColumnSize()); } // Add precision and scale attributes for numeric fields if (column.getScale() > 0 && JdkJavaType.isDecimalType(fieldType)) { columnBuilder.addIntegerAttribute("precision", column.getColumnSize()); columnBuilder.addIntegerAttribute("scale", column.getScale()); } // Add unique = true to @Column if applicable if (column.isUnique()) { columnBuilder.addBooleanAttribute("unique", true); } annotations.add(columnBuilder); // Add @NotNull if applicable if (column.isRequired()) { annotations.add(new AnnotationMetadataBuilder(NOT_NULL)); } // Add JSR 220 @Temporal annotation to date fields if (fieldType.equals(DATE) || fieldType.equals(TIMESTAMP)) { final AnnotationMetadataBuilder temporalBuilder = new AnnotationMetadataBuilder( TEMPORAL); temporalBuilder.addEnumAttribute(VALUE, new EnumDetails( TEMPORAL_TYPE, new JavaSymbolName(column.getJdbcType()))); annotations.add(temporalBuilder); final AnnotationMetadataBuilder dateTimeFormatBuilder = new AnnotationMetadataBuilder( DATE_TIME_FORMAT); if (fieldType.equals(DATE)) { dateTimeFormatBuilder.addStringAttribute("style", "M-"); } else { dateTimeFormatBuilder.addStringAttribute("style", "MM"); } if (fieldName.getSymbolName().equals(CREATED)) { columnBuilder.addBooleanAttribute("updatable", false); } annotations.add(dateTimeFormatBuilder); } // @Type(type="org.hibernate.spatial.GeometryType") if (column.getJdbcType().equals(Column.ORACLE_GEOMETRY_TYPE)) { final AnnotationMetadataBuilder geometryBuilder = new AnnotationMetadataBuilder( new JavaType("org.hibernate.annotations.Type")); geometryBuilder.addStringAttribute("type", "org.hibernate.spatial.GeometryType"); annotations.add(geometryBuilder); } // Add @Lob for CLOB fields if applicable if (column.getJdbcType().equals("CLOB")) { annotations.add(new AnnotationMetadataBuilder(LOB)); } final FieldMetadataBuilder fieldBuilder = new FieldMetadataBuilder( getId(), Modifier.PRIVATE, annotations, fieldName, fieldType); if (fieldName.getSymbolName().equals(CREATED)) { if (fieldType.equals(DATE)) { fieldBuilder.setFieldInitializer("new java.util.Date()"); } else { fieldBuilder .setFieldInitializer("new java.sql.Timestamp(new java.util.Date().getTime())"); } } return fieldBuilder; } private String getInflectorPlural(final String term) { try { return Noun.pluralOf(term, Locale.ENGLISH); } catch (final RuntimeException e) { // Inflector failed (see for example ROO-305), so don't pluralize it return term; } } private AnnotationMetadataBuilder getJoinColumnAnnotation( final Reference reference, final boolean referencedColumn) { return getJoinColumnAnnotation(reference, referencedColumn, null); } private AnnotationMetadataBuilder getJoinColumnAnnotation( final Reference reference, final boolean referencedColumn, final JavaType fieldType) { return getJoinColumnAnnotation(reference, referencedColumn, fieldType, null); } private AnnotationMetadataBuilder getJoinColumnAnnotation( final Reference reference, final boolean referencedColumn, final JavaType fieldType, final Boolean nullable) { final Column localColumn = reference.getLocalColumn(); Validate.notNull(localColumn, "Foreign-key reference local column '" + reference.getLocalColumnName() + "' must not be null"); final AnnotationMetadataBuilder joinColumnBuilder = new AnnotationMetadataBuilder( JOIN_COLUMN); joinColumnBuilder .addStringAttribute(NAME, localColumn.getEscapedName()); if (referencedColumn) { final Column foreignColumn = reference.getForeignColumn(); Validate.notNull( foreignColumn, "Foreign-key reference foreign column '" + reference.getForeignColumnName() + "' must not be null"); joinColumnBuilder.addStringAttribute("referencedColumnName", foreignColumn.getEscapedName()); } if (nullable == null) { if (localColumn.isRequired()) { joinColumnBuilder.addBooleanAttribute("nullable", false); } } else { joinColumnBuilder.addBooleanAttribute("nullable", nullable); } if (fieldType != null) { if (isCompositeKeyColumn(localColumn) || localColumn.isPrimaryKey() || !reference.isInsertableOrUpdatable()) { joinColumnBuilder.addBooleanAttribute("insertable", false); joinColumnBuilder.addBooleanAttribute("updatable", false); } } return joinColumnBuilder; } private AnnotationMetadataBuilder getJoinColumnsAnnotation( final Set<Reference> references, final JavaType fieldType) { final List<NestedAnnotationAttributeValue> arrayValues = new ArrayList<NestedAnnotationAttributeValue>(); // XXX DiSiD: Nullable attribute will have same value for each // If some column not required, all JoinColumn will be nullable boolean nullable = false; for (final Reference reference : references) { if (!reference.getLocalColumn().isRequired()) { nullable = true; } } for (final Reference reference : references) { final AnnotationMetadataBuilder joinColumnAnnotation = getJoinColumnAnnotation( reference, true, fieldType, nullable); arrayValues.add(new NestedAnnotationAttributeValue( new JavaSymbolName(VALUE), joinColumnAnnotation.build())); } final List<AnnotationAttributeValue<?>> attributes = new ArrayList<AnnotationAttributeValue<?>>(); attributes.add(new ArrayAttributeValue<NestedAnnotationAttributeValue>( new JavaSymbolName(VALUE), arrayValues)); return new AnnotationMetadataBuilder(JOIN_COLUMNS, attributes); } private FieldMetadataBuilder getManyToManyInverseSideField( final JavaSymbolName fieldName, final JavaSymbolName mappedByFieldName, final Table owningSideTable, final CascadeAction onUpdate, final CascadeAction onDelete) { final JavaType element = DbreTypeUtils.findTypeForTable( managedEntities, owningSideTable); Validate.notNull( element, "Attempted to create many-to-many inverse-side field '" + fieldName + "' in '" + destination.getFullyQualifiedTypeName() + "'" + getErrorMsg(owningSideTable .getFullyQualifiedTableName())); final List<JavaType> params = Arrays.asList(element); final JavaType fieldType = new JavaType( SET.getFullyQualifiedTypeName(), 0, DataType.TYPE, null, params); // Add annotations to field final List<AnnotationMetadataBuilder> annotations = new ArrayList<AnnotationMetadataBuilder>(); final AnnotationMetadataBuilder manyToManyBuilder = new AnnotationMetadataBuilder( MANY_TO_MANY); manyToManyBuilder.addStringAttribute(MAPPED_BY, mappedByFieldName.getSymbolName()); addCascadeType(manyToManyBuilder, onUpdate, onDelete); annotations.add(manyToManyBuilder); return new FieldMetadataBuilder(getId(), Modifier.PRIVATE, annotations, fieldName, fieldType); } private FieldMetadataBuilder getManyToManyOwningSideField( final JavaSymbolName fieldName, final Table joinTable, final Table inverseSideTable, final CascadeAction onUpdate, final CascadeAction onDelete) { final JavaType element = DbreTypeUtils.findTypeForTable( managedEntities, inverseSideTable); Validate.notNull( element, "Attempted to create many-to-many owning-side field '" + fieldName + "' in '" + destination.getFullyQualifiedTypeName() + "' " + getErrorMsg(inverseSideTable .getFullyQualifiedTableName())); final List<JavaType> params = Arrays.asList(element); final JavaType fieldType = new JavaType( SET.getFullyQualifiedTypeName(), 0, DataType.TYPE, null, params); // Add annotations to field final List<AnnotationMetadataBuilder> annotations = new ArrayList<AnnotationMetadataBuilder>(); // Add @ManyToMany annotation final AnnotationMetadataBuilder manyToManyBuilder = new AnnotationMetadataBuilder( MANY_TO_MANY); annotations.add(manyToManyBuilder); // Add @JoinTable annotation final AnnotationMetadataBuilder joinTableBuilder = new AnnotationMetadataBuilder( JOIN_TABLE); final List<AnnotationAttributeValue<?>> joinTableAnnotationAttributes = new ArrayList<AnnotationAttributeValue<?>>(); joinTableAnnotationAttributes.add(new StringAttributeValue( new JavaSymbolName(NAME), joinTable.getName())); final Iterator<ForeignKey> iter = joinTable.getImportedKeys() .iterator(); // Add "joinColumns" attribute containing nested @JoinColumn annotations final List<NestedAnnotationAttributeValue> joinColumnArrayValues = new ArrayList<NestedAnnotationAttributeValue>(); final Set<Reference> firstKeyReferences = iter.next().getReferences(); for (final Reference reference : firstKeyReferences) { final AnnotationMetadataBuilder joinColumnBuilder = getJoinColumnAnnotation( reference, firstKeyReferences.size() > 1); joinColumnArrayValues.add(new NestedAnnotationAttributeValue( new JavaSymbolName(VALUE), joinColumnBuilder.build())); } joinTableAnnotationAttributes .add(new ArrayAttributeValue<NestedAnnotationAttributeValue>( new JavaSymbolName("joinColumns"), joinColumnArrayValues)); // Add "inverseJoinColumns" attribute containing nested @JoinColumn // annotations final List<NestedAnnotationAttributeValue> inverseJoinColumnArrayValues = new ArrayList<NestedAnnotationAttributeValue>(); final Set<Reference> lastKeyReferences = iter.next().getReferences(); for (final Reference reference : lastKeyReferences) { final AnnotationMetadataBuilder joinColumnBuilder = getJoinColumnAnnotation( reference, lastKeyReferences.size() > 1); inverseJoinColumnArrayValues .add(new NestedAnnotationAttributeValue(new JavaSymbolName( VALUE), joinColumnBuilder.build())); } joinTableAnnotationAttributes .add(new ArrayAttributeValue<NestedAnnotationAttributeValue>( new JavaSymbolName("inverseJoinColumns"), inverseJoinColumnArrayValues)); // Add attributes to a @JoinTable annotation builder joinTableBuilder.setAttributes(joinTableAnnotationAttributes); annotations.add(joinTableBuilder); return new FieldMetadataBuilder(getId(), Modifier.PRIVATE, annotations, fieldName, fieldType); } private FieldMetadataBuilder getOneToManyMappedByField( final JavaSymbolName fieldName, final JavaSymbolName mappedByFieldName, final String foreignTableName, final String foreignSchemaName, final CascadeAction onUpdate, final CascadeAction onDelete) { final JavaType element = DbreTypeUtils.findTypeForTableName( managedEntities, foreignTableName, foreignSchemaName); Validate.notNull(element, "Attempted to create one-to-many mapped-by field '" + fieldName + "' in '" + destination.getFullyQualifiedTypeName() + "'" + getErrorMsg(foreignTableName + "." + foreignSchemaName)); final JavaType fieldType = new JavaType( SET.getFullyQualifiedTypeName(), 0, DataType.TYPE, null, Arrays.asList(element)); // Add @OneToMany annotation final List<AnnotationMetadataBuilder> annotations = new ArrayList<AnnotationMetadataBuilder>(); final AnnotationMetadataBuilder oneToManyBuilder = new AnnotationMetadataBuilder( ONE_TO_MANY); oneToManyBuilder.addStringAttribute(MAPPED_BY, mappedByFieldName.getSymbolName()); addCascadeType(oneToManyBuilder, onUpdate, onDelete); annotations.add(oneToManyBuilder); return new FieldMetadataBuilder(getId(), Modifier.PRIVATE, annotations, fieldName, fieldType); } private FieldMetadataBuilder getOneToOneMappedByField( final JavaSymbolName fieldName, final JavaType fieldType, final JavaSymbolName mappedByFieldName, final CascadeAction onUpdate, final CascadeAction onDelete) { final List<AnnotationMetadataBuilder> annotations = new ArrayList<AnnotationMetadataBuilder>(); final AnnotationMetadataBuilder oneToOneBuilder = new AnnotationMetadataBuilder( ONE_TO_ONE); oneToOneBuilder.addStringAttribute(MAPPED_BY, mappedByFieldName.getSymbolName()); addCascadeType(oneToOneBuilder, onUpdate, onDelete); annotations.add(oneToOneBuilder); return new FieldMetadataBuilder(getId(), Modifier.PRIVATE, annotations, fieldName, fieldType); } // private FieldMetadataBuilder getOneToOneOrManyToOneField( // final JavaSymbolName fieldName, final JavaType fieldType, // final ForeignKey foreignKey, final JavaType annotationType, // final boolean referencedColumn) { // boolean localColumnIsPrimaryKeyColumn = false; // // annotations to field // final List<AnnotationMetadataBuilder> annotations = new ArrayList<AnnotationMetadataBuilder>(); // // // Check for oneToOne where the local column is the primary key // if (foreignKey.getReferenceCount() == 1) { // final Column localColumn = foreignKey.getReferences().iterator().next().getLocalColumn(); // if (localColumn.isPrimaryKey()) { // return null; //// localColumnIsPrimaryKeyColumn = true; //// // Add MapsId annotation to cause child pk to be automatically updated by parent. //// annotations.add(new AnnotationMetadataBuilder(MAPS_ID)); //// annotations.add(new AnnotationMetadataBuilder(NOT_NULL)); // } // } // // // Add annotation // final AnnotationMetadataBuilder annotationBuilder = new AnnotationMetadataBuilder( // annotationType); // if (foreignKey.isExported() || localColumnIsPrimaryKeyColumn) { // addCascadeType(annotationBuilder, foreignKey.getOnUpdate(), // foreignKey.getOnDelete()); // annotationBuilder.addBooleanAttribute("optional", false); // } private FieldMetadataBuilder getOneToOneOrManyToOneField( final JavaSymbolName fieldName, final JavaType fieldType, final ForeignKey foreignKey, final JavaType annotationType, final boolean referencedColumn) { // Add annotations to field final List<AnnotationMetadataBuilder> annotations = new ArrayList<AnnotationMetadataBuilder>(); // Add annotation final AnnotationMetadataBuilder annotationBuilder = new AnnotationMetadataBuilder( annotationType); if (foreignKey.isExported()) { addCascadeType(annotationBuilder, foreignKey.getOnUpdate(), foreignKey.getOnDelete()); } annotations.add(annotationBuilder); final Set<Reference> references = foreignKey.getReferences(); if (references.size() == 1) { final Column localColumn = references.iterator().next().getLocalColumn(); if (annotationType.equals(ONE_TO_ONE) && localColumn.isPrimaryKey()) { return null; } // Add @JoinColumn annotation annotations.add(getJoinColumnAnnotation(references.iterator() .next(), referencedColumn, fieldType)); if (localColumn.isRequired()) { annotations.add(new AnnotationMetadataBuilder(NOT_NULL)); } } else { // Add @JoinColumns annotation annotations.add(getJoinColumnsAnnotation(references, fieldType)); } return new FieldMetadataBuilder(getId(), Modifier.PRIVATE, annotations, fieldName, fieldType); } private boolean hasField(final JavaSymbolName fieldName, final List<AnnotationMetadata> annotations) { // Check governor for field if (governorTypeDetails.getField(fieldName) != null) { return true; } // Check @Column and @JoinColumn annotations on fields in governor with // the same 'name' as the generated field final List<FieldMetadata> governorFields = governorTypeDetails .getFieldsWithAnnotation(COLUMN); governorFields.addAll(governorTypeDetails .getFieldsWithAnnotation(JOIN_COLUMN)); for (final FieldMetadata governorField : governorFields) { governorFieldAnnotations: for (final AnnotationMetadata governorFieldAnnotation : governorField .getAnnotations()) { if (governorFieldAnnotation.getAnnotationType().equals(COLUMN) || governorFieldAnnotation.getAnnotationType().equals( JOIN_COLUMN)) { final AnnotationAttributeValue<?> name = governorFieldAnnotation .getAttribute(new JavaSymbolName(NAME)); if (name == null) { continue governorFieldAnnotations; } for (final AnnotationMetadata annotationMetadata : annotations) { final AnnotationAttributeValue<?> columnName = annotationMetadata .getAttribute(new JavaSymbolName(NAME)); if (columnName != null && columnName.equals(name)) { return true; } } } } } return false; } /** * Indicates whether the ITD being built has a field of the given name * * @param fieldName * @return true if the field exists in the builder, otherwise false */ private boolean hasFieldInItd(final JavaSymbolName fieldName) { for (final FieldMetadataBuilder declaredField : builder .getDeclaredFields()) { if (declaredField.getFieldName().equals(fieldName)) { return true; } } return false; } private boolean isCompositeKeyColumn(final Column column) { if (!identifierHolder.isEmbeddedIdField()) { return false; } for (final FieldMetadata field : identifierHolder .getEmbeddedIdentifierFields()) { for (final AnnotationMetadata annotation : field.getAnnotations()) { if (!annotation.getAnnotationType().equals(COLUMN)) { continue; } final AnnotationAttributeValue<?> nameAttribute = annotation .getAttribute(new JavaSymbolName(NAME)); if (nameAttribute != null) { final String name = (String) nameAttribute.getValue(); if (column.getName().equals(name)) { return true; } } } } return false; } private boolean isCompositeKeyField(final JavaSymbolName fieldName) { if (!identifierHolder.isEmbeddedIdField()) { return false; } for (final FieldMetadata field : identifierHolder .getEmbeddedIdentifierFields()) { if (field.getFieldName().equals(fieldName)) { return true; } } return false; } private boolean isEmbeddedIdField(final JavaSymbolName fieldName) { return identifierHolder.isEmbeddedIdField() && identifierHolder.getIdentifierField().getFieldName() .equals(fieldName); } private boolean isIdField(final JavaSymbolName fieldName) { return !identifierHolder.isEmbeddedIdField() && identifierHolder.getIdentifierField().getFieldName() .equals(fieldName); } private boolean isOneToOne(final Table table, final ForeignKey foreignKey) { Validate.notNull(table, "Table must not be null in determining a one-to-one relationship"); Validate.notNull(foreignKey, "Foreign key must not be null in determining a one-to-one relationship"); boolean equals = table.getPrimaryKeyCount() == foreignKey .getReferenceCount(); final Iterator<Column> primaryKeyIterator = table.getPrimaryKeys() .iterator(); while (equals && primaryKeyIterator.hasNext()) { equals &= foreignKey.hasLocalColumn(primaryKeyIterator.next()); } return equals; } private boolean isVersionField(final JavaSymbolName fieldName) { return versionField != null && versionField.getFieldName().equals(fieldName); } }