From 11507ef97c39537a9352eae11cf7cee6c0405c37 Mon Sep 17 00:00:00 2001 From: jamesagnew Date: Sun, 5 Apr 2015 14:53:15 -0400 Subject: [PATCH] Fix #148 - Add _count support to everything operation --- .../BaseResourceReturningMethodBinding.java | 18 +- .../rest/method/HistoryMethodBinding.java | 5 + .../uhn/fhir/rest/server/BundleProviders.java | 5 + .../fhir/rest/server/Dstu1BundleFactory.java | 11 +- .../uhn/fhir/rest/server/IBundleProvider.java | 23 + .../server/IVersionSpecificBundleFactory.java | 8 +- .../rest/server/ResponseResourceList.java | 98 ++ .../fhir/rest/server/RestfulServer.java.orig | 1070 ----------------- .../rest/server/SimpleBundleProvider.java | 5 + .../java/ca/uhn/fhir/jpa/dao/BaseFhirDao.java | 5 + .../uhn/fhir/jpa/dao/BaseFhirResourceDao.java | 465 +++---- .../provider/ResourceProviderDstu2Test.java | 176 ++- .../src/test/resources/bug147-bundle.json | 105 +- .../fhir-jpabase-spring-test-config.xml | 2 +- .../provider/dstu2/Dstu2BundleFactory.java | 2 +- src/changes/changes.xml | 3 + 16 files changed, 620 insertions(+), 1381 deletions(-) create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/ResponseResourceList.java delete mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java.orig diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseResourceReturningMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseResourceReturningMethodBinding.java index be41546c6b5..fe667ddfa18 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseResourceReturningMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseResourceReturningMethodBinding.java @@ -246,12 +246,11 @@ abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding includes = getRequestIncludesFromParams(params); + Set includes = getRequestIncludesFromParams(params); IBundleProvider result = (IBundleProvider) resultObj; + if (count == null) { + count = result.preferredPageSize(); + } + IVersionSpecificBundleFactory bundleFactory = theServer.getFhirContext().newBundleFactory(); - bundleFactory.initializeBundleFromBundleProvider(theServer, result, responseEncoding, theRequest.getFhirServerBase(), theRequest.getCompleteUrl(), prettyPrint, 0, count, null, - getResponseBundleType(), includes); + bundleFactory.initializeBundleFromBundleProvider(theServer, result, responseEncoding, theRequest.getFhirServerBase(), theRequest.getCompleteUrl(), prettyPrint, 0, count, null, getResponseBundleType(), includes); Bundle bundle = bundleFactory.getDstu1Bundle(); if (bundle != null) { for (int i = theServer.getInterceptors().size() - 1; i >= 0; i--) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HistoryMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HistoryMethodBinding.java index 8d6e3a3f7b7..5ad0c0a097b 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HistoryMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HistoryMethodBinding.java @@ -208,6 +208,11 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding { public int size() { return resources.size(); } + + @Override + public Integer preferredPageSize() { + return null; + } }; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/BundleProviders.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/BundleProviders.java index 6d2c074a48d..db8e294cd1b 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/BundleProviders.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/BundleProviders.java @@ -56,6 +56,11 @@ public class BundleProviders { public InstantDt getPublished() { return published; } + + @Override + public Integer preferredPageSize() { + return null; + } }; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Dstu1BundleFactory.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Dstu1BundleFactory.java index 2aaec894b13..6a7fdcad26e 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Dstu1BundleFactory.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Dstu1BundleFactory.java @@ -26,21 +26,24 @@ import java.util.List; import java.util.Set; import java.util.UUID; -import ca.uhn.fhir.model.api.*; -import ca.uhn.fhir.util.ResourceReferenceInfo; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; -import org.hl7.fhir.instance.model.IBaseResource; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.model.api.Bundle; +import ca.uhn.fhir.model.api.BundleEntry; +import ca.uhn.fhir.model.api.IResource; +import ca.uhn.fhir.model.api.Include; +import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; import ca.uhn.fhir.model.base.composite.BaseResourceReferenceDt; import ca.uhn.fhir.model.base.resource.BaseOperationOutcome; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum; import ca.uhn.fhir.model.valueset.BundleTypeEnum; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.util.ResourceReferenceInfo; public class Dstu1BundleFactory implements IVersionSpecificBundleFactory { @@ -253,7 +256,7 @@ public class Dstu1BundleFactory implements IVersionSpecificBundleFactory { } @Override - public IBaseResource getResourceBundle() { + public IResource getResourceBundle() { return null; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/IBundleProvider.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/IBundleProvider.java index 7bd1bfa9ce1..42b310a90f2 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/IBundleProvider.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/IBundleProvider.java @@ -28,14 +28,37 @@ import ca.uhn.fhir.model.primitive.InstantDt; public interface IBundleProvider { /** + * Load the given collection of resources by index, plus any additional resources per the + * server's processing rules (e.g. _include'd resources, OperationOutcome, etc.). For example, + * if the method is invoked with index 0,10 the method might return 10 search results, plus an + * additional 20 resources which matched a client's _include specification. + * * @param theFromIndex The low index (inclusive) to return * @param theToIndex The high index (exclusive) to return * @return A list of resources. The size of this list must be at least theToIndex - theFromIndex. */ List getResources(int theFromIndex, int theToIndex); + /** + * Optionally may be used to signal a preferred page size to the server, e.g. because + * the implementing code recognizes that the resources which will be returned by this + * implementation are expensive to load so a smaller page size should be used. The value + * returned by this method will only be used if the client has not explicitly requested + * a page size. + * + * @return Returns the preferred page size or null + */ + Integer preferredPageSize(); + + /** + * Returns the total number of results which match the given query (exclusive of any + * _include's or OperationOutcome) + */ int size(); + /** + * Returns the instant as of which this result was valid + */ InstantDt getPublished(); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/IVersionSpecificBundleFactory.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/IVersionSpecificBundleFactory.java index 95439badb4c..5e6e1f8d9ce 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/IVersionSpecificBundleFactory.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/IVersionSpecificBundleFactory.java @@ -23,11 +23,9 @@ package ca.uhn.fhir.rest.server; import java.util.List; import java.util.Set; -import ca.uhn.fhir.model.api.Include; -import org.hl7.fhir.instance.model.IBaseResource; - import ca.uhn.fhir.model.api.Bundle; import ca.uhn.fhir.model.api.IResource; +import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.valueset.BundleTypeEnum; /** @@ -41,11 +39,11 @@ public interface IVersionSpecificBundleFactory { void addRootPropertiesToBundle(String theAuthor, String theServerBase, String theCompleteUrl, Integer theTotalResults, BundleTypeEnum theBundleType); void initializeBundleFromBundleProvider(RestfulServer theServer, IBundleProvider theResult, EncodingEnum theResponseEncoding, String theServerBase, String theCompleteUrl, boolean thePrettyPrint, - int theOffset, Integer theLimit, String theSearchId, BundleTypeEnum theBundleType, Set theIncludes); + int theOffset, Integer theCount, String theSearchId, BundleTypeEnum theBundleType, Set theIncludes); Bundle getDstu1Bundle(); - IBaseResource getResourceBundle(); + IResource getResourceBundle(); void initializeBundleFromResourceList(String theAuthor, List theResult, String theServerBase, String theCompleteUrl, int theTotalResults, BundleTypeEnum theBundleType); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/ResponseResourceList.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/ResponseResourceList.java new file mode 100644 index 00000000000..3e52314ae25 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/ResponseResourceList.java @@ -0,0 +1,98 @@ +package ca.uhn.fhir.rest.server; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.hl7.fhir.instance.model.IBaseResource; + +/** + * Used by {@link IBundleProvider} to provide a single page worth of results. + * + * If the server chooses to, it may return a different number of matching results to the number that the user requested. + * For example, if the client requested 100 results but the server decided to return only 10 (perhaps because they were + * very large), this value should be set to 10. Note that this count refers only to resources which are included in the + * indexes provided to {@link IBundleProvider#getResources(int, int)}, so it should not reflect any additional results + * added to the response as a result of _include parameters, OperationOutcome's etc. + */ +public class ResponseResourceList { + /** + * Singleton unmodifiable empty list + */ + public static final ResponseResourceList EMPTY = new EmptyResponseResourceList(); + + private List myIncludeResults; + private List myMatchResults; + + /** + * Adds an "include" results. Include results are results which are added as a result of _include + * directives in search requests. + */ + public void addIncludeResults(IBaseResource theIncludeResult) { + if (myIncludeResults == null) { + myIncludeResults = new ArrayList(); + } + myIncludeResults.add(theIncludeResult); + } + + /** + * Adds a "match" result. A match result is a result added as a direct result of the operation in question. E.g. for + * a search invocation a match result would be a result which directly matched the search criteria. For a history + * invocation it would be a historical version of a resource or the current version. + */ + public void addMatchResult(IBaseResource theResource) { + Validate.notNull(theResource, "theResource must not be null"); + if (myMatchResults == null) { + myMatchResults = new ArrayList(); + } + myMatchResults.add(theResource); + } + + public List getIncludeResults() { + return myIncludeResults; + } + + public List getMatchResults() { + return myMatchResults; + } + + /** + * Sets the "include" results. Include results are results which are added as a result of _include + * directives in search requests. + */ + public void setIncludeResults(List theIncludeResults) { + myIncludeResults = theIncludeResults; + } + + /** + * Sets the "match" results. A match result is a result added as a direct result of the operation in question. E.g. + * for a search invocation a match result would be a result which directly matched the search criteria. For a + * history invocation it would be a historical version of a resource or the current version. + */ + public void setMatchResults(List theMatchResults) { + myMatchResults = theMatchResults; + } + + private static final class EmptyResponseResourceList extends ResponseResourceList { + @Override + public void addIncludeResults(IBaseResource theIncludeResult) { + throw new UnsupportedOperationException(); + } + + @Override + public void addMatchResult(IBaseResource theResource) { + throw new UnsupportedOperationException(); + } + + @Override + public void setIncludeResults(List theIncludeResults) { + throw new UnsupportedOperationException(); + } + + @Override + public void setMatchResults(List theMatchResults) { + throw new UnsupportedOperationException(); + } + } + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java.orig b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java.orig deleted file mode 100644 index 821eb2cc3ab..00000000000 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java.orig +++ /dev/null @@ -1,1070 +0,0 @@ -package ca.uhn.fhir.rest.server; - -/* - * #%L - * HAPI FHIR - Core Library - * %% - * Copyright (C) 2014 - 2015 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 static org.apache.commons.lang3.StringUtils.isNotBlank; - -import java.io.IOException; -import java.lang.annotation.Annotation; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.*; - -import javax.servlet.ServletException; -import javax.servlet.UnavailableException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.Validate; -import org.hl7.fhir.instance.model.IBaseResource; - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.ProvidedResourceScanner; -import ca.uhn.fhir.context.RuntimeResourceDefinition; -import ca.uhn.fhir.model.api.Bundle; -import ca.uhn.fhir.model.api.IResource; -import ca.uhn.fhir.model.primitive.IdDt; -import ca.uhn.fhir.rest.annotation.Destroy; -import ca.uhn.fhir.rest.annotation.IdParam; -import ca.uhn.fhir.rest.api.RequestTypeEnum; -import ca.uhn.fhir.rest.method.BaseMethodBinding; -import ca.uhn.fhir.rest.method.ConformanceMethodBinding; -import ca.uhn.fhir.rest.method.OtherOperationTypeEnum; -import ca.uhn.fhir.rest.method.Request; -<<<<<<< HEAD -import ca.uhn.fhir.rest.method.RequestDetails; -======= -import ca.uhn.fhir.rest.method.SearchMethodBinding; -import ca.uhn.fhir.rest.method.SearchMethodBinding.RequestType; ->>>>>>> 5956ab75fd9186ccc8b9836b60bc421b19d3d288 -import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; -import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.rest.server.exceptions.NotModifiedException; -import ca.uhn.fhir.rest.server.interceptor.ExceptionHandlingInterceptor; -import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; -import ca.uhn.fhir.util.ReflectionUtil; -import ca.uhn.fhir.util.UrlUtil; -import ca.uhn.fhir.util.VersionUtil; - -public class RestfulServer extends HttpServlet { - - /** - * Default setting for {@link #setETagSupport(ETagSupportEnum) ETag Support}: {@link ETagSupportEnum#ENABLED} - */ - public static final ETagSupportEnum DEFAULT_ETAG_SUPPORT = ETagSupportEnum.ENABLED; - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestfulServer.class); - - private static final long serialVersionUID = 1L; - private AddProfileTagEnum myAddProfileTag; - private BundleInclusionRule myBundleInclusionRule = BundleInclusionRule.BASED_ON_INCLUDES; - private boolean myDefaultPrettyPrint = false; - private EncodingEnum myDefaultResponseEncoding = EncodingEnum.XML; - private ETagSupportEnum myETagSupport = DEFAULT_ETAG_SUPPORT; - private FhirContext myFhirContext; - private String myImplementationDescription; - private final List myInterceptors = new ArrayList(); - private IPagingProvider myPagingProvider; - private Collection myPlainProviders; - private Map myResourceNameToProvider = new HashMap(); - private Collection myResourceProviders; - private IServerAddressStrategy myServerAddressStrategy = new IncomingRequestAddressStrategy(); - private ResourceBinding myServerBinding = new ResourceBinding(); - private BaseMethodBinding myServerConformanceMethod; - private Object myServerConformanceProvider; - private String myServerName = "HAPI FHIR Server"; - /** This is configurable but by default we just use HAPI version */ - private String myServerVersion = VersionUtil.getVersion(); - private boolean myStarted; - private boolean myUseBrowserFriendlyContentTypes; - - /** - * Constructor - */ - public RestfulServer() { - this(new FhirContext()); - } - - public RestfulServer(FhirContext theCtx) { - myFhirContext = theCtx; - } - - /** - * This method is called prior to sending a response to incoming requests. It is used to add custom headers. - *

