From 09af35e5d8bd900542d968de64aaa1997e0e4319 Mon Sep 17 00:00:00 2001 From: jamesagnew Date: Mon, 8 Sep 2014 07:51:21 -0400 Subject: [PATCH 1/4] Site tweaks for release --- hapi-fhir-base/src/changes/changes.xml | 2 +- hapi-fhir-base/src/site/xdoc/index.xml | 8 ++++++++ .../.settings/org.eclipse.wst.common.component | 6 +++--- .../.settings/org.eclipse.wst.common.component | 4 ++-- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/hapi-fhir-base/src/changes/changes.xml b/hapi-fhir-base/src/changes/changes.xml index dc019742bbe..a9f7442a810 100644 --- a/hapi-fhir-base/src/changes/changes.xml +++ b/hapi-fhir-base/src/changes/changes.xml @@ -6,7 +6,7 @@ HAPI FHIR Changelog - + + + Add server interceptor framework, and new interceptor for logging incoming + requests. + + + Add server validation framework for validating resources against the FHIR schemas and schematrons + + + Tester UI created double _format and _pretty param entries in searches. Thanks to Gered King of University + Health Network for reporting! + + + Create method was incorrectly returning an HTTP 204 on sucessful completion, but + should be returning an HTTP 200 per the FHIR specification. Thanks to wanghaisheng + for reporting! + + + FHIR Tester UI now correctly sends UTF-8 charset in responses so that message payloads containing + non US-ASCII characters will correctly display in the browser + + + JSON parser was incorrectly encoding extensions on composite elements outside the element itself + (as is done correctly for non-composite elements) instead of inside of them. Thanks to David Hay of + Orion for reporting this! + + + Contained/included resource instances received by a client are now automatically + added to any ResourceReferenceDt instancea in other resources which reference them. + + + Add documentation on how to use eBay CORS Filter to support Cross Origin Resource + Sharing (CORS) to server. CORS support that was built in to the server itself has + been removed, as it did not work correctly (and was reinventing a wheel that others + have done a great job inventing). Thanks to Peter Bernhardt of Relay Health for all the assistance + in testing this! + + + IResource interface did not expose the getLanguage/setLanguage methods from BaseResource, + so the resource language was difficult to access. + + + JSON Parser now gives a more friendly error message if it tries to parse JSON with invalid use + of single quotes + + + Transaction server method is now allowed to return an OperationOutcome in addition to the + incoming resources. The public test server now does this in order to return status information + about the transaction processing. + + + Update method in the server can now flag (via a field on the MethodOutcome object being returned) + that the result was actually a creation, and Create method can indicate that it was actually an + update. This has no effect other than to switch between the HTTP 200 and HTTP 201 status codes on the + response, but this may be useful in some circumstances. + + + Annotation client search methods with a specific resource type (e.g. List<Patient> search()) + won't return any resources that aren't of the correct type that are received in a response + bundle (generally these are referenced resources, so they are populated in the reference fields instead). + Thanks to Tahura Chaudhry of University Health Network for the unit test! + + + Added narrative generator template for OperationOutcome resource + + + Date/time types did not correctly parse values in the format "yyyymmdd" (although the FHIR-defined format + is "yyyy-mm-dd" anyhow, and this is correctly handled). Thanks to Jeffrey Ting of Systems Made Simple + for reporting! + + + Server search method for an unnamed query gets called if the client requests a named query + with the same parameter list. Thanks to Neal Acharya of University Health Network for reporting! + + + Category header (for tags) is correctly read in client for "read" operation + + + Transaction method in server can now have parameter type Bundle instead of + List<IResource> + + + HAPI parsers now use field access to get/set values instead of method accessors and mutators. + This should give a small performance boost. + + + JSON parser encodes resource references incorrectly, using the name "resource" instead + of the name "reference" for the actual reference. Thanks to + Ricky Nguyen for reporting and tracking down the issue! + + + Rename NotImpementedException to NotImplementedException (to correct typo) + + + Server setUseBrowserFriendlyContentType setting also respected for errors (e.g. OperationOutcome with 4xx/5xx status) + + + Fix performance issue in date/time datatypes where pattern matchers were not static + + + Server now gives a more helpful error message if a @Read method has a search parameter (which is invalid, but + previously lead to a very unhelpful error message). Thanks to Tahura Chaudhry of UHN for reporting! + + + Resource of type "List" failed to parse from a bundle correctly. Thanks to David Hay of Orion Health + for reporting! + + + QuantityParam correctly encodes approximate (~) prefix to values + + + If a server defines a method with parameter "_id", incoming search requests for that method may + get delegated to the wrong method. Thanks to Neal Acharya for reporting! + + + SecurityEvent.Object structural element has been renamed to + SecurityEvent.ObjectElement to avoid conflicting names with the + java Object class. Thanks to Laurie Macdougall-Sookraj of UHN for + reporting! + + + Text/narrative blocks that were created with a non-empty + namespace prefix (e.g. <xhtml:div xmlns:xhtml="...">...</xhtml:div>) + failed to encode correctly (prefix was missing in encoded resource) + + + Resource references previously encoded their children (display and reference) + in the wrong order so references with both would fail schema validation. + + + SecurityEvent resource's enums now use friendly enum names instead of the unfriendly + numeric code values. Thanks to Laurie MacDougall-Sookraj of UHN for the + suggestion! + + + + + having multiple ways of accomplishing the same thing. This means that a number of existing classes + have been deprocated in favour of new naming schemes. +
]]> + All annotation-based clients and all server search method parameters are now named + (type)Param, for example: StringParam, TokenParam, etc. +
]]> + All generic/fluent client method parameters are now named + (type)ClientParam, for example: StringClientParam, TokenClientParam, etc. +
]]> + All renamed classes have been retained and deprocated, so this change should not cause any issues + for existing applications but those applications should be refactored to use the + new parameters when possible. +
+ + Allow server methods to return wildcard generic types (e.g. List<? extends IResource>) + + + Search parameters are not properly escaped and unescaped. E.g. for a token parameter such as + "&identifier=system|codepart1\|codepart2" + + + Add support for OPTIONS verb (which returns the server conformance statement) + + + Add support for CORS headers in server + + + Bump SLF4j dependency to latest version (1.7.7) + + + Add interceptor framework for clients (annotation based and generic), and add interceptors + for configurable logging, capturing requests and responses, and HTTP basic auth. + + + Transaction client invocations with XML encoding were using the wrong content type ("application/xml+fhir" instead + of the correct "application/atom+xml"). Thanks to David Hay of Orion Health for surfacing this one! + + + Bundle entries now support a link type of "search". Thanks to David Hay for the suggestion! + + + If a client receives a non 2xx response (e.g. HTTP 500) and the response body is a text/plain message or + an OperationOutcome resource, include the message in the exception message so that it will be + more conveniently displayed in logs and other places. Thanks to Neal Acharya for the suggestion! + + + Read invocations in the client now process the "Content-Location" header and use it to + populate the ID of the returned resource. Thanks to Neal Acharya for the suggestion! + + + Fix issue where vread invocations on server incorrectly get routed to instance history method if one is + defined. Thanks to Neal Acharya from UHN for surfacing this one! + + + Binary reads on a server not include the Content-Disposition header, to prevent HTML in binary + blobs from being used for nefarious purposes. See + FHIR Tracker Bug 3298]]> + for more information. + + + Support has been added for using an HTTP proxy for outgoing requests. + + + Fix: Primitive extensions declared against custom resource types + are encoded even if they have no value. Thanks to David Hay of Orion for + reporting this! + + + Fix: RESTful server deployed to a location where the URL to access it contained a + space (e.g. a WAR file with a space in the name) failed to work correctly. + Thanks to David Hay of Orion for reporting this! + +
+ + + BREAKING CHANGE:]]>: IdDt has been modified so that it + contains a partial or complete resource identity. Previously it contained + only the simple alphanumeric id of the resource (the part at the end of the "read" URL for + that resource) but it can now contain a complete URL or even a partial URL (e.g. "Patient/123") + and can optionally contain a version (e.g. "Patient/123/_history/456"). New methods have + been added to this datatype which provide just the numeric portion. See the JavaDoc + for more information. + + + API CHANGE:]]>: Most elements in the HAPI FHIR model contain + a getId() and setId() method. This method is confusing because it is only actually used + for IDREF elements (which are rare) but its name makes it easy to confuse with more + important identifiers. For this reason, these methods have been deprocated and replaced with + get/setElementSpecificId() methods. The old methods will be removed at some point. Resource + types are unchanged and retain their get/setId methods. + + + Allow use of QuantityDt as a service parameter to support the "quantity" type. Previously + QuantityDt did not implement IQueryParameterType so it was not valid, and there was no way to + support quantity search parameters on the server (e.g. Observation.value-quantity) + + + Introduce StringParameter type which can be used as a RESTful operation search parameter + type. StringParameter allows ":exact" matches to be specified in clients, and handled in servers. + + + Parsers (XML/JSON) now support deleted entries in bundles + + + Transaction method now supported in servers + + + Support for Binary resources added (in servers, clients, parsers, etc.) + + + Support for Query resources fixed (in parser) + + + Nested contained resources (e.g. encoding a resource with a contained resource that itself contains a resource) + now parse and encode correctly, meaning that all contained resources are placed in the "contained" element + of the root resource, and the parser looks in the root resource for all container levels when stitching + contained resources back together. + + + Server methods with @Include parameter would sometimes fail when no _include was actually + specified in query strings. + + + Client requests for IdentifierDt types (such as Patient.identifier) did not create the correct + query string if the system is null. + + + Add support for paging responses from RESTful servers. + + + Don't fail on narrative blocks in JSON resources with only an XML declaration but no content (these are + produced by the Health Intersections server) + + + Server now automatically compresses responses if the client indicates support + + + Server failed to support optional parameters when type is String and :exact qualifier is used + + + Read method in client correctly populated resource ID in returned object + + + Support added for deleted-entry by/name, by/email, and comment from Tombstones spec + + + + + + diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeChildChoiceDefinition.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeChildChoiceDefinition.java index 2d7d02377f6..0fb7e71dfc6 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeChildChoiceDefinition.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeChildChoiceDefinition.java @@ -107,6 +107,8 @@ public class RuntimeChildChoiceDefinition extends BaseRuntimeDeclaredChildDefini if (IResource.class.isAssignableFrom(next)) { myDatatypeToElementDefinition.put(ResourceReferenceDt.class, nextDef); + alternateElementName = getElementName() + "Resource"; + myDatatypeToElementName.put(ResourceReferenceDt.class, alternateElementName); } myDatatypeToElementDefinition.put(next, nextDef); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Search.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Search.java index 94d46003ef5..574d713ffc8 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Search.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Search.java @@ -28,6 +28,7 @@ import java.lang.annotation.Target; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.rest.client.api.IBasicClient; import ca.uhn.fhir.rest.client.api.IRestfulClient; +import ca.uhn.fhir.rest.server.IDynamicSearchResourceProvider; import ca.uhn.fhir.rest.server.IResourceProvider; @@ -76,5 +77,12 @@ public @interface Search { */ // NB: Read, Search (maybe others) share this annotation method, so update the javadocs everywhere Class type() default IResource.class; + + /** + * This is an experimental option - Use with caution + * + * @see IDynamicSearchResourceProvider + */ + boolean dynamic() default false; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseMethodBinding.java index 2d4a638c60c..0088f34ab6a 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseMethodBinding.java @@ -63,8 +63,10 @@ import ca.uhn.fhir.rest.server.BundleProviders; import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.EncodingEnum; import ca.uhn.fhir.rest.server.IBundleProvider; +import ca.uhn.fhir.rest.server.IDynamicSearchResourceProvider; import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.SearchParameterMap; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; @@ -82,7 +84,6 @@ public abstract class BaseMethodBinding implements IClientResponseHandler private FhirContext myContext; private Method myMethod; private List myParameters; - private Object myProvider; public BaseMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) { @@ -92,7 +93,7 @@ public abstract class BaseMethodBinding implements IClientResponseHandler myMethod = theMethod; myContext = theContext; myProvider = theProvider; - myParameters = MethodUtil.getResourceParameters(theMethod); + myParameters = MethodUtil.getResourceParameters(theMethod, theProvider); } public List> getAllowableParamAnnotations() { @@ -349,7 +350,12 @@ public abstract class BaseMethodBinding implements IClientResponseHandler if (read != null) { return new ReadMethodBinding(returnType, theMethod, theContext, theProvider); } else if (search != null) { - return new SearchMethodBinding(returnType, theMethod, theContext, theProvider); + if (search.dynamic()) { + IDynamicSearchResourceProvider provider = (IDynamicSearchResourceProvider) theProvider; + return new DynamicSearchMethodBinding(returnType, theMethod, theContext, provider); + } else { + return new SearchMethodBinding(returnType, theMethod, theContext, theProvider); + } } else if (conformance != null) { return new ConformanceMethodBinding(theMethod, theContext, theProvider); } else if (create != null) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/DynamicSearchMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/DynamicSearchMethodBinding.java new file mode 100644 index 00000000000..01973006410 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/DynamicSearchMethodBinding.java @@ -0,0 +1,164 @@ +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% + */ + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.model.api.IResource; +import ca.uhn.fhir.model.dstu.valueset.RestfulOperationSystemEnum; +import ca.uhn.fhir.model.dstu.valueset.RestfulOperationTypeEnum; +import ca.uhn.fhir.rest.client.BaseHttpClientInvocation; +import ca.uhn.fhir.rest.method.SearchMethodBinding.RequestType; +import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.rest.server.IBundleProvider; +import ca.uhn.fhir.rest.server.IDynamicSearchResourceProvider; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; + +public class DynamicSearchMethodBinding extends BaseResourceReturningMethodBinding { + + private IDynamicSearchResourceProvider myProvider; + private List mySearchParameters; + private HashSet myParamNames; + private Integer myIdParamIndex; + + public DynamicSearchMethodBinding(Class theReturnResourceType, Method theMethod, FhirContext theConetxt, IDynamicSearchResourceProvider theProvider) { + super(theReturnResourceType, theMethod, theConetxt, theProvider); + + myProvider = theProvider; + mySearchParameters = myProvider.getSearchParameters(); + + myParamNames = new HashSet(); + for (RuntimeSearchParam next : mySearchParameters) { + myParamNames.add(next.getName()); + } + + myIdParamIndex = MethodUtil.findIdParameterIndex(theMethod); + + } + + @Override + public List getParameters() { + List retVal = new ArrayList(super.getParameters()); + + for (RuntimeSearchParam next : mySearchParameters) { + + } + + return retVal; + } + + @Override + public ReturnTypeEnum getReturnType() { + return ReturnTypeEnum.BUNDLE; + } + + @Override + public IBundleProvider invokeServer(RequestDetails theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException { + if (myIdParamIndex != null) { + theMethodParams[myIdParamIndex] = theRequest.getId(); + } + + Object response = invokeServerMethod(theMethodParams); + return toResourceList(response); + } + + @Override + public RestfulOperationTypeEnum getResourceOperationType() { + return RestfulOperationTypeEnum.SEARCH_TYPE; + } + + @Override + public RestfulOperationSystemEnum getSystemOperationType() { + return null; + } + + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(DynamicSearchMethodBinding.class); + + @Override + public boolean incomingServerRequestMatchesMethod(Request theRequest) { + if (!theRequest.getResourceName().equals(getResourceName())) { + ourLog.trace("Method {} doesn't match because resource name {} != {}", getMethod().getName(), theRequest.getResourceName(), getResourceName()); + return false; + } + if (theRequest.getId() != null && myIdParamIndex == null) { + ourLog.trace("Method {} doesn't match because ID is not null: {}", theRequest.getId()); + return false; + } + if (theRequest.getRequestType() == RequestType.GET && theRequest.getOperation() != null && !Constants.PARAM_SEARCH.equals(theRequest.getOperation())) { + ourLog.trace("Method {} doesn't match because request type is GET but operation is not null: {}", theRequest.getId(), theRequest.getOperation()); + return false; + } + if (theRequest.getRequestType() == RequestType.POST && !Constants.PARAM_SEARCH.equals(theRequest.getOperation())) { + ourLog.trace("Method {} doesn't match because request type is POST but operation is not _search: {}", theRequest.getId(), theRequest.getOperation()); + return false; + } + if (theRequest.getRequestType() != RequestType.GET && theRequest.getRequestType() != RequestType.POST) { + ourLog.trace("Method {} doesn't match because request type is {}", getMethod()); + return false; + } + if (theRequest.getCompartmentName() != null) { + ourLog.trace("Method {} doesn't match because it is for compartment {}", new Object[] { getMethod(), theRequest.getCompartmentName() }); + return false; + } + + for (String next : theRequest.getParameters().keySet()) { + if (next.charAt(0) == '_') { + continue; + } + String nextQualified = next; + int colonIndex = next.indexOf(':'); + int dotIndex = next.indexOf('.'); + if (colonIndex != -1 || dotIndex != -1) { + int index; + if (colonIndex != -1 && dotIndex != -1) { + index = Math.min(colonIndex, dotIndex); + } else { + index = (colonIndex != -1) ? colonIndex : dotIndex; + } + next = next.substring(0, index); + } + if (!myParamNames.contains(next)) { + ourLog.trace("Method {} doesn't match because has parameter {}", new Object[] { getMethod(), nextQualified }); + return false; + } + } + + return true; + } + + @Override + public BaseHttpClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException { + // there should be no way to call this.... + throw new UnsupportedOperationException("Dynamic search methods are only used for server implementations"); + } + + public Collection getSearchParams() { + return mySearchParameters; + } + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/DynamicSearchParameter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/DynamicSearchParameter.java new file mode 100644 index 00000000000..076b08007dc --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/DynamicSearchParameter.java @@ -0,0 +1,169 @@ +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% + */ + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.rest.client.BaseHttpClientInvocation; +import ca.uhn.fhir.rest.param.CompositeOrListParam; +import ca.uhn.fhir.rest.param.DateOrListParam; +import ca.uhn.fhir.rest.param.DateParam; +import ca.uhn.fhir.rest.param.NumberOrListParam; +import ca.uhn.fhir.rest.param.NumberParam; +import ca.uhn.fhir.rest.param.QuantityOrListParam; +import ca.uhn.fhir.rest.param.QuantityParam; +import ca.uhn.fhir.rest.param.ReferenceOrListParam; +import ca.uhn.fhir.rest.param.ReferenceParam; +import ca.uhn.fhir.rest.param.StringOrListParam; +import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.rest.param.TokenOrListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.rest.server.IDynamicSearchResourceProvider; +import ca.uhn.fhir.rest.server.SearchParameterMap; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; + +public class DynamicSearchParameter implements IParameter { + + private Map myNameToParam = new HashMap(); + + public DynamicSearchParameter(IDynamicSearchResourceProvider theProvider) { + for (RuntimeSearchParam next : theProvider.getSearchParameters()) { + myNameToParam.put(next.getName(), next); + } + } + + @Override + public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map> theTargetQueryArguments, BaseHttpClientInvocation theClientInvocation) throws InternalErrorException { + throw new UnsupportedOperationException("Dynamic search is not supported in client mode (use fluent client for dynamic-like searches)"); + } + + @SuppressWarnings("unchecked") + @Override + public Object translateQueryParametersIntoServerArgument(Request theRequest, Object theRequestContents) throws InternalErrorException, InvalidRequestException { + SearchParameterMap retVal = new SearchParameterMap(); + + for (String next : theRequest.getParameters().keySet()) { + String qualifier = null; + String qualifiedParamName = next; + RuntimeSearchParam param = myNameToParam.get(next); + if (param == null) { + int colonIndex = next.indexOf(':'); + int dotIndex = next.indexOf('.'); + if (colonIndex != -1 || dotIndex != -1) { + int index; + if (colonIndex != -1 && dotIndex != -1) { + index = Math.min(colonIndex, dotIndex); + } else { + index = (colonIndex != -1) ? colonIndex : dotIndex; + } + qualifier = next.substring(index); + next = next.substring(0, index); + param = myNameToParam.get(next); + } + } + + if (param != null) { + + for (String nextValue : theRequest.getParameters().get(qualifiedParamName)) { + QualifiedParamList paramList = QualifiedParamList.splitQueryStringByCommasIgnoreEscape(qualifier, nextValue); + + switch (param.getParamType()) { + case COMPOSITE: + Class left = toParamType(param.getCompositeOf().get(0)); + Class right = toParamType(param.getCompositeOf().get(0)); + @SuppressWarnings({ "rawtypes" }) + CompositeOrListParam compositeOrListParam = new CompositeOrListParam(left, right); + compositeOrListParam.setValuesAsQueryTokens(paramList); + retVal.add(next, compositeOrListParam); + break; + case DATE: + DateOrListParam dateOrListParam = new DateOrListParam(); + dateOrListParam.setValuesAsQueryTokens(paramList); + retVal.add(next, dateOrListParam); + break; + case NUMBER: + NumberOrListParam numberOrListParam = new NumberOrListParam(); + numberOrListParam.setValuesAsQueryTokens(paramList); + retVal.add(next, numberOrListParam); + break; + case QUANTITY: + QuantityOrListParam quantityOrListParam = new QuantityOrListParam(); + quantityOrListParam.setValuesAsQueryTokens(paramList); + retVal.add(next, quantityOrListParam); + break; + case REFERENCE: + ReferenceOrListParam referenceOrListParam = new ReferenceOrListParam(); + referenceOrListParam.setValuesAsQueryTokens(paramList); + retVal.add(next, referenceOrListParam); + break; + case STRING: + StringOrListParam stringOrListParam = new StringOrListParam(); + stringOrListParam.setValuesAsQueryTokens(paramList); + retVal.add(next, stringOrListParam); + break; + case TOKEN: + TokenOrListParam tokenOrListParam = new TokenOrListParam(); + tokenOrListParam.setValuesAsQueryTokens(paramList); + retVal.add(next, tokenOrListParam); + break; + } + } + } + } + + return retVal; + } + + private Class toParamType(RuntimeSearchParam theRuntimeSearchParam) { + switch (theRuntimeSearchParam.getParamType()) { + case COMPOSITE: + throw new IllegalStateException("Composite subtype"); + case DATE: + return DateParam.class; + case NUMBER: + return NumberParam.class; + case QUANTITY: + return QuantityParam.class; + case REFERENCE: + return ReferenceParam.class; + case STRING: + return StringParam.class; + case TOKEN: + return TokenParam.class; + default: + throw new IllegalStateException("null type"); + } + } + + @Override + public void initializeTypes(Method theMethod, Class> theOuterCollectionType, Class> theInnerCollectionType, Class theParameterType) { + // nothing + } + +} 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 530b23f8ec1..e05f7818b75 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 @@ -1,7 +1,6 @@ package ca.uhn.fhir.rest.method; -import static org.apache.commons.lang3.StringUtils.isBlank; -import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static org.apache.commons.lang3.StringUtils.*; import java.io.IOException; import java.io.PushbackReader; @@ -45,6 +44,7 @@ import ca.uhn.fhir.rest.annotation.IncludeParam; import ca.uhn.fhir.rest.annotation.OptionalParam; import ca.uhn.fhir.rest.annotation.RequiredParam; import ca.uhn.fhir.rest.annotation.ResourceParam; +import ca.uhn.fhir.rest.annotation.Search; import ca.uhn.fhir.rest.annotation.ServerBase; import ca.uhn.fhir.rest.annotation.Since; import ca.uhn.fhir.rest.annotation.Sort; @@ -57,6 +57,8 @@ import ca.uhn.fhir.rest.param.CollectionBinder; import ca.uhn.fhir.rest.param.ResourceParameter; import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.EncodingEnum; +import ca.uhn.fhir.rest.server.IDynamicSearchResourceProvider; +import ca.uhn.fhir.rest.server.SearchParameterMap; import ca.uhn.fhir.util.ReflectionUtil; /* @@ -91,7 +93,7 @@ public class MethodUtil { urlBuilder.append(resourceName); urlBuilder.append('/'); urlBuilder.append(theId.getIdPart()); - + HttpPutClientInvocation retVal; String urlExtension = urlBuilder.toString(); if (StringUtils.isBlank(theResourceBody)) { @@ -109,10 +111,10 @@ public class MethodUtil { urlBuilder.append(versionId); retVal.addHeader(Constants.HEADER_CONTENT_LOCATION, urlBuilder.toString()); } - } - + } + addTagsToPostOrPut(theResource, retVal); - + return retVal; } @@ -135,7 +137,7 @@ public class MethodUtil { String headerValue = clHeaders.get(0); resource.getId().setValue(headerValue); } - + List categoryHeaders = theHeaders.get(Constants.HEADER_CATEGORY_LC); if (categoryHeaders != null && categoryHeaders.size() > 0 && StringUtils.isNotBlank(categoryHeaders.get(0))) { TagList tagList = new TagList(); @@ -223,7 +225,6 @@ public class MethodUtil { } - static void addTagsToPostOrPut(IResource resource, BaseHttpClientInvocation retVal) { TagList list = (TagList) resource.getResourceMetadata().get(ResourceMetadataKeyEnum.TAG_LIST); if (list != null) { @@ -265,8 +266,7 @@ public class MethodUtil { return new HttpGetClientInvocation("metadata"); } - public static MethodOutcome process2xxResponse(FhirContext theContext, String theResourceName, int theResponseStatusCode, String theResponseMimeType, Reader theResponseReader, - Map> theHeaders) { + public static MethodOutcome process2xxResponse(FhirContext theContext, String theResourceName, int theResponseStatusCode, String theResponseMimeType, Reader theResponseReader, Map> theHeaders) { List locationHeaders = new ArrayList(); List lh = theHeaders.get(Constants.HEADER_LOCATION_LC); if (lh != null) { @@ -378,7 +378,7 @@ public class MethodUtil { } @SuppressWarnings("unchecked") - public static List getResourceParameters(Method theMethod) { + public static List getResourceParameters(Method theMethod, Object theProvider) { List parameters = new ArrayList(); Class[] parameterTypes = theMethod.getParameterTypes(); @@ -389,7 +389,14 @@ public class MethodUtil { Class parameterType = parameterTypes[paramIndex]; Class> outerCollectionType = null; Class> innerCollectionType = null; - if (TagList.class.isAssignableFrom(parameterType)) { + if (SearchParameterMap.class.equals(parameterType)) { + if (theProvider instanceof IDynamicSearchResourceProvider) { + Search searchAnnotation = theMethod.getAnnotation(Search.class); + if (searchAnnotation != null && searchAnnotation.dynamic()) { + param = new DynamicSearchParameter((IDynamicSearchResourceProvider) theProvider); + } + } + } else if (TagList.class.isAssignableFrom(parameterType)) { // TagList is handled directly within the method bindings param = new NullParameter(); } else { @@ -403,8 +410,7 @@ public class MethodUtil { parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, paramIndex); } if (Collection.class.isAssignableFrom(parameterType)) { - throw new ConfigurationException("Argument #" + paramIndex + " of Method '" + theMethod.getName() + "' in type '" + theMethod.getDeclaringClass().getCanonicalName() - + "' is of an invalid generic type (can not be a collection of a collection of a collection)"); + throw new ConfigurationException("Argument #" + paramIndex + " of Method '" + theMethod.getName() + "' in type '" + theMethod.getDeclaringClass().getCanonicalName() + "' is of an invalid generic type (can not be a collection of a collection of a collection)"); } } if (parameterType.equals(HttpServletRequest.class) || parameterType.equals(ServletRequest.class)) { @@ -443,19 +449,16 @@ public class MethodUtil { instantiableCollectionType = null; specType = String.class; } else if ((parameterType != Include.class && parameterType != PathSpecification.class) || innerCollectionType == null || outerCollectionType != null) { - throw new ConfigurationException("Method '" + theMethod.getName() + "' is annotated with @" + IncludeParam.class.getSimpleName() + " but has a type other than Collection<" - + Include.class.getSimpleName() + ">"); + throw new ConfigurationException("Method '" + theMethod.getName() + "' is annotated with @" + IncludeParam.class.getSimpleName() + " but has a type other than Collection<" + Include.class.getSimpleName() + ">"); } else { - instantiableCollectionType = (Class>) CollectionBinder.getInstantiableCollectionType(innerCollectionType, "Method '" + theMethod.getName() - + "'"); + instantiableCollectionType = (Class>) CollectionBinder.getInstantiableCollectionType(innerCollectionType, "Method '" + theMethod.getName() + "'"); specType = parameterType; } param = new IncludeParameter((IncludeParam) nextAnnotation, instantiableCollectionType, specType); } else if (nextAnnotation instanceof ResourceParam) { if (!IResource.class.isAssignableFrom(parameterType)) { - throw new ConfigurationException("Method '" + theMethod.getName() + "' is annotated with @" + ResourceParam.class.getSimpleName() - + " but has a type that is not an implemtation of " + IResource.class.getCanonicalName()); + throw new ConfigurationException("Method '" + theMethod.getName() + "' is annotated with @" + ResourceParam.class.getSimpleName() + " but has a type that is not an implemtation of " + IResource.class.getCanonicalName()); } param = new ResourceParameter((Class) parameterType); } else if (nextAnnotation instanceof IdParam || nextAnnotation instanceof VersionIdParam) { @@ -479,8 +482,8 @@ public class MethodUtil { } if (param == null) { - throw new ConfigurationException("Parameter #" + ((paramIndex + 1)) + "/" + (parameterTypes.length) + " of method '" + theMethod.getName() + "' on type '" - + theMethod.getDeclaringClass().getCanonicalName() + "' has no recognized FHIR interface parameter annotations. Don't know how to handle this parameter"); + throw new ConfigurationException("Parameter #" + ((paramIndex + 1)) + "/" + (parameterTypes.length) + " of method '" + theMethod.getName() + "' on type '" + theMethod.getDeclaringClass().getCanonicalName() + + "' has no recognized FHIR interface parameter annotations. Don't know how to handle this parameter"); } param.initializeTypes(theMethod, outerCollectionType, innerCollectionType, parameterType); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/QualifiedParamList.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/QualifiedParamList.java index a717eeb07fa..e2e930270a3 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/QualifiedParamList.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/QualifiedParamList.java @@ -23,7 +23,6 @@ package ca.uhn.fhir.rest.method; import java.util.ArrayList; import java.util.StringTokenizer; -import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.model.api.IQueryParameterOr; import ca.uhn.fhir.model.api.IQueryParameterType; @@ -41,7 +40,7 @@ public class QualifiedParamList extends ArrayList { super(theCapacity); } - public QualifiedParamList(FhirContext theContext, IQueryParameterOr theNextOr) { + public QualifiedParamList(IQueryParameterOr theNextOr) { for (IQueryParameterType next : theNextOr.getValuesAsQueryTokens()) { if (myQualifier==null) { myQualifier=next.getQueryParameterQualifier(); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SearchMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SearchMethodBinding.java index 46bb981d96b..f93ed34f963 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SearchMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SearchMethodBinding.java @@ -60,7 +60,6 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding { private Class myDeclaredResourceType; private String myDescription; private Integer myIdParamIndex; - private String myQueryName; @SuppressWarnings("unchecked") @@ -118,40 +117,6 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding { } - public static QualifierDetails extractQualifiersFromParameterName(String theParamName) { - QualifierDetails retVal = new QualifierDetails(); - if (theParamName == null || theParamName.length() == 0) { - return retVal; - } - - int dotIdx = -1; - int colonIdx = -1; - for (int idx = 0; idx < theParamName.length(); idx++) { - char nextChar = theParamName.charAt(idx); - if (nextChar == '.' && dotIdx == -1) { - dotIdx = idx; - } else if (nextChar == ':' && colonIdx == -1) { - colonIdx = idx; - } - } - - if (dotIdx != -1 && colonIdx != -1) { - if (dotIdx < colonIdx) { - retVal.setDotQualifier(theParamName.substring(dotIdx, colonIdx)); - retVal.setColonQualifier(theParamName.substring(colonIdx)); - } else { - retVal.setColonQualifier(theParamName.substring(colonIdx, dotIdx)); - retVal.setDotQualifier(theParamName.substring(dotIdx)); - } - } else if (dotIdx != -1) { - retVal.setDotQualifier(theParamName.substring(dotIdx)); - } else if (colonIdx != -1) { - retVal.setColonQualifier(theParamName.substring(colonIdx)); - } - - return retVal; - } - public Class getDeclaredResourceType() { return myDeclaredResourceType; } @@ -322,6 +287,15 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding { } + public void setResourceType(Class resourceType) { + this.myDeclaredResourceType = resourceType; + } + + @Override + public String toString() { + return getMethod().toString(); + } + private List processWhitelistAndBlacklist(List theQualifiedNames, Set theQualifierWhitelist, Set theQualifierBlacklist) { if (theQualifierWhitelist == null && theQualifierBlacklist == null) { return theQualifiedNames; @@ -337,10 +311,6 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding { return retVal; } - public void setResourceType(Class resourceType) { - this.myDeclaredResourceType = resourceType; - } - public static BaseHttpClientInvocation createSearchInvocation(FhirContext theContext, String theResourceName, Map> theParameters, IdDt theId, String theCompartmentName, SearchStyleEnum theSearchStyle) { SearchStyleEnum searchStyle = theSearchStyle; @@ -402,6 +372,40 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding { return invocation; } + public static QualifierDetails extractQualifiersFromParameterName(String theParamName) { + QualifierDetails retVal = new QualifierDetails(); + if (theParamName == null || theParamName.length() == 0) { + return retVal; + } + + int dotIdx = -1; + int colonIdx = -1; + for (int idx = 0; idx < theParamName.length(); idx++) { + char nextChar = theParamName.charAt(idx); + if (nextChar == '.' && dotIdx == -1) { + dotIdx = idx; + } else if (nextChar == ':' && colonIdx == -1) { + colonIdx = idx; + } + } + + if (dotIdx != -1 && colonIdx != -1) { + if (dotIdx < colonIdx) { + retVal.setDotQualifier(theParamName.substring(dotIdx, colonIdx)); + retVal.setColonQualifier(theParamName.substring(colonIdx)); + } else { + retVal.setColonQualifier(theParamName.substring(colonIdx, dotIdx)); + retVal.setDotQualifier(theParamName.substring(dotIdx)); + } + } else if (dotIdx != -1) { + retVal.setDotQualifier(theParamName.substring(dotIdx)); + } else if (colonIdx != -1) { + retVal.setColonQualifier(theParamName.substring(colonIdx)); + } + + return retVal; + } + public static class QualifierDetails { private String myColonQualifier; @@ -467,11 +471,6 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding { } - @Override - public String toString() { - return getMethod().toString(); - } - public static enum RequestType { DELETE, GET, OPTIONS, POST, PUT } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SearchParameter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SearchParameter.java index d20fa0a4ae3..448b8d1cd9e 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SearchParameter.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SearchParameter.java @@ -156,7 +156,7 @@ public class SearchParameter extends BaseQueryParameter { List> val = myParamBinder.encode(theContext, theObject); for (IQueryParameterOr nextOr : val) { - retVal.add(new QualifiedParamList(theContext, nextOr)); + retVal.add(new QualifiedParamList(nextOr)); } return retVal; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseAndListParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseAndListParam.java index 53b3099372c..48a6c11b066 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseAndListParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseAndListParam.java @@ -28,10 +28,14 @@ import ca.uhn.fhir.model.api.IQueryParameterOr; import ca.uhn.fhir.rest.method.QualifiedParamList; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -abstract class BaseAndListParam> implements IQueryParameterAnd { +public abstract class BaseAndListParam> implements IQueryParameterAnd { private List myValues=new ArrayList(); + public void addValue(T theValue) { + myValues.add(theValue); + } + @Override public void setValuesAsQueryTokens(List theParameters) throws InvalidRequestException { myValues.clear(); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseOrListParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseOrListParam.java index 0bfd75e090d..3c31bbb2a73 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseOrListParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseOrListParam.java @@ -25,6 +25,7 @@ import java.util.List; import org.apache.commons.lang3.Validate; +import ca.uhn.fhir.model.api.IQueryParameterAnd; import ca.uhn.fhir.model.api.IQueryParameterOr; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.rest.method.QualifiedParamList; @@ -33,10 +34,10 @@ abstract class BaseOrListParam implements IQueryP private List myList=new ArrayList(); - public void addToken(T theParam) { - Validate.notNull(theParam,"Param can not be null"); - myList.add(theParam); - } +// public void addToken(T theParam) { +// Validate.notNull(theParam,"Param can not be null"); +// myList.add(theParam); +// } @Override public void setValuesAsQueryTokens(QualifiedParamList theParameters) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/CodingListParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/CodingListParam.java index 249a7653a41..5191f3e8fcb 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/CodingListParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/CodingListParam.java @@ -117,7 +117,7 @@ public class CodingListParam implements IQueryParameterOr, } @Override - public void setValuesAsQueryTokens(QualifiedParamList theParameters) { + public void setValuesAsQueryTokens(QualifiedParamList theParameters) { getCodings().clear(); for (String string : theParameters) { CodingDt dt = new CodingDt(); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/CompositeOrListParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/CompositeOrListParam.java index 888462d19a0..0b9e7729336 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/CompositeOrListParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/CompositeOrListParam.java @@ -34,6 +34,14 @@ public class CompositeOrListParam getLeftType() { + return myLeftType; + } + + public Class getRightType() { + return myRightType; + } + @Override CompositeParam newInstance() { return new CompositeParam(myLeftType, myRightType); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateParam.java index a192772c2c0..c85eae736c6 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateParam.java @@ -154,7 +154,7 @@ public class DateParam extends DateTimeDt implements IQueryParameterType, IQuery } @Override - public void setValuesAsQueryTokens(QualifiedParamList theParameters) { + public void setValuesAsQueryTokens(QualifiedParamList theParameters) { myBase.setMissing(null); myComparator = null; setValueAsString(null); @@ -164,6 +164,7 @@ public class DateParam extends DateTimeDt implements IQueryParameterType, IQuery } else if (theParameters.size() > 1) { throw new InvalidRequestException("This server does not support multi-valued dates for this paramater: " + theParameters); } + } @Override diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/IdentifierListParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/IdentifierListParam.java index c8528146949..a12648e940b 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/IdentifierListParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/IdentifierListParam.java @@ -59,7 +59,7 @@ public class IdentifierListParam implements IQueryParameterOr getSearchParameters(); + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/SearchParameterMap.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/SearchParameterMap.java new file mode 100644 index 00000000000..a9bd7aa9b22 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/SearchParameterMap.java @@ -0,0 +1,129 @@ +package ca.uhn.fhir.rest.server; + +/* + * #%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% + */ + +import java.util.HashMap; + +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.rest.param.BaseAndListParam; +import ca.uhn.fhir.rest.param.CompositeAndListParam; +import ca.uhn.fhir.rest.param.CompositeOrListParam; +import ca.uhn.fhir.rest.param.DateAndListParam; +import ca.uhn.fhir.rest.param.DateOrListParam; +import ca.uhn.fhir.rest.param.NumberAndListParam; +import ca.uhn.fhir.rest.param.NumberOrListParam; +import ca.uhn.fhir.rest.param.QuantityAndListParam; +import ca.uhn.fhir.rest.param.QuantityOrListParam; +import ca.uhn.fhir.rest.param.ReferenceAndListParam; +import ca.uhn.fhir.rest.param.ReferenceOrListParam; +import ca.uhn.fhir.rest.param.StringAndListParam; +import ca.uhn.fhir.rest.param.StringOrListParam; +import ca.uhn.fhir.rest.param.TokenAndListParam; +import ca.uhn.fhir.rest.param.TokenOrListParam; + +public class SearchParameterMap extends HashMap> { + + private static final long serialVersionUID = 1L; + + public void add(String theName, CompositeOrListParam theCompositeOrListParam) { + @SuppressWarnings("unchecked") + CompositeAndListParam andList = (CompositeAndListParam) get(theName); + if (andList == null) { + andList = new CompositeAndListParam(theCompositeOrListParam.getLeftType(), theCompositeOrListParam.getRightType()); + put(theName, andList); + } + andList.addValue(theCompositeOrListParam); + } + + public void add(String theName, DateOrListParam theOrListParam) { + DateAndListParam andList = (DateAndListParam) get(theName); + if (andList == null) { + andList = new DateAndListParam(); + put(theName, andList); + } + andList.addValue(theOrListParam); + } + + public void add(String theName, NumberOrListParam theOrListParam) { + NumberAndListParam andList = (NumberAndListParam) get(theName); + if (andList == null) { + andList = new NumberAndListParam(); + put(theName, andList); + } + andList.addValue(theOrListParam); + } + + public void add(String theName, TokenOrListParam theOrListParam) { + TokenAndListParam andList = (TokenAndListParam) get(theName); + if (andList == null) { + andList = new TokenAndListParam(); + put(theName, andList); + } + andList.addValue(theOrListParam); + } + + public void add(String theName, StringOrListParam theOrListParam) { + StringAndListParam andList = (StringAndListParam) get(theName); + if (andList == null) { + andList = new StringAndListParam(); + put(theName, andList); + } + andList.addValue(theOrListParam); + } + + public void add(String theName, QuantityOrListParam theOrListParam) { + QuantityAndListParam andList = (QuantityAndListParam) get(theName); + if (andList == null) { + andList = new QuantityAndListParam(); + put(theName, andList); + } + andList.addValue(theOrListParam); + } + + public void add(String theName, ReferenceOrListParam theOrListParam) { + ReferenceAndListParam andList = (ReferenceAndListParam) get(theName); + if (andList == null) { + andList = new ReferenceAndListParam(); + put(theName, andList); + } + andList.addValue(theOrListParam); + } + + // public void add(String theName, IQueryParameterOr theOr) { + // if (theOr == null) { + // return; + // } + // + // switch (theOr.getClass()) { + // + // } + // + // if (!containsKey(theName)) { + // put(theName, new ArrayList>()); + // } + // + // StringAndListParam + // + // IQueryParameterAnd> and = get(theName); + // and.add(theOr); + // } + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/provider/ServerConformanceProvider.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/provider/ServerConformanceProvider.java index 602b7d7b37c..ab7a318fe37 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/provider/ServerConformanceProvider.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/provider/ServerConformanceProvider.java @@ -49,6 +49,7 @@ import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.StringDt; import ca.uhn.fhir.rest.annotation.Metadata; import ca.uhn.fhir.rest.method.BaseMethodBinding; +import ca.uhn.fhir.rest.method.DynamicSearchMethodBinding; import ca.uhn.fhir.rest.method.IParameter; import ca.uhn.fhir.rest.method.SearchMethodBinding; import ca.uhn.fhir.rest.method.SearchParameter; @@ -61,11 +62,10 @@ import ca.uhn.fhir.util.ExtensionConstants; * Server FHIR Provider which serves the conformance statement for a RESTful server implementation * *

