/*
* Copyright 2012 Adaptrex, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.adaptrex.core.persistence.jpa;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import javax.persistence.Cache;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import org.apache.log4j.Logger;
import com.adaptrex.core.ext.ExtConfig;
import com.adaptrex.core.persistence.api.ORMModelInstance;
import com.adaptrex.core.persistence.api.ORMPersistenceManager;
import com.adaptrex.core.AdaptrexRegistry;
import com.adaptrex.core.utilities.Inflector;
import com.adaptrex.core.utilities.StringUtilities;
public class JPAModelInstance implements ORMModelInstance {
private static Logger log = Logger.getLogger(JPAModelInstance.class);
private ORMPersistenceManager orm;
private Map<String, Object> graph;
private ExtConfig extConfig;
private Object id;
private Object entity;
private static SimpleDateFormat dateFormat = new SimpleDateFormat(
"yyyy-MM-dd");
private static SimpleDateFormat timeFormat = new SimpleDateFormat(
"HH:mm:ss");
public JPAModelInstance(ExtConfig extConfig) {
this.extConfig = extConfig;
}
public JPAModelInstance(ExtConfig extConfig, Object entity) {
this(extConfig, entity, null);
}
public JPAModelInstance(ExtConfig extConfig, Object entity, EntityManager db) {
this.extConfig = extConfig;
this.entity = entity;
orm = AdaptrexRegistry.getPersistenceManager(extConfig.getFactoryName());
try {
graph = this.getObjectGraph(entity, extConfig.getModelName(), null,
null);
this.id = graph.get("id");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public Map<String, Object> getData() {
return graph;
}
@Override
public Object getId() {
return id;
}
private Map<String, Object> getObjectGraph(Object entity,
String entityFieldName, Object parentEntity, String parentName) {
Map<String, Object> entityData = new HashMap<String, Object>();
if (entity == null) {
return entityData;
}
Class<?> entityClazz = entity.getClass();
boolean isRoot = parentName == null;
String entityName = isRoot ? entityFieldName : parentName
+ StringUtilities.capitalize(entityFieldName);
List<String> entityIncludes = new ArrayList<String>();
List<String> entityExcludes = new ArrayList<String>();
List<String> entityJoins = new ArrayList<String>();
if (isRoot) {
/*
* Loop each include/exclude/join config and determine if they are
* set for the root entity.
*/
for (String incl : extConfig.getIncludes()) {
if (!incl.contains(".")) {
entityIncludes.add(incl);
}
}
for (String excl : extConfig.getExcludes()) {
if (!excl.contains(".")) {
entityExcludes.add(excl);
}
}
for (String join : extConfig.getAssociations()) {
if (!join.contains(".")) {
entityJoins.add(join);
}
}
} else {
/*
* Loop each include/exclude/join config and determine if they are
* set for the current entity at this position in the tree. If the
* full path to a specific field on this entity is set, we want to
* include it in the entity specific config. We only need the field
* portion when testing against the current entity so that gets
* split out before adding to the list
*/
for (String incl : extConfig.getIncludes()) {
if (incl.contains(entityName + ".")) {
entityIncludes.add(incl.split("\\.")[1]);
}
}
for (String excl : extConfig.getExcludes()) {
if (excl.contains(entityName + ".")) {
entityExcludes.add(excl.split("\\.")[1]);
}
}
for (String join : extConfig.getAssociations()) {
if (join.contains(entityName + ".")) {
entityJoins.add(join = join.split("\\.")[1]);
}
}
}
/*
* Loop through fields on our entity and determine if they should be
* added
*/
for (Field field : entityClazz.getDeclaredFields()) {
/*
* Static fields don't get returned (reveng may add static fields to
* the class)
*/
if (Modifier.isStatic(field.getModifiers())) {
continue;
}
/*
* Get the field name
*/
String fieldName = field.getName();
/*
* Confirm we should be including this field for this entity. ID
* fields get added regardless of whether they are explicitly
* included.
*/
if (!doInclude(entityClazz, fieldName, entityIncludes,
entityExcludes)) {
continue;
}
/*
* Get the field value and make sure it's not this entities parent
* (prevent infinite recursion)
*/
Object fieldValue = orm.getFieldValue(entity, fieldName);
/*
* Process one to many
*/
if (orm.isOneToMany(entityClazz, fieldName)
|| orm.isManyToMany(entityClazz, fieldName)) {
try {
String idFieldName = Inflector.getInstance().singularize(
fieldName)
+ "Ids";
boolean includeAssociationIds = doInclude(entityClazz,
idFieldName, entityIncludes, entityExcludes);
boolean includeAssociation = entityJoins
.contains(StringUtilities.capitalize(fieldName));
if (!includeAssociationIds && !includeAssociation) {
continue;
}
@SuppressWarnings("unchecked")
List<Object> assocObjList = new ArrayList<Object>(
(Collection<? extends Object>) fieldValue);
List<Object> associatedIds = new ArrayList<Object>();
List<Map<String, Object>> associatedData = new ArrayList<Map<String, Object>>();
for (Object assocObj : assocObjList) {
/*
* Don't go back up the tree
*/
if (includeAssociation
&& !assocObjList.contains(parentEntity)) {
Map<String, Object> associatedGraph = getObjectGraph(
assocObj, fieldName, entity, entityName);
associatedData.add(associatedGraph);
}
if (includeAssociationIds) {
associatedIds.add(orm.getEntityId(assocObj));
}
}
if (includeAssociation
&& !assocObjList.contains(parentEntity)) {
entityData.put(fieldName, associatedData);
}
if (includeAssociationIds) {
entityData.put(idFieldName, associatedIds);
}
} catch (Exception e) {
// log.warn("Error", e);
}
continue;
}
/*
* Process many to one
*/
if (orm.isManyToOne(entityClazz, fieldName)) {
try {
String idFieldName = fieldName + "Id";
boolean includeAssociationId = doInclude(entityClazz,
idFieldName, entityIncludes, entityExcludes);
boolean includeAssociation = entityJoins
.contains(StringUtilities.capitalize(fieldName));
Map<String, Object> associatedGraph = null;
if (includeAssociation && !fieldValue.equals(parentEntity)) {
associatedGraph = getObjectGraph(fieldValue, fieldName,
entity, entityName);
entityData.put(fieldName, associatedGraph);
}
if (includeAssociationId) {
Object idValue = orm.getEntityId(fieldValue);
entityData.put(idFieldName, idValue);
}
} catch (Exception e) {
}
continue;
}
/*
* Process standard fields
*/
try {
String fieldType = orm.getFieldType(entityClazz, fieldName);
if (fieldType.equals("date")) {
fieldValue = dateFormat.format(fieldValue);
} else if (fieldType.equals("time")) {
fieldValue = timeFormat.format(fieldValue);
}
entityData.put(fieldName, fieldValue);
} catch (Exception e) {
// log.warn("Error", e);
}
}
return entityData;
}
private static String[] dateFormatStrings = {"yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd", "HH:mm:ss"};
@Override
public ORMModelInstance update(Map<String, Object> data) {
ORMPersistenceManager orm = AdaptrexRegistry.getPersistenceManager(this.extConfig.getFactoryName());
EntityManager em = (EntityManager) orm.getSession();
EntityTransaction tx = em.getTransaction();
try {
tx.begin();
for (String key : data.keySet()) {
if (key.equals("id")) {
continue;
}
Object val = data.get(key);
/*
* Get our class
*/
Class<?> clazz = this.entity.getClass();
/*
* Get info about the field we're updating
*/
Field field = null;
try {
field = clazz.getDeclaredField(key);
} catch (Exception e) {
}
/*
* If we don't have a field with the current name, check for a
* foreign entity
*/
if (key.endsWith("Id")
&& orm.isManyToOne(clazz,
key.substring(0, key.length() - 2))) {
key = key.substring(0, key.length() - 2);
try {
field = entity.getClass().getDeclaredField(key);
if (field != null && val != null) {
val = orm.getEntity(field.getType(), val);
}
} catch (Exception e) {
log.debug("Couldn't set " + field.getName());
continue;
}
}
/*
* Check for 1:m or n:m
*/
if (key.endsWith("Ids")) {
key = StringUtilities.pluralize(key.substring(0, key.length() - 3));
if (orm.isManyToOne(clazz, key) || orm.isManyToMany(clazz, key)) {
Class<?> fieldType = null;
try {
evictAssociated(em, entity);
} catch (Exception e) {
log.info("Couldn't evict associated entity for " + entity.getClass().getName());
}
try {
field = entity.getClass().getDeclaredField(key);
fieldType = field.getType();
List<Object> assocObjList = new ArrayList<Object>();
Class<?> itemType = null;
Type type = field.getGenericType();
if (type instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) type;
itemType = (Class<?>) pt.getActualTypeArguments()[0];
}
@SuppressWarnings("unchecked")
List<Object> listItemIds = (List<Object>) StringUtilities.fromJson((String) val, List.class);
for (Object listItemId : listItemIds) {
assocObjList.add(orm.getEntity(itemType, listItemId));
}
if (fieldType.getSimpleName().equals("Set")) {
val = new HashSet<Object>(assocObjList);
} else {
val = assocObjList;
}
} catch (Exception e) {
log.info("Error: " + e.getLocalizedMessage());
log.info("Couldn't set field: " + "set" + StringUtilities.capitalize(key) +
"(" + fieldType.getSimpleName() + ")");
continue;
}
}
}
if (field == null) {
continue;
}
Class<?> fieldType = field.getType();
String typeName = fieldType.getSimpleName().toLowerCase();
/*
* Handle Date Fields
*/
if (typeName.equals("date")) {
for (String formatString : dateFormatStrings) {
try {
val = new SimpleDateFormat(formatString, Locale.ENGLISH)
.parse((String) val);
break;
} catch (Exception e) {
}
}
} else if (typeName.equals("float")) {
if (val instanceof Integer) {
val = new Float((Integer) val);
} else if (val instanceof Double) {
val = ((Double) val).floatValue();
}
}
try {
Method setter = clazz.getMethod(
"set" + StringUtilities.capitalize(key),
fieldType);
setter.invoke(entity, val);
} catch (Exception e) {
log.info("Couldn't set field: " + "set" + StringUtilities.capitalize(key) +
"(" + field.getType().getSimpleName() + "): Received " +
val.getClass().getSimpleName());
}
}
if (id != null && em.find(entity.getClass(), id) != null) {
entity = em.merge(entity);
} else {
em.persist(entity);
}
try {
evictAssociated(em, entity);
} catch (Exception e) {
log.info("Couldn't evict associated entity for " + entity.getClass().getName());
}
tx.commit();
} catch (Exception e) {
if (tx.isActive()) {
tx.rollback();
}
throw new RuntimeException (e);
} finally {
em.close();
}
graph = this.getObjectGraph(entity, extConfig.getModelName(), null,
null);
this.id = graph.get("id");
return this;
}
@Override
public ORMModelInstance delete() {
ORMPersistenceManager orm = AdaptrexRegistry.getPersistenceManager(this.extConfig.getFactoryName());
EntityManager em = (EntityManager) orm.getSession();
EntityTransaction tx = em.getTransaction();
try {
tx.begin();
try {
evictAssociated(em, entity);
} catch (Exception e) {
log.info("Couldn't evict associated entity for " + entity.getClass().getName());
}
em.remove(em.merge(this.entity));
tx.commit();
} catch (Exception e) {
if (tx.isActive()) {
tx.rollback();
}
throw new RuntimeException (e);
} finally {
em.close();
}
return this;
}
private boolean doInclude(Class<?> entityClazz, String fieldName,
List<String> entityIncludes, List<String> entityExcludes) {
if (orm.isIdField(entityClazz, fieldName)) {
return true;
}
if (extConfig.getIncludes().size() > 0) {
if (entityIncludes.isEmpty()
|| (!entityIncludes.contains("*")
&& !entityIncludes.contains(fieldName)
&& !entityIncludes.contains(fieldName + "Id") && !entityIncludes
.contains(fieldName + "Ids"))) {
return false;
}
}
if (extConfig.getExcludes().size() > 0) {
if (entityExcludes.contains("*")
|| entityExcludes.contains(fieldName)) {
return false;
}
}
return true;
}
private void evictAssociated(EntityManager em, Object entity) {
Class<?> clazz = entity.getClass();
for (Field field : clazz.getDeclaredFields()) {
/*
* Evict ManyToOne
*/
if (orm.isManyToOne(clazz, field.getName())) {
try {
Method getter = clazz.getDeclaredMethod("get"
+ StringUtilities.capitalize(field.getName()));
Object associated = getter.invoke(entity);
if (associated == null) {
continue;
}
Class<?> associatedClazz = associated.getClass();
Method idGetter = associatedClazz
.getDeclaredMethod("getId");
EntityManagerFactory emf = em.getEntityManagerFactory();
Cache emfCache = emf.getCache();
emfCache.evict(associated.getClass(),
idGetter.invoke(associated));
} catch (Exception e) {
log.warn("Error", e);
}
}
/*
* Evict OneToMany or ManyToMany
*/
if (orm.isOneToMany(clazz, field.getName()) || orm.isManyToMany(clazz, field.getName())) {
try {
Method getter = clazz.getDeclaredMethod("get"
+ StringUtilities.capitalize(field.getName()));
Set<?> associatedSet = (Set<?>) getter.invoke(entity);
for (Object associated : associatedSet) {
Class<?> associatedClazz = associated.getClass();
Method idGetter = associatedClazz
.getDeclaredMethod("getId");
em.getEntityManagerFactory()
.getCache()
.evict(associated.getClass(),
idGetter.invoke(associated));
}
} catch (Exception e) {
log.warn("Error", e);
}
}
}
}
}