From b18e71d4f523f620d8c9e292bc7f4ae422214d60 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Sat, 23 Dec 2017 17:13:33 -0500 Subject: [PATCH] Added new callbacks to IServerOperationInterceptor to be invoked before other operation methods --- .../fhir/jpa/dao/BaseHapiFhirResourceDao.java | 53 +- .../r4/FhirResourceDaoR4InterceptorTest.java | 348 ++++++--- .../api/server/IRequestOperationCallback.java | 30 +- .../fhir/rest/api/server/RequestDetails.java | 730 +++++++++--------- .../IServerOperationInterceptor.java | 94 ++- .../ServerOperationInterceptorAdapter.java | 15 + src/changes/changes.xml | 13 + 7 files changed, 790 insertions(+), 493 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java index 7c836781b1e..31acf109abe 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java @@ -196,6 +196,16 @@ public abstract class BaseHapiFhirResourceDao extends B T resourceToDelete = toResource(myResourceType, entity, false); + // Notify IServerOperationInterceptors about pre-action call + if (theRequestDetails != null) { + theRequestDetails.getRequestOperationCallback().resourcePreDelete(resourceToDelete); + } + for (IServerInterceptor next : getConfig().getInterceptors()) { + if (next instanceof IServerOperationInterceptor) { + ((IServerOperationInterceptor) next).resourcePreDelete(theRequestDetails, resourceToDelete); + } + } + validateOkToDelete(theDeleteConflicts, entity); preDelete(resourceToDelete, entity); @@ -267,6 +277,17 @@ public abstract class BaseHapiFhirResourceDao extends B deletedResources.add(entity); T resourceToDelete = toResource(myResourceType, entity, false); + + // Notify IServerOperationInterceptors about pre-action call + if (theRequestDetails != null) { + theRequestDetails.getRequestOperationCallback().resourcePreDelete(resourceToDelete); + } + for (IServerInterceptor next : getConfig().getInterceptors()) { + if (next instanceof IServerOperationInterceptor) { + ((IServerOperationInterceptor) next).resourcePreDelete(theRequestDetails, resourceToDelete); + } + } + validateOkToDelete(deleteConflicts, entity); // Notify interceptors @@ -378,6 +399,16 @@ public abstract class BaseHapiFhirResourceDao extends B notifyInterceptors(RestOperationTypeEnum.CREATE, requestDetails); } + // Notify JPA interceptors + if (theRequestDetails != null) { + theRequestDetails.getRequestOperationCallback().resourcePreCreate(theResource); + } + for (IServerInterceptor next : getConfig().getInterceptors()) { + if (next instanceof IServerOperationInterceptor) { + ((IServerOperationInterceptor) next).resourcePreCreate(theRequestDetails, theResource); + } + } + // Perform actual DB update updateEntity(theResource, entity, null, thePerformIndexing, thePerformIndexing, theUpdateTime, false, thePerformIndexing); theResource.setId(entity.getIdDt()); @@ -558,10 +589,7 @@ public abstract class BaseHapiFhirResourceDao extends B if (theRequestDetails == null || theRequestDetails.getServer() == null) { return false; } - if (theRequestDetails.getServer().getPagingProvider() instanceof DatabaseBackedPagingProvider) { - return true; - } - return false; + return theRequestDetails.getServer().getPagingProvider() instanceof DatabaseBackedPagingProvider; } protected void markResourcesMatchingExpressionAsNeedingReindexing(String theExpression) { @@ -816,9 +844,7 @@ public abstract class BaseHapiFhirResourceDao extends B @Override public BaseHasResource readEntity(IIdType theId) { - boolean checkForForcedId = true; - - BaseHasResource entity = readEntity(theId, checkForForcedId); + BaseHasResource entity = readEntity(theId, true); return entity; } @@ -1048,7 +1074,7 @@ public abstract class BaseHapiFhirResourceDao extends B if (theResource instanceof IResource) { ResourceMetadataKeyEnum.UPDATED.put((IResource) theResource, theEntity.getUpdated()); } else { - IBaseMetaType meta = ((IAnyResource) theResource).getMeta(); + IBaseMetaType meta = theResource.getMeta(); meta.setLastUpdated(theEntity.getUpdatedDate()); } } @@ -1183,8 +1209,17 @@ public abstract class BaseHapiFhirResourceDao extends B IBaseResource oldResource = toResource(entity, false); + // Notify IServerOperationInterceptors about pre-action call + if (theRequestDetails != null) { + theRequestDetails.getRequestOperationCallback().resourcePreUpdate(oldResource, theResource); + } + for (IServerInterceptor next : getConfig().getInterceptors()) { + if (next instanceof IServerOperationInterceptor) { + ((IServerOperationInterceptor) next).resourcePreUpdate(theRequestDetails, oldResource, theResource); + } + } + // Perform update - StopWatch sw = new StopWatch(); ResourceTable savedEntity = updateEntity(theResource, entity, null, thePerformIndexing, thePerformIndexing, new Date(), theForceUpdateVersion, thePerformIndexing); /* diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4InterceptorTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4InterceptorTest.java index ec7cc6efab5..3b8fec3f5d7 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4InterceptorTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4InterceptorTest.java @@ -18,10 +18,14 @@ import org.junit.AfterClass; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; +import java.util.ArrayList; +import java.util.List; + import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.*; import static org.mockito.Matchers.any; @@ -32,6 +36,7 @@ public class FhirResourceDaoR4InterceptorTest extends BaseJpaR4Test { private IServerOperationInterceptor myJpaInterceptor; private ServerOperationInterceptorAdapter myJpaInterceptorAdapter = new ServerOperationInterceptorAdapter(); private IServerOperationInterceptor myServerOperationInterceptor; + private List myIds = new ArrayList<>(); @After public void after() { @@ -43,17 +48,18 @@ public class FhirResourceDaoR4InterceptorTest extends BaseJpaR4Test { @Before public void before() { myJpaInterceptor = mock(IServerOperationInterceptor.class); - + myIds.clear(); + myServerOperationInterceptor = mock(IServerOperationInterceptor.class, new Answer() { @Override - public Object answer(InvocationOnMock theInvocation) throws Throwable { + public Object answer(InvocationOnMock theInvocation) { if (theInvocation.getMethod().getReturnType().equals(boolean.class)) { return true; } return null; } }); - + myDaoConfig.getInterceptors().add(myJpaInterceptor); myDaoConfig.getInterceptors().add(myJpaInterceptorAdapter); myDaoConfig.getInterceptors().add(myServerOperationInterceptor); @@ -180,71 +186,24 @@ public class FhirResourceDaoR4InterceptorTest extends BaseJpaR4Test { doAnswer(new Answer() { @Override - public Void answer(InvocationOnMock theInvocation) throws Throwable { + public Void answer(InvocationOnMock theInvocation) { 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)); + } + }).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)).resourcePreCreate(any(IBaseResource.class)); verify(myRequestOperationCallback, times(1)).resourceCreated(any(IBaseResource.class)); verifyNoMoreInteractions(myRequestOperationCallback); } - @Test - public void testServerOperationCreate() { - verify(myServerOperationInterceptor, times(0)).resourceCreated(Mockito.isNull(RequestDetails.class), any(IBaseResource.class)); - - Patient p = new Patient(); - p.addName().setFamily("PATIENT"); - IIdType id = myPatientDao.create(p, (RequestDetails)null).getId(); - assertEquals(1L, id.getVersionIdPartAsLong().longValue()); - - verify(myServerOperationInterceptor, times(1)).resourceCreated(Mockito.isNull(RequestDetails.class), any(IBaseResource.class)); - } - - @SuppressWarnings("deprecation") - @Test - public void testServerOperationUpdate() { - verify(myServerOperationInterceptor, times(0)).resourceCreated(Mockito.isNull(RequestDetails.class), any(IBaseResource.class)); - verify(myServerOperationInterceptor, times(0)).resourceUpdated(Mockito.isNull(RequestDetails.class), any(IBaseResource.class)); - verify(myServerOperationInterceptor, times(0)).resourceUpdated(Mockito.isNull(RequestDetails.class), any(IBaseResource.class), any(IBaseResource.class)); - - Patient p = new Patient(); - p.addName().setFamily("PATIENT"); - IIdType id = myPatientDao.create(p, (RequestDetails)null).getId(); - assertEquals(1L, id.getVersionIdPartAsLong().longValue()); - - p.addName().setFamily("2"); - myPatientDao.update(p); - - verify(myServerOperationInterceptor, times(1)).resourceCreated(Mockito.isNull(RequestDetails.class), any(IBaseResource.class)); - verify(myServerOperationInterceptor, times(1)).resourceUpdated(Mockito.isNull(RequestDetails.class), any(IBaseResource.class)); - verify(myServerOperationInterceptor, times(1)).resourceUpdated(Mockito.isNull(RequestDetails.class), any(IBaseResource.class), any(IBaseResource.class)); - } - - @Test - public void testServerOperationDelete() { - verify(myServerOperationInterceptor, times(0)).resourceCreated(Mockito.isNull(RequestDetails.class), any(IBaseResource.class)); - verify(myServerOperationInterceptor, times(0)).resourceDeleted(Mockito.isNull(RequestDetails.class), any(IBaseResource.class)); - - Patient p = new Patient(); - p.addName().setFamily("PATIENT"); - IIdType id = myPatientDao.create(p, (RequestDetails)null).getId(); - assertEquals(1L, id.getVersionIdPartAsLong().longValue()); - - p.addName().setFamily("2"); - myPatientDao.delete(p.getIdElement().toUnqualifiedVersionless()); - - verify(myServerOperationInterceptor, times(1)).resourceCreated(Mockito.isNull(RequestDetails.class), any(IBaseResource.class)); - verify(myServerOperationInterceptor, times(1)).resourceDeleted(Mockito.isNull(RequestDetails.class), any(IBaseResource.class)); - } - @Test public void testRequestOperationDelete() { Patient p = new Patient(); @@ -253,16 +212,19 @@ public class FhirResourceDaoR4InterceptorTest extends BaseJpaR4Test { doAnswer(new Answer() { @Override - public Void answer(InvocationOnMock theInvocation) throws Throwable { + public Void answer(InvocationOnMock theInvocation) { 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)); + } + }).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)).resourcePreDelete(any(IBaseResource.class)); + verify(myRequestOperationCallback, times(1)).resourcePreCreate(any(IBaseResource.class)); verify(myRequestOperationCallback, times(1)).resourceDeleted(any(IBaseResource.class)); verify(myRequestOperationCallback, times(1)).resourceCreated(any(IBaseResource.class)); verifyNoMoreInteractions(myRequestOperationCallback); @@ -271,7 +233,7 @@ public class FhirResourceDaoR4InterceptorTest extends BaseJpaR4Test { @Test public void testRequestOperationDeleteMulti() { myDaoConfig.setAllowMultipleDelete(true); - + Patient p = new Patient(); p.addName().setFamily("PATIENT"); Long id = myPatientDao.create(p, mySrd).getId().getIdPartAsLong(); @@ -282,20 +244,23 @@ public class FhirResourceDaoR4InterceptorTest extends BaseJpaR4Test { doAnswer(new Answer() { @Override - public Void answer(InvocationOnMock theInvocation) throws Throwable { + public Void answer(InvocationOnMock theInvocation) { 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)); + } + }).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)); + verify(myRequestOperationCallback, times(2)).resourcePreDelete(any(IBaseResource.class)); + verify(myRequestOperationCallback, times(2)).resourcePreCreate(any(IBaseResource.class)); verifyNoMoreInteractions(myRequestOperationCallback); } @@ -306,27 +271,29 @@ public class FhirResourceDaoR4InterceptorTest extends BaseJpaR4Test { doAnswer(new Answer() { @Override - public Void answer(InvocationOnMock theInvocation) throws Throwable { + public Void answer(InvocationOnMock theInvocation) { 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)); + } + }).when(myRequestOperationCallback).resourceCreated(any(IBaseResource.class)); Bundle xactBundle = new Bundle(); xactBundle.setType(BundleType.TRANSACTION); xactBundle - .addEntry() - .setResource(p) - .getRequest() - .setUrl("Patient") - .setMethod(HTTPVerb.POST); + .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)); + verify(myRequestOperationCallback, times(1)).resourcePreCreate(any(IBaseResource.class)); verifyNoMoreInteractions(myRequestOperationCallback); } @@ -338,25 +305,28 @@ public class FhirResourceDaoR4InterceptorTest extends BaseJpaR4Test { doAnswer(new Answer() { @Override - public Void answer(InvocationOnMock theInvocation) throws Throwable { + public Void answer(InvocationOnMock theInvocation) { 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)); + } + }).when(myRequestOperationCallback).resourceDeleted(any(IBaseResource.class)); Bundle xactBundle = new Bundle(); xactBundle.setType(BundleType.TRANSACTION); xactBundle - .addEntry() - .getRequest() - .setUrl("Patient/" + id) - .setMethod(HTTPVerb.DELETE); + .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)).resourcePreDelete(any(IBaseResource.class)); + verify(myRequestOperationCallback, times(1)).resourcePreCreate(any(IBaseResource.class)); verify(myRequestOperationCallback, times(1)).resourceDeleted(any(IBaseResource.class)); verify(myRequestOperationCallback, times(1)).resourceCreated(any(IBaseResource.class)); verifyNoMoreInteractions(myRequestOperationCallback); @@ -365,7 +335,7 @@ public class FhirResourceDaoR4InterceptorTest extends BaseJpaR4Test { @Test public void testRequestOperationTransactionDeleteMulti() { myDaoConfig.setAllowMultipleDelete(true); - + Patient p = new Patient(); p.addName().setFamily("PATIENT"); Long id = myPatientDao.create(p, mySrd).getId().getIdPartAsLong(); @@ -376,28 +346,31 @@ public class FhirResourceDaoR4InterceptorTest extends BaseJpaR4Test { doAnswer(new Answer() { @Override - public Void answer(InvocationOnMock theInvocation) throws Throwable { + public Void answer(InvocationOnMock theInvocation) { 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)); + } + }).when(myRequestOperationCallback).resourceDeleted(any(IBaseResource.class)); Bundle xactBundle = new Bundle(); xactBundle.setType(BundleType.TRANSACTION); xactBundle - .addEntry() - .getRequest() - .setUrl("Patient?name=PATIENT") - .setMethod(HTTPVerb.DELETE); + .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)); + verify(myRequestOperationCallback, times(2)).resourcePreDelete(any(IBaseResource.class)); + verify(myRequestOperationCallback, times(2)).resourcePreCreate(any(IBaseResource.class)); verifyNoMoreInteractions(myRequestOperationCallback); } @@ -413,20 +386,21 @@ public class FhirResourceDaoR4InterceptorTest extends BaseJpaR4Test { doAnswer(new Answer() { @Override - public Void answer(InvocationOnMock theInvocation) throws Throwable { + public Void answer(InvocationOnMock theInvocation) { IBaseResource res = (IBaseResource) theInvocation.getArguments()[1]; assertEquals("Patient/" + id + "/_history/2", res.getIdElement().getValue()); return null; - }}).when(myRequestOperationCallback).resourceUpdated(any(IBaseResource.class), any(IBaseResource.class)); + } + }).when(myRequestOperationCallback).resourceUpdated(any(IBaseResource.class), any(IBaseResource.class)); Bundle xactBundle = new Bundle(); xactBundle.setType(BundleType.TRANSACTION); xactBundle - .addEntry() - .setResource(p) - .getRequest() - .setUrl("Patient/" + id) - .setMethod(HTTPVerb.PUT); + .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()); @@ -435,6 +409,8 @@ public class FhirResourceDaoR4InterceptorTest extends BaseJpaR4Test { verify(myRequestOperationCallback, times(1)).resourceUpdated(any(IBaseResource.class)); verify(myRequestOperationCallback, times(1)).resourceUpdated(any(IBaseResource.class), any(IBaseResource.class)); verify(myRequestOperationCallback, times(1)).resourceCreated(any(IBaseResource.class)); + verify(myRequestOperationCallback, times(1)).resourcePreCreate(any(IBaseResource.class)); + verify(myRequestOperationCallback, times(1)).resourcePreUpdate(any(IBaseResource.class), any(IBaseResource.class)); verifyNoMoreInteractions(myRequestOperationCallback); } @@ -446,13 +422,14 @@ public class FhirResourceDaoR4InterceptorTest extends BaseJpaR4Test { doAnswer(new Answer() { @Override - public Void answer(InvocationOnMock theInvocation) throws Throwable { + public Void answer(InvocationOnMock theInvocation) { IBaseResource res = (IBaseResource) theInvocation.getArguments()[0]; assertEquals("Patient/" + id + "/_history/1", res.getIdElement().getValue()); res = (IBaseResource) theInvocation.getArguments()[1]; assertEquals("Patient/" + id + "/_history/2", res.getIdElement().getValue()); return null; - }}).when(myRequestOperationCallback).resourceUpdated(any(IBaseResource.class), any(IBaseResource.class)); + } + }).when(myRequestOperationCallback).resourceUpdated(any(IBaseResource.class), any(IBaseResource.class)); p = new Patient(); p.setId(new IdType("Patient/" + id)); @@ -463,12 +440,197 @@ public class FhirResourceDaoR4InterceptorTest extends BaseJpaR4Test { verify(myRequestOperationCallback, times(1)).resourceUpdated(any(IBaseResource.class)); verify(myRequestOperationCallback, times(1)).resourceUpdated(any(IBaseResource.class), any(IBaseResource.class)); verify(myRequestOperationCallback, times(1)).resourceCreated(any(IBaseResource.class)); + verify(myRequestOperationCallback, times(1)).resourcePreCreate(any(IBaseResource.class)); + verify(myRequestOperationCallback, times(1)).resourcePreUpdate(any(IBaseResource.class), any(IBaseResource.class)); verifyNoMoreInteractions(myRequestOperationCallback); } - + + @Test + public void testServerOperationCreate() { + verify(myServerOperationInterceptor, times(0)).resourceCreated(Mockito.isNull(RequestDetails.class), any(IBaseResource.class)); + + Patient p = new Patient(); + p.addName().setFamily("PATIENT"); + IIdType id = myPatientDao.create(p, (RequestDetails) null).getId(); + assertEquals(1L, id.getVersionIdPartAsLong().longValue()); + + verify(myServerOperationInterceptor, times(1)).resourceCreated(Mockito.isNull(RequestDetails.class), any(IBaseResource.class)); + } + + @Test + public void testServerOperationDelete() { + verify(myServerOperationInterceptor, times(0)).resourceCreated(Mockito.isNull(RequestDetails.class), any(IBaseResource.class)); + verify(myServerOperationInterceptor, times(0)).resourceDeleted(Mockito.isNull(RequestDetails.class), any(IBaseResource.class)); + + Patient p = new Patient(); + p.addName().setFamily("PATIENT"); + IIdType id = myPatientDao.create(p, (RequestDetails) null).getId(); + assertEquals(1L, id.getVersionIdPartAsLong().longValue()); + + p.addName().setFamily("2"); + myPatientDao.delete(p.getIdElement().toUnqualifiedVersionless()); + + verify(myServerOperationInterceptor, times(1)).resourceCreated(Mockito.isNull(RequestDetails.class), any(IBaseResource.class)); + verify(myServerOperationInterceptor, times(1)).resourceDeleted(Mockito.isNull(RequestDetails.class), any(IBaseResource.class)); + } + + @Test + public void testServerOperationInterceptorCanModifyOnCreate() { + + ServerOperationInterceptorAdapter interceptor = new ServerOperationInterceptorAdapter() { + @Override + public void resourcePreCreate(RequestDetails theRequest, IBaseResource theResource) { + ((Patient) theResource).setActive(true); + } + }; + myDaoConfig.getInterceptors().add(interceptor); + try { + + doAnswer(new MyOneResourceAnswer()).when(myJpaInterceptor).resourcePreCreate(any(RequestDetails.class), any(IBaseResource.class)); + doAnswer(new MyOneResourceAnswer()).when(myJpaInterceptor).resourceCreated(any(RequestDetails.class), any(IBaseResource.class)); + + Patient p = new Patient(); + p.setActive(false); + IIdType id = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless(); + + InOrder inorder = inOrder(myJpaInterceptor, myRequestOperationCallback); + inorder.verify(myRequestOperationCallback, times(1)).resourcePreCreate(any(IBaseResource.class)); + inorder.verify(myJpaInterceptor, times(1)).resourcePreCreate(any(RequestDetails.class), any(IBaseResource.class)); + inorder.verify(myRequestOperationCallback, times(1)).resourceCreated(any(IBaseResource.class)); + inorder.verify(myJpaInterceptor, times(1)).resourceCreated(any(RequestDetails.class), any(IBaseResource.class)); + + assertNull(myIds.get(0).getIdPart()); + assertNull(myIds.get(0).getVersionIdPart()); + assertNotNull(myIds.get(1).getIdPart()); + assertEquals("1", myIds.get(1).getVersionIdPart()); + + p = myPatientDao.read(id); + assertEquals(true, p.getActive()); + + } finally { + myDaoConfig.getInterceptors().remove(interceptor); + } + } + + @Test + public void testServerOperationInterceptorCanModifyOnUpdate() { + + ServerOperationInterceptorAdapter interceptor = new ServerOperationInterceptorAdapter() { + @Override + public void resourcePreUpdate(RequestDetails theRequest, IBaseResource theOldResource, IBaseResource theNewResource) { + ((Patient) theNewResource).setActive(true); + } + }; + myDaoConfig.getInterceptors().add(interceptor); + try { + + doAnswer(new MyTwoResourceAnswer()).when(myJpaInterceptor).resourcePreUpdate(any(RequestDetails.class), any(IBaseResource.class), any(IBaseResource.class)); + doAnswer(new MyTwoResourceAnswer()).when(myJpaInterceptor).resourceUpdated(any(RequestDetails.class), any(IBaseResource.class), any(IBaseResource.class)); + + Patient p = new Patient(); + p.setActive(false); + IIdType id = myPatientDao.create(p).getId().toUnqualifiedVersionless(); + String idPart = id.getIdPart(); + + p = myPatientDao.read(id); + assertEquals(false, p.getActive()); + + p.setId(p.getIdElement().toUnqualifiedVersionless()); + p.addAddress().setCity("CITY"); + myPatientDao.update(p, mySrd); + + InOrder inorder = inOrder(myJpaInterceptor, myRequestOperationCallback); + inorder.verify(myRequestOperationCallback, times(1)).resourcePreUpdate(any(IBaseResource.class), any(IBaseResource.class)); + inorder.verify(myJpaInterceptor, times(1)).resourcePreUpdate(any(RequestDetails.class), any(IBaseResource.class), any(IBaseResource.class)); + inorder.verify(myRequestOperationCallback, times(1)).resourceUpdated(any(IBaseResource.class), any(IBaseResource.class)); + inorder.verify(myJpaInterceptor, times(1)).resourceUpdated(any(RequestDetails.class), any(IBaseResource.class), any(IBaseResource.class)); + + // resourcePreUpdate + assertEquals(idPart, myIds.get(0).getIdPart()); + assertEquals("1", myIds.get(0).getVersionIdPart()); + assertEquals(idPart, myIds.get(1).getIdPart()); + assertEquals(null, myIds.get(1).getVersionIdPart()); + // resourceUpdated + assertEquals(idPart, myIds.get(2).getIdPart()); + assertEquals("1", myIds.get(2).getVersionIdPart()); + assertEquals(idPart, myIds.get(3).getIdPart()); + assertEquals("2", myIds.get(3).getVersionIdPart()); + + p = myPatientDao.read(id); + assertEquals(true, p.getActive()); + + } finally { + myDaoConfig.getInterceptors().remove(interceptor); + } + } + + @Test + public void testServerOperationPreDelete() { + + doAnswer(new MyOneResourceAnswer()).when(myJpaInterceptor).resourcePreDelete(any(RequestDetails.class), any(IBaseResource.class)); + doAnswer(new MyOneResourceAnswer()).when(myJpaInterceptor).resourceDeleted(any(RequestDetails.class), any(IBaseResource.class)); + + Patient p = new Patient(); + p.setActive(false); + IIdType id = myPatientDao.create(p).getId().toUnqualifiedVersionless(); + String idPart = id.getIdPart(); + + myPatientDao.delete(id); + + InOrder inorder = inOrder(myJpaInterceptor); + inorder.verify(myJpaInterceptor, times(1)).resourcePreDelete(any(RequestDetails.class), any(IBaseResource.class)); + inorder.verify(myJpaInterceptor, times(1)).resourceDeleted(any(RequestDetails.class), any(IBaseResource.class)); + // resourcePreDelete + assertEquals(idPart, myIds.get(0).getIdPart()); + assertEquals("1", myIds.get(0).getVersionIdPart()); + // resourceDeleted + assertEquals(idPart, myIds.get(1).getIdPart()); + assertEquals("2", myIds.get(1).getVersionIdPart()); + + } + + @SuppressWarnings("deprecation") + @Test + public void testServerOperationUpdate() { + verify(myServerOperationInterceptor, times(0)).resourceCreated(Mockito.isNull(RequestDetails.class), any(IBaseResource.class)); + verify(myServerOperationInterceptor, times(0)).resourceUpdated(Mockito.isNull(RequestDetails.class), any(IBaseResource.class)); + verify(myServerOperationInterceptor, times(0)).resourceUpdated(Mockito.isNull(RequestDetails.class), any(IBaseResource.class), any(IBaseResource.class)); + + Patient p = new Patient(); + p.addName().setFamily("PATIENT"); + IIdType id = myPatientDao.create(p, (RequestDetails) null).getId(); + assertEquals(1L, id.getVersionIdPartAsLong().longValue()); + + p.addName().setFamily("2"); + myPatientDao.update(p); + + verify(myServerOperationInterceptor, times(1)).resourceCreated(Mockito.isNull(RequestDetails.class), any(IBaseResource.class)); + verify(myServerOperationInterceptor, times(1)).resourceUpdated(Mockito.isNull(RequestDetails.class), any(IBaseResource.class)); + verify(myServerOperationInterceptor, times(1)).resourceUpdated(Mockito.isNull(RequestDetails.class), any(IBaseResource.class), any(IBaseResource.class)); + } + @AfterClass public static void afterClassClearContext() { TestUtil.clearAllStaticFieldsForUnitTest(); } + private class MyOneResourceAnswer implements Answer { + @Override + public Object answer(InvocationOnMock invocation) { + IIdType id = ((IBaseResource) invocation.getArguments()[1]).getIdElement(); + myIds.add(new IdType(id.getValue())); + return null; + } + } + + private class MyTwoResourceAnswer implements Answer { + @Override + public Object answer(InvocationOnMock invocation) { + IIdType id = ((IBaseResource) invocation.getArguments()[1]).getIdElement(); + myIds.add(new IdType(id.getValue())); + id = ((IBaseResource) invocation.getArguments()[2]).getIdElement(); + myIds.add(new IdType(id.getValue())); + return null; + } + } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/IRequestOperationCallback.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/IRequestOperationCallback.java index 84ed4924ce9..f055239b2a4 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/IRequestOperationCallback.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/IRequestOperationCallback.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.api.server; * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -20,16 +20,30 @@ package ca.uhn.fhir.rest.api.server; * #L% */ -import java.util.Collection; - import org.hl7.fhir.instance.model.api.IBaseResource; +import java.util.Collection; + public interface IRequestOperationCallback { void resourceCreated(IBaseResource theResource); void resourceDeleted(IBaseResource theResource); + void resourcePreCreate(IBaseResource theResource); + + void resourcePreDelete(IBaseResource theResource); + + void resourcePreUpdate(IBaseResource theOldResource, IBaseResource theNewResource); + + /** + * @deprecated Deprecated in HAPI FHIR 2.6 - Use {@link IRequestOperationCallback#resourceUpdated(IBaseResource, IBaseResource)} instead + */ + @Deprecated + void resourceUpdated(IBaseResource theResource); + + void resourceUpdated(IBaseResource theOldResource, IBaseResource theNewResource); + void resourcesCreated(Collection theResource); void resourcesDeleted(Collection theResource); @@ -39,12 +53,4 @@ public interface IRequestOperationCallback { */ @Deprecated void resourcesUpdated(Collection theResource); - - /** - * @deprecated Deprecated in HAPI FHIR 2.6 - Use {@link IRequestOperationCallback#resourceUpdated(IBaseResource, IBaseResource)} instead - */ - @Deprecated - void resourceUpdated(IBaseResource theResource); - - void resourceUpdated(IBaseResource theOldResource, IBaseResource theNewResource); } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/RequestDetails.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/RequestDetails.java index 3950323d6b1..3949b409c01 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/RequestDetails.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/RequestDetails.java @@ -1,12 +1,24 @@ package ca.uhn.fhir.rest.api.server; -import static org.apache.commons.lang3.StringUtils.isBlank; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.RequestTypeEnum; +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; +import ca.uhn.fhir.rest.server.IRestfulServerDefaults; +import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; +import ca.uhn.fhir.rest.server.interceptor.IServerOperationInterceptor; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; +import java.util.*; + +import static org.apache.commons.lang3.StringUtils.isBlank; + /* * #%L * HAPI FHIR - Server Framework @@ -16,9 +28,9 @@ import java.nio.charset.Charset; * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -26,26 +38,316 @@ import java.nio.charset.Charset; * limitations under the License. * #L% */ -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.instance.model.api.IIdType; - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.rest.api.Constants; -import ca.uhn.fhir.rest.api.RequestTypeEnum; -import ca.uhn.fhir.rest.api.RestOperationTypeEnum; -import ca.uhn.fhir.rest.server.IRestfulServerDefaults; -import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; -import ca.uhn.fhir.rest.server.interceptor.IServerOperationInterceptor; public abstract class RequestDetails { + private String myCompartmentName; + private String myCompleteUrl; + private String myFhirServerBase; + private IIdType myId; + private String myOperation; + private Map myParameters; + private byte[] myRequestContents; + private IRequestOperationCallback myRequestOperationCallback = new RequestOperationCallback(); + private String myRequestPath; + private RequestTypeEnum myRequestType; + private String myResourceName; + private boolean myRespondGzip; + private IRestfulResponse myResponse; + private RestOperationTypeEnum myRestOperationType; + private String mySecondaryOperation; + private boolean mySubRequest; + private Map> myUnqualifiedToQualifiedNames; + private Map myUserData; + + protected abstract byte[] getByteStreamRequestContents(); + + /** + * Return the charset as defined by the header contenttype. Return null if it is not set. + */ + public abstract Charset getCharset(); + + public String getCompartmentName() { + return myCompartmentName; + } + + public void setCompartmentName(String theCompartmentName) { + myCompartmentName = theCompartmentName; + } + + public String getCompleteUrl() { + return myCompleteUrl; + } + + public void setCompleteUrl(String theCompleteUrl) { + myCompleteUrl = theCompleteUrl; + } + + /** + * Returns the conditional URL if this request has one, or null otherwise. For an + * update or delete method, this is the part of the URL after the ?. For a create, this + * is the value of the If-None-Exist header. + * + * @param theOperationType The operation type to find the conditional URL for + * @return Returns the conditional URL if this request has one, or null otherwise + */ + public String getConditionalUrl(RestOperationTypeEnum theOperationType) { + if (theOperationType == RestOperationTypeEnum.CREATE) { + String retVal = this.getHeader(Constants.HEADER_IF_NONE_EXIST); + if (isBlank(retVal)) { + return null; + } + if (retVal.startsWith(this.getFhirServerBase())) { + retVal = retVal.substring(this.getFhirServerBase().length()); + } + return retVal; + } else if (theOperationType != RestOperationTypeEnum.DELETE && theOperationType != RestOperationTypeEnum.UPDATE) { + return null; + } + + if (this.getId() != null && this.getId().hasIdPart()) { + return null; + } + + int questionMarkIndex = this.getCompleteUrl().indexOf('?'); + if (questionMarkIndex == -1) { + return null; + } + + return this.getResourceName() + this.getCompleteUrl().substring(questionMarkIndex); + } + + /** + * Returns the HAPI FHIR Context associated with this request + */ + public abstract FhirContext getFhirContext(); + + /** + * The fhir server base url, independant of the query being executed + * + * @return the fhir server base url + */ + public String getFhirServerBase() { + return myFhirServerBase; + } + + public void setFhirServerBase(String theFhirServerBase) { + myFhirServerBase = theFhirServerBase; + } + + public abstract String getHeader(String name); + + public abstract List getHeaders(String name); + + public IIdType getId() { + return myId; + } + + public void setId(IIdType theId) { + myId = theId; + } + + /** + * Retrieves the body of the request as binary data. Either this method or {@link #getReader} may be called to read + * the body, not both. + * + * @return a {@link InputStream} object containing the body of the request + * @throws IllegalStateException if the {@link #getReader} method has already been called for this request + * @throws IOException if an input or output exception occurred + */ + public abstract InputStream getInputStream() throws IOException; + + public String getOperation() { + return myOperation; + } + + public void setOperation(String theOperation) { + myOperation = theOperation; + } + + public Map getParameters() { + if (myParameters == null) { + return Collections.emptyMap(); + } + return myParameters; + } + + public void setParameters(Map theParams) { + myParameters = theParams; + + for (String next : theParams.keySet()) { + for (int i = 0; i < next.length(); i++) { + char nextChar = next.charAt(i); + if (nextChar == ':' || nextChar == '.') { + if (myUnqualifiedToQualifiedNames == null) { + myUnqualifiedToQualifiedNames = new HashMap<>(); + } + String unqualified = next.substring(0, i); + List list = myUnqualifiedToQualifiedNames.get(unqualified); + if (list == null) { + list = new ArrayList<>(4); + myUnqualifiedToQualifiedNames.put(unqualified, list); + } + list.add(next); + break; + } + } + } + + if (myUnqualifiedToQualifiedNames == null) { + myUnqualifiedToQualifiedNames = Collections.emptyMap(); + } + + } + + /** + * Retrieves the body of the request as character data using a BufferedReader. The reader translates the + * character data according to the character encoding used on the body. Either this method or {@link #getInputStream} + * may be called to read the body, not both. + * + * @return a Reader containing the body of the request + * @throws UnsupportedEncodingException if the character set encoding used is not supported and the text cannot be decoded + * @throws IllegalStateException if {@link #getInputStream} method has been called on this request + * @throws IOException if an input or output exception occurred + * @see javax.servlet.http.HttpServletRequest#getInputStream + */ + public abstract Reader getReader() throws IOException; + + /** + * Returns an invoker that can be called from user code to advise the server interceptors + * of any nested operations being invoked within operations. This invoker acts as a proxy for + * all interceptors + */ + public IRequestOperationCallback getRequestOperationCallback() { + return myRequestOperationCallback; + } + + /** + * The part of the request URL that comes after the server base. + *

