Allow operations to better work across multiple versions of FHIR
This commit is contained in:
parent
d03fc0f61d
commit
8158292665
|
@ -76,6 +76,14 @@ public @interface OperationParam {
|
||||||
*/
|
*/
|
||||||
Class<? extends IBase> type() default IBase.class;
|
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)
|
* The minimum number of repetitions allowed for this child (default is 0)
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -219,7 +219,15 @@ public class MethodUtil {
|
||||||
param = new ConditionalParamBinder(theRestfulOperationTypeEnum, ((ConditionalUrlParam) nextAnnotation).supportsMultiple());
|
param = new ConditionalParamBinder(theRestfulOperationTypeEnum, ((ConditionalUrlParam) nextAnnotation).supportsMultiple());
|
||||||
} else if (nextAnnotation instanceof OperationParam) {
|
} else if (nextAnnotation instanceof OperationParam) {
|
||||||
Operation op = theMethod.getAnnotation(Operation.class);
|
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) {
|
} else if (nextAnnotation instanceof Validate.Mode) {
|
||||||
if (parameterType.equals(ValidationModeEnum.class) == false) {
|
if (parameterType.equals(ValidationModeEnum.class) == false) {
|
||||||
throw new ConfigurationException(
|
throw new ConfigurationException(
|
||||||
|
|
|
@ -151,12 +151,10 @@ public class OperationParameter implements IParameter {
|
||||||
|
|
||||||
boolean typeIsConcrete = !myParameterType.isInterface() && !Modifier.isAbstract(myParameterType.getModifiers());
|
boolean typeIsConcrete = !myParameterType.isInterface() && !Modifier.isAbstract(myParameterType.getModifiers());
|
||||||
|
|
||||||
//@formatter:off
|
|
||||||
boolean isSearchParam =
|
boolean isSearchParam =
|
||||||
IQueryParameterType.class.isAssignableFrom(myParameterType) ||
|
IQueryParameterType.class.isAssignableFrom(myParameterType) ||
|
||||||
IQueryParameterOr.class.isAssignableFrom(myParameterType) ||
|
IQueryParameterOr.class.isAssignableFrom(myParameterType) ||
|
||||||
IQueryParameterAnd.class.isAssignableFrom(myParameterType);
|
IQueryParameterAnd.class.isAssignableFrom(myParameterType);
|
||||||
//@formatter:off
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Note: We say here !IBase.class.isAssignableFrom because a bunch of DSTU1/2 datatypes also
|
* Note: We say here !IBase.class.isAssignableFrom because a bunch of DSTU1/2 datatypes also
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -310,6 +310,11 @@
|
||||||
apply to all resource types (or instances of all resource types) if they
|
apply to all resource types (or instances of all resource types) if they
|
||||||
are found on a plain provider.
|
are found on a plain provider.
|
||||||
</action>
|
</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">
|
<action type="add">
|
||||||
A new resource provider for JPA servers called
|
A new resource provider for JPA servers called
|
||||||
<![CDATA[<code>BinaryAccessProvider</code>]]>
|
<![CDATA[<code>BinaryAccessProvider</code>]]>
|
||||||
|
@ -335,7 +340,6 @@
|
||||||
a narrative on an untitled DiagnosticReport were fixed. Thanks to GitHub
|
a narrative on an untitled DiagnosticReport were fixed. Thanks to GitHub
|
||||||
user @navyflower for reporting!
|
user @navyflower for reporting!
|
||||||
</action>
|
</action>
|
||||||
|
|
||||||
</release>
|
</release>
|
||||||
<release version="3.8.0" date="2019-05-30" description="Hippo">
|
<release version="3.8.0" date="2019-05-30" description="Hippo">
|
||||||
<action type="fix">
|
<action type="fix">
|
||||||
|
|
Loading…
Reference in New Issue