OpenAPI Support (#2560)

* Start work on OpenAPI

* Fixes

* Work on OpenAPI

* Cleanup

* Cleanup

* More swagger work

* Build fix

* More work

* More work

* Add documentation

* Docs fixes

* Add changelog

* License updates

* Add API

* Cleanup

* Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_plain/openapi.md

Co-authored-by: patrick-vachon-smilecdr <81274188+patrick-vachon-smilecdr@users.noreply.github.com>

* Work on scripts

* Add docs

* Compile fix

* Work on fixes

* Fix tests

* Test fix

* Test fix

* Build fix

* Tests

* Build fix

* Work on pipeline

* Version bump

* Test fix

* Version bump

* Test fix attempts

* Test fix

* Test fix

* Remove accidentally committed files

* Fixes

* Tets fixes

* Test fix

* Test fix

* Test fix

* Test fixes

* Test fixes

* Test fixes

* License header updates

* test fix

* Test fixes

* Test fix

* Test fixes

* Test fix

* Checkstyle bump

Co-authored-by: patrick-vachon-smilecdr <81274188+patrick-vachon-smilecdr@users.noreply.github.com>
This commit is contained in:
James Agnew 2021-04-25 15:40:50 -04:00 committed by GitHub
parent f842c234eb
commit 2ba1005762
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
170 changed files with 5693 additions and 3485 deletions

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -22,6 +22,7 @@ package ca.uhn.fhir.context;
import ca.uhn.fhir.model.api.annotation.Child;
import ca.uhn.fhir.model.api.annotation.Description;
import ca.uhn.fhir.util.ParametersUtil;
import ca.uhn.fhir.util.ValidateUtil;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBase;
@ -60,7 +61,7 @@ public abstract class BaseRuntimeDeclaredChildDefinition extends BaseRuntimeChil
myElementName = theElementName;
if (theDescriptionAnnotation != null) {
myShortDefinition = theDescriptionAnnotation.shortDefinition();
myFormalDefinition = theDescriptionAnnotation.formalDefinition();
myFormalDefinition = ParametersUtil.extractDescription(theDescriptionAnnotation);
} else {
myShortDefinition = null;
myFormalDefinition = null;

View File

@ -33,6 +33,13 @@ import java.lang.annotation.Target;
@Target(value = {ElementType.FIELD, ElementType.TYPE, ElementType.PARAMETER, ElementType.METHOD})
public @interface Description {
/**
* A description of this method or parameter
*
* @since 5.4.0
*/
String value() default "";
/**
* Optional short name for this child
*/
@ -40,7 +47,16 @@ public @interface Description {
/**
* Optional formal definition for this child
*
* @deprecated Use {@link #value()} instead. Deprecated in 5.4.0.
*/
@Deprecated
String formalDefinition() default "";
/**
* May be used to supply example values for this
*
* @since 5.4.0
*/
String[] example() default {};
}

View File

@ -78,4 +78,13 @@ public @interface AddTags {
*/
Class<? extends IBaseResource> type() default IBaseResource.class;
/**
* This method allows the return type for this method to be specified in a
* non-type-specific way, using the text name of the resource, e.g. "Patient".
*
* This attribute should be populate, or {@link #type()} should be, but not both.
*
* @since 5.4.0
*/
String typeName() default "";
}

View File

@ -47,4 +47,13 @@ public @interface Create {
Class<? extends IBaseResource> type() default IBaseResource.class;
/**
* This method allows the return type for this method to be specified in a
* non-type-specific way, using the text name of the resource, e.g. "Patient".
*
* This attribute should be populate, or {@link #type()} should be, but not both.
*
* @since 5.4.0
*/
String typeName() default "";
}

View File

@ -48,4 +48,15 @@ public @interface Delete {
*/
// NB: Read, Search (maybe others) share this annotation, so update the javadocs everywhere
Class<? extends IBaseResource> type() default IBaseResource.class;
/**
* This method allows the return type for this method to be specified in a
* non-type-specific way, using the text name of the resource, e.g. "Patient".
*
* This attribute should be populate, or {@link #type()} should be, but not both.
*
* @since 5.4.0
*/
String typeName() default "";
}

View File

@ -74,4 +74,14 @@ public @interface DeleteTags {
*/
Class<? extends IBaseResource> type() default IBaseResource.class;
/**
* This method allows the return type for this method to be specified in a
* non-type-specific way, using the text name of the resource, e.g. "Patient".
*
* This attribute should be populate, or {@link #type()} should be, but not both.
*
* @since 5.4.0
*/
String typeName() default "";
}

View File

@ -81,4 +81,14 @@ public @interface History {
*/
Class<? extends IBaseResource> type() default IBaseResource.class;
/**
* This method allows the return type for this method to be specified in a
* non-type-specific way, using the text name of the resource, e.g. "Patient".
*
* This attribute should be populate, or {@link #type()} should be, but not both.
*
* @since 5.4.0
*/
String typeName() default "";
}

View File

@ -132,7 +132,10 @@ public @interface Operation {
/**
* If this is set to <code>true</code>, this method will be a <b>global operation</b>
* meaning that it applies to all resource types
* meaning that it applies to all resource types. Operations with this flag set should be
* placed in Plain Providers (i.e. they don't need to be placed in a resource-type-specific
* <code>IResourceProvider</code> instance) and should have a parameter annotated with
* {@link IdParam}.
*/
boolean global() default false;

View File

@ -39,7 +39,7 @@ public @interface OperationParam {
/**
* Value for {@link OperationParam#max()} indicating no maximum
*/
final int MAX_UNLIMITED = -1;
int MAX_UNLIMITED = -1;
/**
@ -57,7 +57,7 @@ public @interface OperationParam {
*
* @since 1.5
*/
final int MAX_DEFAULT = -2;
int MAX_DEFAULT = -2;
/**
* The name of the parameter

View File

@ -50,4 +50,13 @@ public @interface Patch {
// NB: Read, Search (maybe others) share this annotation, so update the javadocs everywhere
Class<? extends IBaseResource> type() default IBaseResource.class;
/**
* This method allows the return type for this method to be specified in a
* non-type-specific way, using the text name of the resource, e.g. "Patient".
*
* This attribute should be populate, or {@link #type()} should be, but not both.
*
* @since 5.4.0
*/
String typeName() default "";
}

View File

@ -78,6 +78,16 @@ public @interface Search {
// NB: Read, Search (maybe others) share this annotation method, so update the javadocs everywhere
Class<? extends IBaseResource> type() default IBaseResource.class;
/**
* This method allows the return type for this method to be specified in a
* non-type-specific way, using the text name of the resource, e.g. "Patient".
*
* This attribute should be populate, or {@link #type()} should be, but not both.
*
* @since 5.4.0
*/
String typeName() default "";
/**
* In a REST server, should this method be invoked even if it does not have method parameters
* which correspond to all of the URL parameters passed in by the client (default is <code>false</code>).
@ -91,4 +101,5 @@ public @interface Search {
* </p>
*/
boolean allowUnknownParams() default false;
}

View File

@ -49,4 +49,14 @@ public @interface Update {
// NB: Read, Search (maybe others) share this annotation, so update the javadocs everywhere
Class<? extends IResource> type() default IResource.class;
/**
* This method allows the return type for this method to be specified in a
* non-type-specific way, using the text name of the resource, e.g. "Patient".
* <p>
* This attribute should be populate, or {@link #type()} should be, but not both.
*
* @since 5.4.0
*/
String typeName() default "";
}

View File

@ -53,6 +53,16 @@ public @interface Validate {
// NB: Read, Search (maybe others) share this annotation, so update the javadocs everywhere
Class<? extends IBaseResource> type() default IBaseResource.class;
/**
* This method allows the return type for this method to be specified in a
* non-type-specific way, using the text name of the resource, e.g. "Patient".
*
* This attribute should be populate, or {@link #type()} should be, but not both.
*
* @since 5.4.0
*/
String typeName() default "";
/**
* Validation mode parameter annotation for the validation mode parameter (only supported
* in FHIR DSTU2+). Parameter must be of type {@link ValidationModeEnum}.

View File

@ -36,6 +36,13 @@ import java.util.stream.Collectors;
*/
public class ExtensionUtil {
/**
* Non instantiable
*/
private ExtensionUtil() {
// nothing
}
/**
* Returns an extension with the specified URL creating one if it doesn't exist.
*
@ -46,7 +53,7 @@ public class ExtensionUtil {
*/
public static IBaseExtension<?, ?> getOrCreateExtension(IBase theBase, String theUrl) {
IBaseHasExtensions baseHasExtensions = validateExtensionSupport(theBase);
IBaseExtension extension = getExtensionByUrl(baseHasExtensions, theUrl);
IBaseExtension<?,?> extension = getExtensionByUrl(baseHasExtensions, theUrl);
if (extension == null) {
extension = baseHasExtensions.addExtension();
extension.setUrl(theUrl);
@ -75,13 +82,27 @@ public class ExtensionUtil {
*/
public static IBaseExtension<?, ?> addExtension(IBase theBase, String theUrl) {
IBaseHasExtensions baseHasExtensions = validateExtensionSupport(theBase);
IBaseExtension extension = baseHasExtensions.addExtension();
IBaseExtension<?,?> extension = baseHasExtensions.addExtension();
if (theUrl != null) {
extension.setUrl(theUrl);
}
return extension;
}
/**
* Adds an extension with the specified value
*
* @param theBase The resource to update extension on
* @param theUrl Extension URL
* @param theValueType Type of the value to set in the extension
* @param theValue Extension value
* @param theFhirContext The context containing FHIR resource definitions
*/
public static void addExtension(FhirContext theFhirContext, IBase theBase, String theUrl, String theValueType, Object theValue) {
IBaseExtension<?,?> ext = addExtension(theBase, theUrl);
setExtension(theFhirContext, ext, theValueType, theValue);
}
private static IBaseHasExtensions validateExtensionSupport(IBase theBase) {
if (!(theBase instanceof IBaseHasExtensions)) {
throw new IllegalArgumentException(String.format("Expected instance that supports extensions, but got %s", theBase));
@ -118,7 +139,7 @@ public class ExtensionUtil {
if (!hasExtension(theBase, theExtensionUrl)) {
return false;
}
IBaseDatatype value = getExtensionByUrl((IBaseHasExtensions) theBase, theExtensionUrl).getValue();
IBaseDatatype value = getExtensionByUrl(theBase, theExtensionUrl).getValue();
if (value == null) {
return theExtensionValue == null;
}
@ -133,7 +154,7 @@ public class ExtensionUtil {
* @return Returns the first available extension with the specified URL, or null if such extension doesn't exist
*/
public static IBaseExtension<?, ?> getExtensionByUrl(IBase theBase, String theExtensionUrl) {
Predicate<IBaseExtension> filter;
Predicate<IBaseExtension<?,?>> filter;
if (theExtensionUrl == null) {
filter = (e -> true);
} else {
@ -153,7 +174,7 @@ public class ExtensionUtil {
* @param theFilter Predicate to match the extension against
* @return Returns all extension with the specified URL, or an empty list if such extensions do not exist
*/
public static List<IBaseExtension<?, ?>> getExtensionsMatchingPredicate(IBase theBase, Predicate<? super IBaseExtension> theFilter) {
public static List<IBaseExtension<?, ?>> getExtensionsMatchingPredicate(IBase theBase, Predicate<? super IBaseExtension<?,?>> theFilter) {
return validateExtensionSupport(theBase)
.getExtension()
.stream()
@ -189,7 +210,7 @@ public class ExtensionUtil {
* @param theFilter Defines which extensions should be cleared
* @return Returns all extension that were removed
*/
private static List<IBaseExtension<?, ?>> clearExtensionsMatchingPredicate(IBase theBase, Predicate<? super IBaseExtension> theFilter) {
private static List<IBaseExtension<?, ?>> clearExtensionsMatchingPredicate(IBase theBase, Predicate<? super IBaseExtension<?,?>> theFilter) {
List<IBaseExtension<?, ?>> retVal = getExtensionsMatchingPredicate(theBase, theFilter);
validateExtensionSupport(theBase)
.getExtension()
@ -205,7 +226,7 @@ public class ExtensionUtil {
* @return Returns all extension with the specified URL, or an empty list if such extensions do not exist
*/
public static List<IBaseExtension<?, ?>> getExtensionsByUrl(IBaseHasExtensions theBase, String theExtensionUrl) {
Predicate<IBaseExtension> urlEqualityPredicate = e -> theExtensionUrl.equals(e.getUrl());
Predicate<IBaseExtension<?,?>> urlEqualityPredicate = e -> theExtensionUrl.equals(e.getUrl());
return getExtensionsMatchingPredicate(theBase, urlEqualityPredicate);
}
@ -216,8 +237,8 @@ public class ExtensionUtil {
* @param theValue The value to set
* @param theFhirContext The context containing FHIR resource definitions
*/
public static void setExtension(FhirContext theFhirContext, IBaseExtension theExtension, String theValue) {
setExtension(theFhirContext, theExtension, "string", (Object) theValue);
public static void setExtension(FhirContext theFhirContext, IBaseExtension<?,?> theExtension, String theValue) {
setExtension(theFhirContext, theExtension, "string", theValue);
}
/**
@ -228,7 +249,7 @@ public class ExtensionUtil {
* @param theValue The value to set
* @param theFhirContext The context containing FHIR resource definitions
*/
public static void setExtension(FhirContext theFhirContext, IBaseExtension theExtension, String theExtensionType, Object theValue) {
public static void setExtension(FhirContext theFhirContext, IBaseExtension<?,?> theExtension, String theExtensionType, Object theValue) {
theExtension.setValue(TerserUtil.newElement(theFhirContext, theExtensionType, theValue));
}
@ -241,7 +262,7 @@ public class ExtensionUtil {
* @param theFhirContext The context containing FHIR resource definitions
*/
public static void setExtensionAsString(FhirContext theFhirContext, IBase theBase, String theUrl, String theValue) {
IBaseExtension ext = getOrCreateExtension(theBase, theUrl);
IBaseExtension<?,?> ext = getOrCreateExtension(theBase, theUrl);
setExtension(theFhirContext, ext, theValue);
}
@ -255,7 +276,7 @@ public class ExtensionUtil {
* @param theFhirContext The context containing FHIR resource definitions
*/
public static void setExtension(FhirContext theFhirContext, IBase theBase, String theUrl, String theValueType, Object theValue) {
IBaseExtension ext = getOrCreateExtension(theBase, theUrl);
IBaseExtension<?,?> ext = getOrCreateExtension(theBase, theUrl);
setExtension(theFhirContext, ext, theValueType, theValue);
}
@ -266,7 +287,7 @@ public class ExtensionUtil {
* @param theRightExtension : Extension to be evaluated #2
* @return Result of the comparison
*/
public static boolean equals(IBaseExtension theLeftExtension, IBaseExtension theRightExtension) {
public static boolean equals(IBaseExtension<?,?> theLeftExtension, IBaseExtension<?,?> theRightExtension) {
return TerserUtil.equals(theLeftExtension, theRightExtension);
}
}

View File

@ -116,6 +116,12 @@ public class HapiExtensions {
*/
public static final String ASSOCIATED_GOLDEN_RESOURCE_EXTENSION_URL = "https://hapifhir.org/associated-patient-golden-resource/";
/**
* This extension provides an example value for a parameter value for
* a REST operation (eg for an OperationDefinition)
*/
public static final String EXT_OP_PARAMETER_EXAMPLE_VALUE = "http://hapifhir.io/fhir/StructureDefinition/op-parameter-example-value";
/**
* Non instantiable
*/

View File

@ -25,6 +25,7 @@ import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.model.api.annotation.Description;
import ca.uhn.fhir.model.primitive.StringDt;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBase;
@ -34,8 +35,13 @@ import org.hl7.fhir.instance.model.api.IBaseReference;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import javax.annotation.Nullable;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
@ -43,6 +49,7 @@ import java.util.function.Function;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
import static org.apache.commons.lang3.StringUtils.isBlank;
/**
* Utilities for dealing with parameters resources in a version indepenedent way
@ -418,4 +425,60 @@ public class ParametersUtil {
.findFirst();
}
@Nullable
public static String extractDescription(AnnotatedElement theType) {
Description description = theType.getAnnotation(Description.class);
if (description != null) {
return extractDescription(description);
} else {
return null;
}
}
@Nullable
public static String extractDescription(Description desc) {
String description = desc.value();
if (isBlank(description)) {
description = desc.formalDefinition();
}
if (isBlank(description)) {
description = desc.shortDefinition();
}
return defaultIfBlank(description, null);
}
@Nullable
public static String extractShortDefinition(AnnotatedElement theType) {
Description description = theType.getAnnotation(Description.class);
if (description != null) {
return defaultIfBlank(description.shortDefinition(), null);
} else {
return null;
}
}
public static String extractDescription(Annotation[] theParameterAnnotations) {
for (Annotation next : theParameterAnnotations) {
if (next instanceof Description) {
return extractDescription((Description)next);
}
}
return null;
}
public static List<String> extractExamples(Annotation[] theParameterAnnotations) {
ArrayList<String> retVal = null;
for (Annotation next : theParameterAnnotations) {
if (next instanceof Description) {
String[] examples = ((Description) next).example();
if (examples.length > 0) {
if (retVal == null) {
retVal = new ArrayList<>();
}
retVal.addAll(Arrays.asList(examples));
}
}
}
return retVal;
}
}

View File

@ -51,6 +51,7 @@ public class SchemaBaseValidator implements IValidatorModule {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SchemaBaseValidator.class);
private static final Set<String> SCHEMA_NAMES;
private static boolean ourJaxp15Supported;
static {
HashSet<String> sn = new HashSet<>();
@ -132,7 +133,9 @@ public class SchemaBaseValidator implements IValidatorModule {
* https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Processing
*/
schemaFactory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
ourJaxp15Supported = true;
} catch (SAXNotRecognizedException e) {
ourJaxp15Supported = false;
ourLog.warn("Jaxp 1.5 Support not found.", e);
}
schema = schemaFactory.newSchema(new Source[]{baseSource});
@ -216,4 +219,8 @@ public class SchemaBaseValidator implements IValidatorModule {
}
public static boolean isJaxp15Supported() {
return ourJaxp15Supported;
}
}

View File

@ -3,14 +3,14 @@
<modelVersion>4.0.0</modelVersion>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-bom</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<packaging>pom</packaging>
<name>HAPI FHIR BOM</name>
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
@ -41,6 +41,11 @@
<artifactId>hapi-fhir-server-mdm</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>hapi-fhir-server-openapi</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>hapi-fhir-validation</artifactId>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-cli</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -30,6 +30,7 @@ import ca.uhn.fhir.rest.client.impl.BaseHttpClientInvocation;
import ca.uhn.fhir.rest.param.ParameterUtil;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.ParametersUtil;
import org.hl7.fhir.instance.model.api.*;
import java.lang.reflect.Method;
@ -64,10 +65,7 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
Description description = theMethod.getAnnotation(Description.class);
if (description != null) {
myDescription = description.formalDefinition();
if (isBlank(myDescription)) {
myDescription = description.shortDefinition();
}
myDescription = ParametersUtil.extractDescription(description);
}
if (isBlank(myDescription)) {
myDescription = null;

View File

@ -33,6 +33,7 @@ import ca.uhn.fhir.rest.client.impl.BaseHttpClientInvocation;
import ca.uhn.fhir.rest.param.ParameterUtil;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.util.ParametersUtil;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
@ -59,15 +60,7 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
this.myQueryName = StringUtils.defaultIfBlank(search.queryName(), null);
this.myCompartmentName = StringUtils.defaultIfBlank(search.compartmentName(), null);
this.myIdParamIndex = ParameterUtil.findIdParameterIndex(theMethod, getContext());
Description desc = theMethod.getAnnotation(Description.class);
if (desc != null) {
if (isNotBlank(desc.formalDefinition())) {
myDescription = StringUtils.defaultIfBlank(desc.formalDefinition(), null);
} else {
myDescription = StringUtils.defaultIfBlank(desc.shortDefinition(), null);
}
}
this.myDescription = ParametersUtil.extractDescription(theMethod);
/*
* Check for parameter combinations and names that are invalid

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
@ -55,6 +55,11 @@
<artifactId>hapi-fhir-jpaserver-base</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-server-openapi</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
@ -78,13 +83,13 @@
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-dstu2</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-jpaserver-subscription</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
@ -101,7 +106,7 @@
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-testpage-overlay</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<classifier>classes</classifier>
</dependency>
<dependency>

View File

@ -0,0 +1,73 @@
package ca.uhn.hapi.fhir.docs;
/*-
* #%L
* HAPI FHIR - Docs
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* 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 ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Composition;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class CreateCompositionAndGenerateDocument {
private static final Logger ourLog = LoggerFactory.getLogger(CreateCompositionAndGenerateDocument.class);
public static void main(String[] args) {
// START SNIPPET: CreateCompositionAndGenerateDocument
FhirContext ctx = FhirContext.forR4();
IGenericClient client = ctx.newRestfulGenericClient("http://hapi.fhir.org/baseR4");
Patient patient = new Patient();
patient.setId("PATIENT-ABC");
patient.setActive(true);
client.update().resource(patient).execute();
Observation observation = new Observation();
observation.setId("OBSERVATION-ABC");
observation.setSubject(new Reference("Patient/PATIENT-ABC"));
observation.setStatus(Observation.ObservationStatus.FINAL);
client.update().resource(observation).execute();
Composition composition = new Composition();
composition.setId("COMPOSITION-ABC");
composition.setSubject(new Reference("Patient/PATIENT-ABC"));
composition.addSection().setFocus(new Reference("Observation/OBSERVATION-ABC"));
client.update().resource(composition).execute();
Bundle document = client
.operation()
.onInstance("Composition/COMPOSITION-ABC")
.named("$document")
.withNoParameters(Parameters.class)
.returnResourceType(Bundle.class)
.execute();
ourLog.info("Document bundle: {}", ctx.newJsonParser().setPrettyPrint(true).encodeResourceToString(document));
// END SNIPPET: CreateCompositionAndGenerateDocument
}
}

View File

@ -23,6 +23,7 @@ package ca.uhn.hapi.fhir.docs;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.rest.api.PreferHandlingEnum;
import ca.uhn.fhir.rest.openapi.OpenApiInterceptor;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.interceptor.*;
@ -65,6 +66,24 @@ public class ServletExamples {
}
// END SNIPPET: loggingInterceptor
// START SNIPPET: OpenApiInterceptor
@WebServlet(urlPatterns = { "/fhir/*" }, displayName = "FHIR Server")
public class RestfulServerWithOpenApi extends RestfulServer {
@Override
protected void initialize() throws ServletException {
// ... define your resource providers here ...
// Now register the interceptor
OpenApiInterceptor openApiInterceptor = new OpenApiInterceptor();
registerInterceptor(openApiInterceptor);
}
}
// END SNIPPET: OpenApiInterceptor
// START SNIPPET: validatingInterceptor
@WebServlet(urlPatterns = { "/fhir/*" }, displayName = "FHIR Server")
public class ValidatingServerWithLogging extends RestfulServer {

View File

@ -0,0 +1,5 @@
---
type: add
issue: 2560
title: "A new interceptor called `OpenApiInterceptor` has been added. This interceptor can be registered against FHIR Servers to
automatically add support for OpenAPI / Swagger."

View File

@ -161,3 +161,13 @@ This following example shows how to load all pages of a bundle by fetching each
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/BundleFetcher.java|loadAll}}
```
# Create Composition and Generate Document
This example shows how to generate a Composition resource with two linked resources, then apply the server `$document` operation to generate a document based on this composition.
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/CreateCompositionAndGenerateDocument.java|CreateCompositionAndGenerateDocument}}
```

View File

@ -47,6 +47,7 @@ page.server_plain.web_testpage_overlay=Web Testpage Overlay
page.server_plain.multitenancy=Multitenancy
page.server_plain.jax_rs=JAX-RS Support
page.server_plain.customizing_the_capabilitystatement=Customizing the CapabilityStatement
page.server_plain.openapi=OpenAPI / Swagger
section.server_jpa.title=JPA Server
page.server_jpa.introduction=Introduction

View File

@ -196,6 +196,11 @@ Some security audit tools require that servers return an HTTP 405 if an unsuppor
* [BanUnsupportedHttpMethodsInterceptor JavaDoc](/apidocs/hapi-fhir-server/ca/uhn/fhir/rest/server/interceptor/BanUnsupportedHttpMethodsInterceptor.html)
* [BanUnsupportedHttpMethodsInterceptor Source](https://github.com/hapifhir/hapi-fhir/blob/master/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/BanUnsupportedHttpMethodsInterceptor.java)
# Server: OpenAPI / Swagger Support
An interceptor can be registered against your server that enables support for OpenAPI (aka Swagger) automatically. See [OpenAPI](/docs/server_plain/openapi.html) for more information.
# Subscription: Subscription Debug Log Interceptor
When using Subscriptions, the debug log interceptor can be used to add a number of additional lines to the server logs showing the internals of the subscription processing pipeline.

View File

@ -0,0 +1,39 @@
# OpenAPI / Swagger Support
In HAPI FHIR, support for OpenAPI (aka Swagger) is supported via the [OpenApiInterceptor](/hapi-fhir/apidocs/hapi-fhir-server-openapi/ca/uhn/fhir/rest/openapi/OpenApiInterceptor.html).
Note that this interceptor supports servers using the RestfulServer (aka HAPI FHIR Plain Server and JPA Server), and does not currently support JAX-RS servers.
When this interceptor is registered against the server, it performs the following 3 tasks:
### System Functionality
* OpenAPI 3.0 Documentation will be served at `[baseUrl]/api-docs`. This documentation is generated by the interceptor using information from the server's CapabilityStatement as well as from its automatically generated OperationDefinitions.
### User Functionality
* Anytime a user using a browser navigates to the Base URL of the server, they will be automatically redirected to `[baseUrl]/swagger-ui/`
* A customized version of the [Swagger UI](https://swagger.io/tools/swagger-ui/) tool will be served at `[baseUrl]/swagger-ui/`
# Enabling OpenAPI
The HAPI FHIR OpenAPI functionality is supplied in a dedicated module called `hapi-fhir-server-openapi`. To enable this functionality you must first include this module in your project. For example, Maven users should include the following dependency:
```xml
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-server-openapi</artifactId>
<version>VERSION</version>
</dependency>
```
You then simply have to register the interceptor against your RestfulServer instance.
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/ServletExamples.java|OpenApiInterceptor}}
```
# Demonstration
See the HAPI FHIR Test Server for a demonstration of HAPI FHIR OpenAPI functionality: http://hapi.fhir.org/baseR4/swagger-ui/

View File

@ -11,7 +11,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
@ -36,6 +36,11 @@
<artifactId>hapi-fhir-server-mdm</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-server-openapi</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-client</artifactId>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
@ -577,6 +577,13 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-server-openapi</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>

View File

@ -8,10 +8,10 @@ import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.ParametersUtil;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.IntegerType;
import org.hl7.fhir.r4.model.Parameters;
import org.jboss.logging.MDC;
import org.springframework.beans.factory.annotation.Autowired;
@ -78,13 +78,11 @@ public class BaseJpaProvider {
return options;
}
protected Parameters createExpungeResponse(ExpungeOutcome theOutcome) {
Parameters retVal = new Parameters();
retVal
.addParameter()
.setName(JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT)
.setValue(new IntegerType(theOutcome.getDeletedCount()));
return retVal;
protected IBaseParameters createExpungeResponse(ExpungeOutcome theOutcome) {
IBaseParameters parameters = ParametersUtil.newInstance(getContext());
String value = Integer.toString(theOutcome.getDeletedCount());
ParametersUtil.addParameterToParameters(getContext(), parameters, JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, "integer", value);
return parameters;
}
/**

View File

@ -24,30 +24,47 @@ import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
import ca.uhn.fhir.jpa.api.model.ExpungeOptions;
import ca.uhn.fhir.jpa.api.model.ExpungeOutcome;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.model.api.annotation.Description;
import ca.uhn.fhir.rest.annotation.At;
import ca.uhn.fhir.rest.annotation.ConditionalUrlParam;
import ca.uhn.fhir.rest.annotation.Create;
import ca.uhn.fhir.rest.annotation.Delete;
import ca.uhn.fhir.rest.annotation.History;
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.Patch;
import ca.uhn.fhir.rest.annotation.Read;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.annotation.Since;
import ca.uhn.fhir.rest.annotation.Update;
import ca.uhn.fhir.rest.annotation.Validate;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.PatchTypeEnum;
import ca.uhn.fhir.rest.api.ValidationModeEnum;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.util.CoverageIgnore;
import ca.uhn.fhir.util.ParametersUtil;
import org.hl7.fhir.instance.model.api.IBaseMetaType;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.Parameters;
import org.springframework.beans.factory.annotation.Required;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
import static ca.uhn.fhir.jpa.model.util.JpaConstants.OPERATION_META;
import static ca.uhn.fhir.jpa.model.util.JpaConstants.OPERATION_META_ADD;
import static ca.uhn.fhir.jpa.model.util.JpaConstants.OPERATION_META_DELETE;
public abstract class BaseJpaResourceProvider<T extends IBaseResource> extends BaseJpaProvider implements IResourceProvider {
private IFhirResourceDao<T> myDao;
@ -62,7 +79,7 @@ public abstract class BaseJpaResourceProvider<T extends IBaseResource> extends B
}
protected Parameters doExpunge(IIdType theIdParam, IPrimitiveType<? extends Integer> theLimit, IPrimitiveType<? extends Boolean> theExpungeDeletedResources, IPrimitiveType<? extends Boolean> theExpungeOldVersions, IPrimitiveType<? extends Boolean> theExpungeEverything, RequestDetails theRequest) {
protected IBaseParameters doExpunge(IIdType theIdParam, IPrimitiveType<? extends Integer> theLimit, IPrimitiveType<? extends Boolean> theExpungeDeletedResources, IPrimitiveType<? extends Boolean> theExpungeOldVersions, IPrimitiveType<? extends Boolean> theExpungeEverything, RequestDetails theRequest) {
ExpungeOptions options = createExpungeOptions(theLimit, theExpungeDeletedResources, theExpungeOldVersions, theExpungeEverything);
@ -143,4 +160,134 @@ public abstract class BaseJpaResourceProvider<T extends IBaseResource> extends B
}
}
@Create
public MethodOutcome create(HttpServletRequest theRequest, @ResourceParam T theResource, @ConditionalUrlParam String theConditional, RequestDetails theRequestDetails) {
startRequest(theRequest);
try {
if (theConditional != null) {
return getDao().create(theResource, theConditional, theRequestDetails);
} else {
return getDao().create(theResource, theRequestDetails);
}
} finally {
endRequest(theRequest);
}
}
@Delete()
public MethodOutcome delete(HttpServletRequest theRequest, @IdParam IIdType theResource, @ConditionalUrlParam(supportsMultiple = true) String theConditional, RequestDetails theRequestDetails) {
startRequest(theRequest);
try {
if (theConditional != null) {
return getDao().deleteByUrl(theConditional, theRequestDetails);
} else {
return getDao().delete(theResource, theRequestDetails);
}
} finally {
endRequest(theRequest);
}
}
@Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = {
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, typeName = "integer")
})
public IBaseParameters expunge(
@IdParam IIdType theIdParam,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_LIMIT, typeName = "integer") IPrimitiveType<Integer> theLimit,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES, typeName = "boolean") IPrimitiveType<Boolean> theExpungeDeletedResources,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS, typeName = "boolean") IPrimitiveType<Boolean> theExpungeOldVersions,
RequestDetails theRequest) {
return doExpunge(theIdParam, theLimit, theExpungeDeletedResources, theExpungeOldVersions, null, theRequest);
}
@Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = {
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, typeName = "integer")
})
public IBaseParameters expunge(
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_LIMIT, typeName = "integer") IPrimitiveType<Integer> theLimit,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES, typeName = "boolean") IPrimitiveType<Boolean> theExpungeDeletedResources,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS, typeName = "boolean") IPrimitiveType<Boolean> theExpungeOldVersions,
RequestDetails theRequest) {
return doExpunge(null, theLimit, theExpungeDeletedResources, theExpungeOldVersions, null, theRequest);
}
@Description("Request a global list of tags, profiles, and security labels")
@Operation(name = OPERATION_META, idempotent = true, returnParameters = {
@OperationParam(name = "return", typeName = "Meta")
})
public IBaseParameters meta(RequestDetails theRequestDetails) {
Class metaType = getContext().getElementDefinition("Meta").getImplementingClass();
IBaseMetaType metaGetOperation = getDao().metaGetOperation(metaType, theRequestDetails);
IBaseParameters parameters = ParametersUtil.newInstance(getContext());
ParametersUtil.addParameterToParameters(getContext(), parameters, "return", metaGetOperation);
return parameters;
}
@Description("Request a list of tags, profiles, and security labels for a specfic resource instance")
@Operation(name = OPERATION_META, idempotent = true, returnParameters = {
@OperationParam(name = "return", typeName = "Meta")
})
public IBaseParameters meta(@IdParam IIdType theId, RequestDetails theRequestDetails) {
Class metaType = getContext().getElementDefinition("Meta").getImplementingClass();
IBaseMetaType metaGetOperation = getDao().metaGetOperation(metaType, theId, theRequestDetails);
IBaseParameters parameters = ParametersUtil.newInstance(getContext());
ParametersUtil.addParameterToParameters(getContext(), parameters, "return", metaGetOperation);
return parameters;
}
@Description("Add tags, profiles, and/or security labels to a resource")
@Operation(name = OPERATION_META_ADD, idempotent = false, returnParameters = {
@OperationParam(name = "return", typeName = "Meta")
})
public IBaseParameters metaAdd(@IdParam IIdType theId, @OperationParam(name = "meta", typeName = "Meta") IBaseMetaType theMeta, RequestDetails theRequestDetails) {
if (theMeta == null) {
throw new InvalidRequestException("Input contains no parameter with name 'meta'");
}
IBaseMetaType metaAddOperation = getDao().metaAddOperation(theId, theMeta, theRequestDetails);
IBaseParameters parameters = ParametersUtil.newInstance(getContext());
ParametersUtil.addParameterToParameters(getContext(), parameters, "return", metaAddOperation);
return parameters;
}
@Description("Delete tags, profiles, and/or security labels from a resource")
@Operation(name = OPERATION_META_DELETE, idempotent = false, returnParameters = {
@OperationParam(name = "return", typeName = "Meta")
})
public IBaseParameters metaDelete(@IdParam IIdType theId, @OperationParam(name = "meta", typeName = "Meta") IBaseMetaType theMeta, RequestDetails theRequestDetails) {
if (theMeta == null) {
throw new InvalidRequestException("Input contains no parameter with name 'meta'");
}
IBaseMetaType metaDelete = getDao().metaDeleteOperation(theId, theMeta, theRequestDetails);
IBaseParameters parameters = ParametersUtil.newInstance(getContext());
ParametersUtil.addParameterToParameters(getContext(), parameters, "return", metaDelete);
return parameters;
}
@Update
public MethodOutcome update(HttpServletRequest theRequest, @ResourceParam T theResource, @IdParam IIdType theId, @ConditionalUrlParam String theConditional, RequestDetails theRequestDetails) {
startRequest(theRequest);
try {
if (theConditional != null) {
return getDao().update(theResource, theConditional, theRequestDetails);
} else {
return getDao().update(theResource, theRequestDetails);
}
} finally {
endRequest(theRequest);
}
}
@Validate
public MethodOutcome validate(@ResourceParam T theResource, @ResourceParam String theRawResource, @ResourceParam EncodingEnum theEncoding, @Validate.Mode ValidationModeEnum theMode,
@Validate.Profile String theProfile, RequestDetails theRequestDetails) {
return validate(theResource, null, theRawResource, theEncoding, theMode, theProfile, theRequestDetails);
}
@Validate
public MethodOutcome validate(@ResourceParam T theResource, @IdParam IIdType theId, @ResourceParam String theRawResource, @ResourceParam EncodingEnum theEncoding, @Validate.Mode ValidationModeEnum theMode,
@Validate.Profile String theProfile, RequestDetails theRequestDetails) {
return getDao().validate(theResource, theId, theRawResource, theEncoding, theMode, theProfile, theRequestDetails);
}
}

View File

@ -23,15 +23,18 @@ package ca.uhn.fhir.jpa.provider;
import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.api.model.ExpungeOptions;
import ca.uhn.fhir.jpa.api.model.ExpungeOutcome;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc;
import ca.uhn.fhir.rest.annotation.At;
import ca.uhn.fhir.rest.annotation.History;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.annotation.Since;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.DateRangeParam;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.Parameters;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Required;
@ -55,7 +58,22 @@ public class BaseJpaSystemProvider<T, MT> extends BaseJpaProvider implements IJp
return myResourceReindexingSvc;
}
protected Parameters doExpunge(IPrimitiveType<? extends Integer> theLimit, IPrimitiveType<? extends Boolean> theExpungeDeletedResources, IPrimitiveType<? extends Boolean> theExpungeOldVersions, IPrimitiveType<? extends Boolean> theExpungeEverything, RequestDetails theRequestDetails) {
@Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = {
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, typeName = "integer")
})
public IBaseParameters expunge(
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_LIMIT, typeName = "integer") IPrimitiveType<Integer> theLimit,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES, typeName = "boolean") IPrimitiveType<Boolean> theExpungeDeletedResources,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS, typeName = "boolean") IPrimitiveType<Boolean> theExpungeOldVersions,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_EVERYTHING, typeName = "boolean") IPrimitiveType<Boolean> theExpungeEverything,
RequestDetails theRequestDetails
) {
ExpungeOptions options = createExpungeOptions(theLimit, theExpungeDeletedResources, theExpungeOldVersions, theExpungeEverything);
ExpungeOutcome outcome = getDao().expunge(options, theRequestDetails);
return createExpungeResponse(outcome);
}
protected IBaseParameters doExpunge(IPrimitiveType<? extends Integer> theLimit, IPrimitiveType<? extends Boolean> theExpungeDeletedResources, IPrimitiveType<? extends Boolean> theExpungeOldVersions, IPrimitiveType<? extends Boolean> theExpungeEverything, RequestDetails theRequestDetails) {
ExpungeOptions options = createExpungeOptions(theLimit, theExpungeDeletedResources, theExpungeOldVersions, theExpungeEverything);
ExpungeOutcome outcome = getDao().expunge(options, theRequestDetails);
return createExpungeResponse(outcome);

View File

@ -20,19 +20,27 @@ package ca.uhn.fhir.jpa.provider;
* #L%
*/
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.model.api.annotation.Description;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.util.ParametersUtil;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import javax.servlet.http.HttpServletRequest;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public abstract class BaseJpaSystemProviderDstu2Plus<T, MT> extends BaseJpaSystemProvider<T, MT> {
@Operation(name = MARK_ALL_RESOURCES_FOR_REINDEXING, idempotent = true, returnParameters = {
@Description("Marks all currently existing resources of a given type, or all resources of all types, for reindexing.")
@Operation(name = MARK_ALL_RESOURCES_FOR_REINDEXING, idempotent = false, returnParameters = {
@OperationParam(name = "status")
})
public IBaseResource markAllResourcesForReindexing(
@ -53,7 +61,8 @@ public abstract class BaseJpaSystemProviderDstu2Plus<T, MT> extends BaseJpaSyste
return retVal;
}
@Operation(name = PERFORM_REINDEXING_PASS, idempotent = true, returnParameters = {
@Description("Forces a single pass of the resource reindexing processor")
@Operation(name = PERFORM_REINDEXING_PASS, idempotent = false, returnParameters = {
@OperationParam(name = "status")
})
public IBaseResource performReindexingPass() {
@ -72,4 +81,28 @@ public abstract class BaseJpaSystemProviderDstu2Plus<T, MT> extends BaseJpaSyste
return retVal;
}
/**
* $process-message
*/
@Description("Accept a FHIR Message Bundle for processing")
@Operation(name = JpaConstants.OPERATION_PROCESS_MESSAGE, idempotent = false)
public IBaseBundle processMessage(
HttpServletRequest theServletRequest,
RequestDetails theRequestDetails,
@OperationParam(name = "content", min = 1, max = 1, typeName = "Bundle")
@Description(formalDefinition = "The message to process (or, if using asynchronous messaging, it may be a response message to accept)")
IBaseBundle theMessageToProcess
) {
startRequest(theServletRequest);
try {
return getDao().processMessage(theRequestDetails, theMessageToProcess);
} finally {
endRequest(theServletRequest);
}
}
}

View File

@ -24,6 +24,7 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.patch.FhirPatch;
import ca.uhn.fhir.model.api.annotation.Description;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
@ -49,14 +50,23 @@ public class DiffProvider {
@Autowired
private DaoRegistry myDaoRegistry;
@Description(
value="This operation examines two resource versions (can be two versions of the same resource, or two different resources) and generates a FHIR Patch document showing the differences.",
shortDefinition = "Comparte two resources or two versions of a single resource")
@Operation(name = ProviderConstants.DIFF_OPERATION_NAME, global = true, idempotent = true)
public IBaseParameters diff(
@IdParam IIdType theResourceId,
@OperationParam(name = ProviderConstants.DIFF_FROM_VERSION_PARAMETER, typeName = "string", min = 0, max = 1) IPrimitiveType<?> theFromVersion,
@OperationParam(name = ProviderConstants.DIFF_INCLUDE_META_PARAMETER, typeName = "boolean", min = 0, max = 1) IPrimitiveType<Boolean> theIncludeMeta,
@Description(value = "The resource ID and version to diff from", example = "Patient/example/version/1")
@OperationParam(name = ProviderConstants.DIFF_FROM_VERSION_PARAMETER, typeName = "string", min = 0, max = 1)
IPrimitiveType<?> theFromVersion,
@Description(value = "Should differences in the Resource.meta element be included in the diff", example = "false")
@OperationParam(name = ProviderConstants.DIFF_INCLUDE_META_PARAMETER, typeName = "boolean", min = 0, max = 1)
IPrimitiveType<Boolean> theIncludeMeta,
RequestDetails theRequestDetails) {
IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResourceId.getResourceType());
IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(theResourceId.getResourceType());
IBaseResource targetResource = dao.read(theResourceId, theRequestDetails);
IBaseResource sourceResource = null;
@ -82,15 +92,23 @@ public class DiffProvider {
}
FhirPatch fhirPatch = newPatch(theIncludeMeta);
IBaseParameters diff = fhirPatch.diff(sourceResource, targetResource);
return diff;
return fhirPatch.diff(sourceResource, targetResource);
}
@Description("This operation examines two resource versions (can be two versions of the same resource, or two different resources) and generates a FHIR Patch document showing the differences.")
@Operation(name = ProviderConstants.DIFF_OPERATION_NAME, idempotent = true)
public IBaseParameters diff(
@OperationParam(name = ProviderConstants.DIFF_FROM_PARAMETER, typeName = "id", min = 1, max = 1) IIdType theFromVersion,
@OperationParam(name = ProviderConstants.DIFF_TO_PARAMETER, typeName = "id", min = 1, max = 1) IIdType theToVersion,
@OperationParam(name = ProviderConstants.DIFF_INCLUDE_META_PARAMETER, typeName = "boolean", min = 0, max = 1) IPrimitiveType<Boolean> theIncludeMeta,
@Description(value = "The resource ID and version to diff from", example = "Patient/example/version/1")
@OperationParam(name = ProviderConstants.DIFF_FROM_PARAMETER, typeName = "id", min = 1, max = 1)
IIdType theFromVersion,
@Description(value = "The resource ID and version to diff to", example = "Patient/example/version/2")
@OperationParam(name = ProviderConstants.DIFF_TO_PARAMETER, typeName = "id", min = 1, max = 1)
IIdType theToVersion,
@Description(value = "Should differences in the Resource.meta element be included in the diff", example = "false")
@OperationParam(name = ProviderConstants.DIFF_INCLUDE_META_PARAMETER, typeName = "boolean", min = 0, max = 1)
IPrimitiveType<Boolean> theIncludeMeta,
RequestDetails theRequestDetails) {
if (!Objects.equal(theFromVersion.getResourceType(), theToVersion.getResourceType())) {
@ -98,13 +116,12 @@ public class DiffProvider {
throw new InvalidRequestException(msg);
}
IFhirResourceDao dao = myDaoRegistry.getResourceDao(theFromVersion.getResourceType());
IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(theFromVersion.getResourceType());
IBaseResource sourceResource = dao.read(theFromVersion, theRequestDetails);
IBaseResource targetResource = dao.read(theToVersion, theRequestDetails);
FhirPatch fhirPatch = newPatch(theIncludeMeta);
IBaseParameters diff = fhirPatch.diff(sourceResource, targetResource);
return diff;
return fhirPatch.diff(sourceResource, targetResource);
}
@Nonnull

View File

@ -24,6 +24,7 @@ import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.model.api.annotation.Description;
import ca.uhn.fhir.rest.annotation.GraphQL;
import ca.uhn.fhir.rest.annotation.GraphQLQueryBody;
import ca.uhn.fhir.rest.annotation.GraphQLQueryUrl;
@ -110,18 +111,20 @@ public class GraphQLProvider {
myStorageServices = theStorageServices;
}
@Description(value="This operation invokes a GraphQL expression for fetching an joining a graph of resources, returning them in a custom format.")
@GraphQL(type=RequestTypeEnum.GET)
public String processGraphQlGetRequest(ServletRequestDetails theRequestDetails, @IdParam IIdType theId, @GraphQLQueryUrl String queryUrl) {
if (queryUrl != null) {
return processGraphQLRequest(theRequestDetails, theId, queryUrl);
public String processGraphQlGetRequest(ServletRequestDetails theRequestDetails, @IdParam IIdType theId, @GraphQLQueryUrl String theQueryUrl) {
if (theQueryUrl != null) {
return processGraphQLRequest(theRequestDetails, theId, theQueryUrl);
}
throw new InvalidRequestException("Unable to parse empty GraphQL expression");
}
@Description(value="This operation invokes a GraphQL expression for fetching an joining a graph of resources, returning them in a custom format.")
@GraphQL(type=RequestTypeEnum.POST)
public String processGraphQlPostRequest(ServletRequestDetails theRequestDetails, @IdParam IIdType theId, @GraphQLQueryBody String queryBody) {
if (queryBody != null) {
return processGraphQLRequest(theRequestDetails, theId, queryBody);
public String processGraphQlPostRequest(ServletRequestDetails theRequestDetails, @IdParam IIdType theId, @GraphQLQueryBody String theQueryBody) {
if (theQueryBody != null) {
return processGraphQLRequest(theRequestDetails, theId, theQueryBody);
}
throw new InvalidRequestException("Unable to parse empty GraphQL expression");
}

View File

@ -80,6 +80,11 @@ public class JpaCapabilityStatementProvider extends ServerCapabilityStatementPro
if (isNotBlank(myImplementationDescription)) {
theTerser.setElement(theCapabilityStatement, "implementation.description", myImplementationDescription);
}
theTerser.addElement(theCapabilityStatement, "patchFormat", Constants.CT_FHIR_JSON_NEW);
theTerser.addElement(theCapabilityStatement, "patchFormat", Constants.CT_FHIR_XML_NEW);
theTerser.addElement(theCapabilityStatement, "patchFormat", Constants.CT_JSON_PATCH);
theTerser.addElement(theCapabilityStatement, "patchFormat", Constants.CT_XML_PATCH);
}
@Override

View File

@ -63,129 +63,4 @@ public class JpaResourceProviderDstu2<T extends IResource> extends BaseJpaResour
super(theDao);
}
@Create
public MethodOutcome create(HttpServletRequest theRequest, @ResourceParam T theResource, @ConditionalUrlParam String theConditional, RequestDetails theRequestDetails) {
startRequest(theRequest);
try {
if (theConditional != null) {
return getDao().create(theResource, theConditional, theRequestDetails);
} else {
return getDao().create(theResource, theRequestDetails);
}
} finally {
endRequest(theRequest);
}
}
@Delete()
public MethodOutcome delete(HttpServletRequest theRequest, @IdParam IdDt theResource, @ConditionalUrlParam(supportsMultiple = true) String theConditional, RequestDetails theRequestDetails) {
startRequest(theRequest);
try {
if (theConditional != null) {
return getDao().deleteByUrl(theConditional, theRequestDetails);
} else {
return getDao().delete(theResource, theRequestDetails);
}
} finally {
endRequest(theRequest);
}
}
@Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = {
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, type = IntegerDt.class)
})
public Parameters expunge(
@IdParam IIdType theIdParam,
@OperationParam(name = OPERATION_EXPUNGE_PARAM_LIMIT) IntegerDt theLimit,
@OperationParam(name = OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES) BooleanDt theExpungeDeletedResources,
@OperationParam(name = OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS) BooleanDt theExpungeOldVersions,
RequestDetails theRequest) {
org.hl7.fhir.r4.model.Parameters retVal = super.doExpunge(theIdParam, theLimit, theExpungeDeletedResources, theExpungeOldVersions, null, theRequest);
return JpaSystemProviderDstu2.toExpungeResponse(retVal);
}
@Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = {
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, type = IntegerDt.class)
})
public Parameters expunge(
@OperationParam(name = OPERATION_EXPUNGE_PARAM_LIMIT) IntegerDt theLimit,
@OperationParam(name = OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES) BooleanDt theExpungeDeletedResources,
@OperationParam(name = OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS) BooleanDt theExpungeOldVersions,
RequestDetails theRequest) {
org.hl7.fhir.r4.model.Parameters retVal = super.doExpunge(null, theLimit, theExpungeDeletedResources, theExpungeOldVersions, null, theRequest);
return JpaSystemProviderDstu2.toExpungeResponse(retVal);
}
@Operation(name = OPERATION_META, idempotent = true, returnParameters = {
@OperationParam(name = "return", type = MetaDt.class)
})
public Parameters meta(RequestDetails theRequestDetails) {
Parameters parameters = new Parameters();
MetaDt metaGetOperation = getDao().metaGetOperation(MetaDt.class, theRequestDetails);
parameters.addParameter().setName("return").setValue(metaGetOperation);
return parameters;
}
@Operation(name = OPERATION_META, idempotent = true, returnParameters = {
@OperationParam(name = "return", type = MetaDt.class)
})
public Parameters meta(@IdParam IdDt theId, RequestDetails theRequestDetails) {
Parameters parameters = new Parameters();
MetaDt metaGetOperation = getDao().metaGetOperation(MetaDt.class, theId, theRequestDetails);
parameters.addParameter().setName("return").setValue(metaGetOperation);
return parameters;
}
@Operation(name = OPERATION_META_ADD, idempotent = true, returnParameters = {
@OperationParam(name = "return", type = MetaDt.class)
})
public Parameters metaAdd(@IdParam IdDt theId, @OperationParam(name = "meta") MetaDt theMeta, RequestDetails theRequestDetails) {
if (theMeta == null) {
throw new InvalidRequestException("Input contains no parameter with name 'meta'");
}
Parameters parameters = new Parameters();
MetaDt metaAddOperation = getDao().metaAddOperation(theId, theMeta, theRequestDetails);
parameters.addParameter().setName("return").setValue(metaAddOperation);
return parameters;
}
@Operation(name = OPERATION_META_DELETE, idempotent = true, returnParameters = {
@OperationParam(name = "return", type = MetaDt.class)
})
public Parameters metaDelete(@IdParam IdDt theId, @OperationParam(name = "meta") MetaDt theMeta, RequestDetails theRequestDetails) {
if (theMeta == null) {
throw new InvalidRequestException("Input contains no parameter with name 'meta'");
}
Parameters parameters = new Parameters();
parameters.addParameter().setName("return").setValue(getDao().metaDeleteOperation(theId, theMeta, theRequestDetails));
return parameters;
}
@Update
public MethodOutcome update(HttpServletRequest theRequest, @ResourceParam T theResource, @IdParam IdDt theId, @ConditionalUrlParam String theConditional, RequestDetails theRequestDetails) {
startRequest(theRequest);
try {
if (theConditional != null) {
return getDao().update(theResource, theConditional, theRequestDetails);
} else {
theResource.setId(theId);
return getDao().update(theResource, theRequestDetails);
}
} finally {
endRequest(theRequest);
}
}
@Validate
public MethodOutcome validate(@ResourceParam T theResource, @ResourceParam String theRawResource, @ResourceParam EncodingEnum theEncoding, @Validate.Mode ValidationModeEnum theMode,
@Validate.Profile String theProfile, RequestDetails theRequestDetails) {
return validate(theResource, null, theRawResource, theEncoding, theMode, theProfile, theRequestDetails);
}
@Validate
public MethodOutcome validate(@ResourceParam T theResource, @IdParam IdDt theId, @ResourceParam String theRawResource, @ResourceParam EncodingEnum theEncoding, @Validate.Mode ValidationModeEnum theMode,
@Validate.Profile String theProfile, RequestDetails theRequestDetails) {
return getDao().validate(theResource, theId, theRawResource, theEncoding, theMode, theProfile, theRequestDetails);
}
}

View File

@ -1,38 +1,30 @@
package ca.uhn.fhir.jpa.provider;
import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
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.dstu2.resource.Parameters.Parameter;
import ca.uhn.fhir.model.primitive.BooleanDt;
import ca.uhn.fhir.model.primitive.IntegerDt;
import ca.uhn.fhir.model.primitive.StringDt;
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.Transaction;
import ca.uhn.fhir.rest.annotation.TransactionParam;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.IntegerType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import javax.servlet.http.HttpServletRequest;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
import static org.apache.commons.lang3.StringUtils.isBlank;
/*
* #%L
@ -60,38 +52,6 @@ public class JpaSystemProviderDstu2 extends BaseJpaSystemProviderDstu2Plus<Bundl
@Qualifier("mySystemDaoDstu2")
private IFhirSystemDao<Bundle, MetaDt> mySystemDao;
@Autowired(required = false)
private IFulltextSearchSvc mySearchDao;
@Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = {
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, type = IntegerDt.class)
})
public Parameters expunge(
@IdParam IIdType theIdParam,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_LIMIT) IntegerDt theLimit,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES) BooleanDt theExpungeDeletedResources,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS) BooleanDt theExpungeOldVersions,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_EVERYTHING) BooleanDt theExpungeEverything,
RequestDetails theRequestDetails
) {
org.hl7.fhir.r4.model.Parameters retVal = super.doExpunge(theLimit, theExpungeDeletedResources, theExpungeOldVersions, theExpungeEverything, theRequestDetails);
return toExpungeResponse(retVal);
}
@Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = {
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, type = IntegerDt.class)
})
public Parameters expunge(
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_LIMIT) IntegerDt theLimit,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES) BooleanDt theExpungeDeletedResources,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS) BooleanDt theExpungeOldVersions,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_EVERYTHING) BooleanDt theExpungeEverything,
RequestDetails theRequestDetails
) {
org.hl7.fhir.r4.model.Parameters retVal = super.doExpunge(theLimit, theExpungeDeletedResources, theExpungeOldVersions, theExpungeEverything, theRequestDetails);
return toExpungeResponse(retVal);
}
//@formatter:off
// This is generated by hand:
// ls 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),/"
@ -222,28 +182,6 @@ public class JpaSystemProviderDstu2 extends BaseJpaSystemProviderDstu2Plus<Bundl
}
}
/**
* /$process-message
*/
@Operation(name = JpaConstants.OPERATION_PROCESS_MESSAGE, idempotent = false)
public IBaseBundle processMessage(
HttpServletRequest theServletRequest,
RequestDetails theRequestDetails,
@OperationParam(name = "content", min = 1, max = 1)
@Description(formalDefinition = "The message to process (or, if using asynchronous messaging, it may be a response message to accept)")
Bundle theMessageToProcess
) {
startRequest(theServletRequest);
try {
return getDao().processMessage(theRequestDetails, theMessageToProcess);
} finally {
endRequest(theServletRequest);
}
}
public static Parameters toExpungeResponse(org.hl7.fhir.r4.model.Parameters theRetVal) {
Integer count = ((IntegerType) theRetVal.getParameterFirstRep().getValue()).getValue();
return new Parameters()

View File

@ -64,136 +64,4 @@ public class JpaResourceProviderDstu3<T extends IAnyResource> extends BaseJpaRes
super(theDao);
}
@Create
public MethodOutcome create(HttpServletRequest theRequest, @ResourceParam T theResource, @ConditionalUrlParam String theConditional, RequestDetails theRequestDetails) {
startRequest(theRequest);
try {
if (theConditional != null) {
return getDao().create(theResource, theConditional, theRequestDetails);
} else {
return getDao().create(theResource, theRequestDetails);
}
} finally {
endRequest(theRequest);
}
}
@Delete()
public MethodOutcome delete(HttpServletRequest theRequest, @IdParam IdType theResource, @ConditionalUrlParam(supportsMultiple = true) String theConditional, RequestDetails theRequestDetails) {
startRequest(theRequest);
try {
if (theConditional != null) {
return getDao().deleteByUrl(theConditional, theRequestDetails);
} else {
return getDao().delete(theResource, theRequestDetails);
}
} finally {
endRequest(theRequest);
}
}
@Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = {
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, type = IntegerType.class)
})
public Parameters expunge(
@IdParam IIdType theIdParam,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_LIMIT) IntegerType theLimit,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES) BooleanType theExpungeDeletedResources,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS) BooleanType theExpungeOldVersions,
RequestDetails theRequest) {
org.hl7.fhir.r4.model.Parameters retVal = super.doExpunge(theIdParam, theLimit, theExpungeDeletedResources, theExpungeOldVersions, null, theRequest);
try {
return convertParameters(retVal);
} catch (FHIRException e) {
throw new InternalErrorException(e);
}
}
@Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = {
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, type = IntegerType.class)
})
public Parameters expunge(
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_LIMIT) IntegerType theLimit,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES) BooleanType theExpungeDeletedResources,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS) BooleanType theExpungeOldVersions,
RequestDetails theRequest) {
org.hl7.fhir.r4.model.Parameters retVal = super.doExpunge(null, theLimit, theExpungeDeletedResources, theExpungeOldVersions, null, theRequest);
try {
return convertParameters(retVal);
} catch (FHIRException e) {
throw new InternalErrorException(e);
}
}
@Operation(name = OPERATION_META, idempotent = true, returnParameters = {
@OperationParam(name = "return", type = Meta.class)
})
public Parameters meta(RequestDetails theRequestDetails) {
Parameters parameters = new Parameters();
Meta metaGetOperation = getDao().metaGetOperation(Meta.class, theRequestDetails);
parameters.addParameter().setName("return").setValue(metaGetOperation);
return parameters;
}
@Operation(name = OPERATION_META, idempotent = true, returnParameters = {
@OperationParam(name = "return", type = Meta.class)
})
public Parameters meta(@IdParam IdType theId, RequestDetails theRequestDetails) {
Parameters parameters = new Parameters();
Meta metaGetOperation = getDao().metaGetOperation(Meta.class, theId, theRequestDetails);
parameters.addParameter().setName("return").setValue(metaGetOperation);
return parameters;
}
@Operation(name = OPERATION_META_ADD, idempotent = true, returnParameters = {
@OperationParam(name = "return", type = Meta.class)
})
public Parameters metaAdd(@IdParam IdType theId, @OperationParam(name = "meta") Meta theMeta, RequestDetails theRequestDetails) {
if (theMeta == null) {
throw new InvalidRequestException("Input contains no parameter with name 'meta'");
}
Parameters parameters = new Parameters();
Meta metaAddOperation = getDao().metaAddOperation(theId, theMeta, theRequestDetails);
parameters.addParameter().setName("return").setValue(metaAddOperation);
return parameters;
}
@Operation(name = OPERATION_META_DELETE, idempotent = true, returnParameters = {
@OperationParam(name = "return", type = Meta.class)
})
public Parameters metaDelete(@IdParam IdType theId, @OperationParam(name = "meta") Meta theMeta, RequestDetails theRequestDetails) {
if (theMeta == null) {
throw new InvalidRequestException("Input contains no parameter with name 'meta'");
}
Parameters parameters = new Parameters();
parameters.addParameter().setName("return").setValue(getDao().metaDeleteOperation(theId, theMeta, theRequestDetails));
return parameters;
}
@Update
public MethodOutcome update(HttpServletRequest theRequest, @ResourceParam T theResource, @IdParam IdType theId, @ConditionalUrlParam String theConditional, RequestDetails theRequestDetails) {
startRequest(theRequest);
try {
if (theConditional != null) {
return getDao().update(theResource, theConditional, theRequestDetails);
} else {
return getDao().update(theResource, theRequestDetails);
}
} finally {
endRequest(theRequest);
}
}
@Validate
public MethodOutcome validate(@ResourceParam T theResource, @ResourceParam String theRawResource, @ResourceParam EncodingEnum theEncoding, @Validate.Mode ValidationModeEnum theMode,
@Validate.Profile String theProfile, RequestDetails theRequestDetails) {
return validate(theResource, null, theRawResource, theEncoding, theMode, theProfile, theRequestDetails);
}
@Validate
public MethodOutcome validate(@ResourceParam T theResource, @IdParam IdType theId, @ResourceParam String theRawResource, @ResourceParam EncodingEnum theEncoding, @Validate.Mode ValidationModeEnum theMode,
@Validate.Profile String theProfile, RequestDetails theRequestDetails) {
return getDao().validate(theResource, theId, theRawResource, theEncoding, theMode, theProfile, theRequestDetails);
}
}

View File

@ -65,46 +65,6 @@ public class JpaSystemProviderDstu3 extends BaseJpaSystemProviderDstu2Plus<Bundl
@Qualifier("mySystemDaoDstu3")
private IFhirSystemDao<Bundle, Meta> mySystemDao;
@Autowired(required = false)
private IFulltextSearchSvc mySearchDao;
@Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = {
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, type = IntegerType.class)
})
public Parameters expunge(
@IdParam IIdType theIdParam,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_LIMIT) IntegerType theLimit,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES) BooleanType theExpungeDeletedResources,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS) BooleanType theExpungeOldVersions,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_EVERYTHING) BooleanType theExpungeEverything,
RequestDetails theRequestDetails
) {
org.hl7.fhir.r4.model.Parameters retVal = super.doExpunge(theLimit, theExpungeDeletedResources, theExpungeOldVersions, theExpungeEverything, theRequestDetails);
try {
return convertParameters(retVal);
} catch (FHIRException e) {
throw new InternalErrorException(e);
}
}
@Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = {
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, type = IntegerType.class)
})
public Parameters expunge(
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_LIMIT) IntegerType theLimit,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES) BooleanType theExpungeDeletedResources,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS) BooleanType theExpungeOldVersions,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_EVERYTHING) BooleanType theExpungeEverything,
RequestDetails theRequestDetails
) {
org.hl7.fhir.r4.model.Parameters retVal = super.doExpunge(theLimit, theExpungeDeletedResources, theExpungeOldVersions, theExpungeEverything, theRequestDetails);
try {
return convertParameters(retVal);
} catch (FHIRException e) {
throw new InternalErrorException(e);
}
}
// This is generated by hand:
// ls 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=IntegerType.class, min=0, max=1),/"
@Operation(name = JpaConstants.OPERATION_GET_RESOURCE_COUNTS, idempotent = true, returnParameters = {
@ -233,32 +193,4 @@ public class JpaSystemProviderDstu3 extends BaseJpaSystemProviderDstu2Plus<Bundl
}
}
/**
* /$process-message
*/
@Operation(name = JpaConstants.OPERATION_PROCESS_MESSAGE, idempotent = false)
public IBaseBundle processMessage(
HttpServletRequest theServletRequest,
RequestDetails theRequestDetails,
@OperationParam(name = "content", min = 1, max = 1)
@Description(formalDefinition = "The message to process (or, if using asynchronous messaging, it may be a response message to accept)")
Bundle theMessageToProcess
) {
startRequest(theServletRequest);
try {
return getDao().processMessage(theRequestDetails, theMessageToProcess);
} finally {
endRequest(theServletRequest);
}
}
public static void validateFulltextSearchEnabled(IFulltextSearchSvc theSearchDao) {
if (theSearchDao == null || theSearchDao.isDisabled()) {
throw new InvalidRequestException("Fulltext searching is disabled on this server");
}
}
}

View File

@ -61,128 +61,4 @@ public class JpaResourceProviderR4<T extends IAnyResource> extends BaseJpaResour
super(theDao);
}
@Create
public MethodOutcome create(HttpServletRequest theRequest, @ResourceParam T theResource, @ConditionalUrlParam String theConditional, RequestDetails theRequestDetails) {
startRequest(theRequest);
try {
if (theConditional != null) {
return getDao().create(theResource, theConditional, theRequestDetails);
} else {
return getDao().create(theResource, theRequestDetails);
}
} finally {
endRequest(theRequest);
}
}
@Delete()
public MethodOutcome delete(HttpServletRequest theRequest, @IdParam IdType theResource, @ConditionalUrlParam(supportsMultiple = true) String theConditional, RequestDetails theRequestDetails) {
startRequest(theRequest);
try {
if (theConditional != null) {
return getDao().deleteByUrl(theConditional, theRequestDetails);
} else {
return getDao().delete(theResource, theRequestDetails);
}
} finally {
endRequest(theRequest);
}
}
@Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = {
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, type = IntegerType.class)
})
public Parameters expunge(
@IdParam IIdType theIdParam,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_LIMIT) IntegerType theLimit,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES) BooleanType theExpungeDeletedResources,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS) BooleanType theExpungeOldVersions,
RequestDetails theRequest) {
return super.doExpunge(theIdParam, theLimit, theExpungeDeletedResources, theExpungeOldVersions, null, theRequest);
}
@Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = {
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, type = IntegerType.class)
})
public Parameters expunge(
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_LIMIT) IntegerType theLimit,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES) BooleanType theExpungeDeletedResources,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS) BooleanType theExpungeOldVersions,
RequestDetails theRequest) {
return super.doExpunge(null, theLimit, theExpungeDeletedResources, theExpungeOldVersions, null, theRequest);
}
@Operation(name = OPERATION_META, idempotent = true, returnParameters = {
@OperationParam(name = "return", type = Meta.class)
})
public Parameters meta(RequestDetails theRequestDetails) {
Parameters parameters = new Parameters();
Meta metaGetOperation = getDao().metaGetOperation(Meta.class, theRequestDetails);
parameters.addParameter().setName("return").setValue(metaGetOperation);
return parameters;
}
@Operation(name = OPERATION_META, idempotent = true, returnParameters = {
@OperationParam(name = "return", type = Meta.class)
})
public Parameters meta(@IdParam IdType theId, RequestDetails theRequestDetails) {
Parameters parameters = new Parameters();
Meta metaGetOperation = getDao().metaGetOperation(Meta.class, theId, theRequestDetails);
parameters.addParameter().setName("return").setValue(metaGetOperation);
return parameters;
}
@Operation(name = OPERATION_META_ADD, idempotent = true, returnParameters = {
@OperationParam(name = "return", type = Meta.class)
})
public Parameters metaAdd(@IdParam IdType theId, @OperationParam(name = "meta") Meta theMeta, RequestDetails theRequestDetails) {
if (theMeta == null) {
throw new InvalidRequestException("Input contains no parameter with name 'meta'");
}
Parameters parameters = new Parameters();
Meta metaAddOperation = getDao().metaAddOperation(theId, theMeta, theRequestDetails);
parameters.addParameter().setName("return").setValue(metaAddOperation);
return parameters;
}
@Operation(name = OPERATION_META_DELETE, idempotent = true, returnParameters = {
@OperationParam(name = "return", type = Meta.class)
})
public Parameters metaDelete(@IdParam IdType theId, @OperationParam(name = "meta") Meta theMeta, RequestDetails theRequestDetails) {
if (theMeta == null) {
throw new InvalidRequestException("Input contains no parameter with name 'meta'");
}
Parameters parameters = new Parameters();
parameters.addParameter().setName("return").setValue(getDao().metaDeleteOperation(theId, theMeta, theRequestDetails));
return parameters;
}
@Update
public MethodOutcome update(HttpServletRequest theRequest, @ResourceParam T theResource, @IdParam IdType theId, @ConditionalUrlParam String theConditional, RequestDetails theRequestDetails) {
startRequest(theRequest);
try {
if (theConditional != null) {
return getDao().update(theResource, theConditional, theRequestDetails);
} else {
return getDao().update(theResource, theRequestDetails);
}
} finally {
endRequest(theRequest);
}
}
@Validate
public MethodOutcome validate(@ResourceParam T theResource, @ResourceParam String theRawResource, @ResourceParam EncodingEnum theEncoding, @Validate.Mode ValidationModeEnum theMode,
@Validate.Profile String theProfile, RequestDetails theRequestDetails) {
return validate(theResource, null, theRawResource, theEncoding, theMode, theProfile, theRequestDetails);
}
@Validate
public MethodOutcome validate(@ResourceParam T theResource, @IdParam IdType theId, @ResourceParam String theRawResource, @ResourceParam EncodingEnum theEncoding, @Validate.Mode ValidationModeEnum theMode,
@Validate.Profile String theProfile, RequestDetails theRequestDetails) {
return getDao().validate(theResource, theId, theRawResource, theEncoding, theMode, theProfile, theRequestDetails);
}
}

