Allow repeatable prarmater values

This commit is contained in:
James Agnew 2014-07-30 17:43:15 -04:00
parent ef10560c76
commit 0ba7a63803
23 changed files with 1191 additions and 635 deletions

View File

@ -6,6 +6,11 @@
<title>HAPI FHIR Changelog</title>
</properties>
<body>
<release version="0.6" date="TBD">
<action type="add">
Allow generic client
</action>
</release>
<release version="0.5" date="2014-Jul-30">
<action type="add">
having multiple ways of accomplishing the same thing. This means that a number of existing classes

View File

@ -74,11 +74,13 @@ import ca.uhn.fhir.rest.method.DeleteMethodBinding;
import ca.uhn.fhir.rest.method.HistoryMethodBinding;
import ca.uhn.fhir.rest.method.HttpDeleteClientInvocation;
import ca.uhn.fhir.rest.method.HttpGetClientInvocation;
import ca.uhn.fhir.rest.method.HttpPostClientInvocation;
import ca.uhn.fhir.rest.method.HttpSimpleGetClientInvocation;
import ca.uhn.fhir.rest.method.IClientResponseHandler;
import ca.uhn.fhir.rest.method.MethodUtil;
import ca.uhn.fhir.rest.method.ReadMethodBinding;
import ca.uhn.fhir.rest.method.SearchMethodBinding;
import ca.uhn.fhir.rest.method.SearchStyleEnum;
import ca.uhn.fhir.rest.method.TransactionMethodBinding;
import ca.uhn.fhir.rest.method.ValidateMethodBinding;
import ca.uhn.fhir.rest.server.Constants;
@ -420,7 +422,8 @@ public class GenericClient extends BaseClient implements IGenericClient {
}
@Override
public Bundle invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws IOException, BaseServerResponseException {
public Bundle invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws IOException,
BaseServerResponseException {
EncodingEnum respType = EncodingEnum.forContentType(theResponseMimeType);
if (respType == null) {
throw NonFhirResponseException.newInstance(theResponseStatusCode, theResponseMimeType, theResponseReader);
@ -442,7 +445,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
myResource = parseResourceBody(myResourceBody);
}
myId = getPreferredId(myResource, myId);
BaseHttpClientInvocation invocation = MethodUtil.createCreateInvocation(myResource, myResourceBody, myId, myContext);
RuntimeResourceDefinition def = myContext.getResourceDefinition(myResource);
@ -455,7 +458,6 @@ public class GenericClient extends BaseClient implements IGenericClient {
}
@Override
public ICreateTyped resource(IResource theResource) {
Validate.notNull(theResource, "Resource can not be null");
@ -483,7 +485,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
}
}
private class UpdateInternal extends BaseClientExecutable<IUpdateTyped, MethodOutcome> implements IUpdate, IUpdateTyped {
private IdDt myId;
@ -498,10 +500,10 @@ public class GenericClient extends BaseClient implements IGenericClient {
if (myId == null) {
myId = myResource.getId();
}
if (myId==null || myId.hasIdPart() == false) {
if (myId == null || myId.hasIdPart() == false) {
throw new InvalidRequestException("No ID supplied for resource to update, can not invoke server");
}
BaseHttpClientInvocation invocation = MethodUtil.createUpdateInvocation(myResource, myResourceBody, myId, myContext);
RuntimeResourceDefinition def = myContext.getResourceDefinition(myResource);
@ -533,8 +535,8 @@ public class GenericClient extends BaseClient implements IGenericClient {
if (theId == null) {
throw new NullPointerException("theId can not be null");
}
if (theId.hasIdPart()==false) {
throw new NullPointerException("theId must not be blank and must contain an ID, found: "+theId.getValue());
if (theId.hasIdPart() == false) {
throw new NullPointerException("theId must not be blank and must contain an ID, found: " + theId.getValue());
}
myId = theId;
return this;
@ -546,9 +548,9 @@ public class GenericClient extends BaseClient implements IGenericClient {
throw new NullPointerException("theId can not be null");
}
if (isBlank(theId)) {
throw new NullPointerException("theId must not be blank and must contain an ID, found: "+theId);
throw new NullPointerException("theId must not be blank and must contain an ID, found: " + theId);
}
myId=new IdDt(theId);
myId = new IdDt(theId);
return this;
}
@ -712,7 +714,8 @@ public class GenericClient extends BaseClient implements IGenericClient {
private final class OperationOutcomeResponseHandler implements IClientResponseHandler<OperationOutcome> {
@Override
public OperationOutcome invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws IOException, BaseServerResponseException {
public OperationOutcome invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws IOException,
BaseServerResponseException {
EncodingEnum respType = EncodingEnum.forContentType(theResponseMimeType);
if (respType == null) {
return null;
@ -739,7 +742,8 @@ public class GenericClient extends BaseClient implements IGenericClient {
}
@Override
public MethodOutcome invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws IOException, BaseServerResponseException {
public MethodOutcome invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws IOException,
BaseServerResponseException {
MethodOutcome response = MethodUtil.process2xxResponse(myContext, myResourceName, theResponseStatusCode, theResponseMimeType, theResponseReader, theHeaders);
return response;
}
@ -754,7 +758,8 @@ public class GenericClient extends BaseClient implements IGenericClient {
}
@Override
public List<IResource> invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws IOException, BaseServerResponseException {
public List<IResource> invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws IOException,
BaseServerResponseException {
return new BundleResponseHandler(myType).invokeClient(theResponseMimeType, theResponseReader, theResponseStatusCode, theHeaders).toListOfResources();
}
}
@ -799,6 +804,8 @@ public class GenericClient extends BaseClient implements IGenericClient {
private Class<? extends IResource> myResourceType;
private List<SortInternal> mySort = new ArrayList<SortInternal>();
private SearchStyleEnum mySearchStyle;
public SearchInternal() {
myResourceType = null;
myResourceName = null;
@ -838,7 +845,36 @@ public class GenericClient extends BaseClient implements IGenericClient {
}
BundleResponseHandler binding = new BundleResponseHandler(myResourceType);
HttpGetClientInvocation invocation = new HttpGetClientInvocation(params, myResourceName);
SearchStyleEnum searchStyle = mySearchStyle;
if (searchStyle == null) {
int length = 0;
for (Entry<String, List<String>> nextEntry : params.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;
switch (searchStyle) {
case GET:
default:
invocation = new HttpGetClientInvocation(params, myResourceName);
break;
case GET_WITH_SEARCH:
invocation = new HttpGetClientInvocation(params, myResourceName, Constants.PARAM_SEARCH);
break;
case POST:
invocation = new HttpPostClientInvocation(myContext, params, myResourceName, Constants.PARAM_SEARCH);
}
return invoke(params, binding, invocation);
@ -901,6 +937,12 @@ public class GenericClient extends BaseClient implements IGenericClient {
return this;
}
@Override
public IQuery usingStyle(SearchStyleEnum theStyle) {
mySearchStyle = theStyle;
return this;
}
}
private class SortInternal implements ISort {
@ -947,7 +989,8 @@ public class GenericClient extends BaseClient implements IGenericClient {
private final class TagListResponseHandler implements IClientResponseHandler<TagList> {
@Override
public TagList invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws IOException, BaseServerResponseException {
public TagList invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws IOException,
BaseServerResponseException {
EncodingEnum respType = EncodingEnum.forContentType(theResponseMimeType);
if (respType == null) {
throw NonFhirResponseException.newInstance(theResponseStatusCode, theResponseMimeType, theResponseReader);
@ -1014,5 +1057,4 @@ public class GenericClient extends BaseClient implements IGenericClient {
return new UpdateInternal();
}
}

View File

@ -22,6 +22,7 @@ package ca.uhn.fhir.rest.gclient;
import ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.rest.method.SearchStyleEnum;
public interface IQuery extends IClientExecutable<IQuery,Bundle> {
@ -35,4 +36,13 @@ public interface IQuery extends IClientExecutable<IQuery,Bundle> {
IQuery limitTo(int theLimitTo);
/**
* Forces the query to perform the search using the given method (allowable methods are described in the
* <a href="http://www.hl7.org/implement/standards/fhir/http.html#search">FHIR Specification Section 2.1.11</a>)
*
* @see SearchStyleEnum
* @since 0.6
*/
IQuery usingStyle(SearchStyleEnum theStyle);
}

View File

@ -20,6 +20,9 @@ package ca.uhn.fhir.rest.gclient;
* #L%
*/
import java.util.Arrays;
import java.util.List;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.rest.server.Constants;
@ -41,10 +44,8 @@ public class StringClientParam implements IParam {
return myParamName;
}
/**
* The string matches the given value (servers will often, but are not required to) implement this as a left match,
* meaning that a value of "smi" would match "smi" and "smith".
* The string matches the given value (servers will often, but are not required to) implement this as a left match, meaning that a value of "smi" would match "smi" and "smith".
*/
public IStringMatch matches() {
return new StringMatches();
@ -59,10 +60,28 @@ public class StringClientParam implements IParam {
public interface IStringMatch {
/**
* Requests that resources be returned which match the given value
*/
ICriterion<StringClientParam> value(String theValue);
/**
* Requests that resources be returned which match ANY of the given values (this is an OR search). Note that to specify an AND search, simply add a subsequent {@link IQuery#where(ICriterion)
* where} criteria with the same parameter.
*/
ICriterion<StringClientParam> values(List<String> theValues);
/**
* Requests that resources be returned which match the given value
*/
ICriterion<StringClientParam> value(StringDt theValue);
/**
* Requests that resources be returned which match ANY of the given values (this is an OR search). Note that to specify an AND search, simply add a subsequent {@link IQuery#where(ICriterion)
* where} criteria with the same parameter.
*/
ICriterion<?> values(String... theValues);
}
private class StringExactly implements IStringMatch {
@ -75,6 +94,16 @@ public class StringClientParam implements IParam {
public ICriterion<StringClientParam> value(StringDt theValue) {
return new StringCriterion<StringClientParam>(getParamName() + Constants.PARAMQUALIFIER_STRING_EXACT, theValue.getValue());
}
@Override
public ICriterion<StringClientParam> values(List<String> theValue) {
return new StringCriterion<StringClientParam>(getParamName() + Constants.PARAMQUALIFIER_STRING_EXACT, theValue);
}
@Override
public ICriterion<?> values(String... theValues) {
return new StringCriterion<StringClientParam>(getParamName() + Constants.PARAMQUALIFIER_STRING_EXACT, Arrays.asList(theValues));
}
}
private class StringMatches implements IStringMatch {
@ -87,6 +116,17 @@ public class StringClientParam implements IParam {
public ICriterion<StringClientParam> value(StringDt theValue) {
return new StringCriterion<StringClientParam>(getParamName(), theValue.getValue());
}
@Override
public ICriterion<StringClientParam> values(List<String> theValue) {
return new StringCriterion<StringClientParam>(getParamName(), theValue);
}
@Override
public ICriterion<?> values(String... theValues) {
return new StringCriterion<StringClientParam>(getParamName(), Arrays.asList(theValues));
}
}
}

View File

@ -1,5 +1,9 @@
package ca.uhn.fhir.rest.gclient;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import ca.uhn.fhir.rest.param.ParameterUtil;
/*
@ -28,8 +32,23 @@ class StringCriterion<A extends IParam> implements ICriterion<A>, ICriterionInte
private String myName;
public StringCriterion(String theName, String theValue) {
myValue = theValue;
myName=theName;
myValue = ParameterUtil.escape(theValue);
}
public StringCriterion(String theName, List<String> theValue) {
myName=theName;
StringBuilder b = new StringBuilder();
for (String next : theValue) {
if (StringUtils.isBlank(next)) {
continue;
}
if (b.length() > 0) {
b.append(',');
}
b.append(ParameterUtil.escape(next));
}
myValue = b.toString();
}
@Override
@ -39,7 +58,7 @@ class StringCriterion<A extends IParam> implements ICriterion<A>, ICriterionInte
@Override
public String getParameterValue() {
return ParameterUtil.escape(myValue);
return myValue;
}
}

View File

@ -1,6 +1,10 @@
package ca.uhn.fhir.rest.gclient;
import static org.apache.commons.lang3.StringUtils.defaultString;
import java.util.Arrays;
import java.util.List;
import ca.uhn.fhir.model.dstu.composite.IdentifierDt;
/*
@ -66,7 +70,17 @@ public class TokenClientParam implements IParam {
public ICriterion<TokenClientParam> identifier(IdentifierDt theIdentifier) {
return new TokenCriterion(getParamName(), theIdentifier.getSystem().getValueAsString(), theIdentifier.getValue().getValue());
}
};
@Override
public ICriterion<TokenClientParam> identifiers(List<IdentifierDt> theIdentifiers) {
return new TokenCriterion(getParamName(), theIdentifiers);
}
@Override
public ICriterion<TokenClientParam> identifiers(IdentifierDt... theIdentifiers) {
return new TokenCriterion(getParamName(), Arrays.asList(theIdentifiers));
}
};
}
public interface IMatches {
@ -118,6 +132,27 @@ public class TokenClientParam implements IParam {
* @return A criterion
*/
ICriterion<TokenClientParam> identifier(IdentifierDt theIdentifier);
/**
* Creates a search criterion that matches against the given collection of identifiers (system and code if both are present, or whatever is present).
* In the query URL that is generated, identifiers will be joined with a ',' to create an OR query.
*
* @param theIdentifier
* The identifier
* @return A criterion
*/
ICriterion<TokenClientParam> identifiers(List<IdentifierDt> theIdentifiers);
/**
* Creates a search criterion that matches against the given collection of identifiers (system and code if both are present, or whatever is present).
* In the query URL that is generated, identifiers will be joined with a ',' to create an OR query.
*
* @param theIdentifier
* The identifier
* @return A criterion
*/
ICriterion<TokenClientParam> identifiers(IdentifierDt... theIdentifiers);
}
}

View File

@ -20,8 +20,11 @@ package ca.uhn.fhir.rest.gclient;
* #L%
*/
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import ca.uhn.fhir.model.dstu.composite.IdentifierDt;
import ca.uhn.fhir.rest.param.ParameterUtil;
class TokenCriterion implements ICriterion<TokenClientParam>, ICriterionInternal {
@ -31,15 +34,36 @@ class TokenCriterion implements ICriterion<TokenClientParam>, ICriterionInternal
public TokenCriterion(String theName, String theSystem, String theCode) {
myName = theName;
myValue=toValue(theSystem, theCode);
}
private String toValue(String theSystem, String theCode) {
String system = ParameterUtil.escape(theSystem);
String code = ParameterUtil.escape(theCode);
String value;
if (StringUtils.isNotBlank(system)) {
myValue = system + "|" + StringUtils.defaultString(code);
value = system + "|" + StringUtils.defaultString(code);
} else if (system == null) {
myValue = StringUtils.defaultString(code);
value = StringUtils.defaultString(code);
} else {
myValue = "|" + StringUtils.defaultString(code);
value = "|" + StringUtils.defaultString(code);
}
return value;
}
public TokenCriterion(String theParamName, List<IdentifierDt> theValue) {
myName=theParamName;
StringBuilder b = new StringBuilder();
for (IdentifierDt next : theValue) {
if (next.getSystem().isEmpty() && next.getValue().isEmpty()) {
continue;
}
if (b.length() > 0) {
b.append(',');
}
b.append(toValue(next.getSystem().getValueAsString(), next.getValue().getValue()));
}
myValue = b.toString();
}
@Override

View File

@ -22,15 +22,21 @@ package ca.uhn.fhir.rest.method;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.entity.AbstractHttpEntity;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.message.BasicNameValuePair;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.Bundle;
@ -42,6 +48,7 @@ import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.client.BaseHttpClientInvocation;
import ca.uhn.fhir.rest.server.EncodingEnum;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
abstract class BaseHttpClientInvocationWithContents extends BaseHttpClientInvocation {
@ -53,6 +60,7 @@ abstract class BaseHttpClientInvocationWithContents extends BaseHttpClientInvoca
private final Bundle myBundle;
private final String myContents;
private boolean myContentsIsBundle;
private Map<String, List<String>> myParams;
public BaseHttpClientInvocationWithContents(FhirContext theContext, IResource theResource, String theUrlExtension) {
super();
@ -112,6 +120,18 @@ abstract class BaseHttpClientInvocationWithContents extends BaseHttpClientInvoca
myContentsIsBundle = theIsBundle;
}
public BaseHttpClientInvocationWithContents(FhirContext theContext, Map<String, List<String>> theParams, String... theUrlExtension) {
myContext = theContext;
myResource = null;
myTagList = null;
myUrlExtension = StringUtils.join(theUrlExtension, '/');
myResources = null;
myBundle = null;
myContents = null;
myContentsIsBundle = false;
myParams = theParams;
}
@Override
public HttpRequestBase asHttpRequest(String theUrlBase, Map<String, List<String>> theExtraParams, EncodingEnum theEncoding) throws DataFormatException {
StringBuilder b = new StringBuilder();
@ -145,31 +165,43 @@ abstract class BaseHttpClientInvocationWithContents extends BaseHttpClientInvoca
parser = myContext.newXmlParser();
}
String contents;
if (myTagList != null) {
contents = parser.encodeTagListToString(myTagList);
contentType = encoding.getResourceContentType();
} else if (myBundle != null) {
contents = parser.encodeBundleToString(myBundle);
contentType = encoding.getBundleContentType();
} else if (myResources != null) {
Bundle bundle = RestfulServer.createBundleFromResourceList(myContext, "", myResources, "", "", myResources.size());
contents = parser.encodeBundleToString(bundle);
contentType = encoding.getBundleContentType();
} else if (myContents != null) {
contents = myContents;
if (myContentsIsBundle) {
contentType = encoding.getBundleContentType();
} else {
contentType = encoding.getResourceContentType();
AbstractHttpEntity entity;
if (myParams != null) {
List<NameValuePair> parameters = new ArrayList<NameValuePair>();
for (Entry<String, List<String>> nextParam : myParams.entrySet()) {
parameters.add(new BasicNameValuePair(nextParam.getKey(), StringUtils.join(nextParam.getValue(), ',')));
}
try {
entity = new UrlEncodedFormEntity(parameters, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new InternalErrorException("Server does not support UTF-8 (should not happen)", e);
}
} else {
contents = parser.encodeResourceToString(myResource);
contentType = encoding.getResourceContentType();
String contents;
if (myTagList != null) {
contents = parser.encodeTagListToString(myTagList);
contentType = encoding.getResourceContentType();
} else if (myBundle != null) {
contents = parser.encodeBundleToString(myBundle);
contentType = encoding.getBundleContentType();
} else if (myResources != null) {
Bundle bundle = RestfulServer.createBundleFromResourceList(myContext, "", myResources, "", "", myResources.size());
contents = parser.encodeBundleToString(bundle);
contentType = encoding.getBundleContentType();
} else if (myContents != null) {
contents = myContents;
if (myContentsIsBundle) {
contentType = encoding.getBundleContentType();
} else {
contentType = encoding.getResourceContentType();
}
} else {
contents = parser.encodeResourceToString(myResource);
contentType = encoding.getResourceContentType();
}
entity = new StringEntity(contents, ContentType.create(contentType, "UTF-8"));
}
StringEntity entity = new StringEntity(contents, ContentType.create(contentType, "UTF-8"));
HttpRequestBase retVal = createRequest(url, entity);
super.addHeadersToRequest(retVal);

View File

@ -21,6 +21,7 @@ package ca.uhn.fhir.rest.method;
*/
import java.util.List;
import java.util.Map;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.AbstractHttpEntity;
@ -56,6 +57,11 @@ public class HttpPostClientInvocation extends BaseHttpClientInvocationWithConten
}
public HttpPostClientInvocation(FhirContext theContext, Map<String, List<String>> theParams, String... theUrlExtension) {
super(theContext, theParams, theUrlExtension);
}
@Override
protected HttpPost createRequest(String url, AbstractHttpEntity theEntity) {
HttpPost retVal = new HttpPost(url);

View File

@ -31,6 +31,6 @@ interface IParamBinder {
List<IQueryParameterOr<?>> encode(FhirContext theContext, Object theString) throws InternalErrorException;
Object parse(List<QualifiedParamList> theList) throws InternalErrorException, InvalidRequestException;
Object parse(String theName, List<QualifiedParamList> theList) throws InternalErrorException, InvalidRequestException;
}

View File

@ -43,7 +43,7 @@ final class QueryParameterAndBinder extends BaseBinder<IQueryParameterAnd<?>> im
}
@Override
public Object parse(List<QualifiedParamList> theString) throws InternalErrorException, InvalidRequestException {
public Object parse(String theName, List<QualifiedParamList> theString) throws InternalErrorException, InvalidRequestException {
IQueryParameterAnd<?> dt;
try {
dt = newInstance();

View File

@ -44,7 +44,7 @@ final class QueryParameterOrBinder extends BaseBinder<IQueryParameterOr<?>> impl
}
@Override
public Object parse(List<QualifiedParamList> theString) throws InternalErrorException, InvalidRequestException {
public Object parse(String theName, List<QualifiedParamList> theString) throws InternalErrorException, InvalidRequestException {
IQueryParameterOr<?> dt;
try {
dt = newInstance();
@ -52,7 +52,7 @@ final class QueryParameterOrBinder extends BaseBinder<IQueryParameterOr<?>> impl
return dt;
}
if (theString.size() > 1) {
throw new InvalidRequestException("Multiple values detected");
throw new InvalidRequestException("Multiple values detected for non-repeatable parameter '" + theName + "'. This server is not configured to allow multiple (AND/OR) values for this param.");
}
dt.setValuesAsQueryTokens(theString.get(0));

View File

@ -46,7 +46,7 @@ final class QueryParameterTypeBinder extends BaseBinder<IQueryParameterType> imp
}
@Override
public Object parse(List<QualifiedParamList> theParams) throws InternalErrorException, InvalidRequestException {
public Object parse(String theName, List<QualifiedParamList> theParams) throws InternalErrorException, InvalidRequestException {
String value = theParams.get(0).get(0);
if (StringUtils.isBlank(value)) {
return null;
@ -58,7 +58,7 @@ final class QueryParameterTypeBinder extends BaseBinder<IQueryParameterType> imp
return dt;
}
if (theParams.size() > 1 || theParams.get(0).size() > 1) {
throw new InvalidRequestException("Multiple values detected");
throw new InvalidRequestException("Multiple values detected for non-repeatable parameter '" + theName + "'. This server is not configured to allow multiple (AND/OR) values for this param.");
}
dt.setValueAsQueryToken(theParams.get(0).getQualifier(), value);

View File

@ -184,7 +184,7 @@ public class SearchParameter extends BaseQueryParameter {
*/
@Override
public Object parse(List<QualifiedParamList> theString) throws InternalErrorException, InvalidRequestException {
return myParamBinder.parse(theString);
return myParamBinder.parse(getName(), theString);
}
public void setCompositeTypes(Class<? extends IQueryParameterType>[] theCompositeTypes) {

View File

@ -0,0 +1,53 @@
package ca.uhn.fhir.rest.method;
/*
* #%L
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
/**
* Enumerated type to represent the various allowable syntax for a search/query
* as described in the
* <a href="http://www.hl7.org/implement/standards/fhir/http.html#search">FHIR Specification Section 2.1.11</a>
*/
public enum SearchStyleEnum {
/**
* This is the most common (and generally the default) behaviour. Performs the search using the style:
* <br/>
* <code>GET [base]/[resource type]?[params]</code>
*/
GET,
/**
* This is the most common (and generally the default) behaviour. Performs the search using the style:
* <br/>
* <code>GET [base]/[resource type]/_search?[params]</code>
*/
GET_WITH_SEARCH,
/**
* This is the most common (and generally the default) behaviour. Performs the search using the style:
* <br/>
* <code>POST [base]/[resource type]/_search</code>
* <br/>
* and the params in a form encoded POST body.
*/
POST
}

View File

@ -42,12 +42,12 @@ final class StringBinder implements IParamBinder {
}
@Override
public Object parse(List<QualifiedParamList> theParams) throws InternalErrorException, InvalidRequestException {
public Object parse(String theName, List<QualifiedParamList> theParams) throws InternalErrorException, InvalidRequestException {
if (theParams.size() == 0 || theParams.get(0).size() == 0) {
return "";
}
if (theParams.size() > 1 || theParams.get(0).size() > 1) {
throw new InvalidRequestException("Multiple values detected");
throw new InvalidRequestException("Multiple values detected for non-repeatable parameter '" + theName + "'. This server is not configured to allow multiple (AND) values for this param.");
}
return theParams.get(0).get(0);

View File

@ -149,7 +149,7 @@ public class RestfulServer extends HttpServlet {
private void assertProviderIsValid(Object theNext) throws ConfigurationException {
if (Modifier.isPublic(theNext.getClass().getModifiers()) == false) {
throw new ConfigurationException("Can not use provider '" + theNext.getClass() + "' - Must be public");
throw new ConfigurationException("Can not use provider '" + theNext.getClass() + "' - Class ust be public");
}
}

View File

@ -13,6 +13,7 @@ import ca.uhn.fhir.model.dstu.resource.Organization;
import ca.uhn.fhir.model.dstu.resource.Patient;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.client.IGenericClient;
import ca.uhn.fhir.rest.method.SearchStyleEnum;
public class GenericClientExample {
@ -100,10 +101,25 @@ Bundle response = client.search()
.forResource(Patient.class)
.where(Patient.BIRTHDATE.beforeOrEquals().day("2011-01-01"))
.and(Patient.PROVIDER.hasChainedProperty(Organization.NAME.matches().value("Health")))
.andLogRequestAndResponse(true)
.execute();
//END SNIPPET: search
//START SNIPPET: searchOr
response = client.search()
.forResource(Patient.class)
.where(Patient.FAMILY.matches().values("Smith", "Smyth"))
.execute();
//END SNIPPET: searchOr
//START SNIPPET: searchAnd
response = client.search()
.forResource(Patient.class)
.where(Patient.ADDRESS.matches().values("Toronto"))
.where(Patient.ADDRESS.matches().values("Ontario"))
.where(Patient.ADDRESS.matches().values("Canada"))
.execute();
//END SNIPPET: searchAnd
//START SNIPPET: searchAdv
response = client.search()
.forResource(Patient.class)
@ -117,6 +133,15 @@ response = client.search()
.execute();
//END SNIPPET: searchAdv
//START SNIPPET: searchPost
response = client.search()
.forResource("Patient")
.where(Patient.NAME.matches().value("Tester"))
.usingStyle(SearchStyleEnum.POST)
.execute();
//END SNIPPET: searchPost
//START SNIPPET: searchComposite
response = client.search()
.forResource("Observation")

View File

@ -99,6 +99,30 @@
value="src/site/example/java/example/GenericClientExample.java" />
</macro>
<h4>Search - Multi-valued Parameters (ANY/OR)</h4>
<p>
To search for a set of possible values where <b>ANY</b> should be matched,
you can provide multiple values to a parameter, as shown in the example below.
This leads to a URL resembling <code>?family=Smith,Smyth</code>
</p>
<macro name="snippet">
<param name="id" value="searchOr" />
<param name="file"
value="src/site/example/java/example/GenericClientExample.java" />
</macro>
<h4>Search - Multi-valued Parameters (ALL/AND)</h4>
<p>
To search for a set of possible values where <b>ALL</b> should be matched,
you can provide multiple instances of a marameter, as shown in the example below.
This leads to a URL resembling <code>?address=Toronto&amp;address=Ontario&amp;address=Canada</code>
</p>
<macro name="snippet">
<param name="id" value="searchOr" />
<param name="file"
value="src/site/example/java/example/GenericClientExample.java" />
</macro>
<h4>Search - Paging</h4>
<p>
If the server supports paging results, the client has a page method
@ -123,7 +147,7 @@
value="src/site/example/java/example/GenericClientExample.java" />
</macro>
<h4>Search - Query Options</h4>
<h4>Search - Other Query Options</h4>
<p>
The fluent search also has methods for sorting, limiting, specifying
JSON encoding, etc.
@ -133,6 +157,19 @@
<param name="file"
value="src/site/example/java/example/GenericClientExample.java" />
</macro>
<h4>Search - Using HTTP POST or GET with _search</h4>
<p>
The FHIR specification allows several styles of search (HTTP POST, a GET with _search at the end of the URL, etc.)
The <code>usingStyle()</code> method controls which style to use. By default, GET style is used
unless the client detects that the request would result in a very long URL (over 8000 chars) in which
case the client automatically switches to POST.
</p>
<macro name="snippet">
<param name="id" value="searchPost" />
<param name="file"
value="src/site/example/java/example/GenericClientExample.java" />
</macro>
</subsection>

View File

@ -1,6 +1,6 @@
package ca.uhn.fhir.rest.server;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
import java.util.ArrayList;
import java.util.List;
@ -8,10 +8,14 @@ import java.util.concurrent.TimeUnit;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder;
@ -22,11 +26,16 @@ import org.junit.Test;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.dstu.composite.CodingDt;
import ca.uhn.fhir.model.dstu.resource.Observation;
import ca.uhn.fhir.model.dstu.resource.Patient;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
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.param.ReferenceParam;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.param.TokenOrListParam;
import ca.uhn.fhir.testutil.RandomServerPortProvider;
/**
@ -35,24 +44,9 @@ import ca.uhn.fhir.testutil.RandomServerPortProvider;
public class SearchTest {
private static CloseableHttpClient ourClient;
private static FhirContext ourCtx = new FhirContext();
private static int ourPort;
private static Server ourServer;
private static FhirContext ourCtx = new FhirContext();
@Test
public void testSearchById() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_id=aaa");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
Bundle bundle = ourCtx.newXmlParser().parseBundle(responseContent);
assertEquals(1, bundle.getEntries().size());
Patient p = bundle.getResources(Patient.class).get(0);
assertEquals("idaaa", p.getNameFirstRep().getFamilyAsSingleString());
assertEquals("IDAAA (identifier123)", bundle.getEntries().get(0).getTitle().getValue());
}
@Test
public void testOmitEmptyOptionalParam() throws Exception {
@ -63,13 +57,69 @@ public class SearchTest {
assertEquals(200, status.getStatusLine().getStatusCode());
Bundle bundle = ourCtx.newXmlParser().parseBundle(responseContent);
assertEquals(1, bundle.getEntries().size());
Patient p = bundle.getResources(Patient.class).get(0);
assertEquals(null, p.getNameFirstRep().getFamilyFirstRep().getValue());
}
@Test
public void testSearchById() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_id=aaa");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
Bundle bundle = ourCtx.newXmlParser().parseBundle(responseContent);
assertEquals(1, bundle.getEntries().size());
Patient p = bundle.getResources(Patient.class).get(0);
assertEquals("idaaa", p.getNameFirstRep().getFamilyAsSingleString());
assertEquals("IDAAA (identifier123)", bundle.getEntries().get(0).getTitle().getValue());
}
@Test
public void testSearchByPost() throws Exception {
HttpPost filePost = new HttpPost("http://localhost:" + ourPort + "/Patient/_search");
// add parameters to the post method
List <NameValuePair> parameters = new ArrayList <NameValuePair>();
parameters.add(new BasicNameValuePair("_id", "aaa"));
UrlEncodedFormEntity sendentity = new UrlEncodedFormEntity(parameters, "UTF-8");
filePost.setEntity(sendentity);
HttpResponse status = ourClient.execute(filePost);
String responseContent = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
Bundle bundle = ourCtx.newXmlParser().parseBundle(responseContent);
assertEquals(1, bundle.getEntries().size());
Patient p = bundle.getResources(Patient.class).get(0);
assertEquals("idaaa", p.getNameFirstRep().getFamilyAsSingleString());
assertEquals("IDAAA (identifier123)", bundle.getEntries().get(0).getTitle().getValue());
}
@Test
public void testSearchGetWithUnderscoreSearch() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort+"/Observation/_search?subject%3APatient=100&name=3141-9%2C8302-2%2C8287-5%2C39156-5");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
Bundle bundle = ourCtx.newXmlParser().parseBundle(responseContent);
assertEquals(1, bundle.getEntries().size());
Observation p = bundle.getResources(Observation.class).get(0);
assertEquals("Patient/100", p.getSubject().getReference().toString());
assertEquals(4, p.getName().getCoding().size());
assertEquals("3141-9", p.getName().getCoding().get(0).getCode().getValue());
assertEquals("8302-2", p.getName().getCoding().get(1).getCode().getValue());
}
@AfterClass
public static void afterClass() throws Exception {
ourServer.stop();
@ -85,8 +135,8 @@ public class SearchTest {
ServletHandler proxyHandler = new ServletHandler();
RestfulServer servlet = new RestfulServer();
servlet.getFhirContext().setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
servlet.setResourceProviders(patientProvider);
servlet.setResourceProviders(patientProvider, new DummyObservationResourceProvider());
ServletHolder servletHolder = new ServletHolder(servlet);
proxyHandler.addServletWithMapping(servletHolder, "/*");
ourServer.setHandler(proxyHandler);
@ -99,6 +149,28 @@ public class SearchTest {
}
public static class DummyObservationResourceProvider implements IResourceProvider{
@Override
public Class<? extends IResource> getResourceType() {
return Observation.class;
}
@Search
public Observation search(@RequiredParam(name="subject") ReferenceParam theSubject, @RequiredParam(name="name") TokenOrListParam theName) {
Observation o = new Observation();
o.setId("1");
o.getSubject().setReference(theSubject.getResourceType() + "/" + theSubject.getIdPart());
for (CodingDt next : theName.getListAsCodings()) {
o.getName().getCoding().add(next);
}
return o;
}
}
/**
* Created by dsotnikov on 2/25/2014.
*/
@ -111,8 +183,8 @@ public class SearchTest {
Patient patient = new Patient();
patient.setId("1");
patient.addIdentifier("system", "identifier123");
if (theParam!=null) {
patient.addName().addFamily("id"+theParam.getValue());
if (theParam != null) {
patient.addName().addFamily("id" + theParam.getValue());
}
retVal.add(patient);
return retVal;

View File

@ -1,13 +1,17 @@
package ca.uhn.fhir.rest.server;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.hamcrest.core.StringContains;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@ -19,7 +23,12 @@ import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.rest.annotation.RequiredParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.annotation.ServerBase;
import ca.uhn.fhir.rest.client.IGenericClient;
import ca.uhn.fhir.rest.client.api.IBasicClient;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.gclient.StringClientParam;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.testutil.RandomServerPortProvider;
public class ServerExtraParametersTest {
@ -58,6 +67,28 @@ public class ServerExtraParametersTest {
assertEquals("http://localhost:" + myPort, patientProvider.getServerBase());
}
@Test
public void testNonRepeatableParam() throws Exception {
MyServerBaseProvider patientProvider = new MyServerBaseProvider();
myServlet.setResourceProviders(patientProvider);
myServer.start();
FhirContext ctx = new FhirContext();
IGenericClient client = ctx.newRestfulGenericClient("http://localhost:" + myPort + "/");
client.registerInterceptor(new LoggingInterceptor(true));
try {
client.search().forResource("Patient").where(new StringClientParam("singleParam").matches().values(Arrays.asList("AA", "BB"))).execute();
fail();
} catch (InvalidRequestException e) {
assertThat(
e.getMessage(),
StringContains
.containsString("HTTP 400 Bad Request: Multiple values detected for non-repeatable parameter 'singleParam'. This server is not configured to allow multiple (AND/OR) values for this param."));
}
}
@After
public void after() throws Exception {
myServer.stop();
@ -75,6 +106,13 @@ public class ServerExtraParametersTest {
return Patient.class;
}
@Search
public List<Patient> searchSingleParam(@RequiredParam(name = "singleParam") StringParam theFooParam) {
Patient retVal = new Patient();
retVal.setId("1");
return Collections.singletonList(retVal);
}
@Search
public List<Patient> searchForPatients(@RequiredParam(name = "fooParam") StringDt theFooParam, @ServerBase String theServerBase) {
myServerBase = theServerBase;

View File

@ -3,7 +3,7 @@
<wb-resource deploy-path="/" source-path="/target/m2e-wtp/web-resources"/>
<wb-resource deploy-path="/" source-path="/src/main/webapp" tag="defaultRootSource"/>
<wb-resource deploy-path="/WEB-INF/classes" source-path="/src/main/java"/>
<dependent-module archiveName="hapi-fhir-base-0.5.jar" deploy-path="/WEB-INF/lib" handle="module:/resource/hapi-fhir-base/hapi-fhir-base">
<dependent-module archiveName="hapi-fhir-base-0.6-SNAPSHOT.jar" deploy-path="/WEB-INF/lib" handle="module:/resource/hapi-fhir-base/hapi-fhir-base">
<dependency-type>uses</dependency-type>
</dependent-module>
<dependent-module deploy-path="/" handle="module:/overlay/prj/hapi-fhir-testpage-overlay?includes=**/**&amp;excludes=META-INF/MANIFEST.MF">