net.java.ao.schema.SchemaGenerator Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of activeobjects-core Show documentation
Show all versions of activeobjects-core Show documentation
This is the core library for Active Objects. It is generic and can be embedded in any environment.
As such it is generic and won't contain all connection pooling, etc.
/*
* Copyright 2007 Daniel Spiewak
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.java.ao.schema;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import net.java.ao.ActiveObjectsConfigurationException;
import net.java.ao.AnnotationDelegate;
import net.java.ao.Common;
import net.java.ao.DatabaseProvider;
import net.java.ao.ManyToMany;
import net.java.ao.OneToMany;
import net.java.ao.OneToOne;
import net.java.ao.Polymorphic;
import net.java.ao.RawEntity;
import net.java.ao.SchemaConfiguration;
import net.java.ao.schema.ddl.DDLAction;
import net.java.ao.schema.ddl.DDLField;
import net.java.ao.schema.ddl.DDLForeignKey;
import net.java.ao.schema.ddl.DDLIndex;
import net.java.ao.schema.ddl.DDLTable;
import net.java.ao.schema.ddl.SQLAction;
import net.java.ao.schema.ddl.SchemaReader;
import net.java.ao.schema.index.IndexParser;
import net.java.ao.types.TypeInfo;
import net.java.ao.types.TypeManager;
import net.java.ao.types.TypeQualifiers;
import net.java.ao.util.EnumUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.Date;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static com.google.common.collect.Iterables.addAll;
import static net.java.ao.types.TypeQualifiers.MAX_STRING_LENGTH;
import static net.java.ao.types.TypeQualifiers.qualifiers;
/**
* WARNING: Not part of the public API. This class is public only
* to allow its use within other packages in the ActiveObjects library.
*
* @author Daniel Spiewak
*/
public final class SchemaGenerator {
private static final Logger logger = LoggerFactory.getLogger(SchemaGenerator.class);
private static final Set AUTO_INCREMENT_LEGAL_TYPES = ImmutableSet.of(Types.INTEGER, Types.BIGINT);
public static void migrate(DatabaseProvider provider,
SchemaConfiguration schemaConfiguration,
NameConverters nameConverters,
final boolean executeDestructiveUpdates,
Class extends RawEntity>>... classes) throws SQLException {
final Iterable> actionGroups = generateImpl(provider, schemaConfiguration, nameConverters, executeDestructiveUpdates, classes);
final Connection conn = provider.getConnection();
try {
final Statement stmt = conn.createStatement();
try {
Set completedStatements = new HashSet();
for (Iterable actionGroup : actionGroups) {
addAll(completedStatements, provider.executeUpdatesForActions(stmt, actionGroup, completedStatements));
}
} finally {
stmt.close();
}
} finally {
conn.close();
}
}
private static Iterable> generateImpl(final DatabaseProvider provider,
final SchemaConfiguration schemaConfiguration,
final NameConverters nameConverters,
final boolean executeDestructiveUpdates,
Class extends RawEntity>>... classes) throws SQLException {
final DDLTable[] parsedTables = parseDDL(provider, nameConverters, classes);
final DDLTable[] readTables = SchemaReader.readSchema(provider, nameConverters, schemaConfiguration);
final DDLAction[] actions = SchemaReader.sortTopologically(SchemaReader.diffSchema(provider.getTypeManager(), parsedTables, readTables, provider.isCaseSensitive()));
return Iterables.transform(Iterables.filter(ImmutableList.copyOf(actions), new Predicate() {
@Override
public boolean apply(final DDLAction input) {
switch (input.getActionType()) {
case DROP:
case ALTER_DROP_COLUMN:
return executeDestructiveUpdates;
default:
return true;
}
}
}),
new Function>() {
public Iterable apply(DDLAction from) {
return provider.renderAction(nameConverters, from);
}
});
}
static DDLTable[] parseDDL(DatabaseProvider provider, NameConverters nameConverters, Class extends RawEntity>>... classes) {
final Map>, Set>>> deps = new HashMap>, Set>>>();
final Set>> roots = new LinkedHashSet>>();
for (Class extends RawEntity>> cls : classes) {
parseDependencies(nameConverters.getFieldNameConverter(), deps, roots, cls);
}
ArrayList parsedTables = new ArrayList();
parseDDLRoots(provider, nameConverters, deps, roots, parsedTables);
if (!deps.isEmpty()) {
throw new RuntimeException("Circular dependency detected");
}
return parsedTables.toArray(new DDLTable[parsedTables.size()]);
}
private static void parseDDLRoots(final DatabaseProvider provider,
final NameConverters nameConverters,
final Map>, Set>>> deps,
final Set>> roots,
final ArrayList parsedTables) {
while (!roots.isEmpty()) {
Class extends RawEntity>> clazz = roots.iterator().next();
roots.remove(clazz);
if (clazz.getAnnotation(Polymorphic.class) == null) {
parsedTables.add(parseInterface(provider, nameConverters, clazz));
}
List>> toRemove = new LinkedList>>();
for (final Class extends RawEntity>> depClass : deps.keySet()) {
Set>> individualDeps = deps.get(depClass);
individualDeps.remove(clazz);
if (individualDeps.isEmpty()) {
roots.add(depClass);
toRemove.add(depClass);
}
}
for (Class extends RawEntity>> remove : toRemove) {
deps.remove(remove);
}
}
}
private static void parseDependencies(
final FieldNameConverter fieldConverter,
final Map>, Set>>> deps,
final Set>> roots, Class extends RawEntity>> clazz) {
if (deps.containsKey(clazz)) {
return;
}
final Set>> individualDeps = new LinkedHashSet>>();
for (final Method method : clazz.getMethods()) {
final Class> type = Common.getAttributeTypeFromMethod(method);
validateManyToManyAnnotation(method);
validateOneToOneAnnotation(method);
validateOneToManyAnnotation(method);
if (fieldConverter.getName(method) != null && type != null && !type.equals(clazz) &&
RawEntity.class.isAssignableFrom(type) && !individualDeps.contains(type)) {
individualDeps.add((Class extends RawEntity>>) type);
addDeps(deps, clazz, individualDeps);
parseDependencies(fieldConverter, deps, roots, (Class extends RawEntity>>) type);
}
}
if (individualDeps.size() == 0) {
roots.add(clazz);
} else {
addDeps(deps, clazz, individualDeps);
}
}
private static void addDeps(final Map>, Set>>> deps,
final Class extends RawEntity>> clazz,
final Set>> individualDeps) {
Set>> classes = deps.get(clazz);
if (classes != null) {
classes.addAll(individualDeps);
} else {
deps.put(clazz, individualDeps);
}
}
private static void validateManyToManyAnnotation(final Method method) {
final ManyToMany manyToMany = method.getAnnotation(ManyToMany.class);
if (manyToMany != null) {
final Class extends RawEntity>> throughType = manyToMany.value();
final String reverse = manyToMany.reverse();
if (reverse.length() != 0) {
try {
throughType.getMethod(reverse);
} catch (final NoSuchMethodException exception) {
throw new IllegalArgumentException(method + " has a ManyToMany annotation with an invalid reverse element value. It must be the name of the corresponding getter method on the joining entity.", exception);
}
}
if (manyToMany.through().length() != 0) {
try {
throughType.getMethod(manyToMany.through());
} catch (final NoSuchMethodException exception) {
throw new IllegalArgumentException(method + " has a ManyToMany annotation with an invalid through element value. It must be the name of the getter method on the joining entity that refers to the remote entities.", exception);
}
}
}
}
private static void validateOneToManyAnnotation(final Method method) {
final OneToMany oneToMany = method.getAnnotation(OneToMany.class);
if (oneToMany != null) {
final String reverse = oneToMany.reverse();
if (reverse.length() != 0) {
try {
method.getReturnType().getComponentType().getMethod(reverse);
} catch (final NoSuchMethodException exception) {
throw new IllegalArgumentException(method + " has a OneToMany annotation with an invalid reverse element value. It must be the name of the corresponding getter method on the related entity.", exception);
}
}
}
}
private static void validateOneToOneAnnotation(final Method method) {
final OneToOne oneToOne = method.getAnnotation(OneToOne.class);
if (oneToOne != null) {
final String reverse = oneToOne.reverse();
if (reverse.length() != 0) {
try {
method.getReturnType().getMethod(reverse);
} catch (final NoSuchMethodException exception) {
throw new IllegalArgumentException(method + " has OneToMany annotation with an invalid reverse element value. It be the name of the corresponding getter method on the related entity.", exception);
}
}
}
}
/**
* Not intended for public use.
*/
public static DDLTable parseInterface(DatabaseProvider provider, NameConverters nameConverters, Class extends RawEntity>> clazz) {
String sqlName = nameConverters.getTableNameConverter().getName(clazz);
DDLTable table = new DDLTable();
table.setName(sqlName);
table.setFields(parseFields(provider, nameConverters.getFieldNameConverter(), clazz));
table.setForeignKeys(parseForeignKeys(nameConverters.getTableNameConverter(), nameConverters.getFieldNameConverter(), clazz));
table.setIndexes(parseIndexes(provider, nameConverters, clazz));
return table;
}
/**
* Not intended for public usage. This method is declared public
* only to enable use within other ActiveObjects packages. Consider this
* function unsupported.
*/
public static DDLField[] parseFields(DatabaseProvider provider, FieldNameConverter fieldConverter, Class extends RawEntity>> clazz) {
List fields = new ArrayList();
List attributes = new LinkedList();
for (Method method : Common.getValueFieldsMethods(clazz, fieldConverter)) {
String attributeName = fieldConverter.getName(method);
final Class> type = Common.getAttributeTypeFromMethod(method);
if (attributeName != null && type != null) {
checkIsSupportedType(method, type);
if (attributes.contains(attributeName)) {
continue;
}
attributes.add(attributeName);
final AnnotationDelegate annotations = Common.getAnnotationDelegate(fieldConverter, method);
DDLField field = new DDLField();
field.setName(attributeName);
final TypeManager typeManager = provider.getTypeManager();
final TypeInfo> sqlType = getSQLTypeFromMethod(typeManager, type, method, annotations);
field.setType(sqlType);
field.setJdbcType(sqlType.getJdbcWriteType());
field.setPrimaryKey(isPrimaryKey(annotations, field));
field.setNotNull(annotations.isAnnotationPresent(NotNull.class) || annotations.isAnnotationPresent(Unique.class) || annotations.isAnnotationPresent(PrimaryKey.class));
field.setUnique(annotations.isAnnotationPresent(Unique.class));
final boolean isAutoIncrement = isAutoIncrement(type, annotations, field.getType());
field.setAutoIncrement(isAutoIncrement);
if (!isAutoIncrement) {
if (annotations.isAnnotationPresent(Default.class)) {
final Object defaultValue = convertStringDefaultValue(annotations.getAnnotation(Default.class).value(), sqlType, method);
if (type.isEnum() && ((Integer) defaultValue) > EnumUtils.size((Class extends Enum>) type) - 1) {
throw new ActiveObjectsConfigurationException("There is no enum value of '" + type + "'for which the ordinal is " + defaultValue);
}
field.setDefaultValue(defaultValue);
} else if (ImmutableSet.>of(short.class, float.class, int.class, long.class, double.class).contains(type)) {
// set the default value for primitive types (float, short, int, long, char)
field.setDefaultValue(convertStringDefaultValue("0", sqlType, method));
}
}
if (field.isPrimaryKey()) {
fields.add(0, field);
} else {
fields.add(field);
}
if (RawEntity.class.isAssignableFrom(type)
&& type.getAnnotation(Polymorphic.class) != null) {
field.setDefaultValue(null); // polymorphic fields can't have default
attributeName = fieldConverter.getPolyTypeName(method);
field = new DDLField();
field.setName(attributeName);
field.setType(typeManager.getType(String.class, qualifiers().stringLength(127)));
field.setJdbcType(java.sql.Types.VARCHAR);
if (annotations.getAnnotation(NotNull.class) != null) {
field.setNotNull(true);
}
fields.add(field);
}
}
}
return fields.toArray(new DDLField[fields.size()]);
}
private static void checkIsSupportedType(Method method, Class> type) {
if (type.equals(java.sql.Date.class)) {
throw new ActiveObjectsConfigurationException(Date.class.getName()
+ " is not supported! Please use " + java.util.Date.class.getName() + " instead.")
.forMethod(method);
}
}
private static boolean isPrimaryKey(AnnotationDelegate annotations, DDLField field) {
final boolean isPrimaryKey = annotations.isAnnotationPresent(PrimaryKey.class);
if (isPrimaryKey && !field.getType().isAllowedAsPrimaryKey()) {
throw new ActiveObjectsConfigurationException(PrimaryKey.class.getName() + " is not supported for type: " + field.getType());
}
return isPrimaryKey;
}
private static boolean isAutoIncrement(Class> type, AnnotationDelegate annotations, TypeInfo> dbType) {
final boolean isAutoIncrement = annotations.isAnnotationPresent(AutoIncrement.class);
if (isAutoIncrement && (!AUTO_INCREMENT_LEGAL_TYPES.contains(dbType.getJdbcWriteType()) || type.isEnum())) {
throw new ActiveObjectsConfigurationException(AutoIncrement.class.getName() + " is not supported for type: " + dbType);
}
return isAutoIncrement;
}
public static TypeInfo> getSQLTypeFromMethod(TypeManager typeManager, Class> type, Method method, AnnotationDelegate annotations) {
TypeQualifiers qualifiers = qualifiers();
StringLength lengthAnno = annotations.getAnnotation(StringLength.class);
if (lengthAnno != null) {
final int length = lengthAnno.value();
if (length > MAX_STRING_LENGTH) {
throw new ActiveObjectsConfigurationException("@StringLength must be <= " + MAX_STRING_LENGTH + " or UNLIMITED").forMethod(method);
}
try {
qualifiers = qualifiers.stringLength(length);
} catch (ActiveObjectsConfigurationException e) {
throw new ActiveObjectsConfigurationException(e.getMessage()).forMethod(method);
}
}
return typeManager.getType(type, qualifiers);
}
private static DDLForeignKey[] parseForeignKeys(TableNameConverter nameConverter, FieldNameConverter fieldConverter,
Class extends RawEntity>> clazz) {
Set back = new LinkedHashSet();
for (Method method : clazz.getMethods()) {
String attributeName = fieldConverter.getName(method);
Class> type = Common.getAttributeTypeFromMethod(method);
if (type != null && attributeName != null && RawEntity.class.isAssignableFrom(type)
&& type.getAnnotation(Polymorphic.class) == null) {
DDLForeignKey key = new DDLForeignKey();
key.setField(attributeName);
key.setTable(nameConverter.getName((Class extends RawEntity>>) type));
key.setForeignField(Common.getPrimaryKeyField((Class extends RawEntity>>) type, fieldConverter));
key.setDomesticTable(nameConverter.getName(clazz));
back.add(key);
}
}
return back.toArray(new DDLForeignKey[back.size()]);
}
@VisibleForTesting
static DDLIndex[] parseIndexes(
final DatabaseProvider provider,
final NameConverters nameConverters,
final Class extends RawEntity>> clazz
) {
final IndexParser indexParser = new IndexParser(provider, nameConverters, provider.getTypeManager());
final Set indexes = indexParser.parseIndexes(clazz);
return indexes.toArray(new DDLIndex[indexes.size()]);
}
private static Object convertStringDefaultValue(String value, TypeInfo> type, Method method) {
if (value == null) {
return null;
}
if (!type.getSchemaProperties().isDefaultValueAllowed()) {
throw new ActiveObjectsConfigurationException("Default value is not allowed for database type " +
type.getSchemaProperties().getSqlTypeName());
}
try {
Object ret = type.getLogicalType().parseDefault(value);
if (ret == null) {
throw new ActiveObjectsConfigurationException("Default value cannot be empty").forMethod(method);
}
return ret;
} catch (IllegalArgumentException e) {
throw new ActiveObjectsConfigurationException(e.getMessage());
}
}
}