View File

@ -1,40 +1,28 @@
package ca.uhn.fhir.jpa.provider.r4;
import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.provider.BaseJpaSystemProviderDstu2Plus;
import ca.uhn.fhir.model.api.annotation.Description;
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.Transaction;
import ca.uhn.fhir.rest.annotation.TransactionParam;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.BooleanType;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.DecimalType;
import org.hl7.fhir.r4.model.IntegerType;
import org.hl7.fhir.r4.model.Meta;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
import org.hl7.fhir.r4.model.StringType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import javax.servlet.http.HttpServletRequest;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
import static org.apache.commons.lang3.StringUtils.isBlank;
/*
* #%L
@ -62,36 +50,6 @@ public class JpaSystemProviderR4 extends BaseJpaSystemProviderDstu2Plus<Bundle,
@Qualifier("mySystemDaoR4")
private IFhirSystemDao<Bundle, Meta> mySystemDao;
@Autowired(required = false)
private IFulltextSearchSvc mySearchDao;
@Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = {
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, type = IntegerType.class)
})
public Parameters expunge(
@IdParam IIdType theIdParam,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_LIMIT) IntegerType theLimit,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES) BooleanType theExpungeDeletedResources,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS) BooleanType theExpungeOldVersions,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_EVERYTHING) BooleanType theExpungeEverything,
RequestDetails theRequestDetails
) {
return super.doExpunge(theLimit, theExpungeDeletedResources, theExpungeOldVersions, theExpungeEverything, theRequestDetails);
}
@Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = {
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, type = IntegerType.class)
})
public Parameters expunge(
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_LIMIT) IntegerType theLimit,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES) BooleanType theExpungeDeletedResources,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS) BooleanType theExpungeOldVersions,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_EVERYTHING) BooleanType theExpungeEverything,
RequestDetails theRequestDetails
) {
return super.doExpunge(theLimit, theExpungeDeletedResources, theExpungeOldVersions, theExpungeEverything, theRequestDetails);
}
// This is generated by hand:
// ls 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=IntegerType.class, min=0, max=1),/"
@Operation(name = JpaConstants.OPERATION_GET_RESOURCE_COUNTS, idempotent = true, returnParameters = {
@ -210,28 +168,6 @@ public class JpaSystemProviderR4 extends BaseJpaSystemProviderDstu2Plus<Bundle,
return parameters;
}
/**
* /$process-message
*/
@Operation(name = JpaConstants.OPERATION_PROCESS_MESSAGE, idempotent = false)
public IBaseBundle processMessage(
HttpServletRequest theServletRequest,
RequestDetails theRequestDetails,
@OperationParam(name = "content", min = 1, max = 1)
@Description(formalDefinition = "The message to process (or, if using asynchronous messaging, it may be a response message to accept)")
Bundle theMessageToProcess
) {
startRequest(theServletRequest);
try {
return getDao().processMessage(theRequestDetails, theMessageToProcess);
} finally {
endRequest(theServletRequest);
}
}
@Transaction
public Bundle transaction(RequestDetails theRequestDetails, @TransactionParam Bundle theResources) {
startRequest(((ServletRequestDetails) theRequestDetails).getServletRequest());

View File

@ -61,130 +61,4 @@ public class JpaResourceProviderR5<T extends IAnyResource> extends BaseJpaResour
super(theDao);
}
@Create
public MethodOutcome create(HttpServletRequest theRequest, @ResourceParam T theResource, @ConditionalUrlParam String theConditional, RequestDetails theRequestDetails) {
startRequest(theRequest);
try {
if (theConditional != null) {
return getDao().create(theResource, theConditional, theRequestDetails);
} else {
return getDao().create(theResource, theRequestDetails);
}
} finally {
endRequest(theRequest);
}
}
@Delete()
public MethodOutcome delete(HttpServletRequest theRequest, @IdParam IdType theResource, @ConditionalUrlParam(supportsMultiple = true) String theConditional, RequestDetails theRequestDetails) {
startRequest(theRequest);
try {
if (theConditional != null) {
return getDao().deleteByUrl(theConditional, theRequestDetails);
} else {
return getDao().delete(theResource, theRequestDetails);
}
} finally {
endRequest(theRequest);
}
}
@Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = {
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, type = IntegerType.class)
})
public Parameters expunge(
@IdParam IIdType theIdParam,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_LIMIT) IntegerType theLimit,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES) BooleanType theExpungeDeletedResources,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS) BooleanType theExpungeOldVersions,
RequestDetails theRequest) {
org.hl7.fhir.r4.model.Parameters parameters = super.doExpunge(theIdParam, theLimit, theExpungeDeletedResources, theExpungeOldVersions, null, theRequest);
return org.hl7.fhir.convertors.conv40_50.Parameters40_50.convertParameters(parameters);
}
@Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = {
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, type = IntegerType.class)
})
public Parameters expunge(
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_LIMIT) IntegerType theLimit,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES) BooleanType theExpungeDeletedResources,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS) BooleanType theExpungeOldVersions,
RequestDetails theRequest) {
org.hl7.fhir.r4.model.Parameters parameters = super.doExpunge(null, theLimit, theExpungeDeletedResources, theExpungeOldVersions, null, theRequest);
return org.hl7.fhir.convertors.conv40_50.Parameters40_50.convertParameters(parameters);
}
@Operation(name = OPERATION_META, idempotent = true, returnParameters = {
@OperationParam(name = "return", type = Meta.class)
})
public Parameters meta(RequestDetails theRequestDetails) {
Parameters parameters = new Parameters();
Meta metaGetOperation = getDao().metaGetOperation(Meta.class, theRequestDetails);
parameters.addParameter().setName("return").setValue(metaGetOperation);
return parameters;
}
@Operation(name = OPERATION_META, idempotent = true, returnParameters = {
@OperationParam(name = "return", type = Meta.class)
})
public Parameters meta(@IdParam IdType theId, RequestDetails theRequestDetails) {
Parameters parameters = new Parameters();
Meta metaGetOperation = getDao().metaGetOperation(Meta.class, theId, theRequestDetails);
parameters.addParameter().setName("return").setValue(metaGetOperation);
return parameters;
}
@Operation(name = OPERATION_META_ADD, idempotent = true, returnParameters = {
@OperationParam(name = "return", type = Meta.class)
})
public Parameters metaAdd(@IdParam IdType theId, @OperationParam(name = "meta") Meta theMeta, RequestDetails theRequestDetails) {
if (theMeta == null) {
throw new InvalidRequestException("Input contains no parameter with name 'meta'");
}
Parameters parameters = new Parameters();
Meta metaAddOperation = getDao().metaAddOperation(theId, theMeta, theRequestDetails);
parameters.addParameter().setName("return").setValue(metaAddOperation);
return parameters;
}
@Operation(name = OPERATION_META_DELETE, idempotent = true, returnParameters = {
@OperationParam(name = "return", type = Meta.class)
})
public Parameters metaDelete(@IdParam IdType theId, @OperationParam(name = "meta") Meta theMeta, RequestDetails theRequestDetails) {
if (theMeta == null) {
throw new InvalidRequestException("Input contains no parameter with name 'meta'");
}
Parameters parameters = new Parameters();
parameters.addParameter().setName("return").setValue(getDao().metaDeleteOperation(theId, theMeta, theRequestDetails));
return parameters;
}
@Update
public MethodOutcome update(HttpServletRequest theRequest, @ResourceParam T theResource, @IdParam IdType theId, @ConditionalUrlParam String theConditional, RequestDetails theRequestDetails) {
startRequest(theRequest);
try {
if (theConditional != null) {
return getDao().update(theResource, theConditional, theRequestDetails);
} else {
return getDao().update(theResource, theRequestDetails);
}
} finally {
endRequest(theRequest);
}
}
@Validate
public MethodOutcome validate(@ResourceParam T theResource, @ResourceParam String theRawResource, @ResourceParam EncodingEnum theEncoding, @Validate.Mode ValidationModeEnum theMode,
@Validate.Profile String theProfile, RequestDetails theRequestDetails) {
return validate(theResource, null, theRawResource, theEncoding, theMode, theProfile, theRequestDetails);
}
@Validate
public MethodOutcome validate(@ResourceParam T theResource, @IdParam IdType theId, @ResourceParam String theRawResource, @ResourceParam EncodingEnum theEncoding, @Validate.Mode ValidationModeEnum theMode,
@Validate.Profile String theProfile, RequestDetails theRequestDetails) {
return getDao().validate(theResource, theId, theRawResource, theEncoding, theMode, theProfile, theRequestDetails);
}
}

