JPA server interceptor methods for create/update/delete provided the wrong version ID to the interceptors

This commit is contained in:
James 2017-03-11 07:57:33 -05:00
parent bcff22c769
commit 0c6679ac44
9 changed files with 702 additions and 88 deletions

View File

@ -179,7 +179,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
}
@Override
public ResourceTable delete(IIdType theId, List<DeleteConflict> deleteConflicts, RequestDetails theRequestDetails) {
public DaoMethodOutcome delete(IIdType theId, List<DeleteConflict> deleteConflicts, RequestDetails theRequestDetails) {
if (theId == null || !theId.hasIdPart()) {
throw new InvalidRequestException("Can not perform delete, no ID provided");
}
@ -188,6 +188,8 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
throw new ResourceVersionConflictException("Trying to delete " + theId + " but this is not the current version");
}
StopWatch w = new StopWatch();
T resourceToDelete = toResource(myResourceType, entity, false);
validateOkToDelete(deleteConflicts, entity);
@ -202,6 +204,16 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
Date updateTime = new Date();
ResourceTable savedEntity = updateEntity(null, entity, updateTime, updateTime);
resourceToDelete.setId(entity.getIdDt());
// /*
// * If we aren't indexing (meaning we're probably executing a sub-operation within a transaction),
// * we'll manually increase the version. This is important because we want the updated version number
// * to be reflected in the resource shared with interceptors
// */
// if (!thePerformIndexing) {
// incremenetId(resourceToDelete);
// }
// Notify JPA interceptors
if (theRequestDetails != null) {
@ -214,7 +226,16 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
}
}
return savedEntity;
DaoMethodOutcome outcome = toMethodOutcome(savedEntity, resourceToDelete).setCreated(true);
IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(getContext());
String message = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "successfulDeletes", 1, w.getMillis());
String severity = "information";
String code = "informational";
OperationOutcomeUtil.addIssue(getContext(), oo, severity, message, null, code);
outcome.setOperationOutcome(oo);
return outcome;
}
@Override
@ -222,24 +243,22 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
List<DeleteConflict> deleteConflicts = new ArrayList<DeleteConflict>();
StopWatch w = new StopWatch();
ResourceTable savedEntity = delete(theId, deleteConflicts, theRequestDetails);
DaoMethodOutcome retVal = delete(theId, deleteConflicts, theRequestDetails);
validateDeleteConflictsEmptyOrThrowException(deleteConflicts);
IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(getContext());
String message = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "successfulDeletes", 1, w.getMillis());
String severity = "information";
String code = "informational";
OperationOutcomeUtil.addIssue(getContext(), oo, severity, message, null, code);
ourLog.info("Processed delete on {} in {}ms", theId.getValue(), w.getMillisAndRestart());
DaoMethodOutcome retVal = toMethodOutcome(savedEntity, null);
retVal.setOperationOutcome(oo);
return retVal;
}
/**
* This method gets called by {@link #deleteByUrl(String, List, RequestDetails)} as well as by
* transaction processors
*/
@Override
public List<ResourceTable> deleteByUrl(String theUrl, List<DeleteConflict> deleteConflicts, RequestDetails theRequestDetails) {
public DeleteMethodOutcome deleteByUrl(String theUrl, List<DeleteConflict> deleteConflicts, RequestDetails theRequestDetails) {
StopWatch w = new StopWatch();
Set<Long> resource = processMatchUrl(theUrl, myResourceType);
if (resource.size() > 1) {
if (myDaoConfig.isAllowMultipleDelete() == false) {
@ -247,10 +266,10 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
}
}
List<ResourceTable> retVal = new ArrayList<ResourceTable>();
List<ResourceTable> deletedResources = new ArrayList<ResourceTable>();
for (Long pid : resource) {
ResourceTable entity = myEntityManager.find(ResourceTable.class, pid);
retVal.add(entity);
deletedResources.add(entity);
validateOkToDelete(deleteConflicts, entity);
@ -274,18 +293,6 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
}
return retVal;
}
@Override
public DaoMethodOutcome deleteByUrl(String theUrl, RequestDetails theRequestDetails) {
StopWatch w = new StopWatch();
List<DeleteConflict> deleteConflicts = new ArrayList<DeleteConflict>();
List<ResourceTable> deletedResources = deleteByUrl(theUrl, deleteConflicts, theRequestDetails);
validateDeleteConflictsEmptyOrThrowException(deleteConflicts);
IBaseOperationOutcome oo;
if (deletedResources.isEmpty()) {
oo = OperationOutcomeUtil.newInstance(getContext());
@ -302,10 +309,23 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
}
ourLog.info("Processed delete on {} (matched {} resource(s)) in {}ms", new Object[] { theUrl, deletedResources.size(), w.getMillis() });
DaoMethodOutcome retVal = new DaoMethodOutcome();
DeleteMethodOutcome retVal = new DeleteMethodOutcome();
retVal.setDeletedEntities(deletedResources);
retVal.setOperationOutcome(oo);
return retVal;
}
@Override
public DeleteMethodOutcome deleteByUrl(String theUrl, RequestDetails theRequestDetails) {
List<DeleteConflict> deleteConflicts = new ArrayList<DeleteConflict>();
DeleteMethodOutcome outcome = deleteByUrl(theUrl, deleteConflicts, theRequestDetails);
validateDeleteConflictsEmptyOrThrowException(deleteConflicts);
return outcome;
}
@PostConstruct
public void detectSearchDaoDisabled() {
@ -362,6 +382,16 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
updateEntity(theResource, entity, null, thePerformIndexing, thePerformIndexing, theUpdateTime);
theResource.setId(entity.getIdDt());
/*
* If we aren't indexing (meaning we're probably executing a sub-operation within a transaction),
* we'll manually increase the version. This is important because we want the updated version number
* to be reflected in the resource shared with interceptors
*/
if (!thePerformIndexing) {
incremenetId(theResource);
}
// Notify JPA interceptors
if (theRequestDetails != null) {
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, getContext(), theResource);
@ -374,6 +404,9 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
}
DaoMethodOutcome outcome = toMethodOutcome(entity, theResource).setCreated(true);
if (!thePerformIndexing) {
outcome.setId(theResource.getIdElement());
}
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "successfulCreate", outcome.getId(), w.getMillisAndRestart());
outcome.setOperationOutcome(createInfoOperationOutcome(msg));
@ -382,6 +415,12 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
return outcome;
}
private void incremenetId(T theResource) {
String newVersion = Long.toString(theResource.getIdElement().getVersionIdPartAsLong() + 1);
IIdType newId = theResource.getIdElement().withVersion(newVersion);
theResource.setId(newId);
}
private <MT extends IBaseMetaType> void doMetaAdd(MT theMetaAdd, BaseHasResource entity) {
List<TagDefinition> tags = toTagList(theMetaAdd);
@ -1138,6 +1177,15 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
// Perform update
ResourceTable savedEntity = updateEntity(theResource, entity, null, thePerformIndexing, thePerformIndexing, new Date());
/*
* If we aren't indexing (meaning we're probably executing a sub-operation within a transaction),
* we'll manually increase the version. This is important because we want the updated version number
* to be reflected in the resource shared with interceptors
*/
if (!thePerformIndexing) {
incremenetId(theResource);
}
// Notify interceptors
if (theRequestDetails != null) {
theRequestDetails.getRequestOperationCallback().resourceUpdated(theResource);
@ -1150,6 +1198,10 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
DaoMethodOutcome outcome = toMethodOutcome(savedEntity, theResource).setCreated(false);
if (!thePerformIndexing) {
outcome.setId(theResource.getIdElement());
}
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "successfulCreate", outcome.getId(), w.getMillisAndRestart());
outcome.setOperationOutcome(createInfoOperationOutcome(msg));

View File

@ -0,0 +1,25 @@
package ca.uhn.fhir.jpa.dao;
import java.util.List;
import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.rest.api.MethodOutcome;
/**
* This class is a replacement for {@link DaoMethodOutcome} for delete operations,
* as they can perform their operation over multiple resources
*/
public class DeleteMethodOutcome extends MethodOutcome {
private List<ResourceTable> myDeletedEntities;
public List<ResourceTable> getDeletedEntities() {
return myDeletedEntities;
}
public DeleteMethodOutcome setDeletedEntities(List<ResourceTable> theDeletedEntities) {
myDeletedEntities = theDeletedEntities;
return this;
}
}

View File

@ -406,14 +406,16 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle, MetaDt> {
ca.uhn.fhir.jpa.dao.IFhirResourceDao<? extends IBaseResource> dao = toDao(parts, verb.getCode(), url);
int status = Constants.STATUS_HTTP_204_NO_CONTENT;
if (parts.getResourceId() != null) {
ResourceTable deleted = dao.delete(new IdDt(parts.getResourceType(), parts.getResourceId()), deleteConflicts, theRequestDetails);
if (deleted != null) {
deletedResources.add(deleted.getIdDt().toUnqualifiedVersionless());
DaoMethodOutcome outcome = dao.delete(new IdDt(parts.getResourceType(), parts.getResourceId()), deleteConflicts, theRequestDetails);
if (outcome.getEntity() != null) {
deletedResources.add(outcome.getId().toUnqualifiedVersionless());
entriesToProcess.put(nextRespEntry, outcome.getEntity());
}
} else {
List<ResourceTable> allDeleted = dao.deleteByUrl(parts.getResourceType() + '?' + parts.getParams(), deleteConflicts, theRequestDetails);
DeleteMethodOutcome deleteOutcome = dao.deleteByUrl(parts.getResourceType() + '?' + parts.getParams(), deleteConflicts, theRequestDetails);
List<ResourceTable> allDeleted = deleteOutcome.getDeletedEntities();
for (ResourceTable deleted : allDeleted) {
deletedResources.add(deleted.getIdDt().toUnqualifiedVersionless());
deletedResources.add(deleted.getIdDt().toUnqualifiedVersionless());
}
if (allDeleted.isEmpty()) {
status = Constants.STATUS_HTTP_404_NOT_FOUND;

View File

@ -39,7 +39,6 @@ import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.PatchTypeEnum;
import ca.uhn.fhir.rest.api.ValidationModeEnum;
import ca.uhn.fhir.rest.method.RequestDetails;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.server.EncodingEnum;
import ca.uhn.fhir.rest.server.IBundleProvider;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
@ -84,7 +83,7 @@ public interface IFhirResourceDao<T extends IBaseResource> extends IDao {
* in the provided list
* @param theRequestDetails TODO
*/
ResourceTable delete(IIdType theResource, List<DeleteConflict> theDeleteConflictsListToPopulate, RequestDetails theRequestDetails);
DaoMethodOutcome delete(IIdType theResource, List<DeleteConflict> theDeleteConflictsListToPopulate, RequestDetails theRequestDetails);
/**
* This method throws an exception if there are delete conflicts
@ -95,12 +94,12 @@ public interface IFhirResourceDao<T extends IBaseResource> extends IDao {
* This method does not throw an exception if there are delete conflicts, but populates them
* in the provided list
*/
List<ResourceTable> deleteByUrl(String theUrl, List<DeleteConflict> theDeleteConflictsListToPopulate, RequestDetails theRequestDetails);
DeleteMethodOutcome deleteByUrl(String theUrl, List<DeleteConflict> theDeleteConflictsListToPopulate, RequestDetails theRequestDetails);
/**
* This method throws an exception if there are delete conflicts
*/
DaoMethodOutcome deleteByUrl(String theString, RequestDetails theRequestDetails);
DeleteMethodOutcome deleteByUrl(String theString, RequestDetails theRequestDetails);
TagList getAllResourceTags(RequestDetails theRequestDetails);

View File

@ -50,6 +50,7 @@ import com.google.common.collect.ArrayListMultimap;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirSystemDao;
import ca.uhn.fhir.jpa.dao.DaoMethodOutcome;
import ca.uhn.fhir.jpa.dao.DeleteMethodOutcome;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.jpa.entity.TagDefinition;
@ -369,6 +370,12 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
}
}
// } else {
//
// if (isNotBlank(nextReqEntry.getRequest().getUrl())) {
// nextResourceId = new IdType(nextReqEntry.getRequest().getUrl());
// }
}
HTTPVerb verb = nextReqEntry.getRequest().getMethodElement().getValue();
@ -385,7 +392,9 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
DaoMethodOutcome outcome;
String matchUrl = nextReqEntry.getRequest().getIfNoneExist();
outcome = resourceDao.create(res, matchUrl, false, theRequestDetails);
handleTransactionCreateOrUpdateOutcome(idSubstitutions, idToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res);
if (nextResourceId != null) {
handleTransactionCreateOrUpdateOutcome(idSubstitutions, idToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res);
}
entriesToProcess.put(nextRespEntry, outcome.getEntity());
if (outcome.getCreated() == false) {
nonUpdatedEntities.add(outcome.getEntity());
@ -402,22 +411,27 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
if (parts.getResourceId() != null) {
IdType deleteId = new IdType(parts.getResourceType(), parts.getResourceId());
if (!deletedResources.contains(deleteId.getValueAsString())) {
ResourceTable deleted = dao.delete(deleteId, deleteConflicts, theRequestDetails);
if (deleted != null) {
DaoMethodOutcome outcome = dao.delete(deleteId, deleteConflicts, theRequestDetails);
if (outcome.getEntity() != null) {
deletedResources.add(deleteId.getValueAsString());
entriesToProcess.put(nextRespEntry, outcome.getEntity());
}
}
} else {
List<ResourceTable> allDeleted = dao.deleteByUrl(parts.getResourceType() + '?' + parts.getParams(), deleteConflicts, theRequestDetails);
DeleteMethodOutcome deleteOutcome = dao.deleteByUrl(parts.getResourceType() + '?' + parts.getParams(), deleteConflicts, theRequestDetails);
List<ResourceTable> allDeleted = deleteOutcome.getDeletedEntities();
for (ResourceTable deleted : allDeleted) {
deletedResources.add(deleted.getIdDt().toUnqualifiedVersionless().getValueAsString());
}
if (allDeleted.isEmpty()) {
status = Constants.STATUS_HTTP_204_NO_CONTENT;
}
nextRespEntry.getResponse().setOutcome((Resource) deleteOutcome.getOperationOutcome());
}
nextRespEntry.getResponse().setStatus(toStatusString(status));
break;
}
case PUT: {
@ -630,7 +644,7 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
}
private static boolean isPlaceholder(IdType theId) {
if (theId.getValue() != null) {
if (theId != null && theId.getValue() != null) {
if (theId.getValue().startsWith("urn:oid:") || theId.getValue().startsWith("urn:uuid:")) {
return true;
}

View File

@ -67,12 +67,15 @@ public abstract class BaseJpaTest {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseJpaTest.class);
protected ServletRequestDetails mySrd;
protected ArrayList<IServerInterceptor> myServerInterceptorList;
protected IRequestOperationCallback myRequestOperationCallback = mock(IRequestOperationCallback.class);
@Before
public void beforeCreateSrd() {
mySrd = mock(ServletRequestDetails.class, Mockito.RETURNS_DEEP_STUBS);
when(mySrd.getRequestOperationCallback()).thenReturn(mock(IRequestOperationCallback.class));
when(mySrd.getServer().getInterceptors()).thenReturn(new ArrayList<IServerInterceptor>());
when(mySrd.getRequestOperationCallback()).thenReturn(myRequestOperationCallback);
myServerInterceptorList = new ArrayList<IServerInterceptor>();
when(mySrd.getServer().getInterceptors()).thenReturn(myServerInterceptorList);
when(mySrd.getUserData()).thenReturn(new HashMap<Object, Object>());
}

View File

@ -1,58 +1,65 @@
package ca.uhn.fhir.jpa.dao.dstu2;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import org.hl7.fhir.dstu3.model.Bundle.BundleType;
import org.hl7.fhir.dstu3.model.Bundle.HTTPVerb;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.DaoMethodOutcome;
import ca.uhn.fhir.jpa.dao.DeleteMethodOutcome;
import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.jpa.interceptor.IJpaServerInterceptor;
import ca.uhn.fhir.jpa.interceptor.JpaServerInterceptorAdapter;
import ca.uhn.fhir.model.dstu2.resource.Bundle;
import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.dstu2.valueset.BundleTypeEnum;
import ca.uhn.fhir.model.dstu2.valueset.HTTPVerbEnum;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.server.interceptor.IServerOperationInterceptor;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.TestUtil;
public class FhirResourceDaoDstu2InterceptorTest extends BaseJpaDstu2Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoDstu2InterceptorTest.class);
private IJpaServerInterceptor myJpaInterceptor;
private JpaServerInterceptorAdapter myJpaInterceptorAdapter = new JpaServerInterceptorAdapter();
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();
}
@After
public void after() {
myDaoConfig.getInterceptors().remove(myJpaInterceptor);
myDaoConfig.getInterceptors().remove(myJpaInterceptorAdapter);
myDaoConfig.setAllowMultipleDelete(new DaoConfig().isAllowMultipleDelete());
}
@Before
public void before() {
myJpaInterceptor = mock(IJpaServerInterceptor.class);
myDaoConfig.getInterceptors().add(myJpaInterceptor);
myDaoConfig.getInterceptors().add(myJpaInterceptorAdapter);
}
/*
* *****************************************************
* Note that non JPA specific operations get tested in individual
* operation test methods too
* *****************************************************
*/
@Test
public void testJpaCreate() {
Patient p = new Patient();
@ -86,7 +93,7 @@ public class FhirResourceDaoDstu2InterceptorTest extends BaseJpaDstu2Test {
verify(myJpaInterceptor, times(0)).resourceUpdated(detailsCapt.capture(), tableCapt.capture());
}
@Test
public void testJpaDelete() {
Patient p = new Patient();
@ -106,6 +113,12 @@ public class FhirResourceDaoDstu2InterceptorTest extends BaseJpaDstu2Test {
}
/*
* *****************************************************
* Note that non JPA specific operations get tested in individual
* operation test methods too
* *****************************************************
*/
@Test
public void testJpaUpdate() {
@ -162,4 +175,247 @@ public class FhirResourceDaoDstu2InterceptorTest extends BaseJpaDstu2Test {
}
@Test
public void testRequestOperationCreate() {
IServerOperationInterceptor interceptor = mock(IServerOperationInterceptor.class);
myServerInterceptorList.add(interceptor);
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock theInvocation) throws Throwable {
IBaseResource res = (IBaseResource) theInvocation.getArguments()[0];
Long id = res.getIdElement().getIdPartAsLong();
assertEquals("Patient/" + id + "/_history/1", res.getIdElement().getValue());
return null;
}}).when(myRequestOperationCallback).resourceCreated(any(IBaseResource.class));
Patient p = new Patient();
p.addName().addFamily("PATIENT");
IIdType id = myPatientDao.create(p, mySrd).getId();
assertEquals(1L, id.getVersionIdPartAsLong().longValue());
verify(myRequestOperationCallback, times(1)).resourceCreated(any(IBaseResource.class));
verifyNoMoreInteractions(myRequestOperationCallback);
}
@Test
public void testRequestOperationDelete() {
Patient p = new Patient();
p.addName().addFamily("PATIENT");
Long id = myPatientDao.create(p, mySrd).getId().getIdPartAsLong();
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock theInvocation) throws Throwable {
IBaseResource res = (IBaseResource) theInvocation.getArguments()[0];
Long id = res.getIdElement().getIdPartAsLong();
assertEquals("Patient/" + id + "/_history/2", res.getIdElement().getValue());
return null;
}}).when(myRequestOperationCallback).resourceDeleted(any(IBaseResource.class));
IIdType newId = myPatientDao.delete(new IdDt("Patient/" + id), mySrd).getId();
assertEquals(2L, newId.getVersionIdPartAsLong().longValue());
verify(myRequestOperationCallback, times(1)).resourceDeleted(any(IBaseResource.class));
verify(myRequestOperationCallback, times(1)).resourceCreated(any(IBaseResource.class));
verifyNoMoreInteractions(myRequestOperationCallback);
}
@Test
public void testRequestOperationDeleteMulti() {
myDaoConfig.setAllowMultipleDelete(true);
Patient p = new Patient();
p.addName().addFamily("PATIENT");
Long id = myPatientDao.create(p, mySrd).getId().getIdPartAsLong();
p = new Patient();
p.addName().addFamily("PATIENT");
Long id2 = myPatientDao.create(p, mySrd).getId().getIdPartAsLong();
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock theInvocation) throws Throwable {
IBaseResource res = (IBaseResource) theInvocation.getArguments()[0];
Long id = res.getIdElement().getIdPartAsLong();
assertEquals("Patient/" + id + "/_history/2", res.getIdElement().getValue());
return null;
}}).when(myRequestOperationCallback).resourceDeleted(any(IBaseResource.class));
DeleteMethodOutcome outcome = myPatientDao.deleteByUrl("Patient?name=PATIENT", mySrd);
String oo = myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(outcome.getOperationOutcome());
ourLog.info(oo);
assertThat(oo, containsString("deleted 2 resource(s)"));
verify(myRequestOperationCallback, times(2)).resourceDeleted(any(IBaseResource.class));
verify(myRequestOperationCallback, times(2)).resourceCreated(any(IBaseResource.class));
verifyNoMoreInteractions(myRequestOperationCallback);
}
@Test
public void testRequestOperationTransactionCreate() {
Patient p = new Patient();
p.addName().addFamily("PATIENT");
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock theInvocation) throws Throwable {
IBaseResource res = (IBaseResource) theInvocation.getArguments()[0];
Long id = res.getIdElement().getIdPartAsLong();
assertEquals("Patient/" + id + "/_history/1", res.getIdElement().getValue());
return null;
}}).when(myRequestOperationCallback).resourceCreated(any(IBaseResource.class));
Bundle xactBundle = new Bundle();
xactBundle.setType(BundleTypeEnum.TRANSACTION);
xactBundle
.addEntry()
.setResource(p)
.getRequest()
.setUrl("Patient")
.setMethod(HTTPVerbEnum.POST);
Bundle resp = mySystemDao.transaction(mySrd, xactBundle);
IdDt newId = new IdDt(resp.getEntry().get(0).getResponse().getLocation());
assertEquals(1L, newId.getVersionIdPartAsLong().longValue());
verify(myRequestOperationCallback, times(1)).resourceCreated(any(IBaseResource.class));
verifyNoMoreInteractions(myRequestOperationCallback);
}
@Test
public void testRequestOperationTransactionDelete() {
Patient p = new Patient();
p.addName().addFamily("PATIENT");
Long id = myPatientDao.create(p, mySrd).getId().getIdPartAsLong();
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock theInvocation) throws Throwable {
IBaseResource res = (IBaseResource) theInvocation.getArguments()[0];
Long id = res.getIdElement().getIdPartAsLong();
assertEquals("Patient/" + id + "/_history/2", res.getIdElement().getValue());
return null;
}}).when(myRequestOperationCallback).resourceDeleted(any(IBaseResource.class));
Bundle xactBundle = new Bundle();
xactBundle.setType(BundleTypeEnum.TRANSACTION);
xactBundle
.addEntry()
.getRequest()
.setUrl("Patient/" + id)
.setMethod(HTTPVerbEnum.DELETE);
Bundle resp = mySystemDao.transaction(mySrd, xactBundle);
IdDt newId = new IdDt(resp.getEntry().get(0).getResponse().getLocation());
assertEquals(2L, newId.getVersionIdPartAsLong().longValue());
verify(myRequestOperationCallback, times(1)).resourceDeleted(any(IBaseResource.class));
verify(myRequestOperationCallback, times(1)).resourceCreated(any(IBaseResource.class));
verifyNoMoreInteractions(myRequestOperationCallback);
}
@Test
public void testRequestOperationTransactionDeleteMulti() {
myDaoConfig.setAllowMultipleDelete(true);
Patient p = new Patient();
p.addName().addFamily("PATIENT");
Long id = myPatientDao.create(p, mySrd).getId().getIdPartAsLong();
p = new Patient();
p.addName().addFamily("PATIENT");
Long id2 = myPatientDao.create(p, mySrd).getId().getIdPartAsLong();
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock theInvocation) throws Throwable {
IBaseResource res = (IBaseResource) theInvocation.getArguments()[0];
Long id = res.getIdElement().getIdPartAsLong();
assertEquals("Patient/" + id + "/_history/2", res.getIdElement().getValue());
return null;
}}).when(myRequestOperationCallback).resourceDeleted(any(IBaseResource.class));
Bundle xactBundle = new Bundle();
xactBundle.setType(BundleTypeEnum.TRANSACTION);
xactBundle
.addEntry()
.getRequest()
.setUrl("Patient?name=PATIENT")
.setMethod(HTTPVerbEnum.DELETE);
Bundle resp = mySystemDao.transaction(mySrd, xactBundle);
verify(myRequestOperationCallback, times(2)).resourceDeleted(any(IBaseResource.class));
verify(myRequestOperationCallback, times(2)).resourceCreated(any(IBaseResource.class));
verifyNoMoreInteractions(myRequestOperationCallback);
}
@Test
public void testRequestOperationTransactionUpdate() {
Patient p = new Patient();
p.addName().addFamily("PATIENT");
final Long id = myPatientDao.create(p, mySrd).getId().getIdPartAsLong();
p = new Patient();
p.setId(new IdDt("Patient/" + id));
p.addName().addFamily("PATIENT2");
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock theInvocation) throws Throwable {
IBaseResource res = (IBaseResource) theInvocation.getArguments()[0];
assertEquals("Patient/" + id + "/_history/2", res.getIdElement().getValue());
return null;
}}).when(myRequestOperationCallback).resourceUpdated(any(IBaseResource.class));
Bundle xactBundle = new Bundle();
xactBundle.setType(BundleTypeEnum.TRANSACTION);
xactBundle
.addEntry()
.setResource(p)
.getRequest()
.setUrl("Patient/" + id)
.setMethod(HTTPVerbEnum.PUT);
Bundle resp = mySystemDao.transaction(mySrd, xactBundle);
IdDt newId = new IdDt(resp.getEntry().get(0).getResponse().getLocation());
assertEquals(2L, newId.getVersionIdPartAsLong().longValue());
verify(myRequestOperationCallback, times(1)).resourceUpdated(any(IBaseResource.class));
verify(myRequestOperationCallback, times(1)).resourceCreated(any(IBaseResource.class));
verifyNoMoreInteractions(myRequestOperationCallback);
}
@Test
public void testRequestOperationUpdate() {
Patient p = new Patient();
p.addName().addFamily("PATIENT");
final Long id = myPatientDao.create(p, mySrd).getId().getIdPartAsLong();
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock theInvocation) throws Throwable {
IBaseResource res = (IBaseResource) theInvocation.getArguments()[0];
assertEquals("Patient/" + id + "/_history/2", res.getIdElement().getValue());
return null;
}}).when(myRequestOperationCallback).resourceUpdated(any(IBaseResource.class));
p = new Patient();
p.setId(new IdDt("Patient/" + id));
p.addName().addFamily("PATIENT2");
IIdType newId = myPatientDao.update(p, mySrd).getId();
assertEquals(2L, newId.getVersionIdPartAsLong().longValue());
verify(myRequestOperationCallback, times(1)).resourceUpdated(any(IBaseResource.class));
verify(myRequestOperationCallback, times(1)).resourceCreated(any(IBaseResource.class));
verifyNoMoreInteractions(myRequestOperationCallback);
}
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();
}
}

