com.mmnaseri.dragonfly.metadata.impl.AnnotationTableMetadataResolver.java Source code

Java tutorial

Introduction

Here is the source code for com.mmnaseri.dragonfly.metadata.impl.AnnotationTableMetadataResolver.java

Source

/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2013 Milad Naseri.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package com.mmnaseri.dragonfly.metadata.impl;

import com.mmnaseri.couteau.basics.api.Filter;
import com.mmnaseri.couteau.basics.api.Processor;
import com.mmnaseri.couteau.basics.api.Transformer;
import com.mmnaseri.couteau.reflection.util.ReflectionUtils;
import com.mmnaseri.couteau.reflection.util.assets.AnnotatedElementFilter;
import com.mmnaseri.couteau.reflection.util.assets.GetterMethodFilter;
import com.mmnaseri.couteau.reflection.util.assets.MethodReturnTypeFilter;
import com.mmnaseri.couteau.reflection.util.assets.PropertyAccessorFilter;
import com.mmnaseri.dragonfly.annotations.*;
import com.mmnaseri.dragonfly.dialect.DatabaseDialect;
import com.mmnaseri.dragonfly.error.*;
import com.mmnaseri.dragonfly.metadata.*;
import com.mmnaseri.dragonfly.tools.ColumnNameFilter;
import com.mmnaseri.dragonfly.tools.ColumnPropertyFilter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.persistence.*;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Time;
import java.sql.Types;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;

import static com.mmnaseri.couteau.basics.collections.CollectionWrapper.with;
import static com.mmnaseri.couteau.reflection.util.ReflectionUtils.withMethods;

/**
 * @author Milad Naseri (mmnaseri@programmer.net)
 * @since 1.0 (2013/9/5, 12:57)
 */
public class AnnotationTableMetadataResolver implements TableMetadataResolver {

    private static final Log log = LogFactory.getLog(TableMetadataResolver.class);
    private static final String NO_SCHEMA = "";
    public static final String CLASS_PROPERTY = "class";
    private final DatabaseDialect dialect;

    public AnnotationTableMetadataResolver(DatabaseDialect dialect) {
        this.dialect = dialect;
    }

