Operations in server generated conformance statement should only appear once per name, since the name needs to be unique.

This commit is contained in:
jamesagnew 2015-07-19 19:17:39 -04:00
parent a774a654ce
commit 99e92d8fca
6 changed files with 636 additions and 259 deletions

View File

@ -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<ReturnType> myReturnParams;
private final ReturnTypeEnum myReturnType;
public OperationMethodBinding(Class<?> theReturnResourceType, Class<? extends IBaseResource> theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider, boolean theIdempotent, String theOperationName, Class<? extends IBaseResource> theOperationType,
protected OperationMethodBinding(Class<?> theReturnResourceType, Class<? extends IBaseResource> theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider, boolean theIdempotent, String theOperationName, Class<? extends IBaseResource> 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<? extends IBaseResource> 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

View File

@ -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,12 +103,21 @@ 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<? extends IBase>) myParameterType).getName();
}
}
}

View File

@ -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<Con
private boolean myCache = true;
private volatile Conformance myConformance;
private IdentityHashMap<OperationMethodBinding, String> myOperationBindingToName;
private HashMap<String, List<OperationMethodBinding>> myOperationNameToBindings;
private String myPublisher = "Not provided";
private final RestfulServer myRestfulServer;
private IdentityHashMap<OperationMethodBinding, String> myOperationBindingToName;
private HashMap<String, OperationMethodBinding> myOperationNameToBinding;
public ServerConformanceProvider(RestfulServer theRestfulServer) {
myRestfulServer = theRestfulServer;
}
private void checkBindingForSystemOps(Rest rest, Set<SystemRestfulInteractionEnum> 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<String, List<BaseMethodBinding<?>>> collectMethodBindings() {
Map<String, List<BaseMethodBinding<?>>> resourceToMethods = new TreeMap<String, List<BaseMethodBinding<?>>>();
for (ResourceBinding next : myRestfulServer.getResourceBindings()) {
String resourceName = next.getResourceName();
for (BaseMethodBinding<?> nextMethodBinding : next.getMethodBindings()) {
if (resourceToMethods.containsKey(resourceName) == false) {
resourceToMethods.put(resourceName, new ArrayList<BaseMethodBinding<?>>());
}
resourceToMethods.get(resourceName).add(nextMethodBinding);
}
}
for (BaseMethodBinding<?> nextMethodBinding : myRestfulServer.getServerBindings()) {
String resourceName = "";
if (resourceToMethods.containsKey(resourceName) == false) {
resourceToMethods.put(resourceName, new ArrayList<BaseMethodBinding<?>>());
}
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<Con
return myPublisher;
}
@Initialize
public void initializeOperations() {
Set<String> allNames = new HashSet<String>();
myOperationBindingToName = new IdentityHashMap<OperationMethodBinding, String>();
myOperationNameToBinding = new HashMap<String, OperationMethodBinding>();
Map<String, List<BaseMethodBinding<?>>> resourceToMethods = collectMethodBindings();
for (Entry<String, List<BaseMethodBinding<?>>> nextEntry : resourceToMethods.entrySet()) {
List<BaseMethodBinding<?>> 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<Con
rest.setMode(RestfulConformanceModeEnum.SERVER);
Set<SystemRestfulInteractionEnum> systemOps = new HashSet<SystemRestfulInteractionEnum>();
Set<String> operationNames = new HashSet<String>();
Map<String, List<BaseMethodBinding<?>>> resourceToMethods = collectMethodBindings();
for (Entry<String, List<BaseMethodBinding<?>>> nextEntry : resourceToMethods.entrySet()) {
@ -274,8 +239,11 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
} else if (nextMethodBinding instanceof OperationMethodBinding) {
OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
String opName = myOperationBindingToName.get(methodBinding);
if (operationNames.add(opName)) {
// Only add each operation (by name) once
rest.addOperation().setName(methodBinding.getName()).getDefinition().setReference("OperationDefinition/" + opName);
}
}
Collections.sort(resource.getInteraction(), new Comparator<RestResourceInteraction>() {
@Override
@ -306,53 +274,18 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
if (nextMethodBinding instanceof OperationMethodBinding) {
OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
String opName = myOperationBindingToName.get(methodBinding);
if (operationNames.add(opName)) {
rest.addOperation().setName(methodBinding.getName()).getDefinition().setReference("OperationDefinition/" + opName);
}
}
}
}
}
myConformance = retVal;
return retVal;
}
private Map<String, List<BaseMethodBinding<?>>> collectMethodBindings() {
Map<String, List<BaseMethodBinding<?>>> resourceToMethods = new TreeMap<String, List<BaseMethodBinding<?>>>();
for (ResourceBinding next : myRestfulServer.getResourceBindings()) {
String resourceName = next.getResourceName();
for (BaseMethodBinding<?> nextMethodBinding : next.getMethodBindings()) {
if (resourceToMethods.containsKey(resourceName) == false) {
resourceToMethods.put(resourceName, new ArrayList<BaseMethodBinding<?>>());
}
resourceToMethods.get(resourceName).add(nextMethodBinding);
}
}
for (BaseMethodBinding<?> nextMethodBinding : myRestfulServer.getServerBindings()) {
String resourceName = "";
if (resourceToMethods.containsKey(resourceName) == false) {
resourceToMethods.put(resourceName, new ArrayList<BaseMethodBinding<?>>());
}
resourceToMethods.get(resourceName).add(nextMethodBinding);
}
return resourceToMethods;
}
private void checkBindingForSystemOps(Rest rest, Set<SystemRestfulInteractionEnum> 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<String> includes, DynamicSearchMethodBinding searchMethodBinding) {
includes.addAll(searchMethodBinding.getIncludes());
@ -469,6 +402,102 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
}
}
@Initialize
public void initializeOperations() {
myOperationBindingToName = new IdentityHashMap<OperationMethodBinding, String>();
myOperationNameToBindings = new HashMap<String, List<OperationMethodBinding>>();
Map<String, List<BaseMethodBinding<?>>> resourceToMethods = collectMethodBindings();
for (Entry<String, List<BaseMethodBinding<?>>> nextEntry : resourceToMethods.entrySet()) {
List<BaseMethodBinding<?>> 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<OperationMethodBinding>());
}
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<OperationMethodBinding> 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<String> inParams = new HashSet<String>();
Set<String> outParams = new HashSet<String>();
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.
* <p>

View File

@ -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<? extends IBaseResource> getResourceType() {
return Organization.class;
}
}
public static class PatientProvider extends BaseProvider implements IResourceProvider {
@Override
public Class<? extends IBaseResource> getResourceType() {
return Patient.class;
}
}
public static class PlainProvider {
@Operation(name = "$myoperation", idempotent = true)
public Parameters opInstanceReturnsBundleProvider(@OperationParam(name = "myparam") StringDt theString) {
return null;
}
}
}

