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);
}
}