View File

@ -62,38 +62,6 @@ public class JpaSystemProviderR5 extends BaseJpaSystemProviderDstu2Plus<Bundle,
@Qualifier("mySystemDaoR5")
private IFhirSystemDao<Bundle, Meta> mySystemDao;
@Autowired(required = false)
private IFulltextSearchSvc mySearchDao;
@Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = {
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, type = IntegerType.class)
})
public Parameters expunge(
@IdParam IIdType theIdParam,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_LIMIT) IntegerType theLimit,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES) BooleanType theExpungeDeletedResources,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS) BooleanType theExpungeOldVersions,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_EVERYTHING) BooleanType theExpungeEverything,
RequestDetails theRequestDetails
) {
org.hl7.fhir.r4.model.Parameters parameters = super.doExpunge(theLimit, theExpungeDeletedResources, theExpungeOldVersions, theExpungeEverything, theRequestDetails);
return org.hl7.fhir.convertors.conv40_50.Parameters40_50.convertParameters(parameters);
}
@Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = {
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, type = IntegerType.class)
})
public Parameters expunge(
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_LIMIT) IntegerType theLimit,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES) BooleanType theExpungeDeletedResources,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS) BooleanType theExpungeOldVersions,
@OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_EVERYTHING) BooleanType theExpungeEverything,
RequestDetails theRequestDetails
) {
org.hl7.fhir.r4.model.Parameters parameters = super.doExpunge(theLimit, theExpungeDeletedResources, theExpungeOldVersions, theExpungeEverything, theRequestDetails);
return org.hl7.fhir.convertors.conv40_50.Parameters40_50.convertParameters(parameters);
}
// This is generated by hand:
// ls 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=IntegerType.class, min=0, max=1),/"
@Operation(name = JpaConstants.OPERATION_GET_RESOURCE_COUNTS, idempotent = true, returnParameters = {
@ -212,28 +180,6 @@ public class JpaSystemProviderR5 extends BaseJpaSystemProviderDstu2Plus<Bundle,
return parameters;
}
/**
* /$process-message
*/
@Operation(name = JpaConstants.OPERATION_PROCESS_MESSAGE, idempotent = false)
public IBaseBundle processMessage(
HttpServletRequest theServletRequest,
RequestDetails theRequestDetails,
@OperationParam(name = "content", min = 1, max = 1)
@Description(formalDefinition = "The message to process (or, if using asynchronous messaging, it may be a response message to accept)")
Bundle theMessageToProcess
) {
startRequest(theServletRequest);
try {
return getDao().processMessage(theRequestDetails, theMessageToProcess);
} finally {
endRequest(theServletRequest);
}
}
@Transaction
public Bundle transaction(RequestDetails theRequestDetails, @TransactionParam Bundle theResources) {
startRequest(((ServletRequestDetails) theRequestDetails).getServletRequest());

View File

@ -52,6 +52,7 @@ import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Practitioner;
import org.hl7.fhir.r4.model.PractitionerRole;
import org.hl7.fhir.r4.model.SearchParameter;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.slf4j.Logger;
@ -2172,6 +2173,39 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
assertEquals(1, StringUtils.countMatches(searchSql, "SP_VALUE_NORMALIZED"));
}
/*
* Should try to get this down at some point
*/
@Test
@Disabled
public void testSearch_StringParam_SearchOnePartition_AddRevIncludes() {
addReadPartition(1);
addCreatePartition(1, null);
Organization org = new Organization();
org.setName("FOO");
org.setId("FOO-ORG");
myOrganizationDao.update(org, mySrd);
for (int i = 0; i < 50; i++) {
addCreatePartition(1, null);
PractitionerRole pr = new PractitionerRole();
pr.getOrganization().setReference("Organization/FOO-ORG");
myPractitionerRoleDao.create(pr, mySrd);
}
addReadPartition(1);
myCaptureQueriesListener.clear();
SearchParameterMap map = new SearchParameterMap();
map.addRevInclude(PractitionerRole.INCLUDE_ORGANIZATION);
map.setCount(10);
IBundleProvider results = myOrganizationDao.search(map, mySrd);
List<IIdType> ids = toUnqualifiedVersionlessIds(results);
myCaptureQueriesListener.logSelectQueries();
assertEquals(10, ids.size(), () -> ids.toString());
}
@Test
public void testSearch_TagNotParam_SearchAllPartitions() {
IIdType patientIdNull = createPatient(withPartition(null), withActiveTrue(), withTag("http://system", "code"), withIdentifier("http://foo", "bar"));

View File

@ -23,6 +23,7 @@ import com.google.common.base.Charsets;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
@ -387,8 +388,8 @@ public class SystemProviderDstu2Test extends BaseJpaDstu2Test {
@Test
public void testMarkResourcesForReindexing() throws Exception {
HttpGet get = new HttpGet(ourServerBase + "/$mark-all-resources-for-reindexing");
CloseableHttpResponse http = ourHttpClient.execute(get);
HttpPost post = new HttpPost(ourServerBase + "/$mark-all-resources-for-reindexing");
CloseableHttpResponse http = ourHttpClient.execute(post);
try {
String output = IOUtils.toString(http.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(output);

View File

@ -9,6 +9,7 @@ import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.Set;
import ca.uhn.fhir.rest.openapi.OpenApiInterceptor;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
@ -16,6 +17,7 @@ import org.hl7.fhir.dstu3.model.CapabilityStatement;
import org.hl7.fhir.dstu3.model.CapabilityStatement.CapabilityStatementRestResourceComponent;
import org.hl7.fhir.dstu3.model.CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import ca.uhn.fhir.util.TestUtil;
@ -24,6 +26,12 @@ public class ServerDstu3Test extends BaseResourceProviderDstu3Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServerDstu3Test.class);
@Override
@AfterEach
public void after() throws Exception {
super.after();
ourRestServer.getInterceptorService().unregisterInterceptorsIf(t->t instanceof OpenApiInterceptor);
}
/**
@ -61,5 +69,18 @@ public class ServerDstu3Test extends BaseResourceProviderDstu3Test {
}
@Test
public void testFetchOpenApi() throws IOException {
ourRestServer.registerInterceptor(new OpenApiInterceptor());
HttpGet get = new HttpGet(ourServerBase + "/api-docs");
try (CloseableHttpResponse response = ourHttpClient.execute(get)) {
String string = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(string);
assertEquals(200, response.getStatusLine().getStatusCode());
}
}
}

View File

@ -0,0 +1,48 @@
package ca.uhn.fhir.jpa.provider.r4;
import ca.uhn.fhir.rest.openapi.OpenApiInterceptor;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class OpenApiInterceptorJpaTest extends BaseResourceProviderR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OpenApiInterceptorJpaTest.class);
@Override
@AfterEach
public void after() throws Exception {
super.after();
ourRestServer.getInterceptorService().unregisterInterceptorsIf(t -> t instanceof OpenApiInterceptor);
}
@Test
public void testFetchOpenApi() throws IOException {
ourRestServer.registerInterceptor(new OpenApiInterceptor());
HttpGet get = new HttpGet(ourServerBase + "/metadata?_format=json&_pretty=true");
try (CloseableHttpResponse response = ourHttpClient.execute(get)) {
String string = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(string);
assertEquals(200, response.getStatusLine().getStatusCode());
}
get = new HttpGet(ourServerBase + "/api-docs");
try (CloseableHttpResponse response = ourHttpClient.execute(get)) {
String string = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(string);
assertEquals(200, response.getStatusLine().getStatusCode());
}
}
}

View File

@ -17,6 +17,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.util.List;
import java.util.TreeSet;
import java.util.stream.Collectors;
import static org.hamcrest.MatcherAssert.assertThat;
@ -26,6 +27,7 @@ import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
public class ServerCapabilityStatementProviderJpaR4Test extends BaseResourceProviderR4Test {
@ -39,6 +41,38 @@ public class ServerCapabilityStatementProviderJpaR4Test extends BaseResourceProv
assertThat(resourceTypes, hasItems("Patient", "Observation", "SearchParameter"));
}
@Test
public void testNoDuplicateResourceOperationNames() {
CapabilityStatement cs = myClient.capabilities().ofType(CapabilityStatement.class).execute();
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(cs));
for (CapabilityStatement.CapabilityStatementRestResourceComponent next : cs.getRestFirstRep().getResource()) {
List<String> opNames = next
.getOperation()
.stream()
.map(t -> t.getName())
.sorted()
.collect(Collectors.toList());
ourLog.info("System ops: {}", opNames);
assertEquals(opNames.stream().distinct().sorted().collect(Collectors.toList()), opNames);
}
}
@Test
public void testNoDuplicateSystemOperationNames() {
CapabilityStatement cs = myClient.capabilities().ofType(CapabilityStatement.class).execute();
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(cs));
List<String> systemOpNames = cs
.getRestFirstRep()
.getOperation()
.stream()
.map(t -> t.getName())
.sorted()
.collect(Collectors.toList());
ourLog.info("System ops: {}", systemOpNames);
assertEquals(systemOpNames.stream().distinct().sorted().collect(Collectors.toList()), systemOpNames);
}
@Test
public void testCustomSearchParamsReflectedInSearchParams() {
SearchParameter fooSp = new SearchParameter();

View File

@ -18,6 +18,7 @@ import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.client.apache.ResourceEntity;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.interceptor.SimpleRequestHeaderInterceptor;
import ca.uhn.fhir.rest.param.ReferenceParam;
@ -36,6 +37,7 @@ import org.apache.http.Header;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
@ -49,6 +51,7 @@ import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Bundle.BundleType;
import org.hl7.fhir.r4.model.Bundle.HTTPVerb;
import org.hl7.fhir.r4.model.CodeType;
import org.hl7.fhir.r4.model.DecimalType;
import org.hl7.fhir.r4.model.Enumerations.AdministrativeGender;
import org.hl7.fhir.r4.model.IdType;
@ -75,6 +78,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.in;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ -247,8 +251,8 @@ public class SystemProviderR4Test extends BaseJpaR4Test {
@Test
public void testMarkResourcesForReindexing() throws Exception {
HttpGet get = new HttpGet(ourServerBase + "/$mark-all-resources-for-reindexing");
CloseableHttpResponse http = ourHttpClient.execute(get);
HttpRequestBase post = new HttpPost(ourServerBase + "/$mark-all-resources-for-reindexing");
CloseableHttpResponse http = ourHttpClient.execute(post);
try {
String output = IOUtils.toString(http.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(output);
@ -257,8 +261,8 @@ public class SystemProviderR4Test extends BaseJpaR4Test {
IOUtils.closeQuietly(http);
}
get = new HttpGet(ourServerBase + "/$perform-reindexing-pass");
http = ourHttpClient.execute(get);
post = new HttpPost(ourServerBase + "/$perform-reindexing-pass");
http = ourHttpClient.execute(post);
try {
String output = IOUtils.toString(http.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(output);
@ -272,8 +276,10 @@ public class SystemProviderR4Test extends BaseJpaR4Test {
@Test
public void testMarkResourcesForReindexingTyped() throws Exception {
HttpGet get = new HttpGet(ourServerBase + "/$mark-all-resources-for-reindexing?type=Patient");
CloseableHttpResponse http = ourHttpClient.execute(get);
HttpPost post = new HttpPost(ourServerBase + "/$mark-all-resources-for-reindexing?type=Patient");
post.setEntity(new ResourceEntity(myFhirCtx, new Parameters().addParameter("type", new CodeType("Patient"))));
CloseableHttpResponse http = ourHttpClient.execute(post);
try {
String output = IOUtils.toString(http.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(output);
@ -282,8 +288,9 @@ public class SystemProviderR4Test extends BaseJpaR4Test {
IOUtils.closeQuietly(http);
}
get = new HttpGet(ourServerBase + "/$mark-all-resources-for-reindexing?type=FOO");
http = ourHttpClient.execute(get);
post = new HttpPost(ourServerBase + "/$mark-all-resources-for-reindexing?type=FOO");
post.setEntity(new ResourceEntity(myFhirCtx, new Parameters().addParameter("type", new CodeType("FOO"))));
http = ourHttpClient.execute(post);
try {
String output = IOUtils.toString(http.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(output);

View File

@ -29,16 +29,6 @@ import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.testcontainers.elasticsearch.ElasticsearchContainer;
import org.testcontainers.junit.jupiter.Container;
@ -66,20 +56,12 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
public class LastNElasticsearchSvcMultipleObservationsIT {
static private final Calendar baseObservationDate = new GregorianCalendar();
private static ObjectMapper ourMapperNonPrettyPrint;
private static boolean indexLoaded = false;
private final Map<String, Map<String, List<Date>>> createdPatientObservationMap = new HashMap<>();
private final FhirContext myFhirContext = FhirContext.forCached(FhirVersionEnum.R4);
@Container
public static ElasticsearchContainer elasticsearchContainer = TestElasticsearchContainerHelper.getEmbeddedElasticSearch();
private static ObjectMapper ourMapperNonPrettyPrint;
private static boolean indexLoaded = false;
private final Map<String, Map<String, List<Date>>> createdPatientObservationMap = new HashMap<>();
private final FhirContext myFhirContext = FhirContext.forCached(FhirVersionEnum.R4);
private ElasticsearchSvcImpl elasticsearchSvc;
@BeforeEach

View File

@ -40,12 +40,10 @@ import ca.uhn.fhir.util.StopWatch;
import ca.uhn.fhir.validation.IInstanceValidatorModule;
import com.google.common.collect.Lists;
import org.hamcrest.Matchers;
import org.hibernate.Session;
import org.hibernate.internal.SessionImpl;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.ExplanationOfBenefit;
import org.junit.Ignore;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.term;
import org.junit.jupiter.api.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class BaseTermReadSvcImplTest {

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
@ -131,6 +131,11 @@
</exclusions>
</dependency>
<!-- test -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
@ -144,13 +149,13 @@
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-test-utilities</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-jpaserver-test-utilities</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<scope>test</scope>
</dependency>
</dependencies>

View File

@ -13,11 +13,8 @@ import org.cqframework.cql.cql2elm.NamespaceManager;
import org.cqframework.cql.elm.execution.Library;
import org.cqframework.cql.elm.tracking.TrackBack;
import org.hl7.elm.r1.VersionedIdentifier;
import org.junit.After;
import org.junit.Before;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Matchers;
@ -33,8 +30,11 @@ import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import static org.junit.Assert.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.reset;
@ -61,7 +61,7 @@ public class TranslatorHelperTest implements CqlProviderTestBase {
//@BeforeEach
//@BeforeAll
@Before
@BeforeEach
public void createMocks() {
MockitoAnnotations.openMocks(this);
//libraryManager = Mockito.mock(LibraryManager.class);
@ -85,7 +85,7 @@ public class TranslatorHelperTest implements CqlProviderTestBase {
when(libraryManager.getNamespaceManager()).thenReturn(namespaceManager);
when(namespaceManager.hasNamespaces()).thenReturn(false);
CqlTranslator translator = TranslatorHelper.getTranslator(sampleCql, libraryManager, modelManager);
assertNotNull("translator should not be NULL!", translator);
assertNotNull(translator, "translator should not be NULL!");
}
//@Test
@ -95,7 +95,7 @@ public class TranslatorHelperTest implements CqlProviderTestBase {
when(libraryManager.getLibrarySourceLoader()).thenReturn(librarySourceLoader);
when(namespaceManager.hasNamespaces()).thenReturn(true);
CqlTranslator translator = TranslatorHelper.getTranslator(sampleCql, libraryManager, modelManager);
assertNotNull("translator should not be NULL!", translator);
assertNotNull(translator, "translator should not be NULL!");
}
//@Test
@ -109,7 +109,7 @@ public class TranslatorHelperTest implements CqlProviderTestBase {
translator = TranslatorHelper.getTranslator(" ", libraryManager, modelManager);
fail();
} catch (NullPointerException e) {
assertNull("translator should be NULL!", translator);
assertNull(translator, "translator should be NULL!");
}
}
@ -135,7 +135,7 @@ public class TranslatorHelperTest implements CqlProviderTestBase {
fail();
} catch (IllegalArgumentException | IOException e) {
assertTrue(e instanceof IllegalArgumentException);
assertNull("translator should be NULL!", translator);
assertNull(translator, "translator should be NULL!");
}
}
@ -157,7 +157,7 @@ public class TranslatorHelperTest implements CqlProviderTestBase {
e.printStackTrace();
fail();
}
assertNotNull("library should not be NULL!", library);
assertNotNull(library, "library should not be NULL!");
}
@Test
@ -167,7 +167,7 @@ public class TranslatorHelperTest implements CqlProviderTestBase {
library = TranslatorHelper.readLibrary(new ByteArrayInputStream("INVALID-XML-DOCUMENT".getBytes()));
fail();
} catch (IllegalArgumentException e) {
assertNull("library should be NULL!", library);
assertNull(library, "library should be NULL!");
}
}
@ -181,7 +181,7 @@ public class TranslatorHelperTest implements CqlProviderTestBase {
} catch (IOException e) {
e.printStackTrace();
}
assertNotNull("library should not be NULL!", library);
assertNotNull(library, "library should not be NULL!");
}
@Test

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
@ -55,13 +55,13 @@
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-test-utilities</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-jpaserver-test-utilities</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
@ -47,6 +47,11 @@
<version>${project.version}</version>
<classifier>classes</classifier>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-server-openapi</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.helger</groupId>
@ -164,7 +169,7 @@
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-converter</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
</dependency>
</dependencies>

View File

@ -19,6 +19,7 @@ import ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3;
import ca.uhn.fhir.jpa.provider.r4.JpaSystemProviderR4;
import ca.uhn.fhir.jpa.provider.r5.JpaSystemProviderR5;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.rest.openapi.OpenApiInterceptor;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import ca.uhn.fhir.jpa.subscription.match.config.WebsocketDispatcherConfig;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
@ -38,6 +39,7 @@ import ca.uhn.fhirtest.config.TestR4Config;
import ca.uhn.fhirtest.config.TestR5Config;
import ca.uhn.hapi.converters.server.VersionedApiConverterInterceptor;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
@ -79,11 +81,11 @@ public class TestRestfulServer extends RestfulServer {
WebApplicationContext parentAppCtx = ContextLoaderListener.getCurrentWebApplicationContext();
// These two parmeters are also declared in web.xml
String implDesc = getInitParameter("ImplementationDescription");
String fhirVersionParam = getInitParameter("FhirVersion");
if (StringUtils.isBlank(fhirVersionParam)) {
fhirVersionParam = "DSTU1";
}
Validate.notNull(fhirVersionParam);
setImplementationDescription("HAPI FHIR Test/Demo Server " + fhirVersionParam + " Endpoint");
setCopyright("This server is **Open Source Software**, licensed under the terms of the [Apache Software License 2.0](https://www.apache.org/licenses/LICENSE-2.0).");
// Depending on the version this server is supporing, we will
// retrieve all the appropriate resource providers and the
@ -110,7 +112,6 @@ public class TestRestfulServer extends RestfulServer {
systemDao = myAppCtx.getBean("mySystemDaoDstu2", IFhirSystemDao.class);
etagSupport = ETagSupportEnum.ENABLED;
JpaConformanceProviderDstu2 confProvider = new JpaConformanceProviderDstu2(this, systemDao, myAppCtx.getBean(DaoConfig.class));
confProvider.setImplementationDescription(implDesc);
setServerConformanceProvider(confProvider);
break;
}
@ -127,7 +128,6 @@ public class TestRestfulServer extends RestfulServer {
systemDao = myAppCtx.getBean("mySystemDaoDstu3", IFhirSystemDao.class);
etagSupport = ETagSupportEnum.ENABLED;
JpaConformanceProviderDstu3 confProvider = new JpaConformanceProviderDstu3(this, systemDao, myAppCtx.getBean(DaoConfig.class), myAppCtx.getBean(ISearchParamRegistry.class));
confProvider.setImplementationDescription(implDesc);
setServerConformanceProvider(confProvider);
providers.add(myAppCtx.getBean(TerminologyUploaderProvider.class));
providers.add(myAppCtx.getBean(GraphQLProvider.class));
@ -147,7 +147,6 @@ public class TestRestfulServer extends RestfulServer {
etagSupport = ETagSupportEnum.ENABLED;
IValidationSupport validationSupport = myAppCtx.getBean(IValidationSupport.class);
JpaCapabilityStatementProvider confProvider = new JpaCapabilityStatementProvider(this, systemDao, myAppCtx.getBean(DaoConfig.class), myAppCtx.getBean(ISearchParamRegistry.class), validationSupport);
confProvider.setImplementationDescription(implDesc);
setServerConformanceProvider(confProvider);
providers.add(myAppCtx.getBean(TerminologyUploaderProvider.class));
providers.add(myAppCtx.getBean(GraphQLProvider.class));
@ -271,6 +270,10 @@ public class TestRestfulServer extends RestfulServer {
*/
registerProvider(myAppCtx.getBean(DiffProvider.class));
/*
* OpenAPI
*/
registerInterceptor(new OpenApiInterceptor());
}
/**

View File

@ -109,7 +109,7 @@ public class TestDstu2Config extends BaseJavaConfigDstu2 {
retVal.setUsername(myDbUsername);
retVal.setPassword(myDbPassword);
retVal.setDefaultQueryTimeout(20);
retVal.setMaxConnLifetimeMillis(5 * DateUtils.MILLIS_PER_MINUTE);
retVal.setTestOnBorrow(true);
DataSource dataSource = ProxyDataSourceBuilder
.create(retVal)

View File

@ -125,7 +125,7 @@ public class TestDstu3Config extends BaseJavaConfigDstu3 {
retVal.setUsername(myDbUsername);
retVal.setPassword(myDbPassword);
retVal.setDefaultQueryTimeout(20);
retVal.setMaxConnLifetimeMillis(5 * DateUtils.MILLIS_PER_MINUTE);
retVal.setTestOnBorrow(true);
DataSource dataSource = ProxyDataSourceBuilder
.create(retVal)

View File

@ -109,7 +109,7 @@ public class TestR4Config extends BaseJavaConfigR4 {
retVal.setUsername(myDbUsername);
retVal.setPassword(myDbPassword);
retVal.setDefaultQueryTimeout(20);
retVal.setMaxConnLifetimeMillis(5 * DateUtils.MILLIS_PER_MINUTE);
retVal.setTestOnBorrow(true);
DataSource dataSource = ProxyDataSourceBuilder
.create(retVal)

View File

@ -109,7 +109,7 @@ public class TestR5Config extends BaseJavaConfigR5 {
retVal.setUsername(myDbUsername);
retVal.setPassword(myDbPassword);
retVal.setDefaultQueryTimeout(20);
retVal.setMaxConnLifetimeMillis(5 * DateUtils.MILLIS_PER_MINUTE);
retVal.setTestOnBorrow(true);
DataSource dataSource = ProxyDataSourceBuilder
.create(retVal)

View File

@ -31,10 +31,6 @@
<param-name>contextConfigLocation</param-name>
<param-value>
ca.uhn.fhirtest.config.FhirTesterConfig
<!--
ca.uhn.fhir.jpa.config.WebsocketDstu2Config
ca.uhn.fhir.jpa.config.dstu3.WebsocketDstu3Config
-->
</param-value>
</init-param>
<load-on-startup>2</load-on-startup>
@ -43,10 +39,6 @@
<servlet>
<servlet-name>fhirServletR5</servlet-name>
<servlet-class>ca.uhn.fhirtest.TestRestfulServer</servlet-class>
<init-param>
<param-name>ImplementationDescription</param-name>
<param-value>UHN Test Server (R5 Resources)</param-value>
</init-param>
<init-param>
<param-name>FhirVersion</param-name>
<param-value>R5</param-value>
@ -57,10 +49,6 @@
<servlet>
<servlet-name>fhirServletR4</servlet-name>
<servlet-class>ca.uhn.fhirtest.TestRestfulServer</servlet-class>
<init-param>
<param-name>ImplementationDescription</param-name>
<param-value>UHN Test Server (R4 Resources)</param-value>
</init-param>
<init-param>
<param-name>FhirVersion</param-name>
<param-value>R4</param-value>
@ -71,10 +59,6 @@
<servlet>
<servlet-name>fhirServletDstu2</servlet-name>
<servlet-class>ca.uhn.fhirtest.TestRestfulServer</servlet-class>
<init-param>
<param-name>ImplementationDescription</param-name>
<param-value>UHN Test Server (DSTU2 Resources)</param-value>
</init-param>
<init-param>
<param-name>FhirVersion</param-name>
<param-value>DSTU2</param-value>
@ -85,10 +69,6 @@
<servlet>
<servlet-name>fhirServletDstu3</servlet-name>
<servlet-class>ca.uhn.fhirtest.TestRestfulServer</servlet-class>
<init-param>
<param-name>ImplementationDescription</param-name>
<param-value>UHN Test Server (STU3 Resources)</param-value>
</init-param>
<init-param>
<param-name>FhirVersion</param-name>
<param-value>DSTU3</param-value>
@ -96,35 +76,6 @@
<load-on-startup>1</load-on-startup>
</servlet>
<!--
<servlet>
<servlet-name>fhirServletTdl2</servlet-name>
<servlet-class>ca.uhn.fhirtest.TestRestfulServer</servlet-class>
<init-param>
<param-name>ImplementationDescription</param-name>
<param-value>Test Data Library (DSTU2 Resources)</param-value>
</init-param>
<init-param>
<param-name>FhirVersion</param-name>
<param-value>TDL2</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet>
<servlet-name>fhirServletTdl3</servlet-name>
<servlet-class>ca.uhn.fhirtest.TestRestfulServer</servlet-class>
<init-param>
<param-name>ImplementationDescription</param-name>
<param-value>FHIR Test Data Library (STU3 Resources)</param-value>
</init-param>
<init-param>
<param-name>FhirVersion</param-name>
<param-value>TDL3</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
-->
<servlet-mapping>
<servlet-name>fhirServletR5</servlet-name>
<url-pattern>/baseR5/*</url-pattern>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE8-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>hapi-fhir-server-openapi</artifactId>
<dependencies>
<!-- HAPI FHIR -->
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-server</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-r4</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-converter</artifactId>
<version>${project.version}</version>
</dependency>
<!-- OpenAPI/Swagger -->
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-models</artifactId>
<version>2.1.7</version>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-core</artifactId>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>swagger-ui</artifactId>
</dependency>
<!-- Thymeleaf -->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>com.vladsch.flexmark</groupId>
<artifactId>flexmark</artifactId>
</dependency>
<!-- Unit Test Deps-->
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-test-utilities</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.sourceforge.htmlunit</groupId>
<artifactId>htmlunit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,903 @@
package ca.uhn.fhir.rest.openapi;
/*-
* #%L
* hapi-fhir-server-openapi
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* 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 ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.server.IServerAddressStrategy;
import ca.uhn.fhir.rest.server.IServerConformanceProvider;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.RestfulServerUtils;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.ClasspathUtil;
import ca.uhn.fhir.util.ExtensionConstants;
import ca.uhn.fhir.util.HapiExtensions;
import ca.uhn.fhir.util.UrlUtil;
import com.vladsch.flexmark.html.HtmlRenderer;
import com.vladsch.flexmark.parser.Parser;
import io.swagger.v3.core.util.Yaml;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.Paths;
import io.swagger.v3.oas.models.examples.Example;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.media.ObjectSchema;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.parameters.RequestBody;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.responses.ApiResponses;
import io.swagger.v3.oas.models.servers.Server;
import io.swagger.v3.oas.models.tags.Tag;
import org.apache.commons.io.IOUtils;
import org.hl7.fhir.convertors.VersionConvertor_30_40;
import org.hl7.fhir.convertors.VersionConvertor_40_50;
import org.hl7.fhir.instance.model.api.IBaseConformance;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.CapabilityStatement;
import org.hl7.fhir.r4.model.CodeableConcept;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.DateType;
import org.hl7.fhir.r4.model.Extension;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.OperationDefinition;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.StringType;
import org.hl7.fhir.r4.model.Type;
import org.thymeleaf.IEngineConfiguration;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.cache.AlwaysValidCacheEntryValidity;
import org.thymeleaf.cache.ICacheEntryValidity;
import org.thymeleaf.cache.NonCacheableCacheEntryValidity;
import org.thymeleaf.context.IExpressionContext;
import org.thymeleaf.context.WebContext;
import org.thymeleaf.linkbuilder.AbstractLinkBuilder;
import org.thymeleaf.standard.StandardDialect;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ITemplateResolver;
import org.thymeleaf.templateresolver.TemplateResolution;
import org.thymeleaf.templateresource.ClassLoaderTemplateResource;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class OpenApiInterceptor {
public static final String FHIR_JSON_RESOURCE = "FHIR-JSON-RESOURCE";
public static final String FHIR_XML_RESOURCE = "FHIR-XML-RESOURCE";
public static final String PAGE_SYSTEM = "System Level Operations";
public static final String PAGE_ALL = "All";
public static final FhirContext FHIR_CONTEXT_CANONICAL = FhirContext.forR4();
public static final String REQUEST_DETAILS = "REQUEST_DETAILS";
public static final String RACCOON_PNG = "raccoon.png";
private final String mySwaggerUiVersion;
private final TemplateEngine myTemplateEngine;
private final Parser myFlexmarkParser;
private final HtmlRenderer myFlexmarkRenderer;
private final Map<String, String> myResourcePathToClasspath = new HashMap<>();
private final Map<String, String> myExtensionToContentType = new HashMap<>();
private String myBannerImage;
/**
* Constructor
*/
public OpenApiInterceptor() {
mySwaggerUiVersion = initSwaggerUiWebJar();
myTemplateEngine = new TemplateEngine();
ITemplateResolver resolver = new SwaggerUiTemplateResolver();
myTemplateEngine.setTemplateResolver(resolver);
StandardDialect dialect = new StandardDialect();
myTemplateEngine.setDialect(dialect);
myTemplateEngine.setLinkBuilder(new TemplateLinkBuilder());
myFlexmarkParser = Parser.builder().build();
myFlexmarkRenderer = HtmlRenderer.builder().build();
initResources();
}
private void initResources() {
setBannerImage(RACCOON_PNG);
addResourcePathToClasspath("/swagger-ui/index.html", "/ca/uhn/fhir/rest/openapi/index.html");
addResourcePathToClasspath("/swagger-ui/" + RACCOON_PNG, "/ca/uhn/fhir/rest/openapi/raccoon.png");
addResourcePathToClasspath("/swagger-ui/index.css", "/ca/uhn/fhir/rest/openapi/index.css");
myExtensionToContentType.put(".png", "image/png");
myExtensionToContentType.put(".css", "text/css; charset=UTF-8");
}
protected void addResourcePathToClasspath(String thePath, String theClasspath) {
myResourcePathToClasspath.put(thePath, theClasspath);
}
private String initSwaggerUiWebJar() {
final String mySwaggerUiVersion;
Properties props = new Properties();
String resourceName = "/META-INF/maven/org.webjars/swagger-ui/pom.properties";
try {
InputStream resourceAsStream = ClasspathUtil.loadResourceAsStream(resourceName);
props.load(resourceAsStream);
} catch (IOException e) {
throw new ConfigurationException("Failed to load resource: " + resourceName);
}
mySwaggerUiVersion = props.getProperty("version");
return mySwaggerUiVersion;
}
@Hook(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLER_SELECTED)
public boolean serveSwaggerUi(HttpServletRequest theRequest, HttpServletResponse theResponse, ServletRequestDetails theRequestDetails) throws IOException {
String requestPath = theRequest.getPathInfo();
if (isBlank(requestPath) || requestPath.equals("/")) {
Set<String> highestRankedAcceptValues = RestfulServerUtils.parseAcceptHeaderAndReturnHighestRankedOptions(theRequest);
if (highestRankedAcceptValues.contains(Constants.CT_HTML)) {
theResponse.sendRedirect("./swagger-ui/");
return false;
}
}
if (requestPath.startsWith("/swagger-ui/")) {
return !handleResourceRequest(theResponse, theRequestDetails, requestPath);
} else if (requestPath.equals("/api-docs")) {
OpenAPI openApi = generateOpenApi(theRequestDetails);
String response = Yaml.pretty(openApi);
theResponse.setContentType("text/yaml");
theResponse.setStatus(200);
theResponse.getWriter().write(response);
theResponse.getWriter().close();
return false;
}
return true;
}
protected boolean handleResourceRequest(HttpServletResponse theResponse, ServletRequestDetails theRequestDetails, String requestPath) throws IOException {
if (requestPath.equals("/swagger-ui/") || requestPath.equals("/swagger-ui/index.html")) {
serveSwaggerUiHtml(theRequestDetails, theResponse);
return true;
}
String resourceClasspath = myResourcePathToClasspath.get(requestPath);
if (resourceClasspath != null) {
theResponse.setStatus(200);
String extension = requestPath.substring(requestPath.lastIndexOf('.'));
String contentType = myExtensionToContentType.get(extension);
assert contentType != null;
theResponse.setContentType(contentType);
try (InputStream resource = ClasspathUtil.loadResourceAsStream(resourceClasspath)) {
IOUtils.copy(resource, theResponse.getOutputStream());
theResponse.getOutputStream().close();
}
return true;
}
String resourcePath = requestPath.substring("/swagger-ui/".length());
try (InputStream resource = ClasspathUtil.loadResourceAsStream("/META-INF/resources/webjars/swagger-ui/" + mySwaggerUiVersion + "/" + resourcePath)) {
if (resourcePath.endsWith(".js") || resourcePath.endsWith(".map")) {
theResponse.setContentType("application/javascript");
theResponse.setStatus(200);
IOUtils.copy(resource, theResponse.getOutputStream());
theResponse.getOutputStream().close();
return true;
}
if (resourcePath.endsWith(".css")) {
theResponse.setContentType("text/css");
theResponse.setStatus(200);
IOUtils.copy(resource, theResponse.getOutputStream());
theResponse.getOutputStream().close();
return true;
}
}
return false;
}
@SuppressWarnings("unchecked")
private void serveSwaggerUiHtml(ServletRequestDetails theRequestDetails, HttpServletResponse theResponse) throws IOException {
CapabilityStatement cs = getCapabilityStatement(theRequestDetails);
theResponse.setStatus(200);
theResponse.setContentType(Constants.CT_HTML);
HttpServletRequest servletRequest = theRequestDetails.getServletRequest();
ServletContext servletContext = servletRequest.getServletContext();
WebContext context = new WebContext(servletRequest, theResponse, servletContext);
context.setVariable(REQUEST_DETAILS, theRequestDetails);
context.setVariable("DESCRIPTION", cs.getImplementation().getDescription());
context.setVariable("SERVER_NAME", cs.getSoftware().getName());
context.setVariable("SERVER_VERSION", cs.getSoftware().getVersion());
context.setVariable("BASE_URL", cs.getImplementation().getUrl());
context.setVariable("BANNER_IMAGE_URL", getBannerImage());
context.setVariable("OPENAPI_DOCS", cs.getImplementation().getUrl() + "/api-docs");
context.setVariable("FHIR_VERSION", cs.getFhirVersion().toCode());
context.setVariable("FHIR_VERSION_CODENAME", FhirVersionEnum.forVersionString(cs.getFhirVersion().toCode()).name());
String copyright = cs.getCopyright();
if (isNotBlank(copyright)) {
copyright = myFlexmarkRenderer.render(myFlexmarkParser.parse(copyright));
context.setVariable("COPYRIGHT_HTML", copyright);
}
List<String> pageNames = new ArrayList<>();
Map<String, Integer> resourceToCount = new HashMap<>();
cs.getRestFirstRep().getResource().stream().forEach(t -> {
String type = t.getType();
pageNames.add(type);
Extension countExtension = t.getExtensionByUrl(ExtensionConstants.CONF_RESOURCE_COUNT);
if (countExtension != null) {
IPrimitiveType<? extends Number> countExtensionValue = (IPrimitiveType<? extends Number>) countExtension.getValueAsPrimitive();
if (countExtensionValue != null && countExtensionValue.hasValue()) {
resourceToCount.put(type, countExtensionValue.getValue().intValue());
}
}
});
pageNames.sort((o1, o2) -> {
Integer count1 = resourceToCount.get(o1);
Integer count2 = resourceToCount.get(o2);
if (count1 != null && count2 != null) {
return count2 - count1;
}
if (count1 != null) {
return -1;
}
if (count2 != null) {
return 1;
}
return o1.compareTo(o2);
});
pageNames.add(0, PAGE_ALL);
pageNames.add(1, PAGE_SYSTEM);
context.setVariable("PAGE_NAMES", pageNames);
context.setVariable("PAGE_NAME_TO_COUNT", resourceToCount);
String page = extractPageName(theRequestDetails, PAGE_SYSTEM);
context.setVariable("PAGE", page);
String outcome = myTemplateEngine.process("index.html", context);
theResponse.getWriter().write(outcome);
theResponse.getWriter().close();
}
private String extractPageName(ServletRequestDetails theRequestDetails, String theDefault) {
String[] pageValues = theRequestDetails.getParameters().get("page");
String page = null;
if (pageValues != null && pageValues.length > 0) {
page = pageValues[0];
}
if (isBlank(page)) {
page = theDefault;
}
return page;
}
private OpenAPI generateOpenApi(ServletRequestDetails theRequestDetails) {
String page = extractPageName(theRequestDetails, null);
CapabilityStatement cs = getCapabilityStatement(theRequestDetails);
FhirContext ctx = theRequestDetails.getFhirContext();
IServerConformanceProvider<?> capabilitiesProvider = null;
RestfulServer restfulServer = theRequestDetails.getServer();
if (restfulServer.getServerConformanceProvider() instanceof IServerConformanceProvider) {
capabilitiesProvider = (IServerConformanceProvider<?>) restfulServer.getServerConformanceProvider();
}
OpenAPI openApi = new OpenAPI();
openApi.setInfo(new Info());
openApi.getInfo().setDescription(cs.getDescription());
openApi.getInfo().setTitle(cs.getSoftware().getName());
openApi.getInfo().setVersion(cs.getSoftware().getVersion());
openApi.getInfo().setContact(new Contact());
openApi.getInfo().getContact().setName(cs.getContactFirstRep().getName());
openApi.getInfo().getContact().setEmail(cs.getContactFirstRep().getTelecomFirstRep().getValue());
Server server = new Server();
openApi.addServersItem(server);
server.setUrl(cs.getImplementation().getUrl());
server.setDescription(cs.getSoftware().getName());
Paths paths = new Paths();
openApi.setPaths(paths);
if (page == null || page.equals(PAGE_SYSTEM) || page.equals(PAGE_ALL)) {
Tag serverTag = new Tag();
serverTag.setName(PAGE_SYSTEM);
serverTag.setDescription("Server-level operations");
openApi.addTagsItem(serverTag);
Operation capabilitiesOperation = getPathItem(paths, "/metadata", PathItem.HttpMethod.GET);
capabilitiesOperation.addTagsItem(PAGE_SYSTEM);
capabilitiesOperation.setSummary("server-capabilities: Fetch the server FHIR CapabilityStatement");
addFhirResourceResponse(ctx, openApi, capabilitiesOperation, "CapabilityStatement");
Set<CapabilityStatement.SystemRestfulInteraction> systemInteractions = cs.getRestFirstRep().getInteraction().stream().map(t -> t.getCode()).collect(Collectors.toSet());
// Transaction Operation
if (systemInteractions.contains(CapabilityStatement.SystemRestfulInteraction.TRANSACTION) || systemInteractions.contains(CapabilityStatement.SystemRestfulInteraction.BATCH)) {
Operation transaction = getPathItem(paths, "/", PathItem.HttpMethod.POST);
transaction.addTagsItem(PAGE_SYSTEM);
transaction.setSummary("server-transaction: Execute a FHIR Transaction (or FHIR Batch) Bundle");
addFhirResourceResponse(ctx, openApi, transaction, null);
addFhirResourceRequestBody(openApi, transaction, ctx, null);
}
// System History Operation
if (systemInteractions.contains(CapabilityStatement.SystemRestfulInteraction.HISTORYSYSTEM)) {
Operation systemHistory = getPathItem(paths, "/_history", PathItem.HttpMethod.GET);
systemHistory.addTagsItem(PAGE_SYSTEM);
systemHistory.setSummary("server-history: Fetch the resource change history across all resource types on the server");
addFhirResourceResponse(ctx, openApi, systemHistory, null);
}
// System-level Operations
for (CapabilityStatement.CapabilityStatementRestResourceOperationComponent nextOperation : cs.getRestFirstRep().getOperation()) {
addFhirOperation(ctx, openApi, theRequestDetails, capabilitiesProvider, paths, null, nextOperation);
}
}
for (CapabilityStatement.CapabilityStatementRestResourceComponent nextResource : cs.getRestFirstRep().getResource()) {
String resourceType = nextResource.getType();
if (page != null && !page.equals(resourceType) && !page.equals(PAGE_ALL)) {
continue;
}
Set<CapabilityStatement.TypeRestfulInteraction> typeRestfulInteractions = nextResource.getInteraction().stream().map(t -> t.getCodeElement().getValue()).collect(Collectors.toSet());
Tag resourceTag = new Tag();
resourceTag.setName(resourceType);
resourceTag.setDescription("The " + resourceType + " FHIR resource type");
openApi.addTagsItem(resourceTag);
// Instance Read
if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.READ)) {
Operation operation = getPathItem(paths, "/" + resourceType + "/{id}", PathItem.HttpMethod.GET);
operation.addTagsItem(resourceType);
operation.setSummary("read-instance: Read " + resourceType + " instance");
addResourceIdParameter(operation);
addFhirResourceResponse(ctx, openApi, operation, null);
}
// Instance VRead
if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.VREAD)) {
Operation operation = getPathItem(paths, "/" + resourceType + "/{id}/_history/{version_id}", PathItem.HttpMethod.GET);
operation.addTagsItem(resourceType);
operation.setSummary("vread-instance: Read " + resourceType + " instance with specific version");
addResourceIdParameter(operation);
addResourceVersionIdParameter(operation);
addFhirResourceResponse(ctx, openApi, operation, null);
}
// Type Create
if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.CREATE)) {
Operation operation = getPathItem(paths, "/" + resourceType, PathItem.HttpMethod.POST);
operation.addTagsItem(resourceType);
operation.setSummary("create-type: Create a new " + resourceType + " instance");
addFhirResourceRequestBody(openApi, operation, ctx, genericExampleSupplier(ctx, resourceType));
addFhirResourceResponse(ctx, openApi, operation, null);
}
// Instance Update
if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.UPDATE)) {
Operation operation = getPathItem(paths, "/" + resourceType + "/{id}", PathItem.HttpMethod.PUT);
operation.addTagsItem(resourceType);
operation.setSummary("update-instance: Update an existing " + resourceType + " instance, or create using a client-assigned ID");
addResourceIdParameter(operation);
addFhirResourceRequestBody(openApi, operation, ctx, genericExampleSupplier(ctx, resourceType));
addFhirResourceResponse(ctx, openApi, operation, null);
}
// Type history
if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.HISTORYTYPE)) {
Operation operation = getPathItem(paths, "/" + resourceType + "/_history", PathItem.HttpMethod.GET);
operation.addTagsItem(resourceType);
operation.setSummary("type-history: Fetch the resource change history for all resources of type " + resourceType);
addFhirResourceResponse(ctx, openApi, operation, null);
}
// Instance history
if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.HISTORYTYPE)) {
Operation operation = getPathItem(paths, "/" + resourceType + "/{id}/_history", PathItem.HttpMethod.GET);
operation.addTagsItem(resourceType);
operation.setSummary("instance-history: Fetch the resource change history for all resources of type " + resourceType);
addResourceIdParameter(operation);
addFhirResourceResponse(ctx, openApi, operation, null);
}
// Instance Patch
if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.PATCH)) {
Operation operation = getPathItem(paths, "/" + resourceType + "/{id}", PathItem.HttpMethod.PATCH);
operation.addTagsItem(resourceType);
operation.setSummary("instance-patch: Patch a resource instance of type " + resourceType + " by ID");
addResourceIdParameter(operation);
addFhirResourceRequestBody(openApi, operation, FHIR_CONTEXT_CANONICAL, patchExampleSupplier());
addFhirResourceResponse(ctx, openApi, operation, null);
}
// Instance Delete
if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.DELETE)) {
Operation operation = getPathItem(paths, "/" + resourceType + "/{id}", PathItem.HttpMethod.DELETE);
operation.addTagsItem(resourceType);
operation.setSummary("instance-delete: Perform a logical delete on a resource instance");
addResourceIdParameter(operation);
addFhirResourceResponse(ctx, openApi, operation, null);
}
// Search
if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.SEARCHTYPE)) {
Operation operation = getPathItem(paths, "/" + resourceType, PathItem.HttpMethod.GET);
operation.addTagsItem(resourceType);
operation.setDescription("This is a search type");
operation.setSummary("search-type: Update an existing " + resourceType + " instance, or create using a client-assigned ID");
addFhirResourceResponse(ctx, openApi, operation, null);
for (CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent nextSearchParam : nextResource.getSearchParam()) {
Parameter parametersItem = new Parameter();
operation.addParametersItem(parametersItem);
parametersItem.setName(nextSearchParam.getName());
parametersItem.setIn("query");
parametersItem.setDescription(nextSearchParam.getDocumentation());
parametersItem.setStyle(Parameter.StyleEnum.SIMPLE);
}
}
// Resource-level Operations
for (CapabilityStatement.CapabilityStatementRestResourceOperationComponent nextOperation : nextResource.getOperation()) {
addFhirOperation(ctx, openApi, theRequestDetails, capabilitiesProvider, paths, resourceType, nextOperation);
}
}
return openApi;
}
private Supplier<IBaseResource> patchExampleSupplier() {
return () -> {
Parameters example = new Parameters();
Parameters.ParametersParameterComponent operation = example
.addParameter()
.setName("operation");
operation.addPart().setName("type").setValue(new StringType("add"));
operation.addPart().setName("path").setValue(new StringType("Patient"));
operation.addPart().setName("name").setValue(new StringType("birthDate"));
operation.addPart().setName("value").setValue(new DateType("1930-01-01"));
return example;
};
}
private void addSchemaFhirResource(OpenAPI theOpenApi) {
ensureComponentsSchemasPopulated(theOpenApi);
if (!theOpenApi.getComponents().getSchemas().containsKey(FHIR_JSON_RESOURCE)) {
ObjectSchema fhirJsonSchema = new ObjectSchema();
fhirJsonSchema.setDescription("A FHIR resource");
theOpenApi.getComponents().addSchemas(FHIR_JSON_RESOURCE, fhirJsonSchema);
}
if (!theOpenApi.getComponents().getSchemas().containsKey(FHIR_XML_RESOURCE)) {
ObjectSchema fhirXmlSchema = new ObjectSchema();
fhirXmlSchema.setDescription("A FHIR resource");
theOpenApi.getComponents().addSchemas(FHIR_XML_RESOURCE, fhirXmlSchema);
}
}
private void ensureComponentsSchemasPopulated(OpenAPI theOpenApi) {
if (theOpenApi.getComponents() == null) {
theOpenApi.setComponents(new Components());
}
if (theOpenApi.getComponents().getSchemas() == null) {
theOpenApi.getComponents().setSchemas(new LinkedHashMap<>());
}
}
private CapabilityStatement getCapabilityStatement(ServletRequestDetails theRequestDetails) {
RestfulServer restfulServer = theRequestDetails.getServer();
IBaseConformance versionIndependentCapabilityStatement = restfulServer.getCapabilityStatement(theRequestDetails);
return toCanonicalVersion(versionIndependentCapabilityStatement);
}
private void addFhirOperation(FhirContext theFhirContext, OpenAPI theOpenApi, ServletRequestDetails theRequestDetails, IServerConformanceProvider<?> theCapabilitiesProvider, Paths thePaths, String theResourceType, CapabilityStatement.CapabilityStatementRestResourceOperationComponent theOperation) {
if (theCapabilitiesProvider != null) {
IdType definitionId = new IdType(theOperation.getDefinition());
IBaseResource operationDefinitionNonCanonical = theCapabilitiesProvider.readOperationDefinition(definitionId, theRequestDetails);
if (operationDefinitionNonCanonical == null) {
return;
}
OperationDefinition operationDefinition = toCanonicalVersion(operationDefinitionNonCanonical);
if (!operationDefinition.getAffectsState()) {
// GET form for non-state-affecting operations
if (theResourceType != null) {
if (operationDefinition.getType()) {
Operation operation = getPathItem(thePaths, "/" + theResourceType + "/$" + operationDefinition.getCode(), PathItem.HttpMethod.GET);
populateOperation(theFhirContext, theOpenApi, theResourceType, operationDefinition, operation, true);
}
if (operationDefinition.getInstance()) {
Operation operation = getPathItem(thePaths, "/" + theResourceType + "/{id}/$" + operationDefinition.getCode(), PathItem.HttpMethod.GET);
addResourceIdParameter(operation);
populateOperation(theFhirContext, theOpenApi, theResourceType, operationDefinition, operation, true);
}
} else {
if (operationDefinition.getSystem()) {
Operation operation = getPathItem(thePaths, "/$" + operationDefinition.getCode(), PathItem.HttpMethod.GET);
populateOperation(theFhirContext, theOpenApi, null, operationDefinition, operation, true);
}
}
} else {
// POST form for all operations
if (theResourceType != null) {
if (operationDefinition.getType()) {
Operation operation = getPathItem(thePaths, "/" + theResourceType + "/$" + operationDefinition.getCode(), PathItem.HttpMethod.POST);
populateOperation(theFhirContext, theOpenApi, theResourceType, operationDefinition, operation, false);
}
if (operationDefinition.getInstance()) {
Operation operation = getPathItem(thePaths, "/" + theResourceType + "/{id}/$" + operationDefinition.getCode(), PathItem.HttpMethod.POST);
addResourceIdParameter(operation);
populateOperation(theFhirContext, theOpenApi, theResourceType, operationDefinition, operation, false);
}
} else {
if (operationDefinition.getSystem()) {
Operation operation = getPathItem(thePaths, "/$" + operationDefinition.getCode(), PathItem.HttpMethod.POST);
populateOperation(theFhirContext, theOpenApi, null, operationDefinition, operation, false);
}
}
}
}
}
private void populateOperation(FhirContext theFhirContext, OpenAPI theOpenApi, String theResourceType, OperationDefinition theOperationDefinition, Operation theOperation, boolean theGet) {
if (theResourceType == null) {
theOperation.addTagsItem(PAGE_SYSTEM);
} else {
theOperation.addTagsItem(theResourceType);
}
theOperation.setSummary(theOperationDefinition.getTitle());
theOperation.setDescription(theOperationDefinition.getDescription());
addFhirResourceResponse(theFhirContext, theOpenApi, theOperation, null);
if (theGet) {
for (OperationDefinition.OperationDefinitionParameterComponent nextParameter : theOperationDefinition.getParameter()) {
Parameter parametersItem = new Parameter();
theOperation.addParametersItem(parametersItem);
parametersItem.setName(nextParameter.getName());
parametersItem.setIn("query");
parametersItem.setDescription(nextParameter.getDocumentation());
parametersItem.setStyle(Parameter.StyleEnum.SIMPLE);
parametersItem.setRequired(nextParameter.getMin() > 0);
List<Extension> exampleExtensions = nextParameter.getExtensionsByUrl(HapiExtensions.EXT_OP_PARAMETER_EXAMPLE_VALUE);
if (exampleExtensions.size() == 1) {
parametersItem.setExample(exampleExtensions.get(0).getValueAsPrimitive().getValueAsString());
} else if (exampleExtensions.size() > 1) {
for (Extension next : exampleExtensions) {
String nextExample = next.getValueAsPrimitive().getValueAsString();
parametersItem.addExample(nextExample, new Example().value(nextExample));
}
}
}
} else {
Parameters exampleRequestBody = new Parameters();
for (OperationDefinition.OperationDefinitionParameterComponent nextSearchParam : theOperationDefinition.getParameter()) {
Parameters.ParametersParameterComponent param = exampleRequestBody.addParameter();
param.setName(nextSearchParam.getName());
String paramType = nextSearchParam.getType();
switch (defaultString(paramType)) {
case "uri":
case "url":
case "code":
case "string": {
IPrimitiveType<?> type = (IPrimitiveType<?>) FHIR_CONTEXT_CANONICAL.getElementDefinition(paramType).newInstance();
type.setValueAsString("example");
param.setValue((Type) type);
break;
}
case "integer": {
IPrimitiveType<?> type = (IPrimitiveType<?>) FHIR_CONTEXT_CANONICAL.getElementDefinition(paramType).newInstance();
type.setValueAsString("0");
param.setValue((Type) type);
break;
}
case "boolean": {
IPrimitiveType<?> type = (IPrimitiveType<?>) FHIR_CONTEXT_CANONICAL.getElementDefinition(paramType).newInstance();
type.setValueAsString("false");
param.setValue((Type) type);
break;
}
case "CodeableConcept": {
CodeableConcept type = new CodeableConcept();
type.getCodingFirstRep().setSystem("http://example.com");
type.getCodingFirstRep().setCode("1234");
param.setValue(type);
break;
}
case "Coding": {
Coding type = new Coding();
type.setSystem("http://example.com");
type.setCode("1234");
param.setValue(type);
break;
}
case "Reference":
Reference reference = new Reference("example");
param.setValue(reference);
break;
case "Resource":
if (theResourceType != null) {
IBaseResource resource = FHIR_CONTEXT_CANONICAL.getResourceDefinition(theResourceType).newInstance();
resource.setId("1");
param.setResource((Resource) resource);
}
break;
}
}
String exampleRequestBodyString = FHIR_CONTEXT_CANONICAL.newJsonParser().setPrettyPrint(true).encodeResourceToString(exampleRequestBody);
theOperation.setRequestBody(new RequestBody());
theOperation.getRequestBody().setContent(new Content());
MediaType mediaType = new MediaType();
mediaType.setExample(exampleRequestBodyString);
mediaType.setSchema(new Schema().type("object").title("FHIR Resource"));
theOperation.getRequestBody().getContent().addMediaType(Constants.CT_FHIR_JSON_NEW, mediaType);
}
}
private Operation getPathItem(Paths thePaths, String thePath, PathItem.HttpMethod theMethod) {
PathItem pathItem;
if (thePaths.containsKey(thePath)) {
pathItem = thePaths.get(thePath);
} else {
pathItem = new PathItem();
thePaths.addPathItem(thePath, pathItem);
}
switch (theMethod) {
case POST:
assert pathItem.getPost() == null : "Have duplicate POST at path: " + thePath;
return pathItem.post(new Operation()).getPost();
case GET:
assert pathItem.getGet() == null : "Have duplicate GET at path: " + thePath;
return pathItem.get(new Operation()).getGet();
case PUT:
assert pathItem.getPut() == null;
return pathItem.put(new Operation()).getPut();
case PATCH:
assert pathItem.getPatch() == null;
return pathItem.patch(new Operation()).getPatch();
case DELETE:
assert pathItem.getDelete() == null;
return pathItem.delete(new Operation()).getDelete();
case HEAD:
case OPTIONS:
case TRACE:
default:
throw new IllegalStateException();
}
}
private void addFhirResourceRequestBody(OpenAPI theOpenApi, Operation theOperation, FhirContext theExampleFhirContext, Supplier<IBaseResource> theExampleSupplier) {
RequestBody requestBody = new RequestBody();
requestBody.setContent(provideContentFhirResource(theOpenApi, theExampleFhirContext, theExampleSupplier));
theOperation.setRequestBody(requestBody);
}
private void addResourceVersionIdParameter(Operation theOperation) {
Parameter parameter = new Parameter();
parameter.setName("version_id");
parameter.setIn("path");
parameter.setDescription("The resource version ID");
parameter.setExample("1");
parameter.setSchema(new Schema().type("string").minimum(new BigDecimal(1)));
parameter.setStyle(Parameter.StyleEnum.SIMPLE);
theOperation.addParametersItem(parameter);
}
private void addFhirResourceResponse(FhirContext theFhirContext, OpenAPI theOpenApi, Operation theOperation, String theResourceType) {
theOperation.setResponses(new ApiResponses());
ApiResponse response200 = new ApiResponse();
response200.setDescription("Success");
response200.setContent(provideContentFhirResource(theOpenApi, theFhirContext, genericExampleSupplier(theFhirContext, theResourceType)));
theOperation.getResponses().addApiResponse("200", response200);
}
private Supplier<IBaseResource> genericExampleSupplier(FhirContext theFhirContext, String theResourceType) {
if (theResourceType == null) {
return null;
}
return () -> {
IBaseResource example = null;
if (theResourceType != null) {
example = theFhirContext.getResourceDefinition(theResourceType).newInstance();
}
return example;
};
}
private Content provideContentFhirResource(OpenAPI theOpenApi, FhirContext theExampleFhirContext, Supplier<IBaseResource> theExampleSupplier) {
addSchemaFhirResource(theOpenApi);
Content retVal = new Content();
MediaType jsonSchema = new MediaType().schema(new ObjectSchema().$ref("#/components/schemas/" + FHIR_JSON_RESOURCE));
if (theExampleSupplier != null) {
jsonSchema.setExample(theExampleFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(theExampleSupplier.get()));
}
retVal.addMediaType(Constants.CT_FHIR_JSON_NEW, jsonSchema);
MediaType xmlSchema = new MediaType().schema(new ObjectSchema().$ref("#/components/schemas/" + FHIR_XML_RESOURCE));
if (theExampleSupplier != null) {
xmlSchema.setExample(theExampleFhirContext.newXmlParser().setPrettyPrint(true).encodeResourceToString(theExampleSupplier.get()));
}
retVal.addMediaType(Constants.CT_FHIR_XML_NEW, xmlSchema);
return retVal;
}
private void addResourceIdParameter(Operation theOperation) {
Parameter parameter = new Parameter();
parameter.setName("id");
parameter.setIn("path");
parameter.setDescription("The resource ID");
parameter.setExample("123");
parameter.setSchema(new Schema().type("string").minimum(new BigDecimal(1)));
parameter.setStyle(Parameter.StyleEnum.SIMPLE);
theOperation.addParametersItem(parameter);
}
protected ClassLoaderTemplateResource getIndexTemplate() {
return new ClassLoaderTemplateResource(myResourcePathToClasspath.get("/swagger-ui/index.html"), StandardCharsets.UTF_8.name());
}
public void setBannerImage(String theBannerImage) {
myBannerImage = theBannerImage;
}
public String getBannerImage() {
return myBannerImage;
}
private class SwaggerUiTemplateResolver implements ITemplateResolver {
@Override
public String getName() {
return getClass().getName();
}
@Override
public Integer getOrder() {
return 0;
}
@Override
public TemplateResolution resolveTemplate(IEngineConfiguration configuration, String ownerTemplate, String template, Map<String, Object> templateResolutionAttributes) {
ClassLoaderTemplateResource resource = getIndexTemplate();
ICacheEntryValidity cacheValidity = new AlwaysValidCacheEntryValidity();
return new TemplateResolution(resource, TemplateMode.HTML, cacheValidity);
}
}
private static class TemplateLinkBuilder extends AbstractLinkBuilder {
@Override
public String buildLink(IExpressionContext theExpressionContext, String theBase, Map<String, Object> theParameters) {
ServletRequestDetails requestDetails = (ServletRequestDetails) theExpressionContext.getVariable(REQUEST_DETAILS);
IServerAddressStrategy addressStrategy = requestDetails.getServer().getServerAddressStrategy();
String baseUrl = addressStrategy.determineServerBase(requestDetails.getServletRequest().getServletContext(), requestDetails.getServletRequest());
StringBuilder builder = new StringBuilder();
builder.append(baseUrl);
builder.append(theBase);
if (!theParameters.isEmpty()) {
builder.append("?");
for (Iterator<Map.Entry<String, Object>> iter = theParameters.entrySet().iterator(); iter.hasNext(); ) {
Map.Entry<String, Object> nextEntry = iter.next();
builder.append(UrlUtil.escapeUrlParam(nextEntry.getKey()));
builder.append("=");
builder.append(UrlUtil.escapeUrlParam(defaultIfNull(nextEntry.getValue(), "").toString()));
if (iter.hasNext()) {
builder.append("&");
}
}
}
return builder.toString();
}
}
@SuppressWarnings("unchecked")
private static <T extends Resource> T toCanonicalVersion(IBaseResource theNonCanonical) {
IBaseResource canonical;
if (theNonCanonical instanceof org.hl7.fhir.dstu3.model.Resource) {
canonical = VersionConvertor_30_40.convertResource((org.hl7.fhir.dstu3.model.Resource) theNonCanonical, true);
} else if (theNonCanonical instanceof org.hl7.fhir.r5.model.Resource) {
canonical = VersionConvertor_40_50.convertResource((org.hl7.fhir.r5.model.Resource) theNonCanonical);
} else {
canonical = theNonCanonical;
}
return (T) canonical;
}
}

