/*
* Copyright (c) 2005-2016 Vincent Vandenschrick. All rights reserved.
*
* This file is part of the Jspresso framework.
*
* Jspresso is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Jspresso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Jspresso. If not, see <http://www.gnu.org/licenses/>.
*/
package org.jspresso.framework.application.backend.persistence.hibernate;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import org.hibernate.collection.spi.PersistentCollection;
import org.hibernate.internal.util.collections.LazyIterator;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.proxy.LazyInitializer;
import org.hibernate.type.Type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.jspresso.framework.application.backend.BackendControllerHolder;
import org.jspresso.framework.application.backend.IBackendController;
import org.jspresso.framework.model.component.ILifecycleCapable;
import org.jspresso.framework.model.descriptor.IComponentDescriptor;
import org.jspresso.framework.model.descriptor.IPropertyDescriptor;
import org.jspresso.framework.model.entity.IEntity;
import org.jspresso.framework.model.entity.IEntityLifecycleHandler;
import org.jspresso.framework.model.persistence.hibernate.EntityProxyInterceptor;
import org.jspresso.framework.model.persistence.hibernate.entity.HibernateEntityRegistry;
import org.jspresso.framework.security.UserPrincipal;
import org.jspresso.framework.util.bean.PropertyHelper;
import org.jspresso.framework.util.exception.NestedRuntimeException;
import org.jspresso.framework.util.reflect.ReflectHelper;
/**
* Hibernate session interceptor aware of a backend controller to deal with
* uniqueness of entity instances across the JVM.
*
* @author Vincent Vandenschrick
*/
@SuppressWarnings("rawtypes")
public class ControllerAwareEntityProxyInterceptor extends EntityProxyInterceptor {
private static final long serialVersionUID = -6834992000307471098L;
private static final Logger LOG = LoggerFactory.getLogger(ControllerAwareEntityProxyInterceptor.class);
private static final int MAX_LIFECYCLE_ITERATIONS = 10;
/**
* Uses the backend controller to retrieve the dirty properties of the entity.
* <p/>
* {@inheritDoc}
*/
@Override
public int[] findDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState,
String[] propertyNames, Type[] types) {
if (entity instanceof IEntity) {
Map<String, Object> dirtyProperties = getBackendController().getDirtyProperties((IEntity) entity, false);
if (dirtyProperties != null) {
dirtyProperties.remove(IEntity.VERSION);
}
if (dirtyProperties == null) {
return null;
}
if (dirtyProperties.isEmpty()) {
return new int[0];
}
if (!((IEntity) entity).isPersistent()) {
// whenever an entity has just been saved, its state is in the dirty
// store. Hibernate might ask to check dirtiness especially for
// collection members. Those just saved entities must not be considered
// dirty but there might be some reference properties to update, so let
// Hibernate default dirty checking happen.
return null;
}
int[] indices = new int[propertyNames.length];
int n = 0;
for (int i = 0; i < propertyNames.length; i++) {
String propertyName = PropertyHelper.fromJavaBeanPropertyName(propertyNames[i]);
if (dirtyProperties.containsKey(propertyName)) {
indices[n] = i;
n++;
if (currentState[i] instanceof PersistentCollection) {
((PersistentCollection) currentState[i]).dirty();
}
}
}
int[] shrinkedArray = new int[n];
System.arraycopy(indices, 0, shrinkedArray, 0, n);
return shrinkedArray;
}
return super.findDirty(entity, id, currentState, previousState, propertyNames, types);
}
/**
* {@inheritDoc}
*/
@SuppressWarnings("unchecked")
@Override
public Object getEntity(String entityName, Serializable id) {
IEntity registeredEntity = null;
try {
if (getBackendController().isUnitOfWorkActive()) {
registeredEntity = getBackendController().getUnitOfWorkEntity((Class<? extends IEntity>) Class.forName(
entityName), id);
} else {
registeredEntity = getBackendController().getRegisteredEntity((Class<? extends IEntity>) Class.forName(
entityName), id);
if (registeredEntity instanceof HibernateProxy) {
HibernateProxy proxy = (HibernateProxy) registeredEntity;
LazyInitializer li = proxy.getHibernateLazyInitializer();
registeredEntity = (IEntity) li.getImplementation();
}
}
} catch (ClassNotFoundException ex) {
LOG.error("Class for entity {} was not found", entityName, ex);
}
// getEntity should never return transient instances, see #1244
if (registeredEntity != null && !registeredEntity.isPersistent()) {
registeredEntity = null;
}
((HibernateBackendController) getBackendController()).detachFromHibernateInDepth(registeredEntity,
((HibernateBackendController) getBackendController()).getHibernateSession(), new HibernateEntityRegistry(
"detachFromHibernateInDepth"));
return registeredEntity;
}
/**
* {@inheritDoc}
*/
@Override
public boolean onLoad(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
boolean updated = super.onLoad(entity, id, state, propertyNames, types);
if (entity instanceof IEntity) {
IBackendController backendController = getBackendController();
if (backendController.isUnitOfWorkActive()) {
Map<String, Object> properties = new HashMap<>();
for (int i = 0; i < propertyNames.length; i++) {
String propertyName = PropertyHelper.fromJavaBeanPropertyName(propertyNames[i]);
if (!isHibernateInternal(propertyName)) {
properties.put(propertyName, state[i]);
}
}
((IEntity) entity).straightSetProperties(properties);
// So that dirty tracking is started on the entity.
// See bug #1018
backendController.registerEntity((IEntity) entity);
} else {
if (backendController.getRegisteredEntity(((IEntity) entity).getComponentContract(), id) == null) {
Map<String, Object> properties = new HashMap<>();
for (int i = 0; i < propertyNames.length; i++) {
if (state[i] != null) {
String propertyName = PropertyHelper.fromJavaBeanPropertyName(propertyNames[i]);
if (!isHibernateInternal(propertyName)) {
if (state[i] instanceof IEntity) {
IEntity refEntity = (IEntity) state[i];
IEntity mergedEntity = backendController.getRegisteredEntity(HibernateHelper.getComponentContract(
refEntity), refEntity.getId());
if (mergedEntity != null && mergedEntity != refEntity) {
state[i] = mergedEntity;
updated = true;
}
}
properties.put(propertyName, state[i]);
}
}
}
((IEntity) entity).straightSetProperties(properties);
backendController.registerEntity((IEntity) entity);
}
}
}
return updated;
}
/**
* Registers Entities to be merged back from the uow to the session on
* commit.
* <p/>
* {@inheritDoc}
*/
@Override
public void postFlush(Iterator entities) {
while (entities.hasNext()) {
Object entity = entities.next();
if (entity instanceof IEntity) {
getBackendController().recordAsSynchronized((IEntity) entity);
}
}
super.postFlush(entities);
}
/**
* This is the place to trigger the update lifecycle handler. onFlushDirty is
* not the right place since it cannot deal with transient new instances that
* might be added to the object tree. It also registers Entities to be merged
* back from the uow to the session on commit. This last action is done here
* instead of postFlush. See http://www.jspresso.org/mantis/view.php?id=455.
* <p/>
* {@inheritDoc}
*/
@Override
public void preFlush(Iterator entities) {
if (!entities.hasNext()) {
return;
}
//This is a hack to be informed of new additions to the flush during the flush
Map<?, ?> underlyingHibernateMap;
try {
underlyingHibernateMap = (Map<?, ?>) ReflectHelper.getPrivateFieldValue(LazyIterator.class, "map", entities);
} catch (IllegalAccessException | NoSuchFieldException e) {
throw new NestedRuntimeException("Could not extract the underlying Hibernate map.");
}
IBackendController backendController = getBackendController();
if (!backendController.isUnitOfWorkActive() && entities.hasNext()) {
// throw new BackendException(
// "A save has been attempted outside of any transactional context. Jspresso disallows this bad practice.");
LOG.warn(
"A flush has been attempted outside of any transactional context. Jspresso disallows this bad practice.");
}
// To avoid concurrent access modifications
Collection<Object> preFlushedEntities = new LinkedHashSet<>(underlyingHibernateMap.values());
final Set<Object> persistedEntities = new HashSet<>();
final Set<Object> lifecycledEntities = new HashSet<>();
PropertyChangeListener dirtInterceptor = new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
Object source = evt.getSource();
if (source instanceof IEntity) {
IComponentDescriptor<?> componentDescriptor = getBackendController().getEntityFactory()
.getComponentDescriptor(((IEntity) source)
.getComponentContract());
IPropertyDescriptor propertyDescriptor = componentDescriptor.getPropertyDescriptor(evt.getPropertyName());
if (propertyDescriptor != null && !propertyDescriptor.isComputed()) {
lifecycledEntities.remove(source);
}
}
}
};
try {
backendController.addDirtInterceptor(dirtInterceptor);
boolean lifeCycleTriggered = triggerLifecycle(preFlushedEntities, persistedEntities, lifecycledEntities);
int i = 0;
while (lifeCycleTriggered && i < MAX_LIFECYCLE_ITERATIONS) {
// Because new entities might have been added to the underlying map of the original iterator.
preFlushedEntities = new LinkedHashSet<>(underlyingHibernateMap.values());
// Until the state is stable.
lifeCycleTriggered = triggerLifecycle(preFlushedEntities, persistedEntities, lifecycledEntities);
i++;
}
if (i == MAX_LIFECYCLE_ITERATIONS) {
LOG.error("An infinite loop was prevented when flushing to persistent store. This is certainly due to lifecycle "
+ "interceptors that do not converge and keep modifying the entity tree in an incompatible manner. "
+ "The maximum number of iterations ({}) was reached.", MAX_LIFECYCLE_ITERATIONS);
}
} finally {
backendController.removeDirtInterceptor(dirtInterceptor);
}
}
private boolean triggerLifecycle(Collection<Object> preFlushedEntities, Set<Object> persistedEntities,
Set<Object> lifecycledEntities) {
boolean lifecycleTriggered = false;
for (Object entity : preFlushedEntities) {
if (entity instanceof ILifecycleCapable && !lifecycledEntities.contains(entity)) {
if (entity instanceof IEntity) {
if (((IEntity) entity).getVersion() != null) {
boolean isClean = false;
Map<String, Object> dirtyProperties = getBackendController().getDirtyProperties((IEntity) entity, false);
if (dirtyProperties == null) {
isClean = true;
} else if (dirtyProperties.isEmpty()) {
isClean = true;
}
if (getBackendController().isEntityRegisteredForDeletion((IEntity) entity)) {
// already performed onDelete
//((ILifecycleCapable) entity).onDelete(getEntityFactory(), getPrincipal(), getEntityLifecycleHandler());
lifecycledEntities.add(entity);
lifecycleTriggered = true;
} else if (!((IEntity) entity).isPersistent() && !persistedEntities.contains(entity)) {
persistedEntities.add(entity);
// already performed onSave
//((ILifecycleCapable) entity).onPersist(getEntityFactory(), getPrincipal(), getEntityLifecycleHandler());
lifecycledEntities.add(entity);
lifecycleTriggered = true;
} else if (!isClean) {
((ILifecycleCapable) entity).onUpdate(getEntityFactory(), getPrincipal(), getEntityLifecycleHandler());
lifecycledEntities.add(entity);
lifecycleTriggered = true;
}
}
}
}
}
return lifecycleTriggered;
}
/**
* Gets the getBackendController().
*
* @return the backendController.
*/
protected IBackendController getBackendController() {
return BackendControllerHolder.getCurrentBackendController();
}
/**
* {@inheritDoc}
*/
@Override
protected IEntityLifecycleHandler getEntityLifecycleHandler() {
return getBackendController();
}
/**
* Gets the principal of the application session.
* <p/>
* {@inheritDoc}
*/
@Override
protected UserPrincipal getPrincipal() {
return getBackendController().getApplicationSession().getPrincipal();
}
}