mirror of
https://gitee.com/jmix/cuba.git
synced 2024-11-30 10:17:43 +08:00
REST API should support optimistic locking via version field #1196
This commit is contained in:
parent
1c03eac407
commit
8736eda525
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user