View File

@ -0,0 +1,141 @@
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after
{
box-sizing: inherit;
}
body
{
margin:0;
background: #fafafa;
}
.scheme-container, .information-container
{
display: none
}
.banner {
padding-top: 20px;
padding-left: 25px;
display: flex;
flex-direction: row;
background-color: #AAA;
border-bottom: 2px solid #888;
}
.banner H1 {
font-family: sans-serif;
height: 150%;
position: relative;
line-height: 120%;
top: 6px;
left: 18px;
}
.banner .version {
font-size: 30%;
position: relative;
top: -11px;
background-color: #888;
color: #FFF;
padding: 5px;
border-radius: 15px;
}
.bannerCopyright {
font-family: sans-serif;
padding-top: 20px;
padding-left: 30px;
padding-right: 30px;
padding-bottom: 20px;
background-color: #CCC;
border-bottom: 2px solid #888;
width: 100%;
font-size: 0.9em;
}
.banner2 {
font-family: sans-serif;
padding-top: 30px;
padding-left: 30px;
padding-right: 30px;
padding-bottom: 20px;
display: table;
flex-direction: row;
background-color: #CCC;
border-bottom: 2px solid #888;
width: 100%;
}
.banner2 > DIV {
display: table-row;
}
.banner2 > DIV > DIV {
display: table-cell;
padding-bottom: 10px;
}
.banner2_key {
min-width: 100px;
white-space: nowrap;
font-weight: bold;
}
.banner2_value {
padding-left: 20px;
width: 100%;
}
.banner3 {
font-family: sans-serif;
padding-top: 20px;
padding-left: 30px;
padding-right: 30px;
padding-bottom: 20px;
background-color: #EEE;
border-bottom: 2px solid #888;
width: 100%;
}
.pageButtons {
display: flex;
flex-wrap: wrap;
}
.pageButton {
background-color: #FFF;
color: #666;
padding: 10px;
margin: 5px;
border-radius: 8px;
text-decoration: none;
line-height: 0.5em;
border: 1px solid #FFF;
}
.pageButton:HOVER {
border: 1px solid #888;
}
.pageButtonSelected {
background-color: #888;
color: #FFF;
}
.resourceCountBadge {
font-size: 0.8em;
background: #DDD;
padding: 4px;
border-radius: 6px;
color: #000;
}