View File

@ -1,78 +1,87 @@
package ca.uhn.fhir.jpa.dao.dstu3;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import java.util.Arrays;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.Bundle.BundleType;
import org.hl7.fhir.dstu3.model.Bundle.HTTPVerb;
import org.hl7.fhir.dstu3.model.IdType;
import org.hl7.fhir.dstu3.model.Patient;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.DaoMethodOutcome;
import ca.uhn.fhir.jpa.dao.DeleteMethodOutcome;
import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.jpa.interceptor.IJpaServerInterceptor;
import ca.uhn.fhir.jpa.interceptor.JpaServerInterceptorAdapter;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.rest.method.RequestDetails;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.rest.server.interceptor.IServerOperationInterceptor;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.TestUtil;
public class FhirResourceDaoDstu3InterceptorTest extends BaseJpaDstu3Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoDstu3InterceptorTest.class);
private IJpaServerInterceptor myJpaInterceptor;
private JpaServerInterceptorAdapter myJpaInterceptorAdapter = new JpaServerInterceptorAdapter();
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();
}
@After
public void after() {
myDaoConfig.getInterceptors().remove(myJpaInterceptor);
myDaoConfig.getInterceptors().remove(myJpaInterceptorAdapter);
myDaoConfig.setAllowMultipleDelete(new DaoConfig().isAllowMultipleDelete());
}
@Before
public void before() {
myJpaInterceptor = mock(IJpaServerInterceptor.class);
myDaoConfig.getInterceptors().add(myJpaInterceptor);
myDaoConfig.getInterceptors().add(myJpaInterceptorAdapter);
}
/*
* *****************************************************
* Note that non JPA specific operations get tested in individual
* operation test methods too
* *****************************************************
*/
@Test
public void testJpaCreate() {
Patient p = new Patient();
p.addName().setFamily("PATIENT");
Long id = myPatientDao.create(p, mySrd).getId().getIdPartAsLong();
ArgumentCaptor<ActionRequestDetails> detailsCapt;
ArgumentCaptor<ResourceTable> tableCapt;
detailsCapt = ArgumentCaptor.forClass(ActionRequestDetails.class);
tableCapt = ArgumentCaptor.forClass(ResourceTable.class);
verify(myJpaInterceptor, times(1)).resourceCreated(detailsCapt.capture(), tableCapt.capture());
assertNotNull(tableCapt.getValue().getId());
assertEquals(id, tableCapt.getValue().getId());
detailsCapt = ArgumentCaptor.forClass(ActionRequestDetails.class);
tableCapt = ArgumentCaptor.forClass(ResourceTable.class);
verify(myJpaInterceptor, times(0)).resourceUpdated(detailsCapt.capture(), tableCapt.capture());
/*
* Not do a conditional create
*/
@ -88,6 +97,13 @@ public class FhirResourceDaoDstu3InterceptorTest extends BaseJpaDstu3Test {
}
/*
* *****************************************************
* Note that non JPA specific operations get tested in individual
* operation test methods too
* *****************************************************
*/
@Test
public void testJpaDelete() {
Patient p = new Patient();
@ -95,19 +111,18 @@ public class FhirResourceDaoDstu3InterceptorTest extends BaseJpaDstu3Test {
Long id = myPatientDao.create(p, mySrd).getId().getIdPartAsLong();
myPatientDao.delete(new IdType("Patient", id), mySrd);
ArgumentCaptor<ActionRequestDetails> detailsCapt;
ArgumentCaptor<ResourceTable> tableCapt;
detailsCapt = ArgumentCaptor.forClass(ActionRequestDetails.class);
tableCapt = ArgumentCaptor.forClass(ResourceTable.class);
verify(myJpaInterceptor, times(1)).resourceDeleted(detailsCapt.capture(), tableCapt.capture());
assertNotNull(tableCapt.getValue().getId());
assertEquals(id, tableCapt.getValue().getId());
}
@Test
public void testJpaUpdate() {
Patient p = new Patient();
@ -122,13 +137,13 @@ public class FhirResourceDaoDstu3InterceptorTest extends BaseJpaDstu3Test {
ArgumentCaptor<ActionRequestDetails> detailsCapt;
ArgumentCaptor<ResourceTable> tableCapt;
detailsCapt = ArgumentCaptor.forClass(ActionRequestDetails.class);
tableCapt = ArgumentCaptor.forClass(ResourceTable.class);
verify(myJpaInterceptor, times(1)).resourceUpdated(detailsCapt.capture(), tableCapt.capture());
assertNotNull(tableCapt.getValue().getId());
assertEquals(id, tableCapt.getValue().getId());
/*
* Now do a conditional update
*/
@ -160,7 +175,251 @@ public class FhirResourceDaoDstu3InterceptorTest extends BaseJpaDstu3Test {
verify(myJpaInterceptor, times(2)).resourceCreated(detailsCapt.capture(), tableCapt.capture());
assertEquals(id2, tableCapt.getAllValues().get(3).getId());
}
@Test
public void testRequestOperationCreate() {
IServerOperationInterceptor interceptor = mock(IServerOperationInterceptor.class);
myServerInterceptorList.add(interceptor);
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock theInvocation) throws Throwable {
IBaseResource res = (IBaseResource) theInvocation.getArguments()[0];
Long id = res.getIdElement().getIdPartAsLong();
assertEquals("Patient/" + id + "/_history/1", res.getIdElement().getValue());
return null;
}}).when(myRequestOperationCallback).resourceCreated(any(IBaseResource.class));
Patient p = new Patient();
p.addName().setFamily("PATIENT");
IIdType id = myPatientDao.create(p, mySrd).getId();
assertEquals(1L, id.getVersionIdPartAsLong().longValue());
verify(myRequestOperationCallback, times(1)).resourceCreated(any(IBaseResource.class));
verifyNoMoreInteractions(myRequestOperationCallback);
}
@Test
public void testRequestOperationDelete() {
Patient p = new Patient();
p.addName().setFamily("PATIENT");
Long id = myPatientDao.create(p, mySrd).getId().getIdPartAsLong();
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock theInvocation) throws Throwable {
IBaseResource res = (IBaseResource) theInvocation.getArguments()[0];
Long id = res.getIdElement().getIdPartAsLong();
assertEquals("Patient/" + id + "/_history/2", res.getIdElement().getValue());
return null;
}}).when(myRequestOperationCallback).resourceDeleted(any(IBaseResource.class));
IIdType newId = myPatientDao.delete(new IdType("Patient/" + id), mySrd).getId();
assertEquals(2L, newId.getVersionIdPartAsLong().longValue());
verify(myRequestOperationCallback, times(1)).resourceDeleted(any(IBaseResource.class));
verify(myRequestOperationCallback, times(1)).resourceCreated(any(IBaseResource.class));
verifyNoMoreInteractions(myRequestOperationCallback);
}
@Test
public void testRequestOperationDeleteMulti() {
myDaoConfig.setAllowMultipleDelete(true);
Patient p = new Patient();
p.addName().setFamily("PATIENT");
Long id = myPatientDao.create(p, mySrd).getId().getIdPartAsLong();
p = new Patient();
p.addName().setFamily("PATIENT");
Long id2 = myPatientDao.create(p, mySrd).getId().getIdPartAsLong();
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock theInvocation) throws Throwable {
IBaseResource res = (IBaseResource) theInvocation.getArguments()[0];
Long id = res.getIdElement().getIdPartAsLong();
assertEquals("Patient/" + id + "/_history/2", res.getIdElement().getValue());
return null;
}}).when(myRequestOperationCallback).resourceDeleted(any(IBaseResource.class));
DeleteMethodOutcome outcome = myPatientDao.deleteByUrl("Patient?name=PATIENT", mySrd);
String oo = myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(outcome.getOperationOutcome());
ourLog.info(oo);
assertThat(oo, containsString("deleted 2 resource(s)"));
verify(myRequestOperationCallback, times(2)).resourceDeleted(any(IBaseResource.class));
verify(myRequestOperationCallback, times(2)).resourceCreated(any(IBaseResource.class));
verifyNoMoreInteractions(myRequestOperationCallback);
}
@Test
public void testRequestOperationTransactionCreate() {
Patient p = new Patient();
p.addName().setFamily("PATIENT");
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock theInvocation) throws Throwable {
IBaseResource res = (IBaseResource) theInvocation.getArguments()[0];
Long id = res.getIdElement().getIdPartAsLong();
assertEquals("Patient/" + id + "/_history/1", res.getIdElement().getValue());
return null;
}}).when(myRequestOperationCallback).resourceCreated(any(IBaseResource.class));
Bundle xactBundle = new Bundle();
xactBundle.setType(BundleType.TRANSACTION);
xactBundle
.addEntry()
.setResource(p)
.getRequest()
.setUrl("Patient")
.setMethod(HTTPVerb.POST);
Bundle resp = mySystemDao.transaction(mySrd, xactBundle);
IdType newId = new IdType(resp.getEntry().get(0).getResponse().getLocation());
assertEquals(1L, newId.getVersionIdPartAsLong().longValue());
verify(myRequestOperationCallback, times(1)).resourceCreated(any(IBaseResource.class));
verifyNoMoreInteractions(myRequestOperationCallback);
}
@Test
public void testRequestOperationTransactionDelete() {
Patient p = new Patient();
p.addName().setFamily("PATIENT");
Long id = myPatientDao.create(p, mySrd).getId().getIdPartAsLong();
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock theInvocation) throws Throwable {
IBaseResource res = (IBaseResource) theInvocation.getArguments()[0];
Long id = res.getIdElement().getIdPartAsLong();
assertEquals("Patient/" + id + "/_history/2", res.getIdElement().getValue());
return null;
}}).when(myRequestOperationCallback).resourceDeleted(any(IBaseResource.class));
Bundle xactBundle = new Bundle();
xactBundle.setType(BundleType.TRANSACTION);
xactBundle
.addEntry()
.getRequest()
.setUrl("Patient/" + id)
.setMethod(HTTPVerb.DELETE);
Bundle resp = mySystemDao.transaction(mySrd, xactBundle);
IdType newId = new IdType(resp.getEntry().get(0).getResponse().getLocation());
assertEquals(2L, newId.getVersionIdPartAsLong().longValue());
verify(myRequestOperationCallback, times(1)).resourceDeleted(any(IBaseResource.class));
verify(myRequestOperationCallback, times(1)).resourceCreated(any(IBaseResource.class));
verifyNoMoreInteractions(myRequestOperationCallback);
}
@Test
public void testRequestOperationTransactionDeleteMulti() {
myDaoConfig.setAllowMultipleDelete(true);
Patient p = new Patient();
p.addName().setFamily("PATIENT");
Long id = myPatientDao.create(p, mySrd).getId().getIdPartAsLong();
p = new Patient();
p.addName().setFamily("PATIENT");
Long id2 = myPatientDao.create(p, mySrd).getId().getIdPartAsLong();
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock theInvocation) throws Throwable {
IBaseResource res = (IBaseResource) theInvocation.getArguments()[0];
Long id = res.getIdElement().getIdPartAsLong();
assertEquals("Patient/" + id + "/_history/2", res.getIdElement().getValue());
return null;
}}).when(myRequestOperationCallback).resourceDeleted(any(IBaseResource.class));
Bundle xactBundle = new Bundle();
xactBundle.setType(BundleType.TRANSACTION);
xactBundle
.addEntry()
.getRequest()
.setUrl("Patient?name=PATIENT")
.setMethod(HTTPVerb.DELETE);
Bundle resp = mySystemDao.transaction(mySrd, xactBundle);
String oo = myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(resp);
ourLog.info(oo);
assertThat(oo, containsString("deleted 2 resource(s)"));
verify(myRequestOperationCallback, times(2)).resourceDeleted(any(IBaseResource.class));
verify(myRequestOperationCallback, times(2)).resourceCreated(any(IBaseResource.class));
verifyNoMoreInteractions(myRequestOperationCallback);
}
@Test
public void testRequestOperationTransactionUpdate() {
Patient p = new Patient();
p.addName().setFamily("PATIENT");
final Long id = myPatientDao.create(p, mySrd).getId().getIdPartAsLong();
p = new Patient();
p.setId(new IdType("Patient/" + id));
p.addName().setFamily("PATIENT2");
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock theInvocation) throws Throwable {
IBaseResource res = (IBaseResource) theInvocation.getArguments()[0];
assertEquals("Patient/" + id + "/_history/2", res.getIdElement().getValue());
return null;
}}).when(myRequestOperationCallback).resourceUpdated(any(IBaseResource.class));
Bundle xactBundle = new Bundle();
xactBundle.setType(BundleType.TRANSACTION);
xactBundle
.addEntry()
.setResource(p)
.getRequest()
.setUrl("Patient/" + id)
.setMethod(HTTPVerb.PUT);
Bundle resp = mySystemDao.transaction(mySrd, xactBundle);
IdType newId = new IdType(resp.getEntry().get(0).getResponse().getLocation());
assertEquals(2L, newId.getVersionIdPartAsLong().longValue());
verify(myRequestOperationCallback, times(1)).resourceUpdated(any(IBaseResource.class));
verify(myRequestOperationCallback, times(1)).resourceCreated(any(IBaseResource.class));
verifyNoMoreInteractions(myRequestOperationCallback);
}
@Test
public void testRequestOperationUpdate() {
Patient p = new Patient();
p.addName().setFamily("PATIENT");
final Long id = myPatientDao.create(p, mySrd).getId().getIdPartAsLong();
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock theInvocation) throws Throwable {
IBaseResource res = (IBaseResource) theInvocation.getArguments()[0];
assertEquals("Patient/" + id + "/_history/2", res.getIdElement().getValue());
return null;
}}).when(myRequestOperationCallback).resourceUpdated(any(IBaseResource.class));
p = new Patient();
p.setId(new IdType("Patient/" + id));
p.addName().setFamily("PATIENT2");
IIdType newId = myPatientDao.update(p, mySrd).getId();
assertEquals(2L, newId.getVersionIdPartAsLong().longValue());
verify(myRequestOperationCallback, times(1)).resourceUpdated(any(IBaseResource.class));
verify(myRequestOperationCallback, times(1)).resourceCreated(any(IBaseResource.class));
verifyNoMoreInteractions(myRequestOperationCallback);
}
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();
}
}

View File

@ -162,6 +162,10 @@
occur under certain circumstances when the response contained
contained resources
</action>
<action type="fix">
JPA server interceptor methods for create/update/delete provided
the wrong version ID to the interceptors
</action>
</release>
<release version="2.2" date="2016-12-20">
<action type="add">