Starting work on #19

This commit is contained in:
jamesagnew 2014-09-02 08:21:06 -04:00
parent bda33737f1
commit 9217ee28bf
8 changed files with 489 additions and 195 deletions

View File

@ -30,13 +30,52 @@ import ca.uhn.fhir.model.dstu.resource.Patient;
import ca.uhn.fhir.rest.param.CompositeParam;
import ca.uhn.fhir.rest.param.ReferenceParam;
@Retention(RetentionPolicy.RUNTIME)
/**
* Parameter annotation which specifies a search parameter for a {@link Search} method.
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface OptionalParam {
public static final String ALLOW_CHAIN_ANY = "*";
/**
* For reference parameters ({@link ReferenceParam}) this value may be
* used to indicate which chain values (if any) are <b>not</b> valid
* for the given parameter. Values here will supercede any values specified
* in {@link #chainWhitelist()}
* <p>
* If the parameter annotated with this annotation is not a {@link ReferenceParam},
* this value must not be populated.
* </p>
*/
String[] chainBlacklist() default {};
/**
* For reference parameters ({@link ReferenceParam}) this value may be
* used to indicate which chain values (if any) are valid for the given
* parameter. If the list contains the value {@link #ALLOW_CHAIN_ANY}, as is
* the default, all values are valid. Any values specified in
* {@link #chainBlacklist()} will supercede (have priority over) values
* here.
* <p>
* If the parameter annotated with this annotation is not a {@link ReferenceParam},
* this value must not be populated.
* </p>
*/
String[] chainWhitelist() default {ALLOW_CHAIN_ANY};
/**
* For composite parameters ({@link CompositeParam}) this value may be
* used to indicate the parameter type(s) which may be referenced by this param.
* <p>
* If the parameter annotated with this annotation is not a {@link CompositeParam},
* this value must not be populated.
* </p>
*/
Class<? extends IQueryParameterType>[] compositeTypes() default {};
/**
* This is the name for the parameter. Generally this should be a
* simple string (e.g. "name", or "identifier") which will be the name
* of the URL parameter used to populate this method parameter.
@ -54,9 +93,9 @@ public @interface OptionalParam {
* </p>
*/
String name();
/**
* For resource reference parameters ({@link ReferenceParam}) this parameter may be
* For resource reference parameters ({@link ReferenceParam}) this value may be
* used to indicate the resource type(s) which may be referenced by this param.
* <p>
* If the parameter annotated with this annotation is not a {@link ReferenceParam},
@ -64,15 +103,4 @@ public @interface OptionalParam {
* </p>
*/
Class<? extends IResource>[] targetTypes() default {};
/**
* For composite parameters ({@link CompositeParam}) this parameter may be
* used to indicate the parameter type(s) which may be referenced by this param.
* <p>
* If the parameter annotated with this annotation is not a {@link CompositeParam},
* this value must not be populated.
* </p>
*/
Class<? extends IQueryParameterType>[] compositeTypes() default {};
}

View File

