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 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.

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 - 2024 Weber Informatics LLC | Privacy Policy