View File

@ -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;
* </p>
*/
public class ServerConformanceProvider implements IServerConformanceProvider<Conformance> {
private boolean myCache = true;
private volatile Conformance myConformance;
private IdentityHashMap<OperationMethodBinding, String> myOperationBindingToName;
private HashMap<String, List<OperationMethodBinding>> myOperationNameToBindings;
private String myPublisher = "Not provided";
private final RestfulServer myRestfulServer;
private IdentityHashMap<OperationMethodBinding, String> myOperationBindingToName;
private HashMap<String, OperationMethodBinding> myOperationNameToBinding;
public ServerConformanceProvider(RestfulServer theRestfulServer) {
myRestfulServer = theRestfulServer;
}
private void checkBindingForSystemOps(ConformanceRestComponent rest, Set<SystemRestfulInteraction> 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<String, List<BaseMethodBinding<?>>> collectMethodBindings() {
Map<String, List<BaseMethodBinding<?>>> resourceToMethods = new TreeMap<String, List<BaseMethodBinding<?>>>();
for (ResourceBinding next : myRestfulServer.getResourceBindings()) {
String resourceName = next.getResourceName();
for (BaseMethodBinding<?> nextMethodBinding : next.getMethodBindings()) {
if (resourceToMethods.containsKey(resourceName) == false) {
resourceToMethods.put(resourceName, new ArrayList<BaseMethodBinding<?>>());
}
resourceToMethods.get(resourceName).add(nextMethodBinding);
}
}
for (BaseMethodBinding<?> nextMethodBinding : myRestfulServer.getServerBindings()) {
String resourceName = "";
if (resourceToMethods.containsKey(resourceName) == false) {
resourceToMethods.put(resourceName, new ArrayList<BaseMethodBinding<?>>());
}
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<Con
return myPublisher;
}
@Initialize
public void initializeOperations() {
Set<String> allNames = new HashSet<String>();
myOperationBindingToName = new IdentityHashMap<OperationMethodBinding, String>();
myOperationNameToBinding = new HashMap<String, OperationMethodBinding>();
Map<String, List<BaseMethodBinding<?>>> resourceToMethods = collectMethodBindings();
for (Entry<String, List<BaseMethodBinding<?>>> nextEntry : resourceToMethods.entrySet()) {
List<BaseMethodBinding<?>> 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<Con
rest.setMode(RestfulConformanceMode.SERVER);
Set<SystemRestfulInteraction> systemOps = new HashSet<SystemRestfulInteraction>();
Set<String> operationNames = new HashSet<String>();
Map<String, List<BaseMethodBinding<?>>> resourceToMethods = collectMethodBindings();
for (Entry<String, List<BaseMethodBinding<?>>> nextEntry : resourceToMethods.entrySet()) {
@ -277,14 +249,17 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
} else if (nextMethodBinding instanceof OperationMethodBinding) {
OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
String opName = myOperationBindingToName.get(methodBinding);
if (operationNames.add(opName)) {
// Only add each operation (by name) once
rest.addOperation().setName(methodBinding.getName()).getDefinition().setReference("OperationDefinition/" + opName);
}
}
Collections.sort(resource.getInteraction(), new Comparator<ResourceInteractionComponent>() {
@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,58 +284,18 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
if (nextMethodBinding instanceof OperationMethodBinding) {
OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
String opName = myOperationBindingToName.get(methodBinding);
if (operationNames.add(opName)) {
rest.addOperation().setName(methodBinding.getName()).getDefinition().setReference("OperationDefinition/" + opName);
}
}
}
}
}
myConformance = retVal;
return retVal;
}
private Map<String, List<BaseMethodBinding<?>>> collectMethodBindings() {
Map<String, List<BaseMethodBinding<?>>> resourceToMethods = new TreeMap<String, List<BaseMethodBinding<?>>>();
for (ResourceBinding next : myRestfulServer.getResourceBindings()) {
String resourceName = next.getResourceName();
for (BaseMethodBinding<?> nextMethodBinding : next.getMethodBindings()) {
if (resourceToMethods.containsKey(resourceName) == false) {
resourceToMethods.put(resourceName, new ArrayList<BaseMethodBinding<?>>());
}
resourceToMethods.get(resourceName).add(nextMethodBinding);
}
}
for (BaseMethodBinding<?> nextMethodBinding : myRestfulServer.getServerBindings()) {
String resourceName = "";
if (resourceToMethods.containsKey(resourceName) == false) {
resourceToMethods.put(resourceName, new ArrayList<BaseMethodBinding<?>>());
}
resourceToMethods.get(resourceName).add(nextMethodBinding);
}
return resourceToMethods;
}
private void checkBindingForSystemOps(ConformanceRestComponent rest, Set<SystemRestfulInteraction> 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<String> includes, DynamicSearchMethodBinding searchMethodBinding) {
includes.addAll(searchMethodBinding.getIncludes());
@ -393,8 +328,7 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
}
}
ConformanceRestResourceSearchParamComponent param;
param = resource.addSearchParam();
ConformanceRestResourceSearchParamComponent param = resource.addSearchParam();
param.setName(nextParamName);
// if (StringUtils.isNotBlank(chain)) {
@ -467,9 +401,9 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
for (Class<? extends IResource> 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<Con
}
}
@Initialize
public void initializeOperations() {
myOperationBindingToName = new IdentityHashMap<OperationMethodBinding, String>();
myOperationNameToBindings = new HashMap<String, List<OperationMethodBinding>>();
Map<String, List<BaseMethodBinding<?>>> resourceToMethods = collectMethodBindings();
for (Entry<String, List<BaseMethodBinding<?>>> nextEntry : resourceToMethods.entrySet()) {
List<BaseMethodBinding<?>> 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<OperationMethodBinding>());
}
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<OperationMethodBinding> 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<String> inParams = new HashSet<String>();
Set<String> outParams = new HashSet<String>();
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.
* <p>

View File

@ -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<? extends IBaseResource> getResourceType() {
return Organization.class;
}
}
public static class PatientProvider extends BaseProvider implements IResourceProvider {
@Override
public Class<? extends IBaseResource> getResourceType() {
return Patient.class;
}
}
public static class PlainProvider {
@Operation(name = "$myoperation", idempotent = true)
public Parameters opInstanceReturnsBundleProvider(@OperationParam(name = "myparam") StringDt theString) {
return null;
}
}
}