@ -36,44 +36,61 @@ import ca.uhn.fhir.rest.param.ReferenceParam;
*/
public @interface RequiredParam {
/**
* This is the name for the parameter. Generally this should be a
* simple string (e.g. "name", or "identifier") which will be the name
* of the URL parameter used to populate this method parameter.
* For reference parameters ({@link ReferenceParam}) this value may be used to indicate which chain values (if any)
* are <b>not</b> valid for the given parameter. Values here will supercede any values specified in
* {@link #chainWhitelist()}
* <p>
* Most resource model classes have constants which may be used to
* supply values for this field, e.g. {@link Patient#SP_NAME} or
* {@link Observation#SP_DATE}
* </p>
* If the parameter annotated with this annotation is not a {@link ReferenceParam}, this value must not be
* populated.
* </p>
*/
String[] chainBlacklist() default {};
/**
* For reference parameters ({@link ReferenceParam}) this value may be used to indicate which chain values (if any)
* are valid for the given parameter. If the list contains the value {@link OptionalParam#ALLOW_CHAIN_ANY}, as is
* the default, all values are valid. Any values specified in {@link #chainBlacklist()} will supercede (have
* priority over) values here.
* <p>
* If you wish to specify a parameter for a resource reference which
* only accepts a specific chained value, it is also valid to supply
* a chained name here, such as "patient.name". It is recommended to
* supply this using constants where possible, e.g.
* <code>{@link Observation#SP_SUBJECT} + '.' + {@link Patient#SP_IDENTIFIER}</code>
* If the parameter annotated with this annotation is not a {@link ReferenceParam}, this value must not be
* populated.
* </p>
*/
String[] chainWhitelist() default { OptionalParam.ALLOW_CHAIN_ANY };
/**
* For composite parameters ({@link CompositeParam}) this parameter may be used to indicate the parameter type(s)
* which may be referenced by this param.
* <p>
* If the parameter annotated with this annotation is not a {@link CompositeParam}, this value must not be
* populated.
* </p>
*/
Class<? extends IQueryParameterType>[] compositeTypes() default {};
/**
* This is the name for the parameter. Generally this should be a simple string (e.g. "name", or "identifier") which
* will be the name of the URL parameter used to populate this method parameter.
* <p>
* Most resource model classes have constants which may be used to supply values for this field, e.g.
* {@link Patient#SP_NAME} or {@link Observation#SP_DATE}
* </p>
* <p>
* If you wish to specify a parameter for a resource reference which only accepts a specific chained value, it is
* also valid to supply a chained name here, such as "patient.name". It is recommended to supply this using
* constants where possible, e.g. <code>{@link Observation#SP_SUBJECT} + '.' + {@link Patient#SP_IDENTIFIER}</code>
* </p>
*/
String name();
/**
* For resource reference parameters ({@link ReferenceParam}) this parameter may be
* used to indicate the resource type(s) which may be referenced by this param.
* <p>
* If the parameter annotated with this annotation is not a {@link ReferenceParam},
* this value must not be populated.
* </p>
*/
Class<? extends IResource>[] targetTypes() default {};
/**
* For composite parameters ({@link CompositeParam}) this parameter may be
* used to indicate the parameter type(s) which may be referenced by this param.
* <p>
* If the parameter annotated with this annotation is not a {@link CompositeParam},
* this value must not be populated.
* </p>
*/
Class<? extends IQueryParameterType>[] compositeTypes() default {};
/**
* For resource reference parameters ({@link ReferenceParam}) this parameter may be used to indicate the resource
* type(s) which may be referenced by this param.
* <p>
* If the parameter annotated with this annotation is not a {@link ReferenceParam}, this value must not be
* populated.
* </p>
*/
Class<? extends IResource>[] targetTypes() default {};
}

View File

@ -95,11 +95,14 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
myParameters = MethodUtil.getResourceParameters(theMethod);
}
public List<Class<?>> getAllowableParamAnnotations() {
return null;
}
public FhirContext getContext() {
return myContext;
}
public Set<String> getIncludes() {
Set<String> retVal = new TreeSet<String>();
for (IParameter next : myParameters) {
@ -110,14 +113,14 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
return retVal;
}
public FhirContext getContext() {
return myContext;
}
public Method getMethod() {
return myMethod;
}
public OtherOperationTypeEnum getOtherOperationType() {
return null;
}
public List<IParameter> getParameters() {
return myParameters;
}
@ -127,7 +130,8 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
}
/**
* Returns the name of the resource this method handles, or <code>null</code> if this method is not resource specific
* Returns the name of the resource this method handles, or <code>null</code> if this method is not resource
* specific
*/
public abstract String getResourceName();
@ -135,10 +139,6 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
public abstract RestfulOperationSystemEnum getSystemOperationType();
public OtherOperationTypeEnum getOtherOperationType() {
return null;
}
public abstract boolean incomingServerRequestMatchesMethod(Request theRequest);
public abstract BaseHttpClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException;
@ -267,16 +267,14 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
if (theProvider instanceof IResourceProvider) {
returnTypeFromRp = ((IResourceProvider) theProvider).getResourceType();
if (!verifyIsValidResourceReturnType(returnTypeFromRp)) {
throw new ConfigurationException("getResourceType() from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName() + " returned "
+ toLogString(returnTypeFromRp) + " - Must return a resource type");
throw new ConfigurationException("getResourceType() from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName() + " returned " + toLogString(returnTypeFromRp) + " - Must return a resource type");
}
}
Class<?> returnTypeFromMethod = theMethod.getReturnType();
if (getTags != null) {
if (!TagList.class.equals(returnTypeFromMethod)) {
throw new ConfigurationException("Method '" + theMethod.getName() + "' from type " + theMethod.getDeclaringClass().getCanonicalName() + " is annotated with @"
+ GetTags.class.getSimpleName() + " but does not return type " + TagList.class.getName());
throw new ConfigurationException("Method '" + theMethod.getName() + "' from type " + theMethod.getDeclaringClass().getCanonicalName() + " is annotated with @" + GetTags.class.getSimpleName() + " but does not return type " + TagList.class.getName());
}
} else if (MethodOutcome.class.equals(returnTypeFromMethod)) {
// returns a method outcome
@ -289,15 +287,13 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
} else if (Collection.class.isAssignableFrom(returnTypeFromMethod)) {
returnTypeFromMethod = ReflectionUtil.getGenericCollectionTypeOfMethodReturnType(theMethod);
if (!verifyIsValidResourceReturnType(returnTypeFromMethod) && !IResource.class.equals(returnTypeFromMethod)) {
throw new ConfigurationException("Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName()
+ " returns a collection with generic type " + toLogString(returnTypeFromMethod)
throw new ConfigurationException("Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName() + " returns a collection with generic type " + toLogString(returnTypeFromMethod)
+ " - Must return a resource type or a collection (List, Set) with a resource type parameter (e.g. List<Patient> or List<IResource> )");
}
} else {
if (!IResource.class.equals(returnTypeFromMethod) && !verifyIsValidResourceReturnType(returnTypeFromMethod)) {
throw new ConfigurationException("Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName()
+ " returns " + toLogString(returnTypeFromMethod) + " - Must return a resource type (eg Patient, " + Bundle.class.getSimpleName() + ", "
+ IBundleProvider.class.getSimpleName() + ", etc., see the documentation for more details)");
throw new ConfigurationException("Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName() + " returns " + toLogString(returnTypeFromMethod)
+ " - Must return a resource type (eg Patient, " + Bundle.class.getSimpleName() + ", " + IBundleProvider.class.getSimpleName() + ", etc., see the documentation for more details)");
}
}
@ -327,13 +323,12 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
if (returnTypeFromRp != null) {
if (returnTypeFromAnnotation != null && returnTypeFromAnnotation != IResource.class) {
if (!returnTypeFromRp.isAssignableFrom(returnTypeFromAnnotation)) {
throw new ConfigurationException("Method '" + theMethod.getName() + "' in type " + theMethod.getDeclaringClass().getCanonicalName() + " returns type "
+ returnTypeFromMethod.getCanonicalName() + " - Must return " + returnTypeFromRp.getCanonicalName() + " (or a subclass of it) per IResourceProvider contract");
throw new ConfigurationException("Method '" + theMethod.getName() + "' in type " + theMethod.getDeclaringClass().getCanonicalName() + " returns type " + returnTypeFromMethod.getCanonicalName() + " - Must return " + returnTypeFromRp.getCanonicalName()
+ " (or a subclass of it) per IResourceProvider contract");
}
if (!returnTypeFromRp.isAssignableFrom(returnTypeFromAnnotation)) {
throw new ConfigurationException("Method '" + theMethod.getName() + "' in type " + theMethod.getDeclaringClass().getCanonicalName() + " claims to return type "
+ returnTypeFromAnnotation.getCanonicalName() + " per method annotation - Must return " + returnTypeFromRp.getCanonicalName()
+ " (or a subclass of it) per IResourceProvider contract");
throw new ConfigurationException("Method '" + theMethod.getName() + "' in type " + theMethod.getDeclaringClass().getCanonicalName() + " claims to return type " + returnTypeFromAnnotation.getCanonicalName() + " per method annotation - Must return "
+ returnTypeFromRp.getCanonicalName() + " (or a subclass of it) per IResourceProvider contract");
}
returnType = returnTypeFromAnnotation;
} else {
@ -342,8 +337,8 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
} else {
if (returnTypeFromAnnotation != IResource.class) {
if (!verifyIsValidResourceReturnType(returnTypeFromAnnotation)) {
throw new ConfigurationException("Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type "
+ theMethod.getDeclaringClass().getCanonicalName() + " returns " + toLogString(returnTypeFromAnnotation) + " according to annotation - Must return a resource type");
throw new ConfigurationException("Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName() + " returns " + toLogString(returnTypeFromAnnotation)
+ " according to annotation - Must return a resource type");
}
returnType = returnTypeFromAnnotation;
} else {
@ -354,7 +349,6 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
if (read != null) {
return new ReadMethodBinding(returnType, theMethod, theContext, theProvider);
} else if (search != null) {
String queryName = search.queryName();
return new SearchMethodBinding(returnType, theMethod, theContext, theProvider);
} else if (conformance != null) {
return new ConformanceMethodBinding(theMethod, theContext, theProvider);
@ -409,8 +403,8 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
if (obj1 == null) {
obj1 = object;
} else {
throw new ConfigurationException("Method " + theNextMethod.getName() + " on type '" + theNextMethod.getDeclaringClass().getSimpleName() + " has annotations @"
+ obj1.getClass().getSimpleName() + " and @" + object.getClass().getSimpleName() + ". Can not have both.");
throw new ConfigurationException("Method " + theNextMethod.getName() + " on type '" + theNextMethod.getDeclaringClass().getSimpleName() + " has annotations @" + obj1.getClass().getSimpleName() + " and @" + object.getClass().getSimpleName()
+ ". Can not have both.");
}
}

View File

@ -18,10 +18,13 @@ import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.swing.text.html.Option;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.utils.DateUtils;
import com.google.common.base.Optional;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
@ -420,6 +423,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.setType(parameterType, innerCollectionType, outerCollectionType);
MethodUtil.extractDescription(parameter, annotations);
param = parameter;
@ -429,6 +433,7 @@ 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.setType(parameterType, innerCollectionType, outerCollectionType);
MethodUtil.extractDescription(parameter, annotations);
param = parameter;

View File

@ -23,6 +23,7 @@ package ca.uhn.fhir.rest.method;
import static org.apache.commons.lang3.StringUtils.*;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
@ -54,13 +55,13 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchMethodBinding.class);
private Class<? extends IResource> myDeclaredResourceType;
private String myQueryName;
private String myDescription;
private String myCompartmentName;
private Class<? extends IResource> myDeclaredResourceType;
private String myDescription;
private Integer myIdParamIndex;
private String myQueryName;
@SuppressWarnings("unchecked")
public SearchMethodBinding(Class<? extends IResource> theReturnResourceType, Method theMethod, FhirContext theContext, Object theProvider) {
super(theReturnResourceType, theMethod, theContext, theProvider);
@ -79,7 +80,13 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
}
}
for (IParameter next : getParameters()) {
/*
* Check for parameter combinations and names that are invalid
*/
List<IParameter> parameters = getParameters();
List<SearchParameter> searchParameters = new ArrayList<SearchParameter>();
for (int i =0; i < parameters.size(); i++) {
IParameter next = parameters.get(i);
if (!(next instanceof SearchParameter)) {
continue;
}
@ -91,8 +98,17 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
throw new ConfigurationException(msg);
}
}
searchParameters.add(sp);
}
for (int i = 0; i < searchParameters.size(); i++) {
SearchParameter next = searchParameters.get(i);
// next.
}
/*
* Only compartment searching methods may have an ID parameter
*/
if (isBlank(myCompartmentName) && myIdParamIndex != null) {
String msg = theContext.getLocalizer().getMessage(getClass().getName() + ".idWithoutCompartment", theMethod.getName(), theMethod.getDeclaringClass());
throw new ConfigurationException(msg);
@ -100,14 +116,14 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
}
public String getDescription() {
return myDescription;
}
public Class<? extends IResource> getDeclaredResourceType() {
return myDeclaredResourceType;
}
public String getDescription() {
return myDescription;
}
@Override
public RestfulOperationTypeEnum getResourceOperationType() {
return RestfulOperationTypeEnum.SEARCH_TYPE;
@ -123,100 +139,6 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
return null;
}
@Override
public BaseHttpClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException {
assert (myQueryName == null || ((theArgs != null ? theArgs.length : 0) == getParameters().size())) : "Wrong number of arguments: " + (theArgs != null ? theArgs.length : "null");
Map<String, List<String>> queryStringArgs = new LinkedHashMap<String, List<String>>();
if (myQueryName != null) {
queryStringArgs.put(Constants.PARAM_QUERY, Collections.singletonList(myQueryName));
}
IdDt id = (IdDt) (myIdParamIndex != null ? theArgs[myIdParamIndex] : null);
String resourceName = getResourceName();
BaseHttpClientInvocation retVal = createSearchInvocation(getContext(), resourceName, queryStringArgs, id, myCompartmentName, null);
if (theArgs != null) {
for (int idx = 0; idx < theArgs.length; idx++) {
IParameter nextParam = getParameters().get(idx);
nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], queryStringArgs, retVal);
}
}
return retVal;
}
public static BaseHttpClientInvocation createSearchInvocation(FhirContext theContext, String theResourceName, Map<String, List<String>> theParameters, IdDt theId, String theCompartmentName, SearchStyleEnum theSearchStyle) {
SearchStyleEnum searchStyle = theSearchStyle;
if (searchStyle == null) {
int length = 0;
for (Entry<String, List<String>> nextEntry : theParameters.entrySet()) {
length += nextEntry.getKey().length();
for (String next : nextEntry.getValue()) {
length += next.length();
}
}
if (length < 5000) {
searchStyle = SearchStyleEnum.GET;
} else {
searchStyle = SearchStyleEnum.POST;
}
}
BaseHttpClientInvocation invocation;
boolean compartmentSearch = false;
if (theCompartmentName != null) {
if (theId == null || !theId.hasIdPart()) {
String msg = theContext.getLocalizer().getMessage(SearchMethodBinding.class.getName() + ".idNullForCompartmentSearch");
throw new InvalidRequestException(msg);
} else {
compartmentSearch = true;
}
}
switch (searchStyle) {
case GET:
default:
if (compartmentSearch) {
invocation = new HttpGetClientInvocation(theParameters, theResourceName, theId.getIdPart(), theCompartmentName);
} else {
invocation = new HttpGetClientInvocation(theParameters, theResourceName);
}
break;
case GET_WITH_SEARCH:
if (compartmentSearch) {
invocation = new HttpGetClientInvocation(theParameters, theResourceName, theId.getIdPart(), theCompartmentName, Constants.PARAM_SEARCH);
} else {
invocation = new HttpGetClientInvocation(theParameters, theResourceName, Constants.PARAM_SEARCH);
}
break;
case POST:
if (compartmentSearch) {
invocation = new HttpPostClientInvocation(theContext, theParameters, theResourceName, theId.getIdPart(), theCompartmentName, Constants.PARAM_SEARCH);
} else {
invocation = new HttpPostClientInvocation(theContext, theParameters, theResourceName, Constants.PARAM_SEARCH);
}
}
return invocation;
}
@Override
public IBundleProvider invokeServer(RequestDetails theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException {
if (myIdParamIndex != null) {
theMethodParams[myIdParamIndex] = theRequest.getId();
}
Object response = invokeServerMethod(theMethodParams);
return toResourceList(response);
}
@Override
public boolean incomingServerRequestMatchesMethod(Request theRequest) {
if (!theRequest.getResourceName().equals(getResourceName())) {
@ -260,7 +182,9 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
if (qualifiedParamNames.contains(name)) {
methodParamsTemp.add(name);
} else if (unqualifiedNames.contains(name)) {
methodParamsTemp.addAll(theRequest.getUnqualifiedToQualifiedNames().get(name));
List<String> qualifiedNames = theRequest.getUnqualifiedToQualifiedNames().get(name);
qualifiedNames = processWhitelistAndBlacklist(qualifiedNames, temp.getQualifierWhitelist(), temp.getQualifierBlacklist());
methodParamsTemp.addAll(qualifiedNames);
} else {
ourLog.trace("Method {} doesn't match param '{}' is not present", getMethod().getName(), name);
return false;
@ -270,7 +194,9 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
if (qualifiedParamNames.contains(name)) {
methodParamsTemp.add(name);
} else if (unqualifiedNames.contains(name)) {
methodParamsTemp.addAll(theRequest.getUnqualifiedToQualifiedNames().get(name));
List<String> qualifiedNames = theRequest.getUnqualifiedToQualifiedNames().get(name);
qualifiedNames = processWhitelistAndBlacklist(qualifiedNames, temp.getQualifierWhitelist(), temp.getQualifierBlacklist());
methodParamsTemp.addAll(qualifiedNames);
} else {
methodParamsTemp.add(name);
}
@ -317,10 +243,135 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
return true;
}
@Override
public BaseHttpClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException {
assert (myQueryName == null || ((theArgs != null ? theArgs.length : 0) == getParameters().size())) : "Wrong number of arguments: " + (theArgs != null ? theArgs.length : "null");
Map<String, List<String>> queryStringArgs = new LinkedHashMap<String, List<String>>();
if (myQueryName != null) {
queryStringArgs.put(Constants.PARAM_QUERY, Collections.singletonList(myQueryName));
}
IdDt id = (IdDt) (myIdParamIndex != null ? theArgs[myIdParamIndex] : null);
String resourceName = getResourceName();
BaseHttpClientInvocation retVal = createSearchInvocation(getContext(), resourceName, queryStringArgs, id, myCompartmentName, null);
if (theArgs != null) {
for (int idx = 0; idx < theArgs.length; idx++) {
IParameter nextParam = getParameters().get(idx);
nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], queryStringArgs, retVal);
}
}
return retVal;
}
@Override
public IBundleProvider invokeServer(RequestDetails theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException {
if (myIdParamIndex != null) {
theMethodParams[myIdParamIndex] = theRequest.getId();
}
Object response = invokeServerMethod(theMethodParams);
return toResourceList(response);
}
public void setResourceType(Class<? extends IResource> resourceType) {
this.myDeclaredResourceType = resourceType;
}
private List<String> processWhitelistAndBlacklist(List<String> theQualifiedNames, Set<String> theQualifierWhitelist, Set<String> theQualifierBlacklist) {
if (theQualifierWhitelist == null && theQualifierBlacklist == null) {
return theQualifiedNames;
}
ArrayList<String> retVal = new ArrayList<String>(theQualifiedNames.size());
for (String next : theQualifiedNames) {
String qualifier = "";
int idx = next.indexOf('.');
if (idx > -1 && next.length() > (idx + 1)) {
qualifier = next.substring(idx);
}
if (theQualifierWhitelist != null) {
if (!theQualifierWhitelist.contains(qualifier)) {
continue;
}
}
if (theQualifierBlacklist != null) {
if (theQualifierBlacklist.contains(qualifier)) {
continue;
}
}
retVal.add(next);
}
return retVal;
}
public static BaseHttpClientInvocation createSearchInvocation(FhirContext theContext, String theResourceName, Map<String, List<String>> theParameters, IdDt theId, String theCompartmentName, SearchStyleEnum theSearchStyle) {
SearchStyleEnum searchStyle = theSearchStyle;
if (searchStyle == null) {
int length = 0;
for (Entry<String, List<String>> nextEntry : theParameters.entrySet()) {
length += nextEntry.getKey().length();
for (String next : nextEntry.getValue()) {
length += next.length();
}
}
if (length < 5000) {
searchStyle = SearchStyleEnum.GET;
} else {
searchStyle = SearchStyleEnum.POST;
}
}
BaseHttpClientInvocation invocation;
boolean compartmentSearch = false;
if (theCompartmentName != null) {
if (theId == null || !theId.hasIdPart()) {
String msg = theContext.getLocalizer().getMessage(SearchMethodBinding.class.getName() + ".idNullForCompartmentSearch");
throw new InvalidRequestException(msg);
} else {
compartmentSearch = true;
}
}
/*
* Are we doing a get (GET [base]/Patient?name=foo) or a get with search (GET [base]/Patient/_search?name=foo)
* or a post (POST [base]/Patient with parameters in the POST body)
*/
switch (searchStyle) {
case GET:
default:
if (compartmentSearch) {
invocation = new HttpGetClientInvocation(theParameters, theResourceName, theId.getIdPart(), theCompartmentName);
} else {
invocation = new HttpGetClientInvocation(theParameters, theResourceName);
}
break;
case GET_WITH_SEARCH:
if (compartmentSearch) {
invocation = new HttpGetClientInvocation(theParameters, theResourceName, theId.getIdPart(), theCompartmentName, Constants.PARAM_SEARCH);
} else {
invocation = new HttpGetClientInvocation(theParameters, theResourceName, Constants.PARAM_SEARCH);
}
break;
case POST:
if (compartmentSearch) {
invocation = new HttpPostClientInvocation(theContext, theParameters, theResourceName, theId.getIdPart(), theCompartmentName, Constants.PARAM_SEARCH);
} else {
invocation = new HttpPostClientInvocation(theContext, theParameters, theResourceName, Constants.PARAM_SEARCH);
}
}
return invocation;
}
public static enum RequestType {
DELETE, GET, OPTIONS, POST, PUT
}

View File

@ -25,7 +25,9 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.commons.lang3.builder.ToStringBuilder;
@ -39,6 +41,7 @@ import ca.uhn.fhir.model.dstu.composite.IdentifierDt;
import ca.uhn.fhir.model.dstu.composite.QuantityDt;
import ca.uhn.fhir.model.dstu.valueset.SearchParamTypeEnum;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.rest.annotation.OptionalParam;
import ca.uhn.fhir.rest.param.BaseQueryParameter;
import ca.uhn.fhir.rest.param.CodingListParam;
import ca.uhn.fhir.rest.param.CompositeAndListParam;
@ -107,6 +110,8 @@ public class SearchParameter extends BaseQueryParameter {
ourParamTypes.put(CompositeOrListParam.class, SearchParamTypeEnum.COMPOSITE);
ourParamTypes.put(CompositeAndListParam.class, SearchParamTypeEnum.COMPOSITE);
}
private Set<String> myQualifierBlacklist;
private Set<String> myQualifierWhitelist;
private List<Class<? extends IQueryParameterType>> myCompositeTypes;
private List<Class<? extends IResource>> myDeclaredTypes;
private String myDescription;
@ -114,7 +119,6 @@ public class SearchParameter extends BaseQueryParameter {
private IParamBinder myParamBinder;
private SearchParamTypeEnum myParamType;
private boolean myRequired;
private Class<?> myType;
public SearchParameter() {
@ -142,6 +146,16 @@ public class SearchParameter extends BaseQueryParameter {
return retVal;
}
@Override
public Set<String> getQualifierBlacklist() {
return myQualifierBlacklist;
}
@Override
public Set<String> getQualifierWhitelist() {
return myQualifierWhitelist;
}
public List<Class<? extends IResource>> getDeclaredTypes() {
return Collections.unmodifiableList(myDeclaredTypes);
}
@ -189,6 +203,31 @@ public class SearchParameter extends BaseQueryParameter {
return myParamBinder.parse(getName(), theString);
}
public void setChainlists(String[] theChainWhitelist, String[] theChainBlacklist) {
myQualifierWhitelist = new HashSet<String>(theChainWhitelist.length);
for (int i = 0; i < theChainWhitelist.length; i++) {
if (theChainWhitelist[i].equals(OptionalParam.ALLOW_CHAIN_ANY)) {
myQualifierWhitelist = null;
break;
} else if (theChainWhitelist[i].equals("")) {
myQualifierWhitelist.add("");
} else {
myQualifierWhitelist.add('.' + theChainWhitelist[i]);
}
}
if (theChainBlacklist.length > 0) {
myQualifierBlacklist = new HashSet<String>(theChainBlacklist.length);
for (String next : theChainBlacklist) {
if (next.equals("")) {
myQualifierBlacklist.add("");
} else {
myQualifierBlacklist.add('.' + next);
}
}
}
}
public void setCompositeTypes(Class<? extends IQueryParameterType>[] theCompositeTypes) {
myCompositeTypes = Arrays.asList(theCompositeTypes);
}
@ -215,7 +254,7 @@ public class SearchParameter extends BaseQueryParameter {
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);
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)) {

View File

@ -20,11 +20,14 @@ package ca.uhn.fhir.rest.param;
* #L%
*/
import static org.apache.commons.lang3.StringUtils.*;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
@ -113,6 +116,17 @@ public abstract class BaseQueryParameter implements IParameter {
}
private void parseParams(RequestDetails theRequest, List<QualifiedParamList> paramList, String theQualifiedParamName, String theQualifier) {
if (getQualifierWhitelist() != null) {
if (!getQualifierWhitelist().contains(defaultString(theQualifier)) ){
return;
}
}
if (getQualifierBlacklist() != null) {
if (getQualifierBlacklist().contains(defaultString(theQualifier))) {
return;
}
}
String[] value = theRequest.getParameters().get(theQualifiedParamName);
if (value != null) {
for (String nextParam : value) {
@ -129,5 +143,19 @@ public abstract class BaseQueryParameter implements IParameter {
public void initializeTypes(Method theMethod, Class<? extends Collection<?>> theOuterCollectionType, Class<? extends Collection<?>> theInnerCollectionType, Class<?> theParameterType) {
// ignore for now
}
/**
* Returns null if blacklist is "none"
*/
public Set<String> getQualifierBlacklist() {
return null;
}
/**
* Returns null if whitelist is "all"
*/
public Set<String> getQualifierWhitelist() {
return null;
}
}

View File

@ -1,9 +1,11 @@
package ca.uhn.fhir.rest.server;
import static org.apache.commons.lang3.StringUtils.*;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
@ -32,6 +34,7 @@ import ca.uhn.fhir.model.dstu.valueset.ResourceTypeEnum;
import ca.uhn.fhir.rest.annotation.OptionalParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.testutil.RandomServerPortProvider;
/**
@ -99,6 +102,68 @@ public class ReferenceParameterTest {
}
}
@Test
public void testSearchWithMultipleParamsOfTheSameName1() throws Exception {
{
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Organization?partof=po123");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
assertThat(responseContent, containsString("value=\"thePartOfId po123 null\""));
}
}
@Test
public void testSearchWithMultipleParamsOfTheSameName2() throws Exception {
{
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Organization?partof.name=poname");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
assertThat(responseContent, containsString("value=\"thePartOfName poname\""));
}
}
@Test
public void testSearchWithMultipleParamsOfTheSameName3() throws Exception {
{
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Organization?partof=po123&partof.name=poname");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
assertThat(responseContent, containsString("value=\"thePartOfId po123 null\""));
assertThat(responseContent, containsString("value=\"thePartOfName poname\""));
}
}
@Test
public void testSearchWithMultipleParamsOfTheSameName4() throws Exception {
{
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Organization?partof.fooChain=po123&partof.name=poname");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
assertThat(responseContent, containsString("value=\"thePartOfId po123 fooChain\""));
assertThat(responseContent, containsString("value=\"thePartOfName poname\""));
}
}
@Test
public void testSearchWithMultipleParamsOfTheSameName5() throws Exception {
{
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Organization?partof.bar=po123");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
assertThat(responseContent, containsString("value=\"theBarId po123 bar\""));
}
}
@Test
public void testSearchWithValueAndChain() throws Exception {
{
@ -116,7 +181,9 @@ public class ReferenceParameterTest {
assertEquals("2name", p.getName().get(2).getFamilyFirstRep().getValue());
}
}
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ReferenceParameterTest.class);
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ReferenceParameterTest.class);
@Test
public void testParamTypesInConformanceStatement() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/metadata?_pretty=true");
@ -125,20 +192,20 @@ private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger
IOUtils.closeQuietly(status.getEntity().getContent());
ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
Conformance conf = ourCtx.newXmlParser().parseResource(Conformance.class,responseContent);
RestResource res = conf.getRestFirstRep().getResourceFirstRep();
Conformance conf = ourCtx.newXmlParser().parseResource(Conformance.class, responseContent);
RestResource res = conf.getRestFirstRep().getResource().get(1);
assertEquals("Patient", res.getType().getValue());
RestResourceSearchParam param = res.getSearchParamFirstRep();
assertEquals(Patient.SP_PROVIDER, param.getName().getValue());
assertEquals(1, param.getTarget().size());
assertEquals(ResourceTypeEnum.ORGANIZATION, param.getTarget().get(0).getValueAsEnum());
}
@AfterClass
public static void afterClass() throws Exception {
ourServer.stop();
@ -154,7 +221,7 @@ private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger
ServletHandler proxyHandler = new ServletHandler();
RestfulServer servlet = new RestfulServer();
ourCtx = servlet.getFhirContext();
servlet.setResourceProviders(patientProvider);
servlet.setResourceProviders(patientProvider, new DummyOrganizationResourceProvider());
ServletHolder servletHolder = new ServletHolder(servlet);
proxyHandler.addServletWithMapping(servletHolder, "/*");
ourServer.setHandler(proxyHandler);
@ -168,13 +235,78 @@ private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger
ourCtx = servlet.getFhirContext();
}
public static class DummyOrganizationResourceProvider implements IResourceProvider {
@Override
public Class<? extends IResource> getResourceType() {
return Organization.class;
}
/**
* https://github.com/jamesagnew/hapi-fhir/issues/18
*/
//@formatter:off
@Search
public List<Organization> searchByName(
@OptionalParam(name = "partof", chainWhitelist= {"", "fooChain"}) ReferenceParam thePartOfId,
@OptionalParam(name = "partof.name") StringParam thePartOfName) {
//@formatter:on
ArrayList<Organization> retVal = new ArrayList<Organization>();
if (thePartOfId != null) {
Organization org = new Organization();
org.setId("1");
org.getName().setValue("thePartOfId " + thePartOfId.getValue() + " " + thePartOfId.getChain());
retVal.add(org);
}
if (thePartOfName != null) {
Organization org = new Organization();
org.setId("2");
org.getName().setValue("thePartOfName " + thePartOfName.getValue());
retVal.add(org);
}
if (retVal.isEmpty()) {
Organization org = new Organization();
org.setId("0");
org.getName().setValue("none");
retVal.add(org);
}
return retVal;
}
//@formatter:off
@Search
public List<Organization> searchByNameWithDifferentChain(
@OptionalParam(name = "partof", chainWhitelist= {"bar"}) ReferenceParam theBarId) {
//@formatter:on
ArrayList<Organization> retVal = new ArrayList<Organization>();
if (theBarId != null) {
Organization org = new Organization();
org.setId("1");
org.getName().setValue("theBarId " + theBarId.getValue() + " " + theBarId.getChain());
retVal.add(org);
}
if (retVal.isEmpty()) {
Organization org = new Organization();
org.setId("0");
org.getName().setValue("none");
retVal.add(org);
}
return retVal;
}
}
/**
* Created by dsotnikov on 2/25/2014.
*/
public static class DummyPatientResourceProvider implements IResourceProvider {
@Search
public List<Patient> findPatient(@OptionalParam(name = Patient.SP_PROVIDER, targetTypes= {Organization.class}) ReferenceParam theParam) {
public List<Patient> findPatient(@OptionalParam(name = Patient.SP_PROVIDER, targetTypes = { Organization.class }) ReferenceParam theParam) {
ArrayList<Patient> retVal = new ArrayList<Patient>();
Patient p = new Patient();