    @Override
    public <E> TableMetadata<E> resolve(final Class<E> entityType) {
        log.info("Resolving metadata for " + entityType.getCanonicalName());
        final String tableName;
        final String schema;
        final Set<Set<String>> uniqueColumns = new HashSet<Set<String>>();
        final Set<String> keyColumns = new HashSet<String>();
        final Set<String> foreignKeys = new HashSet<String>();
        final HashSet<RelationMetadata<E, ?>> foreignReferences = new HashSet<RelationMetadata<E, ?>>();
        if (entityType.isAnnotationPresent(Table.class)) {
            final Table table = entityType.getAnnotation(Table.class);
            tableName = table.name().isEmpty() ? entityType.getSimpleName() : table.name();
            schema = table.schema();
            for (UniqueConstraint constraint : table.uniqueConstraints()) {
                final HashSet<String> columns = new HashSet<String>();
                uniqueColumns.add(columns);
                Collections.addAll(columns, constraint.columnNames());
            }
        } else {
            tableName = entityType.getSimpleName();
            schema = NO_SCHEMA;
        }
        final Set<StoredProcedureMetadata> storedProcedures = new HashSet<StoredProcedureMetadata>();
        if (entityType.isAnnotationPresent(StoredProcedure.class)) {
            storedProcedures.add(getStoredProcedureMetadata(entityType.getAnnotation(StoredProcedure.class)));
        } else if (entityType.isAnnotationPresent(StoredProcedures.class)) {
            final StoredProcedure[] procedures = entityType.getAnnotation(StoredProcedures.class).value();
            for (StoredProcedure procedure : procedures) {
                storedProcedures.add(getStoredProcedureMetadata(procedure));
            }
        }
        //noinspection unchecked
        if (!withMethods(entityType).keep(new GetterMethodFilter())
                .keep(new AnnotatedElementFilter(Column.class, JoinColumn.class))
                .keep(new AnnotatedElementFilter(Transient.class)).isEmpty()) {
            throw new TransientColumnFoundError(entityType);
        }
        final Collection<SequenceMetadata> sequences = new HashSet<SequenceMetadata>();
        final HashSet<ConstraintMetadata> constraints = new HashSet<ConstraintMetadata>();
        final AtomicReference<ColumnMetadata> versionColumn = new AtomicReference<ColumnMetadata>();
        //noinspection unchecked
        final List<Method> getters = withMethods(entityType).keep(new GetterMethodFilter()).list();
        final List<Method> filteredGetters = new ArrayList<Method>();
        for (Method getter : getters) {
            final PropertyAccessorFilter filter = new PropertyAccessorFilter(
                    ReflectionUtils.getPropertyName(getter.getName()));
            final Method method = with(filteredGetters).find(filter);
            if (method == null) {
                filteredGetters.add(getter);
            } else if (method.getDeclaringClass().equals(getter.getDeclaringClass())) {
                filteredGetters.remove(method);
                filteredGetters.add(pickGetter(method, getter));
            }
        }
        getters.clear();
        getters.addAll(filteredGetters);
        //noinspection unchecked
        final Collection<ColumnMetadata> tableColumns = with(getters)
                .drop(new AnnotatedElementFilter(Transient.class)).drop(new AnnotatedElementFilter(OneToMany.class))
                .drop(new AnnotatedElementFilter(ManyToMany.class)).drop(new Filter<Method>() {
                    @Override
                    public boolean accepts(Method item) {
                        return item.isAnnotationPresent(OneToOne.class)
                                && !item.isAnnotationPresent(JoinColumn.class);
                    }
                }).drop(new PropertyAccessorFilter(CLASS_PROPERTY))
                .transform(new Transformer<Method, ColumnMetadata>() {
                    @Override
                    public ColumnMetadata map(Method method) {
                        final JoinColumn joinColumn = method.getAnnotation(JoinColumn.class);
                        Column column = method.getAnnotation(Column.class);
                        if (column == null && joinColumn == null) {
                            //let's assume it is a column anyway
                            column = new DefaultColumn();
                        }
                        final String propertyName = ReflectionUtils.getPropertyName(method.getName());
                        if (column != null && joinColumn != null) {
                            throw new ColumnDefinitionError(
                                    "Property " + propertyName + " is defined as both a column and a join column");
                        }
                        final Class<?> propertyType = method.getReturnType();
                        String name = column != null ? column.name() : joinColumn.name();
                        if (name.isEmpty()) {
                            name = propertyName;
                        }
                        final boolean nullable = column != null ? column.nullable() : joinColumn.nullable();
                        final int length = column != null ? column.length() : 0;
                        final int precision = column != null ? column.precision() : 0;
                        final int scale = column != null ? column.scale() : 0;
                        final ValueGenerationType generationType = determineValueGenerationType(method);
                        final String valueGenerator = determineValueGenerator(method);
                        final ColumnMetadata foreignColumn = joinColumn == null ? null
                                : determineForeignReference(method);
                        final int type = getColumnType(method, foreignColumn);
                        final Class<?> declaringClass = ReflectionUtils.getDeclaringClass(method);
                        if (method.isAnnotationPresent(BasicCollection.class)
                                && !(Collection.class.isAssignableFrom(method.getReturnType()))) {
                            throw new ColumnDefinitionError(
                                    "Collection column must return a collection value: " + tableName + "." + name);
                        }
                        final ResolvedColumnMetadata columnMetadata = new ResolvedColumnMetadata(
                                new UnresolvedTableMetadata<E>(entityType), declaringClass, name, type,
                                propertyName, propertyType, nullable, length, precision, scale, generationType,
                                valueGenerator, foreignColumn, method.isAnnotationPresent(BasicCollection.class),
                                isComplex(method, foreignColumn));
                        if (foreignColumn != null) {
                            foreignKeys.add(name);
                        }
                        if (method.isAnnotationPresent(Id.class)) {
                            keyColumns.add(name);
                        }
                        if (method.isAnnotationPresent(SequenceGenerator.class)) {
                            final SequenceGenerator annotation = method.getAnnotation(SequenceGenerator.class);
                            sequences.add(new ImmutableSequenceMetadata(annotation.name(),
                                    annotation.initialValue(), annotation.allocationSize()));
                        }
                        if (joinColumn != null) {
                            final RelationType relationType = getRelationType(method);
                            final CascadeMetadata cascadeMetadata = getCascadeMetadata(method);
                            final boolean isLazy = determineLaziness(method);
                            final DefaultRelationMetadata<E, Object> reference = new DefaultRelationMetadata<E, Object>(
                                    declaringClass, columnMetadata.getPropertyName(), true, null, null, null,
                                    relationType, cascadeMetadata, isLazy, null);
                            reference.setForeignColumn(foreignColumn);
                            foreignReferences.add(reference);
                        }
                        if (method.isAnnotationPresent(Version.class)) {
                            if (versionColumn.get() != null) {
                                throw new MultipleVersionColumnsError(entityType);
                            }
                            if (column != null) {
                                if (columnMetadata.isNullable()) {
                                    throw new VersionColumnDefinitionError("Version column cannot be nullable: "
                                            + entityType.getCanonicalName() + "." + columnMetadata.getName());
                                }
                                versionColumn.set(columnMetadata);
                            } else {
                                throw new VersionColumnDefinitionError(
                                        "Only local columns can be used for optimistic locking");
                            }
                        }
                        return columnMetadata;
                    }
                }).list();
        //handling one-to-many relations
        //noinspection unchecked
        withMethods(entityType).keep(new GetterMethodFilter())
                .drop(new AnnotatedElementFilter(Column.class, JoinColumn.class))
                .keep(new AnnotatedElementFilter(OneToMany.class)).each(new Processor<Method>() {
                    @Override
                    public void process(Method method) {
                        if (!Collection.class.isAssignableFrom(method.getReturnType())) {
                            throw new RelationDefinitionError(
                                    "One to many relations must be collections. Error in " + method);
                        }
                        final OneToMany annotation = method.getAnnotation(OneToMany.class);
                        Class<?> foreignEntity = annotation.targetEntity().equals(void.class)
                                ? ((Class) ((ParameterizedType) method.getGenericReturnType())
                                        .getActualTypeArguments()[0])
                                : annotation.targetEntity();
                        String foreignColumnName = annotation.mappedBy();
                        final String propertyName = ReflectionUtils.getPropertyName(method.getName());
                        if (foreignColumnName.isEmpty()) {
                            //noinspection unchecked
                            final List<Method> list = withMethods(foreignEntity)
                                    .keep(new AnnotatedElementFilter(JoinColumn.class))
                                    .keep(new AnnotatedElementFilter(ManyToOne.class))
                                    .keep(new MethodReturnTypeFilter(entityType)).list();
                            if (list.isEmpty()) {
                                throw new RelationDefinitionError(
                                        "No ManyToOne relations for " + entityType.getCanonicalName()
                                                + " were found on " + foreignEntity.getCanonicalName());
                            }
                            if (list.size() > 1) {
                                throw new RelationDefinitionError("Ambiguous one to many relationship on "
                                        + entityType.getCanonicalName() + "." + propertyName);
                            }
                            final Method foreignMethod = list.get(0);
                            final Column column = foreignMethod.getAnnotation(Column.class);
                            final JoinColumn joinColumn = foreignMethod.getAnnotation(JoinColumn.class);
                            foreignColumnName = column == null ? joinColumn.name() : column.name();
                            if (foreignColumnName.isEmpty()) {
                                foreignColumnName = ReflectionUtils.getPropertyName(foreignMethod.getName());
                            }
                        }
                        final List<OrderMetadata> ordering = getOrdering(foreignEntity,
                                method.getAnnotation(OrderBy.class));
                        //noinspection unchecked
                        final UnresolvedColumnMetadata foreignColumn = new UnresolvedColumnMetadata(
                                foreignColumnName,
                                new UnresolvedTableMetadata<Object>((Class<Object>) foreignEntity));
                        final DefaultRelationMetadata<E, Object> reference = new DefaultRelationMetadata<E, Object>(
                                ReflectionUtils.getDeclaringClass(method), propertyName, false, null, null, null,
                                getRelationType(method), getCascadeMetadata(method), determineLaziness(method),
                                ordering);
                        reference.setForeignColumn(foreignColumn);
                        foreignReferences.add(reference);
                    }
                });
        //Handling one-to-one relations where the entity is not the owner of the relationship
        //noinspection unchecked
        withMethods(entityType).keep(new GetterMethodFilter()).keep(new AnnotatedElementFilter(OneToOne.class))
                .drop(new AnnotatedElementFilter(Column.class, JoinColumn.class)).each(new Processor<Method>() {
                    @Override
                    public void process(Method method) {
                        final OneToOne annotation = method.getAnnotation(OneToOne.class);
                        Class<?> foreignEntity = annotation.targetEntity().equals(void.class)
                                ? method.getReturnType()
                                : annotation.targetEntity();
                        final String propertyName = ReflectionUtils.getPropertyName(method.getName());
                        final DefaultRelationMetadata<E, Object> reference = new DefaultRelationMetadata<E, Object>(
                                ReflectionUtils.getDeclaringClass(method), propertyName, false, null, null, null,
                                getRelationType(method), getCascadeMetadata(method), determineLaziness(method),
                                null);
                        String foreignColumnName = annotation.mappedBy();
                        if (foreignColumnName.isEmpty()) {
                            //noinspection unchecked
                            final List<Method> methods = withMethods(foreignEntity).keep(new GetterMethodFilter())
                                    .keep(new MethodReturnTypeFilter(entityType))
                                    .keep(new AnnotatedElementFilter(OneToOne.class))
                                    .keep(new AnnotatedElementFilter(Column.class, JoinColumn.class)).list();
                            if (methods.isEmpty()) {
                                throw new EntityDefinitionError(
                                        "No OneToOne relations were found on " + foreignEntity.getCanonicalName()
                                                + " for " + entityType.getCanonicalName());
                            }
                            if (methods.size() > 1) {
                                throw new EntityDefinitionError("Ambiguous OneToOne relation on "
                                        + entityType.getCanonicalName() + "." + propertyName);
                            }
                            final Method foreignMethod = methods.get(0);
                            final Column column = foreignMethod.getAnnotation(Column.class);
                            final JoinColumn joinColumn = foreignMethod.getAnnotation(JoinColumn.class);
                            foreignColumnName = column == null ? joinColumn.name() : column.name();
                            if (foreignColumnName.isEmpty()) {
                                foreignColumnName = ReflectionUtils.getPropertyName(foreignMethod.getName());
                            }
                        }
                        //noinspection unchecked
                        reference.setForeignColumn(new UnresolvedColumnMetadata(foreignColumnName,
                                new UnresolvedTableMetadata<Object>((Class<Object>) foreignEntity)));
                        foreignReferences.add(reference);
                    }
                });
        final HashSet<NamedQueryMetadata> namedQueries = new HashSet<NamedQueryMetadata>();
        if (entityType.isAnnotationPresent(SequenceGenerator.class)) {
            final SequenceGenerator annotation = entityType.getAnnotation(SequenceGenerator.class);
            sequences.add(new ImmutableSequenceMetadata(annotation.name(), annotation.initialValue(),
                    annotation.allocationSize()));
        }
        //finding orderings
        //noinspection unchecked
        final List<OrderMetadata> ordering = withMethods(entityType).keep(new AnnotatedElementFilter(Column.class))
                .keep(new AnnotatedElementFilter(Order.class)).sort(new Comparator<Method>() {
                    @Override
                    public int compare(Method firstMethod, Method secondMethod) {
                        final Order first = firstMethod.getAnnotation(Order.class);
                        final Order second = secondMethod.getAnnotation(Order.class);
                        return ((Integer) first.priority()).compareTo(second.priority());
                    }
                }).transform(new Transformer<Method, OrderMetadata>() {
                    @Override
                    public OrderMetadata map(Method input) {
                        final Column column = input.getAnnotation(Column.class);
                        String columnName = column.name().isEmpty()
                                ? ReflectionUtils.getPropertyName(input.getName())
                                : column.name();
                        ColumnMetadata columnMetadata = with(tableColumns).find(new ColumnNameFilter(columnName));
                        if (columnMetadata == null) {
                            columnMetadata = with(tableColumns).find(new ColumnPropertyFilter(columnName));
                        }
                        if (columnMetadata == null) {
                            throw new NoSuchColumnError(entityType, columnName);
                        }
                        return new ImmutableOrderMetadata(columnMetadata, input.getAnnotation(Order.class).value());
                    }
                }).list();
        final ResolvedTableMetadata<E> tableMetadata = new ResolvedTableMetadata<E>(entityType, schema, tableName,
                constraints, tableColumns, namedQueries, sequences, storedProcedures, foreignReferences,
                versionColumn.get(), ordering);
        if (!keyColumns.isEmpty()) {
            constraints.add(new PrimaryKeyConstraintMetadata(tableMetadata,
                    with(keyColumns).transform(new Transformer<String, ColumnMetadata>() {
                        @Override
                        public ColumnMetadata map(String columnName) {
                            return getColumnMetadata(columnName, tableColumns, entityType);
                        }
                    }).list()));
        }
        if (entityType.isAnnotationPresent(NamedNativeQueries.class)) {
            final NamedNativeQuery[] queries = entityType.getAnnotation(NamedNativeQueries.class).value();
            for (NamedNativeQuery query : queries) {
                namedQueries.add(new ImmutableNamedQueryMetadata(query.name(), query.query(), tableMetadata,
                        QueryType.NATIVE));
            }
        } else if (entityType.isAnnotationPresent(NamedNativeQuery.class)) {
            final NamedNativeQuery query = entityType.getAnnotation(NamedNativeQuery.class);
            namedQueries.add(
                    new ImmutableNamedQueryMetadata(query.name(), query.query(), tableMetadata, QueryType.NATIVE));
        }
        constraints
                .addAll(with(uniqueColumns).sort().transform(new Transformer<Set<String>, Set<ColumnMetadata>>() {
                    @Override
                    public Set<ColumnMetadata> map(Set<String> columns) {
                        return with(columns).transform(new Transformer<String, ColumnMetadata>() {
                            @Override
                            public ColumnMetadata map(String columnName) {
                                return getColumnMetadata(columnName, tableColumns, entityType);
                            }
                        }).set();
                    }
                }).transform(new Transformer<Set<ColumnMetadata>, UniqueConstraintMetadata>() {
                    @Override
                    public UniqueConstraintMetadata map(Set<ColumnMetadata> columns) {
                        return new UniqueConstraintMetadata(tableMetadata, columns);
                    }
                }).list());
        constraints.addAll(with(foreignKeys).sort().transform(new Transformer<String, ColumnMetadata>() {
            @Override
            public ColumnMetadata map(String columnName) {
                return getColumnMetadata(columnName, tableColumns, entityType);
            }
        }).transform(new Transformer<ColumnMetadata, ForeignKeyConstraintMetadata>() {
            @Override
            public ForeignKeyConstraintMetadata map(ColumnMetadata columnMetadata) {
                return new ForeignKeyConstraintMetadata(tableMetadata, columnMetadata);
            }
        }).list());
        //going after many-to-many relations
        //noinspection unchecked
        withMethods(entityType).drop(new AnnotatedElementFilter(Column.class, JoinColumn.class))
                .keep(new GetterMethodFilter()).forThose(new Filter<Method>() {
                    @Override
                    public boolean accepts(Method item) {
                        return item.isAnnotationPresent(ManyToMany.class);
                    }
                }, new Processor<Method>() {
                    @Override
                    public void process(Method method) {
                        final ManyToMany annotation = method.getAnnotation(ManyToMany.class);
                        Class<?> foreignEntity = annotation.targetEntity().equals(void.class)
                                ? ((Class) ((ParameterizedType) method.getGenericReturnType())
                                        .getActualTypeArguments()[0])
                                : annotation.targetEntity();
                        String foreignProperty = annotation.mappedBy();
                        if (foreignProperty.isEmpty()) {
                            //noinspection unchecked
                            final List<Method> methods = withMethods(foreignEntity).keep(new GetterMethodFilter())
                                    .keep(new AnnotatedElementFilter(ManyToMany.class)).list();
                            if (methods.isEmpty()) {
                                throw new EntityDefinitionError(
                                        "Failed to locate corresponding many-to-many relation on "
                                                + foreignEntity.getCanonicalName());
                            }
                            if (methods.size() == 1) {
                                throw new EntityDefinitionError("Ambiguous many-to-many relationship defined");
                            }
                            foreignProperty = ReflectionUtils.getPropertyName(methods.get(0).getName());
                        }
                        final List<OrderMetadata> ordering = getOrdering(foreignEntity,
                                method.getAnnotation(OrderBy.class));
                        //noinspection unchecked
                        foreignReferences.add(new DefaultRelationMetadata<E, Object>(
                                ReflectionUtils.getDeclaringClass(method),
                                ReflectionUtils.getPropertyName(method.getName()), false, tableMetadata, null,
                                new UnresolvedColumnMetadata(foreignProperty,
                                        new UnresolvedTableMetadata<Object>((Class<Object>) foreignEntity)),
                                RelationType.MANY_TO_MANY, getCascadeMetadata(method), determineLaziness(method),
                                ordering));
                    }
                });
        return tableMetadata;
    }

