Added new callbacks to IServerOperationInterceptor to be invoked before

other operation methods
This commit is contained in:
James Agnew 2017-12-23 17:13:33 -05:00
parent fe37c87e78
commit b18e71d4f5
7 changed files with 790 additions and 493 deletions

View File

@ -196,6 +196,16 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> 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<T extends IBaseResource> 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<T extends IBaseResource> 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<T extends IBaseResource> 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<T extends IBaseResource> 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<T extends IBaseResource> 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<T extends IBaseResource> 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);
/*

View File

@ -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<IIdType> 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<Object>() {
@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<Void>() {
@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<Void>() {
@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<Void>() {
@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<Void>() {
@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<Void>() {
@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<Void>() {
@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<Void>() {
@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<Void>() {
@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;
}
}
}

View File

@ -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<? extends IBaseResource> theResource);
void resourcesDeleted(Collection<? extends IBaseResource> theResource);
@ -39,12 +53,4 @@ public interface IRequestOperationCallback {
*/
@Deprecated
void resourcesUpdated(Collection<? extends IBaseResource> 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);
}

View File

@ -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<String, String[]> 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<String, List<String>> myUnqualifiedToQualifiedNames;
private Map<Object, Object> 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 <b>conditional URL</b> if this request has one, or <code>null</code> otherwise. For an
* update or delete method, this is the part of the URL after the <code>?</code>. For a create, this
* is the value of the <code>If-None-Exist</code> header.
*
* @param theOperationType The operation type to find the conditional URL for
* @return Returns the <b>conditional URL</b> if this request has one, or <code>null</code> 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<String> 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<String, String[]> getParameters() {
if (myParameters == null) {
return Collections.emptyMap();
}
return myParameters;
}
public void setParameters(Map<String, String[]> 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<String> 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 <code>BufferedReader</code>. 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 <code>Reader</code> 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.
* <p>
* Will not contain a leading '/'
* </p>
*/
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<String, List<String>> 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.
* <p>
* 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.
* </p>
*/
public Map<Object, Object> 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.
* <p>
* Defaults to {@literal false}
* </p>
*/
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.
* <p>
* Defaults to {@literal false}
* </p>
*/
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<IServerInterceptor> 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<? extends IBaseResource> 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<String, String[]> 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<String, List<String>> myUnqualifiedToQualifiedNames;
private Map<Object, Object> 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 <b>conditional URL</b> if this request has one, or <code>null</code> otherwise. For an
* update or delete method, this is the part of the URL after the <code>?</code>. For a create, this
* is the value of the <code>If-None-Exist</code> header.
*
* @param theOperationType The operation type to find the conditional URL for
* @return Returns the <b>conditional URL</b> if this request has one, or <code>null</code> 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<String> 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<String, String[]> getParameters() {
if (myParameters == null) {
return Collections.emptyMap();
}
return myParameters;
}
/**
* Retrieves the body of the request as character data using a <code>BufferedReader</code>. 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 <code>Reader</code> 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.
* <p>
* Will not contain a leading '/'
* </p>
*/
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<String, List<String>> 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.
* <p>
* 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.
* </p>
*/
public Map<Object, Object> getUserData() {
if (myUserData == null) {
myUserData = new HashMap<Object, Object>();
}
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.
* <p>
* Defaults to {@literal false}
* </p>
*/
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<String, String[]> 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, List<String>>();
}
String unqualified = next.substring(0, i);
List<String> list = myUnqualifiedToQualifiedNames.get(unqualified);
if (list == null) {
list = new ArrayList<String>(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.
* <p>
* Defaults to {@literal false}
* </p>
*/
public void setSubRequest(boolean theSubRequest) {
mySubRequest = theSubRequest;
}
}

View File

@ -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.
* <p>
* If an exception is thrown by an interceptor during this method,
* the transaction will be rolled back.
* </p>
*/
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.
* <p>
* If an exception is thrown by an interceptor during this method,
* the transaction will be rolled back.
* </p>
*/
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.
* <p>
* This method may be used to modify the resource
* </p>
* <p>
* If an exception is thrown by an interceptor during this method,
* the transaction will be rolled back.
* </p>
*
* @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.
* <p>
* If an exception is thrown by an interceptor during this method,
* the transaction will be rolled back.
* </p>
*
* @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.
* <p>
* This method may be used to modify the resource
* </p>
* <p>
* If an exception is thrown by an interceptor during this method,
* the transaction will be rolled back.
* </p>
*
* @param theOldResource The previous version of the resource, or <code>null</code> 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 <code>null</code> 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.
* <p>
* If an exception is thrown by an interceptor during this method,
* the transaction will be rolled back.
* </p>
*
* @param theOldResource The resource as it was before the update, or <code>null</code> 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);

View File

@ -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

View File

@ -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!
</action>
<action type="add">
A new set of methods have been added to
<![CDATA[<code>IServerOperationInterceptor</code>]]>
called
<![CDATA[<code>resourcePreCreate</code>]]>,
<![CDATA[<code>resourcePreUpdate</code>]]>, and
<![CDATA[<code>resourcePreDelete</code>]]>. 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.
</action>
</release>
<release version="3.1.0" date="2017-11-23">
<action type="add">