From 8736eda52589fe03fc8256be7ff7aee7256e4ae4 Mon Sep 17 00:00:00 2001 From: Andrey Subbotin Date: Tue, 11 Sep 2018 14:26:18 +0400 Subject: [PATCH] REST API should support optimistic locking via version field #1196 --- .../app/importexport/EntityImportExport.java | 58 ++++++++++++------- .../importexport/EntityImportExportAPI.java | 5 ++ .../EntityImportExportServiceBean.java | 5 ++ .../EntityImportExportService.java | 17 ++++++ .../restapi/config/RestApiConfig.java | 6 ++ .../RestControllerExceptionHandler.java | 15 +++++ .../service/EntitiesControllerManager.java | 7 ++- 7 files changed, 92 insertions(+), 21 deletions(-) diff --git a/modules/core/src/com/haulmont/cuba/core/app/importexport/EntityImportExport.java b/modules/core/src/com/haulmont/cuba/core/app/importexport/EntityImportExport.java index aa42d7fe2d..687e46acc7 100644 --- a/modules/core/src/com/haulmont/cuba/core/app/importexport/EntityImportExport.java +++ b/modules/core/src/com/haulmont/cuba/core/app/importexport/EntityImportExport.java @@ -196,11 +196,16 @@ public class EntityImportExport implements EntityImportExportAPI { @Override public Collection importEntities(Collection entities, EntityImportView importView) { - return importEntities(entities, importView, false); + return importEntities(entities, importView, false, false); } @Override public Collection importEntities(Collection entities, EntityImportView importView, boolean validate) { + return importEntities(entities, importView, validate, false); + } + + @Override + public Collection importEntities(Collection entities, EntityImportView importView, boolean validate, boolean optimisticLocking) { List referenceInfoList = new ArrayList<>(); CommitContext commitContext = new CommitContext(); commitContext.setSoftDeletion(false); @@ -221,7 +226,7 @@ public class EntityImportExport implements EntityImportExportAPI { .setAuthorizationRequired(true); Entity dstEntity = dataManager.load(ctx); - importEntity(srcEntity, dstEntity, importView, regularView, commitContext, referenceInfoList); + importEntity(srcEntity, dstEntity, importView, regularView, commitContext, referenceInfoList, optimisticLocking); } //2. references to existing entities are processed @@ -273,6 +278,7 @@ public class EntityImportExport implements EntityImportExportAPI { * @param regularView view that was used for loading dstEntity * @param commitContext entities that must be commited or deleted will be set to the commitContext * @param referenceInfoList list of referenceInfos for further processing + * @param optimisticLocking whether the passed entity version should be validated before entity is persisted * @return dstEntity that has fields values from the srcEntity */ protected Entity importEntity(Entity srcEntity, @@ -280,7 +286,8 @@ public class EntityImportExport implements EntityImportExportAPI { EntityImportView importView, View regularView, CommitContext commitContext, - Collection referenceInfoList) { + Collection referenceInfoList, + boolean optimisticLocking) { MetaClass metaClass = srcEntity.getMetaClass(); boolean createOp = false; if (dstEntity == null) { @@ -317,27 +324,34 @@ public class EntityImportExport implements EntityImportExportAPI { if (BaseEntityInternalAccess.isRequired(dstSecurityState, propertyName) && srcEntity.getValue(propertyName) == null) { throw new CustomValidationException(format("Attribute [%s] is required for entity %s", propertyName, srcEntity)); } - if ((metaProperty.getRange().isDatatype() && !"version".equals(metaProperty.getName())) || metaProperty.getRange().isEnum()) { + if (metaProperty.getRange().isDatatype()) { + if (!"version".equals(metaProperty.getName())) { + dstEntity.setValue(propertyName, srcEntity.getValue(propertyName)); + } else if (optimisticLocking){ + dstEntity.setValue(propertyName, srcEntity.getValue(propertyName)); + } + } else if (metaProperty.getRange().isEnum()) { dstEntity.setValue(propertyName, srcEntity.getValue(propertyName)); } else if (metaProperty.getRange().isClass()) { View regularPropertyView = regularView.getProperty(propertyName) != null ? regularView.getProperty(propertyName).getView() : null; if (metadata.getTools().isEmbedded(metaProperty)) { if (importViewProperty.getView() != null) { - Entity embeddedEntity = importEmbeddedAttribute(srcEntity, dstEntity, createOp, importViewProperty, regularPropertyView, commitContext, referenceInfoList); + Entity embeddedEntity = importEmbeddedAttribute(srcEntity, dstEntity, createOp, importViewProperty, regularPropertyView, + commitContext, referenceInfoList, optimisticLocking); dstEntity.setValue(propertyName, embeddedEntity); } } else { switch (metaProperty.getRange().getCardinality()) { case MANY_TO_MANY: importManyToManyCollectionAttribute(srcEntity, dstEntity, srcSecurityState, - importViewProperty, regularPropertyView, commitContext, referenceInfoList); + importViewProperty, regularPropertyView, commitContext, referenceInfoList, optimisticLocking); break; case ONE_TO_MANY: importOneToManyCollectionAttribute(srcEntity, dstEntity, srcSecurityState, - importViewProperty, regularPropertyView, commitContext, referenceInfoList); + importViewProperty, regularPropertyView, commitContext, referenceInfoList, optimisticLocking); break; default: - importReference(srcEntity, dstEntity, importViewProperty, regularPropertyView, commitContext, referenceInfoList); + importReference(srcEntity, dstEntity, importViewProperty, regularPropertyView, commitContext, referenceInfoList, optimisticLocking); } } } @@ -367,14 +381,15 @@ public class EntityImportExport implements EntityImportExportAPI { EntityImportViewProperty importViewProperty, View regularView, CommitContext commitContext, - Collection referenceInfoList) { + Collection referenceInfoList, + boolean optimisticLocking) { Entity srcPropertyValue = srcEntity.getValue(importViewProperty.getName()); Entity dstPropertyValue = dstEntity.getValue(importViewProperty.getName()); if (importViewProperty.getView() == null) { ReferenceInfo referenceInfo = new ReferenceInfo(dstEntity, null, importViewProperty, srcPropertyValue, dstPropertyValue); referenceInfoList.add(referenceInfo); } else { - dstPropertyValue = importEntity(srcPropertyValue, dstPropertyValue, importViewProperty.getView(), regularView, commitContext, referenceInfoList); + dstPropertyValue = importEntity(srcPropertyValue, dstPropertyValue, importViewProperty.getView(), regularView, commitContext, referenceInfoList, optimisticLocking); dstEntity.setValue(importViewProperty.getName(), dstPropertyValue); } } @@ -385,7 +400,8 @@ public class EntityImportExport implements EntityImportExportAPI { EntityImportViewProperty viewProperty, View regularView, CommitContext commitContext, - Collection referenceInfoList) { + Collection referenceInfoList, + boolean optimisticLocking) { Collection collectionValue = srcEntity.getValue(viewProperty.getName()); Collection prevCollectionValue = dstEntity.getValue(viewProperty.getName()); MetaProperty metaProperty = srcEntity.getMetaClass().getPropertyNN(viewProperty.getName()); @@ -397,7 +413,7 @@ public class EntityImportExport implements EntityImportExportAPI { .onCreate(e -> { if (!dstFilteredIds.contains(referenceToEntitySupport.getReferenceId(e))) { Entity result = importEntity(e, null, viewProperty.getView(), regularView, - commitContext, referenceInfoList); + commitContext, referenceInfoList, optimisticLocking); if (inverseMetaProperty != null) { result.setValue(inverseMetaProperty.getName(), dstEntity); } @@ -407,7 +423,7 @@ public class EntityImportExport implements EntityImportExportAPI { .onUpdate((src, dst) -> { if (!dstFilteredIds.contains(referenceToEntitySupport.getReferenceId(src))) { Entity result = importEntity(src, dst, viewProperty.getView(), regularView, - commitContext, referenceInfoList); + commitContext, referenceInfoList, optimisticLocking); if (inverseMetaProperty != null) { result.setValue(inverseMetaProperty.getName(), dstEntity); } @@ -435,7 +451,8 @@ public class EntityImportExport implements EntityImportExportAPI { EntityImportViewProperty viewProperty, View regularView, CommitContext commitContext, - Collection referenceInfoList) { + Collection referenceInfoList, + boolean optimisticLocking) { Collection collectionValue = srcEntity.getValue(viewProperty.getName()); Collection prevCollectionValue = dstEntity.getValue(viewProperty.getName()); MetaProperty metaProperty = srcEntity.getMetaClass().getPropertyNN(viewProperty.getName()); @@ -448,14 +465,14 @@ public class EntityImportExport implements EntityImportExportAPI { .onCreate(e -> { if (!dstFilteredIds.contains(referenceToEntitySupport.getReferenceId(e))) { Entity result = importEntity(e, null, viewProperty.getView(), regularView, - commitContext, referenceInfoList); + commitContext, referenceInfoList, optimisticLocking); newCollectionValue.add(result); } }) .onUpdate((src, dst) -> { if (!dstFilteredIds.contains(referenceToEntitySupport.getReferenceId(src))) { Entity result = importEntity(src, dst, viewProperty.getView(), regularView, - commitContext, referenceInfoList); + commitContext, referenceInfoList, optimisticLocking); newCollectionValue.add(result); } }) @@ -483,7 +500,8 @@ public class EntityImportExport implements EntityImportExportAPI { EntityImportViewProperty importViewProperty, View regularView, CommitContext commitContext, - Collection referenceInfoList) { + Collection referenceInfoList, + boolean optimisticLock) { String propertyName = importViewProperty.getName(); MetaProperty metaProperty = srcEntity.getMetaClass().getPropertyNN(propertyName); Entity srcEmbeddedEntity = srcEntity.getValue(propertyName); @@ -527,12 +545,12 @@ public class EntityImportExport implements EntityImportExportAPI { View propertyRegularView = regularView.getProperty(propertyName) != null ? regularView.getProperty(propertyName).getView() : null; if (metaProperty.getRange().getCardinality() == Range.Cardinality.ONE_TO_MANY) { importOneToManyCollectionAttribute(srcEmbeddedEntity, dstEmbeddedEntity, srcSecurityState, - vp, propertyRegularView, commitContext, referenceInfoList); + vp, propertyRegularView, commitContext, referenceInfoList, optimisticLock); } else if (metaProperty.getRange().getCardinality() == Range.Cardinality.MANY_TO_MANY) { importManyToManyCollectionAttribute(srcEmbeddedEntity, dstEmbeddedEntity, srcSecurityState, - vp, propertyRegularView, commitContext, referenceInfoList); + vp, propertyRegularView, commitContext, referenceInfoList, optimisticLock); } else { - importReference(srcEmbeddedEntity, dstEmbeddedEntity, vp, propertyRegularView, commitContext, referenceInfoList); + importReference(srcEmbeddedEntity, dstEmbeddedEntity, vp, propertyRegularView, commitContext, referenceInfoList, optimisticLock); } } } diff --git a/modules/core/src/com/haulmont/cuba/core/app/importexport/EntityImportExportAPI.java b/modules/core/src/com/haulmont/cuba/core/app/importexport/EntityImportExportAPI.java index da28505fdf..018b45d20f 100644 --- a/modules/core/src/com/haulmont/cuba/core/app/importexport/EntityImportExportAPI.java +++ b/modules/core/src/com/haulmont/cuba/core/app/importexport/EntityImportExportAPI.java @@ -69,4 +69,9 @@ public interface EntityImportExportAPI { * See documentation for {@link EntityImportExportService#importEntities(Collection, EntityImportView, boolean)} */ Collection importEntities(Collection entities, EntityImportView importView, boolean validate); + + /** + * See documentation for {@link EntityImportExportService#importEntities(Collection, EntityImportView, boolean, boolean)} + */ + Collection importEntities(Collection entities, EntityImportView importView, boolean validate, boolean optimisticLocking); } diff --git a/modules/core/src/com/haulmont/cuba/core/app/importexport/EntityImportExportServiceBean.java b/modules/core/src/com/haulmont/cuba/core/app/importexport/EntityImportExportServiceBean.java index 3c531c2ac1..99f4eb2b95 100644 --- a/modules/core/src/com/haulmont/cuba/core/app/importexport/EntityImportExportServiceBean.java +++ b/modules/core/src/com/haulmont/cuba/core/app/importexport/EntityImportExportServiceBean.java @@ -69,4 +69,9 @@ public class EntityImportExportServiceBean implements EntityImportExportService public Collection importEntities(Collection entities, EntityImportView importView, boolean validate) { return entityImportExport.importEntities(entities, importView, validate); } + + @Override + public Collection importEntities(Collection entities, EntityImportView importView, boolean validate, boolean optimisticLocking) { + return entityImportExport.importEntities(entities, importView, validate, optimisticLocking); + } } \ No newline at end of file diff --git a/modules/global/src/com/haulmont/cuba/core/app/importexport/EntityImportExportService.java b/modules/global/src/com/haulmont/cuba/core/app/importexport/EntityImportExportService.java index f947d15623..bcf0719971 100644 --- a/modules/global/src/com/haulmont/cuba/core/app/importexport/EntityImportExportService.java +++ b/modules/global/src/com/haulmont/cuba/core/app/importexport/EntityImportExportService.java @@ -120,4 +120,21 @@ public interface EntityImportExportService { * @return a collection of entities that have been imported */ Collection importEntities(Collection entities, EntityImportView importView, boolean validate); + + /** + * Persists entities according to the rules, described by the {@code entityImportView} parameter. If the entity is + * not present in the database, it will be saved. Otherwise the fields of the existing entity that are in the {@code + * entityImportView} will be updated. + *