    /**
     * Between two getter methods defined for a given property, both declared in the same class, picks one
     *
     * @param first  the first getter
     * @param second the second getter
     * @return the winning getter
     */
    private static Method pickGetter(Method first, Method second) {
        //if one is an interface, and one is not, the concrete implementation wins
        if (Modifier.isInterface(first.getModifiers()) && !Modifier.isInterface(second.getModifiers())) {
            return second;
        }
        if (!Modifier.isInterface(first.getModifiers()) && Modifier.isInterface(second.getModifiers())) {
            return first;
        }
        //if one is abstract and the other is not, the non-abstract one wins
        if (Modifier.isAbstract(first.getModifiers()) && !Modifier.isAbstract(second.getModifiers())) {
            return second;
        }
        if (!Modifier.isAbstract(first.getModifiers()) && Modifier.isAbstract(second.getModifiers())) {
            return first;
        }
        //given a hierarchy, the child wins
        if (first.getReturnType().isAssignableFrom(second.getReturnType())) {
            return second;
        }
        if (second.getReturnType().isAssignableFrom(first.getReturnType())) {
            return first;
        }
        if (!hasAnnotations(first) && hasAnnotations(second)) {
            return second;
        }
        if (hasAnnotations(first) && !hasAnnotations(second)) {
            return first;
        }
        return first;
    }

