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