/* * 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; import java.beans.PropertyChangeListener; import java.io.Serializable; import java.lang.reflect.InvocationTargetException; import java.text.NumberFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TimeZone; import javax.security.auth.Subject; import org.apache.commons.collections4.map.LRUMap; import org.apache.commons.lang3.LocaleUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.transaction.support.TransactionTemplate; import org.jspresso.framework.action.ActionBusinessException; import org.jspresso.framework.action.ActionContextConstants; import org.jspresso.framework.action.ActionException; import org.jspresso.framework.action.IAction; import org.jspresso.framework.application.AbstractController; import org.jspresso.framework.application.backend.action.Asynchronous; import org.jspresso.framework.application.backend.action.Transactional; import org.jspresso.framework.application.backend.async.AsyncActionExecutor; import org.jspresso.framework.application.backend.entity.ControllerAwareProxyEntityFactory; import org.jspresso.framework.application.backend.session.EMergeMode; import org.jspresso.framework.application.backend.session.IApplicationSession; import org.jspresso.framework.application.backend.session.IEntityUnitOfWork; import org.jspresso.framework.application.backend.session.basic.BasicEntityUnitOfWork; import org.jspresso.framework.application.i18n.ITranslationPlugin; import org.jspresso.framework.application.model.Module; import org.jspresso.framework.application.model.Workspace; import org.jspresso.framework.application.model.descriptor.ModuleDescriptor; import org.jspresso.framework.application.model.descriptor.WorkspaceDescriptor; import org.jspresso.framework.application.security.ISecurityPlugin; import org.jspresso.framework.application.security.SecurityContextBuilder; import org.jspresso.framework.application.security.SecurityContextConstants; import org.jspresso.framework.binding.IValueConnector; import org.jspresso.framework.binding.model.IModelConnectorFactory; import org.jspresso.framework.model.component.IComponent; import org.jspresso.framework.model.component.IComponentCollectionFactory; import org.jspresso.framework.model.component.ILifecycleCapable; import org.jspresso.framework.model.datatransfer.ComponentTransferStructure; import org.jspresso.framework.model.descriptor.ICollectionPropertyDescriptor; import org.jspresso.framework.model.descriptor.IComponentDescriptor; import org.jspresso.framework.model.descriptor.IModelDescriptor; import org.jspresso.framework.model.descriptor.IPropertyDescriptor; import org.jspresso.framework.model.descriptor.IReferencePropertyDescriptor; import org.jspresso.framework.model.descriptor.IRelationshipEndPropertyDescriptor; import org.jspresso.framework.model.entity.IEntity; import org.jspresso.framework.model.entity.IEntityCloneFactory; import org.jspresso.framework.model.entity.IEntityFactory; import org.jspresso.framework.model.entity.IEntityRegistry; import org.jspresso.framework.model.entity.basic.BasicEntityRegistry; import org.jspresso.framework.security.ISecurable; import org.jspresso.framework.security.ISecurityContextBuilder; import org.jspresso.framework.security.SecurityHelper; import org.jspresso.framework.security.UserPrincipal; import org.jspresso.framework.util.accessor.IAccessorFactory; import org.jspresso.framework.util.i18n.ITranslationProvider; import org.jspresso.framework.util.preferences.IPreferencesStore; /** * Base class for backend application controllers. Backend controllers are * responsible for : * <ul> * <li>keeping a reference to the application session</li> * <li>keeping a reference to the application workspaces and their state</li> * <li>keeping a reference to the application clipboard</li> * <li>keeping a reference to the entity registry that guarantees the in-memory * entity reference unicity in the user session</li> * <li>keeping a reference to the entity dirt recorder that keeps track of * entity changes to afterwards optimize the ORM operations</li> * <li>keeping a reference to the Spring transaction template and its peer * "Unit of Work" -aka UOW- that is responsible to manage application * transactions and adapt the underlying transaction system (Hibernate, JTA, * ...)</li> * </ul> * Moreover, the backend controller will provide several model related factories * that can be configured to customize default, built-in behaviour. Most of * these configured properties will be accessible using the corresponding * getters. Those getters should be used by the service layer. * * @author Vincent Vandenschrick */ public abstract class AbstractBackendController extends AbstractController implements IBackendController { private static final Logger LOG = LoggerFactory.getLogger(AbstractBackendController.class); private final IEntityUnitOfWork unitOfWork; private final IEntityUnitOfWork sessionUnitOfWork; private final LRUMap<Module, IValueConnector> moduleConnectors; private final ISecurityContextBuilder securityContextBuilder; private final Set<AsyncActionExecutor> asyncExecutors; private ThreadGroup asyncActionsThreadGroup; private ThreadGroup controllerAsyncActionsThreadGroup; private IApplicationSession applicationSession; private IEntityCloneFactory carbonEntityCloneFactory; private IComponentCollectionFactory collectionFactory; private IEntityFactory entityFactory; private IModelConnectorFactory modelConnectorFactory; private TransactionTemplate transactionTemplate; private ComponentTransferStructure<IComponent> transferStructure; private Map<String, IValueConnector> workspaceConnectors; private IPreferencesStore userPreferencesStore; private ITranslationProvider translationProvider; private ISecurityPlugin customSecurityPlugin; private ITranslationPlugin customTranslationPlugin; private TimeZone referenceTimeZone; private TimeZone clientTimeZone; private boolean throwExceptionOnBadUsage; private IBackendControllerFactory slaveControllerFactory; private int asyncExecutorsMaxCount; private IBackendController masterController; /** * Constructs a new {@code AbstractBackendController} instance. */ @SuppressWarnings("unchecked") protected AbstractBackendController() { unitOfWork = createUnitOfWork(); sessionUnitOfWork = createUnitOfWork(); sessionUnitOfWork.begin(); moduleConnectors = new LRUMap<>(20); securityContextBuilder = new SecurityContextBuilder(); throwExceptionOnBadUsage = true; asyncExecutors = new LinkedHashSet<>(); setAsyncExecutorsMaxCount(10); } /** * {@inheritDoc} */ @Override public void joinTransaction() { joinTransaction(false); } /** * {@inheritDoc} */ @Override public void joinTransaction(boolean nested) { TransactionSynchronizationManager.registerSynchronization(this); if (!isUnitOfWorkActive()) { beginUnitOfWork(); } else if (nested) { beginNestedUnitOfWork(); } } /** * {@inheritDoc} */ @Override public void performPendingOperations() { Collection<IEntity> entitiesToUpdate = sessionUnitOfWork.getEntitiesRegisteredForUpdate(); Collection<IEntity> entitiesToDelete = sessionUnitOfWork.getEntitiesRegisteredForDeletion(); if (entitiesToUpdate != null) { List<IEntity> uowEntitiesToUpdate = cloneInUnitOfWork(new ArrayList<>(entitiesToUpdate)); for (IEntity uowEntity : uowEntitiesToUpdate) { // Must force insertion registerForUpdate(uowEntity); } } if (entitiesToDelete != null) { List<IEntity> uowEntitiesToDelete = cloneInUnitOfWork(new ArrayList<>(entitiesToDelete)); for (IEntity uowEntity : uowEntitiesToDelete) { // Must force deletion registerForDeletion(uowEntity); } } clearPendingOperations(); } /** * {@inheritDoc} */ @Override public final void beginUnitOfWork() { if (isUnitOfWorkActive()) { throw new BackendException("Cannot begin a new unit of work. Another one is already active."); } doBeginUnitOfWork(); } /** * Performs actual UOW begin. */ protected void doBeginUnitOfWork() { unitOfWork.begin(); } /** * {@inheritDoc} */ @Override public final void beginNestedUnitOfWork() { doBeginNestedUnitOfWork(); } /** * Performs actual UOW begin. */ protected void doBeginNestedUnitOfWork() { unitOfWork.beginNested(); } /** * Clears the pending operations. * <p/> * {@inheritDoc} */ @Override public void clearPendingOperations() { sessionUnitOfWork.clearPendingOperations(); } /** * {@inheritDoc} */ @Override public final <E extends IEntity> E cloneInUnitOfWork(E entity) { return cloneInUnitOfWork(Collections.singletonList(entity)).get(0); } /** * {@inheritDoc} */ @Override public <E extends IEntity> List<E> cloneInUnitOfWork(List<E> entities) { if (!isUnitOfWorkActive()) { throw new BackendException("Cannot use a unit of work that has not begun."); } List<E> uowEntities = new ArrayList<>(); IEntityRegistry alreadyCloned = getUowEntityRegistry(); Set<IEntity> eventsToRelease = new LinkedHashSet<>(); boolean wasDirtyTrackingEnabled = isDirtyTrackingEnabled(); try { setDirtyTrackingEnabled(false); for (E entity : entities) { uowEntities.add(cloneInUnitOfWork(entity, alreadyCloned, eventsToRelease)); } } finally { for (IEntity entity : eventsToRelease) { try { entity.releaseEvents(); } catch (Throwable t) { LOG.error("An unexpected exception occurred when releasing events after a merge", t); } } setDirtyTrackingEnabled(wasDirtyTrackingEnabled); } return uowEntities; } /** * Gets uow entity registry. * * @return the uow entity registry */ protected IEntityRegistry getUowEntityRegistry() { Map<Class<? extends IEntity>, Map<Serializable, IEntity>> uowExistingEntities = unitOfWork.getRegisteredEntities(); return createEntityRegistry("cloneInUnitOfWork", uowExistingEntities); } /** * {@inheritDoc} */ @Override public final void commitUnitOfWork() { if (!isUnitOfWorkActive()) { throw new BackendException("Cannot commit a unit of work that has not begun."); } if (unitOfWork.hasNested()) { unitOfWork.commit(); } else { doCommitUnitOfWork(); } } /** * Performs actual UOW commit. */ protected void doCommitUnitOfWork() { try { Collection<IEntity> uowEntitiesRegisteredForDeletion = unitOfWork.getEntitiesRegisteredForDeletion(); if (uowEntitiesRegisteredForDeletion != null) { // manually trigger the fact that deleted entities are synchronized since flush interceptor does not do it. for (IEntity uowEntityRegisteredForDeletion : uowEntitiesRegisteredForDeletion) { recordAsSynchronized(uowEntityRegisteredForDeletion); } } Collection<IEntity> flushedEntities = new LinkedHashSet<>(); Collection<IEntity> updatedEntities = unitOfWork.getUpdatedEntities(); if (updatedEntities != null) { flushedEntities.addAll(updatedEntities); } Collection<IEntity> deletedEntities = unitOfWork.getDeletedEntities(); if (deletedEntities != null) { flushedEntities.addAll(deletedEntities); } mergeBackFlushedEntities(flushedEntities); } finally { unitOfWork.clearPendingOperations(); unitOfWork.commit(); } } /** * Merge back flushed entities. * * @param updatedEntities the updated entities */ protected void mergeBackFlushedEntities(Collection<IEntity> updatedEntities) { if (updatedEntities != null) { List<IEntity> mergedEntities = merge(new ArrayList<>(updatedEntities), EMergeMode.MERGE_CLEAN_LAZY); if (recordedMergedEntities != null) { recordedMergedEntities.addAll(mergedEntities); } } } /** * {@inheritDoc} */ @Override public IValueConnector createModelConnector(String id, IModelDescriptor modelDescriptor) { return modelConnectorFactory.createModelConnector(id, modelDescriptor, this); } /** * Directly delegates execution to the action after having completed its * execution context with the controller's initial context. * <p/> * {@inheritDoc} */ @Override public boolean execute(IAction action, Map<String, Object> context) { if (action == null) { return true; } if (!action.isBackend()) { throw new ActionException( "The backend controller is executing a frontend action. Please check the action chaining : " + action .toString()); } // Should be handled before getting there. // checkAccess(action); final Map<String, Object> actionContext = getInitialActionContext(); if (context != null) { context.putAll(actionContext); } if (action.getClass().isAnnotationPresent(Asynchronous.class)) { int currentExecutorsCount = getRunningExecutors().size(); int maxExecutorsCount = getAsyncExecutorsMaxCount(context); if (maxExecutorsCount >= 0 && currentExecutorsCount >= maxExecutorsCount) { throw new ActionBusinessException( "The number of concurrent asynchronous actions has exceeded the allowed max value : " + currentExecutorsCount, "async.count.exceeded", currentExecutorsCount); } executeAsynchronously(action, context); return true; } if (action.getClass().isAnnotationPresent(Transactional.class)) { return executeTransactionally(action, context); } return executeBackend(action, context); } /** * Executes a backend action. * @param action the action to execute. * @param context the action context * @return true if the action chain should continue, false otherwise. */ protected boolean executeBackend(IAction action, Map<String, Object> context) { return action.execute(this, context); } /** * Executes an action asynchronously, i.e. when * the action is annotated with {@link org.jspresso.framework.application.backend.action.Asynchronous}. * * @param action * the action to execute. * @param context * the context * @return the slave thread executing the action. */ public AsyncActionExecutor executeAsynchronously(IAction action, Map<String, Object> context) { AbstractBackendController slaveBackendController = createBackendController(); AsyncActionExecutor slaveExecutor = new AsyncActionExecutor(action, context, getControllerAsyncActionsThreadGroup(), slaveBackendController); asyncExecutors.add(slaveExecutor); Set<AsyncActionExecutor> oldRunningExecutors = new LinkedHashSet<>(getRunningExecutors()); firePropertyChange("runningExecutors", oldRunningExecutors, getRunningExecutors()); slaveExecutor.start(); if (LOG.isDebugEnabled()) { LOG.debug("List of running executors :"); for (AsyncActionExecutor executor : getRunningExecutors()) { LOG.debug(" --> Executor {} has completed {}", executor.getName(), NumberFormat.getPercentInstance().format( executor.getProgress())); } } return slaveExecutor; } private synchronized ThreadGroup getControllerAsyncActionsThreadGroup() { if (controllerAsyncActionsThreadGroup == null || controllerAsyncActionsThreadGroup.isDestroyed()) { controllerAsyncActionsThreadGroup = new ThreadGroup(asyncActionsThreadGroup, toString()); } return controllerAsyncActionsThreadGroup; } private synchronized void cleanupControllerAsyncActionsThreadGroup() { if (controllerAsyncActionsThreadGroup != null && controllerAsyncActionsThreadGroup.activeCount() == 0 && !controllerAsyncActionsThreadGroup.isDestroyed()) { controllerAsyncActionsThreadGroup.destroy(); } } /** * Creates a slave backend controller, starts it and assign it the same * application session. * <p/> * {@inheritDoc} */ @Override public AbstractBackendController createBackendController() { AbstractBackendController slaveBackendController = (AbstractBackendController) getSlaveControllerFactory() .createBackendController(); // Start the slave controller slaveBackendController.start(getLocale(), getClientTimeZone()); // Use the same application session slaveBackendController.setApplicationSession(getApplicationSession()); slaveBackendController.masterController = this; return slaveBackendController; } /** * Executes an action transactionally, e.g. when the @Transactional annotation * is present. * * @param action * the action to execute. * @param context * the context * @return the action outcome */ public boolean executeTransactionally(final IAction action, final Map<String, Object> context) { Boolean ret = getTransactionTemplate().execute(new TransactionCallback<Boolean>() { @Override public Boolean doInTransaction(TransactionStatus status) { boolean executionStatus = executeBackend(action, context); if (!executionStatus) { status.setRollbackOnly(); } return executionStatus; } }); return ret; } /** * {@inheritDoc} */ @Override public IAccessorFactory getAccessorFactory() { return modelConnectorFactory.getAccessorFactory(); } /** * {@inheritDoc} */ @Override public IApplicationSession getApplicationSession() { return applicationSession; } /** * {@inheritDoc} */ @Override public Map<String, Object> getDirtyProperties(IEntity entity) { return getDirtyProperties(entity, true); } /** * {@inheritDoc} */ @Override public Map<String, Object> getDirtyProperties(IEntity entity, boolean includeComputed) { Map<String, Object> dirtyProperties; if (isUnitOfWorkActive()) { dirtyProperties = unitOfWork.getDirtyProperties(entity); } else { dirtyProperties = sessionUnitOfWork.getDirtyProperties(entity); } if (dirtyProperties != null) { for (Iterator<Map.Entry<String, Object>> ite = dirtyProperties.entrySet().iterator(); ite.hasNext();) { Map.Entry<String, Object> property = ite.next(); boolean include = true; if (!includeComputed) { IComponentDescriptor<?> entityDescriptor = getEntityFactory().getComponentDescriptor(getComponentContract( entity)); IPropertyDescriptor propertyDescriptor = entityDescriptor.getPropertyDescriptor(property.getKey()); include = (propertyDescriptor != null && !propertyDescriptor.isComputed()); } if (include) { Object propertyValue = property.getValue(); Object currentProperty = entity.straightGetProperty(property.getKey()); if ((currentProperty != null && !(currentProperty instanceof Collection<?>) && areEqualWithoutInitializing( currentProperty, property.getValue())) || (currentProperty == null && propertyValue == null)) { // Unfortunately, we cannot ignore collections that have been // changed but reset to their original state. This prevents the // entity to be merged back into the session while the session state // might be wrong. clearPropertyDirtyState(currentProperty); ite.remove(); // actually removes the mapping from the map. } } else { ite.remove(); } } } return dirtyProperties; } private boolean areEqualWithoutInitializing(Object obj1, Object obj2) { if (obj1 == obj2) { return true; } if (obj1 == null || obj2 == null) { return false; } if (obj1 instanceof IEntity) { // To prevent lazy initialization. if (obj2 instanceof IEntity) { return ((IEntity) obj1).getId().equals(((IEntity) obj2).getId()); } return false; } return obj1.equals(obj2); } /** * Resets the property technical dirty state. Gives a chance to subclasses to * reset technical dirty state. Useful in Hibernate for resetting collection * dirty states when their state is identical to the original one after * several modifications. * * @param property * the property to reset the dirty state for. */ protected void clearPropertyDirtyState(Object property) { // NO-OP } /** * {@inheritDoc} */ @Override public IEntityFactory getEntityFactory() { return entityFactory; } /** * Contains the current backend controller. * <p/> * {@inheritDoc} */ @Override public Map<String, Object> getInitialActionContext() { Map<String, Object> initialActionContext = new HashMap<>(); initialActionContext.put(ActionContextConstants.BACK_CONTROLLER, this); return initialActionContext; } /** * Gets the locale used by this controller. The locale is actually held by the * session. * * @return locale used by this controller. */ @Override public Locale getLocale() { return applicationSession.getLocale(); } /** * {@inheritDoc} */ @Override public IEntity getRegisteredEntity(Class<? extends IEntity> entityContract, Serializable entityId) { return sessionUnitOfWork.getRegisteredEntity(entityContract, entityId); } /** * {@inheritDoc} */ @Override public Map<Class<? extends IEntity>, Map<Serializable, IEntity>> getUnitOfWorkEntities() { if (!isUnitOfWorkActive()) { throw new BackendException("Cannot query a unit of work that has not begun."); } return unitOfWork.getRegisteredEntities(); } /** * {@inheritDoc} */ @Override public IEntity getUnitOfWorkEntity(Class<? extends IEntity> entityContract, Serializable entityId) { if (!isUnitOfWorkActive()) { throw new BackendException("Cannot query a unit of work that has not begun."); } return unitOfWork.getRegisteredEntity(entityContract, entityId); } /** * Gets unit of work or registered entity. * * @param entityType * the entity type * @param id * the id * @return the unit of work or registered entity */ @Override public IEntity getUnitOfWorkOrRegisteredEntity(Class<? extends IEntity> entityType, Serializable id) { IEntity entity; if (isUnitOfWorkActive()) { entity = getUnitOfWorkEntity(entityType, id); } else { entity = getRegisteredEntity(entityType, id); } return entity; } /** * Gets the transactionTemplate. * * @return the transactionTemplate. */ @Override public TransactionTemplate getTransactionTemplate() { return transactionTemplate; } /** * {@inheritDoc} */ @Override public IValueConnector getWorkspaceConnector(String workspaceName) { return workspaceConnectors.get(workspaceName); } /** * {@inheritDoc} */ @SuppressWarnings("unchecked") @Override public IValueConnector getModuleConnector(Module module) { if (module == null) { return null; } // we must rehash entries in case in case modules hashcode have changed and // still preserve LRU order. Map<Module, IValueConnector> buff = new LinkedHashMap<>(); buff.putAll(moduleConnectors); moduleConnectors.clear(); moduleConnectors.putAll(buff); IValueConnector moduleConnector = moduleConnectors.get(module); if (moduleConnector == null) { moduleConnector = createModelConnector(module.getName(), ModuleDescriptor.MODULE_DESCRIPTOR); moduleConnectors.put(module, moduleConnector); } moduleConnector.setConnectorValue(module); return moduleConnector; } /** * {@inheritDoc} */ @Override public abstract void initializePropertyIfNeeded(IComponent componentOrEntity, String propertyName); /** * Sets the model controller workspaces. These workspaces are not kept as-is. * Their connectors are. * * @param workspaces * A map containing the workspaces indexed by a well-known key used * to bind them with their views. */ @Override public void installWorkspaces(Map<String, Workspace> workspaces) { workspaceConnectors = new HashMap<>(); for (Map.Entry<String, Workspace> workspaceEntry : workspaces.entrySet()) { String workspaceName = workspaceEntry.getKey(); Workspace workspace = workspaceEntry.getValue(); IModelDescriptor workspaceDescriptor; workspaceDescriptor = WorkspaceDescriptor.WORKSPACE_DESCRIPTOR; IValueConnector nextWorkspaceConnector = modelConnectorFactory.createModelConnector(workspaceName, workspaceDescriptor, this); nextWorkspaceConnector.setConnectorValue(workspace); workspaceConnectors.put(workspaceName, nextWorkspaceConnector); } } /** * {@inheritDoc} */ @Override public boolean isAnyDirtyInDepth(Collection<?> elements) { return isAnyDirtyInDepth(elements, false); } /** * {@inheritDoc} */ @Override public boolean isAnyDirtyInDepth(Collection<?> elements, boolean includeComputed) { IEntityRegistry alreadyTraversed = createEntityRegistry("isAnyDirtyInDepth"); if (elements != null) { for (Object element : elements) { if (element instanceof IEntity) { if (isDirtyInDepth((IEntity) element, includeComputed, alreadyTraversed)) { return true; } } } } return false; } /** * {@inheritDoc} */ @Override public boolean isDirtyInDepth(IEntity entity) { return isDirtyInDepth(entity, false); } /** * {@inheritDoc} */ @Override public boolean isDirtyInDepth(IEntity entity, boolean includeComputed) { return isAnyDirtyInDepth(Collections.singleton(entity), includeComputed); } /** * {@inheritDoc} */ @Override public boolean isEntityRegisteredForDeletion(IEntity entity) { if (isUnitOfWorkActive()) { return unitOfWork.isEntityRegisteredForDeletion(entity); } else { return sessionUnitOfWork.isEntityRegisteredForDeletion(entity); } } /** * {@inheritDoc} */ @Override public boolean isEntityRegisteredForUpdate(IEntity entity) { if (isUnitOfWorkActive()) { return unitOfWork.isEntityRegisteredForUpdate(entity); } else { return sessionUnitOfWork.isEntityRegisteredForUpdate(entity); } } /** * {@inheritDoc} */ @Override public abstract boolean isInitialized(Object objectOrProxy); /** * {@inheritDoc} */ @Override public boolean isUnitOfWorkActive() { return unitOfWork.isActive(); } /** * {@inheritDoc} */ @Override public boolean isUpdatedInUnitOfWork(IEntity entity) { if (!isUnitOfWorkActive()) { throw new BackendException("Cannot access unit of work."); } return unitOfWork.isUpdated(entity); } /** * {@inheritDoc} */ @Override public <E extends IEntity> E merge(E entity, EMergeMode mergeMode) { return merge(Collections.singletonList(entity), mergeMode).get(0); } /** * {@inheritDoc} */ @Override public <E extends IEntity> List<E> merge(List<E> entities, EMergeMode mergeMode) { IEntityRegistry alreadyMerged = createEntityRegistry("merge"); Set<IEntity> eventsToRelease = new LinkedHashSet<>(); List<E> mergedList = new ArrayList<>(); boolean wasDirtyTrackingEnabled = isDirtyTrackingEnabled(); try { setDirtyTrackingEnabled(false); for (E entity : entities) { mergedList.add(merge(entity, mergeMode, alreadyMerged, eventsToRelease)); } } finally { boolean suspendUnitOfWork = isUnitOfWorkActive(); try { if (suspendUnitOfWork) { suspendUnitOfWork(); } for (IEntity entity : eventsToRelease) { try { entity.releaseEvents(); } catch (Throwable t) { LOG.error("An unexpected exception occurred when releasing events after a merge", t); } } } finally { if (suspendUnitOfWork) { resumeUnitOfWork(); } setDirtyTrackingEnabled(wasDirtyTrackingEnabled); } } return mergedList; } /** * {@inheritDoc} */ @Override public void recordAsSynchronized(IEntity flushedEntity) { if (isUnitOfWorkActive()) { Map<String, Object> hasActuallyBeenFlushed = getDirtyProperties(flushedEntity, false); if (hasActuallyBeenFlushed == null) { LOG.error("*BAD UOW USAGE* You are flushing an entity ({})[{}] that you have not cloned before in the UOW.\n" + "You should only work on entities copies you obtain using the " + "backendController.cloneInUnitOfWork(...) method.", new Object[]{flushedEntity, flushedEntity.getComponentContract().getSimpleName()}); throw new BackendException( "An entity has been flushed to the persistent store without being first registered" + " in the UOW : " + flushedEntity); } unitOfWork.clearDirtyState(flushedEntity); if (isEntityRegisteredForDeletion(flushedEntity)) { if (IEntity.DELETED_VERSION.equals(flushedEntity.getVersion())) { unitOfWork.addDeletedEntity(flushedEntity); } else { // To support in-memory TX deletions sessionUnitOfWork.registerForDeletion(flushedEntity); } } else if (!hasActuallyBeenFlushed.isEmpty()) { unitOfWork.addUpdatedEntity(flushedEntity); } } } /** * {@inheritDoc} */ @Override public void registerEntity(IEntity entity) { Map<String, Object> initialDirtyProperties = null; if (!entity.isPersistent()) { initialDirtyProperties = new HashMap<>(); for (Map.Entry<String, Object> property : entity.straightGetProperties().entrySet()) { String propertyName = property.getKey(); Object propertyValue = property.getValue(); if (propertyValue != null && !(propertyValue instanceof Collection<?> && ((Collection<?>) property.getValue()) .isEmpty())) { initialDirtyProperties.put(propertyName, null); } } } if (isUnitOfWorkActive()) { unitOfWork.register(entity, initialDirtyProperties); } else { sessionUnitOfWork.register(entity, initialDirtyProperties); } } /** * {@inheritDoc} */ @Override public void registerForDeletion(IEntity entity) { if (entity == null) { throw new IllegalArgumentException("Passed entity cannot be null"); } if (isUnitOfWorkActive()) { unitOfWork.registerForDeletion(entity); } else { sessionUnitOfWork.registerForDeletion(entity); } } /** * {@inheritDoc} */ @Override public void registerForUpdate(IEntity entity) { if (entity == null) { throw new IllegalArgumentException("Passed entity cannot be null"); } if (isUnitOfWorkActive()) { unitOfWork.registerForUpdate(entity); } else { sessionUnitOfWork.registerForUpdate(entity); } } /** * {@inheritDoc} */ @Override public ComponentTransferStructure<IComponent> retrieveComponents() { return transferStructure; } /** * {@inheritDoc} */ @Override public final void rollbackUnitOfWork() { if (!isUnitOfWorkActive()) { throw new BackendException("Cannot rollback a unit of work that has not begun."); } if (unitOfWork.hasNested()) { unitOfWork.rollback(); } else { doRollbackUnitOfWork(); } } /** * Performs actual UOW rollback. */ protected void doRollbackUnitOfWork() { unitOfWork.rollback(); } /** * Assigns the application session to this backend controller. This property * can only be set once and should only be used by the DI container. It will * rarely be changed from built-in defaults unless you need to specify a * custom implementation instance to be used. * * @param applicationSession * the applicationSession to set. */ public void setApplicationSession(IApplicationSession applicationSession) { this.applicationSession = applicationSession; } /** * Configures the entity clone factory used to carbon-copy entities. An entity * carbon-copy is an technical copy of an entity, including id and version but * excluding relationship properties. This mechanism is used by the controller * when duplicating entities into the UOW to allow for memory state aware * transactions. This property should only be used by the DI container. It * will rarely be changed from built-in defaults unless you need to specify a * custom implementation instance to be used. * * @param carbonEntityCloneFactory * the carbonEntityCloneFactory to set. */ public void setCarbonEntityCloneFactory(IEntityCloneFactory carbonEntityCloneFactory) { this.carbonEntityCloneFactory = carbonEntityCloneFactory; } /** * Configures the factory responsible for creating entities (or components) * collections that are held by domain relationship properties. This property * should only be used by the DI container. It will rarely be changed from * built-in defaults unless you need to specify a custom implementation * instance to be used. * * @param collectionFactory * the collectionFactory to set. */ public void setCollectionFactory(IComponentCollectionFactory collectionFactory) { this.collectionFactory = collectionFactory; } /** * Configures the entity factory to use to create new entities. Backend * controllers only accept instances of * {@code ControllerAwareProxyEntityFactory} or a subclass. This is * because the backend controller must keep track of created entities. * Jspresso entity implementations also use the controller from which they * were created behind the scene. * * @param entityFactory * the entityFactory to set. */ public void setEntityFactory(IEntityFactory entityFactory) { if (entityFactory != null && !(entityFactory instanceof ControllerAwareProxyEntityFactory)) { throw new IllegalArgumentException( "entityFactory must be a " + ControllerAwareProxyEntityFactory.class.getSimpleName()); } this.entityFactory = entityFactory; } /** * Configures the model connector factory to use to create new model * connectors. Connectors are adapters used by the binding layer to access * domain model values. * * @param modelConnectorFactory * the modelConnectorFactory to set. */ public void setModelConnectorFactory(IModelConnectorFactory modelConnectorFactory) { this.modelConnectorFactory = modelConnectorFactory; } /** * Assigns the Spring transaction template to this backend controller. This * property can only be set once and should only be used by the DI container. * It will rarely be changed from built-in defaults unless you need to specify * a custom implementation instance to be used. * <p/> * The configured instance is the one that will be returned by the * controller's {@code getTransactionTemplate()} method that should be * used by the service layer for transaction management. * * @param transactionTemplate * the transactionTemplate to set. */ public void setTransactionTemplate(TransactionTemplate transactionTemplate) { if (this.transactionTemplate != null) { throw new IllegalArgumentException("Spring transaction template can only be configured once."); } if (transactionTemplate != null && !(transactionTemplate instanceof ControllerAwareTransactionTemplate)) { throw new IllegalArgumentException("You have configured a transaction template that is not a controller " + "aware transaction template. This is not legal since this prevents " + "the Unit of Work to be synchronized with the current transaction."); } this.transactionTemplate = transactionTemplate; } /** * Creates a "Unit of Work" to be used by this controller. * * @return the created UOW. */ protected IEntityUnitOfWork createUnitOfWork() { return new BasicEntityUnitOfWork(); } /** * {@inheritDoc} */ @Override public boolean start(Locale startingLocale, TimeZone theClientTimeZone) { applicationSession.setLocale(startingLocale); setClientTimeZone(theClientTimeZone); return true; } /** * Sets client time zone. * * @param clientTimeZone * the client time zone */ @Override public void setClientTimeZone(TimeZone clientTimeZone) { this.clientTimeZone = clientTimeZone; } /** * Sets reference time zone id. * * @param referenceTimeZoneId * the reference time zone id */ public void setReferenceTimeZoneId(String referenceTimeZoneId) { if (referenceTimeZoneId != null) { this.referenceTimeZone = TimeZone.getTimeZone(referenceTimeZoneId); } else { this.referenceTimeZone = null; } } /** * {@inheritDoc} */ @Override public boolean stop() { // The application session can now be shared across async slave // controllers. // if (applicationSession != null) { // applicationSession.clear(); // } if (getUserPreferencesStore() != null) { getUserPreferencesStore().setStorePath(IPreferencesStore.GLOBAL_STORE); } if (sessionUnitOfWork != null) { sessionUnitOfWork.clear(); sessionUnitOfWork.begin(); } if (unitOfWork != null) { unitOfWork.clear(); } if (workspaceConnectors != null) { workspaceConnectors.clear(); } if (moduleConnectors != null) { moduleConnectors.clear(); } transferStructure = null; cleanupControllerAsyncActionsThreadGroup(); return true; } /** * {@inheritDoc} */ @Override public void storeComponents(ComponentTransferStructure<IComponent> components) { this.transferStructure = components; } /** * Creates a transient collection instance, in respect to the type of * collection passed as parameter. * * @param <E> the type parameter * @param collection the collection to take the type from (List, Set, ...) * @return a transient collection instance with the same interface type as the * parameter. */ protected <E> Collection<E> createTransientEntityCollection(Collection<E> collection) { Collection<E> uowEntityCollection = null; if (collection instanceof Set<?>) { uowEntityCollection = collectionFactory.createComponentCollection(Set.class); } else if (collection instanceof List<?>) { uowEntityCollection = collectionFactory.createComponentCollection(List.class); } return uowEntityCollection; } /** * {@inheritDoc} */ @Override public boolean isDirty(IEntity entity) { return isDirty(entity, false); } /** * {@inheritDoc} */ @Override public boolean isDirty(IEntity entity, boolean includeComputed) { if (entity == null) { return false; } Map<String, Object> entityDirtyProperties = getDirtyProperties(entity, includeComputed); if (entityDirtyProperties != null) { entityDirtyProperties.remove(IEntity.VERSION); } return entityDirtyProperties != null && !entityDirtyProperties.isEmpty(); } /** * Gets whether the entity property is dirty (has changes that need to be * updated to the persistent store). * * @param entity * the entity to test. * @param propertyName * the entity property to test. * @return true if the entity is dirty. */ @Override public boolean isDirty(IEntity entity, String propertyName) { if (entity == null) { return false; } Map<String, Object> entityDirtyProperties = getDirtyProperties(entity); if (entityDirtyProperties != null && entityDirtyProperties.containsKey(propertyName)) { IPropertyDescriptor propertyDescriptor = getEntityFactory().getComponentDescriptor(getComponentContract(entity)) .getPropertyDescriptor(propertyName); return propertyDescriptor != null && !propertyDescriptor.isComputed(); } return false; } /** * Gives a chance to the session to wrap a collection before making it part of * the unit of work. * * @param <E> the type parameter * @param owner the entity the collection belongs to. * @param detachedCollection the transient collection to make part of the unit of work. * @param snapshotCollection the original collection state as reported by the dirt recorder. * @param role the name of the property represented by the collection in its * owner. * @return the wrapped collection if any (it may be the collection itself as * in this implementation). */ protected <E> Collection<E> wrapDetachedCollection(IEntity owner, Collection<E> detachedCollection, Collection<E> snapshotCollection, String role) { return detachedCollection; } private IComponent cloneComponentInUnitOfWork(IComponent component, IEntityRegistry alreadyCloned, Set<IEntity> eventsToRelease) { IComponent uowComponent = carbonEntityCloneFactory.cloneComponent(component, entityFactory); Map<String, Object> componentProperties = component.straightGetProperties(); for (Map.Entry<String, Object> property : componentProperties.entrySet()) { String propertyName = property.getKey(); Object propertyValue = property.getValue(); if (propertyValue instanceof IEntity) { if (isInitialized(propertyValue)) { uowComponent.straightSetProperty(propertyName, cloneInUnitOfWork((IEntity) propertyValue, alreadyCloned, eventsToRelease)); } else { uowComponent.straightSetProperty(propertyName, cloneUninitializedProperty(uowComponent, propertyValue)); } } else if (propertyValue instanceof IComponent) { uowComponent.straightSetProperty(propertyName, cloneComponentInUnitOfWork((IComponent) propertyValue, alreadyCloned, eventsToRelease)); } } IComponent owningComponent = component.getOwningComponent(); if (owningComponent != null) { IComponent uowOwningComponent; if (owningComponent instanceof IEntity) { uowOwningComponent = cloneInUnitOfWork((IEntity) owningComponent, alreadyCloned, eventsToRelease); } else { uowOwningComponent = cloneComponentInUnitOfWork(owningComponent, alreadyCloned, eventsToRelease); } uowComponent.setOwningComponent(uowOwningComponent, owningComponent.getOwningPropertyDescriptor()); } return uowComponent; } @SuppressWarnings({"unchecked", "ConstantConditions"}) private <E extends IEntity> E cloneInUnitOfWork(E entity, IEntityRegistry alreadyCloned, Set<IEntity> eventsToRelease) { if (entity == null) { return null; } Class<? extends IEntity> entityContract = getComponentContract(entity); IComponentDescriptor<?> entityDescriptor = getEntityFactory().getComponentDescriptor(entityContract); E uowEntity = (E) alreadyCloned.get(entityContract, entity.getId()); if (uowEntity != null) { return uowEntity; } uowEntity = performUowEntityCloning(entity); boolean eventsBlocked = false; try { if (isInitialized(uowEntity)) { eventsBlocked = uowEntity.blockEvents(); } Map<String, Object> dirtyProperties; if (isInitialized(entity)) { dirtyProperties = unitOfWork.getParentDirtyProperties(entity, sessionUnitOfWork); if (dirtyProperties == null) { dirtyProperties = new HashMap<>(); } } else { dirtyProperties = new HashMap<>(); } alreadyCloned.register(entityContract, entity.getId(), uowEntity); if (isInitialized(entity)) { Map<String, Object> entityProperties = entity.straightGetProperties(); for (Map.Entry<String, Object> property : entityProperties.entrySet()) { String propertyName = property.getKey(); Object propertyValue = property.getValue(); IPropertyDescriptor propertyDescriptor = entityDescriptor.getPropertyDescriptor(propertyName); if (propertyValue instanceof IEntity) { if (isInitialized(propertyValue)) { uowEntity.straightSetProperty(propertyName, cloneInUnitOfWork((IEntity) propertyValue, alreadyCloned, eventsToRelease)); } else { uowEntity.straightSetProperty(propertyName, cloneUninitializedProperty(uowEntity, propertyValue)); } } else if (propertyValue instanceof Collection<?> // to support collections stored as java serializable blob. // and detachedEntities (see bug # 1130) && (propertyDescriptor == null || propertyDescriptor instanceof ICollectionPropertyDescriptor<?>)) { if (isInitialized(propertyValue)) { Collection<Object> uowCollection = createTransientEntityCollection( (Collection<Object>) property.getValue()); for (Object collectionElement : (Collection<?>) property.getValue()) { if (collectionElement != null) { if (collectionElement instanceof IEntity) { uowCollection.add(cloneInUnitOfWork((IEntity) collectionElement, alreadyCloned, eventsToRelease)); } else if (collectionElement instanceof IComponent) { uowCollection.add(cloneComponentInUnitOfWork((IComponent) collectionElement, alreadyCloned, eventsToRelease)); } else { uowCollection.add(collectionElement); } } else { uowCollection.add(null); } } if (propertyDescriptor == null || !propertyDescriptor.isComputed()) { Collection<Object> snapshotCollection = null; Object originalProperty = dirtyProperties.get(propertyName); // Workaround bug #1148 if (originalProperty != null && originalProperty instanceof Collection<?>) { snapshotCollection = (Collection<Object>) originalProperty; Collection<Object> clonedSnapshotCollection = createTransientEntityCollection(snapshotCollection); for (Object snapshotCollectionElement : snapshotCollection) { if (snapshotCollectionElement != null) { if (snapshotCollectionElement instanceof IEntity) { clonedSnapshotCollection.add(cloneInUnitOfWork((IEntity) snapshotCollectionElement, alreadyCloned, eventsToRelease)); } else if (snapshotCollectionElement instanceof IComponent) { clonedSnapshotCollection.add(cloneComponentInUnitOfWork((IComponent) snapshotCollectionElement, alreadyCloned, eventsToRelease)); } else { clonedSnapshotCollection.add(snapshotCollectionElement); } } else { clonedSnapshotCollection.add(null); } } snapshotCollection = clonedSnapshotCollection; } if (entity.isPersistent()) { uowCollection = wrapDetachedCollection(entity, uowCollection, snapshotCollection, propertyName); } } uowEntity.straightSetProperty(propertyName, uowCollection); } else { uowEntity.straightSetProperty(propertyName, cloneUninitializedProperty(uowEntity, propertyValue)); } } else if (propertyValue instanceof IEntity[]) { IEntity[] uowArray = new IEntity[((IEntity[]) propertyValue).length]; for (int i = 0; i < uowArray.length; i++) { uowArray[i] = cloneInUnitOfWork(((IEntity[]) propertyValue)[i], alreadyCloned, eventsToRelease); } uowEntity.straightSetProperty(propertyName, uowArray); } else if (propertyValue instanceof IComponent[]) { IComponent[] uowArray = new IComponent[((IComponent[]) property.getValue()).length]; for (int i = 0; i < uowArray.length; i++) { uowArray[i] = cloneComponentInUnitOfWork(((IComponent[]) propertyValue)[i], alreadyCloned, eventsToRelease); } uowEntity.straightSetProperty(propertyName, uowArray); } else if (propertyValue instanceof IComponent) { uowEntity.straightSetProperty(propertyName, cloneComponentInUnitOfWork((IComponent) propertyValue, alreadyCloned, eventsToRelease)); } } if (eventsBlocked && uowEntity != null && isInitialized(uowEntity)) { eventsToRelease.add(uowEntity); } unitOfWork.register(uowEntity, new HashMap<>(dirtyProperties)); if (uowEntity instanceof ILifecycleCapable) { ((ILifecycleCapable) uowEntity).onClone(entity); } } } finally { if (eventsBlocked && uowEntity != null && isInitialized(uowEntity)) { eventsToRelease.add(uowEntity); } } return uowEntity; } /** * Performs the actual entity cloning in unit of work. Gives a chance to * subclasses to override and take a better decision than just a deep carbon * copy. * * @param <E> * the actual entity type. * @param entity * the source entity. * @return the cloned entity. */ protected <E extends IEntity> E performUowEntityCloning(E entity) { E uowEntity = carbonEntityCloneFactory.cloneEntity(entity, entityFactory); return uowEntity; } private boolean isDirtyInDepth(IEntity entity, boolean includeComputed, IEntityRegistry alreadyTraversed) { alreadyTraversed.register(getComponentContract(entity), entity.getId(), entity); if (isDirty(entity, includeComputed)) { return true; } Map<String, Object> entityProps = entity.straightGetProperties(); for (Map.Entry<String, Object> property : entityProps.entrySet()) { Object propertyValue = property.getValue(); if (propertyValue instanceof IEntity) { if (isInitialized(propertyValue) && alreadyTraversed.get(getComponentContract((IEntity) propertyValue), ((IEntity) propertyValue).getId()) == null) { if (isDirtyInDepth((IEntity) propertyValue, includeComputed, alreadyTraversed)) { return true; } } } else if (propertyValue instanceof Collection<?>) { if (isInitialized(propertyValue)) { for (Object elt : ((Collection<?>) propertyValue)) { if (elt instanceof IEntity && alreadyTraversed.get(getComponentContract((IEntity) elt), ((IEntity) elt).getId()) == null) { if (isDirtyInDepth((IEntity) elt, includeComputed, alreadyTraversed)) { return true; } } } } } } return false; } @SuppressWarnings({"unchecked", "ConstantConditions"}) private <E extends IEntity> E merge(E entity, final EMergeMode mergeMode, IEntityRegistry alreadyMerged, Set<IEntity> eventsToRelease) { if (entity == null) { return null; } Class<? extends IEntity> entityContract = getComponentContract(entity); E alreadyMergedEntity = (E) alreadyMerged.get(entityContract, entity.getId()); if (alreadyMergedEntity != null) { return alreadyMergedEntity; } // Allow for merging eagerly dirty entities if (mergeMode != EMergeMode.MERGE_EAGER) { checkBadMergeUsage(entity); } E registeredEntity = null; boolean eventsBlocked = false; try { registeredEntity = (E) getRegisteredEntity(getComponentContract(entity), entity.getId()); boolean newlyRegistered = false; if (registeredEntity == null) { if (!isInitialized(entity)) { return mergeUninitializedEntity(entity); } registeredEntity = carbonEntityCloneFactory.cloneEntity(entity, entityFactory); if (mergeMode == EMergeMode.MERGE_EAGER) { sessionUnitOfWork.register(registeredEntity, getDirtyProperties(entity)); } else { sessionUnitOfWork.register(registeredEntity, null); } newlyRegistered = true; } else if (mergeMode == EMergeMode.MERGE_KEEP || ( (mergeMode == EMergeMode.MERGE_LAZY || mergeMode == EMergeMode.MERGE_CLEAN_LAZY) && !isInitialized(entity))) { alreadyMerged.register(entityContract, entity.getId(), registeredEntity); return registeredEntity; } else if (mergeMode == EMergeMode.MERGE_EAGER) { sessionUnitOfWork.register(registeredEntity, getDirtyProperties(entity)); } if (isInitialized(registeredEntity)) { eventsBlocked = registeredEntity.blockEvents(); } alreadyMerged.register(entityContract, entity.getId(), registeredEntity); if (newlyRegistered || (mergeMode != EMergeMode.MERGE_CLEAN_LAZY && mergeMode != EMergeMode.MERGE_LAZY) || registeredEntity.getVersion() == null || !registeredEntity.getVersion().equals(entity.getVersion())) { if (mergeMode == EMergeMode.MERGE_CLEAN_EAGER || mergeMode == EMergeMode.MERGE_CLEAN_LAZY || mergeMode == EMergeMode.MERGE_LAZY) { sessionUnitOfWork.clearDirtyState(entity); } IComponentDescriptor<?> entityDescriptor = getEntityFactory().getComponentDescriptor(getComponentContract( entity)); Map<String, Object> entityProperties = entity.straightGetProperties(); Map<String, Object> registeredEntityProperties = registeredEntity.straightGetProperties(); Map<String, Object> mergedProperties = new LinkedHashMap<>(); Set<String> propertiesToSort = new HashSet<>(); for (Map.Entry<String, Object> property : entityProperties.entrySet()) { String propertyName = property.getKey(); Object propertyValue = property.getValue(); IPropertyDescriptor propertyDescriptor = entityDescriptor.getPropertyDescriptor(propertyName); if (propertyValue instanceof IEntity) { // Do not take the registeredProperty from the // registeredEntityProperties. // It might be a different ID from the one we are trying to merge // Object registeredProperty = registeredEntityProperties // .get(propertyName); Object registeredProperty = getRegisteredEntity(getComponentContract((IEntity) propertyValue), ((IEntity) propertyValue).getId()); if (registeredProperty == null) { mergedProperties.put(propertyName, merge((IEntity) propertyValue, mergeMode, alreadyMerged, eventsToRelease)); } else { if (mergeMode == EMergeMode.MERGE_EAGER || mergeMode == EMergeMode.MERGE_LAZY) { if (isInitialized(propertyValue)) { initializePropertyIfNeeded(registeredEntity, propertyName); } else if (isInitialized(registeredProperty)) { initializePropertyIfNeeded(entity, propertyName); } } // Fix for bug #1023 : the referenced entity might have changed // int the TX. Merge must happen unconditionally. // if (isInitialized(registeredProperty)) { mergedProperties.put(propertyName, merge((IEntity) propertyValue, mergeMode, alreadyMerged, eventsToRelease)); } } else if (propertyValue instanceof Collection<?> // to support collections stored as java serializable blob. // and detachedEntities (see bug # 1130) && (propertyDescriptor == null || propertyDescriptor instanceof ICollectionPropertyDescriptor<?>)) { Collection<Object> registeredCollection = (Collection<Object>) registeredEntityProperties.get( propertyName); if (!newlyRegistered && (mergeMode == EMergeMode.MERGE_EAGER || mergeMode == EMergeMode.MERGE_LAZY)) { if (isInitialized(propertyValue)) { initializePropertyIfNeeded(registeredEntity, propertyName); } else if (isInitialized(registeredCollection)) { initializePropertyIfNeeded(entity, propertyName); } } if (isInitialized(registeredCollection)) { if (newlyRegistered && !isInitialized(propertyValue)) { // Must have another collection instance. // See bug #902 registeredCollection = cloneUninitializedProperty(registeredEntity, (Collection<Object>) propertyValue); } else { if (propertyValue instanceof List) { registeredCollection = collectionFactory.createComponentCollection(List.class); } else { registeredCollection = collectionFactory.createComponentCollection(Set.class); } initializePropertyIfNeeded(entity, propertyName); List<?> reusedComponentInstances = null; if (registeredEntityProperties.get(propertyName) != null) { reusedComponentInstances = new ArrayList<>( (Collection<?>) registeredEntityProperties.get(propertyName)); } int i = 0; for (Object collectionElement : (Collection<?>) propertyValue) { if (collectionElement instanceof IEntity) { registeredCollection.add(merge((IEntity) collectionElement, mergeMode, alreadyMerged, eventsToRelease)); } else if (collectionElement instanceof IComponent) { IComponent registeredComponent = null; // We must reuse component instances for binding to operate correctly. if (reusedComponentInstances != null && i < reusedComponentInstances.size()) { registeredComponent = (IComponent) reusedComponentInstances.get(i); } registeredCollection.add(mergeComponent((IComponent) collectionElement, registeredComponent, mergeMode, alreadyMerged, eventsToRelease)); i++; } else { registeredCollection.add(collectionElement); } } } Collection<?> mergedCollection = mergeCollection(propertyName, propertyValue, registeredEntity, registeredCollection); mergedProperties.put(propertyName, mergedCollection); if (isInitialized(registeredCollection)) { propertiesToSort.add(propertyName); } } } else if (propertyValue instanceof IComponent) { IComponent registeredComponent = (IComponent) registeredEntityProperties.get(propertyName); mergedProperties.put(propertyName, mergeComponent((IComponent) propertyValue, registeredComponent, mergeMode, alreadyMerged, eventsToRelease)); } else { mergedProperties.put(propertyName, propertyValue); } } registeredEntity.straightSetProperties(mergedProperties); for (String propertyToSort : propertiesToSort) { getEntityFactory().sortCollectionProperty(registeredEntity, propertyToSort); } } else if (mergeMode == EMergeMode.MERGE_CLEAN_LAZY) { // version has not evolved but we must still reset dirty properties in // case only versionControl false properties have changed. sessionUnitOfWork.clearDirtyState(entity); } if (registeredEntity instanceof ILifecycleCapable) { ((ILifecycleCapable) registeredEntity).onClone(entity); } return registeredEntity; } finally { if (eventsBlocked && registeredEntity != null && isInitialized(registeredEntity)) { eventsToRelease.add(registeredEntity); } } } /** * Merge non initialized entity. * * @param <E> * the actual entity type * @param entity * the entity * @return the merged entity */ protected <E extends IEntity> E mergeUninitializedEntity(E entity) { return entity; } /** * Merge collection. * * @param <E> * the actual entity type. * @param propertyName * the property name * @param propertyValue * the property value * @param registeredEntity * the registered entity * @param registeredCollection * the registered collection * @return the collection */ protected <E extends IEntity> Collection<?> mergeCollection(String propertyName, Object propertyValue, E registeredEntity, Collection<?> registeredCollection) { return registeredCollection; } private <E extends IEntity> void checkBadMergeUsage(E entity) { if (isUnitOfWorkActive()) { if (isInitialized(entity) && entity.isPersistent() && isDirty(entity)) { LOG.error( "*BAD MERGE USAGE* An attempt is made to merge a UOW dirty entity ({})[{}] to the application session.\n" + "This will break transaction isolation since, if the transaction is rolled back," + " the UOW dirty state will be kept.\n" + "Dirty UOW entities will be automatically merged whenever the transaction is committed.", entity, getComponentContract(entity).getSimpleName()); if (isThrowExceptionOnBadUsage()) { throw new BackendException("A bad usage has been detected on the backend controller." + "This is certainly an application coding problem. Please check the logs."); } } } } private IComponent mergeComponent(IComponent componentToMerge, IComponent registeredComponent, EMergeMode mergeMode, IEntityRegistry alreadyMerged, Set<IEntity> eventsToRelease) { IComponent varRegisteredComponent = registeredComponent; if (componentToMerge == null) { return null; } if (varRegisteredComponent == null) { varRegisteredComponent = carbonEntityCloneFactory.cloneComponent(componentToMerge, entityFactory); IComponent owningComponent = componentToMerge.getOwningComponent(); if (owningComponent != null) { IComponent uowOwningComponent; if (owningComponent instanceof IEntity) { uowOwningComponent = merge((IEntity) owningComponent, mergeMode, alreadyMerged, eventsToRelease); } else { uowOwningComponent = mergeComponent(owningComponent, null, mergeMode, alreadyMerged, eventsToRelease); } varRegisteredComponent.setOwningComponent(uowOwningComponent, owningComponent.getOwningPropertyDescriptor()); } } else if (mergeMode == EMergeMode.MERGE_KEEP) { return varRegisteredComponent; } Map<String, Object> componentPropertiesToMerge = componentToMerge.straightGetProperties(); Map<String, Object> registeredComponentProperties = varRegisteredComponent.straightGetProperties(); Map<String, Object> mergedProperties = new HashMap<>(); for (Map.Entry<String, Object> property : componentPropertiesToMerge.entrySet()) { String propertyName = property.getKey(); Object propertyValue = property.getValue(); if (propertyValue instanceof IEntity) { // Do not take the registeredProperty from the // registeredEntityProperties. // It might be a different ID from the one we are trying to merge // Object registeredProperty = registeredEntityProperties // .get(propertyName); Object registeredProperty = getRegisteredEntity(getComponentContract((IEntity) propertyValue), ((IEntity) propertyValue).getId()); if (registeredProperty == null) { mergedProperties.put(propertyName, merge((IEntity) propertyValue, mergeMode, alreadyMerged, eventsToRelease)); } else { if (mergeMode == EMergeMode.MERGE_EAGER || mergeMode == EMergeMode.MERGE_LAZY) { if (isInitialized(propertyValue)) { initializePropertyIfNeeded(varRegisteredComponent, propertyName); } else if (isInitialized(registeredProperty)) { initializePropertyIfNeeded(componentToMerge, propertyName); } } // Fix for bug #1023 : the referenced entity might have changed // int the TX. Merge must happen unconditionally. // if (isInitialized(registeredProperty)) { mergedProperties.put(propertyName, merge((IEntity) propertyValue, mergeMode, alreadyMerged, eventsToRelease)); } } else if (propertyValue instanceof IComponent) { IComponent registeredSubComponent = (IComponent) registeredComponentProperties.get(propertyName); mergedProperties.put(propertyName, mergeComponent((IComponent) propertyValue, registeredSubComponent, mergeMode, alreadyMerged, eventsToRelease)); } else { mergedProperties.put(propertyName, propertyValue); } } varRegisteredComponent.straightSetProperties(mergedProperties); return varRegisteredComponent; } /** * Performs necessary cleanings when an entity or component is deleted. * * @param component * the deleted entity or component. * @param dryRun * set to true to simulate before actually doing it. * @throws IllegalAccessException * whenever this kind of exception occurs. * @throws InvocationTargetException * whenever this kind of exception occurs. * @throws NoSuchMethodException * whenever this kind of exception occurs. */ @Override public void cleanRelationshipsOnDeletion(IComponent component, boolean dryRun) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { Set<IComponent> clearedEntities = new HashSet<>(); Map<IComponent, RuntimeException> integrityViolations = new HashMap<>(); cleanRelationshipsOnDeletion(component, dryRun, clearedEntities, integrityViolations); // Throw exceptions for entities that have not been cleared during the // process. for (Map.Entry<IComponent, RuntimeException> integrityViolation : integrityViolations.entrySet()) { if (!(clearedEntities.contains(integrityViolation.getKey()))) { throw integrityViolation.getValue(); } } } @SuppressWarnings({"unchecked", "ThrowableResultOfMethodCallIgnored", "ConstantConditions", "SuspiciousMethodCalls"}) private void cleanRelationshipsOnDeletion(IComponent componentOrProxy, boolean dryRun, Set<IComponent> clearedEntities, Map<IComponent, RuntimeException> integrityViolations) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { if (componentOrProxy == null) { return; } IComponent component; component = (IComponent) unwrapProxy(componentOrProxy); Class<? extends IComponent> componentContract = getComponentContract(component); if (clearedEntities.contains(component)) { return; } clearedEntities.add(component); if (!dryRun) { if (component instanceof IEntity) { registerForDeletion((IEntity) component); } } try { component.setPropertyProcessorsEnabled(false); IComponentDescriptor<?> componentDescriptor = getEntityFactory().getComponentDescriptor(componentContract); for (Map.Entry<String, Object> property : component.straightGetProperties().entrySet()) { String propertyName = property.getKey(); Object propertyValue = property.getValue(); if (propertyValue != null) { IPropertyDescriptor propertyDescriptor = componentDescriptor.getPropertyDescriptor(propertyName); if (propertyDescriptor instanceof IRelationshipEndPropertyDescriptor) { // force initialization of relationship property. getAccessorFactory().createPropertyAccessor(propertyName, componentContract).getValue(component); if (propertyDescriptor instanceof IReferencePropertyDescriptor && propertyValue instanceof IEntity) { if (((IRelationshipEndPropertyDescriptor) propertyDescriptor).isComposition()) { cleanRelationshipsOnDeletion((IEntity) propertyValue, dryRun, clearedEntities, integrityViolations); } else { if (((IRelationshipEndPropertyDescriptor) propertyDescriptor).getReverseRelationEnd() != null) { IPropertyDescriptor reversePropertyDescriptor = ((IReferencePropertyDescriptor<?>) propertyDescriptor) .getReverseRelationEnd(); //noinspection SuspiciousMethodCalls if (!clearedEntities.contains(propertyValue)) { try { if (reversePropertyDescriptor instanceof IReferencePropertyDescriptor) { if (dryRun) { // manually trigger reverse relations preprocessors. reversePropertyDescriptor.preprocessSetter(propertyValue, null); } else { getAccessorFactory().createPropertyAccessor(reversePropertyDescriptor.getName(), getComponentContract(((IComponent) propertyValue))).setValue(propertyValue, null); // Technically reset to original value to avoid // Hibernate not-null checks component.straightSetProperty(propertyName, propertyValue); } } else if (reversePropertyDescriptor instanceof ICollectionPropertyDescriptor<?>) { if (dryRun) { // manually trigger reverse relations preprocessors. Collection<Object> reverseCollection = getAccessorFactory().createPropertyAccessor( reversePropertyDescriptor.getName(), getComponentContract(((IComponent) propertyValue))) .getValue(propertyValue); ((ICollectionPropertyDescriptor<?>) reversePropertyDescriptor).preprocessRemover( propertyValue, reverseCollection, component); } else { getAccessorFactory().createCollectionPropertyAccessor(reversePropertyDescriptor.getName(), getComponentContract(((IComponent) propertyValue)), componentContract).removeFromValue( propertyValue, component); // but technically reset to original value to avoid // Hibernate not-null checks component.straightSetProperty(propertyName, propertyValue); } } } catch (RuntimeException ex) { integrityViolations.put((IComponent) propertyValue, ex); } } } } } else if (propertyDescriptor instanceof ICollectionPropertyDescriptor) { if (((ICollectionPropertyDescriptor<?>) propertyDescriptor).isComposition()) { for (Object composedElement : new ArrayList<>((Collection<?>) propertyValue)) { if (composedElement instanceof IComponent) { cleanRelationshipsOnDeletion((IComponent) composedElement, dryRun, clearedEntities, integrityViolations); } } } else if (propertyDescriptor.isModifiable() && !((Collection<?>) propertyValue).isEmpty()) { if (((ICollectionPropertyDescriptor<?>) propertyDescriptor).getReverseRelationEnd() != null) { IPropertyDescriptor reversePropertyDescriptor = ((ICollectionPropertyDescriptor<?>) propertyDescriptor) .getReverseRelationEnd(); for (Object collectionElement : new ArrayList<>((Collection<?>) propertyValue)) { if (collectionElement instanceof IComponent && !clearedEntities.contains(collectionElement)) { try { if (reversePropertyDescriptor instanceof IReferencePropertyDescriptor) { if (dryRun) { // manually trigger reverse relations preprocessors. reversePropertyDescriptor.preprocessSetter(collectionElement, null); } else { getAccessorFactory().createPropertyAccessor(reversePropertyDescriptor.getName(), getComponentContract((IComponent) collectionElement)).setValue(collectionElement, null); } } else if (reversePropertyDescriptor instanceof ICollectionPropertyDescriptor<?>) { if (dryRun) { // manually trigger reverse relations preprocessors. Collection<Object> reverseCollection = getAccessorFactory() .createPropertyAccessor(reversePropertyDescriptor.getName(), getComponentContract( (IComponent) collectionElement)).getValue(collectionElement); ((ICollectionPropertyDescriptor<?>) reversePropertyDescriptor).preprocessRemover( collectionElement, reverseCollection, component); } else { getAccessorFactory().createCollectionPropertyAccessor(reversePropertyDescriptor.getName(), getComponentContract((IComponent) collectionElement), componentContract).removeFromValue(collectionElement, component); } } } catch (RuntimeException ex) { integrityViolations.put((IComponent) collectionElement, ex); } } } } } } } } } } finally { component.setPropertyProcessorsEnabled(true); } } /** * Unwrap ORM proxy if needed. * * @param componentOrProxy * the component or proxy. * @return the proxy implementation if it's an ORM proxy. */ protected Object unwrapProxy(Object componentOrProxy) { return componentOrProxy; } /** * Clones an uninitialized (proxied) property. * * @param <E> the type parameter * @param owner the property owner. * @param propertyValue the propertyValue. * @return the property clone. */ protected <E> E cloneUninitializedProperty(Object owner, E propertyValue) { return propertyValue; } /** * {@inheritDoc} */ @Override public void loggedIn(Subject subject) { getApplicationSession().setSubject(subject); String userPreferredLanguageCode = (String) getApplicationSession().getPrincipal().getCustomProperty( UserPrincipal.LANGUAGE_PROPERTY); if (userPreferredLanguageCode != null) { getApplicationSession().setLocale(LocaleUtils.toLocale(userPreferredLanguageCode)); } if (getUserPreferencesStore() != null) { getUserPreferencesStore().setStorePath(getApplicationSession().getUsername()); } } /** * Reads a user preference. * * @param key * the key under which the preference as been stored. * @return the stored preference or null. */ @Override public String getUserPreference(String key) { if (getUserPreferencesStore() != null) { return getUserPreferencesStore().getPreference(key); } return null; } /** * Stores a user preference. * * @param key * the key under which the preference as to be stored. * @param value * the value of the preference to be stored. */ @Override public void putUserPreference(String key, String value) { if (getUserPreferencesStore() != null) { getUserPreferencesStore().putPreference(key, value); } } /** * Deletes a user preference. * * @param key * the key under which the preference is stored. */ @Override public void removeUserPreference(String key) { if (getUserPreferencesStore() != null) { getUserPreferencesStore().removePreference(key); } } /** * Gets the user preferences store. * * @return the user preferences store. */ protected IPreferencesStore getUserPreferencesStore() { return userPreferencesStore; } /** * Sets the user preference store. * * @param userPreferencesStore * the userPreferenceStore to set. */ public void setUserPreferencesStore(IPreferencesStore userPreferencesStore) { this.userPreferencesStore = userPreferencesStore; } /** * Configures the translation provider used to compute internationalized * messages and labels. * * @param translationProvider * the translationProvider to set. */ public void setTranslationProvider(ITranslationProvider translationProvider) { this.translationProvider = translationProvider; } /** * Delegates to the translation provider. * <p/> * {@inheritDoc} */ @Override public String getTranslation(String key, Locale locale) { if (customTranslationPlugin != null) { String translation = customTranslationPlugin.getTranslation(key, locale, getApplicationSession()); if (translation != null) { return translation; } } return translationProvider.getTranslation(key, locale); } /** * Delegates to the translation provider. * <p/> * {@inheritDoc} */ @Override public String getTranslation(String key, Object[] args, Locale locale) { if (customTranslationPlugin != null) { String translation = customTranslationPlugin.getTranslation(key, args, locale, getApplicationSession()); if (translation != null) { return translation; } } return translationProvider.getTranslation(key, args, locale); } /** * Delegates to the translation provider. * <p/> * {@inheritDoc} */ @Override public String getTranslation(String key, String defaultMessage, Locale locale) { if (customTranslationPlugin != null) { String translation = customTranslationPlugin.getTranslation(key, locale, getApplicationSession()); if (translation != null) { return translation; } } return translationProvider.getTranslation(key, defaultMessage, locale); } /** * Delegates to the translation provider. * <p/> * {@inheritDoc} */ @Override public String getTranslation(String key, Object[] args, String defaultMessage, Locale locale) { if (customTranslationPlugin != null) { String translation = customTranslationPlugin.getTranslation(key, args, locale, getApplicationSession()); if (translation != null) { return translation; } } return translationProvider.getTranslation(key, args, defaultMessage, locale); } /** * {@inheritDoc} */ @Override public boolean isAccessGranted(ISecurable securable) { if (SecurityHelper.isSubjectGranted(getApplicationSession().getSubject(), securable)) { if (customSecurityPlugin != null) { try { pushToSecurityContext(securable); Map<String, Object> securityContext = new HashMap<>(); if (getApplicationSession() != null && getApplicationSession().getPrincipal() != null) { securityContext.put(SecurityContextConstants.USER_ROLES, SecurityHelper.getRoles( getApplicationSession().getSubject())); securityContext.put(SecurityContextConstants.USER_ID, getApplicationSession().getUsername()); Map<String, Object> sessionProperties = getApplicationSession().getCustomValues(); sessionProperties.putAll(getApplicationSession().getPrincipal().getCustomProperties()); securityContext.put(SecurityContextConstants.SESSION_PROPERTIES, sessionProperties); } securityContext.putAll(getSecurityContext()); return customSecurityPlugin.isAccessGranted(securable, securityContext); } finally { restoreLastSecurityContextSnapshot(); } } return true; } return false; } /** * Configures a custom security plugin on the controller. The controller * itself is a security handler and is used as such across most of the * application layers. Before delegating to the custom security handler, the * controller will apply role-based security rules that cannot be disabled. * * @param customSecurityPlugin * the customESecurityHandler to set. */ public void setCustomSecurityPlugin(ISecurityPlugin customSecurityPlugin) { this.customSecurityPlugin = customSecurityPlugin; } /** * {@inheritDoc} */ @Override public Map<String, Object> getSecurityContext() { return securityContextBuilder.getSecurityContext(); } /** * {@inheritDoc} */ @Override public ISecurityContextBuilder pushToSecurityContext(Object contextElement) { securityContextBuilder.pushToSecurityContext(contextElement); return this; } /** * {@inheritDoc} */ @Override public ISecurityContextBuilder restoreLastSecurityContextSnapshot() { securityContextBuilder.restoreLastSecurityContextSnapshot(); return this; } /** * Configures a custom translation plugin on the controller. The controller * itself is a translation provider and is used as such across most of the * application layers. The custom translation plugin is used to override the * default static, bundle-based, i18n scheme. * * @param customTranslationPlugin * the customTranslationPlugin to set. */ public void setCustomTranslationPlugin(ITranslationPlugin customTranslationPlugin) { this.customTranslationPlugin = customTranslationPlugin; } /** * {@inheritDoc} */ @Override public TimeZone getReferenceTimeZone() { if (referenceTimeZone != null) { return referenceTimeZone; } return TimeZone.getDefault(); } /** * {@inheritDoc} */ @Override public TimeZone getClientTimeZone() { if (clientTimeZone != null) { return clientTimeZone; } return TimeZone.getDefault(); } /** * {@inheritDoc} */ @Override public void cleanupRequestResources() { // Empty implementation } /** * Gets the throwExceptionOnBadUsage. * * @return the throwExceptionOnBadUsage. */ public boolean isThrowExceptionOnBadUsage() { return throwExceptionOnBadUsage; } /** * Configures the backend controller to throw or not an exception whenever a * bad usage is detected like manually merging a dirty entity from an ongoing * UOW. * * @param throwExceptionOnBadUsage * the throwExceptionOnBadUsage to set. */ public void setThrowExceptionOnBadUsage(boolean throwExceptionOnBadUsage) { this.throwExceptionOnBadUsage = throwExceptionOnBadUsage; } /** * Performs necessary checks in order to ensure isolation on unit of work. * <p/> * {@inheritDoc} */ @Override public Object sanitizeModifierParam(Object target, IPropertyDescriptor propertyDescriptor, Object param) { if (propertyDescriptor.isComputed()) { // do not perform any check regarding computed properties. return param; } if (param instanceof Collection<?>) { for (Object element : (Collection<?>) param) { // the return value is not leveraged. sanitizeModifierParam(target, propertyDescriptor, element); } return param; } Class<?> targetClass; IEntity targetEntity = null; IEntity sessionTargetEntity = null; IEntity paramEntity = null; IEntity sessionParamEntity = null; if (target instanceof IComponent) { targetEntity = refineEntity((IComponent) target); if (targetEntity != null) { sessionTargetEntity = getRegisteredEntity(getComponentContract(targetEntity), targetEntity.getId()); } } if (param instanceof IComponent) { paramEntity = refineEntity((IComponent) param); if (paramEntity != null) { sessionParamEntity = getRegisteredEntity(getComponentContract(paramEntity), paramEntity.getId()); } } if (target instanceof IComponent) { targetClass = getComponentContract(((IComponent) target)); } else { targetClass = target.getClass(); } if (isUnitOfWorkActive()) { if (targetEntity != null && objectEquals(targetEntity, sessionTargetEntity)) { // We are modifying on a session entity inside a unit of work. This is // not legal. LOG.error("*BAD UOW USAGE* You are modifying a session registered entity ({})[{}] inside an ongoing UOW.\n" + "You should only work on entities copies you obtain using the " + "backendController.cloneInUnitOfWork(...) method.\n" + "The property being modified is [{}].", targetEntity, getComponentContract(targetEntity).getSimpleName(), propertyDescriptor.getName()); if (isThrowExceptionOnBadUsage()) { throw new BackendException( "An invalid modification on a session entity has been detected while having an active Unit of Work. " + "Please check the logs."); } } if (paramEntity != null && objectEquals(paramEntity, sessionParamEntity)) { // We are linking an entity with a session entity inside a unit of work. // This is not legal. LOG.error( "*BAD UOW USAGE* You are linking an entity ({})[{}] with a session entity ({})[{}] inside an ongoing UOW.\n" + "You should only work on entities copies you obtain using the " + "backendController.cloneInUnitOfWork(...) method\n" + "The property being modified is [{}].", target, targetClass.getSimpleName(), paramEntity, getComponentContract(paramEntity).getSimpleName(), propertyDescriptor.getName()); if (isThrowExceptionOnBadUsage()) { throw new BackendException( "An invalid usage of a session entity has been detected while having an active Unit of Work. " + "Please check the logs."); } } } else { if (targetEntity != null && !objectEquals(targetEntity, sessionTargetEntity)) { // We are working on an entity that has not been registered in the // session. This is not legal. LOG.error( "*BAD SESSION USAGE* You are modifying an entity ({})[{}] that has not been previously merged in the " + "session.\n" + "You should 1st merge your entities in the session by using the " + "backendController.merge(...) method.\n" + "The property being modified is [{}].", targetEntity, getComponentContract(targetEntity).getSimpleName(), propertyDescriptor.getName()); if (isThrowExceptionOnBadUsage()) { throw new BackendException( "An invalid modification of an entity that was not previously registered in the session has been " + "detected. " + "Please check the logs."); } } if (paramEntity != null && !objectEquals(paramEntity, sessionParamEntity)) { // We are linking an entity with another one that has not been // registered in the session. This is not legal. LOG.error("*BAD SESSION USAGE* You are linking an entity ({})[{}] with another one ({})[{}] " + "that has not been previously merged in the session.\n" + "You should 1st merge your entities in the session by using the " + "backendController.merge(...) method.\n" + "The property being modified is [{}].", target, targetClass.getSimpleName(), paramEntity, getComponentContract(paramEntity).getSimpleName(), propertyDescriptor.getName()); if (isThrowExceptionOnBadUsage()) { throw new BackendException( "An invalid usage of an entity that was not previously registered in the session has been detected. " + "Please check the logs."); } } } return param; } private IEntity refineEntity(IComponent target) { return refineEntity(target, new IdentityHashMap<IComponent, Object>()); } private IEntity refineEntity(IComponent target, IdentityHashMap<IComponent, Object> traversed) { if (traversed.containsKey(target)) { return null; } traversed.put(target, null); if (target instanceof IEntity) { return (IEntity) target; } if (target != null) { return refineEntity(target.getOwningComponent(), traversed); } return null; } /** * Checks object equality between 2 entities ignoring any implementation * details like proxy optimisation. * * @param e1 * the 1st entity. * @param e2 * the 2nd entity. * @return true if both entity are object equal. */ protected boolean objectEquals(IEntity e1, IEntity e2) { return e1 == e2; } /** * Hook to allow subclasses to determine component contract without * initializing it. * * @param <E> * the actual component type. * @param component * the component to get the component contract for. * @return the component contract. */ @SuppressWarnings("unchecked") protected <E extends IComponent> Class<? extends E> getComponentContract(E component) { return (Class<? extends E>) component.getComponentContract(); } /** * Gets the slaveControllerFactory. * * @return the slaveControllerFactory. */ protected IBackendControllerFactory getSlaveControllerFactory() { return slaveControllerFactory; } /** * Sets the slaveControllerFactory. * * @param slaveControllerFactory * the slaveControllerFactory to set. */ public void setSlaveControllerFactory(IBackendControllerFactory slaveControllerFactory) { this.slaveControllerFactory = slaveControllerFactory; } /** * {@inheritDoc} */ @Override public Set<AsyncActionExecutor> getRunningExecutors() { if (controllerAsyncActionsThreadGroup != null) { int activeCount = controllerAsyncActionsThreadGroup.activeCount(); AsyncActionExecutor[] activeExecutors = new AsyncActionExecutor[activeCount]; controllerAsyncActionsThreadGroup.enumerate(activeExecutors); return new LinkedHashSet<>(Arrays.asList(activeExecutors)); } return Collections.emptySet(); } /** * {@inheritDoc} */ @Override public Set<AsyncActionExecutor> getCompletedExecutors() { Set<AsyncActionExecutor> completedExecutors = new LinkedHashSet<>(asyncExecutors); completedExecutors.removeAll(getRunningExecutors()); return completedExecutors; } /** * {@inheritDoc} */ @Override public void purgeCompletedExecutors() { Set<AsyncActionExecutor> oldValue = new LinkedHashSet<>(getCompletedExecutors()); asyncExecutors.removeAll(getCompletedExecutors()); firePropertyChange("completedExecutors", oldValue, getCompletedExecutors()); } /** * Gets the asyncExecutorsMaxCount. * * @param context * the action context. * @return the asyncExecutorsMaxCount. */ @SuppressWarnings("UnusedParameters") protected int getAsyncExecutorsMaxCount(Map<String, Object> context) { return asyncExecutorsMaxCount; } /** * Configures the maximum count of concurrent asynchronous action executors. * It defaults to {@code 10}. * * @param asyncExecutorsMaxCount * the asyncExecutorsMaxCount to set. */ public void setAsyncExecutorsMaxCount(int asyncExecutorsMaxCount) { this.asyncExecutorsMaxCount = asyncExecutorsMaxCount; } /** * Creates an entity registry. * * @param name * the entity registry name. * @return a new entity registry. */ protected final IEntityRegistry createEntityRegistry(String name) { return createEntityRegistry(name, new HashMap<Class<? extends IEntity>, Map<Serializable, IEntity>>()); } /** * Creates an entity registry. * * @param name the entity registry name. * @param backingStore the backing store * @return a new entity registry. */ protected IEntityRegistry createEntityRegistry(String name, Map<Class<? extends IEntity>, Map<Serializable, IEntity>> backingStore) { return new BasicEntityRegistry(name, backingStore); } /** * {@inheritDoc} */ @Override public void suspend() { // NO-OP } /** * {@inheritDoc} */ @Override public void resume() { // NO-OP } /** * {@inheritDoc} */ @Override public void flush() { // NO-OP } /** * {@inheritDoc} */ @Override public void beforeCommit(boolean readOnly) { // NO-OP } /** * {@inheritDoc} */ @Override public void beforeCompletion() { // NO-OP } /** * {@inheritDoc} */ @Override public void afterCommit() { // NO-OP } /** * {@inheritDoc} */ @Override public void afterCompletion(int status) { if (status == STATUS_COMMITTED) { commitUnitOfWork(); } else { rollbackUnitOfWork(); } } /** * {@inheritDoc} */ @Override public boolean isDirtyTrackingEnabled() { // Since both session and UOW dirty tracker are synchronized, we can only query session one. return sessionUnitOfWork.isDirtyTrackingEnabled(); } /** * {@inheritDoc} */ @Override public void setDirtyTrackingEnabled(boolean enabled) { // Both session AND UOW dirty tracker should be disabled at the same time. See bug #jspresso-ce-21. sessionUnitOfWork.setDirtyTrackingEnabled(enabled); if (isUnitOfWorkActive()) { unitOfWork.setDirtyTrackingEnabled(enabled); } } /** * {@inheritDoc}. */ @Override public void addDirtInterceptor(PropertyChangeListener interceptor) { if (isUnitOfWorkActive()) { unitOfWork.addDirtInterceptor(interceptor); } else { sessionUnitOfWork.addDirtInterceptor(interceptor); } } /** * {@inheritDoc}. */ @Override public void removeDirtInterceptor(PropertyChangeListener interceptor) { if (isUnitOfWorkActive()) { unitOfWork.removeDirtInterceptor(interceptor); } else { sessionUnitOfWork.removeDirtInterceptor(interceptor); } } /** * Suspend unit of work. */ protected void suspendUnitOfWork() { unitOfWork.suspend(); } /** * Resume unit of work. */ protected void resumeUnitOfWork() { unitOfWork.resume(); } /** * Delegates to the master controller if any. * * @param action * the action * @param context * the context */ @Override public synchronized void executeLater(IAction action, Map<String, Object> context) { if (masterController != null) { masterController.executeLater(action, context); } else { super.executeLater(action, context); } } private List<IEntity> recordedMergedEntities; /** * Record uow merged entities for later reuse. */ public void recordUowMergedEntities() { recordedMergedEntities = new ArrayList<>(); } /** * Gets recorded uow merged entities. * * @return the recorded uow merged entities */ public List<IEntity> getRecordedUowMergedEntitiesAndClear() { List<IEntity> copy = recordedMergedEntities; recordedMergedEntities = null; return copy; } /** * Sets async actions thread group. * * @param asyncActionsThreadGroup * the async actions thread group */ public void setAsyncActionsThreadGroup(ThreadGroup asyncActionsThreadGroup) { this.asyncActionsThreadGroup = asyncActionsThreadGroup; } }