    /**
     * @param method the method to inspect
     * @return {@code true} if it is annotated with any JPA annotations
     */
    private static boolean hasAnnotations(Method method) {
        return with(method.getAnnotations()).exists(new Filter<Annotation>() {
            @Override
            public boolean accepts(Annotation item) {
                return item.getClass().getCanonicalName().startsWith("javax.presistence.");
            }
        });
    }

    private List<OrderMetadata> getOrdering(final Class<?> foreignEntity, OrderBy annotation) {
        if (annotation == null) {
            return null;
        }
        final String expression = annotation.value();
        return with(expression.trim().split("\\s*,\\s*")).transform(new Transformer<String, OrderMetadata>() {
            @Override
            public OrderMetadata map(String input) {
                final String[] split = input.split("\\s+");
                if (split.length > 2) {
                    throw new EntityOrderDefinitionError(input);
                }
                final String order = (split.length == 2 ? split[1] : "ASC").toUpperCase();
                if (!order.matches("ASC|DESC")) {
                    throw new EntityOrderDefinitionError(input);
                }
                //noinspection unchecked
                return new ImmutableOrderMetadata(
                        new UnresolvedColumnMetadata(split[0],
                                new UnresolvedTableMetadata<Object>((Class<Object>) foreignEntity)),
                        Ordering.getOrdering(order));
            }
        }).list();
    }