View File

@ -0,0 +1,66 @@
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Swagger UI</title>
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" />
<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
<link rel="stylesheet" type="text/css" href="./index.css" />
</head>
<body>
<div class="container">
</div>
<div class="banner">
<img th:src="${BANNER_IMAGE_URL}" style="height: 100px;"/>
<h1>[[${DESCRIPTION}]]<br/><span class="version" th:text="${SERVER_NAME} + ' ' + ${SERVER_VERSION}"></span></h1>
</div>
<div class="banner2">
<div>
<div class="banner2_key">FHIR Server Base URL</div><div class="banner2_value"><a th:href="${BASE_URL}" th:text="${BASE_URL}"></a></div>
</div>
<div>
<div class="banner2_key">OpenAPI Docs</div><div class="banner2_value"><a th:href="${OPENAPI_DOCS}" th:text="${OPENAPI_DOCS}"></a></div>
</div>
<div>
<div class="banner2_key">FHIR Version</div><div class="banner2_value">[[${FHIR_VERSION}]] ([[${FHIR_VERSION_CODENAME}]])</div>
</div>
</div>
<div class="bannerCopyright" th:if="${COPYRIGHT_HTML} != null" th:utext="${COPYRIGHT_HTML}">
</div>
<div class="banner3">
<div class="pageButtons" id="pageButtons">
<a class="pageButton" th:each="pageName : ${PAGE_NAMES}" th:classappend="${pageName} == ${PAGE} ? 'pageButtonSelected' : ''" th:href="@{/swagger-ui/(page=${pageName})}">
[[${pageName}]]
<span th:if="${PAGE_NAME_TO_COUNT.containsKey(pageName)}" th:text="${PAGE_NAME_TO_COUNT.get(pageName)}" class="resourceCountBadge"></span>
</a>
</div>
</div>
<div id="swagger-ui"></div>
<script src="./swagger-ui-bundle.js" charset="UTF-8"> </script>
<script src="./swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
<script>
window.onload = function() {
// Begin Swagger UI call region
const ui = SwaggerUIBundle({
url: "[[@{/api-docs(page=${PAGE})}]]",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
// SwaggerUIBundle.plugins.DownloadUrl
],
// layout: "StandaloneLayout"
});
// End Swagger UI call region
window.ui = ui;
};
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

@ -0,0 +1,274 @@
package ca.uhn.fhir.rest.openapi;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.model.api.annotation.Description;
import ca.uhn.fhir.rest.annotation.ConditionalUrlParam;
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.Patch;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.PatchTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
import ca.uhn.fhir.rest.server.provider.HashMapResourceProvider;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.test.utilities.HtmlUtil;
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
import ca.uhn.fhir.util.ExtensionConstants;
import com.gargoylesoftware.htmlunit.html.DomElement;
import com.gargoylesoftware.htmlunit.html.HtmlDivision;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import io.swagger.v3.core.util.Yaml;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.PathItem;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.hamcrest.Matchers;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseCoding;
import org.hl7.fhir.instance.model.api.IBaseConformance;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseReference;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.CapabilityStatement;
import org.hl7.fhir.r4.model.DecimalType;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
public class OpenApiInterceptorTest {
private static final Logger ourLog = LoggerFactory.getLogger(OpenApiInterceptorTest.class);
private FhirContext myFhirContext = FhirContext.forCached(FhirVersionEnum.R4);
@RegisterExtension
@Order(0)
protected RestfulServerExtension myServer = new RestfulServerExtension(myFhirContext)
.withServletPath("/fhir/*")
.withServer(t -> t.registerProvider(new HashMapResourceProvider<>(myFhirContext, Patient.class)))
.withServer(t -> t.registerProvider(new HashMapResourceProvider<>(myFhirContext, Observation.class)))
.withServer(t -> t.registerProvider(new MyLastNProvider()))
.withServer(t -> t.registerInterceptor(new ResponseHighlighterInterceptor()));
private CloseableHttpClient myClient;
@BeforeEach
public void before() {
myClient = HttpClientBuilder.create().build();
}
@AfterEach
public void after() throws IOException {
myClient.close();
myServer.getRestfulServer().getInterceptorService().unregisterAllInterceptors();
}
@Test
public void testFetchSwagger() throws IOException {
myServer.getRestfulServer().registerInterceptor(new OpenApiInterceptor());
String resp;
HttpGet get = new HttpGet("http://localhost:" + myServer.getPort() + "/fhir/metadata?_pretty=true");
try (CloseableHttpResponse response = myClient.execute(get)) {
resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info("CapabilityStatement: {}", resp);
}
get = new HttpGet("http://localhost:" + myServer.getPort() + "/fhir/api-docs");
try (CloseableHttpResponse response = myClient.execute(get)) {
resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info("Response: {}", response.getStatusLine());
ourLog.info("Response: {}", resp);
}
OpenAPI parsed = Yaml.mapper().readValue(resp, OpenAPI.class);
PathItem fooOpPath = parsed.getPaths().get("/$foo-op");
assertNull(fooOpPath.getGet());
assertNotNull(fooOpPath.getPost());
assertEquals("Foo Op Description", fooOpPath.getPost().getDescription());
assertEquals("Foo Op Short", fooOpPath.getPost().getSummary());
PathItem lastNPath = parsed.getPaths().get("/Observation/$lastn");
assertNull(lastNPath.getPost());
assertNotNull(lastNPath.getGet());
assertEquals("LastN Description", lastNPath.getGet().getDescription());
assertEquals("LastN Short", lastNPath.getGet().getSummary());
assertEquals(4, lastNPath.getGet().getParameters().size());
assertEquals("Subject description", lastNPath.getGet().getParameters().get(0).getDescription());
}
@Test
public void testRedirectFromBaseUrl() throws IOException {
myServer.getRestfulServer().registerInterceptor(new OpenApiInterceptor());
HttpGet get;
get = new HttpGet("http://localhost:" + myServer.getPort() + "/fhir/");
try (CloseableHttpResponse response = myClient.execute(get)) {
assertEquals(400, response.getStatusLine().getStatusCode());
}
get = new HttpGet("http://localhost:" + myServer.getPort() + "/fhir/");
get.addHeader(Constants.HEADER_ACCEPT, Constants.CT_HTML);
try (CloseableHttpResponse response = myClient.execute(get)) {
String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info("Response: {}", response);
ourLog.info("Response: {}", responseString);
assertEquals(200, response.getStatusLine().getStatusCode());
assertThat(responseString, containsString("<title>Swagger UI</title>"));
}
}
@Test
public void testSwaggerUiWithResourceCounts() throws IOException {
myServer.getRestfulServer().registerInterceptor(new AddResourceCountsInterceptor());
myServer.getRestfulServer().registerInterceptor(new OpenApiInterceptor());
String url = "http://localhost:" + myServer.getPort() + "/fhir/swagger-ui/";
String resp = fetchSwaggerUi(url);
List<String> buttonTexts = parsePageButtonTexts(resp, url);
assertThat(buttonTexts.toString(), buttonTexts, Matchers.contains("All", "System Level Operations", "Patient 2", "OperationDefinition 1", "Observation 0"));
}
@Test
public void testSwaggerUiWithCopyright() throws IOException {
myServer.getRestfulServer().registerInterceptor(new AddResourceCountsInterceptor());
myServer.getRestfulServer().registerInterceptor(new OpenApiInterceptor());
String url = "http://localhost:" + myServer.getPort() + "/fhir/swagger-ui/";
String resp = fetchSwaggerUi(url);
assertThat(resp, resp, containsString("<p>This server is copyright <strong>Example Org</strong> 2021</p>"));
}
@Test
public void testSwaggerUiWithResourceCounts_OneResourceOnly() throws IOException {
myServer.getRestfulServer().registerInterceptor(new AddResourceCountsInterceptor("OperationDefinition"));
myServer.getRestfulServer().registerInterceptor(new OpenApiInterceptor());
String url = "http://localhost:" + myServer.getPort() + "/fhir/swagger-ui/";
String resp = fetchSwaggerUi(url);
List<String> buttonTexts = parsePageButtonTexts(resp, url);
assertThat(buttonTexts.toString(), buttonTexts, Matchers.contains("All", "System Level Operations", "OperationDefinition 1", "Observation", "Patient"));
}
private String fetchSwaggerUi(String url) throws IOException {
String resp;
HttpGet get = new HttpGet(url);
try (CloseableHttpResponse response = myClient.execute(get)) {
resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info("Response: {}", response.getStatusLine());
ourLog.info("Response: {}", resp);
}
return resp;
}
private List<String> parsePageButtonTexts(String resp, String url) throws IOException {
HtmlPage html = HtmlUtil.parseAsHtml(resp, new URL(url));
HtmlDivision pageButtons = (HtmlDivision) html.getElementById("pageButtons");
List<String> buttonTexts = new ArrayList<>();
for (DomElement next : pageButtons.getChildElements()) {
buttonTexts.add(next.asNormalizedText());
}
return buttonTexts;
}
public static class AddResourceCountsInterceptor {
private final HashSet<String> myResourceNamesToAddTo;
public AddResourceCountsInterceptor(String... theResourceNamesToAddTo) {
myResourceNamesToAddTo = new HashSet<>(Arrays.asList(theResourceNamesToAddTo));
}
@Hook(Pointcut.SERVER_CAPABILITY_STATEMENT_GENERATED)
public void capabilityStatementGenerated(IBaseConformance theCapabilityStatement) {
CapabilityStatement cs = (CapabilityStatement) theCapabilityStatement;
cs.setCopyright("This server is copyright **Example Org** 2021");
int numResources = cs.getRestFirstRep().getResource().size();
for (int i = 0; i < numResources; i++) {
CapabilityStatement.CapabilityStatementRestResourceComponent restResource = cs.getRestFirstRep().getResource().get(i);
if (!myResourceNamesToAddTo.isEmpty() && !myResourceNamesToAddTo.contains(restResource.getType())) {
continue;
}
restResource.addExtension(
ExtensionConstants.CONF_RESOURCE_COUNT,
new DecimalType(i) // reverse order
);
}
}
}
public static class MyLastNProvider {
@Description(value = "LastN Description", shortDefinition = "LastN Short")
@Operation(name = Constants.OPERATION_LASTN, typeName = "Observation", idempotent = true)
public IBaseBundle lastN(
@Description(value = "Subject description", shortDefinition = "Subject short", example = {"Patient/456", "Patient/789"})
@OperationParam(name = "subject", typeName = "reference", min = 0, max = 1) IBaseReference theSubject,
@OperationParam(name = "category", typeName = "coding", min = 0, max = OperationParam.MAX_UNLIMITED) List<IBaseCoding> theCategories,
@OperationParam(name = "code", typeName = "coding", min = 0, max = OperationParam.MAX_UNLIMITED) List<IBaseCoding> theCodes,
@OperationParam(name = "max", typeName = "integer", min = 0, max = 1) IPrimitiveType<Integer> theMax
) {
throw new IllegalStateException();
}
@Description(value = "Foo Op Description", shortDefinition = "Foo Op Short")
@Operation(name = "foo-op", idempotent = false)
public IBaseBundle foo(
ServletRequestDetails theRequestDetails,
@Description(shortDefinition = "Reference description", example = "Patient/123")
@OperationParam(name = "subject", typeName = "reference", min = 0, max = 1) IBaseReference theSubject,
@OperationParam(name = "category", typeName = "coding", min = 0, max = OperationParam.MAX_UNLIMITED) List<IBaseCoding> theCategories,
@OperationParam(name = "code", typeName = "coding", min = 0, max = OperationParam.MAX_UNLIMITED) List<IBaseCoding> theCodes,
@OperationParam(name = "max", typeName = "integer", min = 0, max = 1) IPrimitiveType<Integer> theMax
) {
throw new IllegalStateException();
}
@Patch(type = Patient.class)
public MethodOutcome patch(HttpServletRequest theRequest, @IdParam IIdType theId, @ConditionalUrlParam String theConditionalUrl, RequestDetails theRequestDetails, @ResourceParam String theBody, PatchTypeEnum thePatchType, @ResourceParam IBaseParameters theRequestBody) {
throw new IllegalStateException();
}
}
}

View File

@ -0,0 +1,129 @@
package ca.uhn.fhir.rest.openapi;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.model.api.annotation.Description;
import ca.uhn.fhir.rest.annotation.ConditionalUrlParam;
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.Patch;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.PatchTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
import ca.uhn.fhir.rest.server.interceptor.auth.AuthorizationInterceptor;
import ca.uhn.fhir.rest.server.interceptor.auth.IAuthRule;
import ca.uhn.fhir.rest.server.interceptor.auth.RuleBuilder;
import ca.uhn.fhir.rest.server.provider.HashMapResourceProvider;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.test.utilities.HtmlUtil;
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
import ca.uhn.fhir.util.ExtensionConstants;
import com.gargoylesoftware.htmlunit.html.DomElement;
import com.gargoylesoftware.htmlunit.html.HtmlDivision;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import io.swagger.v3.core.util.Yaml;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.PathItem;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.hamcrest.Matchers;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseCoding;
import org.hl7.fhir.instance.model.api.IBaseConformance;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseReference;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.CapabilityStatement;
import org.hl7.fhir.r4.model.DecimalType;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
public class OpenApiInterceptorWithAuthorizationInterceptorTest {
private static final Logger ourLog = LoggerFactory.getLogger(OpenApiInterceptorWithAuthorizationInterceptorTest.class);
private FhirContext myFhirContext = FhirContext.forCached(FhirVersionEnum.R4);
@RegisterExtension
@Order(0)
protected RestfulServerExtension myServer = new RestfulServerExtension(myFhirContext)
.withServletPath("/fhir/*")
.withServer(t -> t.registerProvider(new HashMapResourceProvider<>(myFhirContext, Patient.class)))
.withServer(t -> t.registerProvider(new HashMapResourceProvider<>(myFhirContext, Observation.class)))
.withServer(t -> t.registerProvider(new OpenApiInterceptorTest.MyLastNProvider()))
.withServer(t -> t.registerInterceptor(new ResponseHighlighterInterceptor()));
private CloseableHttpClient myClient;
private AuthorizationInterceptor myAuthorizationInterceptor;
private List<IAuthRule> myRules;
@BeforeEach
public void before() {
myClient = HttpClientBuilder.create().build();
myAuthorizationInterceptor = new AuthorizationInterceptor() {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return myRules;
}
};
}
@AfterEach
public void after() throws IOException {
myClient.close();
myServer.getRestfulServer().getInterceptorService().unregisterAllInterceptors();
}
@Test
public void testFetchSwagger_AllowAll() throws IOException {
myServer.getRestfulServer().registerInterceptor(new OpenApiInterceptor());
myServer.getRestfulServer().registerInterceptor(myAuthorizationInterceptor);
myRules = new RuleBuilder()
.allowAll()
.build();
String resp;
HttpGet get;
get = new HttpGet("http://localhost:" + myServer.getPort() + "/fhir/api-docs");
try (CloseableHttpResponse response = myClient.execute(get)) {
resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info("Response: {}", response.getStatusLine());
ourLog.info("Response: {}", resp);
assertEquals(200, response.getStatusLine().getStatusCode());
}
OpenAPI parsed = Yaml.mapper().readValue(resp, OpenAPI.class);
assertNotNull(parsed.getPaths().get("/Patient").getPost());
}
}

View File

@ -0,0 +1,16 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%file:%line] %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT" />
</root>
</configuration>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE7-SNAPSHOT</version>
<version>5.4.0-PRE8-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -7,6 +7,7 @@ import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.server.IRestfulServerDefaults;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.StopWatch;
import ca.uhn.fhir.util.UrlUtil;
import org.apache.commons.lang3.Validate;
@ -51,7 +52,7 @@ import static org.apache.commons.lang3.StringUtils.isBlank;
public abstract class RequestDetails {
private final StopWatch myRequestStopwatch = new StopWatch();
private final StopWatch myRequestStopwatch;
private IInterceptorBroadcaster myInterceptorBroadcaster;
private String myTenantId;
private String myCompartmentName;
@ -81,6 +82,37 @@ public abstract class RequestDetails {
*/
public RequestDetails(IInterceptorBroadcaster theInterceptorBroadcaster) {
myInterceptorBroadcaster = theInterceptorBroadcaster;
myRequestStopwatch = new StopWatch();
}
/**
* Copy constructor
*/
public RequestDetails(ServletRequestDetails theRequestDetails) {
myInterceptorBroadcaster = theRequestDetails.getInterceptorBroadcaster();
myRequestStopwatch = theRequestDetails.getRequestStopwatch();
myTenantId = theRequestDetails.getTenantId();
myCompartmentName = theRequestDetails.getCompartmentName();
myCompleteUrl = theRequestDetails.getCompleteUrl();
myFhirServerBase = theRequestDetails.getFhirServerBase();
myId = theRequestDetails.getId();
myOperation = theRequestDetails.getOperation();
myParameters = theRequestDetails.getParameters();
myRequestContents = theRequestDetails.getRequestContentsIfLoaded();
myRequestPath = theRequestDetails.getRequestPath();
myRequestType = theRequestDetails.getRequestType();
myResourceName = theRequestDetails.getResourceName();
myRespondGzip = theRequestDetails.isRespondGzip();
myResponse = theRequestDetails.getResponse();
myRestOperationType = theRequestDetails.getRestOperationType();
mySecondaryOperation = theRequestDetails.getSecondaryOperation();
mySubRequest = theRequestDetails.isSubRequest();
myUnqualifiedToQualifiedNames = theRequestDetails.getUnqualifiedToQualifiedNames();
myUserData = theRequestDetails.getUserData();
myResource = theRequestDetails.getResource();
myRequestId = theRequestDetails.getRequestId();
myTransactionGuid = theRequestDetails.getTransactionGuid();
myFixedConditionalUrl = theRequestDetails.getFixedConditionalUrl();
}
public String getFixedConditionalUrl() {

View File

@ -30,14 +30,14 @@ import java.util.List;
public class Bindings {
private final IdentityHashMap<SearchMethodBinding, String> myNamedSearchMethodBindingToName;
private final HashMap<String, List<SearchMethodBinding>> mySearchNameToBindings;
private final HashMap<String, List<OperationMethodBinding>> myOperationNameToBindings;
private final IdentityHashMap<OperationMethodBinding, String> myOperationBindingToName;
private final HashMap<String, List<OperationMethodBinding>> myOperationIdToBindings;
private final IdentityHashMap<OperationMethodBinding, String> myOperationBindingToId;
public Bindings(IdentityHashMap<SearchMethodBinding, String> theNamedSearchMethodBindingToName, HashMap<String, List<SearchMethodBinding>> theSearchNameToBindings, HashMap<String, List<OperationMethodBinding>> theOperationNameToBindings, IdentityHashMap<OperationMethodBinding, String> theOperationBindingToName) {
public Bindings(IdentityHashMap<SearchMethodBinding, String> theNamedSearchMethodBindingToName, HashMap<String, List<SearchMethodBinding>> theSearchNameToBindings, HashMap<String, List<OperationMethodBinding>> theOperationIdToBindings, IdentityHashMap<OperationMethodBinding, String> theOperationBindingToName) {
myNamedSearchMethodBindingToName = theNamedSearchMethodBindingToName;
mySearchNameToBindings = theSearchNameToBindings;
myOperationNameToBindings = theOperationNameToBindings;
myOperationBindingToName = theOperationBindingToName;
myOperationIdToBindings = theOperationIdToBindings;
myOperationBindingToId = theOperationBindingToName;
}
public IdentityHashMap<SearchMethodBinding, String> getNamedSearchMethodBindingToName() {
@ -48,11 +48,11 @@ public class Bindings {
return mySearchNameToBindings;
}
public HashMap<String, List<OperationMethodBinding>> getOperationNameToBindings() {
return myOperationNameToBindings;
public HashMap<String, List<OperationMethodBinding>> getOperationIdToBindings() {
return myOperationIdToBindings;
}
public IdentityHashMap<OperationMethodBinding, String> getOperationBindingToName() {
return myOperationBindingToName;
public IdentityHashMap<OperationMethodBinding, String> getOperationBindingToId() {
return myOperationBindingToId;
}
}

View File

@ -22,8 +22,11 @@ package ca.uhn.fhir.rest.server;
import javax.servlet.http.HttpServletRequest;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Read;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
public interface IServerConformanceProvider<T extends IBaseResource> {
@ -34,6 +37,11 @@ public interface IServerConformanceProvider<T extends IBaseResource> {
*/
T getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails);
@Read(typeName = "OperationDefinition")
default IBaseResource readOperationDefinition(@IdParam IIdType theId, RequestDetails theRequestDetails) {
return null;
}
/**
* This setter is needed in implementation classes (along with
* a no-arg constructor) to avoid reference cycles in the

View File

@ -67,6 +67,7 @@ import com.google.common.collect.Lists;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseConformance;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.slf4j.Logger;
@ -148,6 +149,7 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
private FhirContext myFhirContext;
private boolean myIgnoreServerParsedRequestParameters = true;
private String myImplementationDescription;
private String myCopyright;
private IPagingProvider myPagingProvider;
private Integer myDefaultPageSize;
private Integer myMaximumPageSize;
@ -157,7 +159,7 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
private IServerAddressStrategy myServerAddressStrategy = new IncomingRequestAddressStrategy();
private ResourceBinding myServerBinding = new ResourceBinding();
private ResourceBinding myGlobalBinding = new ResourceBinding();
private BaseMethodBinding<?> myServerConformanceMethod;
private ConformanceMethodBinding myServerConformanceMethod;
private Object myServerConformanceProvider;
private String myServerName = "HAPI FHIR Server";
/**
@ -234,6 +236,7 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
RestfulServerConfiguration result = new RestfulServerConfiguration();
result.setResourceBindings(getResourceBindings());
result.setServerBindings(getServerBindings());
result.setGlobalBindings(getGlobalBindings());
result.setImplementationDescription(getImplementationDescription());
result.setServerVersion(getServerVersion());
result.setServerName(getServerName());
@ -252,6 +255,10 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
return result;
}
private List<BaseMethodBinding<?>> getGlobalBindings() {
return myGlobalBinding.getMethodBindings();
}
protected List<String> createPoweredByAttributes() {
return Lists.newArrayList("FHIR Server", "FHIR " + myFhirContext.getVersion().getVersion().getFhirVersionString() + "/" + myFhirContext.getVersion().getVersion().name());
}
@ -458,7 +465,10 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
count++;
if (foundMethodBinding instanceof ConformanceMethodBinding) {
myServerConformanceMethod = foundMethodBinding;
myServerConformanceMethod = (ConformanceMethodBinding) foundMethodBinding;
if (myServerConformanceProvider == null) {
myServerConformanceProvider = theProvider;
}
continue;
}
@ -635,6 +645,20 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
myImplementationDescription = theImplementationDescription;
}
/**
* Returns the server copyright (will be added to the CapabilityStatement). Note that FHIR allows Markdown in this string.
*/
public String getCopyright() {
return myCopyright;
}
/**
* Sets the server copyright (will be added to the CapabilityStatement). Note that FHIR allows Markdown in this string.
*/
public void setCopyright(String theCopyright) {
myCopyright = theCopyright;
}
/**
* Returns a list of all registered server interceptors
*
@ -806,8 +830,8 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
/**
* Provides the resource providers for this server
*/
public Collection<IResourceProvider> getResourceProviders() {
return myResourceProviders;
public List<IResourceProvider> getResourceProviders() {
return Collections.unmodifiableList(myResourceProviders);
}
/**
@ -853,6 +877,7 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
public String getServerBaseForRequest(ServletRequestDetails theRequest) {
String fhirServerBase;
fhirServerBase = myServerAddressStrategy.determineServerBase(getServletContext(), theRequest.getServletRequest());
assert isNotBlank(fhirServerBase) : "Server Address Strategy did not return a value";
if (fhirServerBase.endsWith("/")) {
fhirServerBase = fhirServerBase.substring(0, fhirServerBase.length() - 1);
@ -1363,9 +1388,9 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
*/
findResourceMethods(new PageProvider());
} catch (Exception ex) {
ourLog.error("An error occurred while loading request handlers!", ex);
throw new ServletException("Failed to initialize FHIR Restful server", ex);
} catch (Exception e) {
ourLog.error("An error occurred while loading request handlers!", e);
throw new ServletException("Failed to initialize FHIR Restful server: " + e.getMessage(), e);
}
myStarted = true;
@ -1984,6 +2009,17 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
myDefaultPreferReturn = theDefaultPreferReturn;
}
/**
* Create a CapabilityStatement based on the given request
*/
public IBaseConformance getCapabilityStatement(ServletRequestDetails theRequestDetails) {
// Create a cloned request details so we can make it indicate that this is a capabilities request
ServletRequestDetails requestDetails = new ServletRequestDetails(theRequestDetails);
requestDetails.setRestOperationType(RestOperationTypeEnum.METADATA);
return myServerConformanceMethod.provideCapabilityStatement(this, requestDetails);
}
/**
* Count length of URL string, but treating unescaped sequences (e.g. ' ') as their unescaped equivalent (%20)
*/

View File

@ -30,6 +30,9 @@ import ca.uhn.fhir.rest.server.method.SearchMethodBinding;
import ca.uhn.fhir.rest.server.method.SearchParameter;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import ca.uhn.fhir.util.VersionUtil;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimap;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseResource;
@ -46,27 +49,32 @@ 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.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.isBlank;
public class RestfulServerConfiguration implements ISearchParamRegistry {
public static final String GLOBAL = "GLOBAL";
private static final Logger ourLog = LoggerFactory.getLogger(RestfulServerConfiguration.class);
private Collection<ResourceBinding> resourceBindings;
private List<BaseMethodBinding<?>> serverBindings;
private List<BaseMethodBinding<?>> myGlobalBindings;
private Map<String, Class<? extends IBaseResource>> resourceNameToSharedSupertype;
private String implementationDescription;
private String serverVersion = VersionUtil.getVersion();
private String serverName = "HAPI FHIR";
private FhirContext fhirContext;
private IServerAddressStrategy serverAddressStrategy;
private String myImplementationDescription;
private String myServerName = "HAPI FHIR";
private String myServerVersion = VersionUtil.getVersion();
private FhirContext myFhirContext;
private IServerAddressStrategy myServerAddressStrategy;
private IPrimitiveType<Date> myConformanceDate;
/**
@ -127,10 +135,10 @@ public class RestfulServerConfiguration implements ISearchParamRegistry {
* @return the implementationDescription
*/
public String getImplementationDescription() {
if (isBlank(implementationDescription)) {
if (isBlank(myImplementationDescription)) {
return "HAPI FHIR";
}
return implementationDescription;
return myImplementationDescription;
}
/**
@ -139,7 +147,7 @@ public class RestfulServerConfiguration implements ISearchParamRegistry {
* @param implementationDescription the implementationDescription to set
*/
public RestfulServerConfiguration setImplementationDescription(String implementationDescription) {
this.implementationDescription = implementationDescription;
this.myImplementationDescription = implementationDescription;
return this;
}
@ -149,7 +157,7 @@ public class RestfulServerConfiguration implements ISearchParamRegistry {
* @return the serverVersion
*/
public String getServerVersion() {
return serverVersion;
return myServerVersion;
}
/**
@ -158,7 +166,7 @@ public class RestfulServerConfiguration implements ISearchParamRegistry {
* @param serverVersion the serverVersion to set
*/
public RestfulServerConfiguration setServerVersion(String serverVersion) {
this.serverVersion = serverVersion;
this.myServerVersion = serverVersion;
return this;
}
@ -168,7 +176,7 @@ public class RestfulServerConfiguration implements ISearchParamRegistry {
* @return the serverName
*/
public String getServerName() {
return serverName;
return myServerName;
}
/**
@ -177,7 +185,7 @@ public class RestfulServerConfiguration implements ISearchParamRegistry {
* @param serverName the serverName to set
*/
public RestfulServerConfiguration setServerName(String serverName) {
this.serverName = serverName;
this.myServerName = serverName;
return this;
}
@ -186,7 +194,7 @@ public class RestfulServerConfiguration implements ISearchParamRegistry {
* creating their own.
*/
public FhirContext getFhirContext() {
return this.fhirContext;
return this.myFhirContext;
}
/**
@ -195,7 +203,7 @@ public class RestfulServerConfiguration implements ISearchParamRegistry {
* @param fhirContext the fhirContext to set
*/
public RestfulServerConfiguration setFhirContext(FhirContext fhirContext) {
this.fhirContext = fhirContext;
this.myFhirContext = fhirContext;
return this;
}
@ -205,7 +213,7 @@ public class RestfulServerConfiguration implements ISearchParamRegistry {
* @return the serverAddressStrategy
*/
public IServerAddressStrategy getServerAddressStrategy() {
return serverAddressStrategy;
return myServerAddressStrategy;
}
/**
@ -214,7 +222,7 @@ public class RestfulServerConfiguration implements ISearchParamRegistry {
* @param serverAddressStrategy the serverAddressStrategy to set
*/
public void setServerAddressStrategy(IServerAddressStrategy serverAddressStrategy) {
this.serverAddressStrategy = serverAddressStrategy;
this.myServerAddressStrategy = serverAddressStrategy;
}
/**
@ -236,48 +244,114 @@ public class RestfulServerConfiguration implements ISearchParamRegistry {
}
public Bindings provideBindings() {
IdentityHashMap<SearchMethodBinding, String> myNamedSearchMethodBindingToName = new IdentityHashMap<>();
HashMap<String, List<SearchMethodBinding>> mySearchNameToBindings = new HashMap<>();
IdentityHashMap<OperationMethodBinding, String> myOperationBindingToName = new IdentityHashMap<>();
HashMap<String, List<OperationMethodBinding>> myOperationNameToBindings = new HashMap<>();
IdentityHashMap<SearchMethodBinding, String> namedSearchMethodBindingToName = new IdentityHashMap<>();
HashMap<String, List<SearchMethodBinding>> searchNameToBindings = new HashMap<>();
IdentityHashMap<OperationMethodBinding, String> operationBindingToId = new IdentityHashMap<>();
HashMap<String, List<OperationMethodBinding>> operationIdToBindings = new HashMap<>();
Map<String, List<BaseMethodBinding<?>>> resourceToMethods = collectMethodBindings();
for (Map.Entry<String, List<BaseMethodBinding<?>>> nextEntry : resourceToMethods.entrySet()) {
List<BaseMethodBinding<?>> nextMethodBindings = nextEntry.getValue();
for (BaseMethodBinding<?> nextMethodBinding : nextMethodBindings) {
List<BaseMethodBinding<?>> methodBindings = resourceToMethods
.values()
.stream().flatMap(t -> t.stream())
.collect(Collectors.toList());
if (myGlobalBindings != null) {
methodBindings.addAll(myGlobalBindings);
}
ListMultimap<String, OperationMethodBinding> nameToOperationMethodBindings = ArrayListMultimap.create();
for (BaseMethodBinding<?> nextMethodBinding : methodBindings) {
if (nextMethodBinding instanceof OperationMethodBinding) {
OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
if (myOperationBindingToName.containsKey(methodBinding)) {
continue;
}
String name = createOperationName(methodBinding);
ourLog.debug("Detected operation: {}", name);
myOperationBindingToName.put(methodBinding, name);
if (myOperationNameToBindings.containsKey(name) == false) {
myOperationNameToBindings.put(name, new ArrayList<>());
}
myOperationNameToBindings.get(name).add(methodBinding);
nameToOperationMethodBindings.put(methodBinding.getName(), methodBinding);
} else if (nextMethodBinding instanceof SearchMethodBinding) {
SearchMethodBinding methodBinding = (SearchMethodBinding) nextMethodBinding;
if (myNamedSearchMethodBindingToName.containsKey(methodBinding)) {
if (namedSearchMethodBindingToName.containsKey(methodBinding)) {
continue;
}
String name = createNamedQueryName(methodBinding);
ourLog.debug("Detected named query: {}", name);
myNamedSearchMethodBindingToName.put(methodBinding, name);
if (!mySearchNameToBindings.containsKey(name)) {
mySearchNameToBindings.put(name, new ArrayList<>());
}
mySearchNameToBindings.get(name).add(methodBinding);
namedSearchMethodBindingToName.put(methodBinding, name);
if (!searchNameToBindings.containsKey(name)) {
searchNameToBindings.put(name, new ArrayList<>());
}
searchNameToBindings.get(name).add(methodBinding);
}
}
return new Bindings(myNamedSearchMethodBindingToName, mySearchNameToBindings, myOperationNameToBindings, myOperationBindingToName);
for (String nextName : nameToOperationMethodBindings.keySet()) {
List<OperationMethodBinding> nextMethodBindings = nameToOperationMethodBindings.get(nextName);
boolean global = false;
boolean system = false;
boolean instance = false;
boolean type = false;
Set<String> resourceTypes = null;
for (OperationMethodBinding nextMethodBinding : nextMethodBindings) {
global |= nextMethodBinding.isGlobalMethod();
system |= nextMethodBinding.isCanOperateAtServerLevel();
type |= nextMethodBinding.isCanOperateAtTypeLevel();
instance |= nextMethodBinding.isCanOperateAtInstanceLevel();
if (nextMethodBinding.getResourceName() != null) {
resourceTypes = resourceTypes != null ? resourceTypes : new TreeSet<>();
resourceTypes.add(nextMethodBinding.getResourceName());
}
}
StringBuilder operationIdBuilder = new StringBuilder();
if (global) {
operationIdBuilder.append("Global");
} else if (resourceTypes != null && resourceTypes.size() == 1) {
operationIdBuilder.append(resourceTypes.iterator().next());
} else if (resourceTypes != null && resourceTypes.size() == 2) {
Iterator<String> iterator = resourceTypes.iterator();
operationIdBuilder.append(iterator.next());
operationIdBuilder.append(iterator.next());
} else if (resourceTypes != null) {
operationIdBuilder.append("Multi");
}
operationIdBuilder.append('-');
if (instance) {
operationIdBuilder.append('i');
}
if (type) {
operationIdBuilder.append('t');
}
if (system) {
operationIdBuilder.append('s');
}
operationIdBuilder.append('-');
// Exclude the leading $
operationIdBuilder.append(nextName, 1, nextName.length());
String operationId = operationIdBuilder.toString();
operationIdToBindings.put(operationId, nextMethodBindings);
nextMethodBindings.forEach(t->operationBindingToId.put(t, operationId));
}
for (BaseMethodBinding<?> nextMethodBinding : methodBindings) {
if (nextMethodBinding instanceof OperationMethodBinding) {
OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
if (operationBindingToId.containsKey(methodBinding)) {
continue;
}
String name = createOperationName(methodBinding);
ourLog.debug("Detected operation: {}", name);
operationBindingToId.put(methodBinding, name);
if (operationIdToBindings.containsKey(name) == false) {
operationIdToBindings.put(name, new ArrayList<>());
}
operationIdToBindings.get(name).add(methodBinding);
}
}
return new Bindings(namedSearchMethodBindingToName, searchNameToBindings, operationIdToBindings, operationBindingToId);
}
public Map<String, List<BaseMethodBinding<?>>> collectMethodBindings() {
@ -301,6 +375,14 @@ public class RestfulServerConfiguration implements ISearchParamRegistry {
return resourceToMethods;
}
public List<BaseMethodBinding<?>> getGlobalBindings() {
return myGlobalBindings;
}
public void setGlobalBindings(List<BaseMethodBinding<?>> theGlobalBindings) {
myGlobalBindings = theGlobalBindings;
}
/*
* Populates {@link #resourceNameToSharedSupertype} by scanning the given resource providers. Only resource provider getResourceType values
* are taken into account. {@link ProvidesResources} and method return types are deliberately ignored.
@ -329,28 +411,6 @@ public class RestfulServerConfiguration implements ISearchParamRegistry {
entry -> entry.getValue().getLowestCommonSuperclass().get()));
}
private String createOperationName(OperationMethodBinding theMethodBinding) {
StringBuilder retVal = new StringBuilder();
if (theMethodBinding.getResourceName() != null) {
retVal.append(theMethodBinding.getResourceName());
}
retVal.append('-');
if (theMethodBinding.isCanOperateAtInstanceLevel()) {
retVal.append('i');
}
if (theMethodBinding.isCanOperateAtServerLevel()) {
retVal.append('s');
}
retVal.append('-');
// Exclude the leading $
retVal.append(theMethodBinding.getName(), 1, theMethodBinding.getName().length());
return retVal.toString();
}
private String createNamedQueryName(SearchMethodBinding searchMethodBinding) {
StringBuilder retVal = new StringBuilder();
if (searchMethodBinding.getResourceName() != null) {
@ -443,7 +503,6 @@ public class RestfulServerConfiguration implements ISearchParamRegistry {
}
private static class SearchParameterComparator implements Comparator<SearchParameter> {
private static final SearchParameterComparator INSTANCE = new SearchParameterComparator();
@ -458,4 +517,27 @@ public class RestfulServerConfiguration implements ISearchParamRegistry {
return 1;
}
}
private static String createOperationName(OperationMethodBinding theMethodBinding) {
StringBuilder retVal = new StringBuilder();
if (theMethodBinding.getResourceName() != null) {
retVal.append(theMethodBinding.getResourceName());
} else if (theMethodBinding.isGlobalMethod()) {
retVal.append("Global");
}
retVal.append('-');
if (theMethodBinding.isCanOperateAtInstanceLevel()) {
retVal.append('i');
}
if (theMethodBinding.isCanOperateAtServerLevel()) {
retVal.append('s');
}
retVal.append('-');
// Exclude the leading $
retVal.append(theMethodBinding.getName(), 1, theMethodBinding.getName().length());
return retVal.toString();
}
}

View File

@ -41,6 +41,7 @@ import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetai
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.ReflectionUtil;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseResource;
import javax.annotation.Nonnull;
@ -386,6 +387,10 @@ public abstract class BaseMethodBinding<T> {
+ " returns a collection with generic type " + toLogString(returnTypeFromMethod)
+ " - Must return a resource type or a collection (List, Set) with a resource type parameter (e.g. List<Patient> or List<IBaseResource> )");
}
} else if (IBaseBundle.class.isAssignableFrom(returnTypeFromMethod) && returnTypeFromRp == null) {
// If a plain provider method returns a Bundle, we'll assume it to be a system
// level operation and not a type/instance level operation on the Bundle type.
returnTypeFromMethod = null;
} else {
if (!isResourceInterface(returnTypeFromMethod) && !verifyIsValidResourceReturnType(returnTypeFromMethod)) {
throw new ConfigurationException("Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName()
@ -395,30 +400,41 @@ public abstract class BaseMethodBinding<T> {
}
Class<? extends IBaseResource> returnTypeFromAnnotation = IBaseResource.class;
String returnTypeNameFromAnnotation = null;
if (read != null) {
if (isNotBlank(read.typeName())) {
returnTypeFromAnnotation = theContext.getResourceDefinition(read.typeName()).getImplementingClass();
} else {
returnTypeFromAnnotation = read.type();
}
returnTypeNameFromAnnotation = read.typeName();
} else if (search != null) {
returnTypeFromAnnotation = search.type();
returnTypeNameFromAnnotation = search.typeName();
} else if (history != null) {
returnTypeFromAnnotation = history.type();
returnTypeNameFromAnnotation = history.typeName();
} else if (delete != null) {
returnTypeFromAnnotation = delete.type();
returnTypeNameFromAnnotation = delete.typeName();
} else if (patch != null) {
returnTypeFromAnnotation = patch.type();
returnTypeNameFromAnnotation = patch.typeName();
} else if (create != null) {
returnTypeFromAnnotation = create.type();
returnTypeNameFromAnnotation = create.typeName();
} else if (update != null) {
returnTypeFromAnnotation = update.type();
returnTypeNameFromAnnotation = update.typeName();
} else if (validate != null) {
returnTypeFromAnnotation = validate.type();
returnTypeNameFromAnnotation = validate.typeName();
} else if (addTags != null) {
returnTypeFromAnnotation = addTags.type();
returnTypeNameFromAnnotation = addTags.typeName();
} else if (deleteTags != null) {
returnTypeFromAnnotation = deleteTags.type();
returnTypeNameFromAnnotation = deleteTags.typeName();
}
if (isNotBlank(returnTypeNameFromAnnotation)) {
returnTypeFromAnnotation = theContext.getResourceDefinition(returnTypeNameFromAnnotation).getImplementingClass();
}
if (returnTypeFromRp != null) {
@ -477,7 +493,7 @@ public abstract class BaseMethodBinding<T> {
}
private static boolean isResourceInterface(Class<?> theReturnTypeFromMethod) {
return theReturnTypeFromMethod.equals(IBaseResource.class) || theReturnTypeFromMethod.equals(IResource.class) || theReturnTypeFromMethod.equals(IAnyResource.class);
return theReturnTypeFromMethod != null && (theReturnTypeFromMethod.equals(IBaseResource.class) || theReturnTypeFromMethod.equals(IResource.class) || theReturnTypeFromMethod.equals(IAnyResource.class));
}
private static String toLogString(Class<?> theType) {

Some files were not shown because too many files have changed in this diff Show More