From 99e92d8fcad408785dd8dafea2ae7f97e4b462df Mon Sep 17 00:00:00 2001 From: jamesagnew Date: Sun, 19 Jul 2015 19:17:39 -0400 Subject: [PATCH] Operations in server generated conformance statement should only appear once per name, since the name needs to be unique. --- .../rest/method/OperationMethodBinding.java | 26 +- .../fhir/rest/method/OperationParameter.java | 16 +- .../dstu2/ServerConformanceProvider.java | 269 +++++++++------- .../OperationDuplicateServerDstu2Test.java | 146 +++++++++ .../conf/ServerConformanceProvider.java | 292 ++++++++++-------- ...erationDuplicateServerHl7OrgDstu2Test.java | 146 +++++++++ 6 files changed, 636 insertions(+), 259 deletions(-) create mode 100644 hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationDuplicateServerDstu2Test.java create mode 100644 hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationDuplicateServerHl7OrgDstu2Test.java diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/OperationMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/OperationMethodBinding.java index c0558614afe..c4f72450eca 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/OperationMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/OperationMethodBinding.java @@ -58,6 +58,8 @@ import ca.uhn.fhir.util.FhirTerser; public class OperationMethodBinding extends BaseResourceReturningMethodBinding { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OperationMethodBinding.class); + private boolean myCanOperateAtInstanceLevel; + private boolean myCanOperateAtServerLevel; private String myDescription; private final boolean myIdempotent; private final Integer myIdParamIndex; @@ -66,7 +68,7 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { private List myReturnParams; private final ReturnTypeEnum myReturnType; - public OperationMethodBinding(Class theReturnResourceType, Class theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider, boolean theIdempotent, String theOperationName, Class theOperationType, + protected OperationMethodBinding(Class theReturnResourceType, Class theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider, boolean theIdempotent, String theOperationName, Class theOperationType, OperationParam[] theReturnParams) { super(theReturnResourceType, theMethod, theContext, theProvider); @@ -139,6 +141,14 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { myReturnParams.add(type); } } + + if (myIdParamIndex != null) { + myCanOperateAtInstanceLevel = true; + } + if (getResourceName() == null) { + myCanOperateAtServerLevel = true; + } + } public OperationMethodBinding(Class theReturnResourceType, Class theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider, Operation theAnnotation) { @@ -250,12 +260,20 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { return retVal; } + public boolean isCanOperateAtInstanceLevel() { + return this.myCanOperateAtInstanceLevel; + } + + public boolean isCanOperateAtServerLevel() { + return this.myCanOperateAtServerLevel; + } + public boolean isIdempotent() { return myIdempotent; } - public boolean isInstanceLevel() { - return myIdParamIndex != null; + public void setDescription(String theDescription) { + myDescription = theDescription; } public static BaseHttpClientInvocation createOperationInvocation(FhirContext theContext, String theResourceName, String theId, String theOperationName, IBaseParameters theInput, boolean theUseHttpGet) { @@ -309,9 +327,7 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { public static class ReturnType { private int myMax; - private int myMin; - private String myName; /** * http://hl7-fhir.github.io/valueset-operation-parameter-type.html diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/OperationParameter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/OperationParameter.java index cf2c7291dd8..c3df68cdb0c 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/OperationParameter.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/OperationParameter.java @@ -42,6 +42,7 @@ import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.i18n.HapiLocalizer; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.api.RequestTypeEnum; +import ca.uhn.fhir.rest.api.ValidationModeEnum; import ca.uhn.fhir.rest.param.CollectionBinder; import ca.uhn.fhir.rest.param.ResourceParameter; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; @@ -102,11 +103,20 @@ public class OperationParameter implements IParameter { myMax = 1; } - if (!myParameterType.equals(IBase.class)) { - if (!IBase.class.isAssignableFrom(myParameterType) || myParameterType.isInterface() || Modifier.isAbstract(myParameterType.getModifiers())) { + /* + * The parameter can be of type string for validation methods - This is a bit + * weird. See ValidateDstu2Test. We should probably clean this up.. + */ + if (!myParameterType.equals(IBase.class) && !myParameterType.equals(String.class)) { + if (IBaseResource.class.isAssignableFrom(myParameterType) && myParameterType.isInterface()) { + myParamType = "Resource"; + } else if (!IBase.class.isAssignableFrom(myParameterType) || myParameterType.isInterface() || Modifier.isAbstract(myParameterType.getModifiers())) { throw new ConfigurationException("Invalid type for @OperationParam: " + myParameterType.getName()); + } else if (myParameterType.equals(ValidationModeEnum.class)) { + myParamType = "code"; + } else { + myParamType = myContext.getElementDefinition((Class) myParameterType).getName(); } - myParamType = myContext.getElementDefinition((Class) myParameterType).getName(); } } diff --git a/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/ServerConformanceProvider.java b/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/ServerConformanceProvider.java index 5286e8ce069..34dcbea4884 100644 --- a/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/ServerConformanceProvider.java +++ b/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/ServerConformanceProvider.java @@ -20,6 +20,8 @@ package ca.uhn.fhir.rest.server.provider.dstu2; * #L% */ +import static org.apache.commons.lang3.StringUtils.*; + import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -88,15 +90,56 @@ public class ServerConformanceProvider implements IServerConformanceProvider myOperationBindingToName; + private HashMap> myOperationNameToBindings; private String myPublisher = "Not provided"; private final RestfulServer myRestfulServer; - private IdentityHashMap myOperationBindingToName; - private HashMap myOperationNameToBinding; public ServerConformanceProvider(RestfulServer theRestfulServer) { myRestfulServer = theRestfulServer; } + private void checkBindingForSystemOps(Rest rest, Set systemOps, BaseMethodBinding nextMethodBinding) { + if (nextMethodBinding.getSystemOperationType() != null) { + String sysOpCode = nextMethodBinding.getSystemOperationType().getCode(); + if (sysOpCode != null) { + SystemRestfulInteractionEnum sysOp = SystemRestfulInteractionEnum.VALUESET_BINDER.fromCodeString(sysOpCode); + if (sysOp == null) { + throw new InternalErrorException("Unknown system-restful-interaction: " + sysOpCode); + } + if (systemOps.contains(sysOp) == false) { + systemOps.add(sysOp); + rest.addInteraction().setCode(sysOp); + } + } + } + } + + private Map>> collectMethodBindings() { + Map>> resourceToMethods = new TreeMap>>(); + for (ResourceBinding next : myRestfulServer.getResourceBindings()) { + String resourceName = next.getResourceName(); + for (BaseMethodBinding nextMethodBinding : next.getMethodBindings()) { + if (resourceToMethods.containsKey(resourceName) == false) { + resourceToMethods.put(resourceName, new ArrayList>()); + } + resourceToMethods.get(resourceName).add(nextMethodBinding); + } + } + for (BaseMethodBinding nextMethodBinding : myRestfulServer.getServerBindings()) { + String resourceName = ""; + if (resourceToMethods.containsKey(resourceName) == false) { + resourceToMethods.put(resourceName, new ArrayList>()); + } + resourceToMethods.get(resourceName).add(nextMethodBinding); + } + return resourceToMethods; + } + + private String createOperationName(OperationMethodBinding theMethodBinding) { + return theMethodBinding.getName().substring(1); + } + /** * Gets the value of the "publisher" that will be placed in the generated conformance statement. As this is a * mandatory element, the value should not be null (although this is not enforced). The value defaults to @@ -106,85 +149,6 @@ public class ServerConformanceProvider implements IServerConformanceProvider allNames = new HashSet(); - myOperationBindingToName = new IdentityHashMap(); - myOperationNameToBinding = new HashMap(); - - Map>> resourceToMethods = collectMethodBindings(); - for (Entry>> nextEntry : resourceToMethods.entrySet()) { - List> nextMethodBindings = nextEntry.getValue(); - for (BaseMethodBinding nextMethodBinding : nextMethodBindings) { - if (nextMethodBinding instanceof OperationMethodBinding) { - OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; - String nextName = methodBinding.getName().substring(1); - int count = 1; - while (allNames.add(createOperationName(nextName, count)) == false) { - count++; - } - String name = createOperationName(nextName, count); - myOperationBindingToName.put(methodBinding, name); - myOperationNameToBinding.put(name, methodBinding); - } - } - } - } - - private String createOperationName(String theName, int theCount) { - if (theCount < 2) { - return theName; - } - return theName + '-' + theCount; - } - - @Read(type = OperationDefinition.class) - public OperationDefinition readOperationDefinition(@IdParam IdDt theId) { - if (theId == null || theId.hasIdPart() == false) { - throw new ResourceNotFoundException(theId); - } - OperationMethodBinding methodBinding = myOperationNameToBinding.get(theId.getIdPart()); - if (methodBinding == null) { - throw new ResourceNotFoundException(theId); - } - - OperationDefinition op = new OperationDefinition(); - op.setStatus(ConformanceResourceStatusEnum.ACTIVE); - op.setDescription(methodBinding.getDescription()); - op.setIdempotent(methodBinding.isIdempotent()); - op.setCode(methodBinding.getName()); - op.setInstance(methodBinding.isInstanceLevel()); - op.addType().setValue(methodBinding.getResourceName()); - - for (IParameter nextParamUntyped : methodBinding.getParameters()) { - if (nextParamUntyped instanceof OperationParameter) { - OperationParameter nextParam = (OperationParameter) nextParamUntyped; - Parameter param = op.addParameter(); - param.setUse(OperationParameterUseEnum.IN); - if (nextParam.getParamType() != null) { - param.setType(nextParam.getParamType()); - } - param.setMin(nextParam.getMin()); - param.setMax(nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax())); - param.setName(nextParam.getName()); - } - } - - for (ReturnType nextParam : methodBinding.getReturnParams()) { - Parameter param = op.addParameter(); - param.setUse(OperationParameterUseEnum.OUT); - if (nextParam.getType() != null) { - param.setType(nextParam.getType()); - } - param.setMin(nextParam.getMin()); - param.setMax(nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax())); - param.setName(nextParam.getName()); - } - - - return op; - } - @Override @Metadata public Conformance getServerConformance(HttpServletRequest theRequest) { @@ -210,6 +174,7 @@ public class ServerConformanceProvider implements IServerConformanceProvider systemOps = new HashSet(); + Set operationNames = new HashSet(); Map>> resourceToMethods = collectMethodBindings(); for (Entry>> nextEntry : resourceToMethods.entrySet()) { @@ -274,7 +239,10 @@ public class ServerConformanceProvider implements IServerConformanceProvider() { @@ -306,7 +274,9 @@ public class ServerConformanceProvider implements IServerConformanceProvider>> collectMethodBindings() { - Map>> resourceToMethods = new TreeMap>>(); - for (ResourceBinding next : myRestfulServer.getResourceBindings()) { - String resourceName = next.getResourceName(); - for (BaseMethodBinding nextMethodBinding : next.getMethodBindings()) { - if (resourceToMethods.containsKey(resourceName) == false) { - resourceToMethods.put(resourceName, new ArrayList>()); - } - resourceToMethods.get(resourceName).add(nextMethodBinding); - } - } - for (BaseMethodBinding nextMethodBinding : myRestfulServer.getServerBindings()) { - String resourceName = ""; - if (resourceToMethods.containsKey(resourceName) == false) { - resourceToMethods.put(resourceName, new ArrayList>()); - } - resourceToMethods.get(resourceName).add(nextMethodBinding); - } - return resourceToMethods; - } - - private void checkBindingForSystemOps(Rest rest, Set systemOps, BaseMethodBinding nextMethodBinding) { - if (nextMethodBinding.getSystemOperationType() != null) { - String sysOpCode = nextMethodBinding.getSystemOperationType().getCode(); - if (sysOpCode != null) { - SystemRestfulInteractionEnum sysOp = SystemRestfulInteractionEnum.VALUESET_BINDER.fromCodeString(sysOpCode); - if (sysOp == null) { - throw new InternalErrorException("Unknown system-restful-interaction: " + sysOpCode); - } - if (systemOps.contains(sysOp) == false) { - systemOps.add(sysOp); - rest.addInteraction().setCode(sysOp); - } - } - } - } - private void handleDynamicSearchMethodBinding(RestResource resource, RuntimeResourceDefinition def, TreeSet includes, DynamicSearchMethodBinding searchMethodBinding) { includes.addAll(searchMethodBinding.getIncludes()); @@ -469,6 +402,102 @@ public class ServerConformanceProvider implements IServerConformanceProvider(); + myOperationNameToBindings = new HashMap>(); + + Map>> resourceToMethods = collectMethodBindings(); + for (Entry>> nextEntry : resourceToMethods.entrySet()) { + List> nextMethodBindings = nextEntry.getValue(); + for (BaseMethodBinding nextMethodBinding : nextMethodBindings) { + if (nextMethodBinding instanceof OperationMethodBinding) { + OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; + if (myOperationBindingToName.containsKey(methodBinding)) { + continue; + } + + String name = createOperationName(methodBinding); + myOperationBindingToName.put(methodBinding, name); + if (myOperationNameToBindings.containsKey(name) == false) { + myOperationNameToBindings.put(name, new ArrayList()); + } + myOperationNameToBindings.get(name).add(methodBinding); + } + } + } + } + + @Read(type = OperationDefinition.class) + public OperationDefinition readOperationDefinition(@IdParam IdDt theId) { + if (theId == null || theId.hasIdPart() == false) { + throw new ResourceNotFoundException(theId); + } + List sharedDescriptions = myOperationNameToBindings.get(theId.getIdPart()); + if (sharedDescriptions == null || sharedDescriptions.isEmpty()) { + throw new ResourceNotFoundException(theId); + } + + OperationDefinition op = new OperationDefinition(); + op.setStatus(ConformanceResourceStatusEnum.ACTIVE); + op.setIdempotent(true); + + Set inParams = new HashSet(); + Set outParams = new HashSet(); + + for (OperationMethodBinding sharedDescription : sharedDescriptions) { + if (isNotBlank(sharedDescription.getDescription())) { + op.setDescription(sharedDescription.getDescription()); + } + if (!sharedDescription.isIdempotent()) { + op.setIdempotent(sharedDescription.isIdempotent()); + } + op.setCode(sharedDescription.getName()); + if (sharedDescription.isCanOperateAtInstanceLevel()) { + op.setInstance(sharedDescription.isCanOperateAtInstanceLevel()); + } + if (sharedDescription.isCanOperateAtServerLevel()) { + op.setSystem(sharedDescription.isCanOperateAtServerLevel()); + } + if (isNotBlank(sharedDescription.getResourceName())) { + op.addType().setValue(sharedDescription.getResourceName()); + } + + for (IParameter nextParamUntyped : sharedDescription.getParameters()) { + if (nextParamUntyped instanceof OperationParameter) { + OperationParameter nextParam = (OperationParameter) nextParamUntyped; + Parameter param = op.addParameter(); + if (!inParams.add(nextParam.getName())) { + continue; + } + param.setUse(OperationParameterUseEnum.IN); + if (nextParam.getParamType() != null) { + param.setType(nextParam.getParamType()); + } + param.setMin(nextParam.getMin()); + param.setMax(nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax())); + param.setName(nextParam.getName()); + } + } + + for (ReturnType nextParam : sharedDescription.getReturnParams()) { + if (!outParams.add(nextParam.getName())) { + continue; + } + Parameter param = op.addParameter(); + param.setUse(OperationParameterUseEnum.OUT); + if (nextParam.getType() != null) { + param.setType(nextParam.getType()); + } + param.setMin(nextParam.getMin()); + param.setMax(nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax())); + param.setName(nextParam.getName()); + } + } + + return op; + } + /** * Sets the cache property (default is true). If set to true, the same response will be returned for each invocation. *

diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationDuplicateServerDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationDuplicateServerDstu2Test.java new file mode 100644 index 00000000000..95dd2d80ae9 --- /dev/null +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationDuplicateServerDstu2Test.java @@ -0,0 +1,146 @@ +package ca.uhn.fhir.rest.server; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +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.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.dstu2.resource.Conformance; +import ca.uhn.fhir.model.dstu2.resource.OperationDefinition; +import ca.uhn.fhir.model.dstu2.resource.Organization; +import ca.uhn.fhir.model.dstu2.resource.Parameters; +import ca.uhn.fhir.model.dstu2.resource.Patient; +import ca.uhn.fhir.model.primitive.StringDt; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.util.PortUtil; + +public class OperationDuplicateServerDstu2Test { + private static CloseableHttpClient ourClient; + private static FhirContext ourCtx; + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OperationDuplicateServerDstu2Test.class); + private static int ourPort; + private static Server ourServer; + + @Test + public void testOperationsAreCollapsed() throws Exception { + // Metadata + { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/metadata?_pretty=true"); + HttpResponse status = ourClient.execute(httpGet); + + assertEquals(200, status.getStatusLine().getStatusCode()); + String response = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + ourLog.info(response); + + Conformance resp = ourCtx.newXmlParser().parseResource(Conformance.class, response); + assertEquals(1, resp.getRest().get(0).getOperation().size()); + assertEquals("$myoperation", resp.getRest().get(0).getOperation().get(0).getName()); + assertEquals("OperationDefinition/myoperation", resp.getRest().get(0).getOperation().get(0).getDefinition().getReference().getValue()); + } + + // OperationDefinition + { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/OperationDefinition/myoperation?_pretty=true"); + HttpResponse status = ourClient.execute(httpGet); + + assertEquals(200, status.getStatusLine().getStatusCode()); + String response = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + ourLog.info(response); + + OperationDefinition resp = ourCtx.newXmlParser().parseResource(OperationDefinition.class, response); + assertEquals(true, resp.getSystemElement().getValue().booleanValue()); + assertEquals("$myoperation", resp.getCode()); + assertEquals(true, resp.getIdempotent().booleanValue()); + assertEquals(2, resp.getType().size()); + assertThat(Arrays.asList(resp.getType().get(0).getValue(), resp.getType().get(1).getValue()), containsInAnyOrder("Organization", "Patient")); + assertEquals(1, resp.getParameter().size()); + } + } + + @AfterClass + public static void afterClass() throws Exception { + ourServer.stop(); + } + + @BeforeClass + public static void beforeClass() throws Exception { + ourCtx = FhirContext.forDstu2(); + ourPort = PortUtil.findFreePort(); + ourServer = new Server(ourPort); + + ServletHandler proxyHandler = new ServletHandler(); + RestfulServer servlet = new RestfulServer(ourCtx); + + servlet.setPagingProvider(new FifoMemoryPagingProvider(10).setDefaultPageSize(2)); + + servlet.setFhirContext(ourCtx); + servlet.setResourceProviders(new PatientProvider(), new OrganizationProvider()); + servlet.setPlainProviders(new PlainProvider()); + ServletHolder servletHolder = new ServletHolder(servlet); + proxyHandler.addServletWithMapping(servletHolder, "/*"); + ourServer.setHandler(proxyHandler); + ourServer.start(); + + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); + HttpClientBuilder builder = HttpClientBuilder.create(); + builder.setConnectionManager(connectionManager); + ourClient = builder.build(); + + } + + public static class BaseProvider { + + @Operation(name = "$myoperation", idempotent = true) + public Parameters opInstanceReturnsBundleProvider(@OperationParam(name = "myparam") StringDt theString) { + return null; + } + + } + + public static class OrganizationProvider extends BaseProvider implements IResourceProvider { + + @Override + public Class getResourceType() { + return Organization.class; + } + + } + + public static class PatientProvider extends BaseProvider implements IResourceProvider { + + @Override + public Class getResourceType() { + return Patient.class; + } + + } + + public static class PlainProvider { + + @Operation(name = "$myoperation", idempotent = true) + public Parameters opInstanceReturnsBundleProvider(@OperationParam(name = "myparam") StringDt theString) { + return null; + } + + } + +} diff --git a/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/conf/ServerConformanceProvider.java b/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/conf/ServerConformanceProvider.java index 0a1573ca3d3..b438ad87a3f 100644 --- a/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/conf/ServerConformanceProvider.java +++ b/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/conf/ServerConformanceProvider.java @@ -20,6 +20,8 @@ package org.hl7.fhir.instance.conf; * #L% */ +import static org.apache.commons.lang3.StringUtils.*; + import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -46,6 +48,7 @@ import org.hl7.fhir.instance.model.Conformance.RestfulConformanceMode; import org.hl7.fhir.instance.model.Conformance.SystemRestfulInteraction; import org.hl7.fhir.instance.model.Conformance.TypeRestfulInteraction; import org.hl7.fhir.instance.model.Enumerations.ConformanceResourceStatus; +import org.hl7.fhir.instance.model.Enumerations.ResourceType; import org.hl7.fhir.instance.model.OperationDefinition; import org.hl7.fhir.instance.model.OperationDefinition.OperationDefinitionParameterComponent; import org.hl7.fhir.instance.model.OperationDefinition.OperationParameterUse; @@ -57,7 +60,6 @@ import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Initialize; import ca.uhn.fhir.rest.annotation.Metadata; -import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.annotation.Read; import ca.uhn.fhir.rest.method.BaseMethodBinding; import ca.uhn.fhir.rest.method.DynamicSearchMethodBinding; @@ -85,17 +87,64 @@ import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; *

*/ public class ServerConformanceProvider implements IServerConformanceProvider { + private boolean myCache = true; private volatile Conformance myConformance; + private IdentityHashMap myOperationBindingToName; + private HashMap> myOperationNameToBindings; private String myPublisher = "Not provided"; private final RestfulServer myRestfulServer; - private IdentityHashMap myOperationBindingToName; - private HashMap myOperationNameToBinding; public ServerConformanceProvider(RestfulServer theRestfulServer) { myRestfulServer = theRestfulServer; } + private void checkBindingForSystemOps(ConformanceRestComponent rest, Set systemOps, BaseMethodBinding nextMethodBinding) { + if (nextMethodBinding.getSystemOperationType() != null) { + String sysOpCode = nextMethodBinding.getSystemOperationType().getCode(); + if (sysOpCode != null) { + SystemRestfulInteraction sysOp; + try { + sysOp = SystemRestfulInteraction.fromCode(sysOpCode); + } catch (Exception e) { + sysOp = null; + } + if (sysOp == null) { + throw new InternalErrorException("Unknown system-restful-interaction: " + sysOpCode); + } + if (systemOps.contains(sysOp) == false) { + systemOps.add(sysOp); + rest.addInteraction().setCode(sysOp); + } + } + } + } + + private Map>> collectMethodBindings() { + Map>> resourceToMethods = new TreeMap>>(); + for (ResourceBinding next : myRestfulServer.getResourceBindings()) { + String resourceName = next.getResourceName(); + for (BaseMethodBinding nextMethodBinding : next.getMethodBindings()) { + if (resourceToMethods.containsKey(resourceName) == false) { + resourceToMethods.put(resourceName, new ArrayList>()); + } + resourceToMethods.get(resourceName).add(nextMethodBinding); + } + } + for (BaseMethodBinding nextMethodBinding : myRestfulServer.getServerBindings()) { + String resourceName = ""; + if (resourceToMethods.containsKey(resourceName) == false) { + resourceToMethods.put(resourceName, new ArrayList>()); + } + resourceToMethods.get(resourceName).add(nextMethodBinding); + } + return resourceToMethods; + } + + private String createOperationName(OperationMethodBinding theMethodBinding) { + return theMethodBinding.getName().substring(1); + } + /** * Gets the value of the "publisher" that will be placed in the generated conformance statement. As this is a * mandatory element, the value should not be null (although this is not enforced). The value defaults to @@ -105,84 +154,6 @@ public class ServerConformanceProvider implements IServerConformanceProvider allNames = new HashSet(); - myOperationBindingToName = new IdentityHashMap(); - myOperationNameToBinding = new HashMap(); - - Map>> resourceToMethods = collectMethodBindings(); - for (Entry>> nextEntry : resourceToMethods.entrySet()) { - List> nextMethodBindings = nextEntry.getValue(); - for (BaseMethodBinding nextMethodBinding : nextMethodBindings) { - if (nextMethodBinding instanceof OperationMethodBinding) { - OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; - String nextName = methodBinding.getName().substring(1); - int count = 1; - while (allNames.add(createOperationName(nextName, count)) == false) { - count++; - } - String name = createOperationName(nextName, count); - myOperationBindingToName.put(methodBinding, name); - myOperationNameToBinding.put(name, methodBinding); - } - } - } - } - - private String createOperationName(String theName, int theCount) { - if (theCount < 2) { - return theName; - } - return theName + '-' + theCount; - } - - @Read(type = OperationDefinition.class) - public OperationDefinition readOperationDefinition(@IdParam IdDt theId) { - if (theId == null || theId.hasIdPart() == false) { - throw new ResourceNotFoundException(theId); - } - OperationMethodBinding methodBinding = myOperationNameToBinding.get(theId.getIdPart()); - if (methodBinding == null) { - throw new ResourceNotFoundException(theId); - } - - OperationDefinition op = new OperationDefinition(); - op.setStatus(ConformanceResourceStatus.ACTIVE); - op.setDescription(methodBinding.getDescription()); - op.setIdempotent(methodBinding.isIdempotent()); - op.setCode(methodBinding.getName()); - op.setInstance(methodBinding.isInstanceLevel()); - op.addType(methodBinding.getResourceName()); - - for (IParameter nextParamUntyped : methodBinding.getParameters()) { - if (nextParamUntyped instanceof OperationParameter) { - OperationParameter nextParam = (OperationParameter) nextParamUntyped; - OperationDefinitionParameterComponent param = op.addParameter(); - param.setUse(OperationParameterUse.IN); - if (nextParam.getParamType() != null) { - param.setType(nextParam.getParamType()); - } - param.setMin(nextParam.getMin()); - param.setMax(nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax())); - param.setName(nextParam.getName()); - } - } - - for (ReturnType nextParam : methodBinding.getReturnParams()) { - OperationDefinitionParameterComponent param = op.addParameter(); - param.setUse(OperationParameterUse.OUT); - if (nextParam.getType() != null) { - param.setType(nextParam.getType()); - } - param.setMin(nextParam.getMin()); - param.setMax(nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax())); - param.setName(nextParam.getName()); - } - - return op; - } - @Override @Metadata public Conformance getServerConformance(HttpServletRequest theRequest) { @@ -208,6 +179,7 @@ public class ServerConformanceProvider implements IServerConformanceProvider systemOps = new HashSet(); + Set operationNames = new HashSet(); Map>> resourceToMethods = collectMethodBindings(); for (Entry>> nextEntry : resourceToMethods.entrySet()) { @@ -277,14 +249,17 @@ public class ServerConformanceProvider implements IServerConformanceProvider() { @Override public int compare(ResourceInteractionComponent theO1, ResourceInteractionComponent theO2) { - TypeRestfulInteraction o1 = theO1.getCodeElement().getValue(); - TypeRestfulInteraction o2 = theO2.getCodeElement().getValue(); + TypeRestfulInteraction o1 = theO1.getCode(); + TypeRestfulInteraction o2 = theO2.getCode(); if (o1 == null && o2 == null) { return 0; } @@ -309,7 +284,9 @@ public class ServerConformanceProvider implements IServerConformanceProvider>> collectMethodBindings() { - Map>> resourceToMethods = new TreeMap>>(); - for (ResourceBinding next : myRestfulServer.getResourceBindings()) { - String resourceName = next.getResourceName(); - for (BaseMethodBinding nextMethodBinding : next.getMethodBindings()) { - if (resourceToMethods.containsKey(resourceName) == false) { - resourceToMethods.put(resourceName, new ArrayList>()); - } - resourceToMethods.get(resourceName).add(nextMethodBinding); - } - } - for (BaseMethodBinding nextMethodBinding : myRestfulServer.getServerBindings()) { - String resourceName = ""; - if (resourceToMethods.containsKey(resourceName) == false) { - resourceToMethods.put(resourceName, new ArrayList>()); - } - resourceToMethods.get(resourceName).add(nextMethodBinding); - } - return resourceToMethods; - } - - private void checkBindingForSystemOps(ConformanceRestComponent rest, Set systemOps, BaseMethodBinding nextMethodBinding) { - if (nextMethodBinding.getSystemOperationType() != null) { - String sysOpCode = nextMethodBinding.getSystemOperationType().getCode(); - if (sysOpCode != null) { - SystemRestfulInteraction sysOp; - try { - sysOp = SystemRestfulInteraction.fromCode(sysOpCode); - } catch (Exception e) { - sysOp=null; - } - if (sysOp == null) { - throw new InternalErrorException("Unknown system-restful-interaction: " + sysOpCode); - } - if (systemOps.contains(sysOp) == false) { - systemOps.add(sysOp); - rest.addInteraction().setCode(sysOp); - } - } - } - } - private void handleDynamicSearchMethodBinding(ConformanceRestResourceComponent resource, RuntimeResourceDefinition def, TreeSet includes, DynamicSearchMethodBinding searchMethodBinding) { includes.addAll(searchMethodBinding.getIncludes()); @@ -393,8 +328,7 @@ public class ServerConformanceProvider implements IServerConformanceProvider nextTarget : nextParameter.getDeclaredTypes()) { RuntimeResourceDefinition targetDef = myRestfulServer.getFhirContext().getResourceDefinition(nextTarget); if (targetDef != null) { - org.hl7.fhir.instance.model.Enumerations.ResourceType code; + ResourceType code; try { - code = org.hl7.fhir.instance.model.Enumerations.ResourceType.fromCode(targetDef.getName()); + code = ResourceType.fromCode(targetDef.getName()); } catch (Exception e) { code = null; } @@ -482,6 +416,102 @@ public class ServerConformanceProvider implements IServerConformanceProvider(); + myOperationNameToBindings = new HashMap>(); + + Map>> resourceToMethods = collectMethodBindings(); + for (Entry>> nextEntry : resourceToMethods.entrySet()) { + List> nextMethodBindings = nextEntry.getValue(); + for (BaseMethodBinding nextMethodBinding : nextMethodBindings) { + if (nextMethodBinding instanceof OperationMethodBinding) { + OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; + if (myOperationBindingToName.containsKey(methodBinding)) { + continue; + } + + String name = createOperationName(methodBinding); + myOperationBindingToName.put(methodBinding, name); + if (myOperationNameToBindings.containsKey(name) == false) { + myOperationNameToBindings.put(name, new ArrayList()); + } + myOperationNameToBindings.get(name).add(methodBinding); + } + } + } + } + + @Read(type = OperationDefinition.class) + public OperationDefinition readOperationDefinition(@IdParam IdDt theId) { + if (theId == null || theId.hasIdPart() == false) { + throw new ResourceNotFoundException(theId); + } + List sharedDescriptions = myOperationNameToBindings.get(theId.getIdPart()); + if (sharedDescriptions == null || sharedDescriptions.isEmpty()) { + throw new ResourceNotFoundException(theId); + } + + OperationDefinition op = new OperationDefinition(); + op.setStatus(ConformanceResourceStatus.ACTIVE); + op.setIdempotent(true); + + Set inParams = new HashSet(); + Set outParams = new HashSet(); + + for (OperationMethodBinding sharedDescription : sharedDescriptions) { + if (isNotBlank(sharedDescription.getDescription())) { + op.setDescription(sharedDescription.getDescription()); + } + if (!sharedDescription.isIdempotent()) { + op.setIdempotent(sharedDescription.isIdempotent()); + } + op.setCode(sharedDescription.getName()); + if (sharedDescription.isCanOperateAtInstanceLevel()) { + op.setInstance(sharedDescription.isCanOperateAtInstanceLevel()); + } + if (sharedDescription.isCanOperateAtServerLevel()) { + op.setSystem(sharedDescription.isCanOperateAtServerLevel()); + } + if (isNotBlank(sharedDescription.getResourceName())) { + op.addTypeElement().setValue(sharedDescription.getResourceName()); + } + + for (IParameter nextParamUntyped : sharedDescription.getParameters()) { + if (nextParamUntyped instanceof OperationParameter) { + OperationParameter nextParam = (OperationParameter) nextParamUntyped; + OperationDefinitionParameterComponent param = op.addParameter(); + if (!inParams.add(nextParam.getName())) { + continue; + } + param.setUse(OperationParameterUse.IN); + if (nextParam.getParamType() != null) { + param.setType(nextParam.getParamType()); + } + param.setMin(nextParam.getMin()); + param.setMax(nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax())); + param.setName(nextParam.getName()); + } + } + + for (ReturnType nextParam : sharedDescription.getReturnParams()) { + if (!outParams.add(nextParam.getName())) { + continue; + } + OperationDefinitionParameterComponent param = op.addParameter(); + param.setUse(OperationParameterUse.OUT); + if (nextParam.getType() != null) { + param.setType(nextParam.getType()); + } + param.setMin(nextParam.getMin()); + param.setMax(nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax())); + param.setName(nextParam.getName()); + } + } + + return op; + } + /** * Sets the cache property (default is true). If set to true, the same response will be returned for each invocation. *

diff --git a/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationDuplicateServerHl7OrgDstu2Test.java b/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationDuplicateServerHl7OrgDstu2Test.java new file mode 100644 index 00000000000..2d6adc05ac9 --- /dev/null +++ b/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationDuplicateServerHl7OrgDstu2Test.java @@ -0,0 +1,146 @@ +package ca.uhn.fhir.rest.server; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +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.Conformance; +import org.hl7.fhir.instance.model.OperationDefinition; +import org.hl7.fhir.instance.model.Organization; +import org.hl7.fhir.instance.model.Parameters; +import org.hl7.fhir.instance.model.Patient; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.primitive.StringDt; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.util.PortUtil; + +public class OperationDuplicateServerHl7OrgDstu2Test { + private static CloseableHttpClient ourClient; + private static FhirContext ourCtx; + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OperationDuplicateServerHl7OrgDstu2Test.class); + private static int ourPort; + private static Server ourServer; + + @Test + public void testOperationsAreCollapsed() throws Exception { + // Metadata + { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/metadata?_pretty=true"); + HttpResponse status = ourClient.execute(httpGet); + + assertEquals(200, status.getStatusLine().getStatusCode()); + String response = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + ourLog.info(response); + + Conformance resp = ourCtx.newXmlParser().parseResource(Conformance.class, response); + assertEquals(1, resp.getRest().get(0).getOperation().size()); + assertEquals("$myoperation", resp.getRest().get(0).getOperation().get(0).getName()); + assertEquals("OperationDefinition/myoperation", resp.getRest().get(0).getOperation().get(0).getDefinition().getReference()); + } + + // OperationDefinition + { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/OperationDefinition/myoperation?_pretty=true"); + HttpResponse status = ourClient.execute(httpGet); + + assertEquals(200, status.getStatusLine().getStatusCode()); + String response = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + ourLog.info(response); + + OperationDefinition resp = ourCtx.newXmlParser().parseResource(OperationDefinition.class, response); + assertEquals(true, resp.getSystemElement().getValue().booleanValue()); + assertEquals("$myoperation", resp.getCode()); + assertEquals(true, resp.getIdempotent()); + assertEquals(2, resp.getType().size()); + assertThat(Arrays.asList(resp.getType().get(0).getValue(), resp.getType().get(1).getValue()), containsInAnyOrder("Organization", "Patient")); + assertEquals(1, resp.getParameter().size()); + } + } + + @AfterClass + public static void afterClass() throws Exception { + ourServer.stop(); + } + + @BeforeClass + public static void beforeClass() throws Exception { + ourCtx = FhirContext.forDstu2Hl7Org(); + ourPort = PortUtil.findFreePort(); + ourServer = new Server(ourPort); + + ServletHandler proxyHandler = new ServletHandler(); + RestfulServer servlet = new RestfulServer(ourCtx); + + servlet.setPagingProvider(new FifoMemoryPagingProvider(10).setDefaultPageSize(2)); + + servlet.setFhirContext(ourCtx); + servlet.setResourceProviders(new PatientProvider(), new OrganizationProvider()); + servlet.setPlainProviders(new PlainProvider()); + ServletHolder servletHolder = new ServletHolder(servlet); + proxyHandler.addServletWithMapping(servletHolder, "/*"); + ourServer.setHandler(proxyHandler); + ourServer.start(); + + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); + HttpClientBuilder builder = HttpClientBuilder.create(); + builder.setConnectionManager(connectionManager); + ourClient = builder.build(); + + } + + public static class BaseProvider { + + @Operation(name = "$myoperation", idempotent = true) + public Parameters opInstanceReturnsBundleProvider(@OperationParam(name = "myparam") StringDt theString) { + return null; + } + + } + + public static class OrganizationProvider extends BaseProvider implements IResourceProvider { + + @Override + public Class getResourceType() { + return Organization.class; + } + + } + + public static class PatientProvider extends BaseProvider implements IResourceProvider { + + @Override + public Class getResourceType() { + return Patient.class; + } + + } + + public static class PlainProvider { + + @Operation(name = "$myoperation", idempotent = true) + public Parameters opInstanceReturnsBundleProvider(@OperationParam(name = "myparam") StringDt theString) { + return null; + } + + } + +}