+ * If the view contains a property for composition attribute then all composition collection members that are absent + * in the passed entity will be removed. + * + * @param importView {@code EntityImportView} with the rules that describes how entities should be persisted. + * @param validate whether the passed entities should be validated by the {@link com.haulmont.cuba.core.global.BeanValidation} + * mechanism before entities are persisted + * @param optimisticLocking whether the passed entities versions should be validated before entities are persisted + * @return a collection of entities that have been imported + */ + Collection importEntities(Collection entities, EntityImportView importView, boolean validate, + boolean optimisticLocking); } diff --git a/modules/rest-api/src/com/haulmont/restapi/config/RestApiConfig.java b/modules/rest-api/src/com/haulmont/restapi/config/RestApiConfig.java index 164b4defed..df63be721b 100644 --- a/modules/rest-api/src/com/haulmont/restapi/config/RestApiConfig.java +++ b/modules/rest-api/src/com/haulmont/restapi/config/RestApiConfig.java @@ -66,4 +66,10 @@ public interface RestApiConfig extends Config { @DefaultBoolean(true) boolean getTokenMaskingEnabled(); + /** + * @return whether the passed entities versions should be validated before entities are persisted + */ + @Property("cuba.rest.optimisticLockingEnabled") + @DefaultBoolean(false) + boolean getOptimisticLockingEnabled(); } \ No newline at end of file diff --git a/modules/rest-api/src/com/haulmont/restapi/controllers/RestControllerExceptionHandler.java b/modules/rest-api/src/com/haulmont/restapi/controllers/RestControllerExceptionHandler.java index f982dd79f1..02d4bd3226 100644 --- a/modules/rest-api/src/com/haulmont/restapi/controllers/RestControllerExceptionHandler.java +++ b/modules/rest-api/src/com/haulmont/restapi/controllers/RestControllerExceptionHandler.java @@ -16,6 +16,7 @@ package com.haulmont.restapi.controllers; +import com.haulmont.cuba.core.global.RemoteException; import com.haulmont.cuba.core.global.RowLevelSecurityException; import com.haulmont.cuba.core.global.validation.CustomValidationException; import com.haulmont.cuba.core.global.validation.MethodParametersValidationException; @@ -23,6 +24,7 @@ import com.haulmont.cuba.core.global.validation.MethodResultValidationException; import com.haulmont.restapi.exception.ConstraintViolationInfo; import com.haulmont.restapi.exception.ErrorInfo; import com.haulmont.restapi.exception.RestAPIException; +import org.apache.commons.lang.exception.ExceptionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; @@ -119,6 +121,19 @@ public class RestControllerExceptionHandler { @ResponseBody public ResponseEntity handleException(Exception e) { log.error("Exception in REST controller", e); + @SuppressWarnings("unchecked") + List list = ExceptionUtils.getThrowableList(e); + for (Throwable throwable : list) { + if (throwable instanceof RemoteException) { + RemoteException remoteException = (RemoteException) throwable; + for (RemoteException.Cause cause : remoteException.getCauses()) { + if (Objects.equals("javax.persistence.OptimisticLockException", cause.getClassName())) { + ErrorInfo errorInfo = new ErrorInfo("Optimistic lock", cause.getMessage()); + return new ResponseEntity<>(errorInfo, HttpStatus.BAD_REQUEST); + } + } + } + } ErrorInfo errorInfo = new ErrorInfo("Server error", ""); return new ResponseEntity<>(errorInfo, HttpStatus.INTERNAL_SERVER_ERROR); } diff --git a/modules/rest-api/src/com/haulmont/restapi/service/EntitiesControllerManager.java b/modules/rest-api/src/com/haulmont/restapi/service/EntitiesControllerManager.java index 828a96497d..fad6825aad 100644 --- a/modules/rest-api/src/com/haulmont/restapi/service/EntitiesControllerManager.java +++ b/modules/rest-api/src/com/haulmont/restapi/service/EntitiesControllerManager.java @@ -31,6 +31,7 @@ import com.haulmont.cuba.core.entity.*; import com.haulmont.cuba.core.global.*; import com.haulmont.cuba.security.entity.EntityOp; import com.haulmont.restapi.common.RestControllerUtils; +import com.haulmont.restapi.config.RestApiConfig; import com.haulmont.restapi.data.CreatedEntityInfo; import com.haulmont.restapi.data.EntitiesSearchResult; import com.haulmont.restapi.exception.RestAPIException; @@ -84,6 +85,9 @@ public class EntitiesControllerManager { @Inject protected RestFilterParser restFilterParser; + @Inject + protected RestApiConfig restApiConfig; + public String loadEntity(String entityName, String entityId, @Nullable String viewName, @@ -330,7 +334,8 @@ public class EntitiesControllerManager { EntityImportView entityImportView = entityImportViewBuilderAPI.buildFromJson(entityJson, metaClass); Collection importedEntities; try { - importedEntities = entityImportExportService.importEntities(Collections.singletonList(entity), entityImportView, true); + importedEntities = entityImportExportService.importEntities(Collections.singletonList(entity), + entityImportView, true, restApiConfig.getOptimisticLockingEnabled()); importedEntities.forEach(it-> restControllerUtils.applyAttributesSecurity(it)); } catch (EntityImportException e) { throw new RestAPIException("Entity update failed", e.getMessage(), HttpStatus.BAD_REQUEST, e);