package org.springframework.roo.addon.dbre.addon;
import static org.springframework.roo.model.JdkJavaType.CALENDAR;
import static org.springframework.roo.model.JdkJavaType.DATE;
import static org.springframework.roo.model.JdkJavaType.SET;
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.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 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.addon.model.CascadeAction;
import org.springframework.roo.addon.dbre.addon.model.Column;
import org.springframework.roo.addon.dbre.addon.model.Database;
import org.springframework.roo.addon.dbre.addon.model.ForeignKey;
import org.springframework.roo.addon.dbre.addon.model.Reference;
import org.springframework.roo.addon.dbre.addon.model.Table;
import org.springframework.roo.addon.dbre.annotations.RooDbManaged;
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.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";
private DbManagedAnnotationValues annotationValues;
private Database database;
private IdentifierHolder identifierHolder;
private Iterable<ClassOrInterfaceTypeDetails> managedEntities;
private ClassOrInterfaceTypeDetailsBuilder updatedGovernorBuilder;
private FieldMetadata versionField;
// XXX DiSiD: Move var from method to class (store successive modifications)
// http://projects.disid.com/issues/7455
private AnnotationMetadata toStringAnnotation = governorTypeDetails.getAnnotation(ROO_TO_STRING);
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");
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
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 %s", errMsg);
final Table inverseSideTable = foreignKey2.getForeignTable();
Validate.notNull(inverseSideTable, "Inverse-side %s", 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);
// Exclude these fields in @RooToString to avoid circular references
// - ROO-1399
excludeFieldsInToStringAnnotation(fieldBuilder.getFieldName().getSymbolName());
}
// Add unique inverse-side many-to-one fields
for (final FieldMetadataBuilder fieldBuilder : uniqueInverseSideFields.values()) {
addToBuilder(fieldBuilder);
// Exclude these fields in @RooToString to avoid circular references
// - ROO-1399
excludeFieldsInToStringAnnotation(fieldBuilder.getFieldName().getSymbolName());
}
}
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()));
} 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 '%s' in '%s' %s",
fieldName,
destination.getFullyQualifiedTypeName(),
getErrorMsg(foreignTable.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, MANY_TO_ONE, true);
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());
}
}
private void addOneToManyFields(final Table table) {
Validate.notNull(table, "Table required");
if (table.isJoinTable()) {
return;
}
for (final ForeignKey exportedKey : table.getExportedKeys()) {
final Table exportedKeyForeignTable = exportedKey.getForeignTable();
Validate
.notNull(
exportedKeyForeignTable,
"Foreign key table for foreign key '%s' in table '%s' does not exist. One-to-many relationship not created",
exportedKey.getName(), table.getFullyQualifiedTableName());
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);
// Exclude these fields in @RooToString to avoid circular references
// - ROO-1399
excludeFieldsInToStringAnnotation(fieldBuilder.getFieldName().getSymbolName());
}
}
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();
Validate
.notNull(
importedKeyForeignTable,
"Foreign key table for foreign key '%s' in table '%s' does not exist. One-to-one relationship not created",
foreignKey.getName(), table.getFullyQualifiedTableName());
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 '%s' in '%s' %s",
fieldName,
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);
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;
}
for (final ForeignKey exportedKey : table.getExportedKeys()) {
final Table exportedKeyForeignTable = exportedKey.getForeignTable();
Validate
.notNull(
exportedKeyForeignTable,
"Foreign key table for foreign key '%s' in table '%s' does not exist. One-to-one relationship not created",
exportedKey.getName(), table.getFullyQualifiedTableName());
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 has a foreign-key reference to 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(DbreTypeUtils.suggestFieldName(foreignTableName) + fieldSuffix);
final JavaType fieldType =
DbreTypeUtils.findTypeForTableName(managedEntities, foreignTableName, foreignSchemaName);
Validate.notNull(fieldType, "Attempted to create one-to-one mapped-by field '%s' in '%s' %s",
fieldName, 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()) + fieldSuffix);
final FieldMetadataBuilder fieldBuilder =
getOneToOneMappedByField(fieldName, fieldType, mappedByFieldName,
exportedKey.getOnUpdate(), exportedKey.getOnDelete());
addToBuilder(fieldBuilder);
// Exclude these fields in @RooToString to avoid circular references
// - ROO-1399
excludeFieldsInToStringAnnotation(fieldBuilder.getFieldName().getSymbolName());
}
}
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();
JavaSymbolName fieldName = new JavaSymbolName(DbreTypeUtils.suggestFieldName(columnName));
final boolean isIdField = isIdField(fieldName) || column.isPrimaryKey();
final boolean isVersionField =
isVersionField(fieldName)
|| (columnName.equals("version") && !database.isDisableVersionFields());
final boolean isCompositeKeyField = isCompositeKeyField(fieldName);
final boolean isForeignKey = table.findImportedKeyByLocalColumnName(columnName) != null;
if (isIdField || isVersionField || isCompositeKeyField || isForeignKey) {
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 (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));
}
// XXX DiSiD: Invoke this method when add non other fields to builder
// http://projects.disid.com/issues/7455
private void excludeFieldsInToStringAnnotation(final String fieldName) {
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);
toStringAnnotation = toStringAnnotationBuilder.build();
updatedGovernorBuilder.updateTypeAnnotation(toStringAnnotation, new HashSet<JavaSymbolName>());
}
private String getErrorMsg(final String tableName) {
return String
.format(
" but type for table '%s' could not be found or is not database managed (not annotated with @RooDbManaged)",
tableName);
}
private String getErrorMsg(final String foreignTableName, final String tableName) {
return getErrorMsg(foreignTableName)
+ String.format(" and table '%s' has a foreign-key reference to table '%s'", tableName,
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 '%s' in table '%s' is null",
column.getName(), tableName);
// 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
int columnSize = column.getColumnSize();
if (columnSize < 4000 && fieldType.equals(JavaType.STRING)) {
columnBuilder.addIntegerAttribute("length", columnSize);
}
// Add precision and scale attributes for numeric fields
if (columnSize > 0 && JdkJavaType.isDecimalType(fieldType)) {
columnBuilder.addIntegerAttribute("precision", columnSize);
int scale = column.getScale();
if (scale > 0) {
columnBuilder.addIntegerAttribute("scale", scale);
}
}
// 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(CALENDAR)) {
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);
}
// 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("java.util.Calendar.getInstance()");
}
}
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 '%s' must not be null",
reference.getForeignColumnName());
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>();
// 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 '%s' in '%s' %s", fieldName,
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 '%s' in '%s' %s",
fieldName, 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 '%s' in '%s' %s",
fieldName, 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) {
// 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) {
// Add @JoinColumn annotation
annotations.add(getJoinColumnAnnotation(references.iterator().next(), referencedColumn,
fieldType));
} 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);
}
}