- * Use caution if overriding this method: it is recommended to call super.addHeadersToResponse to avoid - * inadvertantly disabling functionality. - *

- */ - public void addHeadersToResponse(HttpServletResponse theHttpResponse) { - theHttpResponse.addHeader("X-Powered-By", "HAPI FHIR " + VersionUtil.getVersion() + " RESTful Server"); - } - - private void assertProviderIsValid(Object theNext) throws ConfigurationException { - if (Modifier.isPublic(theNext.getClass().getModifiers()) == false) { - throw new ConfigurationException("Can not use provider '" + theNext.getClass() + "' - Class must be public"); - } - } - - @Override - public void destroy() { - if (getResourceProviders() != null) { - for (IResourceProvider iResourceProvider : getResourceProviders()) { - invokeDestroy(iResourceProvider); - } - } - } - - @Override - protected void doDelete(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - handleRequest(RequestTypeEnum.DELETE, request, response); - } - - @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - handleRequest(RequestTypeEnum.GET, request, response); - } - - @Override - protected void doOptions(HttpServletRequest theReq, HttpServletResponse theResp) throws ServletException, IOException { - handleRequest(RequestTypeEnum.OPTIONS, theReq, theResp); - } - - @Override - protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - handleRequest(RequestTypeEnum.POST, request, response); - } - - @Override - protected void doPut(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - handleRequest(RequestTypeEnum.PUT, request, response); - } - - /** - * Count length of URL string, but treating unescaped sequences (e.g. ' ') as their unescaped equivalent (%20) - */ - protected int escapedLength(String theServletPath) { - int delta = 0; - for (int i = 0; i < theServletPath.length(); i++) { - char next = theServletPath.charAt(i); - if (next == ' ') { - delta = delta + 2; - } - } - return theServletPath.length() + delta; - } - - private void findResourceMethods(Object theProvider) throws Exception { - - ourLog.info("Scanning type for RESTful methods: {}", theProvider.getClass()); - int count = 0; - - Class clazz = theProvider.getClass(); - Class supertype = clazz.getSuperclass(); - while (!Object.class.equals(supertype)) { - count += findResourceMethods(theProvider, supertype); - supertype = supertype.getSuperclass(); - } - - count += findResourceMethods(theProvider, clazz); - - if (count == 0) { - throw new ConfigurationException("Did not find any annotated RESTful methods on provider class " + theProvider.getClass().getCanonicalName()); - } - } - - private int findResourceMethods(Object theProvider, Class clazz) throws ConfigurationException { - int count = 0; - - for (Method m : ReflectionUtil.getDeclaredMethods(clazz)) { - BaseMethodBinding foundMethodBinding = BaseMethodBinding.bindMethod(m, myFhirContext, theProvider); - if (foundMethodBinding == null) { - continue; - } - - count++; - - if (!Modifier.isPublic(m.getModifiers())) { - throw new ConfigurationException("Method '" + m.getName() + "' is not public, FHIR RESTful methods must be public"); - } else { - if (Modifier.isStatic(m.getModifiers())) { - throw new ConfigurationException("Method '" + m.getName() + "' is static, FHIR RESTful methods must not be static"); - } else { - ourLog.debug("Scanning public method: {}#{}", theProvider.getClass(), m.getName()); - - String resourceName = foundMethodBinding.getResourceName(); - ResourceBinding resourceBinding; - if (resourceName == null) { - resourceBinding = myServerBinding; - } else { - RuntimeResourceDefinition definition = myFhirContext.getResourceDefinition(resourceName); - if (myResourceNameToProvider.containsKey(definition.getName())) { - resourceBinding = myResourceNameToProvider.get(definition.getName()); - } else { - resourceBinding = new ResourceBinding(); - resourceBinding.setResourceName(resourceName); - myResourceNameToProvider.put(resourceName, resourceBinding); - } - } - - List> allowableParams = foundMethodBinding.getAllowableParamAnnotations(); - if (allowableParams != null) { - for (Annotation[] nextParamAnnotations : m.getParameterAnnotations()) { - for (Annotation annotation : nextParamAnnotations) { - Package pack = annotation.annotationType().getPackage(); - if (pack.equals(IdParam.class.getPackage())) { - if (!allowableParams.contains(annotation.annotationType())) { - throw new ConfigurationException("Method[" + m.toString() + "] is not allowed to have a parameter annotated with " + annotation); - } - } - } - } - } - - resourceBinding.addMethod(foundMethodBinding); - ourLog.debug(" * Method: {}#{} is a handler", theProvider.getClass(), m.getName()); - } - } - } - - return count; - } - - private void findSystemMethods(Object theSystemProvider) { - Class clazz = theSystemProvider.getClass(); - - findSystemMethods(theSystemProvider, clazz); - - } - - private void findSystemMethods(Object theSystemProvider, Class clazz) { - Class supertype = clazz.getSuperclass(); - if (!Object.class.equals(supertype)) { - findSystemMethods(theSystemProvider, supertype); - } - - for (Method m : ReflectionUtil.getDeclaredMethods(clazz)) { - if (Modifier.isPublic(m.getModifiers())) { - ourLog.debug("Scanning public method: {}#{}", theSystemProvider.getClass(), m.getName()); - - BaseMethodBinding foundMethodBinding = BaseMethodBinding.bindMethod(m, myFhirContext, theSystemProvider); - if (foundMethodBinding != null) { - if (foundMethodBinding instanceof ConformanceMethodBinding) { - myServerConformanceMethod = foundMethodBinding; - } else { - myServerBinding.addMethod(foundMethodBinding); - } - ourLog.info(" * Method: {}#{} is a handler", theSystemProvider.getClass(), m.getName()); - } else { - ourLog.debug(" * Method: {}#{} is not a handler", theSystemProvider.getClass(), m.getName()); - } - } - } - } - - /** - * Returns the setting for automatically adding profile tags - * - * @see #setAddProfileTag(AddProfileTagEnum) - */ - public AddProfileTagEnum getAddProfileTag() { - return myAddProfileTag; - } - - public BundleInclusionRule getBundleInclusionRule() { - return myBundleInclusionRule; - } - - /** - * Returns the default encoding to return (XML/JSON) if an incoming request does not specify a preference (either - * with the _format URL parameter, or with an Accept header in the request. The default is - * {@link EncodingEnum#XML}. - */ - public EncodingEnum getDefaultResponseEncoding() { - return myDefaultResponseEncoding; - } - - /** - * Returns the server support for ETags (will not be null). Default is {@link #DEFAULT_ETAG_SUPPORT} - */ - public ETagSupportEnum getETagSupport() { - return myETagSupport; - } - - /** - * Gets the {@link FhirContext} associated with this server. For efficient processing, resource providers and plain - * providers should generally use this context if one is needed, as opposed to creating their own. - */ - public FhirContext getFhirContext() { - return myFhirContext; - } - - public String getImplementationDescription() { - return myImplementationDescription; - } - - /** - * Returns a ist of all registered server interceptors - */ - public List getInterceptors() { - return Collections.unmodifiableList(myInterceptors); - } - - public IPagingProvider getPagingProvider() { - return myPagingProvider; - } - - /** - * Provides the non-resource specific providers which implement method calls on this server - * - * @see #getResourceProviders() - */ - public Collection getPlainProviders() { - return myPlainProviders; - } - - /** - * Allows users of RestfulServer to override the getRequestPath method to let them build their custom request path - * implementation - * - * @param requestFullPath - * the full request path - * @param servletContextPath - * the servelet context path - * @param servletPath - * the servelet path - * @return created resource path - */ - protected String getRequestPath(String requestFullPath, String servletContextPath, String servletPath) { - return requestFullPath.substring(escapedLength(servletContextPath) + escapedLength(servletPath)); - } - - public Collection getResourceBindings() { - return myResourceNameToProvider.values(); - } - - /** - * Provides the resource providers for this server - */ - public Collection getResourceProviders() { - return myResourceProviders; - } - - /** - * Get the server address strategy, which is used to determine what base URL to provide clients to refer to this - * server. Defaults to an instance of {@link IncomingRequestAddressStrategy} - */ - public IServerAddressStrategy getServerAddressStrategy() { - return myServerAddressStrategy; - } - - public String getServerBaseForRequest(HttpServletRequest theRequest) { - String fhirServerBase; - fhirServerBase = myServerAddressStrategy.determineServerBase(getServletContext(), theRequest); - - if (fhirServerBase.endsWith("/")) { - fhirServerBase = fhirServerBase.substring(0, fhirServerBase.length() - 1); - } - return fhirServerBase; - } - - /** - * Returns the server conformance provider, which is the provider that is used to generate the server's conformance - * (metadata) statement if one has been explicitly defined. - *

- * By default, the ServerConformanceProvider for the declared version of FHIR is used, but this can be changed, or - * set to null to use the appropriate one for the given FHIR version. - *

- */ - public Object getServerConformanceProvider() { - return myServerConformanceProvider; - } - - /** - * Gets the server's name, as exported in conformance profiles exported by the server. This is informational only, - * but can be helpful to set with something appropriate. - * - * @see RestfulServer#setServerName(String) - */ - public String getServerName() { - return myServerName; - } - - public IResourceProvider getServerProfilesProvider() { - return myFhirContext.getVersion().createServerProfilesProvider(this); - } - - /** - * Gets the server's version, as exported in conformance profiles exported by the server. This is informational - * only, but can be helpful to set with something appropriate. - */ - public String getServerVersion() { - return myServerVersion; - } - - private void handlePagingRequest(Request theRequest, HttpServletResponse theResponse, String thePagingAction) throws IOException { - IBundleProvider resultList = getPagingProvider().retrieveResultList(thePagingAction); - if (resultList == null) { - ourLog.info("Client requested unknown paging ID[{}]", thePagingAction); - theResponse.setStatus(Constants.STATUS_HTTP_410_GONE); - addHeadersToResponse(theResponse); - theResponse.setContentType("text/plain"); - theResponse.setCharacterEncoding("UTF-8"); - theResponse.getWriter().append("Search ID[" + thePagingAction + "] does not exist and may have expired."); - theResponse.getWriter().close(); - return; - } - - Integer count = RestfulServerUtils.extractCountParameter(theRequest.getServletRequest()); - if (count == null) { - count = getPagingProvider().getDefaultPageSize(); - } else if (count > getPagingProvider().getMaximumPageSize()) { - count = getPagingProvider().getMaximumPageSize(); - } - - Integer offsetI = RestfulServerUtils.tryToExtractNamedParameter(theRequest.getServletRequest(), Constants.PARAM_PAGINGOFFSET); - if (offsetI == null || offsetI < 0) { - offsetI = 0; - } - - int start = Math.min(offsetI, resultList.size() - 1); - - EncodingEnum responseEncoding = RestfulServerUtils.determineResponseEncodingNoDefault(theRequest.getServletRequest()); - boolean prettyPrint = RestfulServerUtils.prettyPrintResponse(this, theRequest); - boolean requestIsBrowser = requestIsBrowser(theRequest.getServletRequest()); - NarrativeModeEnum narrativeMode = RestfulServerUtils.determineNarrativeMode(theRequest); - boolean respondGzip = theRequest.isRespondGzip(); - - IVersionSpecificBundleFactory bundleFactory = myFhirContext.newBundleFactory(); - bundleFactory.initializeBundleFromBundleProvider(this, resultList, responseEncoding, theRequest.getFhirServerBase(), theRequest.getCompleteUrl(), prettyPrint, start, count, thePagingAction, - null, IResource.WILDCARD_ALL_SET); - - Bundle bundle = bundleFactory.getDstu1Bundle(); - if (bundle != null) { - for (int i = getInterceptors().size() - 1; i >= 0; i--) { - IServerInterceptor next = getInterceptors().get(i); - boolean continueProcessing = next.outgoingResponse(theRequest, bundle, theRequest.getServletRequest(), theRequest.getServletResponse()); - if (!continueProcessing) { - ourLog.debug("Interceptor {} returned false, not continuing processing"); - return; - } - } - RestfulServerUtils.streamResponseAsBundle(this, theResponse, bundle, responseEncoding, theRequest.getFhirServerBase(), prettyPrint, narrativeMode, respondGzip, requestIsBrowser); - } else { - IBaseResource resBundle = bundleFactory.getResourceBundle(); - for (int i = getInterceptors().size() - 1; i >= 0; i--) { - IServerInterceptor next = getInterceptors().get(i); - boolean continueProcessing = next.outgoingResponse(theRequest, resBundle, theRequest.getServletRequest(), theRequest.getServletResponse()); - if (!continueProcessing) { - ourLog.debug("Interceptor {} returned false, not continuing processing"); - return; - } - } - RestfulServerUtils.streamResponseAsResource(this, theResponse, (IResource) resBundle, responseEncoding, prettyPrint, requestIsBrowser, narrativeMode, Constants.STATUS_HTTP_200_OK, theRequest.isRespondGzip(), theRequest.getFhirServerBase()); - } - } - - protected void handleRequest(RequestTypeEnum theRequestType, HttpServletRequest theRequest, HttpServletResponse theResponse) throws ServletException, IOException { - for (IServerInterceptor next : myInterceptors) { - boolean continueProcessing = next.incomingRequestPreProcessed(theRequest, theResponse); - if (!continueProcessing) { - ourLog.debug("Interceptor {} returned false, not continuing processing"); - return; - } - } - - String fhirServerBase = null; - boolean requestIsBrowser = requestIsBrowser(theRequest); - Request requestDetails = new Request(); - requestDetails.setServer(this); - - try { - - String resourceName = null; - String requestFullPath = StringUtils.defaultString(theRequest.getRequestURI()); - String servletPath = StringUtils.defaultString(theRequest.getServletPath()); - StringBuffer requestUrl = theRequest.getRequestURL(); - String servletContextPath = ""; - - // if (getServletContext().getMajorVersion() >= 3) { - // // getServletContext is only supported in version 3+ of servlet-api - if (getServletContext() != null) { - servletContextPath = StringUtils.defaultString(getServletContext().getContextPath()); - } - // } - - if (ourLog.isTraceEnabled()) { - ourLog.trace("Request FullPath: {}", requestFullPath); - ourLog.trace("Servlet Path: {}", servletPath); - ourLog.trace("Request Url: {}", requestUrl); - ourLog.trace("Context Path: {}", servletContextPath); - } - - IdDt id = null; - String operation = null; - String compartment = null; - - String requestPath = getRequestPath(requestFullPath, servletContextPath, servletPath); - if (requestPath.length() > 0 && requestPath.charAt(0) == '/') { - requestPath = requestPath.substring(1); - } - - fhirServerBase = getServerBaseForRequest(theRequest); - - String completeUrl = StringUtils.isNotBlank(theRequest.getQueryString()) ? requestUrl + "?" + theRequest.getQueryString() : requestUrl.toString(); - - Map params = new HashMap(theRequest.getParameterMap()); - requestDetails.setParameters(params); - - StringTokenizer tok = new StringTokenizer(requestPath, "/"); - if (tok.hasMoreTokens()) { - resourceName = tok.nextToken(); - if (partIsOperation(resourceName)) { - operation = resourceName; - resourceName = null; - } - } - requestDetails.setResourceName(resourceName); - - ResourceBinding resourceBinding = null; - BaseMethodBinding resourceMethod = null; - if (Constants.URL_TOKEN_METADATA.equals(resourceName) || theRequestType == RequestTypeEnum.OPTIONS) { - resourceMethod = myServerConformanceMethod; - } else if (resourceName == null) { - resourceBinding = myServerBinding; - } else { - resourceBinding = myResourceNameToProvider.get(resourceName); - if (resourceBinding == null) { - throw new InvalidRequestException("Unknown resource type '" + resourceName + "' - Server knows how to handle: " + myResourceNameToProvider.keySet()); - } - } - - if (tok.hasMoreTokens()) { - String nextString = tok.nextToken(); - if (partIsOperation(nextString)) { - operation = nextString; - } else { - id = new IdDt(resourceName, UrlUtil.unescape(nextString)); - } - } - - if (tok.hasMoreTokens()) { - String nextString = tok.nextToken(); - if (nextString.equals(Constants.PARAM_HISTORY)) { - if (tok.hasMoreTokens()) { - String versionString = tok.nextToken(); - if (id == null) { - throw new InvalidRequestException("Don't know how to handle request path: " + requestPath); - } - id = new IdDt(resourceName, id.getIdPart(), UrlUtil.unescape(versionString)); - } else { - operation = Constants.PARAM_HISTORY; - } - } else if (partIsOperation(nextString)) { - // FIXME: this would be untrue for _meta/_delete - if (operation != null) { - throw new InvalidRequestException("URL Path contains two operations: " + requestPath); - } - operation = nextString; - } else { - compartment = nextString; - } - } - - // Secondary is for things like ..../_tags/_delete - String secondaryOperation = null; - - while (tok.hasMoreTokens()) { - String nextString = tok.nextToken(); - if (operation == null) { - operation = nextString; - } else if (secondaryOperation == null) { - secondaryOperation = nextString; - } else { - throw new InvalidRequestException("URL path has unexpected token '" + nextString + "' at the end: " + requestPath); - } - } - - if (theRequestType == RequestTypeEnum.PUT) { - String contentLocation = theRequest.getHeader(Constants.HEADER_CONTENT_LOCATION); - if (contentLocation != null) { - id = new IdDt(contentLocation); - } - } - requestDetails.setId(id); - requestDetails.setOperation(operation); - requestDetails.setSecondaryOperation(secondaryOperation); - requestDetails.setCompartmentName(compartment); - - // TODO: look for more tokens for version, compartments, etc... - - String acceptEncoding = theRequest.getHeader(Constants.HEADER_ACCEPT_ENCODING); - boolean respondGzip = false; - if (acceptEncoding != null) { - String[] parts = acceptEncoding.trim().split("\\s*,\\s*"); - for (String string : parts) { - if (string.equals("gzip")) { - respondGzip = true; - } - } - } - requestDetails.setRespondGzip(respondGzip); - - requestDetails.setRequestType(theRequestType); - requestDetails.setFhirServerBase(fhirServerBase); - requestDetails.setCompleteUrl(completeUrl); - requestDetails.setServletRequest(theRequest); - requestDetails.setServletResponse(theResponse); - - String pagingAction = theRequest.getParameter(Constants.PARAM_PAGINGACTION); - if (getPagingProvider() != null && isNotBlank(pagingAction)) { - requestDetails.setOtherOperationType(OtherOperationTypeEnum.GET_PAGE); - handlePagingRequest(requestDetails, theResponse, pagingAction); - return; - } - - if (resourceMethod == null) { - if (resourceBinding != null) { - resourceMethod = resourceBinding.getMethod(requestDetails); - } - } - if (resourceMethod == null) { - StringBuilder b = new StringBuilder(); - b.append("No resource method available for "); - b.append(theRequestType.name()); - b.append(" operation["); - b.append(requestPath); - b.append("]"); - b.append(" with parameters "); - b.append(params.keySet()); - throw new InvalidRequestException(b.toString()); - } - requestDetails.setResourceOperationType(resourceMethod.getResourceOperationType()); - requestDetails.setSystemOperationType(resourceMethod.getSystemOperationType()); - requestDetails.setOtherOperationType(resourceMethod.getOtherOperationType()); - - for (IServerInterceptor next : myInterceptors) { - boolean continueProcessing = next.incomingRequestPostProcessed(requestDetails, theRequest, theResponse); - if (!continueProcessing) { - ourLog.debug("Interceptor {} returned false, not continuing processing"); - return; - } - } - - resourceMethod.invokeServer(this, requestDetails); - - } catch (NotModifiedException e) { - - for (int i = getInterceptors().size() - 1; i >= 0; i--) { - IServerInterceptor next = getInterceptors().get(i); - if (!next.handleException(this, requestDetails, e, theRequest, theResponse)) { - ourLog.debug("Interceptor {} returned false, not continuing processing"); - return; - } - } - writeExceptionToResponse(theResponse, e); - - } catch (AuthenticationException e) { - - for (int i = getInterceptors().size() - 1; i >= 0; i--) { - IServerInterceptor next = getInterceptors().get(i); - if (!next.handleException(this, requestDetails, e, theRequest, theResponse)) { - ourLog.debug("Interceptor {} returned false, not continuing processing"); - return; - } - } - - if (requestIsBrowser) { - // if request is coming from a browser, prompt the user to enter login credentials - theResponse.setHeader("WWW-Authenticate", "BASIC realm=\"FHIR\""); - } - writeExceptionToResponse(theResponse, e); - - } catch (Throwable e) { - - /* - * We have caught an exception while handling an incoming server request. Start by notifying the interceptors.. - */ - for (int i = getInterceptors().size() - 1; i >= 0; i--) { - IServerInterceptor next = getInterceptors().get(i); - if (!next.handleException(this, requestDetails, e, theRequest, theResponse)) { - ourLog.debug("Interceptor {} returned false, not continuing processing"); - return; - } - } - - new ExceptionHandlingInterceptor().handleException(this, requestDetails, e, theRequest, theResponse); - - } - } - - /** - * Initializes the server. Note that this method is final to avoid accidentally introducing bugs in implementations, - * but subclasses may put initialization code in {@link #initialize()}, which is called immediately before beginning - * initialization of the restful server's internal init. - */ - @Override - public final void init() throws ServletException { - initialize(); - try { - ourLog.info("Initializing HAPI FHIR restful server"); - - ProvidedResourceScanner providedResourceScanner = new ProvidedResourceScanner(getFhirContext()); - providedResourceScanner.scanForProvidedResources(this); - - Collection resourceProvider = getResourceProviders(); - if (resourceProvider != null) { - Map typeToProvider = new HashMap(); - for (IResourceProvider nextProvider : resourceProvider) { - - Class resourceType = nextProvider.getResourceType(); - if (resourceType == null) { - throw new NullPointerException("getResourceType() on class '" + nextProvider.getClass().getCanonicalName() + "' returned null"); - } - - String resourceName = myFhirContext.getResourceDefinition(resourceType).getName(); - if (typeToProvider.containsKey(resourceName)) { - throw new ServletException("Multiple resource providers return resource type[" + resourceName + "]: First[" + typeToProvider.get(resourceName).getClass().getCanonicalName() + "] and Second[" + nextProvider.getClass().getCanonicalName() + "]"); - } - typeToProvider.put(resourceName, nextProvider); - providedResourceScanner.scanForProvidedResources(nextProvider); - } - ourLog.info("Got {} resource providers", typeToProvider.size()); - for (IResourceProvider provider : typeToProvider.values()) { - assertProviderIsValid(provider); - findResourceMethods(provider); - } - } - - Collection providers = getPlainProviders(); - if (providers != null) { - for (Object next : providers) { - assertProviderIsValid(next); - findResourceMethods(next); - } - } - - findResourceMethods(getServerProfilesProvider()); - - Object confProvider = getServerConformanceProvider(); - if (confProvider == null) { - confProvider = myFhirContext.getVersion().createServerConformanceProvider(this); - } - findSystemMethods(confProvider); - - } catch (Exception ex) { - ourLog.error("An error occurred while loading request handlers!", ex); - throw new ServletException("Failed to initialize FHIR Restful server", ex); - } - - myStarted = true; - ourLog.info("A FHIR has been lit on this server"); - } - - /** - * This method may be overridden by subclasses to do perform initialization that needs to be performed prior to the - * server being used. - * - * @throws ServletException - * If the initialization failed. Note that you should consider throwing {@link UnavailableException} - * (which extends {@link ServletException}), as this is a flag to the servlet container that the servlet - * is not usable. - */ - protected void initialize() throws ServletException { - // nothing by default - } - - private void invokeDestroy(Object theProvider) { - Class clazz = theProvider.getClass(); - invokeDestroy(theProvider, clazz); - } - - private void invokeDestroy(Object theProvider, Class clazz) { - for (Method m : ReflectionUtil.getDeclaredMethods(clazz)) { - Destroy destroy = m.getAnnotation(Destroy.class); - if (destroy != null) { - try { - m.invoke(theProvider); - } catch (IllegalAccessException e) { - ourLog.error("Exception occurred in destroy ", e); - } catch (InvocationTargetException e) { - ourLog.error("Exception occurred in destroy ", e); - } - return; - } - } - - Class supertype = clazz.getSuperclass(); - if (!Object.class.equals(supertype)) { - invokeDestroy(theProvider, supertype); - } - } - - /** - * Should the server "pretty print" responses by default (requesting clients can always override this - * default by supplying an Accept header in the request, or a _pretty - * parameter in the request URL. - *

- * The default is false - *

- * @return Returns the default pretty print setting - */ - public boolean isDefaultPrettyPrint() { - return myDefaultPrettyPrint; - } - - public boolean isUseBrowserFriendlyContentTypes() { - return myUseBrowserFriendlyContentTypes; - } - - public void registerInterceptor(IServerInterceptor theInterceptor) { - Validate.notNull(theInterceptor, "Interceptor can not be null"); - myInterceptors.add(theInterceptor); - } - - public static boolean requestIsBrowser(HttpServletRequest theRequest) { - String userAgent = theRequest.getHeader("User-Agent"); - return userAgent != null && userAgent.contains("Mozilla"); - } - - /** - * Sets the profile tagging behaviour for the server. When set to a value other than {@link AddProfileTagEnum#NEVER} - * (which is the default), the server will automatically add a profile tag based on the class of the resource(s) - * being returned. - * - * @param theAddProfileTag - * The behaviour enum (must not be null) - */ - public void setAddProfileTag(AddProfileTagEnum theAddProfileTag) { - Validate.notNull(theAddProfileTag, "theAddProfileTag must not be null"); - myAddProfileTag = theAddProfileTag; - } - - /** - * Set how bundle factory should decide whether referenced resources should be included in bundles - * - * @param theBundleInclusionRule - inclusion rule (@see BundleInclusionRule for behaviors) - */ - public void setBundleInclusionRule(BundleInclusionRule theBundleInclusionRule) { - myBundleInclusionRule = theBundleInclusionRule; - } - - /** - * Should the server "pretty print" responses by default (requesting clients can always override this - * default by supplying an Accept header in the request, or a _pretty - * parameter in the request URL. - *

- * The default is false - *

- * @param theDefaultPrettyPrint The default pretty print setting - */ - public void setDefaultPrettyPrint(boolean theDefaultPrettyPrint) { - myDefaultPrettyPrint = theDefaultPrettyPrint; - } - - /** - * Sets the default encoding to return (XML/JSON) if an incoming request does not specify a preference (either with - * the _format URL parameter, or with an Accept header in the request. The default is - * {@link EncodingEnum#XML}. - */ - public void setDefaultResponseEncoding(EncodingEnum theDefaultResponseEncoding) { - Validate.notNull(theDefaultResponseEncoding, "theDefaultResponseEncoding can not be null"); - myDefaultResponseEncoding = theDefaultResponseEncoding; - } - - /** - * Sets (enables/disables) the server support for ETags. Must not be null. Default is - * {@link #DEFAULT_ETAG_SUPPORT} - * - * @param theETagSupport - * The ETag support mode - */ - public void setETagSupport(ETagSupportEnum theETagSupport) { - if (theETagSupport == null) { - throw new NullPointerException("theETagSupport can not be null"); - } - myETagSupport = theETagSupport; - } - - public void setFhirContext(FhirContext theFhirContext) { - Validate.notNull(theFhirContext, "FhirContext must not be null"); - myFhirContext = theFhirContext; - } - - public void setImplementationDescription(String theImplementationDescription) { - myImplementationDescription = theImplementationDescription; - } - - /** - * Sets (or clears) the list of interceptors - * - * @param theList - * The list of interceptors (may be null) - */ - public void setInterceptors(IServerInterceptor... theList) { - myInterceptors.clear(); - if (theList != null) { - myInterceptors.addAll(Arrays.asList(theList)); - } - } - - /** - * Sets (or clears) the list of interceptors - * - * @param theList - * The list of interceptors (may be null) - */ - public void setInterceptors(List theList) { - myInterceptors.clear(); - if (theList != null) { - myInterceptors.addAll(theList); - } - } - - /** - * Sets the paging provider to use, or null to use no paging (which is the default) - */ - public void setPagingProvider(IPagingProvider thePagingProvider) { - myPagingProvider = thePagingProvider; - } - - /** - * Sets the non-resource specific providers which implement method calls on this server. - * - * @see #setResourceProviders(Collection) - */ - public void setPlainProviders(Collection theProviders) { - myPlainProviders = theProviders; - } - - /** - * Sets the non-resource specific providers which implement method calls on this server. - * - * @see #setResourceProviders(Collection) - */ - public void setPlainProviders(Object... theProv) { - setPlainProviders(Arrays.asList(theProv)); - } - - /** - * Sets the non-resource specific providers which implement method calls on this server - * - * @see #setResourceProviders(Collection) - */ - public void setProviders(Object... theProviders) { - myPlainProviders = Arrays.asList(theProviders); - } - - /** - * Sets the resource providers for this server - */ - public void setResourceProviders(Collection theResourceProviders) { - myResourceProviders = theResourceProviders; - } - - /** - * Sets the resource providers for this server - */ - public void setResourceProviders(IResourceProvider... theResourceProviders) { - myResourceProviders = Arrays.asList(theResourceProviders); - } - - /** - * Provide a server address strategy, which is used to determine what base URL to provide clients to refer to this - * server. Defaults to an instance of {@link IncomingRequestAddressStrategy} - */ - public void setServerAddressStrategy(IServerAddressStrategy theServerAddressStrategy) { - Validate.notNull(theServerAddressStrategy, "Server address strategy can not be null"); - myServerAddressStrategy = theServerAddressStrategy; - } - - /** - * Returns the server conformance provider, which is the provider that is used to generate the server's conformance - * (metadata) statement. - *

- * By default, the ServerConformanceProvider implementation for the declared version of FHIR is used, but this can - * be changed, or set to null if you do not wish to export a conformance statement. - *

- * Note that this method can only be called before the server is initialized. - * - * @throws IllegalStateException - * Note that this method can only be called prior to {@link #init() initialization} and will throw an - * {@link IllegalStateException} if called after that. - */ - public void setServerConformanceProvider(Object theServerConformanceProvider) { - if (myStarted) { - throw new IllegalStateException("Server is already started"); - } - myServerConformanceProvider = theServerConformanceProvider; - } - - /** - * Sets the server's name, as exported in conformance profiles exported by the server. This is informational only, - * but can be helpful to set with something appropriate. - */ - public void setServerName(String theServerName) { - myServerName = theServerName; - } - - /** - * Gets the server's version, as exported in conformance profiles exported by the server. This is informational - * only, but can be helpful to set with something appropriate. - */ - public void setServerVersion(String theServerVersion) { - myServerVersion = theServerVersion; - } - - /** - * If set to true (default is false), the server will use browser friendly content-types (instead of - * standard FHIR ones) when it detects that the request is coming from a browser instead of a FHIR - */ - public void setUseBrowserFriendlyContentTypes(boolean theUseBrowserFriendlyContentTypes) { - myUseBrowserFriendlyContentTypes = theUseBrowserFriendlyContentTypes; - } - - public void unregisterInterceptor(IServerInterceptor theInterceptor) { - Validate.notNull(theInterceptor, "Interceptor can not be null"); - myInterceptors.remove(theInterceptor); - } - - private void writeExceptionToResponse(HttpServletResponse theResponse, BaseServerResponseException theException) throws IOException { - theResponse.setStatus(theException.getStatusCode()); - addHeadersToResponse(theResponse); - theResponse.setContentType("text/plain"); - theResponse.setCharacterEncoding("UTF-8"); - theResponse.getWriter().write(theException.getMessage()); - } - - private static boolean partIsOperation(String nextString) { - return nextString.length() > 0 && (nextString.charAt(0) == '_' || nextString.charAt(0) == '$'); - } - - public enum NarrativeModeEnum { - NORMAL, ONLY, SUPPRESS; - - public static NarrativeModeEnum valueOfCaseInsensitive(String theCode) { - return valueOf(NarrativeModeEnum.class, theCode.toUpperCase()); - } - } -} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/SimpleBundleProvider.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/SimpleBundleProvider.java index 7cb5b3460b1..1c004974537 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/SimpleBundleProvider.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/SimpleBundleProvider.java @@ -59,5 +59,10 @@ public class SimpleBundleProvider implements IBundleProvider { public InstantDt getPublished() { return InstantDt.withCurrentTime(); } + + @Override + public Integer preferredPageSize() { + return null; + } } 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 809299db977..3f8eba5fdb6 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 @@ -494,6 +494,11 @@ public abstract class BaseFhirDao implements IDao { public int size() { return tuples.size(); } + + @Override + public Integer preferredPageSize() { + return null; + } }; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseFhirResourceDao.java index dd3ba463108..e86e0f6611b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseFhirResourceDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseFhirResourceDao.java @@ -659,6 +659,18 @@ public abstract class BaseFhirResourceDao extends BaseFhirD return new HashSet(q.getResultList()); } + private List addResourcesAsIncludesById(List theListToPopulate, Set includePids, List resources) { + if (!includePids.isEmpty()) { + ourLog.info("Loading {} included resources", includePids.size()); + resources = loadResourcesById(includePids); + for (IResource next : resources) { + ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(next, BundleEntrySearchModeEnum.INCLUDE); + } + theListToPopulate.addAll(resources); + } + return resources; + } + @Override public void addTag(IdDt theId, TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel) { StopWatch w = new StopWatch(); @@ -713,50 +725,6 @@ public abstract class BaseFhirResourceDao extends BaseFhirD return doCreate(theResource, theIfNoneExist, thePerformIndexing); } - private DaoMethodOutcome doCreate(T theResource, String theIfNoneExist, boolean thePerformIndexing) { - StopWatch w = new StopWatch(); - ResourceTable entity = new ResourceTable(); - entity.setResourceType(toResourceName(theResource)); - - if (isNotBlank(theIfNoneExist)) { - Set match = processMatchUrl(theIfNoneExist, myResourceType); - if (match.size() > 1) { - String msg = getContext().getLocalizer().getMessage(BaseFhirDao.class, "transactionOperationWithMultipleMatchFailure", "CREATE", theIfNoneExist, match.size()); - throw new PreconditionFailedException(msg); - } else if (match.size() == 1) { - Long pid = match.iterator().next(); - entity = myEntityManager.find(ResourceTable.class, pid); - return toMethodOutcome(entity, theResource).setCreated(false); - } - } - - if (theResource.getId().isEmpty() == false) { - if (isValidPid(theResource.getId())) { - throw new UnprocessableEntityException( - "This server cannot create an entity with a user-specified numeric ID - Client should not specify an ID when creating a new resource, or should include at least one letter in the ID to force a client-defined ID"); - } - createForcedIdIfNeeded(entity, theResource.getId()); - - if (entity.getForcedId() != null) { - try { - translateForcedIdToPid(theResource.getId()); - throw new UnprocessableEntityException(getContext().getLocalizer().getMessage(BaseFhirResourceDao.class, "duplicateCreateForcedId", theResource.getId().getIdPart())); - } catch (ResourceNotFoundException e) { - // good, this ID doesn't exist so we can create it - } - } - - } - - updateEntity(theResource, entity, false, null, thePerformIndexing, true); - - DaoMethodOutcome outcome = toMethodOutcome(entity, theResource).setCreated(true); - - notifyWriteCompleted(); - ourLog.info("Processed create on {} in {}ms", myResourceName, w.getMillisAndRestart()); - return outcome; - } - private Predicate createCompositeParamPart(CriteriaBuilder builder, Root from, RuntimeSearchParam left, IQueryParameterType leftValue) { Predicate retVal = null; switch (left.getParamType()) { @@ -979,6 +947,50 @@ public abstract class BaseFhirResourceDao extends BaseFhirD return toMethodOutcome(savedEntity, null); } + private DaoMethodOutcome doCreate(T theResource, String theIfNoneExist, boolean thePerformIndexing) { + StopWatch w = new StopWatch(); + ResourceTable entity = new ResourceTable(); + entity.setResourceType(toResourceName(theResource)); + + if (isNotBlank(theIfNoneExist)) { + Set match = processMatchUrl(theIfNoneExist, myResourceType); + if (match.size() > 1) { + String msg = getContext().getLocalizer().getMessage(BaseFhirDao.class, "transactionOperationWithMultipleMatchFailure", "CREATE", theIfNoneExist, match.size()); + throw new PreconditionFailedException(msg); + } else if (match.size() == 1) { + Long pid = match.iterator().next(); + entity = myEntityManager.find(ResourceTable.class, pid); + return toMethodOutcome(entity, theResource).setCreated(false); + } + } + + if (theResource.getId().isEmpty() == false) { + if (isValidPid(theResource.getId())) { + throw new UnprocessableEntityException( + "This server cannot create an entity with a user-specified numeric ID - Client should not specify an ID when creating a new resource, or should include at least one letter in the ID to force a client-defined ID"); + } + createForcedIdIfNeeded(entity, theResource.getId()); + + if (entity.getForcedId() != null) { + try { + translateForcedIdToPid(theResource.getId()); + throw new UnprocessableEntityException(getContext().getLocalizer().getMessage(BaseFhirResourceDao.class, "duplicateCreateForcedId", theResource.getId().getIdPart())); + } catch (ResourceNotFoundException e) { + // good, this ID doesn't exist so we can create it + } + } + + } + + updateEntity(theResource, entity, false, null, thePerformIndexing, true); + + DaoMethodOutcome outcome = toMethodOutcome(entity, theResource).setCreated(true); + + notifyWriteCompleted(); + ourLog.info("Processed create on {} in {}ms", myResourceName, w.getMillisAndRestart()); + return outcome; + } + @Override public TagList getAllResourceTags() { StopWatch w = new StopWatch(); @@ -987,6 +999,8 @@ public abstract class BaseFhirResourceDao extends BaseFhirD return tags; } + protected abstract List getIncludeValues(FhirTerser theTerser, Include theInclude, IResource theResource, RuntimeResourceDefinition theResourceDef); + public Class getResourceType() { return myResourceType; } @@ -1092,6 +1106,11 @@ public abstract class BaseFhirResourceDao extends BaseFhirD return retVal; } + @Override + public Integer preferredPageSize() { + return null; + } + @Override public int size() { return count; @@ -1140,6 +1159,159 @@ public abstract class BaseFhirResourceDao extends BaseFhirD } } + protected void loadReverseIncludes(List theMatches, Set theRevIncludes) { + if (theMatches.size() == 0) { + return; + } + + HashSet pidsToInclude = new HashSet(); + + for (Include nextInclude : theRevIncludes) { + boolean matchAll = "*".equals(nextInclude.getValue()); + if (matchAll) { + String sql = "SELECT r FROM ResourceLink r WHERE r.myTargetResourcePid IN (:target_pids)"; + TypedQuery q = myEntityManager.createQuery(sql, ResourceLink.class); + q.setParameter("target_pids", theMatches); + List results = q.getResultList(); + for (ResourceLink resourceLink : results) { + pidsToInclude.add(resourceLink.getSourceResourcePid()); + } + } else { + int colonIdx = nextInclude.getValue().indexOf(':'); + if (colonIdx < 2) { + continue; + } + String resType = nextInclude.getValue().substring(0, colonIdx); + RuntimeResourceDefinition def = getContext().getResourceDefinition(resType); + if (def == null) { + ourLog.warn("Unknown resource type in _revinclude=" + nextInclude.getValue()); + continue; + } + + String paramName = nextInclude.getValue().substring(colonIdx + 1); + RuntimeSearchParam param = def.getSearchParam(paramName); + if (param == null) { + ourLog.warn("Unknown param name in _revinclude=" + nextInclude.getValue()); + continue; + } + + for (String nextPath : param.getPathsSplit()) { + String sql = "SELECT r FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r.myTargetResourcePid IN (:target_pids)"; + TypedQuery q = myEntityManager.createQuery(sql, ResourceLink.class); + q.setParameter("src_path", nextPath); + q.setParameter("target_pids", theMatches); + List results = q.getResultList(); + for (ResourceLink resourceLink : results) { + pidsToInclude.add(resourceLink.getSourceResourcePid()); + } + } + } + } + + theMatches.addAll(pidsToInclude); + } + + @Override + public MetaDt metaAddOperation(IdDt theResourceId, MetaDt theMetaAdd) { + StopWatch w = new StopWatch(); + BaseHasResource entity = readEntity(theResourceId); + if (entity == null) { + throw new ResourceNotFoundException(theResourceId); + } + + List tags = toTagList(theMetaAdd); + + //@formatter:off + for (TagDefinition nextDef : tags) { + + boolean hasTag = false; + for (BaseTag next : new ArrayList(entity.getTags())) { + if (ObjectUtil.equals(next.getTag().getTagType(), nextDef.getTagType()) && + ObjectUtil.equals(next.getTag().getSystem(), nextDef.getSystem()) && + ObjectUtil.equals(next.getTag().getCode(), nextDef.getCode())) { + hasTag = true; + break; + } + } + + if (!hasTag) { + entity.setHasTags(true); + + TagDefinition def = getTag(nextDef.getTagType(), nextDef.getSystem(), nextDef.getCode(), nextDef.getDisplay()); + BaseTag newEntity = entity.addTag(def); + myEntityManager.persist(newEntity); + } + } + //@formatter:on + + myEntityManager.merge(entity); + notifyWriteCompleted(); + ourLog.info("Processed metaAddOperation on {} in {}ms", new Object[] { theResourceId, w.getMillisAndRestart() }); + + return metaGetOperation(theResourceId); + } + + @Override + public MetaDt metaDeleteOperation(IdDt theResourceId, MetaDt theMetaDel) { + StopWatch w = new StopWatch(); + BaseHasResource entity = readEntity(theResourceId); + if (entity == null) { + throw new ResourceNotFoundException(theResourceId); + } + + List tags = toTagList(theMetaDel); + + //@formatter:off + for (TagDefinition nextDef : tags) { + for (BaseTag next : new ArrayList(entity.getTags())) { + if (ObjectUtil.equals(next.getTag().getTagType(), nextDef.getTagType()) && + ObjectUtil.equals(next.getTag().getSystem(), nextDef.getSystem()) && + ObjectUtil.equals(next.getTag().getCode(), nextDef.getCode())) { + myEntityManager.remove(next); + entity.getTags().remove(next); + } + } + } + //@formatter:on + + if (entity.getTags().isEmpty()) { + entity.setHasTags(false); + } + + myEntityManager.merge(entity); + + ourLog.info("Processed metaDeleteOperation on {} in {}ms", new Object[] { theResourceId.getValue(), w.getMillisAndRestart() }); + + return metaGetOperation(theResourceId); + } + + @Override + public MetaDt metaGetOperation() { + String sql = "SELECT d FROM TagDefinition d WHERE d.myId IN (SELECT DISTINCT t.myTagId FROM ResourceTag t WHERE t.myResourceType = :res_type)"; + TypedQuery q = myEntityManager.createQuery(sql, TagDefinition.class); + q.setParameter("res_type", myResourceName); + List tagDefinitions = q.getResultList(); + + MetaDt retVal = super.toMetaDt(tagDefinitions); + + return retVal; + } + + @Override + public MetaDt metaGetOperation(IdDt theId) { + Long pid = super.translateForcedIdToPid(theId); + + String sql = "SELECT d FROM TagDefinition d WHERE d.myId IN (SELECT DISTINCT t.myTagId FROM ResourceTag t WHERE t.myResourceType = :res_type AND t.myResourceId = :res_id)"; + TypedQuery q = myEntityManager.createQuery(sql, TagDefinition.class); + q.setParameter("res_type", myResourceName); + q.setParameter("res_id", pid); + List tagDefinitions = q.getResultList(); + + MetaDt retVal = super.toMetaDt(tagDefinitions); + + return retVal; + } + @PostConstruct public void postConstruct() { RuntimeResourceDefinition def = getContext().getResourceDefinition(myResourceType); @@ -1341,7 +1513,6 @@ public abstract class BaseFhirResourceDao extends BaseFhirD } IBundleProvider retVal = new IBundleProvider() { - @Override public InstantDt getPublished() { return now; @@ -1422,6 +1593,11 @@ public abstract class BaseFhirResourceDao extends BaseFhirD }); } + @Override + public Integer preferredPageSize() { + return theParams.getCount(); + } + @Override public int size() { return pids.size(); @@ -1433,77 +1609,11 @@ public abstract class BaseFhirResourceDao extends BaseFhirD return retVal; } - private List addResourcesAsIncludesById(List theListToPopulate, Set includePids, List resources) { - if (!includePids.isEmpty()) { - ourLog.info("Loading {} included resources", includePids.size()); - resources = loadResourcesById(includePids); - for (IResource next : resources) { - ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(next, BundleEntrySearchModeEnum.INCLUDE); - } - theListToPopulate.addAll(resources); - } - return resources; - } - - protected void loadReverseIncludes(List theMatches, Set theRevIncludes) { - if (theMatches.size() == 0) { - return; - } - - HashSet pidsToInclude = new HashSet(); - - for (Include nextInclude : theRevIncludes) { - boolean matchAll = "*".equals(nextInclude.getValue()); - if (matchAll) { - String sql = "SELECT r FROM ResourceLink r WHERE r.myTargetResourcePid IN (:target_pids)"; - TypedQuery q = myEntityManager.createQuery(sql, ResourceLink.class); - q.setParameter("target_pids", theMatches); - List results = q.getResultList(); - for (ResourceLink resourceLink : results) { - pidsToInclude.add(resourceLink.getSourceResourcePid()); - } - } else { - int colonIdx = nextInclude.getValue().indexOf(':'); - if (colonIdx < 2) { - continue; - } - String resType = nextInclude.getValue().substring(0, colonIdx); - RuntimeResourceDefinition def = getContext().getResourceDefinition(resType); - if (def == null) { - ourLog.warn("Unknown resource type in _revinclude=" + nextInclude.getValue()); - continue; - } - - String paramName = nextInclude.getValue().substring(colonIdx + 1); - RuntimeSearchParam param = def.getSearchParam(paramName); - if (param == null) { - ourLog.warn("Unknown param name in _revinclude=" + nextInclude.getValue()); - continue; - } - - for (String nextPath : param.getPathsSplit()) { - String sql = "SELECT r FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r.myTargetResourcePid IN (:target_pids)"; - TypedQuery q = myEntityManager.createQuery(sql, ResourceLink.class); - q.setParameter("src_path", nextPath); - q.setParameter("target_pids", theMatches); - List results = q.getResultList(); - for (ResourceLink resourceLink : results) { - pidsToInclude.add(resourceLink.getSourceResourcePid()); - } - } - } - } - - theMatches.addAll(pidsToInclude); - } - @Override public IBundleProvider search(String theParameterName, IQueryParameterType theValue) { return search(Collections.singletonMap(theParameterName, theValue)); } - protected abstract List getIncludeValues(FhirTerser theTerser, Include theInclude, IResource theResource, RuntimeResourceDefinition theResourceDef); - @Override public Set searchForIds(Map theParams) { SearchParameterMap map = new SearchParameterMap(); @@ -1708,6 +1818,22 @@ public abstract class BaseFhirResourceDao extends BaseFhirD return qp; } + private ArrayList toTagList(MetaDt theMeta) { + ArrayList retVal = new ArrayList(); + + for (CodingDt next : theMeta.getTag()) { + retVal.add(new TagDefinition(TagTypeEnum.TAG, next.getSystem(), next.getCode(), next.getDisplay())); + } + for (CodingDt next : theMeta.getSecurity()) { + retVal.add(new TagDefinition(TagTypeEnum.SECURITY_LABEL, next.getSystem(), next.getCode(), next.getDisplay())); + } + for (UriDt next : theMeta.getProfile()) { + retVal.add(new TagDefinition(TagTypeEnum.PROFILE, BaseFhirDao.NS_JPA_PROFILE, next.getValue(), null)); + } + + return retVal; + } + @Override public DaoMethodOutcome update(T theResource) { return update(theResource, null); @@ -1788,121 +1914,4 @@ public abstract class BaseFhirResourceDao extends BaseFhirD } } - @Override - public MetaDt metaGetOperation() { - String sql = "SELECT d FROM TagDefinition d WHERE d.myId IN (SELECT DISTINCT t.myTagId FROM ResourceTag t WHERE t.myResourceType = :res_type)"; - TypedQuery q = myEntityManager.createQuery(sql, TagDefinition.class); - q.setParameter("res_type", myResourceName); - List tagDefinitions = q.getResultList(); - - MetaDt retVal = super.toMetaDt(tagDefinitions); - - return retVal; - } - - @Override - public MetaDt metaGetOperation(IdDt theId) { - Long pid = super.translateForcedIdToPid(theId); - - String sql = "SELECT d FROM TagDefinition d WHERE d.myId IN (SELECT DISTINCT t.myTagId FROM ResourceTag t WHERE t.myResourceType = :res_type AND t.myResourceId = :res_id)"; - TypedQuery q = myEntityManager.createQuery(sql, TagDefinition.class); - q.setParameter("res_type", myResourceName); - q.setParameter("res_id", pid); - List tagDefinitions = q.getResultList(); - - MetaDt retVal = super.toMetaDt(tagDefinitions); - - return retVal; - } - - @Override - public MetaDt metaDeleteOperation(IdDt theResourceId, MetaDt theMetaDel) { - StopWatch w = new StopWatch(); - BaseHasResource entity = readEntity(theResourceId); - if (entity == null) { - throw new ResourceNotFoundException(theResourceId); - } - - List tags = toTagList(theMetaDel); - - //@formatter:off - for (TagDefinition nextDef : tags) { - for (BaseTag next : new ArrayList(entity.getTags())) { - if (ObjectUtil.equals(next.getTag().getTagType(), nextDef.getTagType()) && - ObjectUtil.equals(next.getTag().getSystem(), nextDef.getSystem()) && - ObjectUtil.equals(next.getTag().getCode(), nextDef.getCode())) { - myEntityManager.remove(next); - entity.getTags().remove(next); - } - } - } - //@formatter:on - - if (entity.getTags().isEmpty()) { - entity.setHasTags(false); - } - - myEntityManager.merge(entity); - - ourLog.info("Processed metaDeleteOperation on {} in {}ms", new Object[] { theResourceId.getValue(), w.getMillisAndRestart() }); - - return metaGetOperation(theResourceId); - } - - @Override - public MetaDt metaAddOperation(IdDt theResourceId, MetaDt theMetaAdd) { - StopWatch w = new StopWatch(); - BaseHasResource entity = readEntity(theResourceId); - if (entity == null) { - throw new ResourceNotFoundException(theResourceId); - } - - List tags = toTagList(theMetaAdd); - - //@formatter:off - for (TagDefinition nextDef : tags) { - - boolean hasTag = false; - for (BaseTag next : new ArrayList(entity.getTags())) { - if (ObjectUtil.equals(next.getTag().getTagType(), nextDef.getTagType()) && - ObjectUtil.equals(next.getTag().getSystem(), nextDef.getSystem()) && - ObjectUtil.equals(next.getTag().getCode(), nextDef.getCode())) { - hasTag = true; - break; - } - } - - if (!hasTag) { - entity.setHasTags(true); - - TagDefinition def = getTag(nextDef.getTagType(), nextDef.getSystem(), nextDef.getCode(), nextDef.getDisplay()); - BaseTag newEntity = entity.addTag(def); - myEntityManager.persist(newEntity); - } - } - //@formatter:on - - myEntityManager.merge(entity); - notifyWriteCompleted(); - ourLog.info("Processed metaAddOperation on {} in {}ms", new Object[] { theResourceId, w.getMillisAndRestart() }); - - return metaGetOperation(theResourceId); - } - - private ArrayList toTagList(MetaDt theMeta) { - ArrayList retVal = new ArrayList(); - - for (CodingDt next : theMeta.getTag()) { - retVal.add(new TagDefinition(TagTypeEnum.TAG, next.getSystem(), next.getCode(), next.getDisplay())); - } - for (CodingDt next : theMeta.getSecurity()) { - retVal.add(new TagDefinition(TagTypeEnum.SECURITY_LABEL, next.getSystem(), next.getCode(), next.getDisplay())); - } - for (UriDt next : theMeta.getProfile()) { - retVal.add(new TagDefinition(TagTypeEnum.PROFILE, BaseFhirDao.NS_JPA_PROFILE, next.getValue(), null)); - } - - return retVal; - } - } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2Test.java index d6f2f55b604..887f7e00162 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2Test.java @@ -15,6 +15,7 @@ import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.TreeSet; import java.util.concurrent.TimeUnit; import org.apache.commons.io.IOUtils; @@ -50,6 +51,7 @@ import ca.uhn.fhir.model.dstu.resource.Practitioner; import ca.uhn.fhir.model.dstu2.composite.PeriodDt; import ca.uhn.fhir.model.dstu2.composite.ResourceReferenceDt; import ca.uhn.fhir.model.dstu2.resource.Bundle.Entry; +import ca.uhn.fhir.model.dstu2.resource.Condition; import ca.uhn.fhir.model.dstu2.resource.DiagnosticOrder; import ca.uhn.fhir.model.dstu2.resource.DocumentManifest; import ca.uhn.fhir.model.dstu2.resource.DocumentReference; @@ -62,7 +64,9 @@ import ca.uhn.fhir.model.dstu2.resource.Parameters; import ca.uhn.fhir.model.dstu2.resource.Patient; import ca.uhn.fhir.model.dstu2.valueset.EncounterClassEnum; import ca.uhn.fhir.model.dstu2.valueset.EncounterStateEnum; +import ca.uhn.fhir.model.dstu2.valueset.HTTPVerbEnum; import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.model.primitive.IntegerDt; import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum; import ca.uhn.fhir.model.valueset.BundleTypeEnum; import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; @@ -127,15 +131,15 @@ public class ResourceProviderDstu2Test { HttpPost post = new HttpPost(ourServerBase + "/Patient"); post.setEntity(new StringEntity(resource, ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); - + CloseableHttpResponse response = ourHttpClient.execute(post); try { - + assertEquals(201, response.getStatusLine().getStatusCode()); assertThat(response.getFirstHeader(Constants.HEADER_LOCATION_LC).getValue(), startsWith(ourServerBase + "/Patient/")); assertThat(response.getFirstHeader(Constants.HEADER_LOCATION_LC).getValue(), endsWith("/_history/1")); assertThat(response.getFirstHeader(Constants.HEADER_LOCATION_LC).getValue(), not(containsString("1777"))); - + } finally { response.close(); } @@ -144,7 +148,7 @@ public class ResourceProviderDstu2Test { @Test public void testCreateResourceConditional() throws IOException { String methodName = "testCreateResourceConditional"; - + Patient pt = new Patient(); pt.addName().addFamily(methodName); String resource = ourFhirCtx.newXmlParser().encodeResourceToString(pt); @@ -161,7 +165,7 @@ public class ResourceProviderDstu2Test { } finally { response.close(); } - + post = new HttpPost(ourServerBase + "/Patient"); post.setEntity(new StringEntity(resource, ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); post.addHeader(Constants.HEADER_IF_NONE_EXIST, "Patient?name=" + methodName); @@ -173,13 +177,13 @@ public class ResourceProviderDstu2Test { } finally { response.close(); } - + } @Test public void testUpdateResourceConditional() throws IOException { String methodName = "testUpdateResourceConditional"; - + Patient pt = new Patient(); pt.addName().addFamily(methodName); String resource = ourFhirCtx.newXmlParser().encodeResourceToString(pt); @@ -196,7 +200,7 @@ public class ResourceProviderDstu2Test { } finally { response.close(); } - + HttpPut put = new HttpPut(ourServerBase + "/Patient?name=" + methodName); put.setEntity(new StringEntity(resource, ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); response = ourHttpClient.execute(put); @@ -208,13 +212,13 @@ public class ResourceProviderDstu2Test { } finally { response.close(); } - + } - + @Test public void testDeleteResourceConditional() throws IOException { String methodName = "testDeleteResourceConditional"; - + Patient pt = new Patient(); pt.addName().addFamily(methodName); String resource = ourFhirCtx.newXmlParser().encodeResourceToString(pt); @@ -231,7 +235,7 @@ public class ResourceProviderDstu2Test { } finally { response.close(); } - + HttpDelete delete = new HttpDelete(ourServerBase + "/Patient?name=" + methodName); response = ourHttpClient.execute(delete); try { @@ -239,7 +243,7 @@ public class ResourceProviderDstu2Test { } finally { response.close(); } - + HttpGet read = new HttpGet(ourServerBase + "/Patient/" + id.getIdPart()); response = ourHttpClient.execute(read); try { @@ -257,7 +261,7 @@ public class ResourceProviderDstu2Test { @Test public void testReadAllInstancesOfType() throws Exception { Patient pat; - + pat = new Patient(); pat.addIdentifier().setSystem("urn:system").setValue("testReadAllInstancesOfType_01"); ourClient.create().resource(pat).prettyPrint().encodedXml().execute().getId(); @@ -277,11 +281,10 @@ public class ResourceProviderDstu2Test { } } - @Test public void testSearchWithInclude() throws Exception { Organization org = new Organization(); - org.addIdentifier().setSystem("urn:system:rpdstu2").setValue( "testSearchWithInclude01"); + org.addIdentifier().setSystem("urn:system:rpdstu2").setValue("testSearchWithInclude01"); IdDt orgId = ourClient.create().resource(org).prettyPrint().encodedXml().execute().getId(); Patient pat = new Patient(); @@ -298,7 +301,7 @@ public class ResourceProviderDstu2Test { .prettyPrint() .execute(); //@formatter:on - + assertEquals(2, found.size()); assertEquals(Patient.class, found.getEntries().get(0).getResource().getClass()); assertEquals(BundleEntrySearchModeEnum.MATCH, found.getEntries().get(0).getSearchMode().getValueAsEnum()); @@ -311,16 +314,16 @@ public class ResourceProviderDstu2Test { @Test public void testEverythingOperation() throws Exception { String methodName = "testEverythingOperation"; - + Organization org1 = new Organization(); org1.setName(methodName + "1"); IdDt orgId1 = ourClient.create().resource(org1).execute().getId(); - + Patient p = new Patient(); p.addName().addFamily(methodName); p.getManagingOrganization().setReference(orgId1); IdDt patientId = ourClient.create().resource(p).execute().getId(); - + Organization org2 = new Organization(); org2.setName(methodName + "1"); IdDt orgId2 = ourClient.create().resource(org2).execute().getId(); @@ -328,31 +331,30 @@ public class ResourceProviderDstu2Test { Device dev = new Device(); dev.setModel(methodName); dev.getOwner().setReference(orgId2); - IdDt devId = ourClient.create().resource(dev).execute().getId(); - + IdDt devId = ourClient.create().resource(dev).execute().getId(); + Observation obs = new Observation(); obs.getSubject().setReference(patientId); obs.getDevice().setReference(devId); IdDt obsId = ourClient.create().resource(obs).execute().getId(); - + Encounter enc = new Encounter(); enc.getPatient().setReference(patientId); IdDt encId = ourClient.create().resource(enc).execute().getId(); - + Parameters output = ourClient.operation().onInstance(patientId).named("everything").withNoParameters(Parameters.class).execute(); ca.uhn.fhir.model.dstu2.resource.Bundle b = (ca.uhn.fhir.model.dstu2.resource.Bundle) output.getParameterFirstRep().getResource(); - + Set ids = new HashSet(); for (Entry next : b.getEntry()) { ids.add(next.getResource().getId()); } - - + assertThat(ids, containsInAnyOrder(patientId, devId, obsId, encId, orgId1, orgId2)); - + // _revinclude's are counted but not _include's assertEquals(3, b.getTotal().intValue()); - + ourLog.info(ids.toString()); } @@ -360,33 +362,101 @@ public class ResourceProviderDstu2Test { * See #147 */ @Test - public void testEverythingDoesnRepeatPatient() throws Exception { + public void testEverythingDoesntRepeatPatient() throws Exception { ca.uhn.fhir.model.dstu2.resource.Bundle b; b = ourFhirCtx.newJsonParser().parseResource(ca.uhn.fhir.model.dstu2.resource.Bundle.class, new InputStreamReader(ResourceProviderDstu2Test.class.getResourceAsStream("/bug147-bundle.json"))); - + ca.uhn.fhir.model.dstu2.resource.Bundle resp = ourClient.transaction().withBundle(b).execute(); - - ourLog.info(ourFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(resp)); - - IdDt patientId = new IdDt(resp.getEntry().get(1).getTransactionResponse().getLocation()); - assertEquals("Patient", patientId.getResourceType()); - - Parameters output = ourClient.operation().onInstance(patientId).named("everything").withNoParameters(Parameters.class).execute(); - b = (ca.uhn.fhir.model.dstu2.resource.Bundle) output.getParameterFirstRep().getResource(); - List ids = new ArrayList(); - boolean dupes = false; - for (Entry next : b.getEntry()) { - IdDt toAdd = next.getResource().getId().toUnqualifiedVersionless(); - dupes = dupes | ids.contains(toAdd); + for (Entry next : resp.getEntry()) { + IdDt toAdd = new IdDt(next.getTransactionResponse().getLocation()).toUnqualifiedVersionless(); ids.add(toAdd); } - - ourLog.info(ids.toString()); - - assertFalse(ids.toString(), dupes); + ourLog.info("Created: " + ids.toString()); + + IdDt patientId = new IdDt(resp.getEntry().get(1).getTransactionResponse().getLocation()); + assertEquals("Patient", patientId.getResourceType()); + + { + Parameters output = ourClient.operation().onInstance(patientId).named("everything").withNoParameters(Parameters.class).execute(); + b = (ca.uhn.fhir.model.dstu2.resource.Bundle) output.getParameterFirstRep().getResource(); + + ids = new ArrayList(); + boolean dupes = false; + for (Entry next : b.getEntry()) { + IdDt toAdd = next.getResource().getId().toUnqualifiedVersionless(); + dupes = dupes | ids.contains(toAdd); + ids.add(toAdd); + } + ourLog.info("$everything: " + ids.toString()); + + assertFalse(ids.toString(), dupes); + + /* + * Condition/11 is the 11th resource and the default page size is 10 so we don't show the 11th. + */ + assertThat(ids.toString(), not(containsString("Condition"))); + } + + /* + * Now try with a size specified + */ + { + Parameters input = new Parameters(); + input.addParameter().setName(Constants.PARAM_COUNT).setValue(new IntegerDt(100)); + Parameters output = ourClient.operation().onInstance(patientId).named("everything").withParameters(input).execute(); + b = (ca.uhn.fhir.model.dstu2.resource.Bundle) output.getParameterFirstRep().getResource(); + + ids = new ArrayList(); + boolean dupes = false; + for (Entry next : b.getEntry()) { + IdDt toAdd = next.getResource().getId().toUnqualifiedVersionless(); + dupes = dupes | ids.contains(toAdd); + ids.add(toAdd); + } + ourLog.info("$everything: " + ids.toString()); + + assertFalse(ids.toString(), dupes); + assertThat(ids.toString(), containsString("Condition")); + + } } - + + /** + * See #148 + */ + @Test + public void testEverythingIncludesCondition() throws Exception { + ca.uhn.fhir.model.dstu2.resource.Bundle b = new ca.uhn.fhir.model.dstu2.resource.Bundle(); + Patient p = new Patient(); + p.setId("1"); + b.addEntry().setResource(p).getTransaction().setMethod(HTTPVerbEnum.POST); + + Condition c = new Condition(); + c.getPatient().setReference("Patient/1"); + b.addEntry().setResource(c).getTransaction().setMethod(HTTPVerbEnum.POST); + + ca.uhn.fhir.model.dstu2.resource.Bundle resp = ourClient.transaction().withBundle(b).execute(); + + ourLog.info(ourFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(resp)); + + IdDt patientId = new IdDt(resp.getEntry().get(1).getTransactionResponse().getLocation()); + assertEquals("Patient", patientId.getResourceType()); + + Parameters output = ourClient.operation().onInstance(patientId).named("everything").withNoParameters(Parameters.class).execute(); + b = (ca.uhn.fhir.model.dstu2.resource.Bundle) output.getParameterFirstRep().getResource(); + + List ids = new ArrayList(); + for (Entry next : b.getEntry()) { + IdDt toAdd = next.getResource().getId().toUnqualifiedVersionless(); + ids.add(toAdd); + } + + assertThat(ids.toString(), containsString("Patient/")); + assertThat(ids.toString(), containsString("Condition/")); + + } + @Test public void testCountParam() throws Exception { // NB this does not get used- The paging provider has its own limits built in @@ -435,7 +505,7 @@ public class ResourceProviderDstu2Test { public void testDocumentManifestResources() throws Exception { ourFhirCtx.getResourceDefinition(Practitioner.class); ourFhirCtx.getResourceDefinition(ca.uhn.fhir.model.dstu.resource.DocumentManifest.class); - + IGenericClient client = ourClient; int initialSize = client.search().forResource(DocumentManifest.class).execute().size(); @@ -477,7 +547,7 @@ public class ResourceProviderDstu2Test { int initialSize = client.search().forResource(DiagnosticOrder.class).execute().size(); DiagnosticOrder res = new DiagnosticOrder(); - res.addIdentifier().setSystem("urn:foo").setValue( "123"); + res.addIdentifier().setSystem("urn:foo").setValue("123"); client.create().resource(res).execute(); @@ -737,7 +807,7 @@ public class ResourceProviderDstu2Test { RestfulServer restServer = new RestfulServer(); ourFhirCtx = FhirContext.forDstu2(); restServer.setFhirContext(ourFhirCtx); - + ourServerBase = "http://localhost:" + port + "/fhir/context"; ourAppCtx = new ClassPathXmlApplicationContext("hapi-fhir-server-resourceproviders-dstu2.xml", "fhir-jpabase-spring-test-config.xml"); @@ -771,7 +841,7 @@ public class ResourceProviderDstu2Test { ourFhirCtx.getRestfulClientFactory().setSocketTimeout(1200 * 1000); ourClient = ourFhirCtx.newRestfulGenericClient(ourServerBase); ourClient.registerInterceptor(new LoggingInterceptor(true)); - + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); HttpClientBuilder builder = HttpClientBuilder.create(); builder.setConnectionManager(connectionManager); diff --git a/hapi-fhir-jpaserver-base/src/test/resources/bug147-bundle.json b/hapi-fhir-jpaserver-base/src/test/resources/bug147-bundle.json index 91d96a32f0e..4e75e766114 100644 --- a/hapi-fhir-jpaserver-base/src/test/resources/bug147-bundle.json +++ b/hapi-fhir-jpaserver-base/src/test/resources/bug147-bundle.json @@ -24,7 +24,11 @@ "entry": [ { - "transaction": { "method":"POST" }, + "transaction": + { + "method": "POST" + }, + "resource": { "resourceType": "Patient", @@ -114,7 +118,11 @@ }, { - "transaction": { "method":"POST" }, + "transaction": + { + "method": "POST" + }, + "resource": { "resourceType": "Encounter", @@ -148,7 +156,11 @@ }, { - "transaction": { "method":"POST" }, + "transaction": + { + "method": "POST" + }, + "resource": { "resourceType": "ClinicalImpression", @@ -193,7 +205,11 @@ }, { - "transaction": { "method":"POST" }, + "transaction": + { + "method": "POST" + }, + "resource": { "resourceType": "ClinicalImpression", @@ -238,7 +254,11 @@ }, { - "transaction": { "method":"POST" }, + "transaction": + { + "method": "POST" + }, + "resource": { "resourceType": "Encounter", @@ -267,7 +287,11 @@ }, { - "transaction": { "method":"POST" }, + "transaction": + { + "method": "POST" + }, + "resource": { "resourceType": "ClinicalImpression", @@ -311,7 +335,11 @@ }, { - "transaction": { "method":"POST" }, + "transaction": + { + "method": "POST" + }, + "resource": { "resourceType": "Encounter", @@ -340,7 +368,11 @@ }, { - "transaction": { "method":"POST" }, + "transaction": + { + "method": "POST" + }, + "resource": { "resourceType": "ClinicalImpression", @@ -384,7 +416,11 @@ }, { - "transaction": { "method":"POST" }, + "transaction": + { + "method": "POST" + }, + "resource": { "resourceType": "Observation", @@ -417,7 +453,11 @@ }, { - "transaction": { "method":"POST" }, + "transaction": + { + "method": "POST" + }, + "resource": { "resourceType": "Observation", @@ -447,7 +487,50 @@ { "mode": "match" } - } + }, + { + "transaction": + { + "method": "POST" + }, + + "resource": + { + "resourceType": "Condition", + "id": "20443", + "meta": + { + "versionId": "1", + "lastUpdated": "2015-04-03T22:56:49.886-04:00" + }, + + "text": + { + "status": "generated", + "div": "
pharyngitis working
" + }, + + "patient": + { + "reference": "Patient/1702", + "display": "Eve Everywoman " + }, + + "code": + { + "coding": + [ + { + "system": "http://loinc.org", + "code": "28397-8", + "display": "pharyngitis" + } + ], + + "text": "pharyngitis" + } + } + } ] } \ No newline at end of file diff --git a/hapi-fhir-jpaserver-base/src/test/resources/fhir-jpabase-spring-test-config.xml b/hapi-fhir-jpaserver-base/src/test/resources/fhir-jpabase-spring-test-config.xml index 67dcf9c8611..4358f85d330 100644 --- a/hapi-fhir-jpaserver-base/src/test/resources/fhir-jpabase-spring-test-config.xml +++ b/hapi-fhir-jpaserver-base/src/test/resources/fhir-jpabase-spring-test-config.xml @@ -35,7 +35,7 @@ - + diff --git a/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/Dstu2BundleFactory.java b/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/Dstu2BundleFactory.java index 63139e1f4d9..433b161b092 100644 --- a/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/Dstu2BundleFactory.java +++ b/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/Dstu2BundleFactory.java @@ -268,7 +268,7 @@ public class Dstu2BundleFactory implements IVersionSpecificBundleFactory { } @Override - public IBaseResource getResourceBundle() { + public IResource getResourceBundle() { return myBundle; } diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 7aef9dde245..910e70b0c44 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -123,6 +123,9 @@ the main focus resource if it was referred to in a deep chain. Thanks to David Hay for reporting! + + JPA Server $everything operation now allows a _count parameter +