Operations in server generated conformance statement should only appear once per name, since the name needs to be unique.
This commit is contained in:
parent
a774a654ce
commit
99e92d8fca
|
@ -58,6 +58,8 @@ import ca.uhn.fhir.util.FhirTerser;
|
||||||
public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
|
public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
|
||||||
|
|
||||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OperationMethodBinding.class);
|
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OperationMethodBinding.class);
|
||||||
|
private boolean myCanOperateAtInstanceLevel;
|
||||||
|
private boolean myCanOperateAtServerLevel;
|
||||||
private String myDescription;
|
private String myDescription;
|
||||||
private final boolean myIdempotent;
|
private final boolean myIdempotent;
|
||||||
private final Integer myIdParamIndex;
|
private final Integer myIdParamIndex;
|
||||||
|
@ -66,7 +68,7 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
|
||||||
private List<ReturnType> myReturnParams;
|
private List<ReturnType> myReturnParams;
|
||||||
private final ReturnTypeEnum myReturnType;
|
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) {
|
OperationParam[] theReturnParams) {
|
||||||
super(theReturnResourceType, theMethod, theContext, theProvider);
|
super(theReturnResourceType, theMethod, theContext, theProvider);
|
||||||
|
|
||||||
|
@ -139,6 +141,14 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
|
||||||
myReturnParams.add(type);
|
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) {
|
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;
|
return retVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isCanOperateAtInstanceLevel() {
|
||||||
|
return this.myCanOperateAtInstanceLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCanOperateAtServerLevel() {
|
||||||
|
return this.myCanOperateAtServerLevel;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isIdempotent() {
|
public boolean isIdempotent() {
|
||||||
return myIdempotent;
|
return myIdempotent;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isInstanceLevel() {
|
public void setDescription(String theDescription) {
|
||||||
return myIdParamIndex != null;
|
myDescription = theDescription;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static BaseHttpClientInvocation createOperationInvocation(FhirContext theContext, String theResourceName, String theId, String theOperationName, IBaseParameters theInput, boolean theUseHttpGet) {
|
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 {
|
public static class ReturnType {
|
||||||
private int myMax;
|
private int myMax;
|
||||||
|
|
||||||
private int myMin;
|
private int myMin;
|
||||||
|
|
||||||
private String myName;
|
private String myName;
|
||||||
/**
|
/**
|
||||||
* http://hl7-fhir.github.io/valueset-operation-parameter-type.html
|
* http://hl7-fhir.github.io/valueset-operation-parameter-type.html
|
||||||
|
|
|
@ -42,6 +42,7 @@ import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||||
import ca.uhn.fhir.i18n.HapiLocalizer;
|
import ca.uhn.fhir.i18n.HapiLocalizer;
|
||||||
import ca.uhn.fhir.rest.annotation.OperationParam;
|
import ca.uhn.fhir.rest.annotation.OperationParam;
|
||||||
import ca.uhn.fhir.rest.api.RequestTypeEnum;
|
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.CollectionBinder;
|
||||||
import ca.uhn.fhir.rest.param.ResourceParameter;
|
import ca.uhn.fhir.rest.param.ResourceParameter;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||||
|
@ -102,11 +103,20 @@ public class OperationParameter implements IParameter {
|
||||||
myMax = 1;
|
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());
|
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();
|
||||||
}
|
}
|
||||||
myParamType = myContext.getElementDefinition((Class<? extends IBase>) myParameterType).getName();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,8 @@ package ca.uhn.fhir.rest.server.provider.dstu2;
|
||||||
* #L%
|
* #L%
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import static org.apache.commons.lang3.StringUtils.*;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
@ -88,15 +90,56 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
|
||||||
|
|
||||||
private boolean myCache = true;
|
private boolean myCache = true;
|
||||||
private volatile Conformance myConformance;
|
private volatile Conformance myConformance;
|
||||||
|
private IdentityHashMap<OperationMethodBinding, String> myOperationBindingToName;
|
||||||
|
private HashMap<String, List<OperationMethodBinding>> myOperationNameToBindings;
|
||||||
private String myPublisher = "Not provided";
|
private String myPublisher = "Not provided";
|
||||||
private final RestfulServer myRestfulServer;
|
private final RestfulServer myRestfulServer;
|
||||||
private IdentityHashMap<OperationMethodBinding, String> myOperationBindingToName;
|
|
||||||
private HashMap<String, OperationMethodBinding> myOperationNameToBinding;
|
|
||||||
|
|
||||||
public ServerConformanceProvider(RestfulServer theRestfulServer) {
|
public ServerConformanceProvider(RestfulServer theRestfulServer) {
|
||||||
myRestfulServer = 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
|
* 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
|
* 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;
|
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
|
@Override
|
||||||
@Metadata
|
@Metadata
|
||||||
public Conformance getServerConformance(HttpServletRequest theRequest) {
|
public Conformance getServerConformance(HttpServletRequest theRequest) {
|
||||||
|
@ -210,6 +174,7 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
|
||||||
rest.setMode(RestfulConformanceModeEnum.SERVER);
|
rest.setMode(RestfulConformanceModeEnum.SERVER);
|
||||||
|
|
||||||
Set<SystemRestfulInteractionEnum> systemOps = new HashSet<SystemRestfulInteractionEnum>();
|
Set<SystemRestfulInteractionEnum> systemOps = new HashSet<SystemRestfulInteractionEnum>();
|
||||||
|
Set<String> operationNames = new HashSet<String>();
|
||||||
|
|
||||||
Map<String, List<BaseMethodBinding<?>>> resourceToMethods = collectMethodBindings();
|
Map<String, List<BaseMethodBinding<?>>> resourceToMethods = collectMethodBindings();
|
||||||
for (Entry<String, List<BaseMethodBinding<?>>> nextEntry : resourceToMethods.entrySet()) {
|
for (Entry<String, List<BaseMethodBinding<?>>> nextEntry : resourceToMethods.entrySet()) {
|
||||||
|
@ -274,7 +239,10 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
|
||||||
} else if (nextMethodBinding instanceof OperationMethodBinding) {
|
} else if (nextMethodBinding instanceof OperationMethodBinding) {
|
||||||
OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
|
OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
|
||||||
String opName = myOperationBindingToName.get(methodBinding);
|
String opName = myOperationBindingToName.get(methodBinding);
|
||||||
rest.addOperation().setName(methodBinding.getName()).getDefinition().setReference("OperationDefinition/" + opName);
|
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>() {
|
Collections.sort(resource.getInteraction(), new Comparator<RestResourceInteraction>() {
|
||||||
|
@ -306,7 +274,9 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
|
||||||
if (nextMethodBinding instanceof OperationMethodBinding) {
|
if (nextMethodBinding instanceof OperationMethodBinding) {
|
||||||
OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
|
OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
|
||||||
String opName = myOperationBindingToName.get(methodBinding);
|
String opName = myOperationBindingToName.get(methodBinding);
|
||||||
rest.addOperation().setName(methodBinding.getName()).getDefinition().setReference("OperationDefinition/" + opName);
|
if (operationNames.add(opName)) {
|
||||||
|
rest.addOperation().setName(methodBinding.getName()).getDefinition().setReference("OperationDefinition/" + opName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -316,43 +286,6 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
|
||||||
return 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) {
|
private void handleDynamicSearchMethodBinding(RestResource resource, RuntimeResourceDefinition def, TreeSet<String> includes, DynamicSearchMethodBinding searchMethodBinding) {
|
||||||
includes.addAll(searchMethodBinding.getIncludes());
|
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.
|
* Sets the cache property (default is true). If set to true, the same response will be returned for each invocation.
|
||||||
* <p>
|
* <p>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -20,6 +20,8 @@ package org.hl7.fhir.instance.conf;
|
||||||
* #L%
|
* #L%
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import static org.apache.commons.lang3.StringUtils.*;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
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.SystemRestfulInteraction;
|
||||||
import org.hl7.fhir.instance.model.Conformance.TypeRestfulInteraction;
|
import org.hl7.fhir.instance.model.Conformance.TypeRestfulInteraction;
|
||||||
import org.hl7.fhir.instance.model.Enumerations.ConformanceResourceStatus;
|
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;
|
||||||
import org.hl7.fhir.instance.model.OperationDefinition.OperationDefinitionParameterComponent;
|
import org.hl7.fhir.instance.model.OperationDefinition.OperationDefinitionParameterComponent;
|
||||||
import org.hl7.fhir.instance.model.OperationDefinition.OperationParameterUse;
|
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.IdParam;
|
||||||
import ca.uhn.fhir.rest.annotation.Initialize;
|
import ca.uhn.fhir.rest.annotation.Initialize;
|
||||||
import ca.uhn.fhir.rest.annotation.Metadata;
|
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.annotation.Read;
|
||||||
import ca.uhn.fhir.rest.method.BaseMethodBinding;
|
import ca.uhn.fhir.rest.method.BaseMethodBinding;
|
||||||
import ca.uhn.fhir.rest.method.DynamicSearchMethodBinding;
|
import ca.uhn.fhir.rest.method.DynamicSearchMethodBinding;
|
||||||
|
@ -85,17 +87,64 @@ import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
||||||
* </p>
|
* </p>
|
||||||
*/
|
*/
|
||||||
public class ServerConformanceProvider implements IServerConformanceProvider<Conformance> {
|
public class ServerConformanceProvider implements IServerConformanceProvider<Conformance> {
|
||||||
|
|
||||||
private boolean myCache = true;
|
private boolean myCache = true;
|
||||||
private volatile Conformance myConformance;
|
private volatile Conformance myConformance;
|
||||||
|
private IdentityHashMap<OperationMethodBinding, String> myOperationBindingToName;
|
||||||
|
private HashMap<String, List<OperationMethodBinding>> myOperationNameToBindings;
|
||||||
private String myPublisher = "Not provided";
|
private String myPublisher = "Not provided";
|
||||||
private final RestfulServer myRestfulServer;
|
private final RestfulServer myRestfulServer;
|
||||||
private IdentityHashMap<OperationMethodBinding, String> myOperationBindingToName;
|
|
||||||
private HashMap<String, OperationMethodBinding> myOperationNameToBinding;
|
|
||||||
|
|
||||||
public ServerConformanceProvider(RestfulServer theRestfulServer) {
|
public ServerConformanceProvider(RestfulServer theRestfulServer) {
|
||||||
myRestfulServer = 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
|
* 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
|
* 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;
|
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
|
@Override
|
||||||
@Metadata
|
@Metadata
|
||||||
public Conformance getServerConformance(HttpServletRequest theRequest) {
|
public Conformance getServerConformance(HttpServletRequest theRequest) {
|
||||||
|
@ -208,6 +179,7 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
|
||||||
rest.setMode(RestfulConformanceMode.SERVER);
|
rest.setMode(RestfulConformanceMode.SERVER);
|
||||||
|
|
||||||
Set<SystemRestfulInteraction> systemOps = new HashSet<SystemRestfulInteraction>();
|
Set<SystemRestfulInteraction> systemOps = new HashSet<SystemRestfulInteraction>();
|
||||||
|
Set<String> operationNames = new HashSet<String>();
|
||||||
|
|
||||||
Map<String, List<BaseMethodBinding<?>>> resourceToMethods = collectMethodBindings();
|
Map<String, List<BaseMethodBinding<?>>> resourceToMethods = collectMethodBindings();
|
||||||
for (Entry<String, List<BaseMethodBinding<?>>> nextEntry : resourceToMethods.entrySet()) {
|
for (Entry<String, List<BaseMethodBinding<?>>> nextEntry : resourceToMethods.entrySet()) {
|
||||||
|
@ -277,14 +249,17 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
|
||||||
} else if (nextMethodBinding instanceof OperationMethodBinding) {
|
} else if (nextMethodBinding instanceof OperationMethodBinding) {
|
||||||
OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
|
OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
|
||||||
String opName = myOperationBindingToName.get(methodBinding);
|
String opName = myOperationBindingToName.get(methodBinding);
|
||||||
rest.addOperation().setName(methodBinding.getName()).getDefinition().setReference("OperationDefinition/" + opName);
|
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>() {
|
Collections.sort(resource.getInteraction(), new Comparator<ResourceInteractionComponent>() {
|
||||||
@Override
|
@Override
|
||||||
public int compare(ResourceInteractionComponent theO1, ResourceInteractionComponent theO2) {
|
public int compare(ResourceInteractionComponent theO1, ResourceInteractionComponent theO2) {
|
||||||
TypeRestfulInteraction o1 = theO1.getCodeElement().getValue();
|
TypeRestfulInteraction o1 = theO1.getCode();
|
||||||
TypeRestfulInteraction o2 = theO2.getCodeElement().getValue();
|
TypeRestfulInteraction o2 = theO2.getCode();
|
||||||
if (o1 == null && o2 == null) {
|
if (o1 == null && o2 == null) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -309,7 +284,9 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
|
||||||
if (nextMethodBinding instanceof OperationMethodBinding) {
|
if (nextMethodBinding instanceof OperationMethodBinding) {
|
||||||
OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
|
OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
|
||||||
String opName = myOperationBindingToName.get(methodBinding);
|
String opName = myOperationBindingToName.get(methodBinding);
|
||||||
rest.addOperation().setName(methodBinding.getName()).getDefinition().setReference("OperationDefinition/" + opName);
|
if (operationNames.add(opName)) {
|
||||||
|
rest.addOperation().setName(methodBinding.getName()).getDefinition().setReference("OperationDefinition/" + opName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -319,48 +296,6 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
|
||||||
return 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) {
|
private void handleDynamicSearchMethodBinding(ConformanceRestResourceComponent resource, RuntimeResourceDefinition def, TreeSet<String> includes, DynamicSearchMethodBinding searchMethodBinding) {
|
||||||
includes.addAll(searchMethodBinding.getIncludes());
|
includes.addAll(searchMethodBinding.getIncludes());
|
||||||
|
|
||||||
|
@ -393,8 +328,7 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ConformanceRestResourceSearchParamComponent param;
|
ConformanceRestResourceSearchParamComponent param = resource.addSearchParam();
|
||||||
param = resource.addSearchParam();
|
|
||||||
|
|
||||||
param.setName(nextParamName);
|
param.setName(nextParamName);
|
||||||
// if (StringUtils.isNotBlank(chain)) {
|
// if (StringUtils.isNotBlank(chain)) {
|
||||||
|
@ -467,9 +401,9 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
|
||||||
for (Class<? extends IResource> nextTarget : nextParameter.getDeclaredTypes()) {
|
for (Class<? extends IResource> nextTarget : nextParameter.getDeclaredTypes()) {
|
||||||
RuntimeResourceDefinition targetDef = myRestfulServer.getFhirContext().getResourceDefinition(nextTarget);
|
RuntimeResourceDefinition targetDef = myRestfulServer.getFhirContext().getResourceDefinition(nextTarget);
|
||||||
if (targetDef != null) {
|
if (targetDef != null) {
|
||||||
org.hl7.fhir.instance.model.Enumerations.ResourceType code;
|
ResourceType code;
|
||||||
try {
|
try {
|
||||||
code = org.hl7.fhir.instance.model.Enumerations.ResourceType.fromCode(targetDef.getName());
|
code = ResourceType.fromCode(targetDef.getName());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
code = null;
|
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.
|
* Sets the cache property (default is true). If set to true, the same response will be returned for each invocation.
|
||||||
* <p>
|
* <p>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue