Fix #19 - Now have a whitelist and a blacklist for parameter names

This commit is contained in:
James Agnew 2014-09-02 16:41:01 -04:00
parent 9217ee28bf
commit c704aa185d
11 changed files with 284 additions and 126 deletions

View File

@ -70,6 +70,7 @@ import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.param.TokenOrListParam; import ca.uhn.fhir.rest.param.TokenOrListParam;
import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
@ -156,36 +157,82 @@ public List<Patient> findPatients(
} }
//END SNIPPET: underlyingReq //END SNIPPET: underlyingReq
//START SNIPPET: reference //START SNIPPET: referenceSimple
@Search @Search
public List<Patient> findPatients( public List<Patient> findPatientsWithSimpleReference(
@RequiredParam(name=Patient.SP_PROVIDER) ReferenceParam theProvider @OptionalParam(name=Patient.SP_PROVIDER) ReferenceParam theProvider
) { ) {
List<Patient> retVal=new ArrayList<Patient>();
// May be populated with the resource type (e.g. "Patient") if the client requested one // If the parameter passed in includes a resource type (e.g. ?provider:Patient=123)
String type = theProvider.getResourceType(); // that resoruce type is available. Here we just check that it is either not provided
// or set to "Patient"
if (theProvider.hasResourceType()) {
String resourceType = theProvider.getResourceType();
if ("Patient".equals(resourceType) == false) {
throw new InvalidRequestException("Invalid resource type for parameter 'provider': " + resourceType);
}
}
// May be populated with the chain (e.g. "name") if the client requested one if (theProvider != null) {
String chain = theProvider.getChain(); // ReferenceParam extends IdDt so all of the resource ID methods are available
String providerId = theProvider.getIdPart();
/* The actual parameter value. This will be the resource ID if no chain was provided, // .. populate retVal will Patient resources having provider with id "providerId" ..
* but refers to the value of a specific property noted by the chain if one was given.
* For example, the following request: }
* http://example.com/fhir/Patient?provider:Organization.name=FooOrg
* has a type of "Organization" and a chain of "name", meaning that
* the returned patients should have a provider which is an Organization
* with a name matching "FooOrg".
*/
String value = theProvider.getValue();
List<Patient> retVal=new ArrayList<Patient>(); // populate this
return retVal; return retVal;
} }
//END SNIPPET: reference //END SNIPPET: referenceSimple
//START SNIPPET: referenceChain //START SNIPPET: referenceWithChain
@Search
public List<DiagnosticReport> findReportsWithChain(
@RequiredParam(name=DiagnosticReport.SP_SUBJECT, chainWhitelist= {Patient.SP_FAMILY, Patient.SP_GENDER}) ReferenceParam theSubject
) {
List<DiagnosticReport> retVal=new ArrayList<DiagnosticReport>();
String chain = theSubject.getChain();
if (Patient.SP_FAMILY.equals(chain)) {
String familyName = theSubject.getValue();
// .. populate with reports matching subject family name ..
}
if (Patient.SP_GENDER.equals(chain)) {
String gender = theSubject.getValue();
// .. populate with reports matching subject gender ..
}
return retVal;
}
//END SNIPPET: referenceWithChain
//START SNIPPET: referenceWithChainCombo
@Search
public List<DiagnosticReport> findReportsWithChainCombo (
@RequiredParam(name=DiagnosticReport.SP_SUBJECT, chainWhitelist= {"", Patient.SP_FAMILY}) ReferenceParam theSubject
) {
List<DiagnosticReport> retVal=new ArrayList<DiagnosticReport>();
String chain = theSubject.getChain();
if (Patient.SP_FAMILY.equals(chain)) {
String familyName = theSubject.getValue();
// .. populate with reports matching subject family name ..
}
if ("".equals(chain)) {
String resourceId = theSubject.getValue();
// .. populate with reports matching subject with resource ID ..
}
return retVal;
}
//END SNIPPET: referenceWithChainCombo
//START SNIPPET: referenceWithStaticChain
@Search @Search
public List<Patient> findObservations( public List<Patient> findObservations(
@RequiredParam(name=Observation.SP_SUBJECT+'.'+Patient.SP_IDENTIFIER) TokenParam theProvider @RequiredParam(name=Observation.SP_SUBJECT+'.'+Patient.SP_IDENTIFIER) TokenParam theProvider
@ -200,7 +247,7 @@ public List<Patient> findObservations(
return retVal; return retVal;
} }
//END SNIPPET: referenceChain //END SNIPPET: referenceWithStaticChain
//START SNIPPET: read //START SNIPPET: read

View File

@ -39,6 +39,8 @@ public @interface OptionalParam {
public static final String ALLOW_CHAIN_ANY = "*"; public static final String ALLOW_CHAIN_ANY = "*";
public static final String ALLOW_CHAIN_NOTCHAINED = "";
/** /**
* For reference parameters ({@link ReferenceParam}) this value may be * For reference parameters ({@link ReferenceParam}) this value may be
* used to indicate which chain values (if any) are <b>not</b> valid * used to indicate which chain values (if any) are <b>not</b> valid
@ -54,10 +56,23 @@ public @interface OptionalParam {
/** /**
* For reference parameters ({@link ReferenceParam}) this value may be * For reference parameters ({@link ReferenceParam}) this value may be
* used to indicate which chain values (if any) are valid for the given * 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 * parameter. If the list contains the value {@link #ALLOW_CHAIN_ANY}, all values are valid. (this is the default)
* the default, all values are valid. Any values specified in * If the list contains the value {@link #ALLOW_CHAIN_NOTCHAINED}
* then the reference param only supports the empty chain (i.e. the resource
* ID).
* <p>
* Valid values for this parameter include:
* <ul>
* <li><code>chainWhitelist={ OptionalParam.ALLOW_CHAIN_NOTCHAINED }</code> - Only allow resource reference (no chaining allowed for this parameter)</li>
* <li><code>chainWhitelist={ OptionalParam.ALLOW_CHAIN_ANY }</code> - Allow any chaining at all (including a non chained value, <b>this is the default</b>)</li>
* <li><code>chainWhitelist={ "foo", "bar" }</code> - Allow property.foo and property.bar</li>
* </ul>
* </p>
* <p>
* Any values specified in
* {@link #chainBlacklist()} will supercede (have priority over) values * {@link #chainBlacklist()} will supercede (have priority over) values
* here. * here.
* </p>
* <p> * <p>
* If the parameter annotated with this annotation is not a {@link ReferenceParam}, * If the parameter annotated with this annotation is not a {@link ReferenceParam},
* this value must not be populated. * this value must not be populated.

View File

@ -35,60 +35,67 @@ import ca.uhn.fhir.rest.param.ReferenceParam;
* Parameter annotation which specifies a search parameter for a {@link Search} method. * Parameter annotation which specifies a search parameter for a {@link Search} method.
*/ */
public @interface RequiredParam { public @interface RequiredParam {
/** /**
* For reference parameters ({@link ReferenceParam}) this value may be used to indicate which chain values (if 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
* are <b>not</b> valid for the given parameter. Values here will supercede any values specified in * values specified in {@link #chainWhitelist()}
* {@link #chainWhitelist()}
* <p> * <p>
* If the parameter annotated with this annotation is not a {@link ReferenceParam}, this value must not be * If the parameter annotated with this annotation is not a {@link ReferenceParam}, this value must not be populated.
* populated.
* </p> * </p>
*/ */
String[] chainBlacklist() default {}; String[] chainBlacklist() default {};
/** /**
* For reference parameters ({@link ReferenceParam}) this value may be used to indicate which chain values (if any) * For reference parameters ({@link ReferenceParam}) this value may be
* are valid for the given parameter. If the list contains the value {@link OptionalParam#ALLOW_CHAIN_ANY}, as is * used to indicate which chain values (if any) are valid for the given
* the default, all values are valid. Any values specified in {@link #chainBlacklist()} will supercede (have * parameter. If the list contains the value {@link OptionalParam#ALLOW_CHAIN_ANY}, all values are valid. (this is the default)
* priority over) values here. * If the list contains the value {@link OptionalParam#ALLOW_CHAIN_NOTCHAINED}
* then the reference param only supports the empty chain (i.e. the resource
* ID).
* <p> * <p>
* If the parameter annotated with this annotation is not a {@link ReferenceParam}, this value must not be * Valid values for this parameter include:
* populated. * <ul>
* <li><code>chainWhitelist={ OptionalParam.ALLOW_CHAIN_NOTCHAINED }</code> - Only allow resource reference (no chaining allowed for this parameter)</li>
* <li><code>chainWhitelist={ OptionalParam.ALLOW_CHAIN_ANY }</code> - Allow any chaining at all (including a non chained value, <b>this is the default</b>)</li>
* <li><code>chainWhitelist={ "foo", "bar" }</code> - Allow property.foo and property.bar</li>
* </ul>
* </p>
* <p>
* Any values specified in
* {@link #chainBlacklist()} will supercede (have priority over) values
* here.
* </p>
* <p>
* If the parameter annotated with this annotation is not a {@link ReferenceParam},
* this value must not be populated.
* </p> * </p>
*/ */
String[] chainWhitelist() default {OptionalParam.ALLOW_CHAIN_ANY}; String[] chainWhitelist() default {OptionalParam.ALLOW_CHAIN_ANY};
/** /**
* For composite parameters ({@link CompositeParam}) this parameter may be used to indicate the parameter type(s) * For composite parameters ({@link CompositeParam}) this parameter may be used to indicate the parameter type(s) which may be referenced by this param.
* which may be referenced by this param.
* <p> * <p>
* If the parameter annotated with this annotation is not a {@link CompositeParam}, this value must not be * If the parameter annotated with this annotation is not a {@link CompositeParam}, this value must not be populated.
* populated.
* </p> * </p>
*/ */
Class<? extends IQueryParameterType>[] compositeTypes() default {}; 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 * 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.
* will be the name of the URL parameter used to populate this method parameter.
* <p> * <p>
* Most resource model classes have constants which may be used to supply values for this field, e.g. * 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}
* {@link Patient#SP_NAME} or {@link Observation#SP_DATE}
* </p> * </p>
* <p> * <p>
* If you wish to specify a parameter for a resource reference which only accepts a specific chained value, it is * 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
* also valid to supply a chained name here, such as "patient.name". It is recommended to supply this using * recommended to supply this using constants where possible, e.g. <code>{@link Observation#SP_SUBJECT} + '.' + {@link Patient#SP_IDENTIFIER}</code>
* constants where possible, e.g. <code>{@link Observation#SP_SUBJECT} + '.' + {@link Patient#SP_IDENTIFIER}</code>
* </p> * </p>
*/ */
String name(); String name();
/** /**
* For resource reference parameters ({@link ReferenceParam}) this parameter may be used to indicate the resource * For resource reference parameters ({@link ReferenceParam}) this parameter may be used to indicate the resource type(s) which may be referenced by this param.
* type(s) which may be referenced by this param.
* <p> * <p>
* If the parameter annotated with this annotation is not a {@link ReferenceParam}, this value must not be * If the parameter annotated with this annotation is not a {@link ReferenceParam}, this value must not be populated.
* populated.
* </p> * </p>
*/ */
Class<? extends IResource>[] targetTypes() default {}; Class<? extends IResource>[] targetTypes() default {};

View File

@ -1,6 +1,7 @@
package ca.uhn.fhir.rest.method; package ca.uhn.fhir.rest.method;
import static org.apache.commons.lang3.StringUtils.*; import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.io.IOException; import java.io.IOException;
import java.io.PushbackReader; import java.io.PushbackReader;
@ -18,13 +19,10 @@ import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse; import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.swing.text.html.Option;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.utils.DateUtils; import org.apache.http.client.utils.DateUtils;
import com.google.common.base.Optional;
import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeResourceDefinition;

View File

@ -41,6 +41,7 @@ import ca.uhn.fhir.model.api.annotation.Description;
import ca.uhn.fhir.model.dstu.valueset.RestfulOperationSystemEnum; import ca.uhn.fhir.model.dstu.valueset.RestfulOperationSystemEnum;
import ca.uhn.fhir.model.dstu.valueset.RestfulOperationTypeEnum; import ca.uhn.fhir.model.dstu.valueset.RestfulOperationTypeEnum;
import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.annotation.OptionalParam;
import ca.uhn.fhir.rest.annotation.Search; import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.client.BaseHttpClientInvocation; import ca.uhn.fhir.rest.client.BaseHttpClientInvocation;
import ca.uhn.fhir.rest.param.BaseQueryParameter; import ca.uhn.fhir.rest.param.BaseQueryParameter;
@ -84,7 +85,7 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
* Check for parameter combinations and names that are invalid * Check for parameter combinations and names that are invalid
*/ */
List<IParameter> parameters = getParameters(); List<IParameter> parameters = getParameters();
List<SearchParameter> searchParameters = new ArrayList<SearchParameter>(); // List<SearchParameter> searchParameters = new ArrayList<SearchParameter>();
for (int i = 0; i < parameters.size(); i++) { for (int i = 0; i < parameters.size(); i++) {
IParameter next = parameters.get(i); IParameter next = parameters.get(i);
if (!(next instanceof SearchParameter)) { if (!(next instanceof SearchParameter)) {
@ -94,17 +95,18 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
SearchParameter sp = (SearchParameter) next; SearchParameter sp = (SearchParameter) next;
if (sp.getName().startsWith("_")) { if (sp.getName().startsWith("_")) {
if (ALLOWED_PARAMS.contains(sp.getName())) { if (ALLOWED_PARAMS.contains(sp.getName())) {
String msg = getContext().getLocalizer().getMessage(getClass().getName() + ".invalidSpecialParamName", theMethod.getName(), theMethod.getDeclaringClass().getSimpleName(), sp.getName()); String msg = getContext().getLocalizer().getMessage(getClass().getName() + ".invalidSpecialParamName", theMethod.getName(), theMethod.getDeclaringClass().getSimpleName(),
sp.getName());
throw new ConfigurationException(msg); throw new ConfigurationException(msg);
} }
} }
searchParameters.add(sp); // searchParameters.add(sp);
}
for (int i = 0; i < searchParameters.size(); i++) {
SearchParameter next = searchParameters.get(i);
// next.
} }
// for (int i = 0; i < searchParameters.size(); i++) {
// SearchParameter next = searchParameters.get(i);
// // next.
// }
/* /*
* Only compartment searching methods may have an ID parameter * Only compartment searching methods may have an ID parameter
@ -291,16 +293,36 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
ArrayList<String> retVal = new ArrayList<String>(theQualifiedNames.size()); ArrayList<String> retVal = new ArrayList<String>(theQualifiedNames.size());
for (String next : theQualifiedNames) { for (String next : theQualifiedNames) {
String qualifier = ""; String qualifier = "";
int idx = next.indexOf('.');
if (idx > -1 && next.length() > (idx + 1)) { int start = -1;
qualifier = next.substring(idx); int end = -1;
for (int idx = 0; idx < next.length(); idx++) {
char nextChar = next.charAt(idx);
if (nextChar == '.' || nextChar == ':') {
if (start == -1) {
start = idx;
} else {
end = idx;
break;
}
}
}
if (start != -1) {
if (end != -1) {
qualifier = next.substring(start, end);
} else {
qualifier = next.substring(start);
}
} }
if (theQualifierWhitelist != null) { if (theQualifierWhitelist != null) {
if (!theQualifierWhitelist.contains(OptionalParam.ALLOW_CHAIN_ANY)) {
if (!theQualifierWhitelist.contains(qualifier)) { if (!theQualifierWhitelist.contains(qualifier)) {
continue; continue;
} }
} }
}
if (theQualifierBlacklist != null) { if (theQualifierBlacklist != null) {
if (theQualifierBlacklist.contains(qualifier)) { if (theQualifierBlacklist.contains(qualifier)) {
continue; continue;
@ -311,7 +333,8 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
return retVal; return retVal;
} }
public static BaseHttpClientInvocation createSearchInvocation(FhirContext theContext, String theResourceName, Map<String, List<String>> theParameters, IdDt theId, String theCompartmentName, SearchStyleEnum theSearchStyle) { public static BaseHttpClientInvocation createSearchInvocation(FhirContext theContext, String theResourceName, Map<String, List<String>> theParameters, IdDt theId, String theCompartmentName,
SearchStyleEnum theSearchStyle) {
SearchStyleEnum searchStyle = theSearchStyle; SearchStyleEnum searchStyle = theSearchStyle;
if (searchStyle == null) { if (searchStyle == null) {
int length = 0; int length = 0;
@ -342,8 +365,7 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
} }
/* /*
* Are we doing a get (GET [base]/Patient?name=foo) or a get with search (GET [base]/Patient/_search?name=foo) * 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)
* or a post (POST [base]/Patient with parameters in the POST body)
*/ */
switch (searchStyle) { switch (searchStyle) {
case GET: case GET:

View File

@ -68,8 +68,10 @@ import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.param.TokenAndListParam; import ca.uhn.fhir.rest.param.TokenAndListParam;
import ca.uhn.fhir.rest.param.TokenOrListParam; import ca.uhn.fhir.rest.param.TokenOrListParam;
import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.util.CollectionUtil;
/** /**
* Created by dsotnikov on 2/25/2014. * Created by dsotnikov on 2/25/2014.
@ -77,38 +79,49 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
public class SearchParameter extends BaseQueryParameter { public class SearchParameter extends BaseQueryParameter {
private static final String EMPTY_STRING = "";
private static HashMap<Class<?>, SearchParamTypeEnum> ourParamTypes; private static HashMap<Class<?>, SearchParamTypeEnum> ourParamTypes;
private static HashMap<SearchParamTypeEnum, Set<String>> ourParamQualifiers;
static { static {
ourParamTypes = new HashMap<Class<?>, SearchParamTypeEnum>(); ourParamTypes = new HashMap<Class<?>, SearchParamTypeEnum>();
ourParamQualifiers = new HashMap<SearchParamTypeEnum, Set<String>>();
ourParamTypes.put(StringParam.class, SearchParamTypeEnum.STRING); ourParamTypes.put(StringParam.class, SearchParamTypeEnum.STRING);
ourParamTypes.put(StringOrListParam.class, SearchParamTypeEnum.STRING); ourParamTypes.put(StringOrListParam.class, SearchParamTypeEnum.STRING);
ourParamTypes.put(StringAndListParam.class, SearchParamTypeEnum.STRING); ourParamTypes.put(StringAndListParam.class, SearchParamTypeEnum.STRING);
ourParamQualifiers.put(SearchParamTypeEnum.STRING, CollectionUtil.newSet(Constants.PARAMQUALIFIER_STRING_EXACT, Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING));
ourParamTypes.put(TokenParam.class, SearchParamTypeEnum.TOKEN); ourParamTypes.put(TokenParam.class, SearchParamTypeEnum.TOKEN);
ourParamTypes.put(TokenOrListParam.class, SearchParamTypeEnum.TOKEN); ourParamTypes.put(TokenOrListParam.class, SearchParamTypeEnum.TOKEN);
ourParamTypes.put(TokenAndListParam.class, SearchParamTypeEnum.TOKEN); ourParamTypes.put(TokenAndListParam.class, SearchParamTypeEnum.TOKEN);
ourParamQualifiers.put(SearchParamTypeEnum.TOKEN, CollectionUtil.newSet(Constants.PARAMQUALIFIER_TOKEN_TEXT, Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING));
ourParamTypes.put(DateParam.class, SearchParamTypeEnum.DATE); ourParamTypes.put(DateParam.class, SearchParamTypeEnum.DATE);
ourParamTypes.put(DateOrListParam.class, SearchParamTypeEnum.DATE); ourParamTypes.put(DateOrListParam.class, SearchParamTypeEnum.DATE);
ourParamTypes.put(DateAndListParam.class, SearchParamTypeEnum.DATE); ourParamTypes.put(DateAndListParam.class, SearchParamTypeEnum.DATE);
ourParamTypes.put(DateRangeParam.class, SearchParamTypeEnum.DATE); ourParamTypes.put(DateRangeParam.class, SearchParamTypeEnum.DATE);
ourParamQualifiers.put(SearchParamTypeEnum.DATE, CollectionUtil.newSet(Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING));
ourParamTypes.put(QuantityParam.class, SearchParamTypeEnum.QUANTITY); ourParamTypes.put(QuantityParam.class, SearchParamTypeEnum.QUANTITY);
ourParamTypes.put(QuantityOrListParam.class, SearchParamTypeEnum.QUANTITY); ourParamTypes.put(QuantityOrListParam.class, SearchParamTypeEnum.QUANTITY);
ourParamTypes.put(QuantityAndListParam.class, SearchParamTypeEnum.QUANTITY); ourParamTypes.put(QuantityAndListParam.class, SearchParamTypeEnum.QUANTITY);
ourParamQualifiers.put(SearchParamTypeEnum.QUANTITY, CollectionUtil.newSet(Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING));
ourParamTypes.put(NumberParam.class, SearchParamTypeEnum.NUMBER); ourParamTypes.put(NumberParam.class, SearchParamTypeEnum.NUMBER);
ourParamTypes.put(NumberOrListParam.class, SearchParamTypeEnum.NUMBER); ourParamTypes.put(NumberOrListParam.class, SearchParamTypeEnum.NUMBER);
ourParamTypes.put(NumberAndListParam.class, SearchParamTypeEnum.NUMBER); ourParamTypes.put(NumberAndListParam.class, SearchParamTypeEnum.NUMBER);
ourParamQualifiers.put(SearchParamTypeEnum.NUMBER, CollectionUtil.newSet(Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING));
ourParamTypes.put(ReferenceParam.class, SearchParamTypeEnum.REFERENCE); ourParamTypes.put(ReferenceParam.class, SearchParamTypeEnum.REFERENCE);
ourParamTypes.put(ReferenceOrListParam.class, SearchParamTypeEnum.REFERENCE); ourParamTypes.put(ReferenceOrListParam.class, SearchParamTypeEnum.REFERENCE);
ourParamTypes.put(ReferenceAndListParam.class, SearchParamTypeEnum.REFERENCE); ourParamTypes.put(ReferenceAndListParam.class, SearchParamTypeEnum.REFERENCE);
ourParamQualifiers.put(SearchParamTypeEnum.REFERENCE, CollectionUtil.newSet(Constants.PARAMQUALIFIER_MISSING)); // no empty because that gets added from OptionalParam#chainWhitelist
ourParamTypes.put(CompositeParam.class, SearchParamTypeEnum.COMPOSITE); ourParamTypes.put(CompositeParam.class, SearchParamTypeEnum.COMPOSITE);
ourParamTypes.put(CompositeOrListParam.class, SearchParamTypeEnum.COMPOSITE); ourParamTypes.put(CompositeOrListParam.class, SearchParamTypeEnum.COMPOSITE);
ourParamTypes.put(CompositeAndListParam.class, SearchParamTypeEnum.COMPOSITE); ourParamTypes.put(CompositeAndListParam.class, SearchParamTypeEnum.COMPOSITE);
ourParamQualifiers.put(SearchParamTypeEnum.COMPOSITE, CollectionUtil.newSet(Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING));
} }
private Set<String> myQualifierBlacklist; private Set<String> myQualifierBlacklist;
private Set<String> myQualifierWhitelist; private Set<String> myQualifierWhitelist;
@ -205,12 +218,12 @@ public class SearchParameter extends BaseQueryParameter {
public void setChainlists(String[] theChainWhitelist, String[] theChainBlacklist) { public void setChainlists(String[] theChainWhitelist, String[] theChainBlacklist) {
myQualifierWhitelist = new HashSet<String>(theChainWhitelist.length); myQualifierWhitelist = new HashSet<String>(theChainWhitelist.length);
for (int i = 0; i < theChainWhitelist.length; i++) { for (int i = 0; i < theChainWhitelist.length; i++) {
if (theChainWhitelist[i].equals(OptionalParam.ALLOW_CHAIN_ANY)) { if (theChainWhitelist[i].equals(OptionalParam.ALLOW_CHAIN_ANY)) {
myQualifierWhitelist = null; myQualifierWhitelist.add(OptionalParam.ALLOW_CHAIN_ANY);
break; } else if (theChainWhitelist[i].equals(EMPTY_STRING)) {
} else if (theChainWhitelist[i].equals("")) { myQualifierWhitelist.add(EMPTY_STRING);
myQualifierWhitelist.add("");
} else { } else {
myQualifierWhitelist.add('.' + theChainWhitelist[i]); myQualifierWhitelist.add('.' + theChainWhitelist[i]);
} }
@ -219,8 +232,8 @@ public class SearchParameter extends BaseQueryParameter {
if (theChainBlacklist.length > 0) { if (theChainBlacklist.length > 0) {
myQualifierBlacklist = new HashSet<String>(theChainBlacklist.length); myQualifierBlacklist = new HashSet<String>(theChainBlacklist.length);
for (String next : theChainBlacklist) { for (String next : theChainBlacklist) {
if (next.equals("")) { if (next.equals(EMPTY_STRING)) {
myQualifierBlacklist.add(""); myQualifierBlacklist.add(EMPTY_STRING);
} else { } else {
myQualifierBlacklist.add('.' + next); myQualifierBlacklist.add('.' + next);
} }
@ -264,8 +277,23 @@ public class SearchParameter extends BaseQueryParameter {
throw new ConfigurationException("Unsupported data type for parameter: " + type.getCanonicalName()); throw new ConfigurationException("Unsupported data type for parameter: " + type.getCanonicalName());
} }
SearchParamTypeEnum typeEnum = ourParamTypes.get(type);
if (typeEnum != null) {
Set<String> builtInQualifiers = ourParamQualifiers.get(typeEnum);
if (builtInQualifiers != null) {
if (myQualifierWhitelist != null) {
HashSet<String> qualifierWhitelist = new HashSet<String>();
qualifierWhitelist.addAll(myQualifierWhitelist);
qualifierWhitelist.addAll(builtInQualifiers);
myQualifierWhitelist = qualifierWhitelist;
} else {
myQualifierWhitelist = Collections.unmodifiableSet(builtInQualifiers);
}
}
}
if (myParamType == null) { if (myParamType == null) {
myParamType = ourParamTypes.get(type); myParamType = typeEnum;
} }
if (myParamType != null) { if (myParamType != null) {

View File

@ -33,6 +33,7 @@ import org.apache.commons.lang3.StringUtils;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.dstu.valueset.SearchParamTypeEnum; import ca.uhn.fhir.model.dstu.valueset.SearchParamTypeEnum;
import ca.uhn.fhir.rest.annotation.OptionalParam;
import ca.uhn.fhir.rest.client.BaseHttpClientInvocation; import ca.uhn.fhir.rest.client.BaseHttpClientInvocation;
import ca.uhn.fhir.rest.method.IParameter; import ca.uhn.fhir.rest.method.IParameter;
import ca.uhn.fhir.rest.method.QualifiedParamList; import ca.uhn.fhir.rest.method.QualifiedParamList;
@ -52,15 +53,15 @@ public abstract class BaseQueryParameter implements IParameter {
public abstract boolean isRequired(); public abstract boolean isRequired();
/** /**
* Parameter should return true if {@link #parse(List)} should be called even if the query string contained no * Parameter should return true if {@link #parse(List)} should be called even if the query string contained no values for the given parameter
* values for the given parameter
*/ */
public abstract boolean handlesMissing(); public abstract boolean handlesMissing();
public abstract SearchParamTypeEnum getParamType(); public abstract SearchParamTypeEnum getParamType();
@Override @Override
public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map<String, List<String>> theTargetQueryArguments, BaseHttpClientInvocation theClientInvocation) throws InternalErrorException { public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map<String, List<String>> theTargetQueryArguments,
BaseHttpClientInvocation theClientInvocation) throws InternalErrorException {
if (theSourceClientArgument == null) { if (theSourceClientArgument == null) {
if (isRequired()) { if (isRequired()) {
throw new NullPointerException("SearchParameter '" + getName() + "' is required and may not be null"); throw new NullPointerException("SearchParameter '" + getName() + "' is required and may not be null");
@ -117,10 +118,12 @@ public abstract class BaseQueryParameter implements IParameter {
private void parseParams(RequestDetails theRequest, List<QualifiedParamList> paramList, String theQualifiedParamName, String theQualifier) { private void parseParams(RequestDetails theRequest, List<QualifiedParamList> paramList, String theQualifiedParamName, String theQualifier) {
if (getQualifierWhitelist() != null) { if (getQualifierWhitelist() != null) {
if (!getQualifierWhitelist().contains(OptionalParam.ALLOW_CHAIN_ANY)) {
if (!getQualifierWhitelist().contains(defaultString(theQualifier))) { if (!getQualifierWhitelist().contains(defaultString(theQualifier))) {
return; return;
} }
} }
}
if (getQualifierBlacklist() != null) { if (getQualifierBlacklist() != null) {
if (getQualifierBlacklist().contains(defaultString(theQualifier))) { if (getQualifierBlacklist().contains(defaultString(theQualifier))) {
return; return;

View File

@ -0,0 +1,17 @@
package ca.uhn.fhir.util;
import java.util.HashSet;
import java.util.Set;
public class CollectionUtil {
public static <T> Set<T> newSet(T... theValues) {
HashSet<T> retVal = new HashSet<T>();
for (T t : theValues) {
retVal.add(t);
}
return retVal;
}
}

View File

@ -682,35 +682,54 @@
<p> <p>
Reference parameters use the Reference parameters use the
<a href="./apidocs/ca/uhn/fhir/rest/param/ReferenceParam.html">ReferenceParam</a> <a href="./apidocs/ca/uhn/fhir/rest/param/ReferenceParam.html">ReferenceParam</a>
type. Reference param objects being passed into server methods will have three properties type. Reference parameters are, in their most basic form, just a pointer to another
populated: resource. For example, you might want to query for DiagnosticReport resources where the
subject (the Patient resource that the report is about) is Patient/123. The following
example shows a simple resource reference parameter in use.
</p>
<macro name="snippet">
<param name="id" value="referenceSimple" />
<param name="file" value="examples/src/main/java/example/RestfulPatientResourceProviderMore.java" />
</macro>
<h4>Chained Resource References</h4>
<p>
References may also support a "chained" value. This is a search parameter name
on the target resource. For example, you might want to search for DiagnosticReport
resources by subject, but use the subject's last name instead of their resource ID.
In this example, you are chaining "family" (the last name) to "subject" (the patient).
The net result in the query string would look like:<br/>
<code>http://fhir.example.com/DiagnosticReport?subject.family=SMITH</code>
</p>
<p>
Chained values must be explicitly declared through the use
of a whitelist (or blacklist). The following example shows how to declare a
report with an allowable chained parameter:
</p>
<macro name="snippet">
<param name="id" value="referenceWithChain" />
<param name="file" value="examples/src/main/java/example/RestfulPatientResourceProviderMore.java" />
</macro>
<p>
You may also specify the whitelist value of
<code>""</code> to allow an empty chain (e.g. ther resource ID)
and this can be combined with other values, as shown below:
</p> </p>
<ul>
<li>
<b>The resource type (optional):</b> This is the type of resource
being targeted, if the client specifies one in the URL.
</li>
<li>
<b>The chain property (optional):</b> This is the name of the
property on the target resource to search for
</li>
<li>
<b>The value (required):</b> This is the actual value to search for. If
a chain property has not been specified, this is the ID of the resource.
</li>
</ul>
<macro name="snippet"> <macro name="snippet">
<param name="id" value="reference" /> <param name="id" value="referenceWithChainCombo" />
<param name="file" value="examples/src/main/java/example/RestfulPatientResourceProviderMore.java" /> <param name="file" value="examples/src/main/java/example/RestfulPatientResourceProviderMore.java" />
</macro> </macro>
<p> <p>
Example URLs to invoke this method: Example URLs to invoke this method:
<br /> <br />
Resource by ID: <code>http://fhir.example.com/Patient?provider=1234</code> Resource by ID: <code>http://fhir.example.com/DiagnosticReport?subject=1234</code>
<br /> <br />
Resource by chained parameter value: <code>http://fhir.example.com/Patient?provider:Organization.name=FooOrg</code> Resource by chained parameter value: <code>http://fhir.example.com/DiagnosticReport?subject.family=SMITH</code>
</p> </p>
<h4>Static Chains</h4> <h4>Static Chains</h4>
@ -724,7 +743,7 @@
</p> </p>
<macro name="snippet"> <macro name="snippet">
<param name="id" value="referenceChain" /> <param name="id" value="referenceWithStaticChain" />
<param name="file" value="examples/src/main/java/example/RestfulPatientResourceProviderMore.java" /> <param name="file" value="examples/src/main/java/example/RestfulPatientResourceProviderMore.java" />
</macro> </macro>

View File

@ -1,11 +1,11 @@
package ca.uhn.fhir.rest.server; package ca.uhn.fhir.rest.server;
import static org.apache.commons.lang3.StringUtils.*; import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.*; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -221,7 +221,9 @@ public class ReferenceParameterTest {
ServletHandler proxyHandler = new ServletHandler(); ServletHandler proxyHandler = new ServletHandler();
RestfulServer servlet = new RestfulServer(); RestfulServer servlet = new RestfulServer();
ourCtx = servlet.getFhirContext(); ourCtx = servlet.getFhirContext();
servlet.setResourceProviders(patientProvider, new DummyOrganizationResourceProvider()); servlet.setResourceProviders(patientProvider
, new DummyOrganizationResourceProvider()
);
ServletHolder servletHolder = new ServletHolder(servlet); ServletHolder servletHolder = new ServletHolder(servlet);
proxyHandler.addServletWithMapping(servletHolder, "/*"); proxyHandler.addServletWithMapping(servletHolder, "/*");
ourServer.setHandler(proxyHandler); ourServer.setHandler(proxyHandler);

View File

@ -174,7 +174,7 @@ public class StringParameterTest {
public static class DummyPatientResourceProvider implements IResourceProvider { public static class DummyPatientResourceProvider implements IResourceProvider {
@Search @Search
public List<Patient> findPatient(@RequiredParam(name = "str") StringParam theParam) { public List<Patient> findPatientByStringParam(@RequiredParam(name = "str") StringParam theParam) {
ArrayList<Patient> retVal = new ArrayList<Patient>(); ArrayList<Patient> retVal = new ArrayList<Patient>();
if (theParam.isExact() && theParam.getValue().equals("aaa")) { if (theParam.isExact() && theParam.getValue().equals("aaa")) {
@ -192,7 +192,7 @@ public class StringParameterTest {
} }
@Search @Search
public List<Patient> findPatient(@RequiredParam(name = "plain") String theParam) { public List<Patient> findPatientByString(@RequiredParam(name = "plain") String theParam) {
ArrayList<Patient> retVal = new ArrayList<Patient>(); ArrayList<Patient> retVal = new ArrayList<Patient>();
if (theParam.toLowerCase().equals("aaa")) { if (theParam.toLowerCase().equals("aaa")) {