Add $get-resource-counts operation to JPA server

This commit is contained in:
jamesagnew 2015-07-17 18:28:47 -04:00
parent ddbe79cb86
commit 41283d4ed4
15 changed files with 625 additions and 167 deletions

View File

@ -26,7 +26,7 @@ import java.lang.annotation.Target;
*/
/**
* ResourceProvider methods tagged with @Destroy will be invoked when the RestfulServer is shut down.
* ResourceProvider methods tagged with {@link Destroy} will be invoked when the RestfulServer is shut down.
* This is your chance to do any cleanup.
*/
@Retention(RetentionPolicy.RUNTIME)

View File

@ -0,0 +1,35 @@
package ca.uhn.fhir.rest.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/*
* #%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%
*/
/**
* ResourceProvider methods tagged with {@link Initialize} will be invoked when the RestfulServer is starting up.
* This is your chance to do any initialization prior to the server taking any requests.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Initialize {
}

View File

@ -25,7 +25,8 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import ca.uhn.fhir.model.api.IResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import ca.uhn.fhir.rest.client.api.IBasicClient;
import ca.uhn.fhir.rest.client.api.IRestfulClient;
import ca.uhn.fhir.rest.server.IResourceProvider;
@ -57,7 +58,7 @@ public @interface Read {
* </p>
*/
// NB: Read, Search (maybe others) share this annotation, so update the javadocs everywhere
Class<? extends IResource> type() default IResource.class;
Class<? extends IBaseResource> type() default IBaseResource.class;
/**
* If set to true (default is false), this method supports vread operation as well as read

View File

@ -128,6 +128,9 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
return myDescription;
}
/**
* Returns the name of the operation, starting with "$"
*/
public String getName() {
return myName;
}

View File