+ * Will not contain a leading '/' + *

+ */ + public String getRequestPath() { + return myRequestPath; + } + + public void setRequestPath(String theRequestPath) { + assert theRequestPath.length() == 0 || theRequestPath.charAt(0) != '/'; + myRequestPath = theRequestPath; + } + + public RequestTypeEnum getRequestType() { + return myRequestType; + } + + public void setRequestType(RequestTypeEnum theRequestType) { + myRequestType = theRequestType; + } + + public String getResourceName() { + return myResourceName; + } + + public void setResourceName(String theResourceName) { + myResourceName = theResourceName; + } + + public IRestfulResponse getResponse() { + return myResponse; + } + + public void setResponse(IRestfulResponse theResponse) { + this.myResponse = theResponse; + } + + public RestOperationTypeEnum getRestOperationType() { + return myRestOperationType; + } + + public void setRestOperationType(RestOperationTypeEnum theRestOperationType) { + myRestOperationType = theRestOperationType; + } + + public String getSecondaryOperation() { + return mySecondaryOperation; + } + + public void setSecondaryOperation(String theSecondaryOperation) { + mySecondaryOperation = theSecondaryOperation; + } + + public abstract IRestfulServerDefaults getServer(); + + /** + * Returns the server base URL (with no trailing '/') for a given request + */ + public abstract String getServerBaseForRequest(); + + public Map> getUnqualifiedToQualifiedNames() { + return myUnqualifiedToQualifiedNames; + } + + /** + * Returns a map which can be used to hold any user specific data to pass it from one + * part of the request handling chain to another. Data in this map can use any key, although + * user code should try to use keys which are specific enough to avoid conflicts. + *

+ * A new map is created for each individual request that is handled by the server, + * so this map can be used (for example) to pass authorization details from an interceptor + * to the resource providers, or from an interceptor's {@link IServerInterceptor#incomingRequestPreHandled(RestOperationTypeEnum, ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails)} + * method to the {@link IServerInterceptor#outgoingResponse(RequestDetails, org.hl7.fhir.instance.model.api.IBaseResource, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)} + * method. + *

+ */ + public Map getUserData() { + if (myUserData == null) { + myUserData = new HashMap<>(); + } + return myUserData; + } + + public boolean isRespondGzip() { + return myRespondGzip; + } + + public void setRespondGzip(boolean theRespondGzip) { + myRespondGzip = theRespondGzip; + } + + /** + * Is this request a sub-request (i.e. a request within a batch or transaction)? This + * flag is used internally by hapi-fhir-jpaserver-base, but not used in the plain server + * library. You may use it in your client code as a hint when implementing transaction logic in the plain + * server. + *

+ * Defaults to {@literal false} + *

+ */ + public boolean isSubRequest() { + return mySubRequest; + } + + /** + * Is this request a sub-request (i.e. a request within a batch or transaction)? This + * flag is used internally by hapi-fhir-jpaserver-base, but not used in the plain server + * library. You may use it in your client code as a hint when implementing transaction logic in the plain + * server. + *

+ * Defaults to {@literal false} + *

+ */ + public void setSubRequest(boolean theSubRequest) { + mySubRequest = theSubRequest; + } + + public final byte[] loadRequestContents() { + if (myRequestContents == null) { + myRequestContents = getByteStreamRequestContents(); + } + return myRequestContents; + } + private class RequestOperationCallback implements IRequestOperationCallback { private List getInterceptors() { @@ -73,6 +375,55 @@ public abstract class RequestDetails { } } + @Override + public void resourcePreCreate(IBaseResource theResource) { + for (IServerInterceptor next : getInterceptors()) { + if (next instanceof IServerOperationInterceptor) { + ((IServerOperationInterceptor) next).resourcePreCreate(RequestDetails.this, theResource); + } + } + } + + @Override + public void resourcePreDelete(IBaseResource theResource) { + for (IServerInterceptor next : getInterceptors()) { + if (next instanceof IServerOperationInterceptor) { + ((IServerOperationInterceptor) next).resourcePreDelete(RequestDetails.this, theResource); + } + } + } + + @Override + public void resourcePreUpdate(IBaseResource theOldResource, IBaseResource theNewResource) { + for (IServerInterceptor next : getInterceptors()) { + if (next instanceof IServerOperationInterceptor) { + ((IServerOperationInterceptor) next).resourcePreUpdate(RequestDetails.this, theOldResource, theNewResource); + } + } + } + + /** + * @deprecated Deprecated in HAPI FHIR 2.6 - Use {@link IRequestOperationCallback#resourceUpdated(IBaseResource, IBaseResource)} instead + */ + @Deprecated + @Override + public void resourceUpdated(IBaseResource theResource) { + for (IServerInterceptor next : getInterceptors()) { + if (next instanceof IServerOperationInterceptor) { + ((IServerOperationInterceptor) next).resourceUpdated(RequestDetails.this, theResource); + } + } + } + + @Override + public void resourceUpdated(IBaseResource theOldResource, IBaseResource theNewResource) { + for (IServerInterceptor next : getInterceptors()) { + if (next instanceof IServerOperationInterceptor) { + ((IServerOperationInterceptor) next).resourceUpdated(RequestDetails.this, theOldResource, theNewResource); + } + } + } + @Override public void resourcesCreated(Collection theResource) { for (IBaseResource next : theResource) { @@ -97,345 +448,6 @@ public abstract class RequestDetails { } } - - /** - * @deprecated Deprecated in HAPI FHIR 2.6 - Use {@link IRequestOperationCallback#resourceUpdated(IBaseResource, IBaseResource)} instead - */ - @Deprecated - @Override - public void resourceUpdated(IBaseResource theResource) { - for (IServerInterceptor next : getInterceptors()) { - if (next instanceof IServerOperationInterceptor) { - ((IServerOperationInterceptor) next).resourceUpdated(RequestDetails.this, theResource); - } - } - } - - @Override - public void resourceUpdated(IBaseResource theOldResource, IBaseResource theNewResource) { - for (IServerInterceptor next : getInterceptors()) { - if (next instanceof IServerOperationInterceptor) { - ((IServerOperationInterceptor) next).resourceUpdated(RequestDetails.this, theOldResource, theNewResource); - } - } - } - - } - private String myCompartmentName; - private String myCompleteUrl; - private String myFhirServerBase; - private IIdType myId; - private String myOperation; - private Map myParameters; - private byte[] myRequestContents; - private IRequestOperationCallback myRequestOperationCallback = new RequestOperationCallback(); - private String myRequestPath; - private RequestTypeEnum myRequestType; - private String myResourceName; - private boolean myRespondGzip; - private IRestfulResponse myResponse; - private RestOperationTypeEnum myRestOperationType; - private String mySecondaryOperation; - private boolean mySubRequest; - private Map> myUnqualifiedToQualifiedNames; - private Map myUserData; - - protected abstract byte[] getByteStreamRequestContents(); - - /** - * Return the charset as defined by the header contenttype. Return null if it is not set. - */ - public abstract Charset getCharset(); - - public String getCompartmentName() { - return myCompartmentName; - } - public String getCompleteUrl() { - return myCompleteUrl; - } - - /** - * Returns the conditional URL if this request has one, or null otherwise. For an - * update or delete method, this is the part of the URL after the ?. For a create, this - * is the value of the If-None-Exist header. - * - * @param theOperationType The operation type to find the conditional URL for - * @return Returns the conditional URL if this request has one, or null otherwise - */ - public String getConditionalUrl(RestOperationTypeEnum theOperationType) { - if (theOperationType == RestOperationTypeEnum.CREATE) { - String retVal = this.getHeader(Constants.HEADER_IF_NONE_EXIST); - if (isBlank(retVal)) { - return null; - } - if (retVal.startsWith(this.getFhirServerBase())) { - retVal = retVal.substring(this.getFhirServerBase().length()); - } - return retVal; - } else if (theOperationType != RestOperationTypeEnum.DELETE && theOperationType != RestOperationTypeEnum.UPDATE) { - return null; - } - - if (this.getId() != null && this.getId().hasIdPart()) { - return null; - } - - int questionMarkIndex = this.getCompleteUrl().indexOf('?'); - if (questionMarkIndex == -1) { - return null; - } - - return this.getResourceName() + this.getCompleteUrl().substring(questionMarkIndex); - } - - /** - * Returns the HAPI FHIR Context associated with this request - */ - public abstract FhirContext getFhirContext(); - - /** - * The fhir server base url, independant of the query being executed - * - * @return the fhir server base url - */ - public String getFhirServerBase() { - return myFhirServerBase; - } - - public abstract String getHeader(String name); - - public abstract List getHeaders(String name); - - public IIdType getId() { - return myId; - } - - /** - * Retrieves the body of the request as binary data. Either this method or {@link #getReader} may be called to read - * the body, not both. - * - * @return a {@link InputStream} object containing the body of the request - * - * @exception IllegalStateException - * if the {@link #getReader} method has already been called for this request - * - * @exception IOException - * if an input or output exception occurred - */ - public abstract InputStream getInputStream() throws IOException; - - public String getOperation() { - return myOperation; - } - - public Map getParameters() { - if (myParameters == null) { - return Collections.emptyMap(); - } - return myParameters; - } - - /** - * Retrieves the body of the request as character data using a BufferedReader. The reader translates the - * character data according to the character encoding used on the body. Either this method or {@link #getInputStream} - * may be called to read the body, not both. - * - * @return a Reader containing the body of the request - * - * @exception UnsupportedEncodingException - * if the character set encoding used is not supported and the text cannot be decoded - * - * @exception IllegalStateException - * if {@link #getInputStream} method has been called on this request - * - * @exception IOException - * if an input or output exception occurred - * - * @see javax.servlet.http.HttpServletRequest#getInputStream - */ - public abstract Reader getReader() throws IOException; - - /** - * Returns an invoker that can be called from user code to advise the server interceptors - * of any nested operations being invoked within operations. This invoker acts as a proxy for - * all interceptors - */ - public IRequestOperationCallback getRequestOperationCallback() { - return myRequestOperationCallback; - } - - /** - * The part of the request URL that comes after the server base. - *

- * Will not contain a leading '/' - *

- */ - public String getRequestPath() { - return myRequestPath; - } - - public RequestTypeEnum getRequestType() { - return myRequestType; - } - - public String getResourceName() { - return myResourceName; - } - - public IRestfulResponse getResponse() { - return myResponse; - } - - public RestOperationTypeEnum getRestOperationType() { - return myRestOperationType; - } - - public String getSecondaryOperation() { - return mySecondaryOperation; - } - - public abstract IRestfulServerDefaults getServer(); - - /** - * Returns the server base URL (with no trailing '/') for a given request - */ - public abstract String getServerBaseForRequest(); - - public Map> getUnqualifiedToQualifiedNames() { - return myUnqualifiedToQualifiedNames; - } - - /** - * Returns a map which can be used to hold any user specific data to pass it from one - * part of the request handling chain to another. Data in this map can use any key, although - * user code should try to use keys which are specific enough to avoid conflicts. - *

- * A new map is created for each individual request that is handled by the server, - * so this map can be used (for example) to pass authorization details from an interceptor - * to the resource providers, or from an interceptor's {@link IServerInterceptor#incomingRequestPreHandled(RestOperationTypeEnum, ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails)} - * method to the {@link IServerInterceptor#outgoingResponse(RequestDetails, org.hl7.fhir.instance.model.api.IBaseResource, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)} - * method. - *

- */ - public Map getUserData() { - if (myUserData == null) { - myUserData = new HashMap(); - } - return myUserData; - } - - public boolean isRespondGzip() { - return myRespondGzip; - } - - /** - * Is this request a sub-request (i.e. a request within a batch or transaction)? This - * flag is used internally by hapi-fhir-jpaserver-base, but not used in the plain server - * library. You may use it in your client code as a hint when implementing transaction logic in the plain - * server. - *

- * Defaults to {@literal false} - *

- */ - public boolean isSubRequest() { - return mySubRequest; - } - - public final byte[] loadRequestContents() { - if (myRequestContents == null) { - myRequestContents = getByteStreamRequestContents(); - } - return myRequestContents; - } - - public void setCompartmentName(String theCompartmentName) { - myCompartmentName = theCompartmentName; - } - - public void setCompleteUrl(String theCompleteUrl) { - myCompleteUrl = theCompleteUrl; - } - - public void setFhirServerBase(String theFhirServerBase) { - myFhirServerBase = theFhirServerBase; - } - - public void setId(IIdType theId) { - myId = theId; - } - - public void setOperation(String theOperation) { - myOperation = theOperation; - } - - public void setParameters(Map theParams) { - myParameters = theParams; - - for (String next : theParams.keySet()) { - for (int i = 0; i < next.length(); i++) { - char nextChar = next.charAt(i); - if (nextChar == ':' || nextChar == '.') { - if (myUnqualifiedToQualifiedNames == null) { - myUnqualifiedToQualifiedNames = new HashMap>(); - } - String unqualified = next.substring(0, i); - List list = myUnqualifiedToQualifiedNames.get(unqualified); - if (list == null) { - list = new ArrayList(4); - myUnqualifiedToQualifiedNames.put(unqualified, list); - } - list.add(next); - break; - } - } - } - - if (myUnqualifiedToQualifiedNames == null) { - myUnqualifiedToQualifiedNames = Collections.emptyMap(); - } - - } - - public void setRequestPath(String theRequestPath) { - assert theRequestPath.length() == 0 || theRequestPath.charAt(0) != '/'; - myRequestPath = theRequestPath; - } - - public void setRequestType(RequestTypeEnum theRequestType) { - myRequestType = theRequestType; - } - - public void setResourceName(String theResourceName) { - myResourceName = theResourceName; - } - - public void setRespondGzip(boolean theRespondGzip) { - myRespondGzip = theRespondGzip; - } - - public void setResponse(IRestfulResponse theResponse) { - this.myResponse = theResponse; - } - - public void setRestOperationType(RestOperationTypeEnum theRestOperationType) { - myRestOperationType = theRestOperationType; - } - - public void setSecondaryOperation(String theSecondaryOperation) { - mySecondaryOperation = theSecondaryOperation; - } - - /** - * Is this request a sub-request (i.e. a request within a batch or transaction)? This - * flag is used internally by hapi-fhir-jpaserver-base, but not used in the plain server - * library. You may use it in your client code as a hint when implementing transaction logic in the plain - * server. - *

- * Defaults to {@literal false} - *

- */ - public void setSubRequest(boolean theSubRequest) { - mySubRequest = theSubRequest; } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/IServerOperationInterceptor.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/IServerOperationInterceptor.java index 0ba634e43ca..0902ed13df1 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/IServerOperationInterceptor.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/IServerOperationInterceptor.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.server.interceptor; * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -20,49 +20,103 @@ package ca.uhn.fhir.rest.server.interceptor; * #L% */ -import org.hl7.fhir.instance.model.api.IBaseResource; - import ca.uhn.fhir.rest.api.server.RequestDetails; +import org.hl7.fhir.instance.model.api.IBaseResource; /** * Server interceptor with added methods which can be called within the lifecycle of * write operations (create/update/delete) or within transaction and batch * operations that call these sub-operations. - * + * * @see ServerOperationInterceptorAdapter */ public interface IServerOperationInterceptor extends IServerInterceptor { /** - * User code may call this method to indicate to an interceptor that - * a resource is being created + * This method is called by the server immediately after a resource has + * been created, within the database transaction scope of the operation. + *

+ * If an exception is thrown by an interceptor during this method, + * the transaction will be rolled back. + *

*/ void resourceCreated(RequestDetails theRequest, IBaseResource theResource); /** - * User code may call this method to indicate to an interceptor that - * a resource is being deleted + * This method is called by the server immediately after a resource has + * been deleted, within the database transaction scope of the operation. + *

+ * If an exception is thrown by an interceptor during this method, + * the transaction will be rolled back. + *

*/ void resourceDeleted(RequestDetails theRequest, IBaseResource theResource); /** - * User code may call this method to indicate to an interceptor that - * a resource is being updated - * + * This method is called by the server immediately before a resource is about + * to be created, within the database transaction scope of the operation. + *

+ * This method may be used to modify the resource + *

+ *

+ * If an exception is thrown by an interceptor during this method, + * the transaction will be rolled back. + *

+ * + * @param theResource The resource that has been provided by the client as the payload + * to create. Interceptors may modify this + * resource, and modifications will affect what is saved in the database. + */ + void resourcePreCreate(RequestDetails theRequest, IBaseResource theResource); + + /** + * This method is called by the server immediately before a resource is about + * to be deleted, within the database transaction scope of the operation. + *

+ * If an exception is thrown by an interceptor during this method, + * the transaction will be rolled back. + *

+ * + * @param theResource The resource which is about to be deleted + */ + void resourcePreDelete(RequestDetails theRequest, IBaseResource theResource); + + /** + * This method is called by the server immediately before a resource is about + * to be updated, within the database transaction scope of the operation. + *

+ * This method may be used to modify the resource + *

+ *

+ * If an exception is thrown by an interceptor during this method, + * the transaction will be rolled back. + *

+ * + * @param theOldResource The previous version of the resource, or null if this is not available. Interceptors should be able to handle situations where this is null, since it is not always + * convenient or possible to provide a value for this field, but servers should try to populate it. + * @param theNewResource The resource that has been provided by the client as the payload + * to update to the resource to. Interceptors may modify this + * resource, and modifications will affect what is saved in the database. + */ + void resourcePreUpdate(RequestDetails theRequest, IBaseResource theOldResource, IBaseResource theNewResource); + + /** * @deprecated Deprecated in HAPI FHIR 3.0.0 in favour of {@link #resourceUpdated(RequestDetails, IBaseResource, IBaseResource)} */ @Deprecated void resourceUpdated(RequestDetails theRequest, IBaseResource theResource); /** - * User code may call this method to indicate to an interceptor that - * a resource is being updated - * - * @param theOldResource - * The resource as it was before the update, or null if this is not available. Interceptors should be able to handle situations where this is null, since it is not always - * convenient or possible to provide a value for this field, but servers should try to populate it. - * @param theNewResource - * The resource as it will be after the update + * This method is called by the server immediately after a resource has + * been created, within the database transaction scope of the operation. + *

+ * If an exception is thrown by an interceptor during this method, + * the transaction will be rolled back. + *

+ * + * @param theOldResource The resource as it was before the update, or null if this is not available. Interceptors should be able to handle situations where this is null, since it is not always + * convenient or possible to provide a value for this field, but servers should try to populate it. + * @param theNewResource The resource as it will be after the update */ void resourceUpdated(RequestDetails theRequest, IBaseResource theOldResource, IBaseResource theNewResource); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ServerOperationInterceptorAdapter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ServerOperationInterceptorAdapter.java index d7894b549de..848b6da6d17 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ServerOperationInterceptorAdapter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ServerOperationInterceptorAdapter.java @@ -34,6 +34,21 @@ public class ServerOperationInterceptorAdapter extends InterceptorAdapter implem // nothing } + @Override + public void resourcePreCreate(RequestDetails theRequest, IBaseResource theResource) { + // nothing + } + + @Override + public void resourcePreDelete(RequestDetails theRequest, IBaseResource theResource) { + // nothing + } + + @Override + public void resourcePreUpdate(RequestDetails theRequest, IBaseResource theOldResource, IBaseResource theNewResource) { + // nothing + } + @Override public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) { // nothing diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 5ce2c7b7dd0..9c0e88f7246 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -49,6 +49,19 @@ Fix an issue in JPA server where updating a resource sometimes caused date search indexes to be incorrectly deleted. Thanks to Kyle Meadows for the pull request! + + A new set of methods have been added to + IServerOperationInterceptor]]> + called + resourcePreCreate]]>, + resourcePreUpdate]]>, and + resourcePreDelete]]>. These + methods are called within the database transaction + (just as the existing methods were) but are invoked + prior to the contents being saved to the database. This + can be useful in order to allow interceptors to + change payload contents being saved. +