Allow operations to better work across multiple versions of FHIR

This commit is contained in:
jamesagnew 2019-07-26 05:46:12 -04:00
parent d03fc0f61d
commit 8158292665
5 changed files with 207 additions and 4 deletions

View File

@ -76,6 +76,14 @@ public @interface OperationParam {
*/
Class<? extends IBase> type() default IBase.class;
/**
* Optionally specifies the type of the parameter as a string, such as <code>Coding</code> or
* <code>base64Binary</code>. This can be useful if you want to use a generic interface type
* on the actual method,such as {@link org.hl7.fhir.instance.model.api.IPrimitiveType} or
* {@link @org.hl7.fhir.instance.model.api.ICompositeType}.
*/
String typeName() default "";
/**
* The minimum number of repetitions allowed for this child (default is 0)
*/

View File

@ -219,7 +219,15 @@ public class MethodUtil {
param = new ConditionalParamBinder(theRestfulOperationTypeEnum, ((ConditionalUrlParam) nextAnnotation).supportsMultiple());
} else if (nextAnnotation instanceof OperationParam) {
Operation op = theMethod.getAnnotation(Operation.class);
param = new OperationParameter(theContext, op.name(), ((OperationParam) nextAnnotation));
OperationParam operationParam = (OperationParam) nextAnnotation;
param = new OperationParameter(theContext, op.name(), operationParam);
if (isNotBlank(operationParam.typeName())) {
Class<?> newParameterType = theContext.getElementDefinition(operationParam.typeName()).getImplementingClass();
if (!parameterType.isAssignableFrom(newParameterType)) {
throw new ConfigurationException("Non assignable parameter typeName=\"" + operationParam.typeName() + "\" specified on method " + theMethod);
}
parameterType = newParameterType;
}
} else if (nextAnnotation instanceof Validate.Mode) {
if (parameterType.equals(ValidationModeEnum.class) == false) {
throw new ConfigurationException(

View File

@ -151,12 +151,10 @@ public class OperationParameter implements IParameter {
boolean typeIsConcrete = !myParameterType.isInterface() && !Modifier.isAbstract(myParameterType.getModifiers());
//@formatter:off
boolean isSearchParam =
IQueryParameterType.class.isAssignableFrom(myParameterType) ||
IQueryParameterOr.class.isAssignableFrom(myParameterType) ||
IQueryParameterAnd.class.isAssignableFrom(myParameterType);
//@formatter:off
/*
* Note: We say here !IBase.class.isAssignableFrom because a bunch of DSTU1/2 datatypes also

View File

@ -0,0 +1,185 @@
package ca.uhn.fhir.rest.server;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.test.utilities.JettyUtil;
import ca.uhn.fhir.util.TestUtil;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.ICompositeType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.*;
import org.junit.*;
import javax.servlet.ServletException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
public class OperationGenericServer2R4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OperationGenericServer2R4Test.class);
private static CloseableHttpClient ourClient;
private static FhirContext ourCtx;
private static IdType ourLastId;
private static Object ourLastParam1;
private static Object ourLastParam2;
private static Parameters ourLastResourceParam;
private int myPort;
private Server myServer;
@Before
public void before() {
ourLastParam1 = null;
ourLastParam2 = null;
ourLastId = null;
ourLastResourceParam = null;
}
@Test
public void testDeclarativeTypedParameters() throws Exception {
@SuppressWarnings("unused")
class PatientProvider implements IResourceProvider {
@Override
public Class<Patient> getResourceType() {
return Patient.class;
}
@Operation(name = "$OP_INSTANCE")
public Parameters opInstance(
@ResourceParam() IBaseResource theResourceParam,
@IdParam IdType theId,
@OperationParam(name = "PARAM1", typeName = "code") IPrimitiveType<String> theParam1,
@OperationParam(name = "PARAM2", typeName = "Coding") ICompositeType theParam2
) {
ourLastId = theId;
ourLastParam1 = theParam1;
ourLastParam2 = theParam2;
ourLastResourceParam = (Parameters) theResourceParam;
Parameters retVal = new Parameters();
retVal.addParameter().setName("RET1").setValue(new StringType("RETVAL1"));
return retVal;
}
}
PatientProvider provider = new PatientProvider();
startServer(provider);
Parameters p = new Parameters();
p.addParameter().setName("PARAM1").setValue(new CodeType("PARAM1val"));
p.addParameter().setName("PARAM2").setValue(new Coding("sys", "val", "dis"));
String inParamsStr = ourCtx.newXmlParser().encodeResourceToString(p);
HttpPost httpPost = new HttpPost("http://localhost:" + myPort + "/Patient/123/$OP_INSTANCE");
httpPost.setEntity(new StringEntity(inParamsStr, ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
try (CloseableHttpResponse status = ourClient.execute(httpPost)) {
assertEquals(200, status.getStatusLine().getStatusCode());
String response = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(response);
status.getEntity().getContent().close();
CodeType param1 = (CodeType) ourLastParam1;
assertEquals("PARAM1val", param1.getValue());
Coding param2 = (Coding) ourLastParam2;
assertEquals("sys", param2.getSystem());
assertEquals("val", param2.getCode());
assertEquals("dis", param2.getDisplay());
}
}
@Test
public void testDeclarativeTypedParametersInvalid() throws Exception {
@SuppressWarnings("unused")
class PatientProvider implements IResourceProvider {
@Override
public Class<Patient> getResourceType() {
return Patient.class;
}
@Operation(name = "$OP_INSTANCE")
public Parameters opInstance(
@OperationParam(name = "PARAM2", typeName = "code") ICompositeType theParam2
) {
return new Parameters();
}
}
try {
PatientProvider provider = new PatientProvider();
startServer(provider);
fail();
} catch (ServletException e) {
ConfigurationException ce = (ConfigurationException) e.getCause();
assertEquals("Failure scanning class PatientProvider: Non assignable parameter typeName=\"code\" specified on method public org.hl7.fhir.r4.model.Parameters ca.uhn.fhir.rest.server.OperationGenericServer2R4Test$2PatientProvider.opInstance(org.hl7.fhir.instance.model.api.ICompositeType)", ce.getMessage());
}
}
private void startServer(Object theProvider) throws Exception {
myServer = new Server(0);
ServletHandler proxyHandler = new ServletHandler();
RestfulServer servlet = new RestfulServer(ourCtx);
servlet.setDefaultResponseEncoding(EncodingEnum.XML);
servlet.setPagingProvider(new FifoMemoryPagingProvider(10).setDefaultPageSize(2));
servlet.setFhirContext(ourCtx);
servlet.registerProvider(theProvider);
ServletHolder servletHolder = new ServletHolder(servlet);
proxyHandler.addServletWithMapping(servletHolder, "/*");
myServer.setHandler(proxyHandler);
JettyUtil.startServer(myServer);
myPort = JettyUtil.getPortForStartedServer(myServer);
}
@After
public void after() throws Exception {
JettyUtil.closeServer(myServer);
}
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();
}
@BeforeClass
public static void beforeClass() {
ourCtx = FhirContext.forR4();
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS);
HttpClientBuilder builder = HttpClientBuilder.create();
builder.setConnectionManager(connectionManager);
ourClient = builder.build();
}
}

View File

@ -310,6 +310,11 @@
apply to all resource types (or instances of all resource types) if they
are found on a plain provider.
</action>
<action type="add">
@Operation method parameters may now declare their type via a String name such as
"code" or "Coding" in an attribute in @OperationParam. This is useful if you want
to make operation methods that can operate across different versions of FHIR.
</action>
<action type="add">
A new resource provider for JPA servers called
<![CDATA[<code>BinaryAccessProvider</code>]]>
@ -335,7 +340,6 @@
a narrative on an untitled DiagnosticReport were fixed. Thanks to GitHub
user @navyflower for reporting!
</action>
</release>
<release version="3.8.0" date="2019-05-30" description="Hippo">
<action type="fix">