package com.txtr.hibernatedelta.validator; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.net.URL; import java.sql.Types; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.persistence.AttributeOverrides; import javax.persistence.CascadeType; import javax.persistence.DiscriminatorColumn; import javax.persistence.Embedded; import javax.persistence.Entity; import javax.persistence.Inheritance; import javax.persistence.InheritanceType; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToMany; import javax.persistence.ManyToOne; import javax.persistence.MappedSuperclass; import javax.persistence.OneToMany; import javax.persistence.OneToOne; import javax.persistence.PrimaryKeyJoinColumn; import javax.persistence.Table; import javax.persistence.Transient; import javax.persistence.UniqueConstraint; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.hibernate.annotations.Index; import com.google.common.collect.ImmutableList; import com.txtr.hibernatedelta.DatabaseWithIndexes; import com.txtr.hibernatedelta.generator.BackendSqlGenerator; import com.txtr.hibernatedelta.model.ExplicitHibernateIndex; import com.txtr.hibernatedelta.model.FunctionIndex; import com.txtr.hibernatedelta.model.FunctionIndexes; import com.txtr.hibernatedelta.model.HibernateColumn; import com.txtr.hibernatedelta.model.HibernateDatabase; import com.txtr.hibernatedelta.model.HibernateModelUtil; import com.txtr.hibernatedelta.model.HibernatePrimaryKeyType; import com.txtr.hibernatedelta.model.HibernateTable; import com.txtr.hibernatedelta.model.IHibernateDatabaseObject; import com.txtr.hibernatedelta.model.OwnedEntity; import com.txtr.hibernatedelta.model.VirtualRootTable; public class EntityValidator { private HibernateDatabase database = new HibernateDatabase(); private Map<Class<?>, HibernateTable> tableMap = new HashMap<Class<?>, HibernateTable>(); private Set<String> indexes = new HashSet<String>(); private Map<Class<?>, Class<?>> ownerEntities = new HashMap<Class<?>, Class<?>>(); public EntityValidator() { } /** * For example * * Iterable<Class<? extends AbstractEntity>> entities = Iterables.filter(new Reflections("com.bookpac").getSubTypesOf(AbstractEntity.class), new Predicate<Class<? extends AbstractEntity>>() { @Override public boolean apply(Class<? extends AbstractEntity> input) { // mapped super class or wrapper class return input.getAnnotation(Entity.class) != null; } }); * * @param entityClasses * @return */ public DatabaseWithIndexes verify(ImmutableList<Class<?>> entityClasses, URL indexNames) { for (Class<?> entity : entityClasses) { Table table = entity.getAnnotation(Table.class); final VirtualRootTable virtualRootTable = entity.getAnnotation(VirtualRootTable.class); if (table != null || virtualRootTable != null) { tableMap.put(entity, addTableElement(entity.getName(), HibernateModelUtil.getSequenceName(entity), HibernateModelUtil.getTableName(entity), virtualRootTable != null)); } if (isOwned(entity)) { ownerEntities.put(entity, null); } } for (Class<?> entity : entityClasses) { HibernateValidator.validateTable(entity); validateIndexes(entity); addColumns(entity, entity, null); addInheritanceColumns(entity); } for (HibernateTable table : tableMap.values()) { sortAndCheckDuplicates(table.getColumns(), table.getName()); } sortAndCheckDuplicates(database.getTables(), "tables"); return new DatabaseWithIndexes(database, BackendSqlGenerator.getIndexNames(indexNames)); } private boolean isOwned(Class<?> entity) { Class<?> superclass = entity.getSuperclass(); if (superclass == null) { return false; } boolean owned = entity.getAnnotation(OwnedEntity.class) != null; if (owned) { assertOwned(superclass); } else { return isOwned(superclass); } return owned; } private void assertOwned(Class<?> entity) { if (entity.getAnnotation(Entity.class) != null && entity.getAnnotation(OwnedEntity.class) == null) { throw new IllegalStateException("a subclass of " + entity + " is owned"); } Class<?> superclass = entity.getSuperclass(); if (superclass != null) { assertOwned(superclass); } } private void addInheritanceColumns(Class<?> entity) { if (HibernateModelUtil.isTableRoot(entity)) { Inheritance inheritance = entity.getAnnotation(Inheritance.class); if (inheritance != null && inheritance.strategy().equals(InheritanceType.SINGLE_TABLE)) { String name = entity.getAnnotation(DiscriminatorColumn.class).name(); HibernateTable table = getTable(entity); table.addColumn(createColumn(entity, name, null, false, null, new HibernateDataType(Types.VARCHAR, HibernateValidator.DISCRIMINATOR_LENGTH, null))); table.addExplicitIndex(createDiscriminatorIndex(table, name)); } } else { if (HibernateModelUtil.getInheritance(entity).equals(InheritanceType.JOINED)) { HibernateColumn column = createPrimaryKeyColumn(entity); PrimaryKeyJoinColumn primaryKeyJoinColumn = entity.getAnnotation(PrimaryKeyJoinColumn.class); if (primaryKeyJoinColumn != null) { column.setName(primaryKeyJoinColumn.name()); } column.setPrimaryKeyType(HibernatePrimaryKeyType.ASSIGNED); getTable(entity).addColumn(column); } } } private ExplicitHibernateIndex createDiscriminatorIndex(HibernateTable table, String discriminatorColumn) { List<String> columns = new ArrayList<String>(); columns.add(discriminatorColumn); for (HibernateColumn column : table.getPrimaryKeyColumns()) { columns.add(column.getName()); } HibernateModelUtil.checkColumnNames("discriminator index on " + table.getName(), columns.toArray(new String[0]), table.toString()); return new ExplicitHibernateIndex(null, true, false, columns); } private void validateIndexes(Class<?> entity) { Table jpaTable = entity.getAnnotation(Table.class); HibernateTable hibernateTable = getTable(HibernateModelUtil.getTableClass(entity)); org.hibernate.annotations.Table table = entity.getAnnotation(org.hibernate.annotations.Table.class); if (table != null) { for (Index index : table.indexes()) { checkIndexName(entity, index.name()); hibernateTable.addExplicitIndex(new ExplicitHibernateIndex(index.name(), false, false, Arrays.asList(index.columnNames()))); } } FunctionIndexes functionIndexes = entity.getAnnotation(FunctionIndexes.class); if (functionIndexes != null) { for (FunctionIndex index : functionIndexes.value()) { checkIndexName(entity, index.name()); HibernateModelUtil.checkFunctionalIndexColumnNames("functional index " + index.name(), index.columnNames()); hibernateTable.addExplicitIndex(new ExplicitHibernateIndex(index.name(), index.unique(), true, Arrays.asList(index.columnNames()))); } } if (jpaTable != null) { for (UniqueConstraint uniqueConstraint : jpaTable.uniqueConstraints()) { HibernateModelUtil.checkColumnNames("unique constraint on table " + jpaTable.name(), uniqueConstraint.columnNames(), jpaTable.toString()); hibernateTable.addExplicitIndex(new ExplicitHibernateIndex(null, true, false, Arrays.asList(uniqueConstraint.columnNames()))); } } } private void checkIndexName(final Class<?> entity, final String name) { if (!indexes.add(name)) { throw new IllegalStateException("index added twice: " + name); } HibernateModelUtil.checkColumnOrTableName(entity.getName(), name, "index " + name + " in " + entity.getName()); } private void addColumns(Class<?> entity, Class<?> target, AttributeOverrides attributeOverrides) { Class<?> superclass = entity.getSuperclass(); if (superclass != null && inlineClass(superclass)) { addColumns(superclass, target, null); } for (Field field : entity.getDeclaredFields()) { if (field.getAnnotation(Transient.class) != null || Modifier.isStatic(field.getModifiers())) { continue; } if (field.getAnnotation(Embedded.class) != null) { addColumns(field.getType(), target, field.getAnnotation(AttributeOverrides.class)); continue; } HibernateValidator.validateColumn(entity, field); checkOwner(field, entity); HibernateField hibernateField = getHibernateField(field, target, attributeOverrides); if (hibernateField != null) { Class<?> targetTable = hibernateField.isForeignKey() ? field.getType() : null; addColumn(target, hibernateField, !HibernateModelUtil.isTableClass(target), targetTable); if (hibernateField.isUnique()) { getTable(HibernateModelUtil.getTableClass(target)).addExplicitIndex(new ExplicitHibernateIndex(null, true, false, ImmutableList.<String>of(hibernateField.getPhysicalName()))); } } addJoinTable(target, field); } } private boolean inlineClass(Class<?> entity) { if (entity.getAnnotation(MappedSuperclass.class) != null) { return true; } Inheritance inheritance = entity.getAnnotation(Inheritance.class); return inheritance != null && InheritanceType.TABLE_PER_CLASS.equals(inheritance.strategy()); } private HibernateColumn createPrimaryKeyColumn(Class<?> entity) { return createPrimaryKeyImpl(entity, HibernateModelUtil.getTableRootClass(entity)); } private HibernateColumn createPrimaryKeyImpl(Class<?> entity, Class<?> tableClass) { for (Field field : entity.getDeclaredFields()) { if (HibernateFieldFactory.getPrimaryKeyType(field) != null) { return createColumn(entity, getHibernateField(field, tableClass, null), tableClass); } } Class<?> superclass = entity.getSuperclass(); if (superclass == null) { throw new IllegalStateException("primary key not found: " + entity); } return createPrimaryKeyImpl(superclass, tableClass); } private HibernateField getHibernateField(Field field, Class<?> target, AttributeOverrides attributeOverrides) { HibernateColumn foreignKeyTargetTablePrimaryKey = HibernateFieldFactory.isForeignKey(field) ? createPrimaryKeyColumn(field.getType()) : null; return HibernateFieldFactory.createHibernateField(field, foreignKeyTargetTablePrimaryKey, target, attributeOverrides); } private void addJoinTable(Class<?> entity, Field field) { JoinTable joinTable = field.getAnnotation(JoinTable.class); if (joinTable != null) { HibernateTable table = addTableElement(entity.getName() + "." + field.getName(), HibernateModelUtil.getSequenceName(entity), joinTable.name(), false); JoinColumn sourceColumn = joinTable.joinColumns()[0]; JoinColumn targetColumn = joinTable.inverseJoinColumns()[0]; table.addColumn(createJoinTableColumn(entity, sourceColumn.name(), field.getDeclaringClass(), field)); ParameterizedType collectionType = (ParameterizedType) field.getGenericType(); Class<?> type = (Class<?>) collectionType.getActualTypeArguments()[0]; table.addColumn(createJoinTableColumn(entity, targetColumn.name(), type, field)); addIndexForJoinColumn(table, field, sourceColumn, false); addIndexForJoinColumn(table, field, targetColumn, field.getAnnotation(OneToMany.class) != null); } } private void checkOwner(Field field, Class<?> entity) { CascadeType[] cascadeType = getCascadeType(field); if (cascadeType != null) { Class<?> targetType = HibernateFieldFactory.getTargetType(field); boolean isOwned = ownerEntities.containsKey(targetType); boolean cascadeRemove = ArrayUtils.contains(cascadeType, CascadeType.REMOVE); if (isOwned) { if (!cascadeRemove) { throw new IllegalStateException("owned entity is not removed via cascade: " + HibernateValidator.getFullName(field)); } Class<?> oldOwner = ownerEntities.get(targetType); if (oldOwner != null) { throw new IllegalStateException(targetType + " is already owned by " + oldOwner); } ownerEntities.put(targetType, entity); } else { if (cascadeRemove) { throw new IllegalStateException("non owned entity is removed via cascade: " + HibernateValidator.getFullName(field)); } } } } private CascadeType[] getCascadeType(Field field) { ManyToMany manyToMany = field.getAnnotation(ManyToMany.class); if (manyToMany != null) { return manyToMany.cascade(); } ManyToOne manyToOne = field.getAnnotation(ManyToOne.class); if (manyToOne != null) { return manyToOne.cascade(); } OneToMany oneToMany = field.getAnnotation(OneToMany.class); if (oneToMany != null) { return oneToMany.cascade(); } OneToOne onetoOne = field.getAnnotation(OneToOne.class); if (onetoOne != null) { return onetoOne.cascade(); } return null; } private HibernateColumn createJoinTableColumn(Class<?> entity, String physicalName, Class<?> foreignKeyTargetTable, Field field) { if (!isValidJoinColumn(physicalName, foreignKeyTargetTable)) { throw new IllegalStateException("illegal join column name " + physicalName + " in :" + HibernateValidator.getFullName(field)); } return createColumn(entity, physicalName, foreignKeyTargetTable, false, HibernatePrimaryKeyType.ASSIGNED, HibernateDataType.LONG_TYPE); } private boolean isValidJoinColumn(String physicalName, Class<?> foreignKeyTargetTable) { final String expectedName = foreignKeyTargetTable.getSimpleName() + "_ID"; if (expectedName.length() <= 28){ return physicalName.equalsIgnoreCase(expectedName); } else { return StringUtils.getCommonPrefix(physicalName.toLowerCase(), expectedName.toLowerCase()).length() >= 15; } } private void addIndexForJoinColumn(final HibernateTable table, Field field, final JoinColumn column, final boolean implicitUnique) { if (column.unique()) { throw new IllegalStateException("unique annotation on @JoinTable column in : " + HibernateValidator.getFullName(field)); } if (!column.nullable()) { throw new IllegalStateException("nullable annotation on @JoinTable column in : " + HibernateValidator.getFullName(field)); } if (implicitUnique) { table.addExplicitIndex(new ExplicitHibernateIndex(null, true, false, ImmutableList.<String>of(column.name()))); } } private void sortAndCheckDuplicates(List<? extends IHibernateDatabaseObject> list, String parent) { Collections.sort(list, new Comparator<IHibernateDatabaseObject>() { @Override public int compare(IHibernateDatabaseObject o1, IHibernateDatabaseObject o2) { return o1.getName().compareToIgnoreCase(o2.getName()); } }); Set<String> names = new HashSet<String>(); for (IHibernateDatabaseObject object : list) { if (!names.add(object.getName())) { throw new IllegalStateException("duplicate physical name: " + object.getName() + " in " + parent); } } } private HibernateTable addTableElement(String entityName, String sequenceName, String name, boolean virtualRootTable) { HibernateModelUtil.checkColumnOrTableName("HibernateTable#" + entityName, name, "table " + name); HibernateModelUtil.checkColumnOrTableName("HibernateSequence#" + entityName, HibernateModelUtil.buildSequenceName(name), "sequence for table " + name); HibernateTable table = new HibernateTable(name, sequenceName, virtualRootTable); database.getTables().add(table); return table; } private void addColumn(Class<?> entity, HibernateField hibernateField, boolean allowDuplicateColumn, Class<?> foreignKeyTargetClass) { HibernateTable table = getTable(HibernateModelUtil.getTableClass(entity)); if (allowDuplicateColumn) { for (HibernateColumn hibernateColumn : table.getColumns()) { if (hibernateColumn.getName().equals(hibernateField.getPhysicalName())) { return; } } } table.addColumn(createColumn(entity, hibernateField, foreignKeyTargetClass)); } private HibernateColumn createColumn(Class<?> entity, HibernateField hibernateField, Class<?> foreignKeyTargetClass) { HibernateDataType dataType = hibernateField.getDataType(); return createColumn(entity, hibernateField.getPhysicalName(), foreignKeyTargetClass, hibernateField.isNullable(), hibernateField.getPrimaryKeyType(), dataType); } private HibernateColumn createColumn(Class<?> entity, String physicalName, Class<?> foreignKeyTargetClass, boolean nullable, HibernatePrimaryKeyType primaryKeyType, HibernateDataType dataType) { HibernateModelUtil.checkColumnOrTableName(entity.getName(), physicalName, "column " + entity + "." + physicalName); String target = foreignKeyTargetClass == null ? null : getTable(HibernateModelUtil.getTableClass(foreignKeyTargetClass)).getName(); return new HibernateColumn(physicalName, target, nullable, primaryKeyType, dataType.getSqlType().getName(), dataType.getLength(), dataType.getDecimalDigits()); } private HibernateTable getTable(Class<?> targetTable) { return tableMap.get(targetTable); } }