/*
* 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.mongo;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.jspresso.framework.application.backend.AbstractBackendController;
import org.jspresso.framework.application.backend.BackendException;
import org.jspresso.framework.application.backend.session.EMergeMode;
import org.jspresso.framework.model.component.IComponent;
import org.jspresso.framework.model.descriptor.IComponentDescriptor;
import org.jspresso.framework.model.descriptor.IPropertyDescriptor;
import org.jspresso.framework.model.descriptor.IRelationshipEndPropertyDescriptor;
import org.jspresso.framework.model.entity.IEntity;
import org.jspresso.framework.model.persistence.mongo.JspressoMongoEntityProxy;
import org.jspresso.framework.model.persistence.mongo.JspressoMongoProxy;
/**
* This is the default Jspresso implementation of MongoDB-based backend
* controller.
*
* @author Vincent Vandenschrick
* @version $LastChangedRevision : 10441 $
*/
public class MongoBackendController extends AbstractBackendController {
private MongoTemplate mongoTemplate;
private static final Logger LOG = LoggerFactory.getLogger(MongoBackendController.class);
private Set<IEntity> flushedEntities;
/**
* Flushes all changes to Mongo which is not transactional anyway...
*
* @param readOnly
* the read only
*/
@Override
public void beforeCommit(boolean readOnly) {
// flushed entities must be recorded, because MongoDB is not transactional.
if (flushedEntities == null) {
flushedEntities = new HashSet<>();
}
for (Map<Serializable, IEntity> uowEntities : getUnitOfWorkEntities().values()) {
for (IEntity uowEntity : uowEntities.values()) {
if (isEntityRegisteredForDeletion(uowEntity)) {
if (readOnly) {
throw new BackendException("The transaction is read-only but would lead to a flush in the database.");
}
getMongoTemplate().remove(uowEntity);
flushedEntities.add(uowEntity);
} else if (isDirtyInDepth(uowEntity)) {
if (readOnly) {
throw new BackendException("The transaction is read-only but would lead to a flush in the database.");
}
getMongoTemplate().save(uowEntity);
flushedEntities.add(uowEntity);
}
}
}
super.beforeCommit(readOnly);
}
/**
* Cleans flushed entities.
*
* @param status
* the status
*/
@Override
public void afterCompletion(int status) {
if (status == STATUS_ROLLED_BACK) {
// In case of rollback, we must still merge back flushed entities, since MongoDB is not transactional.
if (flushedEntities != null) {
for (IEntity updatedEntity : flushedEntities) {
recordAsSynchronized(updatedEntity);
}
mergeBackFlushedEntities(flushedEntities);
}
}
flushedEntities = null;
super.afterCompletion(status);
}
/**
* Initialize property if needed.
*
* @param componentOrEntity
* the component or entity
* @param propertyName
* the property name
*/
@Override
public void initializePropertyIfNeeded(IComponent componentOrEntity, String propertyName) {
Object propertyValue = componentOrEntity.straightGetProperty(propertyName);
if (!isInitialized(propertyValue)) {
((JspressoMongoProxy) propertyValue).initialize();
if (propertyValue instanceof JspressoMongoEntityProxy) {
if (((JspressoMongoEntityProxy) propertyValue).isNull()) {
componentOrEntity.straightSetProperty(propertyName, null);
}
} else if (propertyValue instanceof Collection<?>) {
for (Iterator<?> ite = ((Collection<?>) propertyValue).iterator(); ite.hasNext(); ) {
Object collectionElement = ite.next();
if (collectionElement instanceof IEntity) {
if (isEntityRegisteredForDeletion((IEntity) collectionElement)) {
ite.remove();
}
}
}
}
}
}
/**
* Is initialized.
*
* @param objectOrProxy
* the object or proxy
* @return the boolean
*/
@Override
public boolean isInitialized(Object objectOrProxy) {
return !(objectOrProxy instanceof JspressoMongoProxy) || ((JspressoMongoProxy) objectOrProxy).isInitialized();
}
/**
* Finds an entity by ID.
*
* @param <T>
* the entity type to return
* @param id
* the entity ID.
* @param mergeMode
* the merge mode to use when merging back retrieved entities or null
* if no merge is requested.
* @param clazz
* the type of the entity.
* @return the found entity
*/
@SuppressWarnings("unchecked")
public <T extends IEntity> T findById(final Serializable id, final EMergeMode mergeMode,
final Class<? extends T> clazz) {
T res;
if (isUnitOfWorkActive()) {
// merge mode must be ignored if a transaction is pre-existing.
res = cloneInUnitOfWork(getMongoTemplate().findById(id, clazz));
} else {
// merge mode is used for merge to occur inside the transaction.
res = getTransactionTemplate().execute(new TransactionCallback<T>() {
@SuppressWarnings("unchecked")
@Override
public T doInTransaction(TransactionStatus status) {
return merge(getMongoTemplate().findById(id, clazz), mergeMode);
}
});
}
return res;
}
/**
* Search Mongo using query. The result is then merged into session unless the method is called into a
* pre-existing transaction, in which case, the merge mode is ignored and the merge is not performed.
*
* @param <T>
* the entity type to return
* @param query
* the Mongo query.
* @param mergeMode
* the merge mode to use when merging back retrieved entities or null
* if no merge is requested.
* @param clazz
* the type of the entity.
* @return the first found entity or null
*/
public <T extends IEntity> T findFirstByQuery(Query query, EMergeMode mergeMode, Class<? extends T> clazz) {
List<T> ret = findByQuery(query, 0, 1, mergeMode, clazz);
if (ret != null && !ret.isEmpty()) {
return ret.get(0);
}
return null;
}
/**
* Search Mongo using query. The result is then merged into session unless the method is called into a
* pre-existing transaction, in which case, the merge mode is ignored and the merge is not performed.
*
* @param <T>
* the entity type to return
* @param query
* the Mongo query.
* @param mergeMode
* the merge mode to use when merging back retrieved entities or null
* if no merge is requested.
* @param clazz
* the type of the entity.
* @return the first found entity or null
*/
public <T extends IEntity> List<T> findByQuery(final Query query, EMergeMode mergeMode, Class<? extends T> clazz) {
return findByQuery(query, -1, -1, mergeMode, clazz);
}
/**
* Search Mongo using query. The result is then merged into session unless the method is called into a
* pre-existing transaction, in which case, the merge mode is ignored and the merge is not performed.
*
* @param <T>
* the entity type to return
* @param query
* the Mongo query.
* @param firstResult
* the first result rank to retrieve.
* @param maxResults
* the max number of results to retrieve.
* @param mergeMode
* the merge mode to use when merging back retrieved entities or null
* if no merge is requested.
* @param clazz
* the type of the entity.
* @return the first found entity or null
*/
@SuppressWarnings("UnusedParameters")
public <T extends IEntity> List<T> findByQuery(final Query query, int firstResult, int maxResults,
EMergeMode mergeMode, Class<? extends T> clazz) {
List<T> res;
if (isUnitOfWorkActive()) {
// merge mode must be ignored if a transaction is pre-existing, so force
// to null.
// This is useless to clone in UOW now that UOW registration is done
// in onLoad interceptor
// res = (List<T>) cloneInUnitOfWork(find(query, firstResult,
// maxResults, null), clazz);
res = find(query, firstResult, maxResults, null, clazz);
} else {
// merge mode is passed for merge to occur inside the transaction.
res = find(query, firstResult, maxResults, mergeMode, clazz);
}
return res;
}
private <T extends IEntity> List<T> find(final Query query, final int firstResult, final int maxResults,
final EMergeMode mergeMode, final Class<? extends T> clazz) {
return getTransactionTemplate().execute(new TransactionCallback<List<T>>() {
@SuppressWarnings("unchecked")
@Override
public List<T> doInTransaction(TransactionStatus status) {
if (firstResult >= 0) {
query.skip(firstResult);
}
if (maxResults > 0) {
query.limit(maxResults);
}
List<? extends T> entities = getMongoTemplate().find(query, clazz);
if (mergeMode != null) {
entities = merge(entities, mergeMode);
}
return (List<T>) entities;
}
});
}
/**
* Reloads an entity in Mongo.
*
* @param entity
* the entity to reload.
*/
@Override
public void reload(final IEntity entity) {
if (entity == null) {
throw new IllegalArgumentException("Passed entity cannot be null");
}
// builds a collection of entities to reload.
Set<IEntity> dirtyReachableEntities = buildReachableDirtyEntitySet(entity);
if (entity.isPersistent()) {
getTransactionTemplate().execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
merge(getMongoTemplate().findById(entity.getId(), getComponentContract(entity)),
EMergeMode.MERGE_CLEAN_EAGER);
}
});
}
// traverse the reachable dirty entities to explicitly reload the
// ones that were not reloaded by the previous pass.
for (IEntity reachableEntity : dirtyReachableEntities) {
if (reachableEntity.isPersistent() && isDirty(reachableEntity)) {
reload(reachableEntity);
}
}
}
private Set<IEntity> buildReachableDirtyEntitySet(IEntity entity) {
Set<IEntity> reachableDirtyEntities = new HashSet<>();
completeReachableDirtyEntities(entity, reachableDirtyEntities, new HashSet<IEntity>());
return reachableDirtyEntities;
}
private void completeReachableDirtyEntities(IEntity entity, Set<IEntity> reachableDirtyEntities,
Set<IEntity> alreadyTraversed) {
if (alreadyTraversed.contains(entity)) {
return;
}
alreadyTraversed.add(entity);
if (isDirty(entity)) {
reachableDirtyEntities.add(entity);
}
Map<String, Object> entityProps = entity.straightGetProperties();
IComponentDescriptor<?> entityDescriptor = getEntityFactory().getComponentDescriptor(getComponentContract(entity));
for (Map.Entry<String, Object> property : entityProps.entrySet()) {
Object propertyValue = property.getValue();
if (propertyValue instanceof IEntity) {
IPropertyDescriptor propertyDescriptor = entityDescriptor.getPropertyDescriptor(property.getKey());
if (isInitialized(propertyValue) && propertyDescriptor instanceof IRelationshipEndPropertyDescriptor
// It's not a master data relationship.
&& ((IRelationshipEndPropertyDescriptor) propertyDescriptor).getReverseRelationEnd() != null) {
completeReachableDirtyEntities((IEntity) propertyValue, reachableDirtyEntities, alreadyTraversed);
}
} else if (propertyValue instanceof Collection<?>) {
if (isInitialized(propertyValue)) {
for (Object elt : ((Collection<?>) propertyValue)) {
if (elt instanceof IEntity) {
completeReachableDirtyEntities((IEntity) elt, reachableDirtyEntities, alreadyTraversed);
}
}
}
}
}
}
/**
* Gets mongo template.
*
* @return the mongo template
*/
public MongoTemplate getMongoTemplate() {
return mongoTemplate;
}
/**
* Sets mongo template.
*
* @param mongoTemplate
* the mongo template
*/
public void setMongoTemplate(MongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
}
}