    private static boolean determineLaziness(Method method) {
        return method.isAnnotationPresent(OneToOne.class)
                && method.getAnnotation(OneToOne.class).fetch().equals(FetchType.LAZY)
                || method.isAnnotationPresent(OneToMany.class)
                        && method.getAnnotation(OneToMany.class).fetch().equals(FetchType.LAZY)
                || method.isAnnotationPresent(ManyToOne.class)
                        && method.getAnnotation(ManyToOne.class).fetch().equals(FetchType.LAZY)
                || method.isAnnotationPresent(ManyToMany.class)
                        && method.getAnnotation(ManyToMany.class).fetch().equals(FetchType.LAZY);
    }

    private static CascadeMetadata getCascadeMetadata(Method method) {
        final List<CascadeType> cascadeTypes = new ArrayList<CascadeType>();
        if (method.isAnnotationPresent(OneToOne.class)) {
            cascadeTypes.addAll(Arrays.asList(method.getAnnotation(OneToOne.class).cascade()));
        } else if (method.isAnnotationPresent(OneToMany.class)) {
            cascadeTypes.addAll(Arrays.asList(method.getAnnotation(OneToMany.class).cascade()));
        } else if (method.isAnnotationPresent(ManyToOne.class)) {
            cascadeTypes.addAll(Arrays.asList(method.getAnnotation(ManyToOne.class).cascade()));
        } else if (method.isAnnotationPresent(ManyToMany.class)) {
            cascadeTypes.addAll(Arrays.asList(method.getAnnotation(ManyToMany.class).cascade()));
        }
        final boolean cascadeAll = cascadeTypes.contains(CascadeType.ALL);
        return new ImmutableCascadeMetadata(cascadeAll || cascadeTypes.contains(CascadeType.PERSIST),
                cascadeAll || cascadeTypes.contains(CascadeType.MERGE),
                cascadeAll || cascadeTypes.contains(CascadeType.REMOVE),
                cascadeAll || cascadeTypes.contains(CascadeType.REFRESH));
    }

