/*
* Copyright 2010 Impetus Infotech.
*
* 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.impetus.kundera.ejb;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.FetchType;
import javax.persistence.PersistenceException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.impetus.kundera.loader.DBType;
import com.impetus.kundera.metadata.EntityMetadata;
import com.impetus.kundera.property.PropertyAccessException;
import com.impetus.kundera.property.PropertyAccessorHelper;
import com.impetus.kundera.proxy.EnhancedEntity;
/**
* The Class EntityReachabilityResolver.
*
* @author animesh.kumar
*/
public class EntityResolver {
/** The Constant log. */
private static final Log log = LogFactory.getLog(EntityResolver.class);
/** The em. */
private EntityManagerImpl em;
/**
* Instantiates a new entity resolver.
*
* @param em
* the em
*/
public EntityResolver(EntityManagerImpl em) {
this.em = em;
}
/**
* Resolve all reachable entities from entity
*
* @param entity
* the entity
* @param cascadeType
* the cascade type
* @return the all reachable entities
*/
public List<EnhancedEntity> resolve (Object entity, CascadeType cascadeType, DBType dbType) {
Map<String, EnhancedEntity> map = new HashMap<String, EnhancedEntity>();
try {
log.debug("Resolving reachable entities for cascade "
+ cascadeType);
//For Document-based data-stores, entities need to be embedded
if(dbType.equals(DBType.MONGODB)) {
resolveEmbeddedEntities(entity, cascadeType, map);
} else {
recursivelyResolveEntities(entity, cascadeType, map);
}
} catch (PropertyAccessException e) {
throw new PersistenceException(e.getMessage());
}
if (log.isDebugEnabled()) {
for (Map.Entry<String, EnhancedEntity> entry : map.entrySet()) {
log.debug("Entity => " + entry.getKey() + ", ForeignKeys => "
+ entry.getValue().getForeignKeysMap());
}
}
return new ArrayList<EnhancedEntity>(map.values());
}
private void resolveEmbeddedEntities(Object o, CascadeType cascadeType,
Map<String, EnhancedEntity> entities)
throws PropertyAccessException {
EntityMetadata m = em.getMetadataManager().getEntityMetadata(o.getClass());
String id = PropertyAccessorHelper.getId(o, m);
// Ensure that @Id is set
if (null == id || id.trim().isEmpty()) {
throw new PersistenceException("Missing primary key >> " + m.getEntityClazz().getName() + "#"
+ m.getIdProperty().getName());
}
String mapKeyForEntity = m.getEntityClazz().getName() + "_" + id;
log.debug("Resolving >> " + mapKeyForEntity);
// Map to hold property-name=>foreign-entity relations
Map<String, Set<String>> foreignKeysMap = new HashMap<String, Set<String>>();
// Save to map
entities.put(mapKeyForEntity, em.getFactory().getEnhancedEntity(o, id,
foreignKeysMap));
}
/**
* helper method to recursively build reachable object list.
*
* @param o
* the o
* @param cascadeType
* the cascade type
* @param entities
* the entities
* @return the all reachable entities
* @throws PropertyAccessException
* the property access exception
*/
private void recursivelyResolveEntities(Object o, CascadeType cascadeType,
Map<String, EnhancedEntity> entities)
throws PropertyAccessException {
EntityMetadata m = null;
try {
m = em.getMetadataManager().getEntityMetadata(o.getClass());
} catch (Exception e) {
// Object might already be an enhanced entity
}
if (m == null) {
return;
}
String id = PropertyAccessorHelper.getId(o, m);
// Ensure that @Id is set
if (null == id || id.trim().isEmpty()) {
throw new PersistenceException("Missing primary key >> "
+ m.getEntityClazz().getName() + "#"
+ m.getIdProperty().getName());
}
// Dummy name to check if the object is already processed
String mapKeyForEntity = m.getEntityClazz().getName() + "_" + id;
if (entities.containsKey(mapKeyForEntity)) {
return;
}
log.debug("Resolving >> " + mapKeyForEntity);
// Map to hold property-name=>foreign-entity relations
Map<String, Set<String>> foreignKeysMap = new HashMap<String, Set<String>>();
// Save to map
entities.put(mapKeyForEntity, em.getFactory().getEnhancedEntity(o, id,
foreignKeysMap));
// Iterate over EntityMetata.Relation relations
for (EntityMetadata.Relation relation : m.getRelations()) {
// Cascade?
if (!relation.getCascades().contains(CascadeType.ALL)
&& !relation.getCascades().contains(cascadeType)) {
continue;
}
// Target entity
Class<?> targetClass = relation.getTargetEntity();
// Mapped to this property
Field targetField = relation.getProperty();
// Is it optional?
boolean optional = relation.isOptional();
// Value
Object value = PropertyAccessorHelper.getObject(o, targetField);
// if object is not null, then proceed
if (null != value) {
if (relation.isUnary()) {
// Unary relation will have single target object.
String targetId = PropertyAccessorHelper.getId(value, em
.getMetadataManager()
.getEntityMetadata(targetClass));
Set<String> foreignKeys = new HashSet<String>();
foreignKeys.add(targetId);
// put to map
foreignKeysMap.put(targetField.getName(), foreignKeys);
// get all other reachable objects from object "value"
recursivelyResolveEntities(value, cascadeType, entities);
}
if (relation.isCollection()) {
// Collection relation can have many target objects.
// Value must map to Collection interface.
@SuppressWarnings("unchecked")
Collection collection = (Collection) value;
Set<String> foreignKeys = new HashSet<String>();
// Iterate over each Object and get the @Id
for (Object o_ : collection) {
String targetId = PropertyAccessorHelper.getId(o_, em
.getMetadataManager().getEntityMetadata(
targetClass));
foreignKeys.add(targetId);
// Get all other reachable objects from "o_"
recursivelyResolveEntities(o_, cascadeType, entities);
}
foreignKeysMap.put(targetField.getName(), foreignKeys);
}
}
// if the value is null
else {
// halt, if this was a non-optional property
if (!optional) {
throw new PersistenceException("Missing "
+ targetClass.getName() + "."
+ targetField.getName());
}
}
}
}
/**
* Populate foreign entities.
*
* @param containingEntity
* the containing entity
* @param containingEntityId
* the containing entity id
* @param relation
* the relation
* @param foreignKeys
* the foreign keys
* @throws PropertyAccessException
* the property access exception
*/
public void populateForeignEntities (Object entity, String entityId,
EntityMetadata.Relation relation, String... foreignKeys)
throws PropertyAccessException {
if (null == foreignKeys || foreignKeys.length == 0) {
return;
}
String entityName = entity.getClass().getName() + "_" + entityId + "#"
+ relation.getProperty().getName();
log.debug("Populating foreign entities for " + entityName);
// foreignEntityClass
Class<?> foreignEntityClass = relation.getTargetEntity();
// Eagerly Caching containing entity to avoid it's own loading,
// in case the target contains a reference to containing entity.
em.getSession().store(entity, entityId, Boolean.FALSE);
if (relation.isUnary()) {
// there is just one target object
String foreignKey = foreignKeys[0];
Object foreignObject = getForeignEntityOrProxy(entityName,
foreignEntityClass, foreignKey, relation);
PropertyAccessorHelper.set(entity, relation.getProperty(),
foreignObject);
}
else if (relation.isCollection()) {
// there could be multiple target objects
// Cast to Collection
Collection<Object> foreignObjects = null;
if (relation.getPropertyType().equals(Set.class)) {
foreignObjects = new HashSet<Object>();
} else if (relation.getPropertyType().equals(List.class)) {
foreignObjects = new ArrayList<Object>();
}
// Iterate over keys
for (String foreignKey : foreignKeys) {
Object foreignObject = getForeignEntityOrProxy(entityName,
foreignEntityClass, foreignKey, relation);
foreignObjects.add(foreignObject);
}
PropertyAccessorHelper.set(entity, relation.getProperty(),
foreignObjects);
}
}
/**
* Helper method to load Foreign Entity/Proxy
*
* @param entityName
* the entity name
* @param persistentClass
* the persistent class
* @param foreignKey
* the foreign key
* @param relation
* the relation
* @return the foreign entity or proxy
*/
private Object getForeignEntityOrProxy(String entityName,
Class<?> persistentClass, String foreignKey,
EntityMetadata.Relation relation) {
// Check in session cache!
Object cached = em.getSession().lookup(persistentClass, foreignKey);
if (cached != null) {
return cached;
}
FetchType fetch = relation.getFetchType();
if (fetch.equals(FetchType.EAGER)) {
log.debug("Eagerly loading >> " + persistentClass.getName() + "_"
+ foreignKey);
// load target eagerly!
return em.immediateLoadAndCache(persistentClass, foreignKey);
} else {
log.debug("Creating proxy for >> " + persistentClass.getName() + "#"
+ relation.getProperty().getName() + "_" + foreignKey);
// metadata
EntityMetadata m = em.getMetadataManager().getEntityMetadata(
persistentClass);
return em.getFactory().getLazyEntity(entityName, persistentClass,
m.getReadIdentifierMethod(), m.getWriteIdentifierMethod(),
foreignKey, em);
}
}
}