All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.java.ao.schema.SchemaGenerator Maven / Gradle / Ivy

Go to download

This is the full Active Objects library, if you don't know which one to use, you probably want this one.

There is a newer version: 6.1.1
Show newest version
/*
 * 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>... 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>... 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>... classes) {
        final Map>, Set>>> deps = new HashMap>, Set>>>();
        final Set>> roots = new LinkedHashSet>>();

        for (Class> 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> 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> depClass : deps.keySet()) {
                Set>> individualDeps = deps.get(depClass);
                individualDeps.remove(clazz);

                if (individualDeps.isEmpty()) {
                    roots.add(depClass);
                    toRemove.add(depClass);
                }
            }

            for (Class> remove : toRemove) {
                deps.remove(remove);
            }
        }
    }

    private static void parseDependencies(
            final FieldNameConverter fieldConverter,
            final Map>, Set>>> deps,
            final Set>> roots, Class> 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>) type);
                addDeps(deps, clazz, individualDeps);
                parseDependencies(fieldConverter, deps, roots, (Class>) type);
            }
        }

        if (individualDeps.size() == 0) {
            roots.add(clazz);
        } else {
            addDeps(deps, clazz, individualDeps);
        }
    }

    private static void addDeps(final Map>, Set>>> deps,
                                final Class> 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> 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> 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> 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) 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> 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>) type));
                key.setForeignField(Common.getPrimaryKeyField((Class>) 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> 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());
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy