package com.txtr.hibernatedelta.validator; import java.lang.reflect.Field; import java.lang.reflect.ParameterizedType; import java.util.Arrays; import java.util.Collection; import java.util.Map; import java.util.Set; import javax.persistence.AttributeOverride; import javax.persistence.AttributeOverrides; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToMany; import javax.persistence.ManyToOne; import javax.persistence.OneToMany; import javax.persistence.OneToOne; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.hibernate.annotations.Cache; import com.google.common.base.Objects; import com.google.common.collect.ImmutableSet; import com.txtr.hibernatedelta.model.HibernateColumn; import com.txtr.hibernatedelta.model.HibernateModelUtil; import com.txtr.hibernatedelta.model.HibernatePrimaryKeyType; public class HibernateFieldFactory { private static final CascadeType[] ALLOWED_CASCADE_TYPES = new CascadeType[]{CascadeType.REMOVE}; private static final Set<String> CASCADE_OVERRIDES = ImmutableSet.of("com.bookpac.server.archive.storage.metastorages.notification.CatalogDocumentWatch.source"); private HibernateFieldFactory() { } public static HibernateField createHibernateField(Field field, HibernateColumn foreignKeyTargetTablePrimaryKey, Class<?> target, AttributeOverrides attributeOverrides) { if (isAssociation(field)) { if (getDeclaredUnique(field) != null || getDeclaredNullable(field) != null) { throw new IllegalStateException("nullable and unique may not be defined on associations: " + HibernateValidator.getFullName(field)); } assertCacheConsistency(field); } if (isMissingMappedBy(field)) { throw new IllegalStateException("OneToMany without mappedBy: " + HibernateValidator.getFullName(field)); } if (isNotLazy(field)) { throw new IllegalStateException("field is not fetched lazy: " + HibernateValidator.getFullName(field)); } if (isVirtualColumn(field)) { if (field.getAnnotation(JoinColumn.class) != null && StringUtils.isNotBlank(field.getAnnotation(JoinColumn.class).name())) { throw new IllegalStateException("mapped field attempting to set join column name: " + HibernateValidator.getFullName(field)); } return null; } if (hasIllegalCascade(field)) { throw new IllegalStateException("illegal cascade: " + field); } return new HibernateField( getPhysicalName(field, foreignKeyTargetTablePrimaryKey, attributeOverrides), isNullable(field, target), isUnique(field), isForeignKey(field), getPrimaryKeyType(field), HibernateDataTypeFactory.create(field, foreignKeyTargetTablePrimaryKey)); } private static void assertCacheConsistency(Field field) { boolean associationIsCached = field.getAnnotation(Cache.class) != null; boolean isCollection = Collection.class.isAssignableFrom(field.getType()) || Map.class.isAssignableFrom(field.getType()); if (isCollection) { boolean entityIsCached = field.getDeclaringClass().getAnnotation(Cache.class) != null; if (associationIsCached && !entityIsCached) { throw new IllegalStateException("collection is cached but entity is not: " + HibernateValidator.getFullName(field)); } boolean targetIsCached = getTargetType(field).getAnnotation(Cache.class) != null; if (associationIsCached != targetIsCached) { //otherwise, we'll hit a hibernate bug throw new IllegalStateException("association must be cached exactly when the target is cached: " + HibernateValidator.getFullName(field)); } } else { if (associationIsCached) { throw new IllegalStateException("only collections may be cached: " + HibernateValidator.getFullName(field)); } } } static Class<?> getTargetType(Field field) { Class<?> type = field.getType(); if (Map.class.isAssignableFrom(type)) { ParameterizedType genericType = (ParameterizedType) field.getGenericType(); return (Class) genericType.getActualTypeArguments()[1]; } if (Collection.class.isAssignableFrom(type)) { ParameterizedType genericType = (ParameterizedType) field.getGenericType(); return (Class) genericType.getActualTypeArguments()[0]; } return field.getType(); } //only OneToMany must have {CascadeType.PERSIST, CascadeType.REMOVE} private static boolean hasIllegalCascade(Field field) { if (CASCADE_OVERRIDES.contains(field.getDeclaringClass().getName() + "." + field.getName())){ return false; } if (field.getAnnotation(OneToOne.class) != null) { final OneToOne oneToOne = field.getAnnotation(OneToOne.class); return ArrayUtils.isNotEmpty(oneToOne.cascade()) && !Arrays.equals(ALLOWED_CASCADE_TYPES, oneToOne.cascade()); } else if (field.getAnnotation(ManyToOne.class) != null) { final ManyToOne manyToOne = field.getAnnotation(ManyToOne.class); return ArrayUtils.isNotEmpty(manyToOne.cascade()); } else if (field.getAnnotation(ManyToMany.class) != null) { final ManyToMany manyToMany = field.getAnnotation(ManyToMany.class); return ArrayUtils.isNotEmpty(manyToMany.cascade()); } else if (field.getAnnotation(OneToMany.class) != null) { final OneToMany oneToMany = field.getAnnotation(OneToMany.class); return !Arrays.equals(ALLOWED_CASCADE_TYPES, oneToMany.cascade()); } else { return false; } } private static boolean isAssociation(Field field) { return field.getAnnotation(OneToOne.class) != null || field.getAnnotation(ManyToMany.class) != null || field.getAnnotation(OneToMany.class) != null || field.getAnnotation(ManyToOne.class) != null; } private static boolean isVirtualColumn(Field field) { OneToOne oneToOne = field.getAnnotation(OneToOne.class); return field.getAnnotation(ManyToMany.class) != null || field.getAnnotation(OneToMany.class) != null || (oneToOne != null && StringUtils.isNotBlank(oneToOne.mappedBy())); } private static String getPhysicalName(Field field, HibernateColumn foreignKeyTargetTablePrimaryKey, AttributeOverrides attributeOverrides) { String declaredColumnName = getDeclaredName(field, attributeOverrides); if (StringUtils.isNotBlank(declaredColumnName)) { return declaredColumnName; } else if (isForeignKey(field)) { Class<?> rootClass = HibernateModelUtil.getTableRootClass(field.getType()); if (rootClass == null) { throw new IllegalStateException("non AbstractEntity class used in associations " + HibernateValidator.getFullName(field)); } return field.getName() + "_" + foreignKeyTargetTablePrimaryKey.getName(); } else { return field.getName(); } } private static String getDeclaredName(Field field, AttributeOverrides attributeOverrides) { if (attributeOverrides != null) { for (AttributeOverride attributeOverride : attributeOverrides.value()) { if (field.getName().equalsIgnoreCase(attributeOverride.name())) { return attributeOverride.column().name(); } } } Column column = field.getAnnotation(Column.class); if (column != null) { return column.name(); } else if (field.getAnnotation(JoinColumn.class) != null) { return field.getAnnotation(JoinColumn.class).name(); } else { return null; } } static boolean isForeignKey(Field field) { return field.getAnnotation(ManyToOne.class) != null || field.getAnnotation(OneToOne.class) != null; } private static boolean isNullable(Field field, Class<?> target) { boolean nullable = shouldBeNullable(field, target); if (!nullable && !canBeNullable(target)) { throw new IllegalStateException("must be nullable because it's in a single table inheritance: " + HibernateValidator.getFullName(field)); } return nullable; } private static boolean shouldBeNullable(Field field, Class<?> target) { Boolean declaredNullable = getDeclaredNullable(field); if (isPrimitiveNonNull(field, target) || getPrimaryKeyType(field) != null) { assertDeclaredNullable(field, declaredNullable); return false; } OneToOne oneToOne = field.getAnnotation(OneToOne.class); if (oneToOne != null) { assertDeclaredNullable(field, declaredNullable); return oneToOne.optional(); } ManyToOne manyToOne = field.getAnnotation(ManyToOne.class); if (manyToOne != null) { assertDeclaredNullable(field, declaredNullable); return manyToOne.optional(); } return Objects.firstNonNull(declaredNullable, true); } private static boolean isPrimitiveNonNull(Field field, Class<?> target) { return field.getType().isPrimitive() && canBeNullable(target); } private static boolean canBeNullable(Class<?> target) { return HibernateModelUtil.isTableClass(target); } private static void assertDeclaredNullable(Field field, Boolean declaredNullable) { if (declaredNullable != null) { throw new IllegalStateException("not null not allowed: " + HibernateValidator.getFullName(field)); } } private static Boolean getDeclaredNullable(Field field) { Column column = field.getAnnotation(Column.class); if (column != null) { return column.nullable() ? null : false; } JoinColumn joinColumn = field.getAnnotation(JoinColumn.class); if (joinColumn != null) { return joinColumn.nullable() ? null : false; } return null; } private static boolean isUnique(Field field) { Boolean declaredUnique = getDeclaredUnique(field); if (field.getAnnotation(OneToOne.class) != null) { if (declaredUnique != null) { throw new IllegalStateException("unique not allowed: " + HibernateValidator.getFullName(field)); } return true; } return Objects.firstNonNull(declaredUnique, false); } private static Boolean getDeclaredUnique(Field field) { Column column = field.getAnnotation(Column.class); if (column != null) { return column.unique() ? true : null; } JoinColumn joinColumn = field.getAnnotation(JoinColumn.class); if (joinColumn != null) { return joinColumn.unique() ? true : null; } return null; } static HibernatePrimaryKeyType getPrimaryKeyType(Field field) { if (field.getAnnotation(Id.class) != null) { return field.getAnnotation(GeneratedValue.class) != null ? HibernatePrimaryKeyType.GENERATED : HibernatePrimaryKeyType.ASSIGNED; } return null; } private static boolean isMissingMappedBy(Field field) { OneToMany oneToMany = field.getAnnotation(OneToMany.class); if (oneToMany != null) { return field.getAnnotation(JoinTable.class) == null && StringUtils.isBlank(oneToMany.mappedBy()); } ManyToMany manyToMany = field.getAnnotation(ManyToMany.class); if (manyToMany != null) { return field.getAnnotation(JoinTable.class) == null && StringUtils.isBlank(manyToMany.mappedBy()); } return false; } private static boolean isNotLazy(Field field) { final ManyToOne manyToOne = field.getAnnotation(ManyToOne.class); if (manyToOne != null) { return !FetchType.LAZY.equals(manyToOne.fetch()); } final OneToMany oneToMany = field.getAnnotation(OneToMany.class); if (oneToMany != null) { return !FetchType.LAZY.equals(oneToMany.fetch()); } final ManyToMany manyToMany = field.getAnnotation(ManyToMany.class); if (manyToMany != null) { return !FetchType.LAZY.equals(manyToMany.fetch()); } final OneToOne oneToOne = field.getAnnotation(OneToOne.class); if (oneToOne != null) { return !FetchType.LAZY.equals(oneToOne.fetch()); } return false; } }