    private static RelationType getRelationType(Method method) {
        if (method.isAnnotationPresent(OneToMany.class)) {
            return RelationType.ONE_TO_MANY;
        } else if (method.isAnnotationPresent(ManyToOne.class)) {
            return RelationType.MANY_TO_ONE;
        } else if (method.isAnnotationPresent(ManyToMany.class)) {
            return RelationType.MANY_TO_MANY;
        }
        return RelationType.ONE_TO_ONE;
    }

    private StoredProcedureMetadata getStoredProcedureMetadata(StoredProcedure annotation) {
        final ArrayList<ParameterMetadata> parameters = new ArrayList<ParameterMetadata>();
        final StoredProcedureMetadata metadata = new DefaultStoredProcedureMetadata(annotation.name(),
                annotation.resultType(), parameters);
        for (StoredProcedureParameter parameter : annotation.parameters()) {
            parameters.add(new ImmutableParameterMetadata(parameter.mode(),
                    getColumnType(parameter.type(), null, null), parameter.type()));
        }
        return metadata;
    }

    private static int getColumnType(Method method, ColumnMetadata foreignReference) {
        return getColumnType(method.getReturnType(), method, foreignReference);
    }

    private static boolean isComplex(Method method, ColumnMetadata foreignReference) {
        return isComplex(method.getReturnType(), method, foreignReference);
    }

