REST API should support optimistic locking via version field #1196

This commit is contained in:
Andrey Subbotin 2018-09-11 14:26:18 +04:00
parent 1c03eac407
commit 8736eda525
7 changed files with 92 additions and 21 deletions

View File

@ -196,11 +196,16 @@ public class EntityImportExport implements EntityImportExportAPI {
@Override
public Collection<Entity> importEntities(Collection<? extends Entity> entities, EntityImportView importView) {
return importEntities(entities, importView, false);
return importEntities(entities, importView, false, false);
}
@Override
public Collection<Entity> importEntities(Collection<? extends Entity> entities, EntityImportView importView, boolean validate) {
return importEntities(entities, importView, validate, false);
}
@Override
public Collection<Entity> importEntities(Collection<? extends Entity> entities, EntityImportView importView, boolean validate, boolean optimisticLocking) {
List<ReferenceInfo> 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<ReferenceInfo> referenceInfoList) {
Collection<ReferenceInfo> 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<ReferenceInfo> referenceInfoList) {
Collection<ReferenceInfo> 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<ReferenceInfo> referenceInfoList) {
Collection<ReferenceInfo> referenceInfoList,
boolean optimisticLocking) {
Collection<Entity> collectionValue = srcEntity.getValue(viewProperty.getName());
Collection<Entity> 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<ReferenceInfo> referenceInfoList) {
Collection<ReferenceInfo> referenceInfoList,
boolean optimisticLocking) {
Collection<Entity> collectionValue = srcEntity.getValue(viewProperty.getName());
Collection<Entity> 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<ReferenceInfo> referenceInfoList) {
Collection<ReferenceInfo> 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);
}
}
}

View File

@ -69,4 +69,9 @@ public interface EntityImportExportAPI {
* See documentation for {@link EntityImportExportService#importEntities(Collection, EntityImportView, boolean)}
*/
Collection<Entity> importEntities(Collection<? extends Entity> entities, EntityImportView importView, boolean validate);
/**
* See documentation for {@link EntityImportExportService#importEntities(Collection, EntityImportView, boolean, boolean)}
*/
Collection<Entity> importEntities(Collection<? extends Entity> entities, EntityImportView importView, boolean validate, boolean optimisticLocking);
}

View File

@ -69,4 +69,9 @@ public class EntityImportExportServiceBean implements EntityImportExportService
public Collection<Entity> importEntities(Collection<? extends Entity> entities, EntityImportView importView, boolean validate) {
return entityImportExport.importEntities(entities, importView, validate);
}
@Override
public Collection<Entity> importEntities(Collection<? extends Entity> entities, EntityImportView importView, boolean validate, boolean optimisticLocking) {
return entityImportExport.importEntities(entities, importView, validate, optimisticLocking);
}
}

View File

@ -120,4 +120,21 @@ public interface EntityImportExportService {
* @return a collection of entities that have been imported
*/
Collection<Entity> importEntities(Collection<? extends Entity> 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.
* <p>
* 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<Entity> importEntities(Collection<? extends Entity> entities, EntityImportView importView, boolean validate,
boolean optimisticLocking);
}

View File

@ -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();
}

View File

@ -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<ErrorInfo> handleException(Exception e) {
log.error("Exception in REST controller", e);
@SuppressWarnings("unchecked")
List<Throwable> 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);
}

View File

@ -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<Entity> 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);