@ -57,6 +57,7 @@ import ca.uhn.fhir.model.api.Include;
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.annotation.Initialize;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.method.BaseMethodBinding;
import ca.uhn.fhir.rest.method.ConformanceMethodBinding;
@ -91,7 +92,7 @@ public class RestfulServer extends HttpServlet {
private final List<IServerInterceptor> myInterceptors = new ArrayList<IServerInterceptor>();
private IPagingProvider myPagingProvider;
private Collection<Object> myPlainProviders;
private Map<String, ResourceBinding> myResourceNameToProvider = new HashMap<String, ResourceBinding>();
private Map<String, ResourceBinding> myResourceNameToBinding = new HashMap<String, ResourceBinding>();
private Collection<IResourceProvider> myResourceProviders;
private IServerAddressStrategy myServerAddressStrategy = new IncomingRequestAddressStrategy();
private ResourceBinding myServerBinding = new ResourceBinding();
@ -105,9 +106,8 @@ public class RestfulServer extends HttpServlet {
/**
* Constructor. Note that if no {@link FhirContext} is passed in to the server (either through the constructor, or
* through {@link #setFhirContext(FhirContext)}) the server will determine which version of FHIR to support
* through classpath scanning. This is brittle, and it is highly recommended to explicitly specify
* a FHIR version.
* through {@link #setFhirContext(FhirContext)}) the server will determine which version of FHIR to support through
* classpath scanning. This is brittle, and it is highly recommended to explicitly specify a FHIR version.
*/
public RestfulServer() {
this(null);
@ -123,7 +123,8 @@ public class RestfulServer extends HttpServlet {
/**
* This method is called prior to sending a response to incoming requests. It is used to add custom headers.
* <p>
* Use caution if overriding this method: it is recommended to call <code>super.addHeadersToResponse</code> to avoid inadvertantly disabling functionality.
* Use caution if overriding this method: it is recommended to call <code>super.addHeadersToResponse</code> to avoid
* inadvertantly disabling functionality.
* </p>
*/
public void addHeadersToResponse(HttpServletResponse theHttpResponse) {
@ -143,6 +144,14 @@ public class RestfulServer extends HttpServlet {
invokeDestroy(iResourceProvider);
}
}
if (myServerConformanceProvider != null) {
invokeDestroy(myServerConformanceProvider);
}
if (getPlainProviders() != null) {
for (Object next : getPlainProviders()) {
invokeDestroy(next);
}
}
}
@Override
@ -228,12 +237,12 @@ public class RestfulServer extends HttpServlet {
resourceBinding = myServerBinding;
} else {
RuntimeResourceDefinition definition = getFhirContext().getResourceDefinition(resourceName);
if (myResourceNameToProvider.containsKey(definition.getName())) {
resourceBinding = myResourceNameToProvider.get(definition.getName());
if (myResourceNameToBinding.containsKey(definition.getName())) {
resourceBinding = myResourceNameToBinding.get(definition.getName());
} else {
resourceBinding = new ResourceBinding();
resourceBinding.setResourceName(resourceName);
myResourceNameToProvider.put(resourceName, resourceBinding);
myResourceNameToBinding.put(resourceName, resourceBinding);
}
}
@ -284,7 +293,7 @@ public class RestfulServer extends HttpServlet {
} else {
myServerBinding.addMethod(foundMethodBinding);
}
ourLog.info(" * Method: {}#{} is a handler", theSystemProvider.getClass(), m.getName());
ourLog.debug(" * Method: {}#{} is a handler", theSystemProvider.getClass(), m.getName());
} else {
ourLog.debug(" * Method: {}#{} is not a handler", theSystemProvider.getClass(), m.getName());
}
@ -306,8 +315,9 @@ public class RestfulServer extends HttpServlet {
}
/**
* Returns the default encoding to return (XML/JSON) if an incoming request does not specify a preference (either with the <code>_format</code> URL parameter, or with an <code>Accept</code> header
* in the request. The default is {@link EncodingEnum#XML}.
* Returns the default encoding to return (XML/JSON) if an incoming request does not specify a preference (either
* with the <code>_format</code> URL parameter, or with an <code>Accept</code> header in the request. The default is
* {@link EncodingEnum#XML}.
*/
public EncodingEnum getDefaultResponseEncoding() {
return myDefaultResponseEncoding;
@ -321,8 +331,8 @@ public class RestfulServer extends HttpServlet {
}
/**
* 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.
* 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() {
if (myFhirContext == null) {
@ -356,14 +366,15 @@ public class RestfulServer extends HttpServlet {
}
/**
* Allows users of RestfulServer to override the getRequestPath method to let them build their custom request path implementation
* Allows users of RestfulServer to override the getRequestPath method to let them build their custom request path
* implementation
*
* @param requestFullPath
* the full request path
* the full request path
* @param servletContextPath
* the servelet context path
* the servelet context path
* @param servletPath
* the servelet path
* the servelet path
* @return created resource path
*/
protected String getRequestPath(String requestFullPath, String servletContextPath, String servletPath) {
@ -371,7 +382,7 @@ public class RestfulServer extends HttpServlet {
}
public Collection<ResourceBinding> getResourceBindings() {
return myResourceNameToProvider.values();
return myResourceNameToBinding.values();
}
/**
@ -382,7 +393,8 @@ public class RestfulServer extends HttpServlet {
}
/**
* 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}
* 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;
@ -402,17 +414,19 @@ public class RestfulServer extends HttpServlet {
}
/**
* Returns the method bindings for this server which are not specific to any particular resource type. This method is internal to HAPI and developers generally do not need to interact with it. Use
* with caution, as it may change.
* Returns the method bindings for this server which are not specific to any particular resource type. This method is
* internal to HAPI and developers generally do not need to interact with it. Use with caution, as it may change.
*/
public List<BaseMethodBinding<?>> getServerBindings() {
return myServerBinding.getMethodBindings();
}
/**
* 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.
* 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.
* <p>
* By default, the ServerConformanceProvider for the declared version of FHIR is used, but this can be changed, or set to <code>null</code> to use the appropriate one for the given FHIR version.
* By default, the ServerConformanceProvider for the declared version of FHIR is used, but this can be changed, or
* set to <code>null</code> to use the appropriate one for the given FHIR version.
* </p>
*/
public Object getServerConformanceProvider() {
@ -420,7 +434,8 @@ public class RestfulServer extends HttpServlet {
}
/**
* 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.
* 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)
*/
@ -433,7 +448,8 @@ public class RestfulServer extends HttpServlet {
}
/**
* 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.
* 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;
@ -508,8 +524,7 @@ public class RestfulServer extends HttpServlet {
return;
}
}
RestfulServerUtils.streamResponseAsResource(this, theResponse, resBundle, responseEncoding, prettyPrint, requestIsBrowser, narrativeMode, Constants.STATUS_HTTP_200_OK,
theRequest.isRespondGzip(), theRequest.getFhirServerBase(), false);
RestfulServerUtils.streamResponseAsResource(this, theResponse, resBundle, responseEncoding, prettyPrint, requestIsBrowser, narrativeMode, Constants.STATUS_HTTP_200_OK, theRequest.isRespondGzip(), theRequest.getFhirServerBase(), false);
}
}
@ -582,9 +597,9 @@ public class RestfulServer extends HttpServlet {
} else if (resourceName == null) {
resourceBinding = myServerBinding;
} else {
resourceBinding = myResourceNameToProvider.get(resourceName);
resourceBinding = myResourceNameToBinding.get(resourceName);
if (resourceBinding == null) {
throw new InvalidRequestException("Unknown resource type '" + resourceName + "' - Server knows how to handle: " + myResourceNameToProvider.keySet());
throw new InvalidRequestException("Unknown resource type '" + resourceName + "' - Server knows how to handle: " + myResourceNameToBinding.keySet());
}
}
@ -668,8 +683,9 @@ public class RestfulServer extends HttpServlet {
requestDetails.setOtherOperationType(OtherOperationTypeEnum.GET_PAGE);
if (theRequestType != RequestTypeEnum.GET) {
/*
* We reconstruct the link-self URL using the request parameters, and this would break if the parameters came in using a POST. We could probably work around that but why bother
* unless someone comes up with a reason for needing it.
* We reconstruct the link-self URL using the request parameters, and this would break if the parameters
* came in using a POST. We could probably work around that but why bother unless someone comes up with a
* reason for needing it.
*/
throw new InvalidRequestException(getFhirContext().getLocalizer().getMessage(RestfulServer.class, "getPagesNonHttpGet"));
}
@ -753,12 +769,15 @@ public class RestfulServer extends HttpServlet {
}
/**
* 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.
* 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();
Object confProvider;
try {
ourLog.info("Initializing HAPI FHIR restful server running in " + getFhirContext().getVersion().getVersion().name() + " mode");
@ -777,8 +796,7 @@ public class RestfulServer extends HttpServlet {
String resourceName = getFhirContext().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() + "]");
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);
@ -800,36 +818,56 @@ public class RestfulServer extends HttpServlet {
findResourceMethods(getServerProfilesProvider());
Object confProvider = getServerConformanceProvider();
confProvider = getServerConformanceProvider();
if (confProvider == null) {
confProvider = getFhirContext().getVersion().createServerConformanceProvider(this);
}
findSystemMethods(confProvider);
findResourceMethods(confProvider);
} catch (Exception ex) {
ourLog.error("An error occurred while loading request handlers!", ex);
throw new ServletException("Failed to initialize FHIR Restful server", ex);
}
ourLog.trace("Invoking provider initialize methods");
if (getResourceProviders() != null) {
for (IResourceProvider iResourceProvider : getResourceProviders()) {
invokeInitialize(iResourceProvider);
}
}
if (confProvider != null) {
invokeInitialize(confProvider);
}
if (getPlainProviders() != null) {
for (Object next : getPlainProviders()) {
invokeInitialize(next);
}
}
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.
* 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.
* 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.
*/
@SuppressWarnings("unused")
protected void initialize() throws ServletException {
// nothing by default
}
private void invokeDestroy(Object theProvider) {
Class<?> clazz = theProvider.getClass();
invokeDestroy(theProvider, clazz);
invokeDestroy(theProvider, theProvider.getClass());
}
private void invokeInitialize(Object theProvider) {
invokeInitialize(theProvider, theProvider.getClass());
}
private void invokeDestroy(Object theProvider, Class<?> clazz) {
@ -853,9 +891,30 @@ public class RestfulServer extends HttpServlet {
}
}
private void invokeInitialize(Object theProvider, Class<?> clazz) {
for (Method m : ReflectionUtil.getDeclaredMethods(clazz)) {
Initialize initialize = m.getAnnotation(Initialize.class);
if (initialize != 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)) {
invokeInitialize(theProvider, supertype);
}
}
/**
* Should the server "pretty print" responses by default (requesting clients can always override this default by supplying an <code>Accept</code> header in the request, or a <code>_pretty</code>
* parameter in the request URL.
* Should the server "pretty print" responses by default (requesting clients can always override this default by
* supplying an <code>Accept</code> header in the request, or a <code>_pretty</code> parameter in the request URL.
* <p>
* The default is <code>false</code>
* </p>
@ -876,11 +935,12 @@ public class RestfulServer extends HttpServlet {
}
/**
* 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.
* 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)
* The behaviour enum (must not be null)
*/
public void setAddProfileTag(AddProfileTagEnum theAddProfileTag) {
Validate.notNull(theAddProfileTag, "theAddProfileTag must not be null");
@ -891,29 +951,30 @@ public class RestfulServer extends HttpServlet {
* Set how bundle factory should decide whether referenced resources should be included in bundles
*
* @param theBundleInclusionRule
* - inclusion rule (@see BundleInclusionRule for behaviors)
* - 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 <code>Accept</code> header in the request, or a <code>_pretty</code>
* parameter in the request URL.
* Should the server "pretty print" responses by default (requesting clients can always override this default by
* supplying an <code>Accept</code> header in the request, or a <code>_pretty</code> parameter in the request URL.
* <p>
* The default is <code>false</code>
* </p>
*
* @param theDefaultPrettyPrint
* The default pretty print setting
* 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 <code>_format</code> URL parameter, or with an <code>Accept</code> header in
* the request. The default is {@link EncodingEnum#XML}.
* Sets the default encoding to return (XML/JSON) if an incoming request does not specify a preference (either with
* the <code>_format</code> URL parameter, or with an <code>Accept</code> header in the request. The default is
* {@link EncodingEnum#XML}.
*/
public void setDefaultResponseEncoding(EncodingEnum theDefaultResponseEncoding) {
Validate.notNull(theDefaultResponseEncoding, "theDefaultResponseEncoding can not be null");
@ -921,10 +982,11 @@ public class RestfulServer extends HttpServlet {
}
/**
* Sets (enables/disables) the server support for ETags. Must not be <code>null</code>. Default is {@link #DEFAULT_ETAG_SUPPORT}
* Sets (enables/disables) the server support for ETags. Must not be <code>null</code>. Default is
* {@link #DEFAULT_ETAG_SUPPORT}
*
* @param theETagSupport
* The ETag support mode
* The ETag support mode
*/
public void setETagSupport(ETagSupportEnum theETagSupport) {
if (theETagSupport == null) {
@ -946,7 +1008,7 @@ public class RestfulServer extends HttpServlet {
* Sets (or clears) the list of interceptors
*
* @param theList
* The list of interceptors (may be null)
* The list of interceptors (may be null)
*/
public void setInterceptors(IServerInterceptor... theList) {
myInterceptors.clear();
@ -959,7 +1021,7 @@ public class RestfulServer extends HttpServlet {
* Sets (or clears) the list of interceptors
*
* @param theList
* The list of interceptors (may be null)
* The list of interceptors (may be null)
*/
public void setInterceptors(List<IServerInterceptor> theList) {
myInterceptors.clear();
@ -1017,7 +1079,8 @@ public class RestfulServer extends HttpServlet {
}
/**
* 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}
* 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");
@ -1025,15 +1088,17 @@ public class RestfulServer extends HttpServlet {
}
/**
* Returns the server conformance provider, which is the provider that is used to generate the server's conformance (metadata) statement.
* Returns the server conformance provider, which is the provider that is used to generate the server's conformance
* (metadata) statement.
* <p>
* By default, the ServerConformanceProvider implementation for the declared version of FHIR is used, but this can be changed, or set to <code>null</code> if you do not wish to export a
* conformance statement.
* By default, the ServerConformanceProvider implementation for the declared version of FHIR is used, but this can be
* changed, or set to <code>null</code> if you do not wish to export a conformance statement.
* </p>
* 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.
* 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) {
@ -1043,22 +1108,24 @@ public class RestfulServer extends HttpServlet {
}
/**
* 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.
* 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.
* 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 <code>true</code> (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
* If set to <code>true</code> (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;

View File

@ -403,6 +403,10 @@
<packageBase>ca.uhn.fhir.jpa.rp.dstu</packageBase>
<targetResourceSpringBeansFile>hapi-fhir-server-resourceproviders-dstu1.xml</targetResourceSpringBeansFile>
<baseResourceNames></baseResourceNames>
<excludeResourceNames>
<excludeResourceName>Conformance</excludeResourceName>
<excludeResourceName>OperationDefinition</excludeResourceName>
</excludeResourceNames>
</configuration>
</execution>
<execution>

View File

@ -20,11 +20,22 @@ package ca.uhn.fhir.jpa.provider;
* #L%
*/
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import ca.uhn.fhir.jpa.dao.IFhirSystemDao;
import ca.uhn.fhir.model.api.annotation.Description;
import ca.uhn.fhir.model.dstu2.composite.MetaDt;
import ca.uhn.fhir.model.dstu2.resource.Bundle;
import ca.uhn.fhir.model.dstu2.resource.Parameters;
import ca.uhn.fhir.model.primitive.IntegerDt;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.annotation.Transaction;
@ -32,6 +43,120 @@ import ca.uhn.fhir.rest.annotation.TransactionParam;
public class JpaSystemProviderDstu2 extends BaseJpaSystemProvider<Bundle> {
@Autowired()
@Qualifier("mySystemDaoDstu2")
private IFhirSystemDao<Bundle> mySystemDao;
//@formatter:off
// This is generated by hand:
// gls hapi-fhir-structures-dstu2/target/generated-sources/tinder/ca/uhn/fhir/model/dstu2/resource/ | sort | sed "s/.java//" | sed "s/^/@OperationParam(name=\"/" | sed "s/$/\", type=IntegerDt.class, min=0, max=1),/"
@Operation(name="$get-resource-counts", idempotent=true, returnParameters= {
@OperationParam(name="AllergyIntolerance", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Appointment", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="AppointmentResponse", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="AuditEvent", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Basic", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Binary", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="BodySite", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Bundle", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="CarePlan", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="CarePlan2", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Claim", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="ClaimResponse", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="ClinicalImpression", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Communication", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="CommunicationRequest", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Composition", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="ConceptMap", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Condition", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Conformance", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Contract", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Contraindication", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Coverage", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="DataElement", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Device", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="DeviceComponent", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="DeviceMetric", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="DeviceUseRequest", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="DeviceUseStatement", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="DiagnosticOrder", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="DiagnosticReport", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="DocumentManifest", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="DocumentReference", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="EligibilityRequest", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="EligibilityResponse", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Encounter", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="EnrollmentRequest", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="EnrollmentResponse", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="EpisodeOfCare", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="ExplanationOfBenefit", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="FamilyMemberHistory", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Flag", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Goal", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Group", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="HealthcareService", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="ImagingObjectSelection", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="ImagingStudy", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Immunization", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="ImmunizationRecommendation", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="ListResource", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Location", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Media", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Medication", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="MedicationAdministration", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="MedicationDispense", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="MedicationPrescription", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="MedicationStatement", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="MessageHeader", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="NamingSystem", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="NutritionOrder", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Observation", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="OperationDefinition", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="OperationOutcome", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Order", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="OrderResponse", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Organization", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Parameters", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Patient", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="PaymentNotice", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="PaymentReconciliation", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Person", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Practitioner", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Procedure", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="ProcedureRequest", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="ProcessRequest", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="ProcessResponse", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Provenance", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Questionnaire", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="QuestionnaireAnswers", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="ReferralRequest", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="RelatedPerson", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="RiskAssessment", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Schedule", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="SearchParameter", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Slot", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Specimen", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="StructureDefinition", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Subscription", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Substance", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="Supply", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="ValueSet", type=IntegerDt.class, min=0, max=1),
@OperationParam(name="VisionPrescription", type=IntegerDt.class, min=0, max=1)
})
@Description(shortDefinition="Provides the number of resources currently stored on the server, broken down by resource type")
//@formatter:on
public Parameters getResourceCounts() {
Parameters retVal = new Parameters();
Map<String, Long> counts = mySystemDao.getResourceCounts();
counts = new TreeMap<String, Long>(counts);
for (Entry<String, Long> nextEntry : counts.entrySet()) {
retVal.addParameter().setName(new StringDt(nextEntry.getKey())).setValue(new IntegerDt(nextEntry.getValue().intValue()));
}
return retVal;
}
//@formatter:off
@Operation(name="$meta", idempotent=true, returnParameters= {
@OperationParam(name="return", type=MetaDt.class)

View File

@ -212,6 +212,27 @@ public class ResourceProviderDstu2Test extends BaseJpaTest {
}
@Test
public void testGetResourceCountsOperation() throws Exception {
String methodName = "testMetaOperations";
Patient pt = new Patient();
pt.addName().addFamily(methodName);
ourClient.create().resource(pt).execute().getId().toUnqualifiedVersionless();
HttpGet get = new HttpGet(ourServerBase + "/$get-resource-counts");
CloseableHttpResponse response = ourHttpClient.execute(get);
try {
assertEquals(200, response.getStatusLine().getStatusCode());
String output = IOUtils.toString(response.getEntity().getContent());
IOUtils.closeQuietly(response.getEntity().getContent());
ourLog.info(output);
assertThat(output, containsString("<parameter><name value=\"Patient\"/><valueInteger value=\""));
} finally {
response.close();
}
}
@Test
public void testCreateResourceConditional() throws IOException {
String methodName = "testCreateResourceConditional";

View File

@ -23,7 +23,9 @@ package ca.uhn.fhir.rest.server.provider.dstu2;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
@ -53,7 +55,10 @@ import ca.uhn.fhir.model.dstu2.valueset.SystemRestfulInteractionEnum;
import ca.uhn.fhir.model.dstu2.valueset.TypeRestfulInteractionEnum;
import ca.uhn.fhir.model.primitive.DateTimeDt;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Initialize;
import ca.uhn.fhir.rest.annotation.Metadata;
import ca.uhn.fhir.rest.annotation.Read;
import ca.uhn.fhir.rest.method.BaseMethodBinding;
import ca.uhn.fhir.rest.method.DynamicSearchMethodBinding;
import ca.uhn.fhir.rest.method.IParameter;
@ -66,6 +71,7 @@ import ca.uhn.fhir.rest.server.IServerConformanceProvider;
import ca.uhn.fhir.rest.server.ResourceBinding;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
/**
* Server FHIR Provider which serves the conformance statement for a RESTful server implementation
@ -83,6 +89,8 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
private volatile Conformance myConformance;
private String myPublisher = "Not provided";
private final RestfulServer myRestfulServer;
private IdentityHashMap<OperationMethodBinding, String> myOperationBindingToName;
private HashMap<String, OperationMethodBinding> myOperationNameToBinding;
public ServerConformanceProvider(RestfulServer theRestfulServer) {
myRestfulServer = theRestfulServer;
@ -97,6 +105,73 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
return myPublisher;
}
@Initialize
public void initializeOperations() {
Set<String> allNames = new HashSet<String>();
myOperationBindingToName = new IdentityHashMap<OperationMethodBinding, String>();
myOperationNameToBinding = new HashMap<String, OperationMethodBinding>();
Map<String, List<BaseMethodBinding<?>>> resourceToMethods = collectMethodBindings();
for (Entry<String, List<BaseMethodBinding<?>>> nextEntry : resourceToMethods.entrySet()) {
List<BaseMethodBinding<?>> nextMethodBindings = nextEntry.getValue();
for (BaseMethodBinding<?> nextMethodBinding : nextMethodBindings) {
if (nextMethodBinding instanceof OperationMethodBinding) {
OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
String nextName = methodBinding.getName().substring(1);
int count = 1;
while (allNames.add(createOperationName(nextName, count)) == false) {
count++;
}
String name = createOperationName(nextName, count);
myOperationBindingToName.put(methodBinding, name);
myOperationNameToBinding.put(name, methodBinding);
}
}
}
}
private String createOperationName(String theName, int theCount) {
if (theCount < 2) {
return theName;
}
return theName + '-' + theCount;
}
@Read(type = OperationDefinition.class)
public OperationDefinition readOperationDefinition(@IdParam IdDt theId) {
if (theId == null || theId.hasIdPart() == false) {
throw new ResourceNotFoundException(theId);
}
OperationMethodBinding methodBinding = myOperationNameToBinding.get(theId.getIdPart());
if (methodBinding == null) {
throw new ResourceNotFoundException(theId);
}
OperationDefinition op = new OperationDefinition();
op.setStatus(ConformanceResourceStatusEnum.ACTIVE);
op.setDescription(methodBinding.getDescription());
op.setIdempotent(methodBinding.isIdempotent());
op.setCode(methodBinding.getName());
op.setInstance(methodBinding.isInstanceLevel());
op.addType().setValue(methodBinding.getResourceName());
for (IParameter nextParamUntyped : methodBinding.getParameters()) {
if (nextParamUntyped instanceof OperationParameter) {
OperationParameter nextParam = (OperationParameter) nextParamUntyped;
Parameter param = op.addParameter();
param.setUse(OperationParameterUseEnum.IN);
if (nextParam.getParamType() != null) {
param.setType(nextParam.getParamType().getCode());
}
param.setMin(nextParam.getMin());
param.setMax(nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax()));
param.setName(nextParam.getName());
}
}
return op;
}
@Override
@Metadata
public Conformance getServerConformance(HttpServletRequest theRequest) {
@ -123,24 +198,7 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
Set<SystemRestfulInteractionEnum> systemOps = new HashSet<SystemRestfulInteractionEnum>();
Map<String, List<BaseMethodBinding<?>>> resourceToMethods = new TreeMap<String, List<BaseMethodBinding<?>>>();
for (ResourceBinding next : myRestfulServer.getResourceBindings()) {
String resourceName = next.getResourceName();
for (BaseMethodBinding<?> nextMethodBinding : next.getMethodBindings()) {
if (resourceToMethods.containsKey(resourceName) == false) {
resourceToMethods.put(resourceName, new ArrayList<BaseMethodBinding<?>>());
}
resourceToMethods.get(resourceName).add(nextMethodBinding);
}
}
for (BaseMethodBinding<?> nextMethodBinding : myRestfulServer.getServerBindings()) {
String resourceName = "";
if (resourceToMethods.containsKey(resourceName) == false) {
resourceToMethods.put(resourceName, new ArrayList<BaseMethodBinding<?>>());
}
resourceToMethods.get(resourceName).add(nextMethodBinding);
}
Map<String, List<BaseMethodBinding<?>>> resourceToMethods = collectMethodBindings();
for (Entry<String, List<BaseMethodBinding<?>>> nextEntry : resourceToMethods.entrySet()) {
if (nextEntry.getKey().isEmpty() == false) {
@ -202,30 +260,8 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
handleDynamicSearchMethodBinding(resource, def, includes, (DynamicSearchMethodBinding) nextMethodBinding);
} else if (nextMethodBinding instanceof OperationMethodBinding) {
OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
OperationDefinition op = new OperationDefinition();
rest.addOperation().setName(methodBinding.getName()).getDefinition().setResource(op);
;
op.setStatus(ConformanceResourceStatusEnum.ACTIVE);
op.setDescription(methodBinding.getDescription());
op.setIdempotent(methodBinding.isIdempotent());
op.setCode(methodBinding.getName());
op.setInstance(methodBinding.isInstanceLevel());
op.addType().setValue(methodBinding.getResourceName());
for (IParameter nextParamUntyped : methodBinding.getParameters()) {
if (nextParamUntyped instanceof OperationParameter) {
OperationParameter nextParam = (OperationParameter) nextParamUntyped;
Parameter param = op.addParameter();
param.setUse(OperationParameterUseEnum.IN);
if (nextParam.getParamType() != null) {
param.setType(nextParam.getParamType().getCode());
}
param.setMin(nextParam.getMin());
param.setMax(nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax()));
param.setName(nextParam.getName());
}
}
String opName = myOperationBindingToName.get(methodBinding);
rest.addOperation().setName(methodBinding.getName()).getDefinition().setReference("OperationDefinition/" + opName);
}
Collections.sort(resource.getInteraction(), new Comparator<RestResourceInteraction>() {
@ -262,6 +298,27 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
return retVal;
}
private Map<String, List<BaseMethodBinding<?>>> collectMethodBindings() {
Map<String, List<BaseMethodBinding<?>>> resourceToMethods = new TreeMap<String, List<BaseMethodBinding<?>>>();
for (ResourceBinding next : myRestfulServer.getResourceBindings()) {
String resourceName = next.getResourceName();
for (BaseMethodBinding<?> nextMethodBinding : next.getMethodBindings()) {
if (resourceToMethods.containsKey(resourceName) == false) {
resourceToMethods.put(resourceName, new ArrayList<BaseMethodBinding<?>>());
}
resourceToMethods.get(resourceName).add(nextMethodBinding);
}
}
for (BaseMethodBinding<?> nextMethodBinding : myRestfulServer.getServerBindings()) {
String resourceName = "";
if (resourceToMethods.containsKey(resourceName) == false) {
resourceToMethods.put(resourceName, new ArrayList<BaseMethodBinding<?>>());
}
resourceToMethods.get(resourceName).add(nextMethodBinding);
}
return resourceToMethods;
}
private void checkBindingForSystemOps(Rest rest, Set<SystemRestfulInteractionEnum> systemOps, BaseMethodBinding<?> nextMethodBinding) {
if (nextMethodBinding.getSystemOperationType() != null) {
String sysOpCode = nextMethodBinding.getSystemOperationType().getCode();

View File

@ -1,7 +1,6 @@
package ca.uhn.fhir.rest.server;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.startsWith;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
@ -32,6 +31,9 @@ import org.junit.Test;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.dstu2.resource.Bundle;
import ca.uhn.fhir.model.dstu2.resource.Conformance;
import ca.uhn.fhir.model.dstu2.resource.OperationDefinition;
import ca.uhn.fhir.model.dstu2.resource.Conformance.RestOperation;
import ca.uhn.fhir.model.dstu2.resource.Parameters;
import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.primitive.DateTimeDt;
@ -42,6 +44,7 @@ import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.annotation.Read;
import ca.uhn.fhir.rest.client.IGenericClient;
import ca.uhn.fhir.util.PortUtil;
/**
@ -312,6 +315,20 @@ public class OperationServerTest {
assertEquals("instance $everything", ourLastMethod);
assertEquals("Patient/123", ourLastId.toUnqualifiedVersionless().getValue());
}
@Test
public void testConformance() throws Exception {
IGenericClient client = ourCtx.newRestfulGenericClient("http://localhost:" + ourPort);
Conformance p = client.fetchConformance().ofType(Conformance.class).execute();
List<RestOperation> ops = p.getRest().get(0).getOperation();
assertThat(ops.size(), greaterThan(1));
assertNull(ops.get(0).getDefinition().getReference().getBaseUrl());
assertThat(ops.get(0).getDefinition().getReference().getValue(), startsWith("OperationDefinition/"));
OperationDefinition def = client.read().resource(OperationDefinition.class).withId(ops.get(0).getDefinition().getReference()).execute();
assertThat(def.getCode(), not(blankOrNullString()));
}
@Test

View File

@ -22,6 +22,7 @@ import ca.uhn.fhir.model.dstu2.resource.Conformance;
import ca.uhn.fhir.model.dstu2.resource.Conformance.Rest;
import ca.uhn.fhir.model.dstu2.resource.Conformance.RestResource;
import ca.uhn.fhir.model.dstu2.resource.DiagnosticReport;
import ca.uhn.fhir.model.dstu2.resource.OperationDefinition;
import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.dstu2.valueset.SystemRestfulInteractionEnum;
import ca.uhn.fhir.model.dstu2.valueset.TypeRestfulInteractionEnum;
@ -86,6 +87,29 @@ public class ServerConformanceProviderDstu2Test {
String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance);
ourLog.info(conf);
assertEquals(1, conformance.getRest().get(0).getOperation().size());
assertEquals("$everything", conformance.getRest().get(0).getOperation().get(0).getName());
assertEquals("OperationDefinition/everything", conformance.getRest().get(0).getOperation().get(0).getDefinition().getReference().getValue());
}
@Test
public void testExtendedOperationReturningBundleOperation() throws Exception {
RestfulServer rs = new RestfulServer(ourCtx);
rs.setProviders(new ProviderWithExtendedOperationReturningBundle());
ServerConformanceProvider sc = new ServerConformanceProvider(rs);
rs.setServerConformanceProvider(sc);
rs.init(createServletConfig());
OperationDefinition opDef = sc.readOperationDefinition(new IdDt("OperationDefinition/everything"));
String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef);
ourLog.info(conf);
assertEquals("$everything", opDef.getCode());
assertEquals(true, opDef.getIdempotent().booleanValue());
}
@Test

View File

@ -20,13 +20,13 @@ package org.hl7.fhir.instance.conf;
* #L%
*/
import static org.apache.commons.lang3.StringUtils.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
@ -53,7 +53,11 @@ import org.hl7.fhir.instance.model.OperationDefinition.OperationParameterUse;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Initialize;
import ca.uhn.fhir.rest.annotation.Metadata;
import ca.uhn.fhir.rest.annotation.Read;
import ca.uhn.fhir.rest.method.BaseMethodBinding;
import ca.uhn.fhir.rest.method.DynamicSearchMethodBinding;
import ca.uhn.fhir.rest.method.IParameter;
@ -66,6 +70,7 @@ import ca.uhn.fhir.rest.server.IServerConformanceProvider;
import ca.uhn.fhir.rest.server.ResourceBinding;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
/**
* Server FHIR Provider which serves the conformance statement for a RESTful server implementation
@ -78,11 +83,12 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
* </p>
*/
public class ServerConformanceProvider implements IServerConformanceProvider<Conformance> {
private boolean myCache = true;
private volatile Conformance myConformance;
private String myPublisher = "Not provided";
private final RestfulServer myRestfulServer;
private IdentityHashMap<OperationMethodBinding, String> myOperationBindingToName;
private HashMap<String, OperationMethodBinding> myOperationNameToBinding;
public ServerConformanceProvider(RestfulServer theRestfulServer) {
myRestfulServer = theRestfulServer;
@ -97,6 +103,73 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
return myPublisher;
}
@Initialize
public void initializeOperations() {
Set<String> allNames = new HashSet<String>();
myOperationBindingToName = new IdentityHashMap<OperationMethodBinding, String>();
myOperationNameToBinding = new HashMap<String, OperationMethodBinding>();
Map<String, List<BaseMethodBinding<?>>> resourceToMethods = collectMethodBindings();
for (Entry<String, List<BaseMethodBinding<?>>> nextEntry : resourceToMethods.entrySet()) {
List<BaseMethodBinding<?>> nextMethodBindings = nextEntry.getValue();
for (BaseMethodBinding<?> nextMethodBinding : nextMethodBindings) {
if (nextMethodBinding instanceof OperationMethodBinding) {
OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
String nextName = methodBinding.getName().substring(1);
int count = 1;
while (allNames.add(createOperationName(nextName, count)) == false) {
count++;
}
String name = createOperationName(nextName, count);
myOperationBindingToName.put(methodBinding, name);
myOperationNameToBinding.put(name, methodBinding);
}
}
}
}
private String createOperationName(String theName, int theCount) {
if (theCount < 2) {
return theName;
}
return theName + '-' + theCount;
}
@Read(type = OperationDefinition.class)
public OperationDefinition readOperationDefinition(@IdParam IdDt theId) {
if (theId == null || theId.hasIdPart() == false) {
throw new ResourceNotFoundException(theId);
}
OperationMethodBinding methodBinding = myOperationNameToBinding.get(theId.getIdPart());
if (methodBinding == null) {
throw new ResourceNotFoundException(theId);
}
OperationDefinition op = new OperationDefinition();
op.setStatus(ConformanceResourceStatus.ACTIVE);
op.setDescription(methodBinding.getDescription());
op.setIdempotent(methodBinding.isIdempotent());
op.setCode(methodBinding.getName());
op.setInstance(methodBinding.isInstanceLevel());
op.addType(methodBinding.getResourceName());
for (IParameter nextParamUntyped : methodBinding.getParameters()) {
if (nextParamUntyped instanceof OperationParameter) {
OperationParameter nextParam = (OperationParameter) nextParamUntyped;
OperationDefinitionParameterComponent param = op.addParameter();
param.setUse(OperationParameterUse.IN);
if (nextParam.getParamType() != null) {
param.setType(nextParam.getParamType().getCode());
}
param.setMin(nextParam.getMin());
param.setMax(nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax()));
param.setName(nextParam.getName());
}
}
return op;
}
@Override
@Metadata
public Conformance getServerConformance(HttpServletRequest theRequest) {
@ -123,24 +196,7 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
Set<SystemRestfulInteraction> systemOps = new HashSet<SystemRestfulInteraction>();
Map<String, List<BaseMethodBinding<?>>> resourceToMethods = new TreeMap<String, List<BaseMethodBinding<?>>>();
for (ResourceBinding next : myRestfulServer.getResourceBindings()) {
String resourceName = next.getResourceName();
for (BaseMethodBinding<?> nextMethodBinding : next.getMethodBindings()) {
if (resourceToMethods.containsKey(resourceName) == false) {
resourceToMethods.put(resourceName, new ArrayList<BaseMethodBinding<?>>());
}
resourceToMethods.get(resourceName).add(nextMethodBinding);
}
}
for (BaseMethodBinding<?> nextMethodBinding : myRestfulServer.getServerBindings()) {
String resourceName = "";
if (resourceToMethods.containsKey(resourceName) == false) {
resourceToMethods.put(resourceName, new ArrayList<BaseMethodBinding<?>>());
}
resourceToMethods.get(resourceName).add(nextMethodBinding);
}
Map<String, List<BaseMethodBinding<?>>> resourceToMethods = collectMethodBindings();
for (Entry<String, List<BaseMethodBinding<?>>> nextEntry : resourceToMethods.entrySet()) {
if (nextEntry.getKey().isEmpty() == false) {
@ -149,7 +205,7 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
String resourceName = nextEntry.getKey();
RuntimeResourceDefinition def = myRestfulServer.getFhirContext().getResourceDefinition(resourceName);
resource.getTypeElement().setValue(def.getName());
resource.getProfile().setReference(def.getResourceProfile(myRestfulServer.getServerBaseForRequest(theRequest)));
resource.getProfile().setReference((def.getResourceProfile(myRestfulServer.getServerBaseForRequest(theRequest))));
TreeSet<String> includes = new TreeSet<String>();
@ -162,8 +218,10 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
TypeRestfulInteraction resOp;
try {
resOp = TypeRestfulInteraction.fromCode(resOpCode);
assert resOp != null;
} catch (Exception e) {
resOp = null;
}
if (resOp == null) {
throw new InternalErrorException("Unknown type-restful-interaction: " + resOpCode);
}
if (resourceOps.contains(resOp) == false) {
@ -205,37 +263,15 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
handleDynamicSearchMethodBinding(resource, def, includes, (DynamicSearchMethodBinding) nextMethodBinding);
} else if (nextMethodBinding instanceof OperationMethodBinding) {
OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
OperationDefinition op = new OperationDefinition();
rest.addOperation().setName(methodBinding.getName()).getDefinition().setResource(op);
;
op.setStatus(ConformanceResourceStatus.ACTIVE);
op.setDescription(methodBinding.getDescription());
op.setIdempotent(methodBinding.isIdempotent());
op.setCode(methodBinding.getName());
op.setInstance(methodBinding.isInstanceLevel());
op.addType(methodBinding.getResourceName());
for (IParameter nextParamUntyped : methodBinding.getParameters()) {
if (nextParamUntyped instanceof OperationParameter) {
OperationParameter nextParam = (OperationParameter) nextParamUntyped;
OperationDefinitionParameterComponent param = op.addParameter();
param.setUse(OperationParameterUse.IN);
if (nextParam.getParamType() != null) {
param.setType(nextParam.getParamType().getCode());
}
param.setMin(nextParam.getMin());
param.setMax(nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax()));
param.setName(nextParam.getName());
}
}
String opName = myOperationBindingToName.get(methodBinding);
rest.addOperation().setName(methodBinding.getName()).getDefinition().setReference("OperationDefinition/" + opName);
}
Collections.sort(resource.getInteraction(), new Comparator<ResourceInteractionComponent>() {
@Override
public int compare(ResourceInteractionComponent theO1, ResourceInteractionComponent theO2) {
TypeRestfulInteraction o1 = theO1.getCode();
TypeRestfulInteraction o2 = theO2.getCode();
TypeRestfulInteraction o1 = theO1.getCodeElement().getValue();
TypeRestfulInteraction o2 = theO2.getCodeElement().getValue();
if (o1 == null && o2 == null) {
return 0;
}
@ -265,6 +301,27 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
return retVal;
}
private Map<String, List<BaseMethodBinding<?>>> collectMethodBindings() {
Map<String, List<BaseMethodBinding<?>>> resourceToMethods = new TreeMap<String, List<BaseMethodBinding<?>>>();
for (ResourceBinding next : myRestfulServer.getResourceBindings()) {
String resourceName = next.getResourceName();
for (BaseMethodBinding<?> nextMethodBinding : next.getMethodBindings()) {
if (resourceToMethods.containsKey(resourceName) == false) {
resourceToMethods.put(resourceName, new ArrayList<BaseMethodBinding<?>>());
}
resourceToMethods.get(resourceName).add(nextMethodBinding);
}
}
for (BaseMethodBinding<?> nextMethodBinding : myRestfulServer.getServerBindings()) {
String resourceName = "";
if (resourceToMethods.containsKey(resourceName) == false) {
resourceToMethods.put(resourceName, new ArrayList<BaseMethodBinding<?>>());
}
resourceToMethods.get(resourceName).add(nextMethodBinding);
}
return resourceToMethods;
}
private void checkBindingForSystemOps(ConformanceRestComponent rest, Set<SystemRestfulInteraction> systemOps, BaseMethodBinding<?> nextMethodBinding) {
if (nextMethodBinding.getSystemOperationType() != null) {
String sysOpCode = nextMethodBinding.getSystemOperationType().getCode();
@ -272,8 +329,10 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
SystemRestfulInteraction sysOp;
try {
sysOp = SystemRestfulInteraction.fromCode(sysOpCode);
assert sysOp != null;
} catch (Exception e) {
sysOp=null;
}
if (sysOp == null) {
throw new InternalErrorException("Unknown system-restful-interaction: " + sysOpCode);
}
if (systemOps.contains(sysOp) == false) {
@ -378,7 +437,7 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
}
}
ConformanceRestResourceSearchParamComponent param = resource.addSearchParam();
ConformanceRestResourceSearchParamComponent param = resource.addSearchParam();
param.setName(nextParamUnchainedName);
if (StringUtils.isNotBlank(chain)) {
param.addChain(chain);
@ -390,8 +449,14 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
for (Class<? extends IResource> nextTarget : nextParameter.getDeclaredTypes()) {
RuntimeResourceDefinition targetDef = myRestfulServer.getFhirContext().getResourceDefinition(nextTarget);
if (targetDef != null) {
if (isNotBlank(targetDef.getName())) {
param.addTarget(targetDef.getName());
org.hl7.fhir.instance.model.Enumerations.ResourceType code;
try {
code = org.hl7.fhir.instance.model.Enumerations.ResourceType.fromCode(targetDef.getName());
} catch (Exception e) {
code = null;
}
if (code != null) {
param.addTarget(code.toCode());
}
}
}

View File

@ -18,6 +18,7 @@ import org.hl7.fhir.instance.model.Conformance.ConformanceRestResourceComponent;
import org.hl7.fhir.instance.model.Conformance.SystemRestfulInteraction;
import org.hl7.fhir.instance.model.Conformance.TypeRestfulInteraction;
import org.hl7.fhir.instance.model.DiagnosticReport;
import org.hl7.fhir.instance.model.OperationDefinition;
import org.hl7.fhir.instance.model.Patient;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.junit.Test;
@ -86,6 +87,29 @@ public class ServerConformanceProviderHl7OrgDstu2Test {
String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance);
ourLog.info(conf);
assertEquals(1, conformance.getRest().get(0).getOperation().size());
assertEquals("$everything", conformance.getRest().get(0).getOperation().get(0).getName());
assertEquals("OperationDefinition/everything", conformance.getRest().get(0).getOperation().get(0).getDefinition().getReference());
}
@Test
public void testExtendedOperationReturningBundleOperation() throws Exception {
RestfulServer rs = new RestfulServer(ourCtx);
rs.setProviders(new ProviderWithExtendedOperationReturningBundle());
ServerConformanceProvider sc = new ServerConformanceProvider(rs);
rs.setServerConformanceProvider(sc);
rs.init(createServletConfig());
OperationDefinition opDef = sc.readOperationDefinition(new IdDt("OperationDefinition/everything"));
String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef);
ourLog.info(conf);
assertEquals("$everything", opDef.getCode());
assertEquals(true, opDef.getIdempotent());
}
@Test

View File

@ -8,7 +8,6 @@ import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import org.apache.commons.lang.WordUtils;
@ -49,6 +48,9 @@ public class TinderJpaRestServerMojo extends AbstractMojo {
@Parameter(required = false)
private List<String> baseResourceNames;
@Parameter(required = false)
private List<String> excludeResourceNames;
@Parameter(required = true, defaultValue = "${project.build.directory}/..")
private String baseDir;
@ -97,6 +99,10 @@ public class TinderJpaRestServerMojo extends AbstractMojo {
}
}
if (excludeResourceNames != null) {
baseResourceNames.removeAll(excludeResourceNames);
}
ourLog.info("Including the following resources: {}", baseResourceNames);
File directoryBase = new File(targetDirectory, packageBase.replace(".", File.separatorChar + ""));

View File

@ -19,6 +19,15 @@
actually point to any codes (e.g. Observation.interpretation). Thanks
to GitHub user @steve1medix for reporting!
</action>
<action type="add">
Server now exports operations as separate resources instead of as contained resources
within Conformance
</action>
<action type="add">
Add new operation $get-resource-counts which will replace the resource
count extensions exported in the Conformance statement by the JPA
server.
</action>
</release>
<release version="1.1" date="2015-07-13">
<action type="add">