/** * *************************************************************************** * Copyright (c) 2010 Qcadoo Limited * Project: Qcadoo Framework * Version: 1.4 * * This file is part of Qcadoo. * * Qcadoo is free software; you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation; either version 3 of the License, * or (at your option) any later version. * * This program 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * *************************************************************************** */ package com.qcadoo.model.internal; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang3.StringUtils; import org.hibernate.Criteria; import org.hibernate.Query; import org.hibernate.exception.ConstraintViolationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.NoTransactionException; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.interceptor.TransactionAspectSupport; import com.google.common.base.Predicate; import com.google.common.collect.Collections2; import com.google.common.collect.Sets; import com.qcadoo.localization.api.TranslationService; import com.qcadoo.model.api.CopyException; import com.qcadoo.model.api.DataDefinition; import com.qcadoo.model.api.DataDefinitionService; import com.qcadoo.model.api.Entity; import com.qcadoo.model.api.EntityList; import com.qcadoo.model.api.EntityMessagesHolder; import com.qcadoo.model.api.EntityOpResult; import com.qcadoo.model.api.ExpressionService; import com.qcadoo.model.api.FieldDefinition; import com.qcadoo.model.api.aop.Auditable; import com.qcadoo.model.api.aop.Monitorable; import com.qcadoo.model.api.search.SearchRestrictions; import com.qcadoo.model.api.search.SearchResult; import com.qcadoo.model.api.types.Cascadeable; import com.qcadoo.model.api.types.CollectionFieldType; import com.qcadoo.model.api.types.DataDefinitionHolder; import com.qcadoo.model.api.types.FieldType; import com.qcadoo.model.api.types.HasManyType; import com.qcadoo.model.api.types.JoinFieldHolder; import com.qcadoo.model.api.types.ManyToManyType; import com.qcadoo.model.api.types.TreeType; import com.qcadoo.model.api.utils.EntityUtils; import com.qcadoo.model.api.validators.ErrorMessage; import com.qcadoo.model.api.validators.GlobalMessage; import com.qcadoo.model.constants.VersionableConstants; import com.qcadoo.model.internal.api.DataAccessService; import com.qcadoo.model.internal.api.EntityService; import com.qcadoo.model.internal.api.HibernateService; import com.qcadoo.model.internal.api.InternalDataDefinition; import com.qcadoo.model.internal.api.InternalFieldDefinition; import com.qcadoo.model.internal.api.PriorityService; import com.qcadoo.model.internal.api.ValidationService; import com.qcadoo.model.internal.search.SearchCriteria; import com.qcadoo.model.internal.search.SearchQuery; import com.qcadoo.model.internal.search.SearchResultImpl; import com.qcadoo.model.internal.utils.EntitySignature; @Service public class DataAccessServiceImpl implements DataAccessService { private static final String L_DATA_DEFINITION_BELONGS_TO_DISABLED_PLUGIN = "DataDefinition belongs to disabled plugin"; private static final String L_DATA_DEFINITION_MUST_BE_GIVEN = "DataDefinition must be given"; @Autowired private DataDefinitionService dataDefinitionService; @Autowired private EntityService entityService; @Autowired private ValidationService validationService; @Autowired private PriorityService priorityService; @Autowired private ExpressionService expressionService; @Autowired private HibernateService hibernateService; @Autowired private TranslationService translationService; private static final Logger LOG = LoggerFactory.getLogger(DataAccessServiceImpl.class); @Auditable @Override @Transactional @Monitorable public Entity fastSave(final InternalDataDefinition dataDefinition, final Entity genericEntity) { return save(dataDefinition, genericEntity, true); } @Auditable @Override @Transactional @Monitorable public Entity save(final InternalDataDefinition dataDefinition, final Entity genericEntity) { return save(dataDefinition, genericEntity, false); } private Entity save(final InternalDataDefinition dataDefinition, final Entity genericEntity, boolean fast) { Set<Entity> newlySavedEntities = new HashSet<Entity>(); Long previousVersion = null; if(dataDefinition.isVersionable()) { previousVersion = genericEntity.getLongField(VersionableConstants.VERSION_FIELD_NAME); } Entity resultEntity = performSave(dataDefinition, genericEntity, new HashSet<Entity>(), newlySavedEntities, fast); try { if (TransactionAspectSupport.currentTransactionStatus().isRollbackOnly()) { resultEntity.setNotValid(); for (Entity e : newlySavedEntities) { e.setId(null); } if(dataDefinition.isVersionable()) { resultEntity.setField(VersionableConstants.VERSION_FIELD_NAME, previousVersion); } } } catch (NoTransactionException e) { LOG.error(e.getMessage(), e); } return resultEntity; } @SuppressWarnings("unchecked") private Entity performSave(final InternalDataDefinition dataDefinition, final Entity genericEntity, final Set<Entity> alreadySavedEntities, final Set<Entity> newlySavedEntities) { return performSave(dataDefinition, genericEntity, alreadySavedEntities, newlySavedEntities, false); } @SuppressWarnings("unchecked") private Entity performSave(final InternalDataDefinition dataDefinition, final Entity genericEntity, final Set<Entity> alreadySavedEntities, final Set<Entity> newlySavedEntities, boolean fast) { checkNotNull(dataDefinition, L_DATA_DEFINITION_MUST_BE_GIVEN); checkState(dataDefinition.isEnabled(), L_DATA_DEFINITION_BELONGS_TO_DISABLED_PLUGIN); checkNotNull(genericEntity, "Entity must be given"); if (alreadySavedEntities.contains(genericEntity)) { return genericEntity; } Entity genericEntityToSave = genericEntity.copy(); Object existingDatabaseEntity = getExistingDatabaseEntity(dataDefinition, genericEntity); Entity existingGenericEntity = null; if (existingDatabaseEntity != null) { existingGenericEntity = entityService.convertToGenericEntity(dataDefinition, existingDatabaseEntity); } if(!fast){ validationService.validateGenericEntity(dataDefinition, genericEntity, existingGenericEntity); } if (!genericEntity.isValid()) { copyValidationErrors(dataDefinition, genericEntityToSave, genericEntity); if (existingGenericEntity != null) { copyMissingFields(genericEntityToSave, existingGenericEntity); } logValidationErrors(genericEntityToSave); return genericEntityToSave; } Object databaseEntity = entityService.convertToDatabaseEntity(dataDefinition, genericEntity, existingDatabaseEntity); if (genericEntity.getId() == null) { priorityService.prioritizeEntity(dataDefinition, databaseEntity); } saveDatabaseEntity(dataDefinition, databaseEntity); if(dataDefinition.isVersionable()){ hibernateService.getCurrentSession().flush(); } Entity savedEntity = entityService.convertToGenericEntity(dataDefinition, databaseEntity); copyGlobalMessages(dataDefinition, savedEntity, genericEntity); for (Entry<String, FieldDefinition> fieldEntry : dataDefinition.getFields().entrySet()) { if (fieldEntry.getValue().getType() instanceof HasManyType) { List<Entity> entities = (List<Entity>) genericEntity.getField(fieldEntry.getKey()); HasManyType hasManyType = (HasManyType) fieldEntry.getValue().getType(); if (entities == null || entities instanceof EntityListImpl) { savedEntity.setField(fieldEntry.getKey(), entities); continue; } List<Entity> savedEntities = saveHasManyEntities(alreadySavedEntities, newlySavedEntities, hasManyType.getJoinFieldName(), savedEntity, entities, (InternalDataDefinition) hasManyType.getDataDefinition()); EntityList dbEntities = savedEntity.getHasManyField(fieldEntry.getKey()); EntityOpResult results = removeOrphans(hasManyType, findOrphans(savedEntities, dbEntities)); if (!results.isSuccessfull()) { // #TODO MAKU copyValidationErrors(dataDefinition, savedEntity, results.getMessagesHolder()); savedEntity.setField(fieldEntry.getKey(), existingGenericEntity.getField(fieldEntry.getKey())); return savedEntity; } savedEntity.setField(fieldEntry.getKey(), savedEntities); } else if (fieldEntry.getValue().getType() instanceof TreeType) { List<Entity> entities = (List<Entity>) genericEntity.getField(fieldEntry.getKey()); if (entities == null || entities instanceof EntityTreeImpl) { savedEntity.setField(fieldEntry.getKey(), entities); continue; } TreeType treeType = (TreeType) fieldEntry.getValue().getType(); List<Entity> savedEntities = saveTreeEntities(alreadySavedEntities, newlySavedEntities, treeType.getJoinFieldName(), savedEntity, entities, (InternalDataDefinition) treeType.getDataDefinition(), null); savedEntity.setField(fieldEntry.getKey(), savedEntities); } } if (LOG.isDebugEnabled()) { LOG.debug(savedEntity + " has been saved"); } alreadySavedEntities.add(savedEntity); if (genericEntity.getId() == null && savedEntity.getId() != null) { newlySavedEntities.add(savedEntity); } return savedEntity; } private void logDeletionErrors(final Entity entity) { logEntityErrors(entity, entity + " hasn't been deleted, because of onDelete hook rejection"); } private void logValidationErrors(final Entity entity) { logEntityErrors(entity, entity + " hasn't been saved, because of validation errors"); } private void logEntityErrors(final Entity entity, final String msg) { if (!LOG.isInfoEnabled()) { return; } StringBuilder sb = new StringBuilder(); if (StringUtils.isNotEmpty(msg)) { sb.append(msg); sb.append('\n'); } for (ErrorMessage error : entity.getGlobalErrors()) { sb.append(" --- " + error.getMessage()); sb.append('\n'); } for (Map.Entry<String, ErrorMessage> error : entity.getErrors().entrySet()) { sb.append(" --- " + error.getKey() + ": " + error.getValue().getMessage()); sb.append('\n'); } LOG.info(sb.toString()); } @Override @Transactional(readOnly = true) public Object convertToDatabaseEntity(final Entity entity) { return entityService.convertToDatabaseEntity((InternalDataDefinition) entity.getDataDefinition(), entity, null); } private List<Entity> saveHasManyEntities(final Set<Entity> alreadySavedEntities, final Set<Entity> newlySavedEntities, final String joinFieldName, final Entity parentEntity, final List<Entity> entities, final InternalDataDefinition dataDefinition) { List<Entity> savedEntities = new ArrayList<Entity>(); for (Entity innerEntity : entities) { innerEntity.setField(joinFieldName, parentEntity.getId()); Entity savedInnerEntity = performSave(dataDefinition, innerEntity, alreadySavedEntities, newlySavedEntities); savedEntities.add(savedInnerEntity); if (!savedInnerEntity.isValid()) { rollbackAndAddGlobalError(parentEntity, savedInnerEntity); } } return savedEntities; } @SuppressWarnings("unchecked") private List<Entity> saveTreeEntities(final Set<Entity> alreadySavedEntities, final Set<Entity> newlySavedEntities, final String joinFieldName, final Entity parentEntity, final List<Entity> entities, final InternalDataDefinition dataDefinition, final Long parentId) { List<Entity> savedEntities = new ArrayList<Entity>(); int i = 0; for (Entity innerEntity : entities) { innerEntity.setField(joinFieldName, parentEntity.getId()); innerEntity.setField("parent", parentId); innerEntity.setField("priority", ++i); List<Entity> children = (List<Entity>) innerEntity.getField("children"); innerEntity.setField("children", null); Entity savedInnerEntity = performSave(dataDefinition, innerEntity, alreadySavedEntities, newlySavedEntities); savedEntities.add(savedInnerEntity); if (children != null) { children = saveTreeEntities(alreadySavedEntities, newlySavedEntities, joinFieldName, parentEntity, children, dataDefinition, savedInnerEntity.getId()); savedInnerEntity.setField("children", children); } if (!savedInnerEntity.isValid()) { rollbackAndAddGlobalError(parentEntity, savedInnerEntity); } } return savedEntities; } private void rollbackAndAddGlobalError(final Entity savedEntity, final Entity errorEntity) { String msg = String.format("Can not save entity '%s' because related entity '%s' has following validation errors:", savedEntity, errorEntity); logEntityErrors(errorEntity, msg); savedEntity.addGlobalError("qcadooView.validate.field.error.invalidRelatedObject", errorDetails(errorEntity)); TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); } private EntityOpResult removeOrphans(final CollectionFieldType fieldType, final Iterable<Entity> orphans) { switch (fieldType.getCascade()) { case NULLIFY: return nullifyOrphans(fieldType.getJoinFieldName(), orphans); case DELETE: return deleteOrphans((InternalDataDefinition) fieldType.getDataDefinition(), orphans); default: throw new IllegalArgumentException(String.format("Unsupported cascade value '%s'", fieldType.getCascade())); } } private EntityOpResult deleteOrphans(final InternalDataDefinition dataDefinition, final Iterable<Entity> orphans) { checkNotNull(dataDefinition, L_DATA_DEFINITION_MUST_BE_GIVEN); checkState(dataDefinition.isDeletable(), "Entity must be deletable"); checkState(dataDefinition.isEnabled(), L_DATA_DEFINITION_BELONGS_TO_DISABLED_PLUGIN); for (Entity orphan : orphans) { EntityOpResult result = deleteEntity(dataDefinition, orphan.getId()); if (!result.isSuccessfull()) { return result; } } return EntityOpResult.successfull(); } private EntityOpResult nullifyOrphans(final String fieldName, final Iterable<Entity> entities) { for (Entity entity : entities) { entity.setField(fieldName, null); Entity savedEntity = entity.getDataDefinition().save(entity); if (!savedEntity.isValid()) { return EntityOpResult.failure(savedEntity); } } return EntityOpResult.successfull(); } private Collection<Entity> findOrphans(final List<Entity> savedEntities, final List<Entity> dbEntities) { final Set<Long> savedEntityIds = Sets.newHashSet(EntityUtils.getIdsView(savedEntities)); return Collections2.filter(dbEntities, new Predicate<Entity>() { @Override public boolean apply(final Entity entity) { return entity != null && !savedEntityIds.contains(entity.getId()); } }); } @Override @Transactional @Monitorable public List<Entity> activate(final InternalDataDefinition dataDefinition, final Long... entityIds) { if (!dataDefinition.isActivable()) { return Collections.emptyList(); } InternalDataDefinition dataDefinitionToActivate = getDataDefinitionByMasterModel(dataDefinition); List<Entity> activatedEntities = new ArrayList<Entity>(); for (Long entityId : entityIds) { Entity entity = get(dataDefinitionToActivate, entityId); if (entity == null) { throw new IllegalStateException("Cannot activate " + entityId); } if (!entity.isActive()) { entity.setActive(true); entity = save(dataDefinitionToActivate, entity); if (!entity.isValid()) { throw new IllegalStateException("Cannot activate " + entity); } LOG.debug(entity + " has been activated"); activatedEntities.add(entity); } } return activatedEntities; } @Override @Transactional @Monitorable public List<Entity> deactivate(final InternalDataDefinition dataDefinition, final Long... entityIds) { if (!dataDefinition.isActivable()) { return Collections.emptyList(); } InternalDataDefinition dataDefinitionToDeactivate = getDataDefinitionByMasterModel(dataDefinition); List<Entity> deactivatedEntities = new ArrayList<Entity>(); for (Long entityId : entityIds) { Entity entity = get(dataDefinitionToDeactivate, entityId); if (entity == null) { throw new IllegalStateException("Cannot deactivate " + entityId + " (entity not found)"); } if (entity.isActive()) { entity.setActive(false); entity = save(dataDefinitionToDeactivate, entity); if (!entity.isValid()) { throw new IllegalStateException("Cannot deactivate " + entity + " because of validation errors"); } LOG.debug(entity + " has been deactivated"); deactivatedEntities.add(entity); } } return deactivatedEntities; } @Override @Transactional @Monitorable public List<Entity> copy(final InternalDataDefinition dataDefinition, final Long... entityIds) { InternalDataDefinition dataDefinitionToCopy = getDataDefinitionByMasterModel(dataDefinition); List<Entity> copiedEntities = new ArrayList<Entity>(); for (Long entityId : entityIds) { Entity sourceEntity = get(dataDefinitionToCopy, entityId); Entity targetEntity = copy(dataDefinitionToCopy, sourceEntity); if (targetEntity == null) { throw new IllegalStateException("Cannot copy " + sourceEntity); } LOG.debug(sourceEntity + " has been copied to " + targetEntity); targetEntity = save(dataDefinitionToCopy, targetEntity); if (!targetEntity.isValid()) { throw new CopyException(targetEntity); } copiedEntities.add(targetEntity); } return copiedEntities; } public Entity copy(final InternalDataDefinition dataDefinition, final Entity sourceEntity) { InternalDataDefinition dataDefinitionToCopy = getDataDefinitionByMasterModel(dataDefinition); Entity targetEntity = dataDefinitionToCopy.create(); for (Entry<String, FieldDefinition> fieldEntry : dataDefinitionToCopy.getFields().entrySet()) { FieldDefinition fieldDefinition = fieldEntry.getValue(); String fieldName = fieldEntry.getKey(); boolean copy = fieldDefinition.getType().isCopyable(); if (copy) { targetEntity.setField(fieldName, getCopyValueOfSimpleField(sourceEntity, dataDefinitionToCopy, fieldName)); } } if (!dataDefinitionToCopy.callCopyHook(targetEntity)) { return null; } for (String fieldName : dataDefinitionToCopy.getFields().keySet()) { copyHasManyField(sourceEntity, targetEntity, dataDefinitionToCopy, fieldName); } for (String fieldName : dataDefinitionToCopy.getFields().keySet()) { copyTreeField(sourceEntity, targetEntity, dataDefinitionToCopy, fieldName); } for (String fieldName : dataDefinitionToCopy.getFields().keySet()) { copyManyToManyField(sourceEntity, targetEntity, dataDefinitionToCopy, fieldName); } return targetEntity; } private void copyTreeField(final Entity sourceEntity, final Entity targetEntity, final DataDefinition dataDefinition, final String fieldName) { FieldDefinition fieldDefinition = dataDefinition.getField(fieldName); if(!isFieldCopyable(TreeType.class, fieldDefinition, dataDefinition)){ return; } TreeType treeType = ((TreeType) fieldDefinition.getType()); List<Entity> entities = new ArrayList<Entity>(); Entity root = sourceEntity.getTreeField(fieldName).getRoot(); if (root != null) { root.setField(treeType.getJoinFieldName(), null); root = copy((InternalDataDefinition) treeType.getDataDefinition(), root); if (root != null) { entities.add(root); } } targetEntity.setField(fieldName, entities); } private void copyHasManyField(final Entity sourceEntity, final Entity targetEntity, final DataDefinition dataDefinition, final String fieldName) { FieldDefinition fieldDefinition = dataDefinition.getField(fieldName); if(!isFieldCopyable(HasManyType.class, fieldDefinition, dataDefinition)){ return; } HasManyType hasManyType = ((HasManyType) fieldDefinition.getType()); List<Entity> entities = new ArrayList<Entity>(); for (Entity childEntity : sourceEntity.getHasManyField(fieldName)) { childEntity.setField(hasManyType.getJoinFieldName(), null); Entity savedChildEntity = copy((InternalDataDefinition) hasManyType.getDataDefinition(), childEntity); if (savedChildEntity != null) { entities.add(savedChildEntity); } } targetEntity.setField(fieldName, entities); } private void copyManyToManyField(final Entity sourceEntity, final Entity targetEntity, final DataDefinition dataDefinition, final String fieldName) { FieldDefinition fieldDefinition = dataDefinition.getField(fieldName); if(!isFieldCopyable(ManyToManyType.class, fieldDefinition, dataDefinition)){ return; } targetEntity.setField(fieldName, sourceEntity.getField(fieldName)); } private boolean isFieldCopyable(Class fieldTypeClass, FieldDefinition fieldDefinition, DataDefinition dataDefinition){ return fieldTypeClass.isInstance(fieldDefinition.getType()) && fieldDefinition.getType().isCopyable() && ((InternalFieldDefinition)fieldDefinition).isEnabled(); } private Object getCopyValueOfSimpleField(final Entity sourceEntity, final DataDefinition dataDefinition, final String fieldName) { InternalFieldDefinition fieldDefinition = (InternalFieldDefinition) dataDefinition.getField(fieldName); if(!fieldDefinition.isEnabled()){ return null; } if (fieldDefinition.isUnique()) { if (fieldDefinition.canBeBothCopyableAndUnique()) { return getCopyValueOfUniqueField(dataDefinition, fieldDefinition, sourceEntity.getStringField(fieldName)); } else { sourceEntity.addError(fieldDefinition, "qcadooView.validate.field.error.invalidUniqueType"); throw new CopyException(sourceEntity); } } else if (fieldDefinition.getType() instanceof HasManyType) { return null; } else if (fieldDefinition.getType() instanceof TreeType) { return null; } else if (fieldDefinition.getType() instanceof ManyToManyType) { return null; } else { return sourceEntity.getField(fieldName); } } private String getCopyValueOfUniqueField(final DataDefinition dataDefinition, final FieldDefinition fieldDefinition, final String value) { if (value == null) { return value; } else { Matcher matcher = Pattern.compile("(.+)\\((\\d+)\\)").matcher(value); String oldValue = value; int index = 1; if (matcher.matches()) { oldValue = matcher.group(1); index = Integer.valueOf(matcher.group(2)) + 1; } while (true) { String newValue = oldValue + "(" + (index++) + ")"; int matches = dataDefinition.find().setMaxResults(1) .add(SearchRestrictions.eq(fieldDefinition.getName(), newValue)).list().getTotalNumberOfEntities(); if (matches == 0) { return newValue; } } } } @Override @Transactional(readOnly = true) @Monitorable public Entity get(final InternalDataDefinition dataDefinition, final Long entityId) { checkNotNull(dataDefinition, L_DATA_DEFINITION_MUST_BE_GIVEN); checkState(dataDefinition.isEnabled(), L_DATA_DEFINITION_BELONGS_TO_DISABLED_PLUGIN); checkNotNull(entityId, "EntityId must be given"); Object databaseEntity = getDatabaseEntity(dataDefinition, entityId); if (databaseEntity == null) { logEntityInfo(dataDefinition, entityId, "hasn't been retrieved, because it doesn't exist"); return null; } Entity entity = entityService.convertToGenericEntity(dataDefinition, databaseEntity); if (LOG.isDebugEnabled()) { LOG.debug(entity + " has been retrieved"); } return entity; } @Override @Transactional @Monitorable public EntityOpResult delete(final InternalDataDefinition dataDefinition, final Long... entityIds) { InternalDataDefinition dataDefinitionToDelete = getDataDefinitionByMasterModel(dataDefinition); checkState(dataDefinitionToDelete.isDeletable(), "Entity must be deletable"); checkState(dataDefinitionToDelete.isEnabled(), L_DATA_DEFINITION_BELONGS_TO_DISABLED_PLUGIN); checkState(entityIds.length > 0, "EntityIds must be given"); for (Long entityId : entityIds) { EntityOpResult result = deleteEntity(dataDefinitionToDelete, entityId); if (!result.isSuccessfull()) { return result; } } return EntityOpResult.successfull(); } @Override public InternalDataDefinition getDataDefinition(final String pluginIdentifier, final String name) { InternalDataDefinition dataDefinition = (InternalDataDefinition) dataDefinitionService.get(pluginIdentifier, name); if (dataDefinition == null) { throw new IllegalStateException("DataDefinition " + pluginIdentifier + "_" + name + " cannot be found"); } else if (!dataDefinition.isEnabled()) { throw new IllegalStateException("DataDefinition " + dataDefinition + " belongs to disabled plugin"); } return dataDefinition; } @Override @Transactional(readOnly = true) @Monitorable public SearchResult find(final SearchQuery searchQuery) { checkArgument(searchQuery != null, "SearchCriteria must be given"); Query query = searchQuery.createQuery(hibernateService.getCurrentSession()); searchQuery.addParameters(query); searchQuery.addCacheable(query); int totalNumberOfEntities = -1; if (searchQuery.hasFirstAndMaxResults()) { totalNumberOfEntities = hibernateService.list(query).size(); searchQuery.addFirstAndMaxResults(query); } if (totalNumberOfEntities == 0) { if (LOG.isDebugEnabled()) { LOG.debug("There is no entity matching criteria " + searchQuery); } return getResultSet(null, totalNumberOfEntities, Collections.emptyList()); } List<?> results = hibernateService.list(query); if (totalNumberOfEntities == -1) { totalNumberOfEntities = results.size(); if (totalNumberOfEntities == 0) { if (LOG.isDebugEnabled()) { LOG.debug("There is no entity matching criteria " + searchQuery); } return getResultSet(null, totalNumberOfEntities, Collections.emptyList()); } } if (LOG.isDebugEnabled()) { LOG.debug("There are " + totalNumberOfEntities + " entities matching criteria " + searchQuery); } InternalDataDefinition searchQueryDataDefinition = (InternalDataDefinition) searchQuery.getDataDefinition(); if (searchQueryDataDefinition == null) { searchQueryDataDefinition = hibernateService.resolveDataDefinition(query); } return getResultSet(searchQueryDataDefinition, totalNumberOfEntities, results); } @Override @Transactional(readOnly = true) @Monitorable public SearchResult find(final SearchCriteria searchCriteria) { checkArgument(searchCriteria != null, "SearchCriteria must be given"); Criteria criteria = searchCriteria.createCriteria(hibernateService.getCurrentSession()); int totalNumberOfEntities = hibernateService.getTotalNumberOfEntities(criteria); if (totalNumberOfEntities == 0) { LOG.debug("There is no entity matching criteria " + searchCriteria); return getResultSet(null, totalNumberOfEntities, Collections.emptyList()); } searchCriteria.addFirstAndMaxResults(criteria); searchCriteria.addOrders(criteria); searchCriteria.addCacheable(criteria); List<?> results = hibernateService.list(criteria); if (LOG.isDebugEnabled()) { LOG.debug("There are " + totalNumberOfEntities + " entities matching criteria " + searchCriteria); } InternalDataDefinition searchQueryDataDefinition = (InternalDataDefinition) searchCriteria.getDataDefinition(); if (searchQueryDataDefinition == null) { searchQueryDataDefinition = hibernateService.resolveDataDefinition(criteria); } return getResultSet(searchQueryDataDefinition, totalNumberOfEntities, results); } @Override public void moveTo(final InternalDataDefinition dataDefinition, final Long entityId, final int position) { checkState(position > 0, "Position must be greaten than 0"); move(dataDefinition, entityId, position, 0); } @Override public void move(final InternalDataDefinition dataDefinition, final Long entityId, final int offset) { checkState(offset != 0, "Offset must be different than 0"); move(dataDefinition, entityId, 0, offset); } @Transactional @Monitorable private void move(final InternalDataDefinition dataDefinition, final Long entityId, final int position, final int offset) { checkNotNull(dataDefinition, L_DATA_DEFINITION_MUST_BE_GIVEN); checkState(dataDefinition.isPrioritizable(), "Entity must be prioritizable"); checkState(dataDefinition.isEnabled(), L_DATA_DEFINITION_BELONGS_TO_DISABLED_PLUGIN); checkNotNull(entityId, "EntityId must be given"); Object databaseEntity = getDatabaseEntity(dataDefinition, entityId); if (databaseEntity == null) { logEntityInfo(dataDefinition, entityId, "hasn't been prioritized, because it doesn't exist"); return; } priorityService.move(dataDefinition, databaseEntity, position, offset); logEntityInfo(dataDefinition, entityId, "has been prioritized"); } private Object getExistingDatabaseEntity(final InternalDataDefinition dataDefinition, final Entity entity) { Object existingDatabaseEntity = null; if (entity.getId() != null) { existingDatabaseEntity = getDatabaseEntity(dataDefinition, entity.getId()); checkState(existingDatabaseEntity != null, "Entity[%s][id=%s] cannot be found", dataDefinition.getPluginIdentifier() + "." + dataDefinition.getName(), entity.getId()); } return existingDatabaseEntity; } private EntityOpResult deleteEntity(final InternalDataDefinition dataDefinition, final Long entityId) { return deleteEntity(dataDefinition, entityId, Sets.<EntitySignature> newHashSet()); } private EntityOpResult deleteEntity(final InternalDataDefinition dataDefinition, final Long entityId, final Set<EntitySignature> traversedEntities) { return deleteEntity(dataDefinition, entityId, false, traversedEntities); } private EntityOpResult deleteEntity(final InternalDataDefinition dataDefinition, final Long entityId, final boolean testOnly, final Set<EntitySignature> traversedEntities) { Object databaseEntity = getDatabaseEntity(dataDefinition, entityId); if(databaseEntity == null){ logEntityDebug(dataDefinition, entityId, "has been deleted earlier, for example onDelete hook"); return new EntityOpResult(true, new EntityMessagesHolderImpl()); } Entity entity = get(dataDefinition, entityId); if (!dataDefinition.callDeleteHook(entity)) { logDeletionErrors(entity); entity.addGlobalError("qcadooView.message.deleteFailedMessage"); TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); return EntityOpResult.failure(entity); } priorityService.deprioritizeEntity(dataDefinition, databaseEntity); Map<String, FieldDefinition> fields = dataDefinition.getFields(); for (FieldDefinition fieldDefinition : fields.values()) { if (fieldDefinition.getType() instanceof CollectionFieldType) { CollectionFieldType collectionFieldType = (CollectionFieldType) fieldDefinition.getType(); @SuppressWarnings("unchecked") Collection<Entity> children = (Collection<Entity>) entity.getField(fieldDefinition.getName()); EntityOpResult cascadeDeletionRes = performCascadeStrategy(entity, collectionFieldType, children, traversedEntities); if (!cascadeDeletionRes.isSuccessfull()) { return cascadeDeletionRes; } } } if (testOnly) { logEntityInfo(dataDefinition, entityId, "may be cascade deleted"); } else { try { databaseEntity = getDatabaseEntity(dataDefinition, entityId); if (databaseEntity != null) { hibernateService.getCurrentSession().delete(databaseEntity); hibernateService.getCurrentSession().flush(); } } catch (ConstraintViolationException e) { throw new IllegalStateException(getConstraintViolationMessage(entity), e); } logEntityDebug(dataDefinition, entityId, "has been deleted"); } return new EntityOpResult(true, entity); } private EntityOpResult performCascadeStrategy(final Entity entity, final FieldType fieldType, final Collection<Entity> children, final Set<EntitySignature> traversedEntities) { if (children == null || children.isEmpty()) { return EntityOpResult.successfull(); } boolean isManyToManyType = fieldType instanceof ManyToManyType; InternalDataDefinition childDataDefinition = (InternalDataDefinition) ((DataDefinitionHolder) fieldType) .getDataDefinition(); Cascadeable.Cascade cascade = ((Cascadeable) fieldType).getCascade(); if (Cascadeable.Cascade.NULLIFY.equals(cascade)) { if (!isManyToManyType) { return performCascadeNullification(childDataDefinition, children, entity, fieldType); } return EntityOpResult.successfull(); } else if (Cascadeable.Cascade.DELETE.equals(cascade)) { return performCascadeDelete(childDataDefinition, children, isManyToManyType, traversedEntities); } else { throw new IllegalArgumentException(String.format("Unsupported cascade value '%s'", cascade)); } } private EntityOpResult performCascadeNullification(final InternalDataDefinition childDataDefinition, final Collection<Entity> children, final Entity entity, final FieldType fieldType) { String joinFieldName = ((JoinFieldHolder) fieldType).getJoinFieldName(); for (Entity child : children) { child.setField(joinFieldName, null); child = save(childDataDefinition, child); if (!child.isValid()) { String msg = String.format("Can not nullify field '%s' in %s because of following validation errors:", joinFieldName, child); logEntityErrors(child, msg); EntityMessagesHolder msgHolder = new EntityMessagesHolderImpl(child); msgHolder.addGlobalError("qcadooView.errorPage.error.dataIntegrityViolationException.objectInUse.explanation"); return EntityOpResult.failure(msgHolder); } } return EntityOpResult.successfull(); } private EntityOpResult performCascadeDelete(final InternalDataDefinition childDataDefinition, final Collection<Entity> children, final boolean testOnly, final Set<EntitySignature> traversedEntities) { for (Entity child : children) { EntitySignature childSignature = EntitySignature.of(child); if (!traversedEntities.contains(childSignature)) { traversedEntities.add(childSignature); EntityOpResult result = deleteEntity(childDataDefinition, child.getId(), testOnly, traversedEntities); if (!result.isSuccessfull()) { return result; } } } return EntityOpResult.successfull(); } private String getConstraintViolationMessage(final Entity entity) { String message = null; try { message = String.format("Entity [ENTITY.%s] is in use", getIdentifierExpression(entity)); } catch (Exception e) { message = "Entity is in use"; } return message; } private String getIdentifierExpression(final Entity entity) { InternalDataDefinition dataDef = (InternalDataDefinition) entity.getDataDefinition(); return expressionService.getValue(entity, dataDef.getIdentifierExpression(), Locale.ENGLISH); } private SearchResultImpl getResultSet(final InternalDataDefinition dataDefinition, final int totalNumberOfEntities, final List<?> results) { List<Entity> genericResults = new ArrayList<Entity>(); for (Object databaseEntity : results) { genericResults.add(entityService.convertToGenericEntity(dataDefinition, databaseEntity)); } SearchResultImpl resultSet = new SearchResultImpl(); resultSet.setResults(genericResults); resultSet.setTotalNumberOfEntities(totalNumberOfEntities); return resultSet; } protected Object getDatabaseEntity(final InternalDataDefinition dataDefinition, final Long entityId) { return hibernateService.getCurrentSession().get(dataDefinition.getClassForEntity(), entityId); } protected void saveDatabaseEntity(final InternalDataDefinition dataDefinition, final Object databaseEntity) { hibernateService.getCurrentSession().save(databaseEntity); } private void copyMissingFields(final Entity genericEntityToSave, final Entity existingGenericEntity) { for (Map.Entry<String, Object> field : existingGenericEntity.getFields().entrySet()) { if (!genericEntityToSave.getFields().containsKey(field.getKey())) { genericEntityToSave.setField(field.getKey(), field.getValue()); } } } private void copyValidationErrors(final DataDefinition dataDefinition, final EntityMessagesHolder target, final EntityMessagesHolder source) { for (ErrorMessage error : source.getGlobalErrors()) { target.addGlobalError(error.getMessage(), error.getAutoClose(), error.isExtraLarge(), error.getVars()); } for (Map.Entry<String, ErrorMessage> error : source.getErrors().entrySet()) { target.addError(dataDefinition.getField(error.getKey()), error.getValue().getMessage(), error.getValue().getVars()); } } private void copyGlobalMessages(final DataDefinition dataDefinition, final EntityMessagesHolder target, final EntityMessagesHolder source) { for (GlobalMessage message : source.getGlobalMessages()) { target.addGlobalMessage(message.getMessage(), message.getAutoClose(), message.isExtraLarge(), message.getVars()); } } private void logEntityInfo(final DataDefinition dataDefinition, final Long entityId, final String message) { if (LOG.isInfoEnabled()) { StringBuilder entityInfo = new StringBuilder("Entity["); entityInfo.append(dataDefinition.getPluginIdentifier()).append('.').append(dataDefinition.getName()); entityInfo.append("][id=").append(entityId).append("] "); entityInfo.append(message); LOG.info(entityInfo.toString()); } } private void logEntityDebug(final DataDefinition dataDefinition, final Long entityId, final String message) { if (LOG.isDebugEnabled()) { StringBuilder entityInfo = new StringBuilder("Entity["); entityInfo.append(dataDefinition.getPluginIdentifier()).append('.').append(dataDefinition.getName()); entityInfo.append("][id=").append(entityId).append("] "); entityInfo.append(message); LOG.debug(entityInfo.toString()); } } protected void setEntityService(final EntityService entityService) { this.entityService = entityService; } protected void setExpressionService(final ExpressionService expressionService) { this.expressionService = expressionService; } protected void setPriorityService(final PriorityService priorityService) { this.priorityService = priorityService; } protected void setValidationService(final ValidationService validationService) { this.validationService = validationService; } protected void setHibernateService(final HibernateService hibernateService) { this.hibernateService = hibernateService; } private InternalDataDefinition getDataDefinitionByMasterModel(InternalDataDefinition dataDefinition) { InternalDataDefinition masterDataDefinition; if(dataDefinition.getMasterModel() == null){ masterDataDefinition = dataDefinition; } else { masterDataDefinition = (InternalDataDefinition)dataDefinitionService.get(dataDefinition.getMasterModel().getPluginIdentifier(), dataDefinition.getMasterModel().getName()); } checkNotNull(masterDataDefinition, L_DATA_DEFINITION_MUST_BE_GIVEN); return masterDataDefinition; } @Override @Transactional(readOnly = true) @Monitorable public Entity getMasterModelEntity(InternalDataDefinition dataDefinition, Long id) { InternalDataDefinition dataDefinitionByMasterModel = getDataDefinitionByMasterModel(dataDefinition); return dataDefinitionByMasterModel.get(id); } private String errorDetails(Entity errorEntity) { StringBuilder sb = new StringBuilder(); for (ErrorMessage error : errorEntity.getGlobalErrors()) { sb.append("<br />").append(translationService.translate(error.getMessage(), LocaleContextHolder.getLocale(), error.getVars())); } for (Map.Entry<String, ErrorMessage> error : errorEntity.getErrors().entrySet()) { sb.append("<br />").append(translationService.translate(error.getValue().getMessage(), LocaleContextHolder.getLocale(), error.getValue().getMessage())); } return sb.toString(); } }