- * Note: This class is safe to extend, but it is important to note that the same instance of - * {@link Conformance} is always returned unless {@link #setCache(boolean)} is called with a value - * of false. This means that if you are adding anything to the returned - * conformance instance on each call you should call setCache(false) in - * your provider constructor. + * Note: This class is safe to extend, but it is important to note that the same instance of {@link Conformance} is + * always returned unless {@link #setCache(boolean)} is called with a value of false. This means that if + * you are adding anything to the returned conformance instance on each call you should call + * setCache(false) in your provider constructor. *

*/ public class ServerConformanceProvider { @@ -142,91 +142,9 @@ public class ServerConformanceProvider { } if (nextMethodBinding instanceof SearchMethodBinding) { - SearchMethodBinding searchMethodBinding = (SearchMethodBinding) nextMethodBinding; - includes.addAll(searchMethodBinding.getIncludes()); - - List params = searchMethodBinding.getParameters(); - List searchParameters = new ArrayList(); - for (IParameter nextParameter : params) { - if ((nextParameter instanceof SearchParameter)) { - searchParameters.add((SearchParameter) nextParameter); - } - } - Collections.sort(searchParameters, new Comparator() { - @Override - public int compare(SearchParameter theO1, SearchParameter theO2) { - if (theO1.isRequired() == theO2.isRequired()) { - return theO1.getName().compareTo(theO2.getName()); - } - if (theO1.isRequired()) { - return -1; - } - return 1; - } - }); - if (searchParameters.isEmpty()) { - continue; - } - boolean allOptional = searchParameters.get(0).isRequired() == false; - - RestQuery query = null; - if (!allOptional) { - query = rest.addQuery(); - query.getDocumentation().setValue(searchMethodBinding.getDescription()); - query.addUndeclaredExtension(false, ExtensionConstants.QUERY_RETURN_TYPE, new CodeDt(resourceName)); - for (String nextInclude : searchMethodBinding.getIncludes()) { - query.addUndeclaredExtension(false, ExtensionConstants.QUERY_ALLOWED_INCLUDE, new StringDt(nextInclude)); - } - } - - for (SearchParameter nextParameter : searchParameters) { - - String nextParamName = nextParameter.getName(); - -// String chain = null; - String nextParamUnchainedName = nextParamName; - if (nextParamName.contains(".")) { -// chain = nextParamName.substring(nextParamName.indexOf('.') + 1); - nextParamUnchainedName = nextParamName.substring(0, nextParamName.indexOf('.')); - } - - String nextParamDescription = nextParameter.getDescription(); - - /* - * If the parameter has no description, default to the one from the resource - */ - if (StringUtils.isBlank(nextParamDescription)) { - RuntimeSearchParam paramDef = def.getSearchParam(nextParamUnchainedName); - if (paramDef != null) { - nextParamDescription = paramDef.getDescription(); - } - } - - RestResourceSearchParam param; - if (query == null) { - param = resource.addSearchParam(); - } else { - param = query.addParameter(); - param.addUndeclaredExtension(false, ExtensionConstants.PARAM_IS_REQUIRED, new BooleanDt(nextParameter.isRequired())); - } - - param.setName(nextParamName); -// if (StringUtils.isNotBlank(chain)) { -// param.addChain(chain); -// } - param.setDocumentation(nextParamDescription); - param.setType(nextParameter.getParamType()); - for (Class nextTarget : nextParameter.getDeclaredTypes()) { - RuntimeResourceDefinition targetDef = myRestfulServer.getFhirContext().getResourceDefinition(nextTarget); - if (targetDef != null) { - ResourceTypeEnum code = ResourceTypeEnum.VALUESET_BINDER.fromCodeString(targetDef.getName()); - if (code != null) { - param.addTarget(code); - } - } - } - - } + handleSearchMethodBinding(rest, resource, resourceName, def, includes, (SearchMethodBinding) nextMethodBinding); + } else if (nextMethodBinding instanceof DynamicSearchMethodBinding) { + handleDynamicSearchMethodBinding(resource, def, includes, (DynamicSearchMethodBinding) nextMethodBinding); } Collections.sort(resource.getOperation(), new Comparator() { @@ -259,6 +177,149 @@ public class ServerConformanceProvider { return retVal; } + private void handleSearchMethodBinding(Rest rest, RestResource resource, String resourceName, RuntimeResourceDefinition def, TreeSet includes, SearchMethodBinding searchMethodBinding) { + includes.addAll(searchMethodBinding.getIncludes()); + + List params = searchMethodBinding.getParameters(); + List searchParameters = new ArrayList(); + for (IParameter nextParameter : params) { + if ((nextParameter instanceof SearchParameter)) { + searchParameters.add((SearchParameter) nextParameter); + } + } + sortSearchParameters(searchParameters); + if (!searchParameters.isEmpty()) { + boolean allOptional = searchParameters.get(0).isRequired() == false; + + RestQuery query = null; + if (!allOptional) { + query = rest.addQuery(); + query.getDocumentation().setValue(searchMethodBinding.getDescription()); + query.addUndeclaredExtension(false, ExtensionConstants.QUERY_RETURN_TYPE, new CodeDt(resourceName)); + for (String nextInclude : searchMethodBinding.getIncludes()) { + query.addUndeclaredExtension(false, ExtensionConstants.QUERY_ALLOWED_INCLUDE, new StringDt(nextInclude)); + } + } + + for (SearchParameter nextParameter : searchParameters) { + + String nextParamName = nextParameter.getName(); + + // String chain = null; + String nextParamUnchainedName = nextParamName; + if (nextParamName.contains(".")) { + // chain = nextParamName.substring(nextParamName.indexOf('.') + 1); + nextParamUnchainedName = nextParamName.substring(0, nextParamName.indexOf('.')); + } + + String nextParamDescription = nextParameter.getDescription(); + + /* + * If the parameter has no description, default to the one from the resource + */ + if (StringUtils.isBlank(nextParamDescription)) { + RuntimeSearchParam paramDef = def.getSearchParam(nextParamUnchainedName); + if (paramDef != null) { + nextParamDescription = paramDef.getDescription(); + } + } + + RestResourceSearchParam param; + if (query == null) { + param = resource.addSearchParam(); + } else { + param = query.addParameter(); + param.addUndeclaredExtension(false, ExtensionConstants.PARAM_IS_REQUIRED, new BooleanDt(nextParameter.isRequired())); + } + + param.setName(nextParamName); + // if (StringUtils.isNotBlank(chain)) { + // param.addChain(chain); + // } + param.setDocumentation(nextParamDescription); + param.setType(nextParameter.getParamType()); + for (Class nextTarget : nextParameter.getDeclaredTypes()) { + RuntimeResourceDefinition targetDef = myRestfulServer.getFhirContext().getResourceDefinition(nextTarget); + if (targetDef != null) { + ResourceTypeEnum code = ResourceTypeEnum.VALUESET_BINDER.fromCodeString(targetDef.getName()); + if (code != null) { + param.addTarget(code); + } + } + } + } + } + } + + private void handleDynamicSearchMethodBinding(RestResource resource, RuntimeResourceDefinition def, TreeSet includes, DynamicSearchMethodBinding searchMethodBinding) { + includes.addAll(searchMethodBinding.getIncludes()); + + List searchParameters = new ArrayList(); + searchParameters.addAll(searchMethodBinding.getSearchParams()); + sortRuntimeSearchParameters(searchParameters); + + if (!searchParameters.isEmpty()) { + + for (RuntimeSearchParam nextParameter : searchParameters) { + + String nextParamName = nextParameter.getName(); + + // String chain = null; + String nextParamUnchainedName = nextParamName; + if (nextParamName.contains(".")) { + // chain = nextParamName.substring(nextParamName.indexOf('.') + 1); + nextParamUnchainedName = nextParamName.substring(0, nextParamName.indexOf('.')); + } + + String nextParamDescription = nextParameter.getDescription(); + + /* + * If the parameter has no description, default to the one from the resource + */ + if (StringUtils.isBlank(nextParamDescription)) { + RuntimeSearchParam paramDef = def.getSearchParam(nextParamUnchainedName); + if (paramDef != null) { + nextParamDescription = paramDef.getDescription(); + } + } + + RestResourceSearchParam param; + param = resource.addSearchParam(); + + param.setName(nextParamName); + // if (StringUtils.isNotBlank(chain)) { + // param.addChain(chain); + // } + param.setDocumentation(nextParamDescription); + param.setType(nextParameter.getParamType()); + } + } + } + + private void sortRuntimeSearchParameters(List searchParameters) { + Collections.sort(searchParameters, new Comparator() { + @Override + public int compare(RuntimeSearchParam theO1, RuntimeSearchParam theO2) { + return theO1.getName().compareTo(theO2.getName()); + } + }); + } + + private void sortSearchParameters(List searchParameters) { + Collections.sort(searchParameters, new Comparator() { + @Override + public int compare(SearchParameter theO1, SearchParameter theO2) { + if (theO1.isRequired() == theO2.isRequired()) { + return theO1.getName().compareTo(theO2.getName()); + } + if (theO1.isRequired()) { + return -1; + } + return 1; + } + }); + } + /** * Sets the cache property (default is true). If set to true, the same response will be returned for each * invocation. diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/PortUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/PortUtil.java index b35bf434741..5e979ce4d17 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/PortUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/PortUtil.java @@ -1,4 +1,25 @@ -package ca.uhn.fhir.util; +package ca.uhn.fhir.util; + +/* + * #%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% + */ + import java.io.IOException; import java.net.ServerSocket; diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorTest.java index 05b4131a903..6fbd6a2a56d 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorTest.java @@ -82,7 +82,8 @@ public class DefaultThymeleafNarrativeGeneratorTest { enc.setType(EncounterTypeEnum.ANNUAL_DIABETES_MELLITUS_SCREENING); String title = gen.generateTitle(enc); - assertEquals("1234567 / ADMS / ambulatory / Tue Jan 02 11:11:00 EST 2001 - ?", title); + title = title.replaceAll("00 [A-Z]+ 2001", "00 TZ 2001"); // account for whatever time zone + assertEquals("1234567 / ADMS / ambulatory / Tue Jan 02 11:11:00 TZ 2001 - ?", title); ourLog.info(title); } diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/parser/XmlParserTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/parser/XmlParserTest.java index bcd61f37e13..4f40b82c41f 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/parser/XmlParserTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/parser/XmlParserTest.java @@ -45,6 +45,7 @@ import ca.uhn.fhir.model.dstu.resource.ListResource; import ca.uhn.fhir.model.dstu.resource.Observation; import ca.uhn.fhir.model.dstu.resource.Organization; import ca.uhn.fhir.model.dstu.resource.Patient; +import ca.uhn.fhir.model.dstu.resource.Profile; import ca.uhn.fhir.model.dstu.resource.Query; import ca.uhn.fhir.model.dstu.resource.Specimen; import ca.uhn.fhir.model.dstu.resource.ValueSet; @@ -67,6 +68,16 @@ public class XmlParserTest { private static FhirContext ourCtx; private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(XmlParserTest.class); + @Test + public void testEncodeProfile() { + + Profile p = new Profile(); + p.getStructureFirstRep().getElementFirstRep().getDefinition().getBinding().setReference(new ResourceReferenceDt("ValudSet/123")); + + String encoded = ourCtx.newXmlParser().encodeResourceToString(p); + ourLog.info(encoded); + } + @Test public void testEncodeBinaryResource() { diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/DynamicSearchTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/DynamicSearchTest.java new file mode 100644 index 00000000000..8077e824ffb --- /dev/null +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/DynamicSearchTest.java @@ -0,0 +1,197 @@ +package ca.uhn.fhir.rest.server; + +import static org.apache.commons.lang.StringUtils.*; +import static org.junit.Assert.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpResponse; +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.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.model.api.Bundle; +import ca.uhn.fhir.model.api.IResource; +import ca.uhn.fhir.model.dstu.resource.Conformance; +import ca.uhn.fhir.model.dstu.resource.Patient; +import ca.uhn.fhir.model.dstu.valueset.SearchParamTypeEnum; +import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; +import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.param.StringAndListParam; +import ca.uhn.fhir.rest.param.StringOrListParam; +import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.util.PortUtil; + +/** + * Created by dsotnikov on 2/25/2014. + */ +public class DynamicSearchTest { + + private static CloseableHttpClient ourClient; + private static FhirContext ourCtx = new FhirContext(); + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(DynamicSearchTest.class); + private static int ourPort; + + private static Server ourServer; + + private static SearchParameterMap ourLastSearchParams; + + @Before + public void before() { + ourLastSearchParams = null; + } + + @Test + public void testSearchOneStringParam() throws Exception { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?param1=param1value"); + 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()); + + assertEquals(1, ourLastSearchParams.size()); + StringAndListParam andList =(StringAndListParam) ourLastSearchParams.get("param1"); + assertEquals(1,andList.getValuesAsQueryTokens().size()); + StringOrListParam orList = andList.getValuesAsQueryTokens().get(0); + assertEquals(1,orList.getValuesAsQueryTokens().size()); + StringParam param1 = orList.getValuesAsQueryTokens().get(0); + assertEquals("param1value", param1.getValue()); + } + + + @Test + public void testSearchOneStringParamWithOr() throws Exception { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?param1=param1value,param1value2"); + 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()); + + assertEquals(1, ourLastSearchParams.size()); + StringAndListParam andList =(StringAndListParam) ourLastSearchParams.get("param1"); + assertEquals(1,andList.getValuesAsQueryTokens().size()); + StringOrListParam orList = andList.getValuesAsQueryTokens().get(0); + assertEquals(2,orList.getValuesAsQueryTokens().size()); + StringParam param1 = orList.getValuesAsQueryTokens().get(0); + assertEquals("param1value", param1.getValue()); + StringParam param1b = orList.getValuesAsQueryTokens().get(1); + assertEquals("param1value2", param1b.getValue()); + } + + @Test + public void testSearchOneStringParamWithAnd() throws Exception { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?param1=param1value¶m1=param1value2"); + 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()); + + assertEquals(1, ourLastSearchParams.size()); + StringAndListParam andList =(StringAndListParam) ourLastSearchParams.get("param1"); + assertEquals(2,andList.getValuesAsQueryTokens().size()); + StringOrListParam orList = andList.getValuesAsQueryTokens().get(0); + assertEquals(1,orList.getValuesAsQueryTokens().size()); + StringParam param1 = orList.getValuesAsQueryTokens().get(0); + assertEquals("param1value", param1.getValue()); + + orList = andList.getValuesAsQueryTokens().get(1); + assertEquals(1,orList.getValuesAsQueryTokens().size()); + StringParam param1b = orList.getValuesAsQueryTokens().get(0); + assertEquals("param1value2", param1b.getValue()); + } + + @Test + public void testConformance() throws Exception { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/metadata?_pretty=true"); + HttpResponse status = ourClient.execute(httpGet); + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + assertEquals(200, status.getStatusLine().getStatusCode()); + Conformance conf = ourCtx.newXmlParser().parseResource(Conformance.class,responseContent); + + ourLog.info(responseContent); + } + + + @AfterClass + public static void afterClass() throws Exception { + ourServer.stop(); + } + + @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(); + servlet.getFhirContext().setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator()); + + 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(); + + } + + /** + * Created by dsotnikov on 2/25/2014. + */ + public static class DummyPatientResourceProvider implements IDynamicSearchResourceProvider { + + @Override + public Class getResourceType() { + return Patient.class; + } + + @Override + public List getSearchParameters() { + ArrayList retVal = new ArrayList(); + retVal.add(new RuntimeSearchParam("param1", "This is the first parameter", "Patient.param1", SearchParamTypeEnum.STRING)); + retVal.add(new RuntimeSearchParam("param2", "This is the second parameter", "Patient.param2", SearchParamTypeEnum.DATE)); + return retVal; + } + + @Search(dynamic=true) + public List search(SearchParameterMap theSearchParams) { + ourLastSearchParams = theSearchParams; + + ArrayList retVal = new ArrayList(); + + Patient patient = new Patient(); + patient.setId("Patient/1"); + patient.addIdentifier("system", "fooCompartment"); + retVal.add(patient); + return retVal; + } + + } + +} diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/IncomingRequestAddressStrategyTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/IncomingRequestAddressStrategyTest.java new file mode 100644 index 00000000000..d047a0c84f5 --- /dev/null +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/IncomingRequestAddressStrategyTest.java @@ -0,0 +1,36 @@ +package ca.uhn.fhir.rest.server; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import javax.servlet.http.HttpServletRequest; + +import org.junit.Test; + +public class IncomingRequestAddressStrategyTest { + + @Test + public void testAwsUrl() { + + HttpServletRequest req = mock(HttpServletRequest.class); + when(req.getRequestURI()).thenReturn("/FhirStorm/fhir/Patient/_search"); + when(req.getServletPath()).thenReturn("/fhir"); + when(req.getRequestURL()).thenReturn(new StringBuffer().append("http://fhirstorm.dyndns.org:8080/FhirStorm/fhir/Patient/_search")); + when(req.getContextPath()).thenReturn("/FhirStorm"); + + IncomingRequestAddressStrategy incomingRequestAddressStrategy = new IncomingRequestAddressStrategy(); + String actual = incomingRequestAddressStrategy.determineServerBase(req); + assertEquals("http://fhirstorm.dyndns.org:8080/FhirStorm/fhir", actual); + } + + /* + IncomingRequestAddressStrategy15:29:02.876 [http-bio-8080-exec-3] TRACE c.uhn.fhir.rest.server.RestfulServer - + Request FullPath: /FhirStorm/fhir/Patient/_search + 15:29:02.876 [http-bio-8080-exec-3] TRACE c.uhn.fhir.rest.server.RestfulServer - + Servlet Path: /fhir + 15:29:02.876 [http-bio-8080-exec-3] TRACE c.uhn.fhir.rest.server.RestfulServer - + Request Url: http://fhirstorm.dyndns.org:8080/FhirStorm/fhir/Patient/_search + 15:29:02.876 [http-bio-8080-exec-3] TRACE c.uhn.fhir.rest.server.RestfulServer - + Context Path: /FhirStorm + */ +} diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/SearchTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/SearchTest.java index c79e34559e2..5e6ee0b9a6f 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/SearchTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/SearchTest.java @@ -39,6 +39,7 @@ 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.StringOrListParam; import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.param.TokenOrListParam; import ca.uhn.fhir.util.PortUtil; @@ -105,6 +106,21 @@ public class SearchTest { assertEquals("IDAAA (identifier123)", bundle.getEntries().get(0).getTitle().getValue()); } + @Test + public void testSearchWithOrList() throws Exception { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?findPatientWithOrList=aaa,bbb"); + 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("aaa", p.getIdentifier().get(0).getValue().getValue()); + assertEquals("bbb", p.getIdentifier().get(1).getValue().getValue()); + } + @Test public void testSearchByPost() throws Exception { HttpPost filePost = new HttpPost("http://localhost:" + ourPort + "/Patient/_search"); @@ -295,6 +311,20 @@ public class SearchTest { return retVal; } + @Search() + public List findPatientWithOrList(@RequiredParam(name = "findPatientWithOrList") StringOrListParam theParam) { + ArrayList retVal = new ArrayList(); + + Patient patient = new Patient(); + patient.setId("1"); + for (StringParam next : theParam.getValuesAsQueryTokens()) { + patient.addIdentifier("system", next.getValue()); + } + retVal.add(patient); + return retVal; + } + + @Search(queryName = "findWithLinks") public List findWithLinks() { ArrayList retVal = new ArrayList(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseFhirDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseFhirDao.java index da1cf63d807..76d3321036e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseFhirDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseFhirDao.java @@ -109,17 +109,14 @@ public abstract class BaseFhirDao implements IDao { private EntityManager myEntityManager; private List myListeners = new ArrayList(); + @Autowired + private PlatformTransactionManager myPlatformTransactionManager; + @Autowired private List> myResourceDaos; private Map, IFhirResourceDao> myResourceTypeToDao; - protected void notifyWriteCompleted() { - for (IDaoListener next : myListeners) { - next.writeCompleted(); - } - } - public FhirContext getContext() { return myContext; } @@ -293,6 +290,18 @@ public abstract class BaseFhirDao implements IDao { } } + protected void createForcedIdIfNeeded(ResourceTable entity, IdDt id) { + if (id.isEmpty() == false && id.hasIdPart()) { + if (isValidPid(id)) { + return; + } + ForcedId fid = new ForcedId(); + fid.setForcedId(id.getIdPart()); + fid.setResource(entity); + entity.setForcedId(fid); + } + } + protected List extractResourceLinks(ResourceTable theEntity, IResource theResource) { ArrayList retVal = new ArrayList(); @@ -829,9 +838,6 @@ public abstract class BaseFhirDao implements IDao { } } - @Autowired - private PlatformTransactionManager myPlatformTransactionManager; - protected IBundleProvider history(String theResourceName, Long theId, Date theSince) { final List tuples = new ArrayList(); @@ -905,6 +911,17 @@ public abstract class BaseFhirDao implements IDao { }; } + protected boolean isValidPid(IdDt theId) { + String idPart = theId.getIdPart(); + for (int i = 0; i < idPart.length(); i++) { + char nextChar = idPart.charAt(i); + if (nextChar < '0' || nextChar > '9') { + return false; + } + } + return true; + } + protected List loadResourcesById(Set theIncludePids) { Set pids = new HashSet(); for (IdDt next : theIncludePids) { @@ -943,6 +960,12 @@ public abstract class BaseFhirDao implements IDao { return new String(out).toUpperCase(); } + protected void notifyWriteCompleted() { + for (IDaoListener next : myListeners) { + next.writeCompleted(); + } + } + protected void populateResourceIntoEntity(IResource theResource, ResourceTable theEntity) { if (theEntity.getPublished().isEmpty()) { @@ -1002,48 +1025,11 @@ public abstract class BaseFhirDao implements IDao { return retVal; } - protected void createForcedIdIfNeeded(ResourceTable entity, IdDt id) { - if (id.isEmpty() == false && id.hasIdPart()) { - if (isValidPid(id)) { - return; - } - ForcedId fid = new ForcedId(); - fid.setForcedId(id.getIdPart()); - fid.setResource(entity); - entity.setForcedId(fid); - } - } - protected IResource toResource(BaseHasResource theEntity) { RuntimeResourceDefinition type = myContext.getResourceDefinition(theEntity.getResourceType()); return toResource(type.getImplementingClass(), theEntity); } - protected Long translateForcedIdToPid(IdDt theId) { - if (isValidPid(theId)) { - return theId.getIdPartAsLong(); - } else { - TypedQuery q = myEntityManager.createNamedQuery("Q_GET_FORCED_ID", ForcedId.class); - q.setParameter("ID", theId.getIdPart()); - try { - return q.getSingleResult().getResourcePid(); - } catch (NoResultException e) { - throw new ResourceNotFoundException(theId); - } - } - } - - protected boolean isValidPid(IdDt theId) { - String idPart = theId.getIdPart(); - for (int i = 0; i < idPart.length(); i++) { - char nextChar = idPart.charAt(i); - if (nextChar < '0' || nextChar > '9') { - return false; - } - } - return true; - } - protected T toResource(Class theResourceType, BaseHasResource theEntity) { String resourceText = null; switch (theEntity.getEncoding()) { @@ -1095,6 +1081,20 @@ public abstract class BaseFhirDao implements IDao { return myContext.getResourceDefinition(theResource).getName(); } + protected Long translateForcedIdToPid(IdDt theId) { + if (isValidPid(theId)) { + return theId.getIdPartAsLong(); + } else { + TypedQuery q = myEntityManager.createNamedQuery("Q_GET_FORCED_ID", ForcedId.class); + q.setParameter("ID", theId.getIdPart()); + try { + return q.getSingleResult().getResourcePid(); + } catch (NoResultException e) { + throw new ResourceNotFoundException(theId); + } + } + } + protected ResourceTable updateEntity(final IResource theResource, ResourceTable entity, boolean theUpdateHistory, boolean theDelete) { if (entity.getPublished() == null) { entity.setPublished(new Date()); diff --git a/hapi-fhir-jpaserver-uhnfhirtest/.settings/org.eclipse.wst.common.component b/hapi-fhir-jpaserver-uhnfhirtest/.settings/org.eclipse.wst.common.component index c136c9ada95..ff207aac0dd 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/.settings/org.eclipse.wst.common.component +++ b/hapi-fhir-jpaserver-uhnfhirtest/.settings/org.eclipse.wst.common.component @@ -6,13 +6,13 @@ - + uses - + uses - + consumes diff --git a/hapi-fhir-jpaserver-uhnfhirtest/.settings/org.eclipse.wst.common.component.orig b/hapi-fhir-jpaserver-uhnfhirtest/.settings/org.eclipse.wst.common.component.orig new file mode 100644 index 00000000000..d1a37bbd3a4 --- /dev/null +++ b/hapi-fhir-jpaserver-uhnfhirtest/.settings/org.eclipse.wst.common.component.orig @@ -0,0 +1,34 @@ + + + + + + + + +<<<<<<< HEAD + + uses + + + uses + + +======= + + uses + + + uses + + +>>>>>>> cdd4b137fb40dd72f895c2bcb644d6e668e1015b + consumes + + + consumes + + + + + diff --git a/hapi-fhir-mitreid-integration/target/classes/ca/uhn/fhir/rest/server/security/OpenIdConnectBearerTokenSecurityManager.class b/hapi-fhir-mitreid-integration/target/classes/ca/uhn/fhir/rest/server/security/OpenIdConnectBearerTokenSecurityManager.class new file mode 100644 index 0000000000000000000000000000000000000000..50e882a9955d3f0b4a2c8ce29c99e3dcf6cb9270 GIT binary patch literal 2771 zcmd^BOLH4V5bkk=S5X|CI5y-7Yo0Qg437|EDz;4AztJCm{_-0DybEOy<{0b*f+rot zTODb*!PxQ`O%DwXL1LsG@VzdoYI9pFg@N5dVbJXBBUJTcg9k#1HX09d49*>jp5UQS zZNAqyBzp#Dw<_gYrS{F|waUHfR}7YRPi*SeNYyTp5y8h+D0?8n1oLp3fj120Gnm(j z+0||5s!f~6Ve7^rrv(a1u2-YMb|_IculLy1C z@E~krBHu>o&6y-uR*r(jj-LW?baMv4`(J)qM?0Pfq0QfxO4>UN zP8HV<805Bf6Fs=fVDY4kD#mD&hZh(teW9p^b=bqEALytn!!!@SYc!o&6mR;4)72VT zBqpHU4lCRb($n!9=#QMO_JwZ;T_pQQc6*5>O#B=3{SNv>X-7AGk=PCurVNC|;*Pal z$8U(XhTkUCX=}c2r7`>&Tg;D=*!r5v#mFZ5+W77uFrpZgZJcz`xRj5>KW*j(xB~tH zT!dAsi{fOLI{FuPrNYmWs6iSah|#ns0}&nwBi;OXF=soH4*mY_eB0TNiD@-V=z6N=b_HyCVAOB6?Tu5TZmCi#s3^03a>nOW-Y ze^N&bmfL7E1cy4fnuoU;tUj47YElN*Vv>R<==vwcF-7^u!tQF?MVc<344xv=beNKh zv`mpQc*zy3?&}JDrR#4(Iyc65I!%l-w9{c0>Z#J*lde_QXOx-iPFX3Rs-l}Rma!#$ z*^y|32|5E0K4fryLPI^cOSk{yokm^zK*llMi~FQYF{}dVIQ{wX;4(Y|^EC5lHV1i{ z7vN0#z5r)w{aJc<-xt%hb8voQZ3!-<-9Wt!cfXpF|fuW;q- zrE5RKi@&F~FVWMX1FXZ#^jsKP!7K18%^17}uTxe7&p`Mt6X6~Dvvz`Tl0z24_n8Rq5fif_{E&(8KHQuc;m1sb58&3!2tQ>YoPrHv Te-1XGL?ggQ^qZ&oZMgFn496l{ literal 0 HcmV?d00001 diff --git a/hapi-fhir-mitreid-integration/target/test-classes/ca/uhn/fhir/rest/server/security/OpenIdConnectBearerTokenSecurityManagerTest.class b/hapi-fhir-mitreid-integration/target/test-classes/ca/uhn/fhir/rest/server/security/OpenIdConnectBearerTokenSecurityManagerTest.class new file mode 100644 index 0000000000000000000000000000000000000000..9dddda69419dc173b65ddec1cc509650f157807d GIT binary patch literal 994 zcmd5*&rcIU6#k|y6jl@{pn{sLx8lJi#*-yRB^pU8O(-_;INiQ>r|!-yv$Ku-S)NEV z(Yt??@y&viBL@#&-kUe?y>GtvV}AYq_5;8(JZhpQ(2ZqOCOR4?%0`w77Zqe@WLd?f zRqib6<)jZs2S#g(-5ZjY?1}kIx_=q)L25bSJI-yQA+R=;Ga02)Pomy%$~gjy`${YK zQlQ@69SAfI%!mTivAlu>EDEf4m8PRI8#0U>rXjVg}~82<=hci zcg%etQ#F!~s@epCw_jq)oie&;VqIWkUe%RHVEee#PGvMug<_tswKh&xN!*97v6E<8 za^;b4PN#`2F7+CYwZ<;to)E7-YHqzrXEX|9ljSO{K*HP_M$L*BVX%CXP?(WR%qWzl zONif%nQr^Nb9tXfmXbTB(~=+B4ae4GA;*O7ET`~>0}L&V$rhnARcfY+Lg~o`#Nppz z_e#eE>3^dWU=!PZnRW!W+TD4N`_6I^9lv!!-;_3{cgpYA-VK}L2lv#&;W2o9`QrdJ zKJnVJY_b)x7OWSZoMY*0bq8|PM%{d_q literal 0 HcmV?d00001 diff --git a/hapi-fhir-mitreid-integration/target/test-classes/ca/uhn/fhir/rest/server/security/RestAuthenticationEntryPoint.class b/hapi-fhir-mitreid-integration/target/test-classes/ca/uhn/fhir/rest/server/security/RestAuthenticationEntryPoint.class new file mode 100644 index 0000000000000000000000000000000000000000..b120aa2522d1936eaafeb12eb11c3b7a6e84f4d9 GIT binary patch literal 1038 zcmbu8OK%e~5Xb*cw%Ihh2w?kAIo}>vcGR3zuX}A%&<+(7s z!jMP{Bkcj-B;)hKWUW1&*I=yN-E?gltNP?V7NOI zLdl8<)985~46^}G$uTUriVn+UI0;Nb+0%td#Y^c1YW29Wf8d~D6*Qpq&CzqJkfhP1 zb)Ga%l98NS`-a9>|2v|!=8a7(&^{_1a0-j0(IdhV1!>VfAZ^))c~9X3=X-yNeQBNj uj&oHUMQe~h=v$$TOwS;b?+Tf(l>n4H|azLxBdW`Z7A6Q literal 0 HcmV?d00001 diff --git a/restful-server-example/.settings/org.eclipse.wst.common.component b/restful-server-example/.settings/org.eclipse.wst.common.component index 5947ef2be24..b22f61289cf 100644 --- a/restful-server-example/.settings/org.eclipse.wst.common.component +++ b/restful-server-example/.settings/org.eclipse.wst.common.component @@ -6,7 +6,7 @@ uses - + consumes diff --git a/restful-server-example/.settings/org.eclipse.wst.common.component.orig b/restful-server-example/.settings/org.eclipse.wst.common.component.orig new file mode 100644 index 00000000000..a14684026f0 --- /dev/null +++ b/restful-server-example/.settings/org.eclipse.wst.common.component.orig @@ -0,0 +1,22 @@ + + + + + +<<<<<<< HEAD + +======= + +>>>>>>> cdd4b137fb40dd72f895c2bcb644d6e668e1015b + uses + + + consumes + + + consumes + + + + + diff --git a/restful-server-example/pom.xml b/restful-server-example/pom.xml index b5938789ce3..6679bda8be1 100644 --- a/restful-server-example/pom.xml +++ b/restful-server-example/pom.xml @@ -13,7 +13,7 @@ ca.uhn.hapi.example restful-server-example - 0.7 + 0.7-SNAPSHOT war HAPI FHIR Sample RESTful Server From 5701572e08301d3f4a7337f49cbb76775c482f74 Mon Sep 17 00:00:00 2001 From: jamesagnew Date: Sat, 13 Sep 2014 12:15:35 -0500 Subject: [PATCH 3/4] Fix url determination --- .../rest/server/IncomingRequestAddressStrategy.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/IncomingRequestAddressStrategy.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/IncomingRequestAddressStrategy.java index 8e9cb2645df..0cae6bfe87a 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/IncomingRequestAddressStrategy.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/IncomingRequestAddressStrategy.java @@ -46,15 +46,23 @@ public class IncomingRequestAddressStrategy implements IServerAddressStrategy { requestPath = requestPath.substring(1); } + int startOfPath = requestUrl.indexOf("//"); + if (startOfPath != -1 && (startOfPath + 2) < requestUrl.length()) { + startOfPath = requestUrl.indexOf("/", startOfPath + 2); + } + if (startOfPath == -1) { + startOfPath = 0; + } + int contextIndex; if (servletPath.length() == 0) { if (requestPath.length() == 0) { contextIndex = requestUrl.length(); } else { - contextIndex = requestUrl.indexOf(requestPath); + contextIndex = requestUrl.indexOf(requestPath, startOfPath); } } else { - contextIndex = requestUrl.indexOf(servletPath); + contextIndex = requestUrl.indexOf(servletPath, startOfPath); } String fhirServerBase; From f79b7adabd98972bbafe18dfd2a2cc72a3c21d63 Mon Sep 17 00:00:00 2001 From: t106uhn Date: Mon, 15 Sep 2014 20:40:40 -0400 Subject: [PATCH 4/4] added jetty-http dependancy to allow test case classes to compile in eclipse --- hapi-fhir-base/pom.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/hapi-fhir-base/pom.xml b/hapi-fhir-base/pom.xml index 2d735d6f822..93c91bf3b0f 100644 --- a/hapi-fhir-base/pom.xml +++ b/hapi-fhir-base/pom.xml @@ -225,6 +225,13 @@ 9.1.1.v20140108 test + + org.eclipse.jetty + jetty-http + 9.1.1.v20140108 + test + +