Allow fluent client to handle return types other than Parameters when

invoking operations
This commit is contained in:
James Agnew 2016-10-07 11:29:53 -04:00
parent 5384af1004
commit 2c4139dc82
10 changed files with 2891 additions and 61 deletions

View File

@ -10,7 +10,7 @@ package ca.uhn.fhir.rest.client;
* 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
* 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,
@ -243,7 +243,8 @@ public class GenericClient extends BaseClient implements IGenericClient {
return delete(theType, new IdDt(theId));
}
private <T extends IBaseResource> T doReadOrVRead(final Class<T> theType, IIdType theId, boolean theVRead, ICallable<T> theNotModifiedHandler, String theIfVersionMatches, Boolean thePrettyPrint, SummaryEnum theSummary, EncodingEnum theEncoding, Set<String> theSubsetElements) {
private <T extends IBaseResource> T doReadOrVRead(final Class<T> theType, IIdType theId, boolean theVRead, ICallable<T> theNotModifiedHandler, String theIfVersionMatches, Boolean thePrettyPrint,
SummaryEnum theSummary, EncodingEnum theEncoding, Set<String> theSubsetElements) {
String resName = toResourceName(theType);
IIdType id = theId;
if (!id.hasBaseUrl()) {
@ -394,8 +395,8 @@ public class GenericClient extends BaseClient implements IGenericClient {
/**
* @deprecated Use {@link LoggingInterceptor} as a client interceptor registered to your
* client instead, as this provides much more fine-grained control over what is logged. This
* method will be removed at some point (deprecated in HAPI 1.6 - 2016-06-16)
* client instead, as this provides much more fine-grained control over what is logged. This
* method will be removed at some point (deprecated in HAPI 1.6 - 2016-06-16)
*/
@Deprecated
public boolean isLogRequestAndResponse() {
@ -526,7 +527,6 @@ public class GenericClient extends BaseClient implements IGenericClient {
return new ArrayList<IBaseResource>(resp.toListOfResources());
}
@Override
public IPatch patch() {
return new PatchInternal();
@ -1483,6 +1483,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
private RuntimeResourceDefinition myParametersDef;
private Class<? extends IBaseResource> myType;
private boolean myUseHttpGet;
private Class myReturnResourceType;
@SuppressWarnings("unchecked")
private void addParam(String theName, IBase theValue) {
@ -1552,25 +1553,33 @@ public class GenericClient extends BaseClient implements IGenericClient {
BaseHttpClientInvocation invocation = OperationMethodBinding.createOperationInvocation(myContext, resourceName, id, myOperationName, myParameters, myUseHttpGet);
ResourceResponseHandler handler = new ResourceResponseHandler();
handler.setPreferResponseTypes(getPreferResponseTypes(myType));
Object retVal = invoke(null, handler, invocation);
if (myContext.getResourceDefinition((IBaseResource) retVal).getName().equals("Parameters")) {
if (myReturnResourceType != null) {
ResourceResponseHandler handler;
handler = new ResourceResponseHandler(myReturnResourceType);
Object retVal = invoke(null, handler, invocation);
return retVal;
} else {
RuntimeResourceDefinition def = myContext.getResourceDefinition("Parameters");
IBaseResource parameters = def.newInstance();
ResourceResponseHandler handler;
handler = new ResourceResponseHandler();
handler.setPreferResponseTypes(getPreferResponseTypes(myType));
BaseRuntimeChildDefinition paramChild = def.getChildByName("parameter");
BaseRuntimeElementCompositeDefinition<?> paramChildElem = (BaseRuntimeElementCompositeDefinition<?>) paramChild.getChildByName("parameter");
IBase parameter = paramChildElem.newInstance();
paramChild.getMutator().addValue(parameters, parameter);
Object retVal = invoke(null, handler, invocation);
if (myContext.getResourceDefinition((IBaseResource) retVal).getName().equals("Parameters")) {
return retVal;
} else {
RuntimeResourceDefinition def = myContext.getResourceDefinition("Parameters");
IBaseResource parameters = def.newInstance();
BaseRuntimeChildDefinition resourceElem = paramChildElem.getChildByName("resource");
resourceElem.getMutator().addValue(parameter, (IBase) retVal);
BaseRuntimeChildDefinition paramChild = def.getChildByName("parameter");
BaseRuntimeElementCompositeDefinition<?> paramChildElem = (BaseRuntimeElementCompositeDefinition<?>) paramChild.getChildByName("parameter");
IBase parameter = paramChildElem.newInstance();
paramChild.getMutator().addValue(parameters, parameter);
return parameters;
BaseRuntimeChildDefinition resourceElem = paramChildElem.getChildByName("resource");
resourceElem.getMutator().addValue(parameter, (IBase) retVal);
return parameters;
}
}
}
@ -1613,7 +1622,8 @@ public class GenericClient extends BaseClient implements IGenericClient {
throw new IllegalArgumentException("theOutputParameterType must refer to a HAPI FHIR Resource type: " + theOutputParameterType.getName());
}
if (!"Parameters".equals(def.getName())) {
throw new IllegalArgumentException("theOutputParameterType must refer to a HAPI FHIR Resource type for a resource named " + "Parameters" + " - " + theOutputParameterType.getName() + " is a resource named: " + def.getName());
throw new IllegalArgumentException("theOutputParameterType must refer to a HAPI FHIR Resource type for a resource named " + "Parameters" + " - " + theOutputParameterType.getName()
+ " is a resource named: " + def.getName());
}
myParameters = (IBaseParameters) def.newInstance();
return this;
@ -1657,12 +1667,21 @@ public class GenericClient extends BaseClient implements IGenericClient {
return this;
}
@Override
public IOperationUntypedWithInput returnResourceType(Class theReturnType) {
Validate.notNull(theReturnType, "theReturnType must not be null");
Validate.isTrue(IBaseResource.class.isAssignableFrom(theReturnType), "theReturnType must be a class which extends from IBaseResource");
myReturnResourceType = theReturnType;
return this;
}
}
private final class OperationOutcomeResponseHandler implements IClientResponseHandler<IBaseOperationOutcome> {
@Override
public IBaseOperationOutcome invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws BaseServerResponseException {
public IBaseOperationOutcome invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders)
throws BaseServerResponseException {
EncodingEnum respType = EncodingEnum.forContentType(theResponseMimeType);
if (respType == null) {
return null;
@ -1858,7 +1877,8 @@ public class GenericClient extends BaseClient implements IGenericClient {
@SuppressWarnings("unchecked")
@Override
public List<IBaseResource> invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws BaseServerResponseException {
public List<IBaseResource> invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders)
throws BaseServerResponseException {
if (myContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU1)) {
Class<? extends IBaseResource> bundleType = myContext.getResourceDefinition("Bundle").getImplementingClass();
ResourceResponseHandler<IBaseResource> handler = new ResourceResponseHandler<IBaseResource>((Class<IBaseResource>) bundleType);
@ -2011,7 +2031,8 @@ public class GenericClient extends BaseClient implements IGenericClient {
}
if (myReturnBundleType == null && myContext.getVersion().getVersion().isRi()) {
throw new IllegalArgumentException("When using the client with HL7.org structures, you must specify " + "the bundle return type for the client by adding \".returnBundle(org.hl7.fhir.instance.model.Bundle.class)\" to your search method call before the \".execute()\" method");
throw new IllegalArgumentException("When using the client with HL7.org structures, you must specify "
+ "the bundle return type for the client by adding \".returnBundle(org.hl7.fhir.instance.model.Bundle.class)\" to your search method call before the \".execute()\" method");
}
IClientResponseHandler<? extends IBase> binding;
@ -2212,7 +2233,8 @@ public class GenericClient extends BaseClient implements IGenericClient {
private final class StringResponseHandler implements IClientResponseHandler<String> {
@Override
public String invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws IOException, BaseServerResponseException {
public String invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders)
throws IOException, BaseServerResponseException {
return IOUtils.toString(theResponseReader);
}
}
@ -2367,7 +2389,6 @@ public class GenericClient extends BaseClient implements IGenericClient {
throw new InvalidRequestException("No patch body supplied, cannot invoke server");
}
if (myId == null) {
myId = myResource.getIdElement();
}

View File

@ -48,6 +48,17 @@ public interface IOperationUntyped {
* Use chained method calls to construct a Parameters input. This form is a convenience
* in order to allow simple method chaining to be used to build up a parameters
* resource for the input of an operation without needing to manually construct one.
* <p>
* A sample invocation of this class could look like:<br/>
* <pre>Bundle bundle = client.operation()
* .onInstance(new IdType("Patient/A161443"))
* .named("everything")
* .withParameter(Parameters.class, "_count", new IntegerType(50))
* .useHttpGet()
* .returnResourceType(Bundle.class)
* .execute();
* </pre>
* </p>
*
* @param theParameterType The type to use for the output parameters (this should be set to
* <code>Parameters.class</code> drawn from the version of the FHIR structures you are using)

View File

@ -1,28 +1,8 @@
package ca.uhn.fhir.rest.gclient;
/*
* #%L
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2016 University Health Network
* %%
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IBaseParameters;
public interface IOperationUntypedWithInput<T extends IBaseParameters> extends IClientExecutable<IOperationUntypedWithInput<T>, T> {
public interface IOperationUntypedWithInput<T> extends IClientExecutable<IOperationUntypedWithInput<T>, T> {
/**
* The client should invoke this method using an HTTP GET instead of an HTTP POST. Note that
@ -35,4 +15,12 @@ public interface IOperationUntypedWithInput<T extends IBaseParameters> extends I
*/
IOperationUntypedWithInput<T> useHttpGet();
/**
* If this operation returns a single resource body as its return type instead of a <code>Parameters</code>
* resource, use this method to specify that resource type. This is useful for certain
* operations (e.g. <code>Patient/NNN/$everything</code>) which return a bundle instead of
* a Parameters resource.
*/
<R extends IBaseResource> IOperationUntypedWithInput<R> returnResourceType(Class<R> theReturnType);
}

View File

@ -27,7 +27,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.env.Environment;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@ -38,12 +37,8 @@ import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler;
import org.springframework.scheduling.concurrent.ScheduledExecutorFactoryBean;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.ParserOptions;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.StaleSearchDeletingSvc;
import ca.uhn.fhir.jpa.term.BaseHapiTerminologySvc;
import ca.uhn.fhir.jpa.term.IHapiTerminologySvc;
@Configuration
@EnableScheduling
@ -64,11 +59,10 @@ public class BaseConfig implements SchedulingConfigurer {
@Bean(autowire = Autowire.BY_TYPE)
public DatabaseBackedPagingProvider databaseBackedPagingProvider() {
return new DatabaseBackedPagingProvider(10);
DatabaseBackedPagingProvider retVal = new DatabaseBackedPagingProvider(10);
return retVal;
}
@Bean(autowire=Autowire.BY_TYPE)
public StaleSearchDeletingSvc staleSearchDeletingSvc() {
return new StaleSearchDeletingSvc();

View File

@ -6,6 +6,7 @@ import static org.hamcrest.Matchers.containsInRelativeOrder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
@ -25,6 +26,9 @@ import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.boot.model.source.spi.IdentifierSourceSimple;
import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent;
import org.hl7.fhir.dstu3.model.Bundle.BundleType;
import org.hl7.fhir.dstu3.model.Bundle.HTTPVerb;
import org.hl7.fhir.dstu3.model.ContactPoint.ContactPointSystem;
import org.hl7.fhir.dstu3.model.Enumerations.AdministrativeGender;
import org.hl7.fhir.dstu3.model.Subscription.SubscriptionChannelType;
@ -32,6 +36,7 @@ import org.hl7.fhir.dstu3.model.Subscription.SubscriptionStatus;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.junit.AfterClass;
import org.junit.Ignore;
import org.junit.Test;
@ -41,6 +46,7 @@ import org.springframework.transaction.support.TransactionTemplate;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
import ca.uhn.fhir.jpa.dao.SearchParameterMap;
import ca.uhn.fhir.jpa.dao.SearchParameterMap.EverythingModeEnum;
import ca.uhn.fhir.jpa.entity.*;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.model.api.Include;
@ -122,6 +128,54 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test {
assertThat(ids, contains(moId.getValue()));
}
/**
* Per message from David Hay on Skype
*/
@Test
public void testEverythingWithLargeSet() throws Exception {
String inputString = IOUtils.toString(getClass().getResourceAsStream("/david_big_bundle.json"), StandardCharsets.UTF_8);
Bundle inputBundle = myFhirCtx.newJsonParser().parseResource(Bundle.class, inputString);
inputBundle.setType(BundleType.TRANSACTION);
Set<String> allIds = new TreeSet<String>();
for (BundleEntryComponent nextEntry : inputBundle.getEntry()) {
nextEntry.getRequest().setMethod(HTTPVerb.PUT);
nextEntry.getRequest().setUrl(nextEntry.getResource().getId());
allIds.add(nextEntry.getResource().getIdElement().toUnqualifiedVersionless().getValue());
}
mySystemDao.transaction(mySrd, inputBundle);
SearchParameterMap map = new SearchParameterMap();
map.setEverythingMode(EverythingModeEnum.PATIENT_INSTANCE);
IPrimitiveType<Integer> count = new IntegerType(1000);
IBundleProvider everything = myPatientDao.patientInstanceEverything(mySrd.getServletRequest(), new IdType("Patient/A161443"), count, null, null, null, null, mySrd);
TreeSet<String> ids = new TreeSet<String>(toUnqualifiedVersionlessIdValues(everything));
assertThat(ids, hasItem("List/A161444"));
assertThat(ids, hasItem("List/A161468"));
assertThat(ids, hasItem("List/A161500"));
ourLog.info("Expected {} - {}", allIds.size(), allIds);
ourLog.info("Actual {} - {}", ids.size(), ids);
assertEquals(allIds, ids);
ids = new TreeSet<String>();
for (int i = 0; i < everything.size(); i++) {
for (IBaseResource next : everything.getResources(i, i+1)) {
ids.add(next.getIdElement().toUnqualifiedVersionless().getValue());
}
}
assertThat(ids, hasItem("List/A161444"));
assertThat(ids, hasItem("List/A161468"));
assertThat(ids, hasItem("List/A161500"));
ourLog.info("Expected {} - {}", allIds.size(), allIds);
ourLog.info("Actual {} - {}", ids.size(), ids);
assertEquals(allIds, ids);
}
@Test
public void testCodeSearch() {
Subscription subs = new Subscription();

View File

@ -7,6 +7,7 @@ import static org.hamcrest.Matchers.containsInRelativeOrder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.not;
@ -28,9 +29,7 @@ import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.*;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
@ -57,12 +56,15 @@ import org.hl7.fhir.dstu3.model.Subscription.SubscriptionStatus;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.junit.AfterClass;
import org.junit.Ignore;
import org.junit.Test;
import com.google.common.collect.Lists;
import ca.uhn.fhir.jpa.dao.SearchParameterMap;
import ca.uhn.fhir.jpa.dao.SearchParameterMap.EverythingModeEnum;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.model.primitive.UriDt;
import ca.uhn.fhir.parser.IParser;
@ -73,6 +75,7 @@ import ca.uhn.fhir.rest.api.ValidationModeEnum;
import ca.uhn.fhir.rest.client.IGenericClient;
import ca.uhn.fhir.rest.param.*;
import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.rest.server.IBundleProvider;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
@ -115,6 +118,62 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
}
/**
* Per message from David Hay on Skype
*/
@Test
public void testEverythingWithLargeSet() throws Exception {
String inputString = IOUtils.toString(getClass().getResourceAsStream("/david_big_bundle.json"), StandardCharsets.UTF_8);
Bundle inputBundle = myFhirCtx.newJsonParser().parseResource(Bundle.class, inputString);
inputBundle.setType(BundleType.TRANSACTION);
Set<String> allIds = new TreeSet<String>();
for (BundleEntryComponent nextEntry : inputBundle.getEntry()) {
nextEntry.getRequest().setMethod(HTTPVerb.PUT);
nextEntry.getRequest().setUrl(nextEntry.getResource().getId());
allIds.add(nextEntry.getResource().getIdElement().toUnqualifiedVersionless().getValue());
}
mySystemDao.transaction(mySrd, inputBundle);
Bundle responseBundle = ourClient
.operation()
.onInstance(new IdType("Patient/A161443"))
.named("everything")
.withParameter(Parameters.class, "_count", new IntegerType(50))
.useHttpGet()
.returnResourceType(Bundle.class)
.execute();
TreeSet<String> ids = new TreeSet<String>();
for (int i = 0; i < responseBundle.getEntry().size(); i++) {
for (BundleEntryComponent nextEntry : responseBundle.getEntry()) {
ids.add(nextEntry.getResource().getIdElement().toUnqualifiedVersionless().getValue());
}
}
String nextUrl = responseBundle.getLink("next").getUrl();
responseBundle = ourClient.fetchResourceFromUrl(Bundle.class, nextUrl);
for (int i = 0; i < responseBundle.getEntry().size(); i++) {
for (BundleEntryComponent nextEntry : responseBundle.getEntry()) {
ids.add(nextEntry.getResource().getIdElement().toUnqualifiedVersionless().getValue());
}
}
assertThat(ids, hasItem("List/A161444"));
assertThat(ids, hasItem("List/A161468"));
assertThat(ids, hasItem("List/A161500"));
assertEquals(null, responseBundle.getLink("next"));
ourLog.info("Expected {} - {}", allIds.size(), allIds);
ourLog.info("Actual {} - {}", ids.size(), ids);
assertEquals(allIds, ids);
}
/**
* See #411
*

View File

@ -251,7 +251,7 @@ public class SystemProviderDstu3Test extends BaseJpaDstu3Test {
HttpGet get = new HttpGet(ourServerBase);
// get.addHeader("Accept", "application/xml, text/html");
CloseableHttpResponse http = ourHttpClient.execute(get);
assertThat(http.getFirstHeader("Content-Type").getValue(), containsString("application/json+fhir"));
assertThat(http.getFirstHeader("Content-Type").getValue(), containsString("application/fhir+json"));
}

File diff suppressed because it is too large Load Diff

View File

@ -19,6 +19,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
import ca.uhn.fhir.jpa.config.BaseJavaConfigDstu3;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.util.SubscriptionsRequireManualActivationInterceptorDstu3;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor;
@ -50,9 +51,20 @@ public class TestDstu3Config extends BaseJavaConfigDstu3 {
retVal.setAllowExternalReferences(true);
retVal.getTreatBaseUrlsAsLocal().add("http://fhirtest.uhn.ca/baseDstu3");
retVal.getTreatBaseUrlsAsLocal().add("https://fhirtest.uhn.ca/baseDstu3");
retVal.setHardSearchLimit(500);
return retVal;
}
@Override
@Bean(autowire = Autowire.BY_TYPE)
public DatabaseBackedPagingProvider databaseBackedPagingProvider() {
DatabaseBackedPagingProvider retVal = super.databaseBackedPagingProvider();
retVal.setDefaultPageSize(20);
retVal.setMaximumPageSize(500);
return retVal;
}
@Bean
public IServerInterceptor securityInterceptor() {
return new PublicSecurityInterceptor();

View File

@ -158,6 +158,10 @@
interceptors are accessing the parameters and there is are also
parameters on the URL. Thanks to Jim Steel for reporting!
</action>
<action type="add">
Fluent client can now return types other than Parameters
when invoking operations.
</action>
</release>
<release version="2.0" date="2016-08-30">
<action type="fix">