Improve search method binding priority (#1802)

* Work on search method binding priority

* Work on method priority

* Work on binding priority

* Test fixes

* Add changelog

* Test fixes

* compile fix

* One more comple fix

* Test cleanup

* Test fix
This commit is contained in:
James Agnew 2020-04-17 09:28:33 -04:00 committed by GitHub
parent 8873749d9c
commit 497757501b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 691 additions and 300 deletions

View File

@ -118,6 +118,19 @@ public class ParameterUtil {
return parseQueryParams(theContext, paramType, theUnqualifiedParamName, theParameters);
}
/**
* Removes :modifiers and .chains from URL parameter names
*/
public static String stripModifierPart(String theParam) {
for (int i = 0; i < theParam.length(); i++) {
char nextChar = theParam.charAt(i);
if (nextChar == ':' || nextChar == '.') {
return theParam.substring(0, i);
}
}
return theParam;
}
/**
* Escapes a string according to the rules for parameter escaping specified in the <a href="http://www.hl7.org/implement/standards/fhir/search.html#escaping">FHIR Specification Escaping
* Section</a>

View File

@ -31,31 +31,63 @@ import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
public class ReflectionUtil {
private static final ConcurrentHashMap<String, Object> ourFhirServerVersions = new ConcurrentHashMap<>();
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ReflectionUtil.class);
public static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
public static final Class<?>[] EMPTY_CLASS_ARRAY = new Class[0];
private static final ConcurrentHashMap<String, Object> ourFhirServerVersions = new ConcurrentHashMap<>();
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ReflectionUtil.class);
public static LinkedHashSet<Method> getDeclaredMethods(Class<?> theClazz) {
LinkedHashSet<Method> retVal = new LinkedHashSet<>();
for (Method next : theClazz.getDeclaredMethods()) {
/**
* Returns all methods declared against {@literal theClazz}. This method returns a predictable order, which is
* sorted by method name and then by parameters.
*/
public static List<Method> getDeclaredMethods(Class<?> theClazz) {
HashSet<Method> foundMethods = new LinkedHashSet<>();
Method[] declaredMethods = theClazz.getDeclaredMethods();
for (Method next : declaredMethods) {
try {
Method method = theClazz.getMethod(next.getName(), next.getParameterTypes());
retVal.add(method);
foundMethods.add(method);
} catch (NoSuchMethodException | SecurityException e) {
retVal.add(next);
foundMethods.add(next);
}
}
List<Method> retVal = new ArrayList<>(foundMethods);
retVal.sort((Comparator.comparing(ReflectionUtil::describeMethodInSortFriendlyWay)));
return retVal;
}
/**
* Returns a description like <code>startsWith params(java.lang.String, int) returns(boolean)</code>.
* The format is chosen in order to provide a predictable and useful sorting order.
*/
public static String describeMethodInSortFriendlyWay(Method theMethod) {
StringBuilder b = new StringBuilder();
b.append(theMethod.getName());
b.append(" returns(");
b.append(theMethod.getReturnType().getName());
b.append(") params(");
Class<?>[] parameterTypes = theMethod.getParameterTypes();
for (int i = 0, parameterTypesLength = parameterTypes.length; i < parameterTypesLength; i++) {
if (i > 0) {
b.append(", ");
}
Class<?> next = parameterTypes[i];
b.append(next.getName());
}
b.append(")");
return b.toString();
}
public static Class<?> getGenericCollectionTypeOfField(Field next) {
ParameterizedType collectionType = (ParameterizedType) next.getGenericType();
return getGenericCollectionTypeOf(collectionType.getActualTypeArguments()[0]);

View File

@ -2,6 +2,7 @@ package ca.uhn.fhir.util;
import static org.junit.Assert.*;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
@ -51,4 +52,12 @@ public class ReflectionUtilTest {
assertFalse(ReflectionUtil.typeExists("ca.Foo"));
assertTrue(ReflectionUtil.typeExists(String.class.getName()));
}
@Test
public void testDescribeMethod() throws NoSuchMethodException {
Method method = String.class.getMethod("startsWith", String.class, int.class);
String description = ReflectionUtil.describeMethodInSortFriendlyWay(method);
assertEquals("startsWith returns(boolean) params(java.lang.String, int)", description);
}
}

View File

@ -0,0 +1,8 @@
---
type: add
issue: 1802
title: "In a plain server, if a Resource Provider class had two methods with the same parameter names
(as specified in the @OptionalParam or @RequiredParam) but different cardinalities, the server could
sometimes pick the incorrect method to execute. The selection algorithm has been improved to no longer
have this issue, and to be more consistent and predictable in terms of which resource provider
method is selected when the choice is somewhat ambiguous."

View File

@ -224,7 +224,7 @@ public abstract class BaseStorageDao {
Set<String> paramNames = theSource.keySet();
for (String nextParamName : paramNames) {
QualifierDetails qualifiedParamName = SearchMethodBinding.extractQualifiersFromParameterName(nextParamName);
QualifierDetails qualifiedParamName = QualifierDetails.extractQualifiersFromParameterName(nextParamName);
RuntimeSearchParam param = searchParams.get(qualifiedParamName.getParamName());
if (param == null) {
String msg = getContext().getLocalizer().getMessageSanitized(BaseHapiFhirResourceDao.class, "invalidSearchParameter", qualifiedParamName.getParamName(), new TreeSet<>(searchParams.keySet()));

View File

@ -29,10 +29,18 @@ import java.util.ArrayList;
import java.util.List;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.in;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.nullable;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
public class FhirResourceDaoR4InterceptorTest extends BaseJpaR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4InterceptorTest.class);

View File

@ -25,9 +25,10 @@ import java.util.List;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.method.BaseMethodBinding;
import ca.uhn.fhir.rest.server.method.MethodMatchEnum;
/**
* Created by dsotnikov on 2/25/2014.
* Holds all method bindings for an individual resource type
*/
public class ResourceBinding {
@ -36,9 +37,16 @@ public class ResourceBinding {
private String resourceName;
private List<BaseMethodBinding<?>> myMethodBindings = new ArrayList<>();
/**
* Constructor
*/
public ResourceBinding() {
super();
}
/**
* Constructor
*/
public ResourceBinding(String resourceName, List<BaseMethodBinding<?>> methods) {
this.resourceName = resourceName;
this.myMethodBindings = methods;
@ -51,14 +59,28 @@ public class ResourceBinding {
}
ourLog.debug("Looking for a handler for {}", theRequest);
/*
* Look for the method with the highest match strength
*/
BaseMethodBinding<?> matchedMethod = null;
MethodMatchEnum matchedMethodStrength = null;
for (BaseMethodBinding<?> rm : myMethodBindings) {
if (rm.incomingServerRequestMatchesMethod(theRequest)) {
ourLog.debug("Handler {} matches", rm);
return rm;
MethodMatchEnum nextMethodMatch = rm.incomingServerRequestMatchesMethod(theRequest);
if (nextMethodMatch != MethodMatchEnum.NONE) {
if (matchedMethodStrength == null || matchedMethodStrength.ordinal() < nextMethodMatch.ordinal()) {
matchedMethod = rm;
matchedMethodStrength = nextMethodMatch;
}
if (matchedMethodStrength == MethodMatchEnum.EXACT) {
break;
}
}
ourLog.trace("Handler {} does not match", rm);
}
return null;
return matchedMethod;
}
public String getResourceName() {

View File

@ -45,6 +45,7 @@ import ca.uhn.fhir.rest.server.interceptor.ExceptionHandlingInterceptor;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.method.BaseMethodBinding;
import ca.uhn.fhir.rest.server.method.ConformanceMethodBinding;
import ca.uhn.fhir.rest.server.method.MethodMatchEnum;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.rest.server.tenant.ITenantIdentificationStrategy;
import ca.uhn.fhir.util.*;
@ -298,7 +299,7 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
ResourceBinding resourceBinding = null;
BaseMethodBinding<?> resourceMethod = null;
String resourceName = requestDetails.getResourceName();
if (myServerConformanceMethod.incomingServerRequestMatchesMethod(requestDetails)) {
if (myServerConformanceMethod.incomingServerRequestMatchesMethod(requestDetails) != MethodMatchEnum.NONE) {
resourceMethod = myServerConformanceMethod;
} else if (resourceName == null) {
resourceBinding = myServerBinding;
@ -1785,6 +1786,21 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
}
}
/**
* Unregisters all plain and resource providers (but not the conformance provider).
*/
public void unregisterAllProviders() {
unregisterAllProviders(myPlainProviders);
unregisterAllProviders(myResourceProviders);
}
private void unregisterAllProviders(List<?> theProviders) {
while (theProviders.size() > 0) {
unregisterProvider(theProviders.get(0));
}
}
private void writeExceptionToResponse(HttpServletResponse theResponse, BaseServerResponseException theException) throws IOException {
theResponse.setStatus(theException.getStatusCode());
addHeadersToResponse(theResponse);

View File

@ -26,47 +26,44 @@ import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.annotation.*;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.IRestfulServer;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.client.exceptions.NonFhirResponseException;
import ca.uhn.fhir.rest.server.BundleProviders;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.ReflectionUtil;
import org.apache.commons.io.IOUtils;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import static org.apache.commons.lang3.StringUtils.isBlank;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
public abstract class BaseMethodBinding<T> {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseMethodBinding.class);
private final List<BaseQueryParameter> myQueryParameters;
private FhirContext myContext;
private Method myMethod;
private List<IParameter> myParameters;
private Object myProvider;
private boolean mySupportsConditional;
private boolean mySupportsConditionalMultiple;
public BaseMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) {
assert theMethod != null;
assert theContext != null;
@ -75,6 +72,11 @@ public abstract class BaseMethodBinding<T> {
myContext = theContext;
myProvider = theProvider;
myParameters = MethodUtil.getResourceParameters(theContext, theMethod, theProvider, getRestOperationType());
myQueryParameters = myParameters
.stream()
.filter(t -> t instanceof BaseQueryParameter)
.map(t -> (BaseQueryParameter) t)
.collect(Collectors.toList());
for (IParameter next : myParameters) {
if (next instanceof ConditionalParamBinder) {
@ -90,6 +92,10 @@ public abstract class BaseMethodBinding<T> {
myMethod.setAccessible(true);
}
protected List<BaseQueryParameter> getQueryParameters() {
return myQueryParameters;
}
protected Object[] createMethodParams(RequestDetails theRequest) {
Object[] params = new Object[getParameters().size()];
for (int i = 0; i < getParameters().size(); i++) {
@ -211,7 +217,7 @@ public abstract class BaseMethodBinding<T> {
return getRestOperationType();
}
public abstract boolean incomingServerRequestMatchesMethod(RequestDetails theRequest);
public abstract MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequest);
public abstract Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest) throws BaseServerResponseException, IOException;

View File

@ -111,20 +111,20 @@ abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding<Metho
}
@Override
public boolean incomingServerRequestMatchesMethod(RequestDetails theRequest) {
public MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequest) {
Set<RequestTypeEnum> allowableRequestTypes = provideAllowableRequestTypes();
RequestTypeEnum requestType = theRequest.getRequestType();
if (!allowableRequestTypes.contains(requestType)) {
return false;
return MethodMatchEnum.NONE;
}
if (!getResourceName().equals(theRequest.getResourceName())) {
return false;
return MethodMatchEnum.NONE;
}
if (getMatchingOperation() == null && StringUtils.isNotBlank(theRequest.getOperation())) {
return false;
return MethodMatchEnum.NONE;
}
if (getMatchingOperation() != null && !getMatchingOperation().equals(theRequest.getOperation())) {
return false;
return MethodMatchEnum.NONE;
}
/*
@ -137,7 +137,7 @@ abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding<Metho
* It's also needed for conditional update..
*/
return true;
return MethodMatchEnum.EXACT;
}
@Override

View File

@ -58,6 +58,8 @@ public abstract class BaseQueryParameter implements IParameter {
return null;
}
protected abstract boolean supportsRepetition();
/**
* Parameter should return true if {@link #parse(FhirContext, List)} should be called even if the query string
* contained no values for the given parameter

View File

@ -152,25 +152,25 @@ public class ConformanceMethodBinding extends BaseResourceReturningMethodBinding
}
@Override
public boolean incomingServerRequestMatchesMethod(RequestDetails theRequest) {
public MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequest) {
if (theRequest.getRequestType() == RequestTypeEnum.OPTIONS) {
if (theRequest.getOperation() == null && theRequest.getResourceName() == null) {
return true;
return MethodMatchEnum.EXACT;
}
}
if (theRequest.getResourceName() != null) {
return false;
return MethodMatchEnum.NONE;
}
if ("metadata".equals(theRequest.getOperation())) {
if (theRequest.getRequestType() == RequestTypeEnum.GET) {
return true;
return MethodMatchEnum.EXACT;
}
throw new MethodNotAllowedException("/metadata request must use HTTP GET", RequestTypeEnum.GET);
}
return false;
return MethodMatchEnum.NONE;
}
@Nonnull

View File

@ -29,10 +29,8 @@ import ca.uhn.fhir.rest.api.server.IRestfulServer;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.ResponseDetails;
import ca.uhn.fhir.rest.param.ParameterUtil;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import org.hl7.fhir.instance.model.api.IBaseCoding;
import org.hl7.fhir.instance.model.api.IBaseResource;
import javax.annotation.Nonnull;
@ -69,12 +67,12 @@ public class GraphQLMethodBinding extends BaseMethodBinding<String> {
}
@Override
public boolean incomingServerRequestMatchesMethod(RequestDetails theRequest) {
public MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequest) {
if (Constants.OPERATION_NAME_GRAPHQL.equals(theRequest.getOperation())) {
return true;
return MethodMatchEnum.EXACT;
}
return false;
return MethodMatchEnum.NONE;
}
@Override

View File

@ -51,7 +51,7 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
private final Integer myIdParamIndex;
private final RestOperationTypeEnum myResourceOperationType;
private String myResourceName;
private final String myResourceName;
public HistoryMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) {
super(toReturnType(theMethod, theProvider), theMethod, theContext, theProvider);
@ -105,30 +105,36 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
// ObjectUtils.equals is replaced by a JDK7 method..
@Override
public boolean incomingServerRequestMatchesMethod(RequestDetails theRequest) {
public MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequest) {
if (!Constants.PARAM_HISTORY.equals(theRequest.getOperation())) {
return false;
return MethodMatchEnum.NONE;
}
if (theRequest.getResourceName() == null) {
return myResourceOperationType == RestOperationTypeEnum.HISTORY_SYSTEM;
if (myResourceOperationType == RestOperationTypeEnum.HISTORY_SYSTEM) {
return MethodMatchEnum.EXACT;
} else {
return MethodMatchEnum.NONE;
}
}
if (!StringUtils.equals(theRequest.getResourceName(), myResourceName)) {
return false;
return MethodMatchEnum.NONE;
}
boolean haveIdParam = theRequest.getId() != null && !theRequest.getId().isEmpty();
boolean wantIdParam = myIdParamIndex != null;
if (haveIdParam != wantIdParam) {
return false;
return MethodMatchEnum.NONE;
}
if (theRequest.getId() == null) {
return myResourceOperationType == RestOperationTypeEnum.HISTORY_TYPE;
if (myResourceOperationType != RestOperationTypeEnum.HISTORY_TYPE) {
return MethodMatchEnum.NONE;
}
} else if (theRequest.getId().hasVersionIdPart()) {
return false;
return MethodMatchEnum.NONE;
}
return true;
return MethodMatchEnum.EXACT;
}
@ -179,7 +185,7 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
}
if (isBlank(nextResource.getIdElement().getVersionIdPart()) && nextResource instanceof IResource) {
//TODO: Use of a deprecated method should be resolved.
IdDt versionId = (IdDt) ResourceMetadataKeyEnum.VERSION_ID.get((IResource) nextResource);
IdDt versionId = ResourceMetadataKeyEnum.VERSION_ID.get((IResource) nextResource);
if (versionId == null || versionId.isEmpty()) {
throw new InternalErrorException("Server provided resource at index " + index + " with no Version ID set (using IResource#setId(IdDt))");
}

View File

@ -106,6 +106,11 @@ class IncludeParameter extends BaseQueryParameter {
return null;
}
@Override
protected boolean supportsRepetition() {
return myInstantiableCollectionType != null;
}
@Override
public boolean handlesMissing() {
return true;

View File

@ -0,0 +1,19 @@
package ca.uhn.fhir.rest.server.method;
public enum MethodMatchEnum {
// Order these from worst to best!
NONE,
APPROXIMATE,
EXACT;
public MethodMatchEnum weakerOf(MethodMatchEnum theOther) {
if (this.ordinal() < theOther.ordinal()) {
return this;
} else {
return theOther;
}
}
}

View File

@ -36,7 +36,6 @@ import ca.uhn.fhir.rest.server.method.ResourceParameter.Mode;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.ParametersUtil;
import ca.uhn.fhir.util.ReflectionUtil;
import ca.uhn.fhir.util.ValidateUtil;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
@ -144,7 +143,7 @@ public class MethodUtil {
parameter.setRequired(true);
parameter.setDeclaredTypes(((RequiredParam) nextAnnotation).targetTypes());
parameter.setCompositeTypes(((RequiredParam) nextAnnotation).compositeTypes());
parameter.setChainlists(((RequiredParam) nextAnnotation).chainWhitelist(), ((RequiredParam) nextAnnotation).chainBlacklist());
parameter.setChainLists(((RequiredParam) nextAnnotation).chainWhitelist(), ((RequiredParam) nextAnnotation).chainBlacklist());
parameter.setType(theContext, parameterType, innerCollectionType, outerCollectionType);
MethodUtil.extractDescription(parameter, annotations);
param = parameter;
@ -154,12 +153,12 @@ public class MethodUtil {
parameter.setRequired(false);
parameter.setDeclaredTypes(((OptionalParam) nextAnnotation).targetTypes());
parameter.setCompositeTypes(((OptionalParam) nextAnnotation).compositeTypes());
parameter.setChainlists(((OptionalParam) nextAnnotation).chainWhitelist(), ((OptionalParam) nextAnnotation).chainBlacklist());
parameter.setChainLists(((OptionalParam) nextAnnotation).chainWhitelist(), ((OptionalParam) nextAnnotation).chainBlacklist());
parameter.setType(theContext, parameterType, innerCollectionType, outerCollectionType);
MethodUtil.extractDescription(parameter, annotations);
param = parameter;
} else if (nextAnnotation instanceof RawParam) {
param = new RawParamsParmeter(parameters);
param = new RawParamsParameter(parameters);
} else if (nextAnnotation instanceof IncludeParam) {
Class<? extends Collection<Include>> instantiableCollectionType;
Class<?> specType;

View File

@ -226,43 +226,43 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
}
@Override
public boolean incomingServerRequestMatchesMethod(RequestDetails theRequest) {
public MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequest) {
if (isBlank(theRequest.getOperation())) {
return false;
return MethodMatchEnum.NONE;
}
if (!myName.equals(theRequest.getOperation())) {
if (!myName.equals(WILDCARD_NAME)) {
return false;
return MethodMatchEnum.NONE;
}
}
if (getResourceName() == null) {
if (isNotBlank(theRequest.getResourceName())) {
if (!isGlobalMethod()) {
return false;
return MethodMatchEnum.NONE;
}
}
}
if (getResourceName() != null && !getResourceName().equals(theRequest.getResourceName())) {
return false;
return MethodMatchEnum.NONE;
}
RequestTypeEnum requestType = theRequest.getRequestType();
if (requestType != RequestTypeEnum.GET && requestType != RequestTypeEnum.POST) {
// Operations can only be invoked with GET and POST
return false;
return MethodMatchEnum.NONE;
}
boolean requestHasId = theRequest.getId() != null;
if (requestHasId) {
return myCanOperateAtInstanceLevel;
return myCanOperateAtInstanceLevel ? MethodMatchEnum.EXACT : MethodMatchEnum.NONE;
}
if (isNotBlank(theRequest.getResourceName())) {
return myCanOperateAtTypeLevel;
return myCanOperateAtTypeLevel ? MethodMatchEnum.EXACT : MethodMatchEnum.NONE;
}
return myCanOperateAtServerLevel;
return myCanOperateAtServerLevel ? MethodMatchEnum.EXACT : MethodMatchEnum.NONE;
}
@Override

View File

@ -174,12 +174,16 @@ public class PageMethodBinding extends BaseResourceReturningMethodBinding {
}
@Override
public boolean incomingServerRequestMatchesMethod(RequestDetails theRequest) {
public MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequest) {
String[] pageId = theRequest.getParameters().get(Constants.PARAM_PAGINGACTION);
if (pageId == null || pageId.length == 0 || isBlank(pageId[0])) {
return false;
return MethodMatchEnum.NONE;
}
return theRequest.getRequestType() == RequestTypeEnum.GET;
if (theRequest.getRequestType() != RequestTypeEnum.GET) {
return MethodMatchEnum.NONE;
}
return MethodMatchEnum.EXACT;
}

View File

@ -80,9 +80,9 @@ public class PatchMethodBinding extends BaseOutcomeReturningMethodBindingWithRes
}
@Override
public boolean incomingServerRequestMatchesMethod(RequestDetails theRequest) {
boolean retVal = super.incomingServerRequestMatchesMethod(theRequest);
if (retVal) {
public MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequest) {
MethodMatchEnum retVal = super.incomingServerRequestMatchesMethod(theRequest);
if (retVal.ordinal() > MethodMatchEnum.NONE.ordinal()) {
PatchTypeParameter.getTypeForRequestOrThrowInvalidRequestException(theRequest);
}
return retVal;

View File

@ -35,11 +35,11 @@ import ca.uhn.fhir.rest.param.QualifierDetails;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
public class RawParamsParmeter implements IParameter {
public class RawParamsParameter implements IParameter {
private final List<IParameter> myAllMethodParameters;
public RawParamsParmeter(List<IParameter> theParameters) {
public RawParamsParameter(List<IParameter> theParameters) {
myAllMethodParameters = theParameters;
}
@ -53,7 +53,7 @@ public class RawParamsParmeter implements IParameter {
continue;
}
QualifierDetails qualifiers = SearchMethodBinding.extractQualifiersFromParameterName(nextName);
QualifierDetails qualifiers = QualifierDetails.extractQualifiersFromParameterName(nextName);
boolean alreadyCaptured = false;
for (IParameter nextParameter : myAllMethodParameters) {
@ -70,7 +70,7 @@ public class RawParamsParmeter implements IParameter {
if (!alreadyCaptured) {
if (retVal == null) {
retVal = new HashMap<String, List<String>>();
retVal = new HashMap<>();
}
retVal.put(nextName, Arrays.asList(theRequest.getParameters().get(nextName)));
}

View File

@ -114,40 +114,40 @@ public class ReadMethodBinding extends BaseResourceReturningMethodBinding {
}
@Override
public boolean incomingServerRequestMatchesMethod(RequestDetails theRequest) {
public MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequest) {
if (!theRequest.getResourceName().equals(getResourceName())) {
return false;
return MethodMatchEnum.NONE;
}
for (String next : theRequest.getParameters().keySet()) {
if (!next.startsWith("_")) {
return false;
return MethodMatchEnum.NONE;
}
}
if (theRequest.getId() == null) {
return false;
return MethodMatchEnum.NONE;
}
if (mySupportsVersion == false) {
if (theRequest.getId().hasVersionIdPart()) {
return false;
return MethodMatchEnum.NONE;
}
}
if (isNotBlank(theRequest.getCompartmentName())) {
return false;
return MethodMatchEnum.NONE;
}
if (theRequest.getRequestType() != RequestTypeEnum.GET && theRequest.getRequestType() != RequestTypeEnum.HEAD ) {
ourLog.trace("Method {} doesn't match because request type is not GET or HEAD: {}", theRequest.getId(), theRequest.getRequestType());
return false;
return MethodMatchEnum.NONE;
}
if (Constants.PARAM_HISTORY.equals(theRequest.getOperation())) {
if (mySupportsVersion == false) {
return false;
return MethodMatchEnum.NONE;
} else if (theRequest.getId().hasVersionIdPart() == false) {
return false;
return MethodMatchEnum.NONE;
}
} else if (!StringUtils.isBlank(theRequest.getOperation())) {
return false;
return MethodMatchEnum.NONE;
}
return true;
return MethodMatchEnum.EXACT;
}

View File

@ -41,7 +41,11 @@ import org.hl7.fhir.instance.model.api.IBaseResource;
import javax.annotation.Nonnull;
import java.lang.reflect.Method;
import java.util.*;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@ -61,11 +65,13 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
}
private final String myResourceProviderResourceName;
private String myCompartmentName;
private final List<String> myRequiredParamNames;
private final List<String> myOptionalParamNames;
private final String myCompartmentName;
private String myDescription;
private Integer myIdParamIndex;
private String myQueryName;
private boolean myAllowUnknownParams;
private final Integer myIdParamIndex;
private final String myQueryName;
private final boolean myAllowUnknownParams;
public SearchMethodBinding(Class<? extends IBaseResource> theReturnResourceType, Class<? extends IBaseResource> theResourceProviderResourceType, Method theMethod, FhirContext theContext, Object theProvider) {
super(theReturnResourceType, theMethod, theContext, theProvider);
@ -98,6 +104,17 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
this.myResourceProviderResourceName = null;
}
myRequiredParamNames = getQueryParameters()
.stream()
.filter(t -> t.isRequired())
.map(t -> t.getName())
.collect(Collectors.toList());
myOptionalParamNames = getQueryParameters()
.stream()
.filter(t -> !t.isRequired())
.map(t -> t.getName())
.collect(Collectors.toList());
}
public String getDescription() {
@ -129,122 +146,129 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
}
@Override
public boolean incomingServerRequestMatchesMethod(RequestDetails theRequest) {
public MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequest) {
if (theRequest.getId() != null && myIdParamIndex == null) {
ourLog.trace("Method {} doesn't match because ID is not null: {}", getMethod(), theRequest.getId());
return false;
return MethodMatchEnum.NONE;
}
if (theRequest.getRequestType() == RequestTypeEnum.GET && theRequest.getOperation() != null && !Constants.PARAM_SEARCH.equals(theRequest.getOperation())) {
ourLog.trace("Method {} doesn't match because request type is GET but operation is not null: {}", theRequest.getId(), theRequest.getOperation());
return false;
return MethodMatchEnum.NONE;
}
if (theRequest.getRequestType() == RequestTypeEnum.POST && !Constants.PARAM_SEARCH.equals(theRequest.getOperation())) {
ourLog.trace("Method {} doesn't match because request type is POST but operation is not _search: {}", theRequest.getId(), theRequest.getOperation());
return false;
return MethodMatchEnum.NONE;
}
if (theRequest.getRequestType() != RequestTypeEnum.GET && theRequest.getRequestType() != RequestTypeEnum.POST) {
ourLog.trace("Method {} doesn't match because request type is {}", getMethod(), theRequest.getRequestType());
return false;
return MethodMatchEnum.NONE;
}
if (!StringUtils.equals(myCompartmentName, theRequest.getCompartmentName())) {
ourLog.trace("Method {} doesn't match because it is for compartment {} but request is compartment {}", getMethod(), myCompartmentName, theRequest.getCompartmentName());
return false;
return MethodMatchEnum.NONE;
}
if (theRequest.getParameters().get(Constants.PARAM_PAGINGACTION) != null) {
return false;
return MethodMatchEnum.NONE;
}
// This is used to track all the parameters so we can reject queries that
// have additional params we don't understand
Set<String> methodParamsTemp = new HashSet<>();
Set<String> unqualifiedNames = theRequest.getUnqualifiedToQualifiedNames().keySet();
Set<String> qualifiedParamNames = theRequest.getParameters().keySet();
for (IParameter nextParameter : getParameters()) {
if (!(nextParameter instanceof BaseQueryParameter)) {
continue;
}
BaseQueryParameter nextQueryParameter = (BaseQueryParameter) nextParameter;
String name = nextQueryParameter.getName();
if (nextQueryParameter.isRequired()) {
if (qualifiedParamNames.contains(name)) {
QualifierDetails qualifiers = extractQualifiersFromParameterName(name);
if (qualifiers.passes(nextQueryParameter.getQualifierWhitelist(), nextQueryParameter.getQualifierBlacklist())) {
methodParamsTemp.add(name);
}
}
if (unqualifiedNames.contains(name)) {
List<String> qualifiedNames = theRequest.getUnqualifiedToQualifiedNames().get(name);
qualifiedNames = processWhitelistAndBlacklist(qualifiedNames, nextQueryParameter.getQualifierWhitelist(), nextQueryParameter.getQualifierBlacklist());
methodParamsTemp.addAll(qualifiedNames);
}
if (!qualifiedParamNames.contains(name) && !unqualifiedNames.contains(name)) {
ourLog.trace("Method {} doesn't match param '{}' is not present", getMethod().getName(), name);
return false;
}
} else {
if (qualifiedParamNames.contains(name)) {
QualifierDetails qualifiers = extractQualifiersFromParameterName(name);
if (qualifiers.passes(nextQueryParameter.getQualifierWhitelist(), nextQueryParameter.getQualifierBlacklist())) {
methodParamsTemp.add(name);
}
}
if (unqualifiedNames.contains(name)) {
List<String> qualifiedNames = theRequest.getUnqualifiedToQualifiedNames().get(name);
qualifiedNames = processWhitelistAndBlacklist(qualifiedNames, nextQueryParameter.getQualifierWhitelist(), nextQueryParameter.getQualifierBlacklist());
methodParamsTemp.addAll(qualifiedNames);
}
if (!qualifiedParamNames.contains(name)) {
methodParamsTemp.add(name);
}
}
}
if (myQueryName != null) {
String[] queryNameValues = theRequest.getParameters().get(Constants.PARAM_QUERY);
if (queryNameValues != null && StringUtils.isNotBlank(queryNameValues[0])) {
String queryName = queryNameValues[0];
if (!myQueryName.equals(queryName)) {
ourLog.trace("Query name does not match {}", myQueryName);
return false;
return MethodMatchEnum.NONE;
}
methodParamsTemp.add(Constants.PARAM_QUERY);
} else {
ourLog.trace("Query name does not match {}", myQueryName);
return false;
return MethodMatchEnum.NONE;
}
} else {
String[] queryNameValues = theRequest.getParameters().get(Constants.PARAM_QUERY);
if (queryNameValues != null && StringUtils.isNotBlank(queryNameValues[0])) {
ourLog.trace("Query has name");
return false;
return MethodMatchEnum.NONE;
}
}
for (String next : theRequest.getParameters().keySet()) {
if (next.startsWith("_") && !SPECIAL_SEARCH_PARAMS.contains(truncModifierPart(next))) {
methodParamsTemp.add(next);
}
}
Set<String> keySet = theRequest.getParameters().keySet();
if (myAllowUnknownParams == false) {
for (String next : keySet) {
if (!methodParamsTemp.contains(next)) {
return false;
Set<String> unqualifiedNames = theRequest.getUnqualifiedToQualifiedNames().keySet();
Set<String> qualifiedParamNames = theRequest.getParameters().keySet();
MethodMatchEnum retVal = MethodMatchEnum.EXACT;
for (String nextRequestParam : theRequest.getParameters().keySet()) {
String nextUnqualifiedRequestParam = ParameterUtil.stripModifierPart(nextRequestParam);
if (nextRequestParam.startsWith("_") && !SPECIAL_SEARCH_PARAMS.contains(nextUnqualifiedRequestParam)) {
continue;
}
boolean parameterMatches = false;
boolean approx = false;
for (BaseQueryParameter nextMethodParam : getQueryParameters()) {
if (nextRequestParam.equals(nextMethodParam.getName())) {
QualifierDetails qualifiers = QualifierDetails.extractQualifiersFromParameterName(nextRequestParam);
if (qualifiers.passes(nextMethodParam.getQualifierWhitelist(), nextMethodParam.getQualifierBlacklist())) {
parameterMatches = true;
}
} else if (nextUnqualifiedRequestParam.equals(nextMethodParam.getName())) {
List<String> qualifiedNames = theRequest.getUnqualifiedToQualifiedNames().get(nextUnqualifiedRequestParam);
if (passesWhitelistAndBlacklist(qualifiedNames, nextMethodParam.getQualifierWhitelist(), nextMethodParam.getQualifierBlacklist())) {
parameterMatches = true;
}
}
// Repetitions supplied by URL but not supported by this parameter
if (theRequest.getParameters().get(nextRequestParam).length > 1 != nextMethodParam.supportsRepetition()) {
approx = true;
}
}
if (parameterMatches) {
if (approx) {
retVal = retVal.weakerOf(MethodMatchEnum.APPROXIMATE);
}
} else {
if (myAllowUnknownParams) {
retVal = retVal.weakerOf(MethodMatchEnum.APPROXIMATE);
} else {
retVal = retVal.weakerOf(MethodMatchEnum.NONE);
}
}
if (retVal == MethodMatchEnum.NONE) {
break;
}
}
if (retVal != MethodMatchEnum.NONE) {
for (String nextRequiredParamName : myRequiredParamNames) {
if (!qualifiedParamNames.contains(nextRequiredParamName)) {
if (!unqualifiedNames.contains(nextRequiredParamName)) {
retVal = MethodMatchEnum.NONE;
break;
}
}
}
}
return true;
}
private String truncModifierPart(String param) {
int indexOfSeparator = param.indexOf(":");
if (indexOfSeparator != -1) {
return param.substring(0, indexOfSeparator);
if (retVal != MethodMatchEnum.NONE) {
for (String nextRequiredParamName : myOptionalParamNames) {
if (!qualifiedParamNames.contains(nextRequiredParamName)) {
if (!unqualifiedNames.contains(nextRequiredParamName)) {
retVal = retVal.weakerOf(MethodMatchEnum.APPROXIMATE);
}
}
}
}
return param;
return retVal;
}
@Override
@ -264,19 +288,18 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
return false;
}
private List<String> processWhitelistAndBlacklist(List<String> theQualifiedNames, Set<String> theQualifierWhitelist, Set<String> theQualifierBlacklist) {
private boolean passesWhitelistAndBlacklist(List<String> theQualifiedNames, Set<String> theQualifierWhitelist, Set<String> theQualifierBlacklist) {
if (theQualifierWhitelist == null && theQualifierBlacklist == null) {
return theQualifiedNames;
return true;
}
ArrayList<String> retVal = new ArrayList<>(theQualifiedNames.size());
for (String next : theQualifiedNames) {
QualifierDetails qualifiers = extractQualifiersFromParameterName(next);
QualifierDetails qualifiers = QualifierDetails.extractQualifiersFromParameterName(next);
if (!qualifiers.passes(theQualifierWhitelist, theQualifierBlacklist)) {
continue;
return false;
}
retVal.add(next);
}
return retVal;
return true;
}
@Override
@ -284,52 +307,5 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
return getMethod().toString();
}
public static QualifierDetails extractQualifiersFromParameterName(String theParamName) {
QualifierDetails retVal = new QualifierDetails();
if (theParamName == null || theParamName.length() == 0) {
return retVal;
}
int dotIdx = -1;
int colonIdx = -1;
for (int idx = 0; idx < theParamName.length(); idx++) {
char nextChar = theParamName.charAt(idx);
if (nextChar == '.' && dotIdx == -1) {
dotIdx = idx;
} else if (nextChar == ':' && colonIdx == -1) {
colonIdx = idx;
}
}
if (dotIdx != -1 && colonIdx != -1) {
if (dotIdx < colonIdx) {
retVal.setDotQualifier(theParamName.substring(dotIdx, colonIdx));
retVal.setColonQualifier(theParamName.substring(colonIdx));
retVal.setParamName(theParamName.substring(0, dotIdx));
retVal.setWholeQualifier(theParamName.substring(dotIdx));
} else {
retVal.setColonQualifier(theParamName.substring(colonIdx, dotIdx));
retVal.setDotQualifier(theParamName.substring(dotIdx));
retVal.setParamName(theParamName.substring(0, colonIdx));
retVal.setWholeQualifier(theParamName.substring(colonIdx));
}
} else if (dotIdx != -1) {
retVal.setDotQualifier(theParamName.substring(dotIdx));
retVal.setParamName(theParamName.substring(0, dotIdx));
retVal.setWholeQualifier(theParamName.substring(dotIdx));
} else if (colonIdx != -1) {
retVal.setColonQualifier(theParamName.substring(colonIdx));
retVal.setParamName(theParamName.substring(0, colonIdx));
retVal.setWholeQualifier(theParamName.substring(colonIdx));
} else {
retVal.setParamName(theParamName);
retVal.setColonQualifier(null);
retVal.setDotQualifier(null);
retVal.setWholeQualifier(null);
}
return retVal;
}
}

View File

@ -42,8 +42,8 @@ import ca.uhn.fhir.util.ReflectionUtil;
public class SearchParameter extends BaseQueryParameter {
private static final String EMPTY_STRING = "";
private static HashMap<RestSearchParameterTypeEnum, Set<String>> ourParamQualifiers;
private static HashMap<Class<?>, RestSearchParameterTypeEnum> ourParamTypes;
private static final HashMap<RestSearchParameterTypeEnum, Set<String>> ourParamQualifiers;
private static final HashMap<Class<?>, RestSearchParameterTypeEnum> ourParamTypes;
static final String QUALIFIER_ANY_TYPE = ":*";
static {
@ -114,6 +114,7 @@ public class SearchParameter extends BaseQueryParameter {
private Set<String> myQualifierWhitelist;
private boolean myRequired;
private Class<?> myType;
private boolean mySupportsRepetition = false;
public SearchParameter() {
}
@ -202,17 +203,17 @@ public class SearchParameter extends BaseQueryParameter {
return myParamBinder.parse(theContext, getName(), theString);
}
public void setChainlists(String[] theChainWhitelist, String[] theChainBlacklist) {
public void setChainLists(String[] theChainWhitelist, String[] theChainBlacklist) {
myQualifierWhitelist = new HashSet<>(theChainWhitelist.length);
myQualifierWhitelist.add(QUALIFIER_ANY_TYPE);
for (int i = 0; i < theChainWhitelist.length; i++) {
if (theChainWhitelist[i].equals(OptionalParam.ALLOW_CHAIN_ANY)) {
for (String nextChain : theChainWhitelist) {
if (nextChain.equals(OptionalParam.ALLOW_CHAIN_ANY)) {
myQualifierWhitelist.add('.' + OptionalParam.ALLOW_CHAIN_ANY);
} else if (theChainWhitelist[i].equals(EMPTY_STRING)) {
} else if (nextChain.equals(EMPTY_STRING)) {
myQualifierWhitelist.add(".");
} else {
myQualifierWhitelist.add('.' + theChainWhitelist[i]);
myQualifierWhitelist.add('.' + nextChain);
}
}
@ -248,42 +249,48 @@ public class SearchParameter extends BaseQueryParameter {
this.myRequired = required;
}
@Override
protected boolean supportsRepetition() {
return mySupportsRepetition;
}
@SuppressWarnings("unchecked")
public void setType(FhirContext theContext, final Class<?> type, Class<? extends Collection<?>> theInnerCollectionType, Class<? extends Collection<?>> theOuterCollectionType) {
public void setType(FhirContext theContext, final Class<?> theType, Class<? extends Collection<?>> theInnerCollectionType, Class<? extends Collection<?>> theOuterCollectionType) {
this.myType = type;
if (IQueryParameterType.class.isAssignableFrom(type)) {
myParamBinder = new QueryParameterTypeBinder((Class<? extends IQueryParameterType>) type, myCompositeTypes);
} else if (IQueryParameterOr.class.isAssignableFrom(type)) {
myParamBinder = new QueryParameterOrBinder((Class<? extends IQueryParameterOr<?>>) type, myCompositeTypes);
} else if (IQueryParameterAnd.class.isAssignableFrom(type)) {
myParamBinder = new QueryParameterAndBinder((Class<? extends IQueryParameterAnd<?>>) type, myCompositeTypes);
} else if (String.class.equals(type)) {
this.myType = theType;
if (IQueryParameterType.class.isAssignableFrom(theType)) {
myParamBinder = new QueryParameterTypeBinder((Class<? extends IQueryParameterType>) theType, myCompositeTypes);
} else if (IQueryParameterOr.class.isAssignableFrom(theType)) {
myParamBinder = new QueryParameterOrBinder((Class<? extends IQueryParameterOr<?>>) theType, myCompositeTypes);
} else if (IQueryParameterAnd.class.isAssignableFrom(theType)) {
myParamBinder = new QueryParameterAndBinder((Class<? extends IQueryParameterAnd<?>>) theType, myCompositeTypes);
mySupportsRepetition = true;
} else if (String.class.equals(theType)) {
myParamBinder = new StringBinder();
myParamType = RestSearchParameterTypeEnum.STRING;
} else if (Date.class.equals(type)) {
} else if (Date.class.equals(theType)) {
myParamBinder = new DateBinder();
myParamType = RestSearchParameterTypeEnum.DATE;
} else if (Calendar.class.equals(type)) {
} else if (Calendar.class.equals(theType)) {
myParamBinder = new CalendarBinder();
myParamType = RestSearchParameterTypeEnum.DATE;
} else if (IPrimitiveType.class.isAssignableFrom(type) && ReflectionUtil.isInstantiable(type)) {
RuntimePrimitiveDatatypeDefinition def = (RuntimePrimitiveDatatypeDefinition) theContext.getElementDefinition((Class<? extends IPrimitiveType<?>>) type);
} else if (IPrimitiveType.class.isAssignableFrom(theType) && ReflectionUtil.isInstantiable(theType)) {
RuntimePrimitiveDatatypeDefinition def = (RuntimePrimitiveDatatypeDefinition) theContext.getElementDefinition((Class<? extends IPrimitiveType<?>>) theType);
if (def.getNativeType() != null) {
if (def.getNativeType().equals(Date.class)) {
myParamBinder = new FhirPrimitiveBinder((Class<IPrimitiveType<?>>) type);
myParamBinder = new FhirPrimitiveBinder((Class<IPrimitiveType<?>>) theType);
myParamType = RestSearchParameterTypeEnum.DATE;
} else if (def.getNativeType().equals(String.class)) {
myParamBinder = new FhirPrimitiveBinder((Class<IPrimitiveType<?>>) type);
myParamBinder = new FhirPrimitiveBinder((Class<IPrimitiveType<?>>) theType);
myParamType = RestSearchParameterTypeEnum.STRING;
}
}
} else {
throw new ConfigurationException("Unsupported data type for parameter: " + type.getCanonicalName());
throw new ConfigurationException("Unsupported data type for parameter: " + theType.getCanonicalName());
}
RestSearchParameterTypeEnum typeEnum = ourParamTypes.get(type);
RestSearchParameterTypeEnum typeEnum = ourParamTypes.get(theType);
if (typeEnum != null) {
Set<String> builtInQualifiers = ourParamQualifiers.get(typeEnum);
if (builtInQualifiers != null) {
@ -304,22 +311,22 @@ public class SearchParameter extends BaseQueryParameter {
if (myParamType != null) {
// ok
} else if (StringDt.class.isAssignableFrom(type)) {
} else if (StringDt.class.isAssignableFrom(theType)) {
myParamType = RestSearchParameterTypeEnum.STRING;
} else if (BaseIdentifierDt.class.isAssignableFrom(type)) {
} else if (BaseIdentifierDt.class.isAssignableFrom(theType)) {
myParamType = RestSearchParameterTypeEnum.TOKEN;
} else if (BaseQuantityDt.class.isAssignableFrom(type)) {
} else if (BaseQuantityDt.class.isAssignableFrom(theType)) {
myParamType = RestSearchParameterTypeEnum.QUANTITY;
} else if (ReferenceParam.class.isAssignableFrom(type)) {
} else if (ReferenceParam.class.isAssignableFrom(theType)) {
myParamType = RestSearchParameterTypeEnum.REFERENCE;
} else if (HasParam.class.isAssignableFrom(type)) {
} else if (HasParam.class.isAssignableFrom(theType)) {
myParamType = RestSearchParameterTypeEnum.STRING;
} else {
throw new ConfigurationException("Unknown search parameter type: " + type);
throw new ConfigurationException("Unknown search parameter theType: " + theType);
}
// NB: Once this is enabled, we should return true from handlesMissing if
// it's a collection type
// it's a collection theType
// if (theInnerCollectionType != null) {
// this.parser = new CollectionBinder(this.parser, theInnerCollectionType);
// }

View File

@ -22,17 +22,13 @@ package ca.uhn.fhir.rest.server.method;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.lang.reflect.Method;
import java.util.IdentityHashMap;
import java.util.List;
import org.hl7.fhir.instance.model.api.IBaseResource;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.model.base.resource.BaseOperationOutcome;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.valueset.BundleTypeEnum;
import ca.uhn.fhir.rest.annotation.Transaction;
import ca.uhn.fhir.rest.annotation.TransactionParam;
@ -92,17 +88,17 @@ public class TransactionMethodBinding extends BaseResourceReturningMethodBinding
}
@Override
public boolean incomingServerRequestMatchesMethod(RequestDetails theRequest) {
public MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequest) {
if (theRequest.getRequestType() != RequestTypeEnum.POST) {
return false;
return MethodMatchEnum.NONE;
}
if (isNotBlank(theRequest.getOperation())) {
return false;
return MethodMatchEnum.NONE;
}
if (isNotBlank(theRequest.getResourceName())) {
return false;
return MethodMatchEnum.NONE;
}
return true;
return MethodMatchEnum.EXACT;
}
@SuppressWarnings("unchecked")

View File

@ -0,0 +1,16 @@
package ca.uhn.fhir.rest.server.method;
import org.junit.Test;
import static org.junit.Assert.*;
public class MethodMatchEnumTest {
@Test
public void testOrder() {
assertEquals(0, MethodMatchEnum.NONE.ordinal());
assertEquals(1, MethodMatchEnum.APPROXIMATE.ordinal());
assertEquals(2, MethodMatchEnum.EXACT.ordinal());
}
}

View File

@ -20,6 +20,7 @@ import org.mockito.junit.MockitoJUnitRunner;
import java.lang.reflect.Method;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
@ -56,17 +57,17 @@ public class ReadMethodBindingTest {
// Read
ReadMethodBinding binding = createBinding(new MyProvider());
when(myRequestDetails.getId()).thenReturn(new IdDt("Patient/123"));
assertTrue(binding.incomingServerRequestMatchesMethod(myRequestDetails));
assertEquals(MethodMatchEnum.EXACT, binding.incomingServerRequestMatchesMethod(myRequestDetails));
// VRead
when(myRequestDetails.getId()).thenReturn(new IdDt("Patient/123/_history/123"));
assertFalse(binding.incomingServerRequestMatchesMethod(myRequestDetails));
assertEquals(MethodMatchEnum.NONE, binding.incomingServerRequestMatchesMethod(myRequestDetails));
// Type history
when(myRequestDetails.getId()).thenReturn(new IdDt("Patient/123"));
when(myRequestDetails.getResourceName()).thenReturn("Patient");
when(myRequestDetails.getOperation()).thenReturn("_history");
assertFalse(binding.incomingServerRequestMatchesMethod(myRequestDetails));
assertEquals(MethodMatchEnum.NONE, binding.incomingServerRequestMatchesMethod(myRequestDetails));
}
@ -89,27 +90,27 @@ public class ReadMethodBindingTest {
ReadMethodBinding binding = createBinding(new MyProvider());
when(myRequestDetails.getResourceName()).thenReturn("Observation");
when(myRequestDetails.getId()).thenReturn(new IdDt("Observation/123"));
assertFalse(binding.incomingServerRequestMatchesMethod(myRequestDetails));
assertEquals(MethodMatchEnum.NONE, binding.incomingServerRequestMatchesMethod(myRequestDetails));
// Read
when(myRequestDetails.getResourceName()).thenReturn("Patient");
when(myRequestDetails.getId()).thenReturn(new IdDt("Patient/123"));
assertTrue(binding.incomingServerRequestMatchesMethod(myRequestDetails));
assertEquals(MethodMatchEnum.EXACT, binding.incomingServerRequestMatchesMethod(myRequestDetails));
// VRead
when(myRequestDetails.getId()).thenReturn(new IdDt("Patient/123/_history/123"));
when(myRequestDetails.getOperation()).thenReturn("_history");
assertTrue(binding.incomingServerRequestMatchesMethod(myRequestDetails));
assertEquals(MethodMatchEnum.EXACT, binding.incomingServerRequestMatchesMethod(myRequestDetails));
// Some other operation
when(myRequestDetails.getId()).thenReturn(new IdDt("Patient/123/_history/123"));
when(myRequestDetails.getOperation()).thenReturn("$foo");
assertFalse(binding.incomingServerRequestMatchesMethod(myRequestDetails));
assertEquals(MethodMatchEnum.NONE, binding.incomingServerRequestMatchesMethod(myRequestDetails));
// History operation
when(myRequestDetails.getId()).thenReturn(new IdDt("Patient/123"));
when(myRequestDetails.getOperation()).thenReturn("_history");
assertFalse(binding.incomingServerRequestMatchesMethod(myRequestDetails));
assertEquals(MethodMatchEnum.NONE, binding.incomingServerRequestMatchesMethod(myRequestDetails));
}
@ -130,7 +131,7 @@ public class ReadMethodBindingTest {
when(myRequestDetails.getId()).thenReturn(new IdDt("Patient/123"));
ReadMethodBinding binding = createBinding(new MyProvider());
assertFalse(binding.incomingServerRequestMatchesMethod(myRequestDetails));
assertEquals(MethodMatchEnum.NONE, binding.incomingServerRequestMatchesMethod(myRequestDetails));
}
public ReadMethodBinding createBinding(Object theProvider) throws NoSuchMethodException {

View File

@ -42,39 +42,39 @@ public class SearchMethodBindingTest {
public void methodShouldNotMatchWhenUnderscoreQueryParameter() throws NoSuchMethodException {
Assert.assertThat(getBinding("param", String.class).incomingServerRequestMatchesMethod(
mockSearchRequest(ImmutableMap.of("param", new String[]{"value"}, "_include", new String[]{"test"}))),
Matchers.is(false));
Matchers.is(MethodMatchEnum.NONE));
Assert.assertThat(getBinding("paramAndTest", String.class, String.class).incomingServerRequestMatchesMethod(
mockSearchRequest(ImmutableMap.of("param", new String[]{"value"}, "_include", new String[]{"test"}))),
Matchers.is(false));
Matchers.is(MethodMatchEnum.NONE));
Assert.assertThat(getBinding("paramAndUnderscoreTest", String.class, String.class).incomingServerRequestMatchesMethod(
mockSearchRequest(ImmutableMap.of("param", new String[]{"value"}, "_include", new String[]{"test"}))),
Matchers.is(false));
Matchers.is(MethodMatchEnum.NONE));
}
@Test
public void methodShouldNotMatchWhenExtraQueryParameter() throws NoSuchMethodException {
Assert.assertThat(getBinding("param", String.class).incomingServerRequestMatchesMethod(
mockSearchRequest(ImmutableMap.of("param", new String[]{"value"}, "extra", new String[]{"test"}))),
Matchers.is(false));
Matchers.is(MethodMatchEnum.NONE));
Assert.assertThat(getBinding("paramAndTest", String.class, String.class).incomingServerRequestMatchesMethod(
mockSearchRequest(ImmutableMap.of("param", new String[]{"value"}, "extra", new String[]{"test"}))),
Matchers.is(false));
Matchers.is(MethodMatchEnum.NONE));
Assert.assertThat(getBinding("paramAndUnderscoreTest", String.class, String.class).incomingServerRequestMatchesMethod(
mockSearchRequest(ImmutableMap.of("param", new String[]{"value"}, "extra", new String[]{"test"}))),
Matchers.is(false));
Matchers.is(MethodMatchEnum.NONE));
}
@Test
public void methodMatchesOwnParams() throws NoSuchMethodException {
Assert.assertThat(getBinding("param", String.class).incomingServerRequestMatchesMethod(
mockSearchRequest(ImmutableMap.of("param", new String[]{"value"}))),
Matchers.is(true));
Matchers.is(MethodMatchEnum.EXACT));
Assert.assertThat(getBinding("paramAndTest", String.class, String.class).incomingServerRequestMatchesMethod(
mockSearchRequest(ImmutableMap.of("param", new String[]{"value"}, "test", new String[]{"test"}))),
Matchers.is(true));
Matchers.is(MethodMatchEnum.EXACT));
Assert.assertThat(getBinding("paramAndUnderscoreTest", String.class, String.class).incomingServerRequestMatchesMethod(
mockSearchRequest(ImmutableMap.of("param", new String[]{"value"}, "_test", new String[]{"test"}))),
Matchers.is(true));
Matchers.is(MethodMatchEnum.EXACT));
}
@Test
@ -83,10 +83,10 @@ public class SearchMethodBindingTest {
ourLog.info("Testing binding: {}", binding);
Assert.assertThat(binding.incomingServerRequestMatchesMethod(
mockSearchRequest(ImmutableMap.of("refChainBlacklist.badChain", new String[]{"foo"}))),
Matchers.is(false));
Matchers.is(MethodMatchEnum.NONE));
Assert.assertThat(binding.incomingServerRequestMatchesMethod(
mockSearchRequest(ImmutableMap.of("refChainBlacklist.goodChain", new String[]{"foo"}))),
Matchers.is(true));
Matchers.is(MethodMatchEnum.EXACT));
}
private SearchMethodBinding getBinding(String name, Class<?>... parameters) throws NoSuchMethodException {

View File

@ -156,53 +156,47 @@ public class ServerSearchDstu2Test {
public static class DummyPatientResourceProvider {
//@formatter:off
@Search(allowUnknownParams=true)
public List<IBaseResource> searchParam1(
@RequiredParam(name = "param1") StringParam theParam) {
ourLastMethod = "searchParam1";
ourLastRef = theParam;
List<IBaseResource> retVal = new ArrayList<IBaseResource>();
List<IBaseResource> retVal = new ArrayList<>();
Patient patient = new Patient();
patient.setId("123");
patient.addName().addGiven("GIVEN");
retVal.add(patient);
return retVal;
}
//@formatter:on
//@formatter:off
@Search(allowUnknownParams=true)
public List<IBaseResource> searchParam2(
@RequiredParam(name = "param2") StringParam theParam) {
ourLastMethod = "searchParam2";
ourLastRef = theParam;
List<IBaseResource> retVal = new ArrayList<IBaseResource>();
List<IBaseResource> retVal = new ArrayList<>();
Patient patient = new Patient();
patient.setId("123");
patient.addName().addGiven("GIVEN");
retVal.add(patient);
return retVal;
}
//@formatter:on
//@formatter:off
@Search(allowUnknownParams=true)
public List<IBaseResource> searchParam3(
@RequiredParam(name = "param3") ReferenceParam theParam) {
ourLastMethod = "searchParam3";
ourLastRef2 = theParam;
List<IBaseResource> retVal = new ArrayList<IBaseResource>();
List<IBaseResource> retVal = new ArrayList<>();
Patient patient = new Patient();
patient.setId("123");
patient.addName().addGiven("GIVEN");
retVal.add(patient);
return retVal;
}
//@formatter:on
}

View File

@ -112,7 +112,6 @@ public class SearchCountParamDstu3Test {
assertEquals("searchWithNoCountParam", ourLastMethod);
assertEquals(null, ourLastParam);
//@formatter:off
assertThat(responseContent, stringContainsInOrder(
"<link>",
"<relation value=\"self\"/>",
@ -122,7 +121,6 @@ public class SearchCountParamDstu3Test {
"<relation value=\"next\"/>",
"<url value=\"http://localhost:" + ourPort + "?_getpages=", "&amp;_getpagesoffset=2&amp;_count=2&amp;_bundletype=searchset\"/>",
"</link>"));
//@formatter:on
} finally {
IOUtils.closeQuietly(status.getEntity().getContent());

View File

@ -0,0 +1,221 @@
package ca.uhn.fhir.rest.server;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.rest.annotation.OptionalParam;
import ca.uhn.fhir.rest.annotation.RequiredParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.param.StringAndListParam;
import ca.uhn.fhir.test.utilities.server.RestfulServerRule;
import com.google.common.collect.Lists;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Patient;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
public class SearchMethodPriorityTest {
@ClassRule
public static RestfulServerRule ourServerRule = new RestfulServerRule(FhirVersionEnum.R4);
private String myLastMethod;
private IGenericClient myClient;
@Before
public void before() {
myLastMethod = null;
myClient = ourServerRule.getFhirClient();
}
@After
public void after() {
ourServerRule.getRestfulServer().unregisterAllProviders();
}
@Test
public void testDateRangeSelectedWhenMultipleParametersProvided() {
ourServerRule.getRestfulServer().registerProviders(new DateStrengthsWithRequiredResourceProvider());
myClient
.search()
.forResource("Patient")
.where(Patient.BIRTHDATE.after().day("2001-01-01"))
.and(Patient.BIRTHDATE.before().day("2002-01-01"))
.returnBundle(Bundle.class)
.execute();
assertEquals("findDateRangeParam", myLastMethod);
}
@Test
public void testDateRangeNotSelectedWhenSingleParameterProvided() {
ourServerRule.getRestfulServer().registerProviders(new DateStrengthsWithRequiredResourceProvider());
myClient
.search()
.forResource("Patient")
.where(Patient.BIRTHDATE.after().day("2001-01-01"))
.returnBundle(Bundle.class)
.execute();
assertEquals("findDateParam", myLastMethod);
}
@Test
public void testEmptyDateSearchProvidedWithNoParameters() {
ourServerRule.getRestfulServer().registerProviders(new DateStrengthsWithRequiredResourceProvider());
myClient
.search()
.forResource("Patient")
.returnBundle(Bundle.class)
.execute();
assertEquals("find", myLastMethod);
}
@Test
public void testStringAndListSelectedWhenMultipleParametersProvided() {
ourServerRule.getRestfulServer().registerProviders(new StringStrengthsWithOptionalResourceProvider());
myClient
.search()
.forResource("Patient")
.where(Patient.NAME.matches().value("hello"))
.and(Patient.NAME.matches().value("goodbye"))
.returnBundle(Bundle.class)
.execute();
assertEquals("findStringAndListParam", myLastMethod);
}
@Test
public void testStringAndListNotSelectedWhenSingleParameterProvided() {
ourServerRule.getRestfulServer().registerProviders(new StringStrengthsWithOptionalResourceProvider());
myClient
.search()
.forResource("Patient")
.where(Patient.NAME.matches().value("hello"))
.returnBundle(Bundle.class)
.execute();
assertEquals("findString", myLastMethod);
}
@Test
public void testEmptyStringSearchProvidedWithNoParameters() {
ourServerRule.getRestfulServer().registerProviders(new StringStrengthsWithOptionalResourceProvider());
myClient
.search()
.forResource("Patient")
.returnBundle(Bundle.class)
.execute();
assertEquals("find", myLastMethod);
}
@Test
public void testEmptyStringSearchProvidedWithNoParameters2() {
ourServerRule.getRestfulServer().registerProviders(new StringStrengthsWithOptionalResourceProviderReverseOrder());
myClient
.search()
.forResource("Patient")
.returnBundle(Bundle.class)
.execute();
assertEquals("find", myLastMethod);
}
public class DateStrengthsWithRequiredResourceProvider implements IResourceProvider {
@Override
public Class<Patient> getResourceType() {
return Patient.class;
}
@Search
public List<Patient> find() {
myLastMethod = "find";
return Lists.newArrayList();
}
@Search()
public List<Patient> findDateParam(
@RequiredParam(name = Patient.SP_BIRTHDATE) DateParam theDate) {
myLastMethod = "findDateParam";
return Lists.newArrayList();
}
@Search()
public List<Patient> findDateRangeParam(
@RequiredParam(name = Patient.SP_BIRTHDATE) DateRangeParam theRange) {
myLastMethod = "findDateRangeParam";
return Lists.newArrayList();
}
}
public class StringStrengthsWithOptionalResourceProvider implements IResourceProvider {
@Override
public Class<Patient> getResourceType() {
return Patient.class;
}
@Search()
public List<Patient> findString(
@OptionalParam(name = Patient.SP_NAME) String theDate) {
myLastMethod = "findString";
return Lists.newArrayList();
}
@Search()
public List<Patient> findStringAndListParam(
@OptionalParam(name = Patient.SP_NAME) StringAndListParam theRange) {
myLastMethod = "findStringAndListParam";
return Lists.newArrayList();
}
@Search
public List<Patient> find() {
myLastMethod = "find";
return Lists.newArrayList();
}
}
public class StringStrengthsWithOptionalResourceProviderReverseOrder implements IResourceProvider {
@Override
public Class<Patient> getResourceType() {
return Patient.class;
}
@Search()
public List<Patient> findA(
@OptionalParam(name = Patient.SP_NAME) String theDate) {
myLastMethod = "findString";
return Lists.newArrayList();
}
@Search
public List<Patient> findB() {
myLastMethod = "find";
return Lists.newArrayList();
}
}
}

View File

@ -21,10 +21,12 @@ package ca.uhn.fhir.test.utilities.server;
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.test.utilities.JettyUtil;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
@ -43,35 +45,66 @@ import java.util.concurrent.TimeUnit;
public class RestfulServerRule implements TestRule {
private static final Logger ourLog = LoggerFactory.getLogger(RestfulServerRule.class);
private final FhirContext myFhirContext;
private final Object[] myProviders;
private FhirContext myFhirContext;
private Object[] myProviders;
private FhirVersionEnum myFhirVersion;
private Server myServer;
private RestfulServer myServlet;
private int myPort;
private CloseableHttpClient myHttpClient;
private IGenericClient myFhirClient;
/**
* Constructor
*/
public RestfulServerRule(FhirContext theFhirContext, Object... theProviders) {
Validate.notNull(theFhirContext);
myFhirContext = theFhirContext;
myProviders = theProviders;
}
/**
* Constructor: If this is used, it will create and tear down a FhirContext which is good for memory
*/
public RestfulServerRule(FhirVersionEnum theFhirVersionEnum) {
Validate.notNull(theFhirVersionEnum);
myFhirVersion = theFhirVersionEnum;
}
@Override
public Statement apply(Statement theBase, Description theDescription) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
createContextIfNeeded();
startServer();
theBase.evaluate();
stopServer();
destroyContextIfWeCreatedIt();
}
};
}
private void createContextIfNeeded() {
if (myFhirVersion != null) {
myFhirContext = new FhirContext(myFhirVersion);
}
}
private void destroyContextIfWeCreatedIt() {
if (myFhirVersion != null) {
myFhirContext = null;
}
}
private void stopServer() throws Exception {
JettyUtil.closeServer(myServer);
myServer = null;
myFhirClient = null;
myHttpClient.close();
myHttpClient = null;
}
private void startServer() throws Exception {
@ -80,7 +113,9 @@ public class RestfulServerRule implements TestRule {
ServletHandler servletHandler = new ServletHandler();
myServlet = new RestfulServer(myFhirContext);
myServlet.setDefaultPrettyPrint(true);
myServlet.registerProviders(myProviders);
if (myProviders != null) {
myServlet.registerProviders(myProviders);
}
ServletHolder servletHolder = new ServletHolder(myServlet);
servletHandler.addServletWithMapping(servletHolder, "/*");