    private static boolean isComplex(Class<?> javaType, Method method, ColumnMetadata foreignReference) {
        final int dimensions = ReflectionUtils.getArrayDimensions(javaType);
        javaType = ReflectionUtils.mapType(ReflectionUtils.getComponentType(javaType));
        if (dimensions > 1) {
            throw new UnsupportedColumnTypeError("Arrays of dimension > 1 are not supported");
        }
        return !(Byte.class.equals(javaType) && dimensions == 0) && !Short.class.equals(javaType)
                && !Integer.class.equals(javaType) && !Long.class.equals(javaType) && !Float.class.equals(javaType)
                && !Double.class.equals(javaType) && !BigDecimal.class.equals(javaType)
                && !BigInteger.class.equals(javaType) && !Character.class.equals(javaType)
                && !(String.class.equals(javaType) || Class.class.equals(javaType))
                && !Date.class.isAssignableFrom(javaType) && !(Byte.class.equals(javaType) && dimensions > 0)
                && !Enum.class.isAssignableFrom(javaType) && !Boolean.class.equals(javaType)
                && (!Collection.class.isAssignableFrom(javaType)
                        || !method.isAnnotationPresent(BasicCollection.class))
                && foreignReference == null;
    }

    private static int getColumnType(Class<?> javaType, Method method, ColumnMetadata foreignReference) {
        final int dimensions = ReflectionUtils.getArrayDimensions(javaType);
        javaType = ReflectionUtils.mapType(ReflectionUtils.getComponentType(javaType));
        if (dimensions > 1) {
            throw new UnsupportedColumnTypeError("Arrays of dimension > 1 are not supported");
        }
        if (Byte.class.equals(javaType) && dimensions == 0) {
            return Types.TINYINT;
        } else if (Short.class.equals(javaType)) {
            return Types.SMALLINT;
        } else if (Integer.class.equals(javaType)) {
            return Types.INTEGER;
        } else if (Long.class.equals(javaType)) {
            return Types.BIGINT;
        } else if (Float.class.equals(javaType)) {
            return Types.FLOAT;
        } else if (Double.class.equals(javaType)) {
            return Types.DOUBLE;
        } else if (BigDecimal.class.equals(javaType)) {
            return Types.DECIMAL;
        } else if (BigInteger.class.equals(javaType)) {
            return Types.NUMERIC;
        } else if (Character.class.equals(javaType)) {
            return Types.CHAR;
        } else if (String.class.equals(javaType) || Class.class.equals(javaType)) {
            if (method != null && method.isAnnotationPresent(Column.class)
                    && method.getAnnotation(Column.class).length() > 0) {
                return Types.VARCHAR;
            } else {
                return Types.LONGVARCHAR;
            }
        } else if (Date.class.isAssignableFrom(javaType)) {
            if (javaType.equals(java.sql.Date.class)) {
                return Types.DATE;
            } else if (javaType.equals(Time.class)) {
                return Types.TIME;
            } else if (javaType.equals(java.sql.Timestamp.class)) {
                return Types.TIMESTAMP;
            }
            final TemporalType temporalType = method != null && method.isAnnotationPresent(Temporal.class)
                    ? method.getAnnotation(Temporal.class).value()
                    : null;
            return (temporalType == null || temporalType.equals(TemporalType.TIMESTAMP)) ? Types.TIMESTAMP
                    : (temporalType.equals(TemporalType.DATE) ? Types.DATE : Types.TIME);
        } else if (Byte.class.equals(javaType) && dimensions > 0) {
            return Types.VARBINARY;
        } else if (Enum.class.isAssignableFrom(javaType)) {
            return Types.VARCHAR;
        } else if (Boolean.class.equals(javaType)) {
            return Types.BOOLEAN;
        } else if (Collection.class.isAssignableFrom(javaType)
                && method.isAnnotationPresent(BasicCollection.class)) {
            return Types.LONGVARCHAR;
        }
        if (foreignReference != null) {
            return Integer.MIN_VALUE;
        }
        return Types.LONGVARCHAR;
    }

    private static <E> ColumnMetadata getColumnMetadata(String columnName, Collection<ColumnMetadata> tableColumns,
            Class<E> entityType) {
        final ColumnMetadata metadata = with(tableColumns).keep(new ColumnNameFilter(columnName)).first();
        if (metadata == null) {
            throw new NoSuchColumnError(entityType, columnName);
        }
        return metadata;
    }

    private static ColumnMetadata determineForeignReference(Method method) {
        final String name;
        final Class<?> entityType;
        if (method.isAnnotationPresent(OneToOne.class)) {
            final OneToOne annotation = method.getAnnotation(OneToOne.class);
            name = "";
            entityType = annotation.targetEntity().equals(void.class) ? method.getReturnType()
                    : annotation.targetEntity();
        } else if (method.isAnnotationPresent(ManyToOne.class)) {
            final ManyToOne annotation = method.getAnnotation(ManyToOne.class);
            name = "";
            entityType = annotation.targetEntity().equals(void.class) ? method.getReturnType()
                    : annotation.targetEntity();
        } else {
            throw new UnsupportedOperationException();
        }
        //noinspection unchecked
        return new UnresolvedColumnMetadata(name, new UnresolvedTableMetadata<Object>((Class<Object>) entityType));
    }

    private static String determineValueGenerator(Method method) {
        return method.isAnnotationPresent(GeneratedValue.class)
                ? method.getAnnotation(GeneratedValue.class).generator()
                : null;
    }

    private ValueGenerationType determineValueGenerationType(Method method) {
        if (method.isAnnotationPresent(GeneratedValue.class)) {
            final GeneratedValue generatedValue = method.getAnnotation(GeneratedValue.class);
            return generatedValue.strategy().equals(GenerationType.AUTO) ? dialect.getDefaultGenerationType()
                    : (generatedValue.strategy().equals(GenerationType.IDENTITY) ? ValueGenerationType.IDENTITY
                            : (generatedValue.strategy().equals(GenerationType.SEQUENCE)
                                    ? ValueGenerationType.SEQUENCE
                                    : (generatedValue.strategy().equals(GenerationType.TABLE)
                                            ? ValueGenerationType.TABLE
                                            : null)));
        } else {
            return null;
        }
    }

    @Override
    public boolean accepts(Class<?> entityType) {
        return entityType.isAnnotationPresent(Entity.class);
    }

    @SuppressWarnings("ClassExplicitlyAnnotation")
    private static class DefaultColumn implements Column {

        @Override
        public String name() {
            return "";
        }

        @Override
        public boolean unique() {
            return false;
        }

        @Override
        public boolean nullable() {
            return true;
        }

        @Override
        public boolean insertable() {
            return true;
        }

        @Override
        public boolean updatable() {
            return true;
        }

        @Override
        public String columnDefinition() {
            return "";
        }

        @Override
        public String table() {
            return "";
        }

        @Override
        public int length() {
            return 255;
        }

        @Override
        public int precision() {
            return 0;
        }

        @Override
        public int scale() {
            return 0;
        }

        @Override
        public Class<? extends Annotation> annotationType() {
            return Column.class;
        }

    }

}