diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/SortSpec.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/SortSpec.java index 00021b9d480..34d514d9ec4 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/SortSpec.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/SortSpec.java @@ -34,6 +34,7 @@ public class SortSpec { * Constructor */ public SortSpec() { + super(); } /** diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java index b00c074c4cd..c66f7a3e60a 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java @@ -70,6 +70,8 @@ import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.PreferReturnEnum; +import ca.uhn.fhir.rest.api.SortOrderEnum; +import ca.uhn.fhir.rest.api.SortSpec; import ca.uhn.fhir.rest.api.SummaryEnum; import ca.uhn.fhir.rest.client.api.IHttpClient; import ca.uhn.fhir.rest.client.api.IHttpRequest; @@ -131,6 +133,7 @@ import ca.uhn.fhir.rest.method.OperationMethodBinding; 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.SortParameter; import ca.uhn.fhir.rest.method.TransactionMethodBinding; import ca.uhn.fhir.rest.method.ValidateMethodBindingDstu1; import ca.uhn.fhir.rest.method.ValidateMethodBindingDstu2; @@ -233,8 +236,7 @@ public class GenericClient extends BaseClient implements IGenericClient { return delete(theType, new IdDt(theId)); } - private T doReadOrVRead(final Class theType, IIdType theId, boolean theVRead, ICallable theNotModifiedHandler, String theIfVersionMatches, Boolean thePrettyPrint, - SummaryEnum theSummary, EncodingEnum theEncoding, Set theSubsetElements) { + private T doReadOrVRead(final Class theType, IIdType theId, boolean theVRead, ICallable theNotModifiedHandler, String theIfVersionMatches, Boolean thePrettyPrint, SummaryEnum theSummary, EncodingEnum theEncoding, Set theSubsetElements) { String resName = toResourceName(theType); IIdType id = theId; if (!id.hasBaseUrl()) { @@ -264,7 +266,7 @@ public class GenericClient extends BaseClient implements IGenericClient { } boolean allowHtmlResponse = (theSummary == SummaryEnum.TEXT) || (theSummary == null && getSummary() == SummaryEnum.TEXT); - ResourceResponseHandler binding = new ResourceResponseHandler(theType, (Class)null, id, allowHtmlResponse); + ResourceResponseHandler binding = new ResourceResponseHandler(theType, (Class) null, id, allowHtmlResponse); if (theNotModifiedHandler == null) { return invokeClient(myContext, binding, invocation, theEncoding, thePrettyPrint, myLogRequestAndResponse, theSummary, theSubsetElements); @@ -596,44 +598,48 @@ public class GenericClient extends BaseClient implements IGenericClient { } } - private abstract class BaseClientExecutable, Y> implements IClientExecutable { - - private List> myPreferResponseTypes; - - public List> getPreferResponseTypes() { - return myPreferResponseTypes; - } - - public List> getPreferResponseTypes(Class theDefault) { - if (myPreferResponseTypes != null) { - return myPreferResponseTypes; + private static String validateAndEscapeConditionalUrl(String theSearchUrl) { + Validate.notBlank(theSearchUrl, "Conditional URL can not be blank/null"); + StringBuilder b = new StringBuilder(); + boolean haveHadQuestionMark = false; + for (int i = 0; i < theSearchUrl.length(); i++) { + char nextChar = theSearchUrl.charAt(i); + if (!haveHadQuestionMark) { + if (nextChar == '?') { + haveHadQuestionMark = true; + } else if (!Character.isLetter(nextChar)) { + throw new IllegalArgumentException("Conditional URL must be in the format \"[ResourceType]?[Params]\" and must not have a base URL - Found: " + theSearchUrl); + } + b.append(nextChar); } else { - return toTypeList(theDefault); + switch (nextChar) { + case '|': + case '?': + case '$': + case ':': + b.append(UrlUtil.escape(Character.toString(nextChar))); + break; + default: + b.append(nextChar); + break; + } } } + return b.toString(); + } - @SuppressWarnings("unchecked") - @Override - public T preferResponseType(Class theClass) { - myPreferResponseTypes = null; - if (theClass != null) { - myPreferResponseTypes = new ArrayList>(); - myPreferResponseTypes.add(theClass); - } - return (T) this; - } - - @SuppressWarnings("unchecked") - @Override - public T preferResponseTypes(List> theClass) { - myPreferResponseTypes = theClass; - return (T) this; - } + private abstract class BaseClientExecutable, Y> implements IClientExecutable { protected EncodingEnum myParamEncoding; + + private List> myPreferResponseTypes; + protected Boolean myPrettyPrint; + private boolean myQueryLogRequestAndResponse; + private HashSet mySubsetElements; + protected SummaryEnum mySummaryMode; @SuppressWarnings("unchecked") @@ -643,6 +649,17 @@ public class GenericClient extends BaseClient implements IGenericClient { return (T) this; } + @SuppressWarnings("unchecked") + @Override + public T elementsSubset(String... theElements) { + if (theElements != null && theElements.length > 0) { + mySubsetElements = new HashSet(Arrays.asList(theElements)); + } else { + mySubsetElements = null; + } + return (T) this; + } + @SuppressWarnings("unchecked") @Override public T encodedJson() { @@ -661,6 +678,18 @@ public class GenericClient extends BaseClient implements IGenericClient { return myParamEncoding; } + public List> getPreferResponseTypes() { + return myPreferResponseTypes; + } + + public List> getPreferResponseTypes(Class theDefault) { + if (myPreferResponseTypes != null) { + return myPreferResponseTypes; + } else { + return toTypeList(theDefault); + } + } + protected HashSet getSubsetElements() { return mySubsetElements; } @@ -692,19 +721,26 @@ public class GenericClient extends BaseClient implements IGenericClient { @SuppressWarnings("unchecked") @Override - public T prettyPrint() { - myPrettyPrint = true; + public T preferResponseType(Class theClass) { + myPreferResponseTypes = null; + if (theClass != null) { + myPreferResponseTypes = new ArrayList>(); + myPreferResponseTypes.add(theClass); + } return (T) this; } @SuppressWarnings("unchecked") @Override - public T elementsSubset(String... theElements) { - if (theElements != null && theElements.length > 0) { - mySubsetElements = new HashSet(Arrays.asList(theElements)); - } else { - mySubsetElements = null; - } + public T preferResponseTypes(List> theClass) { + myPreferResponseTypes = theClass; + return (T) this; + } + + @SuppressWarnings("unchecked") + @Override + public T prettyPrint() { + myPrettyPrint = true; return (T) this; } @@ -736,7 +772,7 @@ public class GenericClient extends BaseClient implements IGenericClient { } } - private class CreateInternal extends BaseClientExecutableimplements ICreate, ICreateTyped, ICreateWithQuery, ICreateWithQueryTyped { + private class CreateInternal extends BaseClientExecutable implements ICreate, ICreateTyped, ICreateWithQuery, ICreateWithQueryTyped { private CriterionList myCriterionList; private String myId; @@ -856,7 +892,7 @@ public class GenericClient extends BaseClient implements IGenericClient { } - private class DeleteInternal extends BaseClientExecutableimplements IDelete, IDeleteTyped, IDeleteWithQuery, IDeleteWithQueryTyped { + private class DeleteInternal extends BaseClientExecutable implements IDelete, IDeleteTyped, IDeleteWithQuery, IDeleteWithQueryTyped { private CriterionList myCriterionList; private IIdType myId; @@ -921,6 +957,14 @@ public class GenericClient extends BaseClient implements IGenericClient { return this; } + @Override + public IDeleteWithQuery resourceConditionalByType(Class theResourceType) { + Validate.notNull(theResourceType, "theResourceType can not be null"); + myCriterionList = new CriterionList(); + myResourceType = myContext.getResourceDefinition(theResourceType).getName(); + return this; + } + @Override public IDeleteWithQuery resourceConditionalByType(String theResourceType) { Validate.notBlank(theResourceType, "theResourceType can not be blank/null"); @@ -943,14 +987,6 @@ public class GenericClient extends BaseClient implements IGenericClient { myCriterionList.add((ICriterionInternal) theCriterion); return this; } - - @Override - public IDeleteWithQuery resourceConditionalByType(Class theResourceType) { - Validate.notNull(theResourceType, "theResourceType can not be null"); - myCriterionList = new CriterionList(); - myResourceType = myContext.getResourceDefinition(theResourceType).getName(); - return this; - } } @SuppressWarnings({ "rawtypes", "unchecked" }) @@ -977,7 +1013,7 @@ public class GenericClient extends BaseClient implements IGenericClient { } @SuppressWarnings({ "unchecked", "rawtypes" }) - private class GetPageInternal extends BaseClientExecutable, Object>implements IGetPageTyped { + private class GetPageInternal extends BaseClientExecutable, Object> implements IGetPageTyped { private Class myBundleType; private String myUrl; @@ -1007,7 +1043,7 @@ public class GenericClient extends BaseClient implements IGenericClient { } - private class GetTagsInternal extends BaseClientExecutableimplements IGetTags { + private class GetTagsInternal extends BaseClientExecutable implements IGetTags { private String myId; private String myResourceName; @@ -1403,9 +1439,59 @@ public class GenericClient extends BaseClient implements IGenericClient { private IIdType myId; private String myOperationName; private IBaseParameters myParameters; + private RuntimeResourceDefinition myParametersDef; private Class myType; private boolean myUseHttpGet; - private RuntimeResourceDefinition myParametersDef; + + @SuppressWarnings("unchecked") + private void addParam(String theName, IBase theValue) { + BaseRuntimeChildDefinition parameterChild = myParametersDef.getChildByName("parameter"); + BaseRuntimeElementCompositeDefinition parameterElem = (BaseRuntimeElementCompositeDefinition) parameterChild.getChildByName("parameter"); + + IBase parameter = parameterElem.newInstance(); + parameterChild.getMutator().addValue(myParameters, parameter); + + IPrimitiveType name = (IPrimitiveType) myContext.getElementDefinition("string").newInstance(); + name.setValue(theName); + parameterElem.getChildByName("name").getMutator().setValue(parameter, name); + + if (theValue instanceof IBaseDatatype) { + BaseRuntimeElementDefinition datatypeDef = myContext.getElementDefinition(theValue.getClass()); + if (datatypeDef instanceof IRuntimeDatatypeDefinition) { + Class profileOf = ((IRuntimeDatatypeDefinition) datatypeDef).getProfileOf(); + if (profileOf != null) { + datatypeDef = myContext.getElementDefinition(profileOf); + } + } + String childElementName = "value" + StringUtils.capitalize(datatypeDef.getName()); + BaseRuntimeChildDefinition childByName = parameterElem.getChildByName(childElementName); + childByName.getMutator().setValue(parameter, theValue); + } else if (theValue instanceof IBaseResource) { + parameterElem.getChildByName("resource").getMutator().setValue(parameter, theValue); + } else { + throw new IllegalArgumentException("Don't know how to handle parameter of type " + theValue.getClass()); + } + } + + private void addParam(String theName, IQueryParameterType theValue) { + IPrimitiveType stringType = ParametersUtil.createString(myContext, theValue.getValueAsQueryToken(myContext)); + addParam(theName, stringType); + } + + @Override + public IOperationUntypedWithInputAndPartialOutput andParameter(String theName, IBase theValue) { + Validate.notEmpty(theName, "theName must not be null"); + Validate.notNull(theValue, "theValue must not be null"); + addParam(theName, theValue); + return this; + } + + @Override + public IOperationUntypedWithInputAndPartialOutput andSearchParameter(String theName, IQueryParameterType theValue) { + addParam(theName, theValue); + + return this; + } @SuppressWarnings("unchecked") @Override @@ -1427,7 +1513,7 @@ public class GenericClient extends BaseClient implements IGenericClient { ResourceResponseHandler handler = new ResourceResponseHandler(); handler.setPreferResponseTypes(getPreferResponseTypes(myType)); - + Object retVal = invoke(null, handler, invocation); if (myContext.getResourceDefinition((IBaseResource) retVal).getName().equals("Parameters")) { return retVal; @@ -1486,21 +1572,12 @@ public class GenericClient extends BaseClient implements IGenericClient { throw new IllegalArgumentException("theOutputParameterType must refer to a HAPI FHIR Resource type: " + theOutputParameterType.getName()); } if (!"Parameters".equals(def.getName())) { - throw new IllegalArgumentException("theOutputParameterType must refer to a HAPI FHIR Resource type for a resource named " + "Parameters" + " - " + theOutputParameterType.getName() - + " is a resource named: " + def.getName()); + throw new IllegalArgumentException("theOutputParameterType must refer to a HAPI FHIR Resource type for a resource named " + "Parameters" + " - " + theOutputParameterType.getName() + " is a resource named: " + def.getName()); } myParameters = (IBaseParameters) def.newInstance(); return this; } - @SuppressWarnings({ "unchecked" }) - @Override - public IOperationUntypedWithInput withParameters(IBaseParameters theParameters) { - Validate.notNull(theParameters, "theParameters can not be null"); - myParameters = theParameters; - return this; - } - @SuppressWarnings("unchecked") @Override public IOperationUntypedWithInputAndPartialOutput withParameter(Class theParameterType, String theName, IBase theValue) { @@ -1516,48 +1593,11 @@ public class GenericClient extends BaseClient implements IGenericClient { return this; } - @SuppressWarnings("unchecked") - private void addParam(String theName, IBase theValue) { - BaseRuntimeChildDefinition parameterChild = myParametersDef.getChildByName("parameter"); - BaseRuntimeElementCompositeDefinition parameterElem = (BaseRuntimeElementCompositeDefinition) parameterChild.getChildByName("parameter"); - - IBase parameter = parameterElem.newInstance(); - parameterChild.getMutator().addValue(myParameters, parameter); - - IPrimitiveType name = (IPrimitiveType) myContext.getElementDefinition("string").newInstance(); - name.setValue(theName); - parameterElem.getChildByName("name").getMutator().setValue(parameter, name); - - if (theValue instanceof IBaseDatatype) { - BaseRuntimeElementDefinition datatypeDef = myContext.getElementDefinition(theValue.getClass()); - if (datatypeDef instanceof IRuntimeDatatypeDefinition) { - Class profileOf = ((IRuntimeDatatypeDefinition) datatypeDef).getProfileOf(); - if (profileOf != null) { - datatypeDef = myContext.getElementDefinition(profileOf); - } - } - String childElementName = "value" + StringUtils.capitalize(datatypeDef.getName()); - BaseRuntimeChildDefinition childByName = parameterElem.getChildByName(childElementName); - childByName.getMutator().setValue(parameter, theValue); - } else if (theValue instanceof IBaseResource) { - parameterElem.getChildByName("resource").getMutator().setValue(parameter, theValue); - } else { - throw new IllegalArgumentException("Don't know how to handle parameter of type " + theValue.getClass()); - } - } - + @SuppressWarnings({ "unchecked" }) @Override - public IOperationUntypedWithInputAndPartialOutput andParameter(String theName, IBase theValue) { - Validate.notEmpty(theName, "theName must not be null"); - Validate.notNull(theValue, "theValue must not be null"); - addParam(theName, theValue); - return this; - } - - @Override - public IOperationUntypedWithInputAndPartialOutput andSearchParameter(String theName, IQueryParameterType theValue) { - addParam(theName, theValue); - + public IOperationUntypedWithInput withParameters(IBaseParameters theParameters) { + Validate.notNull(theParameters, "theParameters can not be null"); + myParameters = theParameters; return this; } @@ -1576,18 +1616,12 @@ public class GenericClient extends BaseClient implements IGenericClient { return this; } - private void addParam(String theName, IQueryParameterType theValue) { - IPrimitiveType stringType = ParametersUtil.createString(myContext, theValue.getValueAsQueryToken(myContext)); - addParam(theName, stringType); - } - } private final class OperationOutcomeResponseHandler implements IClientResponseHandler { @Override - public BaseOperationOutcome invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) - throws BaseServerResponseException { + public BaseOperationOutcome invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) throws BaseServerResponseException { EncodingEnum respType = EncodingEnum.forContentType(theResponseMimeType); if (respType == null) { return null; @@ -1608,25 +1642,25 @@ public class GenericClient extends BaseClient implements IGenericClient { } private final class OutcomeResponseHandler implements IClientResponseHandler { - private final String myResourceName; private PreferReturnEnum myPrefer; + private final String myResourceName; + + private OutcomeResponseHandler(String theResourceName) { + myResourceName = theResourceName; + } private OutcomeResponseHandler(String theResourceName, PreferReturnEnum thePrefer) { this(theResourceName); myPrefer = thePrefer; } - private OutcomeResponseHandler(String theResourceName) { - myResourceName = theResourceName; - } - @Override public MethodOutcome invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) throws BaseServerResponseException { MethodOutcome response = MethodUtil.process2xxResponse(myContext, myResourceName, theResponseStatusCode, theResponseMimeType, theResponseReader, theHeaders); if (theResponseStatusCode == Constants.STATUS_HTTP_201_CREATED) { response.setCreated(true); } - + if (myPrefer == PreferReturnEnum.REPRESENTATION) { if (response.getResource() == null) { if (response.getId() != null && isNotBlank(response.getId().getValue()) && response.getId().hasBaseUrl()) { @@ -1636,7 +1670,7 @@ public class GenericClient extends BaseClient implements IGenericClient { } } } - + return response; } } @@ -1779,8 +1813,7 @@ public class GenericClient extends BaseClient implements IGenericClient { @SuppressWarnings("unchecked") @Override - public List invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) - throws BaseServerResponseException { + public List invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) throws BaseServerResponseException { if (myContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU1)) { Class bundleType = myContext.getResourceDefinition("Bundle").getImplementingClass(); ResourceResponseHandler handler = new ResourceResponseHandler((Class) bundleType); @@ -1795,7 +1828,7 @@ public class GenericClient extends BaseClient implements IGenericClient { } @SuppressWarnings({ "rawtypes", "unchecked" }) - private class SearchInternal extends BaseClientExecutable, Object>implements IQuery, IUntypedQuery { + private class SearchInternal extends BaseClientExecutable, Object> implements IQuery, IUntypedQuery { private String myCompartmentName; private CriterionList myCriterion = new CriterionList(); @@ -1809,10 +1842,10 @@ public class GenericClient extends BaseClient implements IGenericClient { private Class myReturnBundleType; private List myRevInclude = new ArrayList(); private SearchStyleEnum mySearchStyle; + private String mySearchUrl; private List mySecurity = new ArrayList(); private List mySort = new ArrayList(); private List myTags = new ArrayList(); - private String mySearchUrl; public SearchInternal() { myResourceType = null; @@ -1826,6 +1859,44 @@ public class GenericClient extends BaseClient implements IGenericClient { return this; } + @Override + public IQuery byUrl(String theSearchUrl) { + Validate.notBlank(theSearchUrl, "theSearchUrl must not be blank/null"); + + if (theSearchUrl.startsWith("http://") || theSearchUrl.startsWith("https://")) { + mySearchUrl = theSearchUrl; + int qIndex = mySearchUrl.indexOf('?'); + if (qIndex != -1) { + mySearchUrl = mySearchUrl.substring(0, qIndex) + validateAndEscapeConditionalUrl(mySearchUrl.substring(qIndex)); + } + } else { + String searchUrl = theSearchUrl; + if (searchUrl.startsWith("/")) { + searchUrl = searchUrl.substring(1); + } + if (!searchUrl.matches("[a-zA-Z]+($|\\?.*)")) { + throw new IllegalArgumentException("Search URL must be either a complete URL starting with http: or https:, or a relative FHIR URL in the form [ResourceType]?[Params]"); + } + int qIndex = searchUrl.indexOf('?'); + if (qIndex == -1) { + mySearchUrl = getUrlBase() + '/' + searchUrl; + } else { + mySearchUrl = getUrlBase() + '/' + validateAndEscapeConditionalUrl(searchUrl); + } + } + return this; + } + + @Override + public IQuery count(int theLimitTo) { + if (theLimitTo > 0) { + myParamLimit = theLimitTo; + } else { + myParamLimit = null; + } + return this; + } + @Override public IBase execute() { @@ -1861,8 +1932,27 @@ public class GenericClient extends BaseClient implements IGenericClient { addParam(params, Constants.PARAM_REVINCLUDE, next.getValue()); } - for (SortInternal next : mySort) { - addParam(params, next.getParamName(), next.getParamValue()); + if (myContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU2)) { + SortSpec rootSs = null; + SortSpec lastSs = null; + for (SortInternal next : mySort) { + SortSpec nextSortSpec = new SortSpec(); + nextSortSpec.setParamName(next.getParamValue()); + nextSortSpec.setOrder(next.getDirection()); + if (rootSs == null) { + rootSs = nextSortSpec; + } else { + lastSs.setChain(nextSortSpec); + } + lastSs = nextSortSpec; + } + if (rootSs != null) { + addParam(params, Constants.PARAM_SORT, SortParameter.createSortStringDstu3(rootSs)); + } + } else { + for (SortInternal next : mySort) { + addParam(params, next.getParamName(), next.getParamValue()); + } } if (myParamLimit != null) { @@ -1876,8 +1966,7 @@ public class GenericClient extends BaseClient implements IGenericClient { } if (myReturnBundleType == null && myContext.getVersion().getVersion().isRi()) { - throw new IllegalArgumentException("When using the client with HL7.org structures, you must specify " - + "the bundle return type for the client by adding \".returnBundle(org.hl7.fhir.instance.model.Bundle.class)\" to your search method call before the \".execute()\" method"); + throw new IllegalArgumentException("When using the client with HL7.org structures, you must specify " + "the bundle return type for the client by adding \".returnBundle(org.hl7.fhir.instance.model.Bundle.class)\" to your search method call before the \".execute()\" method"); } IClientResponseHandler binding; @@ -1934,16 +2023,6 @@ public class GenericClient extends BaseClient implements IGenericClient { return count(theLimitTo); } - @Override - public IQuery count(int theLimitTo) { - if (theLimitTo > 0) { - myParamLimit = theLimitTo; - } else { - myParamLimit = null; - } - return this; - } - @Override public IQuery returnBundle(Class theClass) { if (theClass == null) { @@ -2017,34 +2096,6 @@ public class GenericClient extends BaseClient implements IGenericClient { return this; } - @Override - public IQuery byUrl(String theSearchUrl) { - Validate.notBlank(theSearchUrl, "theSearchUrl must not be blank/null"); - - if (theSearchUrl.startsWith("http://") || theSearchUrl.startsWith("https://")) { - mySearchUrl = theSearchUrl; - int qIndex = mySearchUrl.indexOf('?'); - if (qIndex != -1) { - mySearchUrl = mySearchUrl.substring(0, qIndex) + validateAndEscapeConditionalUrl(mySearchUrl.substring(qIndex)); - } - } else { - String searchUrl = theSearchUrl; - if (searchUrl.startsWith("/")) { - searchUrl = searchUrl.substring(1); - } - if (!searchUrl.matches("[a-zA-Z]+($|\\?.*)")) { - throw new IllegalArgumentException("Search URL must be either a complete URL starting with http: or https:, or a relative FHIR URL in the form [ResourceType]?[Params]"); - } - int qIndex = searchUrl.indexOf('?'); - if (qIndex == -1) { - mySearchUrl = getUrlBase() + '/' + searchUrl; - } else { - mySearchUrl = getUrlBase() + '/' + validateAndEscapeConditionalUrl(searchUrl); - } - } - return this; - } - } @SuppressWarnings("rawtypes") @@ -2053,6 +2104,7 @@ public class GenericClient extends BaseClient implements IGenericClient { private SearchInternal myFor; private String myParamName; private String myParamValue; + private SortOrderEnum myDirection; public SortInternal(SearchInternal theFor) { myFor = theFor; @@ -2061,13 +2113,23 @@ public class GenericClient extends BaseClient implements IGenericClient { @Override public IQuery ascending(IParam theParam) { myParamName = Constants.PARAM_SORT_ASC; + myDirection = SortOrderEnum.ASC; myParamValue = theParam.getParamName(); return myFor; } + @Override + public IQuery ascending(String theParam) { + myParamName = Constants.PARAM_SORT_ASC; + myDirection = SortOrderEnum.ASC; + myParamValue = theParam; + return myFor; + } + @Override public IQuery defaultOrder(IParam theParam) { myParamName = Constants.PARAM_SORT; + myDirection = null; myParamValue = theParam.getParamName(); return myFor; } @@ -2075,10 +2137,23 @@ public class GenericClient extends BaseClient implements IGenericClient { @Override public IQuery descending(IParam theParam) { myParamName = Constants.PARAM_SORT_DESC; + myDirection = SortOrderEnum.DESC; myParamValue = theParam.getParamName(); return myFor; } + @Override + public IQuery descending(String theParam) { + myParamName = Constants.PARAM_SORT_DESC; + myDirection = SortOrderEnum.DESC; + myParamValue = theParam; + return myFor; + } + + public SortOrderEnum getDirection() { + return myDirection; + } + public String getParamName() { return myParamName; } @@ -2092,8 +2167,7 @@ public class GenericClient extends BaseClient implements IGenericClient { private final class StringResponseHandler implements IClientResponseHandler { @Override - public String invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) - throws IOException, BaseServerResponseException { + public String invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) throws IOException, BaseServerResponseException { return IOUtils.toString(theResponseReader); } } @@ -2111,7 +2185,7 @@ public class GenericClient extends BaseClient implements IGenericClient { } } - private final class TransactionExecutable extends BaseClientExecutable, T>implements ITransactionTyped { + private final class TransactionExecutable extends BaseClientExecutable, T> implements ITransactionTyped { private IBaseBundle myBaseBundle; private Bundle myBundle; @@ -2201,7 +2275,7 @@ public class GenericClient extends BaseClient implements IGenericClient { } - private class UpdateInternal extends BaseClientExecutableimplements IUpdate, IUpdateTyped, IUpdateExecutable, IUpdateWithQuery, IUpdateWithQueryTyped { + private class UpdateInternal extends BaseClientExecutable implements IUpdate, IUpdateTyped, IUpdateExecutable, IUpdateWithQuery, IUpdateWithQueryTyped { private CriterionList myCriterionList; private IIdType myId; @@ -2319,7 +2393,7 @@ public class GenericClient extends BaseClient implements IGenericClient { } - private class ValidateInternal extends BaseClientExecutableimplements IValidate, IValidateUntyped { + private class ValidateInternal extends BaseClientExecutable implements IValidate, IValidateUntyped { private IBaseResource myResource; @Override @@ -2361,34 +2435,4 @@ public class GenericClient extends BaseClient implements IGenericClient { } - private static String validateAndEscapeConditionalUrl(String theSearchUrl) { - Validate.notBlank(theSearchUrl, "Conditional URL can not be blank/null"); - StringBuilder b = new StringBuilder(); - boolean haveHadQuestionMark = false; - for (int i = 0; i < theSearchUrl.length(); i++) { - char nextChar = theSearchUrl.charAt(i); - if (!haveHadQuestionMark) { - if (nextChar == '?') { - haveHadQuestionMark = true; - } else if (!Character.isLetter(nextChar)) { - throw new IllegalArgumentException("Conditional URL must be in the format \"[ResourceType]?[Params]\" and must not have a base URL - Found: " + theSearchUrl); - } - b.append(nextChar); - } else { - switch (nextChar) { - case '|': - case '?': - case '$': - case ':': - b.append(UrlUtil.escape(Character.toString(nextChar))); - break; - default: - b.append(nextChar); - break; - } - } - } - return b.toString(); - } - } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/ISort.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/ISort.java index 7ffa3a92e93..ac20927e5a7 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/ISort.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/ISort.java @@ -22,10 +22,39 @@ package ca.uhn.fhir.rest.gclient; public interface ISort { + /** + * Sort ascending + */ IQuery ascending(IParam theParam); + /** + * Sort ascending + * + * @param theParam The param name, e.g. "address" + */ + IQuery ascending(String theParam); + + /** + * Sort by the default order. Note that as of STU3, there is no longer + * a concept of default order, only ascending and descending. This method + * technically implies "ascending" but it makes more sense to use + * {@link #ascending(IParam)} + */ IQuery defaultOrder(IParam theParam); - + + /** + * Sort descending + * + * @param A query param - Could be a constant such as Patient.ADDRESS or a custom + * param such as new StringClientParam("foo") + */ IQuery descending(IParam theParam); + + /** + * Sort ascending + * + * @param theParam The param name, e.g. "address" + */ + IQuery descending(String theParam); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/MethodUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/MethodUtil.java index b55cbafa022..ac78cc55be2 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/MethodUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/MethodUtil.java @@ -483,7 +483,7 @@ public class MethodUtil { } else if (nextAnnotation instanceof Count) { param = new CountParameter(); } else if (nextAnnotation instanceof Sort) { - param = new SortParameter(); + param = new SortParameter(theContext); } else if (nextAnnotation instanceof TransactionParam) { param = new TransactionParameter(theContext); } else if (nextAnnotation instanceof ConditionalUrlParam) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SortParameter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SortParameter.java index c21a944b67e..761f6a3a6c9 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SortParameter.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SortParameter.java @@ -26,40 +26,74 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.StringTokenizer; import org.hl7.fhir.instance.model.api.IBaseResource; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.rest.annotation.Sort; import ca.uhn.fhir.rest.api.SortOrderEnum; import ca.uhn.fhir.rest.api.SortSpec; +import ca.uhn.fhir.rest.param.ParameterUtil; import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; public class SortParameter implements IParameter { + private FhirContext myContext; + + public SortParameter(FhirContext theContext) { + myContext = theContext; + } + + @Override + public void initializeTypes(Method theMethod, Class> theOuterCollectionType, Class> theInnerCollectionType, Class theParameterType) { + if (theOuterCollectionType != null || theInnerCollectionType != null) { + throw new ConfigurationException("Method '" + theMethod.getName() + "' in type '" + theMethod.getDeclaringClass().getCanonicalName() + "' is annotated with @" + Sort.class.getName() + " but can not be of collection type"); + } + if (!theParameterType.equals(SortSpec.class)) { + throw new ConfigurationException("Method '" + theMethod.getName() + "' in type '" + theMethod.getDeclaringClass().getCanonicalName() + "' is annotated with @" + Sort.class.getName() + " but is an invalid type, must be: " + SortSpec.class.getCanonicalName()); + } + + } + @Override public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map> theTargetQueryArguments, IBaseResource theTargetResource) throws InternalErrorException { SortSpec ss = (SortSpec) theSourceClientArgument; - while (ss != null) { - String name; - if (ss.getOrder() == null) { - name = Constants.PARAM_SORT; - } else if (ss.getOrder() == SortOrderEnum.ASC) { - name = Constants.PARAM_SORT_ASC; - } else { - name = Constants.PARAM_SORT_DESC; + + if (myContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU2)) { + String string = createSortStringDstu3(ss); + if (string.length() > 0) { + if (!theTargetQueryArguments.containsKey(Constants.PARAM_SORT)) { + theTargetQueryArguments.put(Constants.PARAM_SORT, new ArrayList()); + } + theTargetQueryArguments.get(Constants.PARAM_SORT).add(string); } - if (ss.getParamName() != null) { - if (!theTargetQueryArguments.containsKey(name)) { - theTargetQueryArguments.put(name, new ArrayList()); + } else { + + while (ss != null) { + String name; + if (ss.getOrder() == null) { + name = Constants.PARAM_SORT; + } else if (ss.getOrder() == SortOrderEnum.ASC) { + name = Constants.PARAM_SORT_ASC; + } else { + name = Constants.PARAM_SORT_DESC; } - theTargetQueryArguments.get(name).add(ss.getParamName()); + + if (ss.getParamName() != null) { + if (!theTargetQueryArguments.containsKey(name)) { + theTargetQueryArguments.put(name, new ArrayList()); + } + + theTargetQueryArguments.get(name).add(ss.getParamName()); + } + ss = ss.getChain(); } - ss = ss.getChain(); } } @@ -89,17 +123,47 @@ public class SortParameter implements IParameter { String[] values = theRequest.getParameters().get(nextParamName); if (values != null) { + for (String nextValue : values) { - if (isNotBlank(nextValue)) { - SortSpec spec = new SortSpec(); - spec.setOrder(order); - spec.setParamName(nextValue); - if (innerSpec == null) { - outerSpec = spec; - innerSpec = spec; - } else { - innerSpec.setChain(spec); - innerSpec = spec; + + if (myContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU2) && order == null) { + StringTokenizer tok = new StringTokenizer(nextValue, ","); + while (tok.hasMoreTokens()) { + String next = tok.nextToken(); + if (isNotBlank(next) && !next.equals("-")) { + order = SortOrderEnum.ASC; + if (next.startsWith("-")) { + order = SortOrderEnum.DESC; + next = next.substring(1); + } + + SortSpec spec = new SortSpec(); + spec.setOrder(order); + spec.setParamName(next); + if (innerSpec == null) { + outerSpec = spec; + innerSpec = spec; + } else { + innerSpec.setChain(spec); + innerSpec = spec; + } + + } + } + + } else { + + if (isNotBlank(nextValue)) { + SortSpec spec = new SortSpec(); + spec.setOrder(order); + spec.setParamName(nextValue); + if (innerSpec == null) { + outerSpec = spec; + innerSpec = spec; + } else { + innerSpec.setChain(spec); + innerSpec = spec; + } } } } @@ -109,15 +173,25 @@ public class SortParameter implements IParameter { return outerSpec; } - @Override - public void initializeTypes(Method theMethod, Class> theOuterCollectionType, Class> theInnerCollectionType, Class theParameterType) { - if (theOuterCollectionType != null || theInnerCollectionType != null) { - throw new ConfigurationException("Method '" + theMethod.getName() + "' in type '" + theMethod.getDeclaringClass().getCanonicalName() + "' is annotated with @" + Sort.class.getName() + " but can not be of collection type"); - } - if (!theParameterType.equals(SortSpec.class)) { - throw new ConfigurationException("Method '" + theMethod.getName() + "' in type '" + theMethod.getDeclaringClass().getCanonicalName() + "' is annotated with @" + Sort.class.getName() + " but is an invalid type, must be: " + SortSpec.class.getCanonicalName()); + public static String createSortStringDstu3(SortSpec ss) { + StringBuilder val = new StringBuilder(); + while (ss != null) { + + if (isNotBlank(ss.getParamName())) { + if (val.length() > 0) { + val.append(','); + } + if (ss.getOrder() == SortOrderEnum.DESC) { + val.append('-'); + } + val.append(ParameterUtil.escape(ss.getParamName())); + } + + ss = ss.getChain(); } + String string = val.toString(); + return string; } } diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu3Test.java index 3c810b53384..900e3dce854 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu3Test.java @@ -24,6 +24,7 @@ import org.apache.http.message.BasicHeader; import org.apache.http.message.BasicStatusLine; import org.hl7.fhir.dstu3.model.Binary; import org.hl7.fhir.dstu3.model.Bundle; +import org.hl7.fhir.dstu3.model.Bundle.BundleType; import org.hl7.fhir.dstu3.model.Conformance; import org.hl7.fhir.dstu3.model.OperationOutcome; import org.hl7.fhir.dstu3.model.Parameters; @@ -607,6 +608,60 @@ public class GenericClientDstu3Test { } + /** + * See #371 + */ + @Test + public void testSortDstu3Test() throws Exception { + IParser p = ourCtx.newXmlParser(); + + Bundle b = new Bundle(); + b.setType(BundleType.SEARCHSET); + + final String respString = p.encodeResourceToString(b); + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer() { + @Override + public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable { + return new ReaderInputStream(new StringReader(respString), Charset.forName("UTF-8")); + } + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + int idx = 0; + + //@formatter:off + client + .search() + .forResource(Patient.class) + .sort().ascending("address") + .returnBundle(Bundle.class) + .execute(); + assertEquals("http://example.com/fhir/Patient?_sort=address", capt.getAllValues().get(idx++).getURI().toASCIIString()); + + client + .search() + .forResource(Patient.class) + .sort().descending("address") + .returnBundle(Bundle.class) + .execute(); + assertEquals("http://example.com/fhir/Patient?_sort=-address", capt.getAllValues().get(idx++).getURI().toASCIIString()); + + client + .search() + .forResource(Patient.class) + .sort().descending("address") + .sort().ascending("name") + .sort().descending(Patient.BIRTHDATE) + .returnBundle(Bundle.class) + .execute(); + assertEquals("http://example.com/fhir/Patient?_sort=-address%2Cname%2C-birthdate", capt.getAllValues().get(idx++).getURI().toASCIIString()); + //@formatter:on + } + @Test public void testUserAgentForConformance() throws Exception { IParser p = ourCtx.newXmlParser(); diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/client/SearchClientDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/client/SearchClientDstu3Test.java index 1d2fbb6bd0a..73b3d1fa2e8 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/client/SearchClientDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/client/SearchClientDstu3Test.java @@ -1,6 +1,6 @@ package ca.uhn.fhir.rest.client; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -33,6 +33,9 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.annotation.Count; import ca.uhn.fhir.rest.annotation.RequiredParam; import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.annotation.Sort; +import ca.uhn.fhir.rest.api.SortOrderEnum; +import ca.uhn.fhir.rest.api.SortSpec; import ca.uhn.fhir.rest.client.api.IRestfulClient; import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.server.Constants; @@ -91,6 +94,35 @@ public class SearchClientDstu3Test { assertEquals("http://localhost:8081/hapi-fhir/fhir/Location?_query=match&name=smith&_count=100", value.getURI().toString()); } + /** + * See #371 + */ + @Test + public void testSortForDstu3() throws Exception { + + final String response = createBundleWithSearchExtension(); + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(ourHttpClient.execute(capt.capture())).thenReturn(ourHttpResponse); + when(ourHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(ourHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(ourHttpResponse.getEntity().getContent()).thenAnswer(new Answer() { + @Override + public InputStream answer(InvocationOnMock theInvocation) throws Throwable { + return new ReaderInputStream(new StringReader(response), Charset.forName("UTF-8")); + } + }); + + ILocationClient client = ourCtx.newRestfulClient(ILocationClient.class, "http://localhost/fhir"); + + int idx = 0; + + client.search(new SortSpec("param1", SortOrderEnum.ASC)); + assertEquals("http://localhost/fhir/Bundle?_sort=param1", ((HttpGet) capt.getAllValues().get(idx++)).getURI().toString()); + + client.search(new SortSpec("param1", SortOrderEnum.ASC).setChain(new SortSpec("param2", SortOrderEnum.DESC))); + assertEquals("http://localhost/fhir/Bundle?_sort=param1%2C-param2", ((HttpGet) capt.getAllValues().get(idx++)).getURI().toString()); + } + /** * See #299 */ @@ -159,6 +191,9 @@ public class SearchClientDstu3Test { @Search(queryName = "match", type=Location.class) public Bundle getMatchesReturnBundle(final @RequiredParam(name = Location.SP_NAME) StringParam name, final @Count Integer count); + + @Search + public Bundle search(@Sort SortSpec theSort); } } diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/SearchSortDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/SearchSortDstu3Test.java new file mode 100644 index 00000000000..9cc7e983b64 --- /dev/null +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/SearchSortDstu3Test.java @@ -0,0 +1,138 @@ +package ca.uhn.fhir.rest.server; + +import static org.hamcrest.Matchers.stringContainsInOrder; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.io.IOUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.hl7.fhir.dstu3.model.HumanName; +import org.hl7.fhir.dstu3.model.Patient; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.annotation.Count; +import ca.uhn.fhir.rest.annotation.OptionalParam; +import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.annotation.Sort; +import ca.uhn.fhir.rest.api.SortOrderEnum; +import ca.uhn.fhir.rest.api.SortSpec; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.util.PortUtil; +import ca.uhn.fhir.util.TestUtil; + +public class SearchSortDstu3Test { + + private static CloseableHttpClient ourClient; + private static FhirContext ourCtx = FhirContext.forDstu3(); + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchSortDstu3Test.class); + private static int ourPort; + private static Server ourServer; + private static String ourLastMethod; + private static SortSpec ourLastSortSpec; + + @Before + public void before() { + ourLastMethod = null; + ourLastSortSpec = null; + } + + @Test + public void testSearch() throws Exception { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_sort=param1,-param2,param3,-param4"); + CloseableHttpResponse status = ourClient.execute(httpGet); + try { + String responseContent = IOUtils.toString(status.getEntity().getContent()); + ourLog.info(responseContent); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertEquals("search", ourLastMethod); + + assertEquals("param1", ourLastSortSpec.getParamName()); + assertEquals(SortOrderEnum.ASC, ourLastSortSpec.getOrder()); + + assertEquals("param2", ourLastSortSpec.getChain().getParamName()); + assertEquals(SortOrderEnum.DESC, ourLastSortSpec.getChain().getOrder()); + + assertEquals("param3", ourLastSortSpec.getChain().getChain().getParamName()); + assertEquals(SortOrderEnum.ASC, ourLastSortSpec.getChain().getChain().getOrder()); + + assertEquals("param4", ourLastSortSpec.getChain().getChain().getChain().getParamName()); + assertEquals(SortOrderEnum.DESC, ourLastSortSpec.getChain().getChain().getChain().getOrder()); + + } finally { + IOUtils.closeQuietly(status.getEntity().getContent()); + } + + } + + @AfterClass + public static void afterClassClearContext() throws Exception { + ourServer.stop(); + TestUtil.clearAllStaticFieldsForUnitTest(); + } + + @BeforeClass + public static void beforeClass() throws Exception { + ourPort = PortUtil.findFreePort(); + ourServer = new Server(ourPort); + + DummyPatientResourceProvider patientProvider = new DummyPatientResourceProvider(); + + ServletHandler proxyHandler = new ServletHandler(); + RestfulServer servlet = new RestfulServer(ourCtx); + servlet.setPagingProvider(new FifoMemoryPagingProvider(10)); + + servlet.setResourceProviders(patientProvider); + ServletHolder servletHolder = new ServletHolder(servlet); + proxyHandler.addServletWithMapping(servletHolder, "/*"); + ourServer.setHandler(proxyHandler); + ourServer.start(); + + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); + HttpClientBuilder builder = HttpClientBuilder.create(); + builder.setConnectionManager(connectionManager); + ourClient = builder.build(); + + } + + public static class DummyPatientResourceProvider implements IResourceProvider { + + @Override + public Class getResourceType() { + return Patient.class; + } + + //@formatter:off + @SuppressWarnings("rawtypes") + @Search() + public List search( + @Sort SortSpec theSortSpec + ) { + ourLastMethod = "search"; + ourLastSortSpec = theSortSpec; + ArrayList retVal = new ArrayList(); + for (int i = 1; i < 100; i++) { + retVal.add((Patient) new Patient().addName(new HumanName().addFamily("FAMILY")).setId("" + i)); + } + return retVal; + } + //@formatter:on + + } + +} diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 7df1d9dc142..a8f7edcf7a9 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -259,6 +259,10 @@ the spec says they should. Thanks to Jim Steel for reporting! + + Update STU3 client and server to use the new sort parameter style (param1,-param2,param). Thanks to GitHub user @euz1e4r for + reporting! +