diff --git a/hapi-deployable-pom/pom.xml b/hapi-deployable-pom/pom.xml index 9df4f52f5df..e5c236da510 100644 --- a/hapi-deployable-pom/pom.xml +++ b/hapi-deployable-pom/pom.xml @@ -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> diff --git a/hapi-fhir-android/pom.xml b/hapi-fhir-android/pom.xml index a03a6465c2a..29daf3ce599 100644 --- a/hapi-fhir-android/pom.xml +++ b/hapi-fhir-android/pom.xml @@ -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> diff --git a/hapi-fhir-base/pom.xml b/hapi-fhir-base/pom.xml index 912fe4efd95..d841d0053e9 100644 --- a/hapi-fhir-base/pom.xml +++ b/hapi-fhir-base/pom.xml @@ -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> diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/BaseRuntimeDeclaredChildDefinition.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/BaseRuntimeDeclaredChildDefinition.java index 3008324e443..e0dc2a4dceb 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/BaseRuntimeDeclaredChildDefinition.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/BaseRuntimeDeclaredChildDefinition.java @@ -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; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/annotation/Description.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/annotation/Description.java index 21cc111e9c6..ecb5317a5f6 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/annotation/Description.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/annotation/Description.java @@ -30,17 +30,33 @@ import java.lang.annotation.Target; * a search parameter definition in order to provide documentation for that item. */ @Retention(RetentionPolicy.RUNTIME) -@Target(value= {ElementType.FIELD, ElementType.TYPE, ElementType.PARAMETER, ElementType.METHOD}) +@Target(value = {ElementType.FIELD, ElementType.TYPE, ElementType.PARAMETER, ElementType.METHOD}) public @interface Description { /** - * Optional short name for this child + * A description of this method or parameter + * + * @since 5.4.0 + */ + String value() default ""; + + /** + * Optional short name for this child */ String shortDefinition() default ""; - + /** * 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 {}; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/AddTags.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/AddTags.java index efb3959e40f..00999343985 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/AddTags.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/AddTags.java @@ -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 ""; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Create.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Create.java index 2d0ab2811b9..f00f6af8e59 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Create.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Create.java @@ -45,6 +45,15 @@ public @interface Create { */ // 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 ""; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Delete.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Delete.java index 33985849f09..82d7e4f1ac8 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Delete.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Delete.java @@ -47,5 +47,16 @@ public @interface Delete { * for client implementations. */ // NB: Read, Search (maybe others) share this annotation, so update the javadocs everywhere - Class<? extends IBaseResource> type() default IBaseResource.class; + 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 ""; + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/DeleteTags.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/DeleteTags.java index cfed68e45cb..202e82f7c14 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/DeleteTags.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/DeleteTags.java @@ -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 ""; + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/History.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/History.java index 6f43678de2f..cd2e0591838 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/History.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/History.java @@ -80,5 +80,15 @@ public @interface History { * for information on usage patterns. */ 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 ""; + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Operation.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Operation.java index 05f2b59f176..d7b1129dd33 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Operation.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Operation.java @@ -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; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParam.java index f807cb10613..6a9af947295 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParam.java @@ -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 diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Patch.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Patch.java index 25db4fc6b3b..2e788cba624 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Patch.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Patch.java @@ -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 ""; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Search.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Search.java index 7f7b0b3799b..3ba92da4e75 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Search.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Search.java @@ -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; + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Update.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Update.java index 4392f5d6df5..763f44c8956 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Update.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Update.java @@ -44,9 +44,19 @@ public @interface Update { * The return type for this search method. This generally does not need * to be populated for a server implementation, since servers will return * only one resource per class, but generally does need to be populated - * for client implementations. + * for client implementations. */ // 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 ""; + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Validate.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Validate.java index 28b675544da..434700d90bc 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Validate.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Validate.java @@ -52,7 +52,17 @@ 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}. diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ExtensionUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ExtensionUtil.java index 95b08d8ceb8..e14667a1af3 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ExtensionUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ExtensionUtil.java @@ -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); } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/HapiExtensions.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/HapiExtensions.java index f2cf7903407..6dfdca94d4b 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/HapiExtensions.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/HapiExtensions.java @@ -116,7 +116,13 @@ 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 */ private HapiExtensions() { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ParametersUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ParametersUtil.java index 40750ba0bef..5c873c94114 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ParametersUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ParametersUtil.java @@ -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; + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/SchemaBaseValidator.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/SchemaBaseValidator.java index c5b1b9832b3..f71d6e4c4a0 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/SchemaBaseValidator.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/SchemaBaseValidator.java @@ -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; + } + } diff --git a/hapi-fhir-bom/pom.xml b/hapi-fhir-bom/pom.xml index fdb411e91dc..e069462596a 100644 --- a/hapi-fhir-bom/pom.xml +++ b/hapi-fhir-bom/pom.xml @@ -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> diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml index 92bafa40399..175508dea4a 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml +++ b/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml @@ -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> diff --git a/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml index 0c0e9694712..ee4cd6101ed 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml +++ b/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml @@ -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> diff --git a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml index 23fdc156c0e..6735fe66129 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml +++ b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml @@ -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> diff --git a/hapi-fhir-cli/pom.xml b/hapi-fhir-cli/pom.xml index ae67b1dad2f..377ac7c1d20 100644 --- a/hapi-fhir-cli/pom.xml +++ b/hapi-fhir-cli/pom.xml @@ -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> diff --git a/hapi-fhir-client-okhttp/pom.xml b/hapi-fhir-client-okhttp/pom.xml index 52fed03fb71..4656c910de6 100644 --- a/hapi-fhir-client-okhttp/pom.xml +++ b/hapi-fhir-client-okhttp/pom.xml @@ -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> diff --git a/hapi-fhir-client/pom.xml b/hapi-fhir-client/pom.xml index f407f56be2d..d86715e8679 100644 --- a/hapi-fhir-client/pom.xml +++ b/hapi-fhir-client/pom.xml @@ -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> diff --git a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/OperationMethodBinding.java b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/OperationMethodBinding.java index 9e5f4c100da..eb17cbb2241 100644 --- a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/OperationMethodBinding.java +++ b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/OperationMethodBinding.java @@ -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; diff --git a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/SearchMethodBinding.java b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/SearchMethodBinding.java index 1e19ab8abe5..7341e3f6fa4 100644 --- a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/SearchMethodBinding.java +++ b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/SearchMethodBinding.java @@ -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 diff --git a/hapi-fhir-converter/pom.xml b/hapi-fhir-converter/pom.xml index a988e2f45c5..7778e520ac6 100644 --- a/hapi-fhir-converter/pom.xml +++ b/hapi-fhir-converter/pom.xml @@ -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> diff --git a/hapi-fhir-dist/pom.xml b/hapi-fhir-dist/pom.xml index 8ea8d57f03b..95e10af58f2 100644 --- a/hapi-fhir-dist/pom.xml +++ b/hapi-fhir-dist/pom.xml @@ -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> diff --git a/hapi-fhir-docs/pom.xml b/hapi-fhir-docs/pom.xml index 2fcd0c94674..d15eda3ec2e 100644 --- a/hapi-fhir-docs/pom.xml +++ b/hapi-fhir-docs/pom.xml @@ -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> @@ -118,7 +123,7 @@ <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> - </dependencies> + </dependencies> <build> <plugins> diff --git a/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/CreateCompositionAndGenerateDocument.java b/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/CreateCompositionAndGenerateDocument.java new file mode 100644 index 00000000000..546bf051f39 --- /dev/null +++ b/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/CreateCompositionAndGenerateDocument.java @@ -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 + + } +} diff --git a/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/ServletExamples.java b/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/ServletExamples.java index e682175e0fb..9ed097ad9ed 100644 --- a/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/ServletExamples.java +++ b/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/ServletExamples.java @@ -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 { diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2560-openapi-support.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2560-openapi-support.yaml new file mode 100644 index 00000000000..cfd9b53dc35 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2560-openapi-support.yaml @@ -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." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2571-add-delete-to-bundle-builder.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2571-add-delete-to-bundle-builder.yaml new file mode 100644 index 00000000000..6da48fc802c --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2571-add-delete-to-bundle-builder.yaml @@ -0,0 +1,4 @@ +--- +type: fix +issue: 2571 +title: "Added support for deleting resources to BundleBuilder via method `addTransactionDeleteEntry`." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/changes.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/changes.yaml index 928093b5dd3..c9053c1890b 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/changes.yaml +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/changes.yaml @@ -11,6 +11,7 @@ <li>Guava (Core): 30.1-jre -> 30.1.1-jre</li> <li>Jackson (Core): 2.12.1 -> 2.12.3</li> <li>Woodstox (Core): 6.2.3 -> 6.2.5</li> +<li>Apache Jena (Core/RDF): 3.16.0 -> 3.17.0</li> <li>Gson (JPA): 2.8.5 -> 2.8.6</li> <li>Caffeine (JPA): 2.7.0 -> 3.0.1</li> <li>Hibernate (JPA): 5.4.26.Final -> 5.4.30.Final</li> diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/client/examples.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/client/examples.md index e3267ffed28..9a6c405390d 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/client/examples.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/client/examples.md @@ -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}} +``` + + diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties index 580cda79968..dac40d5f791 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties @@ -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 diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/built_in_server_interceptors.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/built_in_server_interceptors.md index f21e80292ec..2f6ecef421d 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/built_in_server_interceptors.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/built_in_server_interceptors.md @@ -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. diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_plain/openapi.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_plain/openapi.md new file mode 100644 index 00000000000..4f8a7f040ff --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_plain/openapi.md @@ -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/ diff --git a/hapi-fhir-jacoco/pom.xml b/hapi-fhir-jacoco/pom.xml index 1d22f0b5e4a..6af26480e26 100644 --- a/hapi-fhir-jacoco/pom.xml +++ b/hapi-fhir-jacoco/pom.xml @@ -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> diff --git a/hapi-fhir-jaxrsserver-base/pom.xml b/hapi-fhir-jaxrsserver-base/pom.xml index b2004c3ad14..697e4479704 100644 --- a/hapi-fhir-jaxrsserver-base/pom.xml +++ b/hapi-fhir-jaxrsserver-base/pom.xml @@ -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> diff --git a/hapi-fhir-jaxrsserver-example/pom.xml b/hapi-fhir-jaxrsserver-example/pom.xml index c3417a5963f..6626a7653dc 100644 --- a/hapi-fhir-jaxrsserver-example/pom.xml +++ b/hapi-fhir-jaxrsserver-example/pom.xml @@ -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> diff --git a/hapi-fhir-jpaserver-api/pom.xml b/hapi-fhir-jpaserver-api/pom.xml index fa591624b35..35aecfe7055 100644 --- a/hapi-fhir-jpaserver-api/pom.xml +++ b/hapi-fhir-jpaserver-api/pom.xml @@ -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> diff --git a/hapi-fhir-jpaserver-base/pom.xml b/hapi-fhir-jpaserver-base/pom.xml index 3fed26d92f7..357583621aa 100644 --- a/hapi-fhir-jpaserver-base/pom.xml +++ b/hapi-fhir-jpaserver-base/pom.xml @@ -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,7 +577,14 @@ <scope>test</scope> </dependency> - <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> </dependency> diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaProvider.java index 2b4cf56a725..4f772f60abf 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaProvider.java @@ -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; } /** diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProvider.java index 3d73fd326d8..307625c456a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProvider.java @@ -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); + } + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaSystemProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaSystemProvider.java index 4bd29532d1f..84751fb6c96 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaSystemProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaSystemProvider.java @@ -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); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaSystemProviderDstu2Plus.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaSystemProviderDstu2Plus.java index a1a3f68f303..9c3b95007d8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaSystemProviderDstu2Plus.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaSystemProviderDstu2Plus.java @@ -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); + } + + } + + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/DiffProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/DiffProvider.java index cc2f98a7797..2f82366aca9 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/DiffProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/DiffProvider.java @@ -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 diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/GraphQLProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/GraphQLProvider.java index f3d27f59a3e..ed68657798c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/GraphQLProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/GraphQLProvider.java @@ -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"); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaCapabilityStatementProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaCapabilityStatementProvider.java index 07c0fd5769c..08c123988cd 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaCapabilityStatementProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaCapabilityStatementProvider.java @@ -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 diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaResourceProviderDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaResourceProviderDstu2.java index 92b926c8f10..53f0ca41d3e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaResourceProviderDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaResourceProviderDstu2.java @@ -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); - } - } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProviderDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProviderDstu2.java index 538e7a9a283..b7c5f32596b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProviderDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProviderDstu2.java @@ -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() diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/JpaResourceProviderDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/JpaResourceProviderDstu3.java index 12d1b58c506..e85c5c23af6 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/JpaResourceProviderDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/JpaResourceProviderDstu3.java @@ -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); - } - } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/JpaSystemProviderDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/JpaSystemProviderDstu3.java index 7cd6746cd48..18f89c03889 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/JpaSystemProviderDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/JpaSystemProviderDstu3.java @@ -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"); - } - } - } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/JpaResourceProviderR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/JpaResourceProviderR4.java index 0c701c6f7e0..034f5956efd 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/JpaResourceProviderR4.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/JpaResourceProviderR4.java @@ -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); - } - } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/JpaSystemProviderR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/JpaSystemProviderR4.java index 662efc47afd..b77a4a05c54 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/JpaSystemProviderR4.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/JpaSystemProviderR4.java @@ -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()); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r5/JpaResourceProviderR5.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r5/JpaResourceProviderR5.java index 6f78a303b44..c31a1ca0f0e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r5/JpaResourceProviderR5.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r5/JpaResourceProviderR5.java @@ -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); - } - } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r5/JpaSystemProviderR5.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r5/JpaSystemProviderR5.java index 6cb29aa50f2..631645099b4 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r5/JpaSystemProviderR5.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r5/JpaSystemProviderR5.java @@ -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()); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java index c8814ae0e4a..13cfc0ce1f9 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java @@ -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")); @@ -2914,7 +2948,7 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test { myResourceReindexingSvc.markAllResourcesForReindexing(); myResourceReindexingSvc.forceReindexingPass(); - runInTransaction(()->{ + runInTransaction(() -> { assertNotEquals(BaseHapiFhirDao.INDEX_STATUS_INDEXING_FAILED, myResourceTableDao.findById(patientIdNull.getIdPartAsLong()).get().getIndexStatus()); assertNotEquals(BaseHapiFhirDao.INDEX_STATUS_INDEXING_FAILED, myResourceTableDao.findById(patientId1.getIdPartAsLong()).get().getIndexStatus()); }); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderDstu2Test.java index d1dbf9d1f30..bcaba0b3547 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderDstu2Test.java @@ -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); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ServerDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ServerDstu3Test.java index db6bde81d17..8a56f262375 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ServerDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ServerDstu3Test.java @@ -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,7 +26,13 @@ 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); + } + /** * See #519 @@ -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()); + } + } + } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/OpenApiInterceptorJpaTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/OpenApiInterceptorJpaTest.java new file mode 100644 index 00000000000..5ca0f688fca --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/OpenApiInterceptorJpaTest.java @@ -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()); + } + } + + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ServerCapabilityStatementProviderJpaR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ServerCapabilityStatementProviderJpaR4Test.java index 9f4d6945e5c..eda0afa7e9c 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ServerCapabilityStatementProviderJpaR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ServerCapabilityStatementProviderJpaR4Test.java @@ -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(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/SystemProviderR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/SystemProviderR4Test.java index 0b7976947cb..759f065ed1f 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/SystemProviderR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/SystemProviderR4Test.java @@ -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); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/lastn/LastNElasticsearchSvcMultipleObservationsIT.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/lastn/LastNElasticsearchSvcMultipleObservationsIT.java index 8a837f94034..ae0bec25252 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/lastn/LastNElasticsearchSvcMultipleObservationsIT.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/lastn/LastNElasticsearchSvcMultipleObservationsIT.java @@ -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 diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/stresstest/GiantTransactionPerfTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/stresstest/GiantTransactionPerfTest.java index fe5f13de317..cd8b745e224 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/stresstest/GiantTransactionPerfTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/stresstest/GiantTransactionPerfTest.java @@ -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; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImplTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImplTest.java index 45e7b561598..57f44b23eda 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImplTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImplTest.java @@ -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 { diff --git a/hapi-fhir-jpaserver-batch/pom.xml b/hapi-fhir-jpaserver-batch/pom.xml index 6b49017689b..d3df9443653 100644 --- a/hapi-fhir-jpaserver-batch/pom.xml +++ b/hapi-fhir-jpaserver-batch/pom.xml @@ -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> diff --git a/hapi-fhir-jpaserver-cql/pom.xml b/hapi-fhir-jpaserver-cql/pom.xml index 9be25f3da9e..8f5317c6e83 100644 --- a/hapi-fhir-jpaserver-cql/pom.xml +++ b/hapi-fhir-jpaserver-cql/pom.xml @@ -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> diff --git a/hapi-fhir-jpaserver-cql/src/test/java/ca/uhn/fhir/cql/common/helper/TranslatorHelperTest.java b/hapi-fhir-jpaserver-cql/src/test/java/ca/uhn/fhir/cql/common/helper/TranslatorHelperTest.java index c4a9393ab75..ed94139951b 100644 --- a/hapi-fhir-jpaserver-cql/src/test/java/ca/uhn/fhir/cql/common/helper/TranslatorHelperTest.java +++ b/hapi-fhir-jpaserver-cql/src/test/java/ca/uhn/fhir/cql/common/helper/TranslatorHelperTest.java @@ -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 @@ -108,8 +108,8 @@ public class TranslatorHelperTest implements CqlProviderTestBase { try { translator = TranslatorHelper.getTranslator(" ", libraryManager, modelManager); fail(); - } catch(NullPointerException e) { - assertNull("translator should be NULL!", translator); + } catch (NullPointerException e) { + assertNull(translator, "translator should be NULL!"); } } @@ -133,9 +133,9 @@ public class TranslatorHelperTest implements CqlProviderTestBase { when(CqlTranslator.fromStream(any(InputStream.class), any(ModelManager.class), any(LibraryManager.class), Matchers.<CqlTranslator.Options>anyVararg())).thenThrow(IOException.class); translator = TranslatorHelper.getTranslator(new ByteArrayInputStream("INVALID-FILENAME".getBytes(StandardCharsets.UTF_8)), libraryManager, modelManager); fail(); - } catch(IllegalArgumentException | IOException e) { + } catch (IllegalArgumentException | IOException e) { assertTrue(e instanceof IllegalArgumentException); - assertNull("translator should be NULL!", translator); + assertNull(translator, "translator should be NULL!"); } } @@ -153,11 +153,11 @@ public class TranslatorHelperTest implements CqlProviderTestBase { Library library = null; try { library = TranslatorHelper.translateLibrary("INVALID-FILENAME", libraryManager, modelManager); - } catch(Exception e) { + } catch (Exception e) { e.printStackTrace(); fail(); } - assertNotNull("library should not be NULL!", library); + assertNotNull(library, "library should not be NULL!"); } @Test @@ -166,8 +166,8 @@ public class TranslatorHelperTest implements CqlProviderTestBase { try { library = TranslatorHelper.readLibrary(new ByteArrayInputStream("INVALID-XML-DOCUMENT".getBytes())); fail(); - } catch(IllegalArgumentException e) { - assertNull("library should be NULL!", library); + } catch (IllegalArgumentException e) { + 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 diff --git a/hapi-fhir-jpaserver-mdm/pom.xml b/hapi-fhir-jpaserver-mdm/pom.xml index 29d7f7a9503..f49c0de98fa 100644 --- a/hapi-fhir-jpaserver-mdm/pom.xml +++ b/hapi-fhir-jpaserver-mdm/pom.xml @@ -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> diff --git a/hapi-fhir-jpaserver-migrate/pom.xml b/hapi-fhir-jpaserver-migrate/pom.xml index be260a03b39..aa549ab2385 100644 --- a/hapi-fhir-jpaserver-migrate/pom.xml +++ b/hapi-fhir-jpaserver-migrate/pom.xml @@ -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> diff --git a/hapi-fhir-jpaserver-model/pom.xml b/hapi-fhir-jpaserver-model/pom.xml index 7a40544f634..189bf7f38da 100644 --- a/hapi-fhir-jpaserver-model/pom.xml +++ b/hapi-fhir-jpaserver-model/pom.xml @@ -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> diff --git a/hapi-fhir-jpaserver-searchparam/pom.xml b/hapi-fhir-jpaserver-searchparam/pom.xml index 5bc11f73872..54c7f179c7b 100755 --- a/hapi-fhir-jpaserver-searchparam/pom.xml +++ b/hapi-fhir-jpaserver-searchparam/pom.xml @@ -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> diff --git a/hapi-fhir-jpaserver-subscription/pom.xml b/hapi-fhir-jpaserver-subscription/pom.xml index 369918d69c8..cd087657fa9 100644 --- a/hapi-fhir-jpaserver-subscription/pom.xml +++ b/hapi-fhir-jpaserver-subscription/pom.xml @@ -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> diff --git a/hapi-fhir-jpaserver-test-utilities/pom.xml b/hapi-fhir-jpaserver-test-utilities/pom.xml index 06b73aa7bac..5a330441e24 100644 --- a/hapi-fhir-jpaserver-test-utilities/pom.xml +++ b/hapi-fhir-jpaserver-test-utilities/pom.xml @@ -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> diff --git a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml index 9cec5a13cf8..407bad660b7 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml +++ b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml @@ -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> diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java index a7e2c06d36a..caa380403cd 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java @@ -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()); } /** diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java index cbaf424d495..fd1b72bec5a 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java @@ -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) diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu3Config.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu3Config.java index bcbdb4a825f..a8d454a92e2 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu3Config.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu3Config.java @@ -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) diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR4Config.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR4Config.java index f1194ba835d..e0db7abf4a9 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR4Config.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR4Config.java @@ -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) diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR5Config.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR5Config.java index f7f4d08b241..5fc6e03cde4 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR5Config.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR5Config.java @@ -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) diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/web.xml b/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/web.xml index 8fb6dbdfa9c..dd383fa7648 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/web.xml +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/web.xml @@ -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> diff --git a/hapi-fhir-server-mdm/pom.xml b/hapi-fhir-server-mdm/pom.xml index 1952a50d819..213863947bf 100644 --- a/hapi-fhir-server-mdm/pom.xml +++ b/hapi-fhir-server-mdm/pom.xml @@ -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> diff --git a/hapi-fhir-server-openapi/pom.xml b/hapi-fhir-server-openapi/pom.xml new file mode 100644 index 00000000000..59d85a5e228 --- /dev/null +++ b/hapi-fhir-server-openapi/pom.xml @@ -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> diff --git a/hapi-fhir-server-openapi/src/main/java/ca/uhn/fhir/rest/openapi/OpenApiInterceptor.java b/hapi-fhir-server-openapi/src/main/java/ca/uhn/fhir/rest/openapi/OpenApiInterceptor.java new file mode 100644 index 00000000000..5d0dfe7c0e3 --- /dev/null +++ b/hapi-fhir-server-openapi/src/main/java/ca/uhn/fhir/rest/openapi/OpenApiInterceptor.java @@ -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; + } + + +} diff --git a/hapi-fhir-server-openapi/src/main/resources/ca/uhn/fhir/rest/openapi/index.css b/hapi-fhir-server-openapi/src/main/resources/ca/uhn/fhir/rest/openapi/index.css new file mode 100644 index 00000000000..a94d230ed3c --- /dev/null +++ b/hapi-fhir-server-openapi/src/main/resources/ca/uhn/fhir/rest/openapi/index.css @@ -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; +} diff --git a/hapi-fhir-server-openapi/src/main/resources/ca/uhn/fhir/rest/openapi/index.html b/hapi-fhir-server-openapi/src/main/resources/ca/uhn/fhir/rest/openapi/index.html new file mode 100644 index 00000000000..029918433e3 --- /dev/null +++ b/hapi-fhir-server-openapi/src/main/resources/ca/uhn/fhir/rest/openapi/index.html @@ -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> diff --git a/hapi-fhir-server-openapi/src/main/resources/ca/uhn/fhir/rest/openapi/raccoon.png b/hapi-fhir-server-openapi/src/main/resources/ca/uhn/fhir/rest/openapi/raccoon.png new file mode 100644 index 00000000000..cf043960680 Binary files /dev/null and b/hapi-fhir-server-openapi/src/main/resources/ca/uhn/fhir/rest/openapi/raccoon.png differ diff --git a/hapi-fhir-server-openapi/src/test/java/ca/uhn/fhir/rest/openapi/OpenApiInterceptorTest.java b/hapi-fhir-server-openapi/src/test/java/ca/uhn/fhir/rest/openapi/OpenApiInterceptorTest.java new file mode 100644 index 00000000000..cf113b46703 --- /dev/null +++ b/hapi-fhir-server-openapi/src/test/java/ca/uhn/fhir/rest/openapi/OpenApiInterceptorTest.java @@ -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(); + } + + + } +} diff --git a/hapi-fhir-server-openapi/src/test/java/ca/uhn/fhir/rest/openapi/OpenApiInterceptorWithAuthorizationInterceptorTest.java b/hapi-fhir-server-openapi/src/test/java/ca/uhn/fhir/rest/openapi/OpenApiInterceptorWithAuthorizationInterceptorTest.java new file mode 100644 index 00000000000..766e79e5d89 --- /dev/null +++ b/hapi-fhir-server-openapi/src/test/java/ca/uhn/fhir/rest/openapi/OpenApiInterceptorWithAuthorizationInterceptorTest.java @@ -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()); + } +} diff --git a/hapi-fhir-server-openapi/src/test/resources/logback-test.xml b/hapi-fhir-server-openapi/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..2e29c0dfe82 --- /dev/null +++ b/hapi-fhir-server-openapi/src/test/resources/logback-test.xml @@ -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> diff --git a/hapi-fhir-server/pom.xml b/hapi-fhir-server/pom.xml index 4fe87a2ee16..b0334c6fbbc 100644 --- a/hapi-fhir-server/pom.xml +++ b/hapi-fhir-server/pom.xml @@ -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> diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/RequestDetails.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/RequestDetails.java index 5f7a405a59c..a2e541fb5fc 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/RequestDetails.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/RequestDetails.java @@ -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() { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/Bindings.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/Bindings.java index a449d2bf88a..f17d1bf04c5 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/Bindings.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/Bindings.java @@ -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; } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/IServerConformanceProvider.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/IServerConformanceProvider.java index 1e7459c9068..854f625e268 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/IServerConformanceProvider.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/IServerConformanceProvider.java @@ -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 diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java index 51439570c7f..60385fd101a 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java @@ -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) */ diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerConfiguration.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerConfiguration.java index 568e3624149..7c7157a5940 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerConfiguration.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerConfiguration.java @@ -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) { - if (nextMethodBinding instanceof OperationMethodBinding) { - OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; - if (myOperationBindingToName.containsKey(methodBinding)) { - continue; - } + List<BaseMethodBinding<?>> methodBindings = resourceToMethods + .values() + .stream().flatMap(t -> t.stream()) + .collect(Collectors.toList()); + if (myGlobalBindings != null) { + methodBindings.addAll(myGlobalBindings); + } - 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); - } else if (nextMethodBinding instanceof SearchMethodBinding) { - SearchMethodBinding methodBinding = (SearchMethodBinding) nextMethodBinding; - if (myNamedSearchMethodBindingToName.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); + ListMultimap<String, OperationMethodBinding> nameToOperationMethodBindings = ArrayListMultimap.create(); + for (BaseMethodBinding<?> nextMethodBinding : methodBindings) { + if (nextMethodBinding instanceof OperationMethodBinding) { + OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; + nameToOperationMethodBindings.put(methodBinding.getName(), methodBinding); + } else if (nextMethodBinding instanceof SearchMethodBinding) { + SearchMethodBinding methodBinding = (SearchMethodBinding) nextMethodBinding; + if (namedSearchMethodBindingToName.containsKey(methodBinding)) { + continue; } + + String name = createNamedQueryName(methodBinding); + ourLog.debug("Detected named query: {}", name); + + 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(); + } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java index 8b83bc55a56..229a7e49802 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java @@ -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(); - } + 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) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseResourceReturningMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseResourceReturningMethodBinding.java index 5c37205e17b..302eabac9ea 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseResourceReturningMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseResourceReturningMethodBinding.java @@ -27,6 +27,8 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.util.ReflectionUtil; +import ca.uhn.fhir.util.ValidateUtil; +import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -79,7 +81,13 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi super(theMethod, theContext, theProvider); Class<?> methodReturnType = theMethod.getReturnType(); - if (Collection.class.isAssignableFrom(methodReturnType)) { + + Set<Class<?>> expectedReturnTypes = provideExpectedReturnTypes(); + if (expectedReturnTypes != null) { + + Validate.isTrue(expectedReturnTypes.contains(methodReturnType), "Unexpected method return type on %s - Allowed: %s", theMethod, expectedReturnTypes); + + } else if (Collection.class.isAssignableFrom(methodReturnType)) { myMethodReturnType = MethodReturnTypeEnum.LIST_OF_RESOURCES; Class<?> collectionType = ReflectionUtil.getGenericCollectionTypeOfMethodReturnType(theMethod); @@ -123,6 +131,13 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi } + /** + * Subclasses may override + */ + protected Set<Class<?>> provideExpectedReturnTypes() { + return null; + } + IBaseResource createBundleFromBundleProvider(IRestfulServer<?> theServer, RequestDetails theRequest, Integer theLimit, String theLinkSelf, Set<Include> theIncludes, IBundleProvider theResult, int theOffset, BundleTypeEnum theBundleType, EncodingEnum theLinkEncoding, String theSearchId) { IVersionSpecificBundleFactory bundleFactory = theServer.getFhirContext().newBundleFactory(); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ConformanceMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ConformanceMethodBinding.java index 02db0a82632..1930d176f5b 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ConformanceMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ConformanceMethodBinding.java @@ -33,6 +33,7 @@ import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.IRestfulServer; import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.SimpleBundleProvider; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; @@ -194,6 +195,7 @@ public class ConformanceMethodBinding extends BaseResourceReturningMethodBinding myCachedResponse.set(conf); myCachedResponseExpires.set(System.currentTimeMillis() + getCacheMillis()); } + return conf; } @@ -230,4 +232,13 @@ public class ConformanceMethodBinding extends BaseResourceReturningMethodBinding return null; } + /** + * Create and return the server's CapabilityStatement + */ + public IBaseConformance provideCapabilityStatement(RestfulServer theServer, RequestDetails theRequest) { + Object[] params = createMethodParams(theRequest); + IBundleProvider resultObj = invokeServer(theServer, theRequest, params); + return (IBaseConformance) resultObj.getResources(0,1).get(0); + } + } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/GraphQLMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/GraphQLMethodBinding.java index 96401f0dd04..35779595f40 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/GraphQLMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/GraphQLMethodBinding.java @@ -42,8 +42,10 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.Writer; import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Set; -public class GraphQLMethodBinding extends BaseMethodBinding<String> { +public class GraphQLMethodBinding extends OperationMethodBinding { private final Integer myIdParamIndex; private final Integer myQueryUrlParamIndex; @@ -51,7 +53,7 @@ public class GraphQLMethodBinding extends BaseMethodBinding<String> { private final RequestTypeEnum myMethodRequestType; public GraphQLMethodBinding(Method theMethod, RequestTypeEnum theMethodRequestType, FhirContext theContext, Object theProvider) { - super(theMethod, theContext, theProvider); + super(null, null, theMethod, theContext, theProvider, true, Constants.OPERATION_NAME_GRAPHQL, null, null, null, null, true); myIdParamIndex = ParameterUtil.findIdParameterIndex(theMethod, theContext); myQueryUrlParamIndex = ParameterUtil.findParamAnnotationIndex(theMethod, GraphQLQueryUrl.class); @@ -71,10 +73,30 @@ public class GraphQLMethodBinding extends BaseMethodBinding<String> { } @Override - public boolean isGlobalMethod() { + public RestOperationTypeEnum getRestOperationType(RequestDetails theRequestDetails) { + return getRestOperationType(); + } + + @Override + protected Set<Class<?>> provideExpectedReturnTypes() { + return Collections.singleton(String.class); + } + + @Override + public boolean isCanOperateAtServerLevel() { return true; } + @Override + public boolean isCanOperateAtTypeLevel() { + return false; + } + + @Override + public boolean isCanOperateAtInstanceLevel() { + return myIdParamIndex != null; + } + @Override public MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequest) { if (Constants.OPERATION_NAME_GRAPHQL.equals(theRequest.getOperation()) && myMethodRequestType.equals(theRequest.getRequestType())) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index e01d1daa859..31a608c2846 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -52,15 +52,19 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; public class MethodUtil { + /** + * Non instantiable + */ + private MethodUtil() { + // nothing + } + public static void extractDescription(SearchParameter theParameter, Annotation[] theAnnotations) { for (Annotation annotation : theAnnotations) { if (annotation instanceof Description) { Description desc = (Description) annotation; - if (isNotBlank(desc.formalDefinition())) { - theParameter.setDescription(desc.formalDefinition()); - } else { - theParameter.setDescription(desc.shortDefinition()); - } + String description = ParametersUtil.extractDescription(desc); + theParameter.setDescription(description); } } } @@ -72,7 +76,7 @@ public class MethodUtil { Class<?>[] parameterTypes = theMethod.getParameterTypes(); int paramIndex = 0; - for (Annotation[] annotations : theMethod.getParameterAnnotations()) { + for (Annotation[] nextParameterAnnotations : theMethod.getParameterAnnotations()) { IParameter param = null; Class<?> declaredParameterType = parameterTypes[paramIndex]; @@ -136,8 +140,8 @@ public class MethodUtil { } else if (parameterType.equals(SearchTotalModeEnum.class)) { param = new SearchTotalModeParameter(); } else { - for (int i = 0; i < annotations.length && param == null; i++) { - Annotation nextAnnotation = annotations[i]; + for (int i = 0; i < nextParameterAnnotations.length && param == null; i++) { + Annotation nextAnnotation = nextParameterAnnotations[i]; if (nextAnnotation instanceof RequiredParam) { SearchParameter parameter = new SearchParameter(); @@ -147,7 +151,7 @@ public class MethodUtil { parameter.setCompositeTypes(((RequiredParam) nextAnnotation).compositeTypes()); parameter.setChainLists(((RequiredParam) nextAnnotation).chainWhitelist(), ((RequiredParam) nextAnnotation).chainBlacklist()); parameter.setType(theContext, parameterType, innerCollectionType, outerCollectionType); - MethodUtil.extractDescription(parameter, annotations); + MethodUtil.extractDescription(parameter, nextParameterAnnotations); param = parameter; } else if (nextAnnotation instanceof OptionalParam) { SearchParameter parameter = new SearchParameter(); @@ -157,7 +161,7 @@ public class MethodUtil { parameter.setCompositeTypes(((OptionalParam) nextAnnotation).compositeTypes()); parameter.setChainLists(((OptionalParam) nextAnnotation).chainWhitelist(), ((OptionalParam) nextAnnotation).chainBlacklist()); parameter.setType(theContext, parameterType, innerCollectionType, outerCollectionType); - MethodUtil.extractDescription(parameter, annotations); + MethodUtil.extractDescription(parameter, nextParameterAnnotations); param = parameter; } else if (nextAnnotation instanceof RawParam) { param = new RawParamsParameter(parameters); @@ -235,7 +239,9 @@ public class MethodUtil { } OperationParam operationParam = (OperationParam) nextAnnotation; - param = new OperationParameter(theContext, op.name(), operationParam); + String description = ParametersUtil.extractDescription(nextParameterAnnotations); + List<String> examples = ParametersUtil.extractExamples(nextParameterAnnotations);; + param = new OperationParameter(theContext, op.name(), operationParam.name(), operationParam.min(), operationParam.max(), description, examples); if (isNotBlank(operationParam.typeName())) { BaseRuntimeElementDefinition<?> elementDefinition = theContext.getElementDefinition(operationParam.typeName()); if (elementDefinition == null) { @@ -254,7 +260,9 @@ public class MethodUtil { throw new ConfigurationException( "Parameter annotated with @" + Validate.class.getSimpleName() + "." + Validate.Mode.class.getSimpleName() + " must be of type " + ValidationModeEnum.class.getName()); } - param = new OperationParameter(theContext, Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_MODE, 0, 1).setConverter(new IOperationParamConverter() { + String description = ParametersUtil.extractDescription(nextParameterAnnotations); + List<String> examples = ParametersUtil.extractExamples(nextParameterAnnotations); + param = new OperationParameter(theContext, Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_MODE, 0, 1, description, examples).setConverter(new IOperationParamConverter() { @Override public Object incomingServer(Object theObject) { if (isNotBlank(theObject.toString())) { @@ -277,7 +285,9 @@ public class MethodUtil { throw new ConfigurationException( "Parameter annotated with @" + Validate.class.getSimpleName() + "." + Validate.Profile.class.getSimpleName() + " must be of type " + String.class.getName()); } - param = new OperationParameter(theContext, Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_PROFILE, 0, 1).setConverter(new IOperationParamConverter() { + String description = ParametersUtil.extractDescription(nextParameterAnnotations); + List<String> examples = ParametersUtil.extractExamples(nextParameterAnnotations); + param = new OperationParameter(theContext, Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_PROFILE, 0, 1, description, examples).setConverter(new IOperationParamConverter() { @Override public Object incomingServer(Object theObject) { return theObject.toString(); @@ -299,7 +309,7 @@ public class MethodUtil { if (param == null) { throw new ConfigurationException( "Parameter #" + ((paramIndex + 1)) + "/" + (parameterTypes.length) + " of method '" + theMethod.getName() + "' on type '" + theMethod.getDeclaringClass().getCanonicalName() - + "' has no recognized FHIR interface parameter annotations. Don't know how to handle this parameter"); + + "' has no recognized FHIR interface parameter nextParameterAnnotations. Don't know how to handle this parameter"); } param.initializeTypes(theMethod, outerCollectionType, innerCollectionType, parameterType); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java index 5af53c33af2..b3dc81fe3f5 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java @@ -22,7 +22,6 @@ package ca.uhn.fhir.rest.server.method; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.model.api.annotation.Description; import ca.uhn.fhir.model.valueset.BundleTypeEnum; import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.rest.annotation.IdParam; @@ -39,6 +38,7 @@ import ca.uhn.fhir.rest.param.ParameterUtil; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; +import ca.uhn.fhir.util.ParametersUtil; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import org.hl7.fhir.instance.model.api.IBase; @@ -64,6 +64,7 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { private final String myName; private final RestOperationTypeEnum myOtherOperationType; private final ReturnTypeEnum myReturnType; + private final String myShortDescription; private boolean myGlobal; private BundleTypeEnum myBundleType; private boolean myCanOperateAtInstanceLevel; @@ -74,24 +75,29 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { private boolean myManualRequestMode; private boolean myManualResponseMode; + /** + * Constructor - This is the constructor that is called when binding a + * standard @Operation method. + */ + public OperationMethodBinding(Class<?> theReturnResourceType, Class<? extends IBaseResource> theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider, + Operation theAnnotation) { + this(theReturnResourceType, theReturnTypeFromRp, theMethod, theContext, theProvider, theAnnotation.idempotent(), theAnnotation.name(), theAnnotation.type(), theAnnotation.typeName(), theAnnotation.returnParameters(), + theAnnotation.bundleType(), theAnnotation.global()); + + myManualRequestMode = theAnnotation.manualRequest(); + myManualResponseMode = theAnnotation.manualResponse(); + } + protected OperationMethodBinding(Class<?> theReturnResourceType, Class<? extends IBaseResource> theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider, boolean theIdempotent, String theOperationName, Class<? extends IBaseResource> theOperationType, String theOperationTypeName, - OperationParam[] theReturnParams, BundleTypeEnum theBundleType) { + OperationParam[] theReturnParams, BundleTypeEnum theBundleType, boolean theGlobal) { super(theReturnResourceType, theMethod, theContext, theProvider); myBundleType = theBundleType; myIdempotent = theIdempotent; - - Description description = theMethod.getAnnotation(Description.class); - if (description != null) { - myDescription = description.formalDefinition(); - if (isBlank(myDescription)) { - myDescription = description.shortDefinition(); - } - } - if (isBlank(myDescription)) { - myDescription = null; - } + myDescription = ParametersUtil.extractDescription(theMethod); + myShortDescription = ParametersUtil.extractShortDefinition(theMethod); + myGlobal = theGlobal; for (Annotation[] nextParamAnnotations : theMethod.getParameterAnnotations()) { for (Annotation nextParam : nextParamAnnotations) { @@ -113,7 +119,7 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { try { if (theReturnTypeFromRp != null) { setResourceName(theContext.getResourceType(theReturnTypeFromRp)); - } else if (Modifier.isAbstract(theOperationType.getModifiers()) == false) { + } else if (theOperationType != null && Modifier.isAbstract(theOperationType.getModifiers()) == false) { setResourceName(theContext.getResourceType(theOperationType)); } else if (isNotBlank(theOperationTypeName)) { setResourceName(theContext.getResourceType(theOperationTypeName)); @@ -133,9 +139,10 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { myIdParamIndex = ParameterUtil.findIdParameterIndex(theMethod, getContext()); if (getResourceName() == null) { myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_SERVER; - myCanOperateAtServerLevel = true; if (myIdParamIndex != null) { myCanOperateAtInstanceLevel = true; + } else { + myCanOperateAtServerLevel = true; } } else if (myIdParamIndex == null) { myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_TYPE; @@ -169,20 +176,16 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { myReturnParams.add(type); } } + + // Parameter Validation + if (myCanOperateAtInstanceLevel && !isGlobalMethod() && getResourceName() == null) { + throw new ConfigurationException("@" + Operation.class.getSimpleName() + " method is an instance level method (it has an @" + IdParam.class.getSimpleName() + " parameter) but is not marked as global() and is not declared in a resource provider: " + theMethod.getName()); + } + } - /** - * Constructor - This is the constructor that is called when binding a - * standard @Operation method. - */ - public OperationMethodBinding(Class<?> theReturnResourceType, Class<? extends IBaseResource> theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider, - Operation theAnnotation) { - this(theReturnResourceType, theReturnTypeFromRp, theMethod, theContext, theProvider, theAnnotation.idempotent(), theAnnotation.name(), theAnnotation.type(), theAnnotation.typeName(), theAnnotation.returnParameters(), - theAnnotation.bundleType()); - - myManualRequestMode = theAnnotation.manualRequest(); - myManualResponseMode = theAnnotation.manualResponse(); - myGlobal = theAnnotation.global(); + public String getShortDescription() { + return myShortDescription; } @Override diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java index fc79dfdb0c5..13a9d115b2d 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java @@ -20,6 +20,7 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; import ca.uhn.fhir.util.FhirTerser; +import ca.uhn.fhir.util.ParametersUtil; import ca.uhn.fhir.util.ReflectionUtil; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.*; @@ -68,17 +69,23 @@ public class OperationParameter implements IParameter { private Class<?> myParameterType; private String myParamType; private SearchParameter mySearchParameterBinding; + private String myDescription; + private List<String> myExampleValues; - public OperationParameter(FhirContext theCtx, String theOperationName, OperationParam theOperationParam) { - this(theCtx, theOperationName, theOperationParam.name(), theOperationParam.min(), theOperationParam.max()); - } - - OperationParameter(FhirContext theCtx, String theOperationName, String theParameterName, int theMin, int theMax) { + OperationParameter(FhirContext theCtx, String theOperationName, String theParameterName, int theMin, int theMax, String theDescription, List<String> theExampleValues) { myOperationName = theOperationName; myName = theParameterName; myMin = theMin; myMax = theMax; myContext = theCtx; + myDescription = theDescription; + + List<String> exampleValues = new ArrayList<>(); + if (theExampleValues != null) { + exampleValues.addAll(theExampleValues); + } + myExampleValues = Collections.unmodifiableList(exampleValues); + } @SuppressWarnings({"rawtypes", "unchecked"}) @@ -438,6 +445,14 @@ public class OperationParameter implements IParameter { } } + public String getDescription() { + return myDescription; + } + + public List<String> getExampleValues() { + return myExampleValues; + } + interface IOperationParamConverter { Object incomingServer(Object theObject); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchMethodBinding.java index cdb569d43e4..233414fcabc 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchMethodBinding.java @@ -35,6 +35,7 @@ import ca.uhn.fhir.rest.param.ParameterUtil; import ca.uhn.fhir.rest.param.QualifierDetails; 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.IAnyResource; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -80,15 +81,7 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding { this.myCompartmentName = StringUtils.defaultIfBlank(search.compartmentName(), null); this.myIdParamIndex = ParameterUtil.findIdParameterIndex(theMethod, getContext()); this.myAllowUnknownParams = search.allowUnknownParams(); - - 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); /* * Only compartment searching methods may have an ID parameter diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ValidateMethodBindingDstu2Plus.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ValidateMethodBindingDstu2Plus.java index ee7cf623818..3568ac9e345 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ValidateMethodBindingDstu2Plus.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ValidateMethodBindingDstu2Plus.java @@ -20,10 +20,12 @@ package ca.uhn.fhir.rest.server.method; * #L% */ +import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; +import ca.uhn.fhir.util.ParametersUtil; import org.hl7.fhir.instance.model.api.IBaseResource; import ca.uhn.fhir.context.FhirContext; @@ -37,9 +39,9 @@ public class ValidateMethodBindingDstu2Plus extends OperationMethodBinding { public ValidateMethodBindingDstu2Plus(Class<?> theReturnResourceType, Class<? extends IBaseResource> theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider, Validate theAnnotation) { - super(theReturnResourceType, theReturnTypeFromRp, theMethod, theContext, theProvider, true, Constants.EXTOP_VALIDATE, theAnnotation.type(), null, new OperationParam[0], BundleTypeEnum.COLLECTION); + super(theReturnResourceType, theReturnTypeFromRp, theMethod, theContext, theProvider, true, Constants.EXTOP_VALIDATE, theAnnotation.type(), null, new OperationParam[0], BundleTypeEnum.COLLECTION, false); - List<IParameter> newParams = new ArrayList<IParameter>(); + List<IParameter> newParams = new ArrayList<>(); int idx = 0; for (IParameter next : getParameters()) { if (next instanceof ResourceParameter) { @@ -48,7 +50,10 @@ public class ValidateMethodBindingDstu2Plus extends OperationMethodBinding { if (String.class.equals(parameterType) || EncodingEnum.class.equals(parameterType)) { newParams.add(next); } else { - OperationParameter parameter = new OperationParameter(theContext, Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_RESOURCE, 0, 1); + Annotation[] parameterAnnotations = theMethod.getParameterAnnotations()[idx]; + String description = ParametersUtil.extractDescription(parameterAnnotations); + List<String> examples = ParametersUtil.extractExamples(parameterAnnotations); + OperationParameter parameter = new OperationParameter(theContext, Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_RESOURCE, 0, 1, description, examples); parameter.initializeTypes(theMethod, null, null, parameterType); newParams.add(parameter); } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ServerCapabilityStatementProvider.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ServerCapabilityStatementProvider.java index 230763bc8a6..d856dfd9598 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ServerCapabilityStatementProvider.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ServerCapabilityStatementProvider.java @@ -27,16 +27,22 @@ import ca.uhn.fhir.rest.server.method.SearchMethodBinding; import ca.uhn.fhir.rest.server.method.SearchParameter; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; +import ca.uhn.fhir.util.ExtensionUtil; import ca.uhn.fhir.util.FhirTerser; +import ca.uhn.fhir.util.HapiExtensions; import com.google.common.collect.TreeMultimap; +import org.apache.commons.text.WordUtils; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseConformance; +import org.hl7.fhir.instance.model.api.IBaseExtension; +import org.hl7.fhir.instance.model.api.IBaseHasExtensions; 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.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.Nonnull; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import java.util.Date; @@ -83,641 +89,758 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; */ public class ServerCapabilityStatementProvider implements IServerConformanceProvider<IBaseConformance> { - public static final boolean DEFAULT_REST_RESOURCE_REV_INCLUDES_ENABLED = true; - private static final Logger ourLog = LoggerFactory.getLogger(ServerCapabilityStatementProvider.class); - private final FhirContext myContext; - private final RestfulServer myServer; - private final ISearchParamRegistry mySearchParamRegistry; - private final RestfulServerConfiguration myServerConfiguration; - private final IValidationSupport myValidationSupport; - private String myPublisher = "Not provided"; - private boolean myRestResourceRevIncludesEnabled = DEFAULT_REST_RESOURCE_REV_INCLUDES_ENABLED; - - /** - * Constructor - */ - public ServerCapabilityStatementProvider(RestfulServer theServer) { - myServer = theServer; - myContext = theServer.getFhirContext(); - mySearchParamRegistry = null; - myServerConfiguration = null; - myValidationSupport = null; - } - - /** - * Constructor - */ - public ServerCapabilityStatementProvider(FhirContext theContext, RestfulServerConfiguration theServerConfiguration) { - myContext = theContext; - myServerConfiguration = theServerConfiguration; - mySearchParamRegistry = null; - myServer = null; - myValidationSupport = null; - } - - /** - * Constructor - */ - public ServerCapabilityStatementProvider(RestfulServer theRestfulServer, ISearchParamRegistry theSearchParamRegistry, IValidationSupport theValidationSupport) { - myContext = theRestfulServer.getFhirContext(); - mySearchParamRegistry = theSearchParamRegistry; - myServer = theRestfulServer; - myServerConfiguration = null; - myValidationSupport = theValidationSupport; - } - - private void checkBindingForSystemOps(FhirTerser theTerser, IBase theRest, Set<String> theSystemOps, BaseMethodBinding<?> theMethodBinding) { - RestOperationTypeEnum restOperationType = theMethodBinding.getRestOperationType(); - if (restOperationType.isSystemLevel()) { - String sysOp = restOperationType.getCode(); - if (theSystemOps.contains(sysOp) == false) { - theSystemOps.add(sysOp); - IBase interaction = theTerser.addElement(theRest, "interaction"); - theTerser.addElement(interaction, "code", sysOp); - } - } - } - - - private String conformanceDate(RestfulServerConfiguration theServerConfiguration) { - IPrimitiveType<Date> buildDate = theServerConfiguration.getConformanceDate(); - if (buildDate != null && buildDate.getValue() != null) { - try { - return buildDate.getValueAsString(); - } catch (DataFormatException e) { - // fall through - } - } - return InstantDt.withCurrentTime().getValueAsString(); - } - - private RestfulServerConfiguration getServerConfiguration() { - if (myServer != null) { - return myServer.createConfiguration(); - } - return myServerConfiguration; - } - - - /** - * Gets the value of the "publisher" that will be placed in the generated conformance statement. As this is a mandatory element, the value should not be null (although this is not enforced). The - * value defaults to "Not provided" but may be set to null, which will cause this element to be omitted. - */ - public String getPublisher() { - return myPublisher; - } - - /** - * Sets the value of the "publisher" that will be placed in the generated conformance statement. As this is a mandatory element, the value should not be null (although this is not enforced). The - * value defaults to "Not provided" but may be set to null, which will cause this element to be omitted. - */ - public void setPublisher(String thePublisher) { - myPublisher = thePublisher; - } - - @Override - @Metadata - public IBaseConformance getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) { - - HttpServletRequest servletRequest = null; - if (theRequestDetails instanceof ServletRequestDetails) { - servletRequest = ((ServletRequestDetails) theRequestDetails).getServletRequest(); - } - - RestfulServerConfiguration configuration = getServerConfiguration(); - Bindings bindings = configuration.provideBindings(); - - IBaseConformance retVal = (IBaseConformance) myContext.getResourceDefinition("CapabilityStatement").newInstance(); - - FhirTerser terser = myContext.newTerser(); - - TreeMultimap<String, String> resourceTypeToSupportedProfiles = getSupportedProfileMultimap(terser); - - terser.addElement(retVal, "id", UUID.randomUUID().toString()); - terser.addElement(retVal, "name", "RestServer"); - terser.addElement(retVal, "publisher", myPublisher); - terser.addElement(retVal, "date", conformanceDate(configuration)); - terser.addElement(retVal, "fhirVersion", myContext.getVersion().getVersion().getFhirVersionString()); - - ServletContext servletContext = (ServletContext) (theRequest == null ? null : theRequest.getAttribute(RestfulServer.SERVLET_CONTEXT_ATTRIBUTE)); - String serverBase = configuration.getServerAddressStrategy().determineServerBase(servletContext, theRequest); - terser.addElement(retVal, "implementation.url", serverBase); - terser.addElement(retVal, "implementation.description", configuration.getImplementationDescription()); - terser.addElement(retVal, "kind", "instance"); - terser.addElement(retVal, "software.name", configuration.getServerName()); - terser.addElement(retVal, "software.version", configuration.getServerVersion()); - if (myContext.isFormatXmlSupported()) { - terser.addElement(retVal, "format", Constants.CT_FHIR_XML_NEW); - terser.addElement(retVal, "format", Constants.FORMAT_XML); - } - if (myContext.isFormatJsonSupported()) { - terser.addElement(retVal, "format", Constants.CT_FHIR_JSON_NEW); - terser.addElement(retVal, "format", Constants.FORMAT_JSON); - } - if (myContext.isFormatRdfSupported()) { - terser.addElement(retVal, "format", Constants.CT_RDF_TURTLE); - terser.addElement(retVal, "format", Constants.FORMAT_TURTLE); - } - terser.addElement(retVal, "status", "active"); - - IBase rest = terser.addElement(retVal, "rest"); - terser.addElement(rest, "mode", "server"); - - Set<String> systemOps = new HashSet<>(); - Set<String> operationNames = new HashSet<>(); - - Map<String, List<BaseMethodBinding<?>>> resourceToMethods = configuration.collectMethodBindings(); - Map<String, Class<? extends IBaseResource>> resourceNameToSharedSupertype = configuration.getNameToSharedSupertype(); - - TreeMultimap<String, String> resourceNameToIncludes = TreeMultimap.create(); - TreeMultimap<String, String> resourceNameToRevIncludes = TreeMultimap.create(); - for (Entry<String, List<BaseMethodBinding<?>>> nextEntry : resourceToMethods.entrySet()) { - String resourceName = nextEntry.getKey(); - for (BaseMethodBinding<?> nextMethod : nextEntry.getValue()) { - if (nextMethod instanceof SearchMethodBinding) { - resourceNameToIncludes.putAll(resourceName, nextMethod.getIncludes()); - resourceNameToRevIncludes.putAll(resourceName, nextMethod.getRevIncludes()); - } - } - - } - - for (Entry<String, List<BaseMethodBinding<?>>> nextEntry : resourceToMethods.entrySet()) { - - String resourceName = nextEntry.getKey(); - if (nextEntry.getKey().isEmpty() == false) { - Set<String> resourceOps = new HashSet<>(); - IBase resource = terser.addElement(rest, "resource"); - - postProcessRestResource(terser, resource, resourceName); - - RuntimeResourceDefinition def; - FhirContext context = configuration.getFhirContext(); - if (resourceNameToSharedSupertype.containsKey(resourceName)) { - def = context.getResourceDefinition(resourceNameToSharedSupertype.get(resourceName)); - } else { - def = context.getResourceDefinition(resourceName); - } - terser.addElement(resource, "type", def.getName()); - terser.addElement(resource, "profile", def.getResourceProfile(serverBase)); - - for (BaseMethodBinding<?> nextMethodBinding : nextEntry.getValue()) { - RestOperationTypeEnum resOpCode = nextMethodBinding.getRestOperationType(); - if (resOpCode.isTypeLevel() || resOpCode.isInstanceLevel()) { - String resOp; - resOp = resOpCode.getCode(); - if (resourceOps.contains(resOp) == false) { - resourceOps.add(resOp); - IBase interaction = terser.addElement(resource, "interaction"); - terser.addElement(interaction, "code", resOp); - } - if (RestOperationTypeEnum.VREAD.equals(resOpCode)) { - // vread implies read - resOp = "read"; - if (resourceOps.contains(resOp) == false) { - resourceOps.add(resOp); - IBase interaction = terser.addElement(resource, "interaction"); - terser.addElement(interaction, "code", resOp); - } - } - } - - if (nextMethodBinding.isSupportsConditional()) { - switch (resOpCode) { - case CREATE: - terser.setElement(resource, "conditionalCreate", "true"); - break; - case DELETE: - if (nextMethodBinding.isSupportsConditionalMultiple()) { - terser.setElement(resource, "conditionalDelete", "multiple"); - } else { - terser.setElement(resource, "conditionalDelete", "single"); - } - break; - case UPDATE: - terser.setElement(resource, "conditionalUpdate", "true"); - break; - case HISTORY_INSTANCE: - case HISTORY_SYSTEM: - case HISTORY_TYPE: - case READ: - case SEARCH_SYSTEM: - case SEARCH_TYPE: - case TRANSACTION: - case VALIDATE: - case VREAD: - case METADATA: - case META_ADD: - case META: - case META_DELETE: - case PATCH: - case BATCH: - case ADD_TAGS: - case DELETE_TAGS: - case GET_TAGS: - case GET_PAGE: - case GRAPHQL_REQUEST: - case EXTENDED_OPERATION_SERVER: - case EXTENDED_OPERATION_TYPE: - case EXTENDED_OPERATION_INSTANCE: - default: - break; - } - } - - checkBindingForSystemOps(terser, rest, systemOps, nextMethodBinding); - - if (nextMethodBinding instanceof SearchMethodBinding) { - SearchMethodBinding methodBinding = (SearchMethodBinding) nextMethodBinding; - if (methodBinding.getQueryName() != null) { - String queryName = bindings.getNamedSearchMethodBindingToName().get(methodBinding); - if (operationNames.add(queryName)) { - IBase operation = terser.addElement(rest, "operation"); - terser.addElement(operation, "name", methodBinding.getQueryName()); - terser.addElement(operation, "definition", (getOperationDefinitionPrefix(theRequestDetails) + "OperationDefinition/" + queryName)); - } - } - } else if (nextMethodBinding instanceof OperationMethodBinding) { - OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; - String opName = bindings.getOperationBindingToName().get(methodBinding); - // Only add each operation (by name) once - if (operationNames.add(opName)) { - IBase operation = terser.addElement(rest, "operation"); - terser.addElement(operation, "name", methodBinding.getName().substring(1)); - terser.addElement(operation, "definition", getOperationDefinitionPrefix(theRequestDetails) + "OperationDefinition/" + opName); - } - } - - } - - - ISearchParamRegistry serverConfiguration; - if (myServerConfiguration != null) { - serverConfiguration = myServerConfiguration; - } else { - serverConfiguration = myServer.createConfiguration(); - } - - /* - * If we have an explicit registry (which will be the case in the JPA server) we use it as priority, - * but also fill in any gaps using params from the server itself. This makes sure we include - * global params like _lastUpdated - */ - Map<String, RuntimeSearchParam> searchParams; - ISearchParamRegistry searchParamRegistry; - if (mySearchParamRegistry != null) { - searchParamRegistry = mySearchParamRegistry; - searchParams = new HashMap<>(mySearchParamRegistry.getActiveSearchParams(resourceName)); - for (Entry<String, RuntimeSearchParam> nextBuiltInSp : serverConfiguration.getActiveSearchParams(resourceName).entrySet()) { - if (nextBuiltInSp.getKey().startsWith("_") && !searchParams.containsKey(nextBuiltInSp.getKey())) { - searchParams.put(nextBuiltInSp.getKey(), nextBuiltInSp.getValue()); - } - } - } else { - searchParamRegistry = serverConfiguration; - searchParams = serverConfiguration.getActiveSearchParams(resourceName); - } - - - for (RuntimeSearchParam next : searchParams.values()) { - IBase searchParam = terser.addElement(resource, "searchParam"); - terser.addElement(searchParam, "name", next.getName()); - terser.addElement(searchParam, "type", next.getParamType().getCode()); - if (isNotBlank(next.getDescription())) { - terser.addElement(searchParam, "documentation", next.getDescription()); - } - - String spUri = next.getUri(); - if (isBlank(spUri) && servletRequest != null) { - String id; - if (next.getId() != null) { - id = next.getId().toUnqualifiedVersionless().getValue(); - } else { - id = resourceName + "-" + next.getName(); - } - spUri = configuration.getServerAddressStrategy().determineServerBase(servletRequest.getServletContext(), servletRequest) + "/" + id; - } - if (isNotBlank(spUri)) { - terser.addElement(searchParam, "definition", spUri); - } - } - - // Add Include to CapabilityStatement.rest.resource - NavigableSet<String> resourceIncludes = resourceNameToIncludes.get(resourceName); - if (resourceIncludes.isEmpty()) { - List<String> includes = searchParams - .values() - .stream() - .filter(t -> t.getParamType() == RestSearchParameterTypeEnum.REFERENCE) - .map(t -> resourceName + ":" + t.getName()) - .sorted() - .collect(Collectors.toList()); - terser.addElement(resource, "searchInclude", "*"); - for (String nextInclude : includes) { - terser.addElement(resource, "searchInclude", nextInclude); - } - } else { - for (String resourceInclude : resourceIncludes) { - terser.addElement(resource, "searchInclude", resourceInclude); - } - } - - // Add RevInclude to CapabilityStatement.rest.resource - if (myRestResourceRevIncludesEnabled) { - NavigableSet<String> resourceRevIncludes = resourceNameToRevIncludes.get(resourceName); - if (resourceRevIncludes.isEmpty()) { - TreeSet<String> revIncludes = new TreeSet<>(); - for (String nextResourceName : resourceToMethods.keySet()) { - if (isBlank(nextResourceName)) { - continue; - } - - for (RuntimeSearchParam t : searchParamRegistry.getActiveSearchParams(nextResourceName).values()) { - if (t.getParamType() == RestSearchParameterTypeEnum.REFERENCE) { - if (isNotBlank(t.getName())) { - boolean appropriateTarget = false; - if (t.getTargets().contains(resourceName) || t.getTargets().isEmpty()) { - appropriateTarget = true; - } - - if (appropriateTarget) { - revIncludes.add(nextResourceName + ":" + t.getName()); - } - } - } - } - } - for (String nextInclude : revIncludes) { - terser.addElement(resource, "searchRevInclude", nextInclude); - } - } else { - for (String resourceInclude : resourceRevIncludes) { - terser.addElement(resource, "searchRevInclude", resourceInclude); - } - } - } - - // Add SupportedProfile to CapabilityStatement.rest.resource - for (String supportedProfile : resourceTypeToSupportedProfiles.get(resourceName)) { - terser.addElement(resource, "supportedProfile", supportedProfile); - } - - } else { - for (BaseMethodBinding<?> nextMethodBinding : nextEntry.getValue()) { - checkBindingForSystemOps(terser, rest, systemOps, nextMethodBinding); - if (nextMethodBinding instanceof OperationMethodBinding) { - OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; - String opName = bindings.getOperationBindingToName().get(methodBinding); - if (operationNames.add(opName)) { - ourLog.debug("Found bound operation: {}", opName); - IBase operation = terser.addElement(rest, "operation"); - terser.addElement(operation, "name", methodBinding.getName().substring(1)); - terser.addElement(operation, "definition", getOperationDefinitionPrefix(theRequestDetails) + "OperationDefinition/" + opName); - } - } - } - } - - postProcessRest(terser, rest); - - } - - postProcess(terser, retVal); - - return retVal; - } - - private TreeMultimap<String, String> getSupportedProfileMultimap(FhirTerser terser) { - TreeMultimap<String, String> resourceTypeToSupportedProfiles = TreeMultimap.create(); - if (myValidationSupport != null) { - List<IBaseResource> allStructureDefinitions = myValidationSupport.fetchAllNonBaseStructureDefinitions(); - if (allStructureDefinitions != null) { - for (IBaseResource next : allStructureDefinitions) { - String kind = terser.getSinglePrimitiveValueOrNull(next, "kind"); - String url = terser.getSinglePrimitiveValueOrNull(next, "url"); - String baseDefinition = defaultString(terser.getSinglePrimitiveValueOrNull(next, "baseDefinition")); - if ("resource".equals(kind) && isNotBlank(url)) { - - // Don't include the base resource definitions in the supported profile list - This isn't helpful - if (baseDefinition.equals("http://hl7.org/fhir/StructureDefinition/DomainResource") || baseDefinition.equals("http://hl7.org/fhir/StructureDefinition/Resource")) { - continue; - } - - String resourceType = terser.getSinglePrimitiveValueOrNull(next, "snapshot.element.path"); - if (isBlank(resourceType)) { - resourceType = terser.getSinglePrimitiveValueOrNull(next, "differential.element.path"); - } - - if (isNotBlank(resourceType)) { - resourceTypeToSupportedProfiles.put(resourceType, url); - } - } - } - } - } - return resourceTypeToSupportedProfiles; - } - - /** - * Subclasses may override - */ - protected void postProcess(FhirTerser theTerser, IBaseConformance theCapabilityStatement) { - // nothing - } - - /** - * Subclasses may override - */ - protected void postProcessRest(FhirTerser theTerser, IBase theRest) { - // nothing - } - - /** - * Subclasses may override - */ - protected void postProcessRestResource(FhirTerser theTerser, IBase theResource, String theResourceName) { - // nothing - } - - protected String getOperationDefinitionPrefix(RequestDetails theRequestDetails) { - if (theRequestDetails == null) { - return ""; - } - return theRequestDetails.getServerBaseForRequest() + "/"; - } - - - @Read(typeName = "OperationDefinition") - public IBaseResource readOperationDefinition(@IdParam IIdType theId, RequestDetails theRequestDetails) { - if (theId == null || theId.hasIdPart() == false) { - throw new ResourceNotFoundException(theId); - } - RestfulServerConfiguration configuration = getServerConfiguration(); - Bindings bindings = configuration.provideBindings(); - - List<OperationMethodBinding> operationBindings = bindings.getOperationNameToBindings().get(theId.getIdPart()); - if (operationBindings != null && !operationBindings.isEmpty()) { - return readOperationDefinitionForOperation(operationBindings); - } - List<SearchMethodBinding> searchBindings = bindings.getSearchNameToBindings().get(theId.getIdPart()); - if (searchBindings != null && !searchBindings.isEmpty()) { - return readOperationDefinitionForNamedSearch(searchBindings); - } - throw new ResourceNotFoundException(theId); - } - - private IBaseResource readOperationDefinitionForNamedSearch(List<SearchMethodBinding> bindings) { - IBaseResource op = myContext.getResourceDefinition("OperationDefinition").newInstance(); - FhirTerser terser = myContext.newTerser(); - - terser.addElement(op, "status", "active"); - terser.addElement(op, "kind", "query"); - terser.addElement(op, "affectsState", "false"); - - terser.addElement(op, "instance", "false"); - - Set<String> inParams = new HashSet<>(); - - String operationCode = null; - for (SearchMethodBinding binding : bindings) { - if (isNotBlank(binding.getDescription())) { - terser.addElement(op, "description", binding.getDescription()); - } - if (isBlank(binding.getResourceProviderResourceName())) { - terser.addElement(op, "system", "true"); - terser.addElement(op, "type", "false"); - } else { - terser.addElement(op, "system", "false"); - terser.addElement(op, "type", "true"); - terser.addElement(op, "resource", binding.getResourceProviderResourceName()); - } - - if (operationCode == null) { - operationCode = binding.getQueryName(); - } - - for (IParameter nextParamUntyped : binding.getParameters()) { - if (nextParamUntyped instanceof SearchParameter) { - SearchParameter nextParam = (SearchParameter) nextParamUntyped; - if (!inParams.add(nextParam.getName())) { - continue; - } - - IBase param = terser.addElement(op, "parameter"); - terser.addElement(param, "use", "in"); - terser.addElement(param, "type", "string"); - terser.addElement(param, "searchType", nextParam.getParamType().getCode()); - terser.addElement(param, "min", nextParam.isRequired() ? "1" : "0"); - terser.addElement(param, "max", "1"); - terser.addElement(param, "name", nextParam.getName()); - } - } - - } - - terser.addElement(op, "code", operationCode); - terser.addElement(op, "name", "Search_" + operationCode); - - return op; - } - - private IBaseResource readOperationDefinitionForOperation(List<OperationMethodBinding> bindings) { - IBaseResource op = myContext.getResourceDefinition("OperationDefinition").newInstance(); - FhirTerser terser = myContext.newTerser(); - - terser.addElement(op, "status", "active"); - terser.addElement(op, "kind", "operation"); - - boolean systemLevel = false; - boolean typeLevel = false; - boolean instanceLevel = false; - boolean affectsState = false; - String description = null; - String code = null; - String name; - - Set<String> resourceNames = new TreeSet<>(); - Set<String> inParams = new HashSet<>(); - Set<String> outParams = new HashSet<>(); - - for (OperationMethodBinding sharedDescription : bindings) { - if (isNotBlank(sharedDescription.getDescription()) && isBlank(description)) { - description = sharedDescription.getDescription(); - } - if (sharedDescription.isCanOperateAtInstanceLevel()) { - instanceLevel = true; - } - if (sharedDescription.isCanOperateAtServerLevel()) { - systemLevel = true; - } - if (sharedDescription.isCanOperateAtTypeLevel()) { - typeLevel = true; - } - if (!sharedDescription.isIdempotent()) { - affectsState |= true; - } - - code = sharedDescription.getName().substring(1); - - if (isNotBlank(sharedDescription.getResourceName())) { - resourceNames.add(sharedDescription.getResourceName()); - } - - for (IParameter nextParamUntyped : sharedDescription.getParameters()) { - if (nextParamUntyped instanceof OperationParameter) { - OperationParameter nextParam = (OperationParameter) nextParamUntyped; - if (!inParams.add(nextParam.getName())) { - continue; - } - IBase param = terser.addElement(op, "parameter"); - terser.addElement(param, "use", "in"); - if (nextParam.getParamType() != null) { - terser.addElement(param, "type", nextParam.getParamType()); - } - if (nextParam.getSearchParamType() != null) { - terser.addElement(param, "searchType", nextParam.getSearchParamType()); - } - terser.addElement(param, "min", Integer.toString(nextParam.getMin())); - terser.addElement(param, "max", (nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax()))); - terser.addElement(param, "name", nextParam.getName()); - } - } - - for (ReturnType nextParam : sharedDescription.getReturnParams()) { - if (!outParams.add(nextParam.getName())) { - continue; - } - IBase param = terser.addElement(op, "parameter"); - terser.addElement(param, "use", "out"); - if (nextParam.getType() != null) { - terser.addElement(param, "type", nextParam.getType()); - } - terser.addElement(param, "min", Integer.toString(nextParam.getMin())); - terser.addElement(param, "max", (nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax()))); - terser.addElement(param, "name", nextParam.getName()); - } - } - - name = "Operation_" + code; - - terser.addElements(op, "resource", resourceNames); - terser.addElement(op, "name", name); - terser.addElement(op, "code", code); - terser.addElement(op, "description", description); - terser.addElement(op, "affectsState", Boolean.toString(affectsState)); - terser.addElement(op, "system", Boolean.toString(systemLevel)); - terser.addElement(op, "type", Boolean.toString(typeLevel)); - terser.addElement(op, "instance", Boolean.toString(instanceLevel)); - - return op; - } - - @Override - public void setRestfulServer(RestfulServer theRestfulServer) { - // ignore - } - - public void setRestResourceRevIncludesEnabled(boolean theRestResourceRevIncludesEnabled) { - myRestResourceRevIncludesEnabled = theRestResourceRevIncludesEnabled; - } + public static final boolean DEFAULT_REST_RESOURCE_REV_INCLUDES_ENABLED = true; + private static final Logger ourLog = LoggerFactory.getLogger(ServerCapabilityStatementProvider.class); + private final FhirContext myContext; + private final RestfulServer myServer; + private final ISearchParamRegistry mySearchParamRegistry; + private final RestfulServerConfiguration myServerConfiguration; + private final IValidationSupport myValidationSupport; + private String myPublisher = "Not provided"; + private boolean myRestResourceRevIncludesEnabled = DEFAULT_REST_RESOURCE_REV_INCLUDES_ENABLED; + + /** + * Constructor + */ + public ServerCapabilityStatementProvider(RestfulServer theServer) { + myServer = theServer; + myContext = theServer.getFhirContext(); + mySearchParamRegistry = null; + myServerConfiguration = null; + myValidationSupport = null; + } + + /** + * Constructor + */ + public ServerCapabilityStatementProvider(FhirContext theContext, RestfulServerConfiguration theServerConfiguration) { + myContext = theContext; + myServerConfiguration = theServerConfiguration; + mySearchParamRegistry = null; + myServer = null; + myValidationSupport = null; + } + + /** + * Constructor + */ + public ServerCapabilityStatementProvider(RestfulServer theRestfulServer, ISearchParamRegistry theSearchParamRegistry, IValidationSupport theValidationSupport) { + myContext = theRestfulServer.getFhirContext(); + mySearchParamRegistry = theSearchParamRegistry; + myServer = theRestfulServer; + myServerConfiguration = null; + myValidationSupport = theValidationSupport; + } + + private void checkBindingForSystemOps(FhirTerser theTerser, IBase theRest, Set<String> theSystemOps, BaseMethodBinding<?> theMethodBinding) { + RestOperationTypeEnum restOperationType = theMethodBinding.getRestOperationType(); + if (restOperationType.isSystemLevel()) { + String sysOp = restOperationType.getCode(); + if (theSystemOps.contains(sysOp) == false) { + theSystemOps.add(sysOp); + IBase interaction = theTerser.addElement(theRest, "interaction"); + theTerser.addElement(interaction, "code", sysOp); + } + } + } + + + private String conformanceDate(RestfulServerConfiguration theServerConfiguration) { + IPrimitiveType<Date> buildDate = theServerConfiguration.getConformanceDate(); + if (buildDate != null && buildDate.getValue() != null) { + try { + return buildDate.getValueAsString(); + } catch (DataFormatException e) { + // fall through + } + } + return InstantDt.withCurrentTime().getValueAsString(); + } + + private RestfulServerConfiguration getServerConfiguration() { + if (myServer != null) { + return myServer.createConfiguration(); + } + return myServerConfiguration; + } + + + /** + * Gets the value of the "publisher" that will be placed in the generated conformance statement. As this is a mandatory element, the value should not be null (although this is not enforced). The + * value defaults to "Not provided" but may be set to null, which will cause this element to be omitted. + */ + public String getPublisher() { + return myPublisher; + } + + /** + * Sets the value of the "publisher" that will be placed in the generated conformance statement. As this is a mandatory element, the value should not be null (although this is not enforced). The + * value defaults to "Not provided" but may be set to null, which will cause this element to be omitted. + */ + public void setPublisher(String thePublisher) { + myPublisher = thePublisher; + } + + @Override + @Metadata + public IBaseConformance getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) { + + HttpServletRequest servletRequest = null; + if (theRequestDetails instanceof ServletRequestDetails) { + servletRequest = ((ServletRequestDetails) theRequestDetails).getServletRequest(); + } + + RestfulServerConfiguration configuration = getServerConfiguration(); + Bindings bindings = configuration.provideBindings(); + + IBaseConformance retVal = (IBaseConformance) myContext.getResourceDefinition("CapabilityStatement").newInstance(); + + FhirTerser terser = myContext.newTerser(); + + TreeMultimap<String, String> resourceTypeToSupportedProfiles = getSupportedProfileMultimap(terser); + + terser.addElement(retVal, "id", UUID.randomUUID().toString()); + terser.addElement(retVal, "name", "RestServer"); + terser.addElement(retVal, "publisher", myPublisher); + terser.addElement(retVal, "date", conformanceDate(configuration)); + terser.addElement(retVal, "fhirVersion", myContext.getVersion().getVersion().getFhirVersionString()); + + ServletContext servletContext = (ServletContext) (theRequest == null ? null : theRequest.getAttribute(RestfulServer.SERVLET_CONTEXT_ATTRIBUTE)); + String serverBase = configuration.getServerAddressStrategy().determineServerBase(servletContext, theRequest); + terser.addElement(retVal, "implementation.url", serverBase); + terser.addElement(retVal, "implementation.description", configuration.getImplementationDescription()); + terser.addElement(retVal, "kind", "instance"); + if (myServer != null && isNotBlank(myServer.getCopyright())) { + terser.addElement(retVal, "copyright", myServer.getCopyright()); + } + terser.addElement(retVal, "software.name", configuration.getServerName()); + terser.addElement(retVal, "software.version", configuration.getServerVersion()); + if (myContext.isFormatXmlSupported()) { + terser.addElement(retVal, "format", Constants.CT_FHIR_XML_NEW); + terser.addElement(retVal, "format", Constants.FORMAT_XML); + } + if (myContext.isFormatJsonSupported()) { + terser.addElement(retVal, "format", Constants.CT_FHIR_JSON_NEW); + terser.addElement(retVal, "format", Constants.FORMAT_JSON); + } + if (myContext.isFormatRdfSupported()) { + terser.addElement(retVal, "format", Constants.CT_RDF_TURTLE); + terser.addElement(retVal, "format", Constants.FORMAT_TURTLE); + } + terser.addElement(retVal, "status", "active"); + + IBase rest = terser.addElement(retVal, "rest"); + terser.addElement(rest, "mode", "server"); + + Set<String> systemOps = new HashSet<>(); + + Map<String, List<BaseMethodBinding<?>>> resourceToMethods = configuration.collectMethodBindings(); + Map<String, Class<? extends IBaseResource>> resourceNameToSharedSupertype = configuration.getNameToSharedSupertype(); + List<BaseMethodBinding<?>> globalMethodBindings = configuration.getGlobalBindings(); + + TreeMultimap<String, String> resourceNameToIncludes = TreeMultimap.create(); + TreeMultimap<String, String> resourceNameToRevIncludes = TreeMultimap.create(); + for (Entry<String, List<BaseMethodBinding<?>>> nextEntry : resourceToMethods.entrySet()) { + String resourceName = nextEntry.getKey(); + for (BaseMethodBinding<?> nextMethod : nextEntry.getValue()) { + if (nextMethod instanceof SearchMethodBinding) { + resourceNameToIncludes.putAll(resourceName, nextMethod.getIncludes()); + resourceNameToRevIncludes.putAll(resourceName, nextMethod.getRevIncludes()); + } + } + + } + + for (Entry<String, List<BaseMethodBinding<?>>> nextEntry : resourceToMethods.entrySet()) { + + Set<String> operationNames = new HashSet<>(); + String resourceName = nextEntry.getKey(); + if (nextEntry.getKey().isEmpty() == false) { + Set<String> resourceOps = new HashSet<>(); + IBase resource = terser.addElement(rest, "resource"); + + postProcessRestResource(terser, resource, resourceName); + + RuntimeResourceDefinition def; + FhirContext context = configuration.getFhirContext(); + if (resourceNameToSharedSupertype.containsKey(resourceName)) { + def = context.getResourceDefinition(resourceNameToSharedSupertype.get(resourceName)); + } else { + def = context.getResourceDefinition(resourceName); + } + terser.addElement(resource, "type", def.getName()); + terser.addElement(resource, "profile", def.getResourceProfile(serverBase)); + + for (BaseMethodBinding<?> nextMethodBinding : nextEntry.getValue()) { + RestOperationTypeEnum resOpCode = nextMethodBinding.getRestOperationType(); + if (resOpCode.isTypeLevel() || resOpCode.isInstanceLevel()) { + String resOp; + resOp = resOpCode.getCode(); + if (resourceOps.contains(resOp) == false) { + resourceOps.add(resOp); + IBase interaction = terser.addElement(resource, "interaction"); + terser.addElement(interaction, "code", resOp); + } + if (RestOperationTypeEnum.VREAD.equals(resOpCode)) { + // vread implies read + resOp = "read"; + if (resourceOps.contains(resOp) == false) { + resourceOps.add(resOp); + IBase interaction = terser.addElement(resource, "interaction"); + terser.addElement(interaction, "code", resOp); + } + } + } + + if (nextMethodBinding.isSupportsConditional()) { + switch (resOpCode) { + case CREATE: + terser.setElement(resource, "conditionalCreate", "true"); + break; + case DELETE: + if (nextMethodBinding.isSupportsConditionalMultiple()) { + terser.setElement(resource, "conditionalDelete", "multiple"); + } else { + terser.setElement(resource, "conditionalDelete", "single"); + } + break; + case UPDATE: + terser.setElement(resource, "conditionalUpdate", "true"); + break; + case HISTORY_INSTANCE: + case HISTORY_SYSTEM: + case HISTORY_TYPE: + case READ: + case SEARCH_SYSTEM: + case SEARCH_TYPE: + case TRANSACTION: + case VALIDATE: + case VREAD: + case METADATA: + case META_ADD: + case META: + case META_DELETE: + case PATCH: + case BATCH: + case ADD_TAGS: + case DELETE_TAGS: + case GET_TAGS: + case GET_PAGE: + case GRAPHQL_REQUEST: + case EXTENDED_OPERATION_SERVER: + case EXTENDED_OPERATION_TYPE: + case EXTENDED_OPERATION_INSTANCE: + default: + break; + } + } + + checkBindingForSystemOps(terser, rest, systemOps, nextMethodBinding); + + // Resource Operations + if (nextMethodBinding instanceof SearchMethodBinding) { + addSearchMethodIfSearchIsNamedQuery(theRequestDetails, bindings, terser, operationNames, resource, (SearchMethodBinding) nextMethodBinding); + } else if (nextMethodBinding instanceof OperationMethodBinding) { + OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; + String opName = bindings.getOperationBindingToId().get(methodBinding); + // Only add each operation (by name) once + if (operationNames.add(opName)) { + IBase operation = terser.addElement(resource, "operation"); + populateOperation(theRequestDetails, terser, methodBinding, opName, operation); + } + } + + } + + // Find any global operations (Operations defines at the system level but with the + // global flag set to true, meaning they apply to all resource types) + if (globalMethodBindings != null) { + Set<String> globalOperationNames = new HashSet<>(); + for (BaseMethodBinding<?> next : globalMethodBindings) { + if (next instanceof OperationMethodBinding) { + OperationMethodBinding methodBinding = (OperationMethodBinding) next; + if (methodBinding.isGlobalMethod()) { + if (methodBinding.isCanOperateAtInstanceLevel() || methodBinding.isCanOperateAtTypeLevel()) { + String opName = bindings.getOperationBindingToId().get(methodBinding); + // Only add each operation (by name) once + if (globalOperationNames.add(opName)) { + IBase operation = terser.addElement(resource, "operation"); + populateOperation(theRequestDetails, terser, methodBinding, opName, operation); + } + } + } + } + } + } + + ISearchParamRegistry serverConfiguration; + if (myServerConfiguration != null) { + serverConfiguration = myServerConfiguration; + } else { + serverConfiguration = myServer.createConfiguration(); + } + + /* + * If we have an explicit registry (which will be the case in the JPA server) we use it as priority, + * but also fill in any gaps using params from the server itself. This makes sure we include + * global params like _lastUpdated + */ + Map<String, RuntimeSearchParam> searchParams; + ISearchParamRegistry searchParamRegistry; + if (mySearchParamRegistry != null) { + searchParamRegistry = mySearchParamRegistry; + searchParams = new HashMap<>(mySearchParamRegistry.getActiveSearchParams(resourceName)); + for (Entry<String, RuntimeSearchParam> nextBuiltInSp : serverConfiguration.getActiveSearchParams(resourceName).entrySet()) { + if (nextBuiltInSp.getKey().startsWith("_") && !searchParams.containsKey(nextBuiltInSp.getKey())) { + searchParams.put(nextBuiltInSp.getKey(), nextBuiltInSp.getValue()); + } + } + } else { + searchParamRegistry = serverConfiguration; + searchParams = serverConfiguration.getActiveSearchParams(resourceName); + } + + + for (RuntimeSearchParam next : searchParams.values()) { + IBase searchParam = terser.addElement(resource, "searchParam"); + terser.addElement(searchParam, "name", next.getName()); + terser.addElement(searchParam, "type", next.getParamType().getCode()); + if (isNotBlank(next.getDescription())) { + terser.addElement(searchParam, "documentation", next.getDescription()); + } + + String spUri = next.getUri(); + if (isBlank(spUri) && servletRequest != null) { + String id; + if (next.getId() != null) { + id = next.getId().toUnqualifiedVersionless().getValue(); + } else { + id = resourceName + "-" + next.getName(); + } + spUri = configuration.getServerAddressStrategy().determineServerBase(servletRequest.getServletContext(), servletRequest) + "/" + id; + } + if (isNotBlank(spUri)) { + terser.addElement(searchParam, "definition", spUri); + } + } + + // Add Include to CapabilityStatement.rest.resource + NavigableSet<String> resourceIncludes = resourceNameToIncludes.get(resourceName); + if (resourceIncludes.isEmpty()) { + List<String> includes = searchParams + .values() + .stream() + .filter(t -> t.getParamType() == RestSearchParameterTypeEnum.REFERENCE) + .map(t -> resourceName + ":" + t.getName()) + .sorted() + .collect(Collectors.toList()); + terser.addElement(resource, "searchInclude", "*"); + for (String nextInclude : includes) { + terser.addElement(resource, "searchInclude", nextInclude); + } + } else { + for (String resourceInclude : resourceIncludes) { + terser.addElement(resource, "searchInclude", resourceInclude); + } + } + + // Add RevInclude to CapabilityStatement.rest.resource + if (myRestResourceRevIncludesEnabled) { + NavigableSet<String> resourceRevIncludes = resourceNameToRevIncludes.get(resourceName); + if (resourceRevIncludes.isEmpty()) { + TreeSet<String> revIncludes = new TreeSet<>(); + for (String nextResourceName : resourceToMethods.keySet()) { + if (isBlank(nextResourceName)) { + continue; + } + + for (RuntimeSearchParam t : searchParamRegistry.getActiveSearchParams(nextResourceName).values()) { + if (t.getParamType() == RestSearchParameterTypeEnum.REFERENCE) { + if (isNotBlank(t.getName())) { + boolean appropriateTarget = false; + if (t.getTargets().contains(resourceName) || t.getTargets().isEmpty()) { + appropriateTarget = true; + } + + if (appropriateTarget) { + revIncludes.add(nextResourceName + ":" + t.getName()); + } + } + } + } + } + for (String nextInclude : revIncludes) { + terser.addElement(resource, "searchRevInclude", nextInclude); + } + } else { + for (String resourceInclude : resourceRevIncludes) { + terser.addElement(resource, "searchRevInclude", resourceInclude); + } + } + } + + // Add SupportedProfile to CapabilityStatement.rest.resource + for (String supportedProfile : resourceTypeToSupportedProfiles.get(resourceName)) { + terser.addElement(resource, "supportedProfile", supportedProfile); + } + + } else { + for (BaseMethodBinding<?> nextMethodBinding : nextEntry.getValue()) { + checkBindingForSystemOps(terser, rest, systemOps, nextMethodBinding); + if (nextMethodBinding instanceof OperationMethodBinding) { + OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; + if (!methodBinding.isGlobalMethod()) { + String opName = bindings.getOperationBindingToId().get(methodBinding); + if (operationNames.add(opName)) { + ourLog.debug("Found bound operation: {}", opName); + IBase operation = terser.addElement(rest, "operation"); + populateOperation(theRequestDetails, terser, methodBinding, opName, operation); + } + } + } else if (nextMethodBinding instanceof SearchMethodBinding) { + addSearchMethodIfSearchIsNamedQuery(theRequestDetails, bindings, terser, operationNames, rest, (SearchMethodBinding) nextMethodBinding); + } + } + } + + } + + + // Find any global operations (Operations defines at the system level but with the + // global flag set to true, meaning they apply to all resource types) + if (globalMethodBindings != null) { + Set<String> globalOperationNames = new HashSet<>(); + for (BaseMethodBinding<?> next : globalMethodBindings) { + if (next instanceof OperationMethodBinding) { + OperationMethodBinding methodBinding = (OperationMethodBinding) next; + if (methodBinding.isGlobalMethod()) { + if (methodBinding.isCanOperateAtServerLevel()) { + String opName = bindings.getOperationBindingToId().get(methodBinding); + // Only add each operation (by name) once + if (globalOperationNames.add(opName)) { + IBase operation = terser.addElement(rest, "operation"); + populateOperation(theRequestDetails, terser, methodBinding, opName, operation); + } + } + } + } + } + } + + + postProcessRest(terser, rest); + postProcess(terser, retVal); + + return retVal; + } + + private void addSearchMethodIfSearchIsNamedQuery(RequestDetails theRequestDetails, Bindings theBindings, FhirTerser theTerser, Set<String> theOperationNamesAlreadyAdded, IBase theElementToAddTo, SearchMethodBinding theSearchMethodBinding) { + if (theSearchMethodBinding.getQueryName() != null) { + String queryName = theBindings.getNamedSearchMethodBindingToName().get(theSearchMethodBinding); + if (theOperationNamesAlreadyAdded.add(queryName)) { + IBase operation = theTerser.addElement(theElementToAddTo, "operation"); + theTerser.addElement(operation, "name", theSearchMethodBinding.getQueryName()); + theTerser.addElement(operation, "definition", (createOperationUrl(theRequestDetails, queryName))); + } + } + } + + private void populateOperation(RequestDetails theRequestDetails, FhirTerser theTerser, OperationMethodBinding theMethodBinding, String theOpName, IBase theOperation) { + String operationName = theMethodBinding.getName().substring(1); + theTerser.addElement(theOperation, "name", operationName); + theTerser.addElement(theOperation, "definition", createOperationUrl(theRequestDetails, theOpName)); + if (isNotBlank(theMethodBinding.getDescription())) { + theTerser.addElement(theOperation, "documentation", theMethodBinding.getDescription()); + } + } + + @Nonnull + private String createOperationUrl(RequestDetails theRequestDetails, String theOpName) { + return getOperationDefinitionPrefix(theRequestDetails) + "OperationDefinition/" + theOpName; + } + + private TreeMultimap<String, String> getSupportedProfileMultimap(FhirTerser terser) { + TreeMultimap<String, String> resourceTypeToSupportedProfiles = TreeMultimap.create(); + if (myValidationSupport != null) { + List<IBaseResource> allStructureDefinitions = myValidationSupport.fetchAllNonBaseStructureDefinitions(); + if (allStructureDefinitions != null) { + for (IBaseResource next : allStructureDefinitions) { + String kind = terser.getSinglePrimitiveValueOrNull(next, "kind"); + String url = terser.getSinglePrimitiveValueOrNull(next, "url"); + String baseDefinition = defaultString(terser.getSinglePrimitiveValueOrNull(next, "baseDefinition")); + if ("resource".equals(kind) && isNotBlank(url)) { + + // Don't include the base resource definitions in the supported profile list - This isn't helpful + if (baseDefinition.equals("http://hl7.org/fhir/StructureDefinition/DomainResource") || baseDefinition.equals("http://hl7.org/fhir/StructureDefinition/Resource")) { + continue; + } + + String resourceType = terser.getSinglePrimitiveValueOrNull(next, "snapshot.element.path"); + if (isBlank(resourceType)) { + resourceType = terser.getSinglePrimitiveValueOrNull(next, "differential.element.path"); + } + + if (isNotBlank(resourceType)) { + resourceTypeToSupportedProfiles.put(resourceType, url); + } + } + } + } + } + return resourceTypeToSupportedProfiles; + } + + /** + * Subclasses may override + */ + protected void postProcess(FhirTerser theTerser, IBaseConformance theCapabilityStatement) { + // nothing + } + + /** + * Subclasses may override + */ + protected void postProcessRest(FhirTerser theTerser, IBase theRest) { + // nothing + } + + /** + * Subclasses may override + */ + protected void postProcessRestResource(FhirTerser theTerser, IBase theResource, String theResourceName) { + // nothing + } + + protected String getOperationDefinitionPrefix(RequestDetails theRequestDetails) { + if (theRequestDetails == null) { + return ""; + } + return theRequestDetails.getServerBaseForRequest() + "/"; + } + + + @Override + @Read(typeName = "OperationDefinition") + public IBaseResource readOperationDefinition(@IdParam IIdType theId, RequestDetails theRequestDetails) { + if (theId == null || theId.hasIdPart() == false) { + throw new ResourceNotFoundException(theId); + } + RestfulServerConfiguration configuration = getServerConfiguration(); + Bindings bindings = configuration.provideBindings(); + + List<OperationMethodBinding> operationBindings = bindings.getOperationIdToBindings().get(theId.getIdPart()); + if (operationBindings != null && !operationBindings.isEmpty()) { + return readOperationDefinitionForOperation(theRequestDetails, bindings, operationBindings); + } + + List<SearchMethodBinding> searchBindings = bindings.getSearchNameToBindings().get(theId.getIdPart()); + if (searchBindings != null && !searchBindings.isEmpty()) { + return readOperationDefinitionForNamedSearch(searchBindings); + } + throw new ResourceNotFoundException(theId); + } + + private IBaseResource readOperationDefinitionForNamedSearch(List<SearchMethodBinding> bindings) { + IBaseResource op = myContext.getResourceDefinition("OperationDefinition").newInstance(); + FhirTerser terser = myContext.newTerser(); + + terser.addElement(op, "status", "active"); + terser.addElement(op, "kind", "query"); + terser.addElement(op, "affectsState", "false"); + + terser.addElement(op, "instance", "false"); + + Set<String> inParams = new HashSet<>(); + + String operationCode = null; + for (SearchMethodBinding binding : bindings) { + if (isNotBlank(binding.getDescription())) { + terser.addElement(op, "description", binding.getDescription()); + } + if (isBlank(binding.getResourceProviderResourceName())) { + terser.addElement(op, "system", "true"); + terser.addElement(op, "type", "false"); + } else { + terser.addElement(op, "system", "false"); + terser.addElement(op, "type", "true"); + terser.addElement(op, "resource", binding.getResourceProviderResourceName()); + } + + if (operationCode == null) { + operationCode = binding.getQueryName(); + } + + for (IParameter nextParamUntyped : binding.getParameters()) { + if (nextParamUntyped instanceof SearchParameter) { + SearchParameter nextParam = (SearchParameter) nextParamUntyped; + if (!inParams.add(nextParam.getName())) { + continue; + } + + IBase param = terser.addElement(op, "parameter"); + terser.addElement(param, "use", "in"); + terser.addElement(param, "type", "string"); + terser.addElement(param, "searchType", nextParam.getParamType().getCode()); + terser.addElement(param, "min", nextParam.isRequired() ? "1" : "0"); + terser.addElement(param, "max", "1"); + terser.addElement(param, "name", nextParam.getName()); + } + } + + } + + terser.addElement(op, "code", operationCode); + + String operationName = WordUtils.capitalize(operationCode); + terser.addElement(op, "name", operationName); + + return op; + } + + private IBaseResource readOperationDefinitionForOperation(RequestDetails theRequestDetails, Bindings theBindings, List<OperationMethodBinding> theOperationMethodBindings) { + IBaseResource op = myContext.getResourceDefinition("OperationDefinition").newInstance(); + FhirTerser terser = myContext.newTerser(); + + terser.addElement(op, "status", "active"); + terser.addElement(op, "kind", "operation"); + + boolean systemLevel = false; + boolean typeLevel = false; + boolean instanceLevel = false; + boolean affectsState = false; + String description = null; + String title = null; + String code = null; + String url = null; + + Set<String> resourceNames = new TreeSet<>(); + Map<String, IBase> inParams = new HashMap<>(); + Map<String, IBase> outParams = new HashMap<>(); + + for (OperationMethodBinding operationMethodBinding : theOperationMethodBindings) { + if (isNotBlank(operationMethodBinding.getDescription()) && isBlank(description)) { + description = operationMethodBinding.getDescription(); + } + if (isNotBlank(operationMethodBinding.getShortDescription()) && isBlank(title)) { + title = operationMethodBinding.getShortDescription(); + } + if (operationMethodBinding.isCanOperateAtInstanceLevel()) { + instanceLevel = true; + } + if (operationMethodBinding.isCanOperateAtServerLevel()) { + systemLevel = true; + } + if (operationMethodBinding.isCanOperateAtTypeLevel()) { + typeLevel = true; + } + if (!operationMethodBinding.isIdempotent()) { + affectsState |= true; + } + + code = operationMethodBinding.getName().substring(1); + + if (isNotBlank(operationMethodBinding.getResourceName())) { + resourceNames.add(operationMethodBinding.getResourceName()); + } + + if (isBlank(url)) { + url = theBindings.getOperationBindingToId().get(operationMethodBinding); + if (isNotBlank(url)) { + url = createOperationUrl(theRequestDetails, url); + } + } + + + for (IParameter nextParamUntyped : operationMethodBinding.getParameters()) { + if (nextParamUntyped instanceof OperationParameter) { + OperationParameter nextParam = (OperationParameter) nextParamUntyped; + + IBase param = inParams.get(nextParam.getName()); + if (param == null){ + param = terser.addElement(op, "parameter"); + inParams.put(nextParam.getName(), param); + } + + IBase existingParam = inParams.get(nextParam.getName()); + if (isNotBlank(nextParam.getDescription()) && terser.getValues(existingParam, "documentation").isEmpty()) { + terser.addElement(existingParam, "documentation", nextParam.getDescription()); + } + + if (nextParam.getParamType() != null) { + String existingType = terser.getSinglePrimitiveValueOrNull(existingParam, "type"); + if (!nextParam.getParamType().equals(existingType)) { + if (existingType == null) { + terser.setElement(existingParam, "type", nextParam.getParamType()); + } else { + terser.setElement(existingParam, "type", "Resource"); + } + } + } + + terser.setElement(param, "use", "in"); + if (nextParam.getSearchParamType() != null) { + terser.setElement(param, "searchType", nextParam.getSearchParamType()); + } + terser.setElement(param, "min", Integer.toString(nextParam.getMin())); + terser.setElement(param, "max", (nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax()))); + terser.setElement(param, "name", nextParam.getName()); + + List<IBaseExtension<?, ?>> existingExampleExtensions = ExtensionUtil.getExtensionsByUrl((IBaseHasExtensions) param, HapiExtensions.EXT_OP_PARAMETER_EXAMPLE_VALUE); + Set<String> existingExamples = existingExampleExtensions + .stream() + .map(t -> t.getValue()) + .filter(t -> t != null) + .map(t -> (IPrimitiveType<?>) t) + .map(t -> t.getValueAsString()) + .collect(Collectors.toSet()); + for (String nextExample : nextParam.getExampleValues()) { + if (!existingExamples.contains(nextExample)) { + ExtensionUtil.addExtension(myContext, param, HapiExtensions.EXT_OP_PARAMETER_EXAMPLE_VALUE, "string", nextExample); + } + } + + } + } + + for (ReturnType nextParam : operationMethodBinding.getReturnParams()) { + if (outParams.containsKey(nextParam.getName())) { + continue; + } + + IBase param = terser.addElement(op, "parameter"); + outParams.put(nextParam.getName(), param); + + terser.addElement(param, "use", "out"); + if (nextParam.getType() != null) { + terser.addElement(param, "type", nextParam.getType()); + } + terser.addElement(param, "min", Integer.toString(nextParam.getMin())); + terser.addElement(param, "max", (nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax()))); + terser.addElement(param, "name", nextParam.getName()); + } + } + String name = WordUtils.capitalize(code); + + terser.addElements(op, "resource", resourceNames); + terser.addElement(op, "name", name); + terser.addElement(op, "url", url); + terser.addElement(op, "code", code); + terser.addElement(op, "description", description); + terser.addElement(op, "title", title); + terser.addElement(op, "affectsState", Boolean.toString(affectsState)); + terser.addElement(op, "system", Boolean.toString(systemLevel)); + terser.addElement(op, "type", Boolean.toString(typeLevel)); + terser.addElement(op, "instance", Boolean.toString(instanceLevel)); + + return op; + } + + @Override + public void setRestfulServer(RestfulServer theRestfulServer) { + // ignore + } + + public void setRestResourceRevIncludesEnabled(boolean theRestResourceRevIncludesEnabled) { + myRestResourceRevIncludesEnabled = theRestResourceRevIncludesEnabled; + } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/servlet/ServletRequestDetails.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/servlet/ServletRequestDetails.java index fa5ac4e117c..8d90901b141 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/servlet/ServletRequestDetails.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/servlet/ServletRequestDetails.java @@ -49,11 +49,32 @@ public class ServletRequestDetails extends RequestDetails { private HttpServletRequest myServletRequest; private HttpServletResponse myServletResponse; + /** + * Constructor for testing only + */ + public ServletRequestDetails() { + this((IInterceptorBroadcaster)null); + } + + /** + * Constructor + */ public ServletRequestDetails(IInterceptorBroadcaster theInterceptorBroadcaster) { super(theInterceptorBroadcaster); setResponse(new ServletRestfulResponse(this)); } + /** + * Copy constructor + */ + public ServletRequestDetails(ServletRequestDetails theRequestDetails) { + super(theRequestDetails); + + myServer = theRequestDetails.getServer(); + myServletRequest = theRequestDetails.getServletRequest(); + myServletResponse = theRequestDetails.getServletResponse(); + } + @Override protected byte[] getByteStreamRequestContents() { try { diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/SearchMethodBindingTest.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/SearchMethodBindingTest.java index c2f3add03e5..0a79048cb87 100644 --- a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/SearchMethodBindingTest.java +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/SearchMethodBindingTest.java @@ -104,7 +104,7 @@ public class SearchMethodBindingTest { when(requestDetails.getParameters()).thenReturn(params); when(requestDetails.getUnqualifiedToQualifiedNames()).thenAnswer(t -> { - RequestDetails rd = new ServletRequestDetails(null); + RequestDetails rd = new ServletRequestDetails(); rd.setParameters(params); return rd.getUnqualifiedToQualifiedNames(); }); diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml index 19eedc6bfd2..db762d268b7 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml @@ -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> diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml index 656d16080d7..d0ef59739a9 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml @@ -5,7 +5,7 @@ <parent> <groupId>ca.uhn.hapi.fhir</groupId> <artifactId>hapi-fhir-spring-boot-samples</artifactId> - <version>5.4.0-PRE7-SNAPSHOT</version> + <version>5.4.0-PRE8-SNAPSHOT</version> </parent> <artifactId>hapi-fhir-spring-boot-sample-client-apache</artifactId> diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml index 0f9be244025..c7dba5afe65 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml @@ -5,7 +5,7 @@ <parent> <groupId>ca.uhn.hapi.fhir</groupId> <artifactId>hapi-fhir-spring-boot-samples</artifactId> - <version>5.4.0-PRE7-SNAPSHOT</version> + <version>5.4.0-PRE8-SNAPSHOT</version> </parent> <artifactId>hapi-fhir-spring-boot-sample-client-okhttp</artifactId> diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml index 9d5b6483f92..fd0b2400bd2 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml @@ -5,7 +5,7 @@ <parent> <groupId>ca.uhn.hapi.fhir</groupId> <artifactId>hapi-fhir-spring-boot-samples</artifactId> - <version>5.4.0-PRE7-SNAPSHOT</version> + <version>5.4.0-PRE8-SNAPSHOT</version> </parent> <artifactId>hapi-fhir-spring-boot-sample-server-jersey</artifactId> diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml index a57c5e80e1b..23dc4f4cc96 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml @@ -5,7 +5,7 @@ <parent> <groupId>ca.uhn.hapi.fhir</groupId> <artifactId>hapi-fhir-spring-boot</artifactId> - <version>5.4.0-PRE7-SNAPSHOT</version> + <version>5.4.0-PRE8-SNAPSHOT</version> </parent> <artifactId>hapi-fhir-spring-boot-samples</artifactId> diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml index 3f90ae6bbcc..37bb5fbff89 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml @@ -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> diff --git a/hapi-fhir-spring-boot/pom.xml b/hapi-fhir-spring-boot/pom.xml index ff3fe4a179a..e3050e67351 100644 --- a/hapi-fhir-spring-boot/pom.xml +++ b/hapi-fhir-spring-boot/pom.xml @@ -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> diff --git a/hapi-fhir-structures-dstu2.1/pom.xml b/hapi-fhir-structures-dstu2.1/pom.xml index 61d4ea265db..143a1fd9ffc 100644 --- a/hapi-fhir-structures-dstu2.1/pom.xml +++ b/hapi-fhir-structures-dstu2.1/pom.xml @@ -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> diff --git a/hapi-fhir-structures-dstu2.1/src/main/java/org/hl7/fhir/dstu2016may/hapi/rest/server/ServerConformanceProvider.java b/hapi-fhir-structures-dstu2.1/src/main/java/org/hl7/fhir/dstu2016may/hapi/rest/server/ServerConformanceProvider.java index 31045cdab6d..d9122d3f71e 100644 --- a/hapi-fhir-structures-dstu2.1/src/main/java/org/hl7/fhir/dstu2016may/hapi/rest/server/ServerConformanceProvider.java +++ b/hapi-fhir-structures-dstu2.1/src/main/java/org/hl7/fhir/dstu2016may/hapi/rest/server/ServerConformanceProvider.java @@ -275,7 +275,7 @@ public class ServerConformanceProvider extends BaseServerCapabilityStatementProv handleSearchMethodBinding(rest, resource, resourceName, def, includes, (SearchMethodBinding) nextMethodBinding, theRequestDetails); } else if (nextMethodBinding instanceof OperationMethodBinding) { OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; - String opName = bindings.getOperationBindingToName().get(methodBinding); + String opName = bindings.getOperationBindingToId().get(methodBinding); if (operationNames.add(opName)) { // Only add each operation (by name) once rest.addOperation().setName(methodBinding.getName().substring(1)).setDefinition(new Reference("OperationDefinition/" + opName)); @@ -310,7 +310,7 @@ public class ServerConformanceProvider extends BaseServerCapabilityStatementProv checkBindingForSystemOps(rest, systemOps, nextMethodBinding); if (nextMethodBinding instanceof OperationMethodBinding) { OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; - String opName = bindings.getOperationBindingToName().get(methodBinding); + String opName = bindings.getOperationBindingToId().get(methodBinding); if (operationNames.add(opName)) { ourLog.debug("Found bound operation: {}", opName); rest.addOperation().setName(methodBinding.getName().substring(1)).setDefinition(new Reference("OperationDefinition/" + opName)); @@ -419,7 +419,7 @@ public class ServerConformanceProvider extends BaseServerCapabilityStatementProv RestfulServerConfiguration serverConfiguration = getServerConfiguration(theRequestDetails); Bindings bindings = serverConfiguration.provideBindings(); - List<OperationMethodBinding> sharedDescriptions = bindings.getOperationNameToBindings().get(theId.getIdPart()); + List<OperationMethodBinding> sharedDescriptions = bindings.getOperationIdToBindings().get(theId.getIdPart()); if (sharedDescriptions == null || sharedDescriptions.isEmpty()) { throw new ResourceNotFoundException(theId); } diff --git a/hapi-fhir-structures-dstu2.1/src/test/java/ca/uhn/fhir/rest/server/OperationServerDstu2_1Test.java b/hapi-fhir-structures-dstu2.1/src/test/java/ca/uhn/fhir/rest/server/OperationServerDstu2_1Test.java index f4bcddd6bbb..93970cb0ca7 100644 --- a/hapi-fhir-structures-dstu2.1/src/test/java/ca/uhn/fhir/rest/server/OperationServerDstu2_1Test.java +++ b/hapi-fhir-structures-dstu2.1/src/test/java/ca/uhn/fhir/rest/server/OperationServerDstu2_1Test.java @@ -98,7 +98,7 @@ public class OperationServerDstu2_1Test { */ @Test public void testOperationDefinition() { - OperationDefinition def = myFhirClient.read().resource(OperationDefinition.class).withId("OperationDefinition/Patient--OP_TYPE").execute(); + OperationDefinition def = myFhirClient.read().resource(OperationDefinition.class).withId("OperationDefinition/Patient-t-OP_TYPE").execute(); ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(def)); diff --git a/hapi-fhir-structures-dstu2.1/src/test/java/ca/uhn/fhir/rest/server/OperationServerWithSearchParamTypesDstu2_1Test.java b/hapi-fhir-structures-dstu2.1/src/test/java/ca/uhn/fhir/rest/server/OperationServerWithSearchParamTypesDstu2_1Test.java index 8dbb9066dd8..63be245658c 100644 --- a/hapi-fhir-structures-dstu2.1/src/test/java/ca/uhn/fhir/rest/server/OperationServerWithSearchParamTypesDstu2_1Test.java +++ b/hapi-fhir-structures-dstu2.1/src/test/java/ca/uhn/fhir/rest/server/OperationServerWithSearchParamTypesDstu2_1Test.java @@ -193,7 +193,7 @@ public class OperationServerWithSearchParamTypesDstu2_1Test { /* * Check the operation definitions themselves */ - OperationDefinition andListDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient--andlist"), createRequestDetails(rs)); + OperationDefinition andListDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient-t-andlist"), createRequestDetails(rs)); String def = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(andListDef); ourLog.info(def); //@formatter:off @@ -209,7 +209,7 @@ public class OperationServerWithSearchParamTypesDstu2_1Test { )); //@formatter:on - andListDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient--andlist-withnomax"), createRequestDetails(rs)); + andListDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient-t-andlist-withnomax"), createRequestDetails(rs)); def = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(andListDef); ourLog.info(def); //@formatter:off @@ -225,7 +225,7 @@ public class OperationServerWithSearchParamTypesDstu2_1Test { )); //@formatter:on - OperationDefinition orListDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient--orlist"), createRequestDetails(rs)); + OperationDefinition orListDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient-t-orlist"), createRequestDetails(rs)); def = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(orListDef); ourLog.info(def); //@formatter:off @@ -241,7 +241,7 @@ public class OperationServerWithSearchParamTypesDstu2_1Test { )); //@formatter:on - orListDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient--orlist-withnomax"), createRequestDetails(rs)); + orListDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient-t-orlist-withnomax"), createRequestDetails(rs)); def = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(orListDef); ourLog.info(def); //@formatter:off @@ -505,7 +505,7 @@ public class OperationServerWithSearchParamTypesDstu2_1Test { private RequestDetails createRequestDetails(RestfulServer theServer) { - ServletRequestDetails retVal = new ServletRequestDetails(null); + ServletRequestDetails retVal = new ServletRequestDetails(); retVal.setServer(theServer); return retVal; } diff --git a/hapi-fhir-structures-dstu2/pom.xml b/hapi-fhir-structures-dstu2/pom.xml index 9359dc2e980..eba9cfc2801 100644 --- a/hapi-fhir-structures-dstu2/pom.xml +++ b/hapi-fhir-structures-dstu2/pom.xml @@ -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> diff --git a/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/ServerConformanceProvider.java b/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/ServerConformanceProvider.java index cf51eda513a..79d0b6fc093 100644 --- a/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/ServerConformanceProvider.java +++ b/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/ServerConformanceProvider.java @@ -271,7 +271,7 @@ public class ServerConformanceProvider extends BaseServerCapabilityStatementProv handleSearchMethodBinding(resource, def, includes, (SearchMethodBinding) nextMethodBinding, theRequestDetails); } else if (nextMethodBinding instanceof OperationMethodBinding) { OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; - String opName = bindings.getOperationBindingToName().get(methodBinding); + String opName = bindings.getOperationBindingToId().get(methodBinding); if (operationNames.add(opName)) { // Only add each operation (by name) once rest.addOperation().setName(methodBinding.getName().substring(1)).getDefinition().setReference("OperationDefinition/" + opName); @@ -306,7 +306,7 @@ public class ServerConformanceProvider extends BaseServerCapabilityStatementProv checkBindingForSystemOps(rest, systemOps, nextMethodBinding); if (nextMethodBinding instanceof OperationMethodBinding) { OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; - String opName = bindings.getOperationBindingToName().get(methodBinding); + String opName = bindings.getOperationBindingToId().get(methodBinding); if (operationNames.add(opName)) { rest.addOperation().setName(methodBinding.getName().substring(1)).getDefinition().setReference("OperationDefinition/" + opName); } @@ -415,7 +415,7 @@ public class ServerConformanceProvider extends BaseServerCapabilityStatementProv RestfulServerConfiguration serverConfiguration = getServerConfiguration(theRequestDetails); Bindings bindings = serverConfiguration.provideBindings(); - List<OperationMethodBinding> sharedDescriptions = bindings.getOperationNameToBindings().get(theId.getIdPart()); + List<OperationMethodBinding> sharedDescriptions = bindings.getOperationIdToBindings().get(theId.getIdPart()); if (sharedDescriptions == null || sharedDescriptions.isEmpty()) { throw new ResourceNotFoundException(theId); } @@ -449,10 +449,10 @@ public class ServerConformanceProvider extends BaseServerCapabilityStatementProv for (IParameter nextParamUntyped : sharedDescription.getParameters()) { if (nextParamUntyped instanceof OperationParameter) { OperationParameter nextParam = (OperationParameter) nextParamUntyped; - Parameter param = op.addParameter(); if (!inParams.add(nextParam.getName())) { continue; } + Parameter param = op.addParameter(); param.setUse(OperationParameterUseEnum.IN); if (nextParam.getParamType() != null) { param.setType(nextParam.getParamType()); diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/parser/XmlParserDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/parser/XmlParserDstu2Test.java index d2681d3376b..40ada003db2 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/parser/XmlParserDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/parser/XmlParserDstu2Test.java @@ -1134,7 +1134,7 @@ public class XmlParserDstu2Test { ourLog.info(string); parsed = parser.parseResource(Composition.class, string); - assertEquals(2, parsed.getContained().getContainedResources().size()); + assertEquals(3, parsed.getContained().getContainedResources().size()); } /** @@ -1159,7 +1159,7 @@ public class XmlParserDstu2Test { ourLog.info(string); parsed = parser.parseResource(Composition.class, string); - assertEquals(2, parsed.getContained().getContainedResources().size()); + assertEquals(3, parsed.getContained().getContainedResources().size()); } @Test diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationDuplicateServerDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationDuplicateServerDstu2Test.java index b042b34659b..db916678122 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationDuplicateServerDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationDuplicateServerDstu2Test.java @@ -50,14 +50,14 @@ public class OperationDuplicateServerDstu2Test { ourLog.info(response); Conformance resp = ourCtx.newXmlParser().parseResource(Conformance.class, response); - assertEquals(3, resp.getRest().get(0).getOperation().size()); + assertEquals(1, resp.getRest().get(0).getOperation().size()); assertEquals("myoperation", resp.getRest().get(0).getOperation().get(0).getName()); - assertEquals("OperationDefinition/-s-myoperation", resp.getRest().get(0).getOperation().get(0).getDefinition().getReference().getValue()); + assertEquals("OperationDefinition/OrganizationPatient-ts-myoperation", resp.getRest().get(0).getOperation().get(0).getDefinition().getReference().getValue()); } // OperationDefinition { - HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/OperationDefinition/-s-myoperation?_pretty=true"); + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/OperationDefinition/OrganizationPatient-ts-myoperation?_pretty=true"); HttpResponse status = ourClient.execute(httpGet); assertEquals(200, status.getStatusLine().getStatusCode()); @@ -69,25 +69,7 @@ public class OperationDuplicateServerDstu2Test { assertEquals(true, resp.getSystemElement().getValue().booleanValue()); assertEquals("myoperation", resp.getCode()); assertEquals(true, resp.getIdempotent().booleanValue()); - assertEquals(0, resp.getType().size()); - assertEquals(1, resp.getParameter().size()); - } - // OperationDefinition - { - HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/OperationDefinition/Organization--myoperation?_pretty=true"); - HttpResponse status = ourClient.execute(httpGet); - - assertEquals(200, status.getStatusLine().getStatusCode()); - String response = IOUtils.toString(status.getEntity().getContent()); - IOUtils.closeQuietly(status.getEntity().getContent()); - ourLog.info(response); - - OperationDefinition resp = ourCtx.newXmlParser().parseResource(OperationDefinition.class, response); - assertEquals(false, resp.getSystemElement().getValue().booleanValue()); - assertEquals("myoperation", resp.getCode()); - assertEquals(true, resp.getIdempotent().booleanValue()); - assertEquals(1, resp.getType().size()); - assertEquals("Organization", resp.getType().get(0).getValue()); + assertEquals(2, resp.getType().size()); assertEquals(1, resp.getParameter().size()); } } diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationServerDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationServerDstu2Test.java index b6f51ed2243..4dfa2813cc4 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationServerDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationServerDstu2Test.java @@ -105,7 +105,7 @@ public class OperationServerDstu2Test { List<String> opNames = toOpNames(ops); assertThat(opNames, containsInRelativeOrder("OP_TYPE")); - assertEquals("OperationDefinition/Patient--OP_TYPE", ops.get(opNames.indexOf("OP_TYPE")).getDefinition().getReference().getValue()); + assertEquals("OperationDefinition/Patient-t-OP_TYPE", ops.get(opNames.indexOf("OP_TYPE")).getDefinition().getReference().getValue()); } /** @@ -113,7 +113,7 @@ public class OperationServerDstu2Test { */ @Test public void testOperationDefinition() { - OperationDefinition def = myFhirClient.read().resource(OperationDefinition.class).withId("OperationDefinition/Patient--OP_TYPE").execute(); + OperationDefinition def = myFhirClient.read().resource(OperationDefinition.class).withId("OperationDefinition/Patient-t-OP_TYPE").execute(); ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(def)); diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationServerWithSearchParamTypesDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationServerWithSearchParamTypesDstu2Test.java index 837db498fd1..5b7ff389697 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationServerWithSearchParamTypesDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationServerWithSearchParamTypesDstu2Test.java @@ -182,7 +182,7 @@ public class OperationServerWithSearchParamTypesDstu2Test { /* * Check the operation definitions themselves */ - OperationDefinition andListDef = sc.readOperationDefinition(new IdDt("OperationDefinition/Patient--andlist"), createRequestDetails(rs)); + OperationDefinition andListDef = sc.readOperationDefinition(new IdDt("OperationDefinition/Patient-t-andlist"), createRequestDetails(rs)); String def = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(andListDef); ourLog.info(def); //@formatter:off @@ -418,7 +418,7 @@ public class OperationServerWithSearchParamTypesDstu2Test { } private RequestDetails createRequestDetails(RestfulServer theServer) { - ServletRequestDetails retVal = new ServletRequestDetails(null); + ServletRequestDetails retVal = new ServletRequestDetails(); retVal.setServer(theServer); return retVal; } diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/ServerConformanceProviderDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/ServerConformanceProviderDstu2Test.java index a0be908d5ce..1d6bf3f7930 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/ServerConformanceProviderDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/ServerConformanceProviderDstu2Test.java @@ -25,7 +25,21 @@ import ca.uhn.fhir.model.primitive.DateDt; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.StringDt; import ca.uhn.fhir.model.primitive.UriDt; -import ca.uhn.fhir.rest.annotation.*; +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.IncludeParam; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.annotation.OptionalParam; +import ca.uhn.fhir.rest.annotation.Read; +import ca.uhn.fhir.rest.annotation.RequiredParam; +import ca.uhn.fhir.rest.annotation.ResourceParam; +import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.annotation.Update; +import ca.uhn.fhir.rest.annotation.Validate; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.RequestDetails; @@ -67,9 +81,9 @@ import static org.mockito.Mockito.when; public class ServerConformanceProviderDstu2Test { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServerConformanceProviderDstu2Test.class); private static FhirContext ourCtx; private static FhirValidator ourValidator; - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServerConformanceProviderDstu2Test.class); static { ourCtx = FhirContext.forDstu2(); @@ -85,7 +99,7 @@ public class ServerConformanceProviderDstu2Test { ValidationResult result = ourValidator.validateWithResult(theOpDef); String outcome = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(result.toOperationOutcome()); ourLog.info("Outcome: {}", outcome); - + assertTrue(result.isSuccessful(), outcome); } @@ -97,6 +111,7 @@ public class ServerConformanceProviderDstu2Test { when(req.getContextPath()).thenReturn("/FhirStorm"); return req; } + private ServletConfig createServletConfig() { ServletConfig sc = mock(ServletConfig.class); when(sc.getServletContext()).thenReturn(null); @@ -255,7 +270,9 @@ public class ServerConformanceProviderDstu2Test { assertNull(res.getConditionalUpdate()); } - /** See #379 */ + /** + * See #379 + */ @Test public void testOperationAcrossMultipleTypes() throws Exception { RestfulServer rs = new RestfulServer(ourCtx); @@ -271,55 +288,26 @@ public class ServerConformanceProviderDstu2Test { String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); ourLog.info(conf); - assertEquals(4, conformance.getRest().get(0).getOperation().size()); + assertEquals(2, conformance.getRest().get(0).getOperation().size()); List<String> operationNames = toOperationNames(conformance.getRest().get(0).getOperation()); - assertThat(operationNames, containsInAnyOrder("someOp", "validate", "someOp", "validate")); - + assertThat(operationNames, containsInAnyOrder("someOp", "validate")); + List<String> operationIdParts = toOperationIdParts(conformance.getRest().get(0).getOperation()); - assertThat(operationIdParts, containsInAnyOrder("Patient-i-someOp","Encounter-i-someOp","Patient-i-validate","Encounter-i-validate")); - + assertThat(operationIdParts, containsInAnyOrder("EncounterPatient-i-someOp", "EncounterPatient-i-validate")); + { - OperationDefinition opDef = sc.readOperationDefinition(new IdDt("OperationDefinition/Patient-i-someOp"), createRequestDetails(rs)); - validate(opDef); - - Set<String> types = toStrings(opDef.getType()); - assertEquals("someOp", opDef.getCode()); - assertEquals(true, opDef.getInstance()); - assertEquals(false, opDef.getSystem()); - assertThat(types, containsInAnyOrder("Patient")); - assertEquals(2, opDef.getParameter().size()); - assertEquals("someOpParam1", opDef.getParameter().get(0).getName()); - assertEquals("date", opDef.getParameter().get(0).getType()); - assertEquals("someOpParam2", opDef.getParameter().get(1).getName()); - assertEquals("Patient", opDef.getParameter().get(1).getType()); - } - { - OperationDefinition opDef = sc.readOperationDefinition(new IdDt("OperationDefinition/Encounter-i-someOp"), createRequestDetails(rs)); + OperationDefinition opDef = sc.readOperationDefinition(new IdDt("OperationDefinition/EncounterPatient-i-someOp"), createRequestDetails(rs)); validate(opDef); Set<String> types = toStrings(opDef.getType()); assertEquals("someOp", opDef.getCode()); assertEquals(true, opDef.getInstance()); assertEquals(false, opDef.getSystem()); - assertThat(types, containsInAnyOrder("Encounter")); + assertThat(types, containsInAnyOrder("Patient", "Encounter")); assertEquals(2, opDef.getParameter().size()); assertEquals("someOpParam1", opDef.getParameter().get(0).getName()); assertEquals("date", opDef.getParameter().get(0).getType()); assertEquals("someOpParam2", opDef.getParameter().get(1).getName()); - assertEquals("Encounter", opDef.getParameter().get(1).getType()); - } - { - OperationDefinition opDef = sc.readOperationDefinition(new IdDt("OperationDefinition/Patient-i-validate"), createRequestDetails(rs)); - validate(opDef); - - Set<String> types = toStrings(opDef.getType()); - assertEquals("validate", opDef.getCode()); - assertEquals(true, opDef.getInstance()); - assertEquals(false, opDef.getSystem()); - assertThat(types, containsInAnyOrder("Patient")); - assertEquals(1, opDef.getParameter().size()); - assertEquals("resource", opDef.getParameter().get(0).getName()); - assertEquals("Patient", opDef.getParameter().get(0).getType()); } } @@ -344,45 +332,9 @@ public class ServerConformanceProviderDstu2Test { } - @Test - public void testOperationOnNoTypes() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); - rs.setProviders(new PlainProviderWithExtendedOperationOnNoType()); - - ServerConformanceProvider sc = new ServerConformanceProvider(rs) { - @Override - public Conformance getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) { - return super.getServerConformance(theRequest, theRequestDetails); - } - }; - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - Conformance sconf = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - assertEquals("OperationDefinition/-is-plain", sconf.getRest().get(0).getOperation().get(0).getDefinition().getReference().getValue()); - - OperationDefinition opDef = sc.readOperationDefinition(new IdDt("OperationDefinition/-is-plain"), createRequestDetails(rs)); - validate(opDef); - - assertEquals("plain", opDef.getCode()); - assertEquals(true, opDef.getIdempotent().booleanValue()); - assertEquals(3, opDef.getParameter().size()); - assertEquals("start", opDef.getParameter().get(0).getName()); - assertEquals("in", opDef.getParameter().get(0).getUse()); - assertEquals("0", opDef.getParameter().get(0).getMinElement().getValueAsString()); - assertEquals("date", opDef.getParameter().get(0).getTypeElement().getValueAsString()); - - assertEquals("out1", opDef.getParameter().get(2).getName()); - assertEquals("out", opDef.getParameter().get(2).getUse()); - assertEquals("1", opDef.getParameter().get(2).getMinElement().getValueAsString()); - assertEquals("2", opDef.getParameter().get(2).getMaxElement().getValueAsString()); - assertEquals("string", opDef.getParameter().get(2).getTypeElement().getValueAsString()); - } - @Test public void testProviderForSmart() throws ServletException { - + RestfulServer rs = new RestfulServer(ourCtx); rs.createConfiguration(); rs.setProviders(new ProviderWithRequiredAndOptional()); @@ -391,28 +343,28 @@ public class ServerConformanceProviderDstu2Test { @Override public Conformance getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) { Conformance conformance = super.getServerConformance(theRequest, theRequestDetails); - ExtensionDt extensionDt = new ExtensionDt(); - ExtensionDt extensionDtToken = new ExtensionDt(); - ExtensionDt extensionDtAuthorize = new ExtensionDt(); - Rest rest = conformance.getRestFirstRep(); - RestSecurity restSecurity = rest.getSecurity(); - - conformance.setAcceptUnknown(UnknownContentCodeEnum.UNKNOWN_ELEMENTS_AND_EXTENSIONS); - restSecurity.addService(RestfulSecurityServiceEnum.SMART_ON_FHIR); - restSecurity.getServiceFirstRep().setText("OAuth2 using SMART-on-FHIR profile (see http://docs.smarthealthit.org)"); - extensionDt.setUrl("http://fhir-registry.smarthealthit.org/StructureDefinition/oauth-uris"); - extensionDtToken.setUrl("token"); - extensionDtToken.setValue(new UriDt("https://SERVERNAME/token")); - extensionDtAuthorize.setUrl("authorize"); - extensionDtAuthorize.setValue(new UriDt("https://SERVERNAME/authorize")); - extensionDt.addUndeclaredExtension(extensionDtToken); + ExtensionDt extensionDt = new ExtensionDt(); + ExtensionDt extensionDtToken = new ExtensionDt(); + ExtensionDt extensionDtAuthorize = new ExtensionDt(); + Rest rest = conformance.getRestFirstRep(); + RestSecurity restSecurity = rest.getSecurity(); + + conformance.setAcceptUnknown(UnknownContentCodeEnum.UNKNOWN_ELEMENTS_AND_EXTENSIONS); + restSecurity.addService(RestfulSecurityServiceEnum.SMART_ON_FHIR); + restSecurity.getServiceFirstRep().setText("OAuth2 using SMART-on-FHIR profile (see http://docs.smarthealthit.org)"); + extensionDt.setUrl("http://fhir-registry.smarthealthit.org/StructureDefinition/oauth-uris"); + extensionDtToken.setUrl("token"); + extensionDtToken.setValue(new UriDt("https://SERVERNAME/token")); + extensionDtAuthorize.setUrl("authorize"); + extensionDtAuthorize.setValue(new UriDt("https://SERVERNAME/authorize")); + extensionDt.addUndeclaredExtension(extensionDtToken); extensionDt.addUndeclaredExtension(extensionDtAuthorize); restSecurity.addUndeclaredExtension(extensionDt); - + return conformance; } }; - + rs.init(createServletConfig()); Conformance conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); @@ -561,8 +513,8 @@ public class ServerConformanceProviderDstu2Test { ourLog.info(conf); } - - + + /** * See #286 */ @@ -595,7 +547,7 @@ public class ServerConformanceProviderDstu2Test { ourLog.info(conf); RestResource resource = findRestResource(conformance, "Patient"); - + RestResourceSearchParam param = resource.getSearchParam().get(0); assertEquals("bar", param.getChain().get(0).getValue()); assertEquals("foo", param.getChain().get(1).getValue()); @@ -696,7 +648,7 @@ public class ServerConformanceProviderDstu2Test { ValidationResult result = ourCtx.newValidator().validateWithResult(conformance); assertTrue(result.isSuccessful(), result.getMessages().toString()); } - + private List<String> toOperationIdParts(List<RestOperation> theOperation) { ArrayList<String> retVal = Lists.newArrayList(); for (RestOperation next : theOperation) { @@ -721,9 +673,10 @@ public class ServerConformanceProviderDstu2Test { return retVal; } - @AfterAll - public static void afterClassClearContext() { - TestUtil.clearAllStaticFieldsForUnitTest(); + private RequestDetails createRequestDetails(RestfulServer theServer) { + ServletRequestDetails retVal = new ServletRequestDetails(); + retVal.setServer(theServer); + return retVal; } public static class ConditionalProvider implements IResourceProvider { @@ -779,7 +732,7 @@ public class ServerConformanceProviderDstu2Test { @Operation(name = "someOp") public IBundleProvider everything(javax.servlet.http.HttpServletRequest theServletRequest, @IdParam IdDt theId, - @OperationParam(name = "someOpParam1") DateDt theStart, @OperationParam(name = "someOpParam2") Encounter theEnd) { + @OperationParam(name = "someOpParam1") DateDt theStart, @OperationParam(name = "someOpParam2") Encounter theEnd) { return null; } @@ -799,7 +752,7 @@ public class ServerConformanceProviderDstu2Test { @Operation(name = "someOp") public IBundleProvider everything(javax.servlet.http.HttpServletRequest theServletRequest, @IdParam IdDt theId, - @OperationParam(name = "someOpParam1") DateDt theStart, @OperationParam(name = "someOpParam2") Patient theEnd) { + @OperationParam(name = "someOpParam1") DateDt theStart, @OperationParam(name = "someOpParam2") Patient theEnd) { return null; } @@ -841,7 +794,7 @@ public class ServerConformanceProviderDstu2Test { public static class PlainProviderWithExtendedOperationOnNoType { - @Operation(name = "plain", idempotent = true, returnParameters = { @OperationParam(min = 1, max = 2, name = "out1", type = StringDt.class) }) + @Operation(name = "plain", idempotent = true, returnParameters = {@OperationParam(min = 1, max = 2, name = "out1", type = StringDt.class)}) public IBundleProvider everything(javax.servlet.http.HttpServletRequest theServletRequest, @IdParam ca.uhn.fhir.model.primitive.IdDt theId, @OperationParam(name = "start") DateDt theStart, @OperationParam(name = "end") DateDt theEnd) { return null; } @@ -867,7 +820,7 @@ public class ServerConformanceProviderDstu2Test { @Description(shortDefinition = "This is a search for stuff!") @Search public List<DiagnosticReport> findDiagnosticReportsByPatient(@RequiredParam(name = DiagnosticReport.SP_SUBJECT + '.' + Patient.SP_IDENTIFIER) IdentifierDt thePatientId, @OptionalParam(name = DiagnosticReport.SP_CODE) TokenOrListParam theNames, - @OptionalParam(name = DiagnosticReport.SP_DATE) DateRangeParam theDateRange, @IncludeParam(allow = { "DiagnosticReport.result" }) Set<Include> theIncludes) throws Exception { + @OptionalParam(name = DiagnosticReport.SP_DATE) DateRangeParam theDateRange, @IncludeParam(allow = {"DiagnosticReport.result"}) Set<Include> theIncludes) throws Exception { return null; } @@ -895,7 +848,7 @@ public class ServerConformanceProviderDstu2Test { } @Search(type = Patient.class) - public Patient findPatient2(@Description(shortDefinition = "All patients linked to the given patient") @OptionalParam(name = "link", targetTypes = { Patient.class }) ReferenceAndListParam theLink) { + public Patient findPatient2(@Description(shortDefinition = "All patients linked to the given patient") @OptionalParam(name = "link", targetTypes = {Patient.class}) ReferenceAndListParam theLink) { return null; } @@ -905,8 +858,8 @@ public class ServerConformanceProviderDstu2Test { @Search(type = Patient.class) public Patient findPatient1( - @Description(shortDefinition = "The organization at which this person is a patient") - @RequiredParam(name = Patient.SP_ORGANIZATION, chainWhitelist= {"foo", "bar"}) + @Description(shortDefinition = "The organization at which this person is a patient") + @RequiredParam(name = Patient.SP_ORGANIZATION, chainWhitelist = {"foo", "bar"}) ReferenceAndListParam theIdentifier) { return null; } @@ -918,8 +871,8 @@ public class ServerConformanceProviderDstu2Test { @Search(type = Patient.class) public Patient findPatient1( @Description(shortDefinition = "The organization at which this person is a patient") - @RequiredParam(name = "organization.foo") ReferenceAndListParam theFoo, - @RequiredParam(name = "organization.bar") ReferenceAndListParam theBar, + @RequiredParam(name = "organization.foo") ReferenceAndListParam theFoo, + @RequiredParam(name = "organization.bar") ReferenceAndListParam theBar, @RequiredParam(name = "organization.baz.bob") ReferenceAndListParam theBazbob) { return null; } @@ -966,10 +919,9 @@ public class ServerConformanceProviderDstu2Test { } - private RequestDetails createRequestDetails(RestfulServer theServer) { - ServletRequestDetails retVal = new ServletRequestDetails(null); - retVal.setServer(theServer); - return retVal; + @AfterAll + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); } diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/InterceptorUserDataMapDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/InterceptorUserDataMapDstu2Test.java index 6f46fa68e66..d543ecfa2d1 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/InterceptorUserDataMapDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/InterceptorUserDataMapDstu2Test.java @@ -42,6 +42,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; @@ -78,7 +79,7 @@ public class InterceptorUserDataMapDstu2Test { @BeforeEach public void beforePurgeMap() { myMap = null; - myMapCheckMethods = new LinkedHashSet<>(); + myMapCheckMethods = Collections.synchronizedSet(new LinkedHashSet<>()); } diff --git a/hapi-fhir-structures-dstu3/pom.xml b/hapi-fhir-structures-dstu3/pom.xml index 5a825a62517..f9d795a9068 100644 --- a/hapi-fhir-structures-dstu3/pom.xml +++ b/hapi-fhir-structures-dstu3/pom.xml @@ -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> diff --git a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProvider.java b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProvider.java index c01caf7a9db..1deaa202fc2 100644 --- a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProvider.java +++ b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProvider.java @@ -318,7 +318,7 @@ public class ServerCapabilityStatementProvider extends BaseServerCapabilityState } } else if (nextMethodBinding instanceof OperationMethodBinding) { OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; - String opName = bindings.getOperationBindingToName().get(methodBinding); + String opName = bindings.getOperationBindingToId().get(methodBinding); if (operationNames.add(opName)) { // Only add each operation (by name) once rest.addOperation().setName(methodBinding.getName().substring(1)).setDefinition(new Reference("OperationDefinition/" + opName)); @@ -353,7 +353,7 @@ public class ServerCapabilityStatementProvider extends BaseServerCapabilityState checkBindingForSystemOps(rest, systemOps, nextMethodBinding); if (nextMethodBinding instanceof OperationMethodBinding) { OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; - String opName = bindings.getOperationBindingToName().get(methodBinding); + String opName = bindings.getOperationBindingToId().get(methodBinding); if (operationNames.add(opName)) { ourLog.debug("Found bound operation: {}", opName); rest.addOperation().setName(methodBinding.getName().substring(1)).setDefinition(new Reference("OperationDefinition/" + opName)); @@ -466,7 +466,7 @@ public class ServerCapabilityStatementProvider extends BaseServerCapabilityState RestfulServerConfiguration serverConfiguration = getServerConfiguration(theRequestDetails); Bindings bindings = serverConfiguration.provideBindings(); - List<OperationMethodBinding> operationBindings = bindings.getOperationNameToBindings().get(theId.getIdPart()); + List<OperationMethodBinding> operationBindings = bindings.getOperationIdToBindings().get(theId.getIdPart()); if (operationBindings != null && !operationBindings.isEmpty()) { return readOperationDefinitionForOperation(operationBindings); } @@ -572,10 +572,10 @@ public class ServerCapabilityStatementProvider extends BaseServerCapabilityState for (IParameter nextParamUntyped : sharedDescription.getParameters()) { if (nextParamUntyped instanceof OperationParameter) { OperationParameter nextParam = (OperationParameter) nextParamUntyped; - OperationDefinitionParameterComponent param = op.addParameter(); if (!inParams.add(nextParam.getName())) { continue; } + OperationDefinitionParameterComponent param = op.addParameter(); param.setUse(OperationParameterUse.IN); if (nextParam.getParamType() != null) { param.setType(nextParam.getParamType()); diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/OperationServerDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/OperationServerDstu3Test.java index 63c2f2befd8..b5382da1516 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/OperationServerDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/OperationServerDstu3Test.java @@ -102,7 +102,7 @@ public class OperationServerDstu3Test { */ @Test public void testOperationDefinition() { - OperationDefinition def = myFhirClient.read().resource(OperationDefinition.class).withId("OperationDefinition/Patient--OP_TYPE").execute(); + OperationDefinition def = myFhirClient.read().resource(OperationDefinition.class).withId("OperationDefinition/Patient-t-OP_TYPE").execute(); ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(def)); @@ -565,14 +565,6 @@ public class OperationServerDstu3Test { } - public static void main(String[] theValue) { - Parameters p = new Parameters(); - p.addParameter().setName("start").setValue(new DateTimeType("2001-01-02")); - p.addParameter().setName("end").setValue(new DateTimeType("2015-07-10")); - String inParamsStr = FhirContext.forDstu2().newXmlParser().encodeResourceToString(p); - ourLog.info(inParamsStr.replace("\"", "\\\"")); - } - public static class PatientProvider implements IResourceProvider { @Override diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/OperationServerWithSearchParamTypesDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/OperationServerWithSearchParamTypesDstu3Test.java index 6393450147a..3842f6ba77c 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/OperationServerWithSearchParamTypesDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/OperationServerWithSearchParamTypesDstu3Test.java @@ -193,7 +193,7 @@ public class OperationServerWithSearchParamTypesDstu3Test { /* * Check the operation definitions themselves */ - OperationDefinition andListDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient--andlist"), createRequestDetails(rs)); + OperationDefinition andListDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient-t-andlist"), createRequestDetails(rs)); String def = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(andListDef); ourLog.info(def); //@formatter:off @@ -209,7 +209,7 @@ public class OperationServerWithSearchParamTypesDstu3Test { )); //@formatter:on - andListDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient--andlist-withnomax"), createRequestDetails(rs)); + andListDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient-t-andlist-withnomax"), createRequestDetails(rs)); def = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(andListDef); ourLog.info(def); //@formatter:off @@ -225,7 +225,7 @@ public class OperationServerWithSearchParamTypesDstu3Test { )); //@formatter:on - OperationDefinition orListDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient--orlist"), createRequestDetails(rs)); + OperationDefinition orListDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient-t-orlist"), createRequestDetails(rs)); def = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(orListDef); ourLog.info(def); //@formatter:off @@ -241,7 +241,7 @@ public class OperationServerWithSearchParamTypesDstu3Test { )); //@formatter:on - orListDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient--orlist-withnomax"), createRequestDetails(rs)); + orListDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient-t-orlist-withnomax"), createRequestDetails(rs)); def = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(orListDef); ourLog.info(def); //@formatter:off @@ -490,7 +490,8 @@ public class OperationServerWithSearchParamTypesDstu3Test { @Operation(name = "$orlist-withnomax", idempotent = true) public Parameters orlistWithNoMax( //@formatter:off - @OperationParam(name="valstr") List<StringOrListParam> theValStr, + @OperationParam(name="valstr" + ) List<StringOrListParam> theValStr, @OperationParam(name="valtok") List<TokenOrListParam> theValTok //@formatter:on ) { @@ -504,7 +505,7 @@ public class OperationServerWithSearchParamTypesDstu3Test { } private RequestDetails createRequestDetails(RestfulServer theServer) { - ServletRequestDetails retVal = new ServletRequestDetails(null); + ServletRequestDetails retVal = new ServletRequestDetails(); retVal.setServer(theServer); return retVal; } diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/util/XmlUtilDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/util/XmlUtilDstu3Test.java index 6cdee55aaf6..92cea006890 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/util/XmlUtilDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/util/XmlUtilDstu3Test.java @@ -81,9 +81,11 @@ public class XmlUtilDstu3Test { public void testEncodePrettyPrint() throws IOException, SAXException, TransformerException { String input = "<document><tag id=\"1\"/></document>"; Document parsed = XmlUtil.parseDocument(input); - String output = XmlUtil.encodeDocument(parsed, true); + String output = XmlUtil.encodeDocument(parsed, true) + .replace("\r\n", "\n") + .replaceAll("^ *", ""); assertEquals("<document>\n" + - " <tag id=\"1\"/>\n" + + "<tag id=\"1\"/>\n" + "</document>\n", output); } diff --git a/hapi-fhir-structures-dstu3/src/test/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProviderDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProviderDstu3Test.java index 7de10333d3f..2074d19d5f2 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProviderDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProviderDstu3Test.java @@ -338,55 +338,26 @@ public class ServerCapabilityStatementProviderDstu3Test { String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); ourLog.info(conf); - assertEquals(4, conformance.getRest().get(0).getOperation().size()); + assertEquals(2, conformance.getRest().get(0).getOperation().size()); List<String> operationNames = toOperationNames(conformance.getRest().get(0).getOperation()); - assertThat(operationNames, containsInAnyOrder("someOp", "validate", "someOp", "validate")); + assertThat(operationNames, containsInAnyOrder("someOp", "validate")); List<String> operationIdParts = toOperationIdParts(conformance.getRest().get(0).getOperation()); - assertThat(operationIdParts, containsInAnyOrder("Patient-i-someOp", "Encounter-i-someOp", "Patient-i-validate", "Encounter-i-validate")); + assertThat(operationIdParts, containsInAnyOrder("EncounterPatient-i-someOp", "EncounterPatient-i-validate")); { - OperationDefinition opDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-someOp"), createRequestDetails(rs)); + OperationDefinition opDef = sc.readOperationDefinition(new IdType("OperationDefinition/EncounterPatient-i-someOp"), createRequestDetails(rs)); validate(opDef); ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); Set<String> types = toStrings(opDef.getResource()); assertEquals("someOp", opDef.getCode()); assertEquals(true, opDef.getInstance()); assertEquals(false, opDef.getSystem()); - assertThat(types, containsInAnyOrder("Patient")); + assertThat(types, containsInAnyOrder("Patient", "Encounter")); assertEquals(2, opDef.getParameter().size()); assertEquals("someOpParam1", opDef.getParameter().get(0).getName()); assertEquals("date", opDef.getParameter().get(0).getType()); assertEquals("someOpParam2", opDef.getParameter().get(1).getName()); - assertEquals("Patient", opDef.getParameter().get(1).getType()); - } - { - OperationDefinition opDef = sc.readOperationDefinition(new IdType("OperationDefinition/Encounter-i-someOp"), createRequestDetails(rs)); - validate(opDef); - ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); - Set<String> types = toStrings(opDef.getResource()); - assertEquals("someOp", opDef.getCode()); - assertEquals(true, opDef.getInstance()); - assertEquals(false, opDef.getSystem()); - assertThat(types, containsInAnyOrder("Encounter")); - assertEquals(2, opDef.getParameter().size()); - assertEquals("someOpParam1", opDef.getParameter().get(0).getName()); - assertEquals("date", opDef.getParameter().get(0).getType()); - assertEquals("someOpParam2", opDef.getParameter().get(1).getName()); - assertEquals("Encounter", opDef.getParameter().get(1).getType()); - } - { - OperationDefinition opDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-validate"), createRequestDetails(rs)); - validate(opDef); - ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); - Set<String> types = toStrings(opDef.getResource()); - assertEquals("validate", opDef.getCode()); - assertEquals(true, opDef.getInstance()); - assertEquals(false, opDef.getSystem()); - assertThat(types, containsInAnyOrder("Patient")); - assertEquals(1, opDef.getParameter().size()); - assertEquals("resource", opDef.getParameter().get(0).getName()); - assertEquals("Patient", opDef.getParameter().get(0).getType()); } } @@ -410,45 +381,6 @@ public class ServerCapabilityStatementProviderDstu3Test { } - @Test - public void testOperationOnNoTypes() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); - rs.setProviders(new PlainProviderWithExtendedOperationOnNoType()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider() { - @Override - public CapabilityStatement getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) { - return super.getServerConformance(theRequest, createRequestDetails(rs)); - } - }; - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - OperationDefinition opDef = sc.readOperationDefinition(new IdType("OperationDefinition/-is-plain"), createRequestDetails(rs)); - validate(opDef); - - assertEquals("plain", opDef.getCode()); - assertEquals(true, opDef.getIdempotent()); - assertEquals(3, opDef.getParameter().size()); - - assertTrue(opDef.getParameter().get(0).hasName()); - assertEquals("start", opDef.getParameter().get(0).getName()); - assertEquals("in", opDef.getParameter().get(0).getUse().toCode()); - assertEquals("0", opDef.getParameter().get(0).getMinElement().getValueAsString()); - assertEquals("date", opDef.getParameter().get(0).getTypeElement().getValueAsString()); - - assertEquals("out1", opDef.getParameter().get(2).getName()); - assertEquals("out", opDef.getParameter().get(2).getUse().toCode()); - assertEquals("1", opDef.getParameter().get(2).getMinElement().getValueAsString()); - assertEquals("2", opDef.getParameter().get(2).getMaxElement().getValueAsString()); - assertEquals("string", opDef.getParameter().get(2).getTypeElement().getValueAsString()); - - assertThat(opDef.getSystem(), is(true)); - assertThat(opDef.getType(), is(false)); - assertThat(opDef.getInstance(), is(true)); - } - @Test public void testProviderWithRequiredAndOptional() throws Exception { @@ -726,6 +658,7 @@ public class ServerCapabilityStatementProviderDstu3Test { } @Test + @Disabled // This was working incorrectly previously public void testSystemLevelNamedQueryWithParameters() throws Exception { RestfulServer rs = new RestfulServer(ourCtx); rs.setProviders(new NamedQueryPlainProvider()); @@ -1272,7 +1205,7 @@ public class ServerCapabilityStatementProviderDstu3Test { public static class PatientTripleSub extends PatientSubSub {} private RequestDetails createRequestDetails(RestfulServer theServer) { - ServletRequestDetails retVal = new ServletRequestDetails(null); + ServletRequestDetails retVal = new ServletRequestDetails(); retVal.setServer(theServer); return retVal; } diff --git a/hapi-fhir-structures-hl7org-dstu2/pom.xml b/hapi-fhir-structures-hl7org-dstu2/pom.xml index e842ad1c049..5f1386f1496 100644 --- a/hapi-fhir-structures-hl7org-dstu2/pom.xml +++ b/hapi-fhir-structures-hl7org-dstu2/pom.xml @@ -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> diff --git a/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/dstu2/hapi/rest/server/ServerConformanceProvider.java b/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/dstu2/hapi/rest/server/ServerConformanceProvider.java index c68ff5fd5f6..89108b0b695 100644 --- a/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/dstu2/hapi/rest/server/ServerConformanceProvider.java +++ b/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/dstu2/hapi/rest/server/ServerConformanceProvider.java @@ -263,7 +263,7 @@ public class ServerConformanceProvider extends BaseServerCapabilityStatementProv (SearchMethodBinding) nextMethodBinding, theRequestDetails); } else if (nextMethodBinding instanceof OperationMethodBinding) { OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; - String opName = bindings.getOperationBindingToName().get(methodBinding); + String opName = bindings.getOperationBindingToId().get(methodBinding); if (operationNames.add(opName)) { // Only add each operation (by name) once rest.addOperation().setName(methodBinding.getName()).getDefinition() @@ -299,7 +299,7 @@ public class ServerConformanceProvider extends BaseServerCapabilityStatementProv checkBindingForSystemOps(rest, systemOps, nextMethodBinding); if (nextMethodBinding instanceof OperationMethodBinding) { OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; - String opName = bindings.getOperationBindingToName().get(methodBinding); + String opName = bindings.getOperationBindingToId().get(methodBinding); if (operationNames.add(opName)) { rest.addOperation().setName(methodBinding.getName()).getDefinition() .setReference("OperationDefinition/" + opName); @@ -412,7 +412,7 @@ public class ServerConformanceProvider extends BaseServerCapabilityStatementProv if (theId == null || theId.hasIdPart() == false) { throw new ResourceNotFoundException(theId); } - List<OperationMethodBinding> sharedDescriptions = getServerConfiguration(theRequestDetails).provideBindings().getOperationNameToBindings().get(theId.getIdPart()); + List<OperationMethodBinding> sharedDescriptions = getServerConfiguration(theRequestDetails).provideBindings().getOperationIdToBindings().get(theId.getIdPart()); if (sharedDescriptions == null || sharedDescriptions.isEmpty()) { throw new ResourceNotFoundException(theId); } diff --git a/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationDuplicateServerHl7OrgDstu2Test.java b/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationDuplicateServerHl7OrgDstu2Test.java index 84a10a258f5..7aabf4fc2fa 100644 --- a/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationDuplicateServerHl7OrgDstu2Test.java +++ b/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationDuplicateServerHl7OrgDstu2Test.java @@ -56,14 +56,14 @@ public class OperationDuplicateServerHl7OrgDstu2Test { ourLog.info(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(resp)); - assertEquals(3, resp.getRest().get(0).getOperation().size()); + assertEquals(1, resp.getRest().get(0).getOperation().size()); assertEquals("$myoperation", resp.getRest().get(0).getOperation().get(0).getName()); - assertEquals("OperationDefinition/-s-myoperation", resp.getRest().get(0).getOperation().get(0).getDefinition().getReference()); + assertEquals("OperationDefinition/OrganizationPatient-ts-myoperation", resp.getRest().get(0).getOperation().get(0).getDefinition().getReference()); } // OperationDefinition { - HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/OperationDefinition/Patient--myoperation?_pretty=true"); + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/OperationDefinition/OrganizationPatient-ts-myoperation?_pretty=true"); HttpResponse status = ourClient.execute(httpGet); assertEquals(200, status.getStatusLine().getStatusCode()); @@ -74,7 +74,7 @@ public class OperationDuplicateServerHl7OrgDstu2Test { OperationDefinition resp = ourCtx.newXmlParser().parseResource(OperationDefinition.class, response); assertEquals("$myoperation", resp.getCode()); assertEquals(true, resp.getIdempotent()); - assertEquals(1, resp.getType().size()); + assertEquals(2, resp.getType().size()); assertEquals(1, resp.getParameter().size()); } } diff --git a/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/rest/server/ServerConformanceProviderHl7OrgDstu2Test.java b/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/rest/server/ServerConformanceProviderHl7OrgDstu2Test.java index fccaab41961..7108cb5fe1d 100644 --- a/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/rest/server/ServerConformanceProviderHl7OrgDstu2Test.java +++ b/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/rest/server/ServerConformanceProviderHl7OrgDstu2Test.java @@ -243,44 +243,6 @@ public class ServerConformanceProviderHl7OrgDstu2Test { } - @Test - public void testOperationOnNoTypes() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); - rs.setProviders(new PlainProviderWithExtendedOperationOnNoType()); - - ServerConformanceProvider sc = new ServerConformanceProvider(rs) { - @Override - public Conformance getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) { - return super.getServerConformance(theRequest, theRequestDetails); - } - }; - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - Conformance sconf = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - assertEquals("OperationDefinition/-is-plain", sconf.getRest().get(0).getOperation().get(0).getDefinition().getReference()); - - OperationDefinition opDef = sc.readOperationDefinition(new IdType("OperationDefinition/-is-plain"), createRequestDetails(rs)); - - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef); - ourLog.info(conf); - - assertEquals("$plain", opDef.getCode()); - assertEquals(true, opDef.getIdempotent()); - assertEquals(3, opDef.getParameter().size()); - assertEquals("start", opDef.getParameter().get(0).getName()); - assertEquals("in", opDef.getParameter().get(0).getUse().toCode()); - assertEquals("0", opDef.getParameter().get(0).getMinElement().getValueAsString()); - assertEquals("date", opDef.getParameter().get(0).getTypeElement().getValueAsString()); - - assertEquals("out1", opDef.getParameter().get(2).getName()); - assertEquals("out", opDef.getParameter().get(2).getUse().toCode()); - assertEquals("1", opDef.getParameter().get(2).getMinElement().getValueAsString()); - assertEquals("2", opDef.getParameter().get(2).getMaxElement().getValueAsString()); - assertEquals("string", opDef.getParameter().get(2).getTypeElement().getValueAsString()); -} - @Test public void testProviderWithRequiredAndOptional() throws Exception { @@ -617,7 +579,7 @@ public class ServerConformanceProviderHl7OrgDstu2Test { } private RequestDetails createRequestDetails(RestfulServer theServer) { - ServletRequestDetails retVal = new ServletRequestDetails(null); + ServletRequestDetails retVal = new ServletRequestDetails(); retVal.setServer(theServer); return retVal; } diff --git a/hapi-fhir-structures-r4/pom.xml b/hapi-fhir-structures-r4/pom.xml index d5f9e85f836..6bdf2b268dd 100644 --- a/hapi-fhir-structures-r4/pom.xml +++ b/hapi-fhir-structures-r4/pom.xml @@ -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> diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/OperationServerR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/OperationServerR4Test.java index 8ee22ee169b..84cfb5b1038 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/OperationServerR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/OperationServerR4Test.java @@ -33,8 +33,18 @@ import org.eclipse.jetty.servlet.ServletHolder; 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.*; +import org.hl7.fhir.r4.model.Binary; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.CapabilityStatement; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.IntegerType; +import org.hl7.fhir.r4.model.MoneyQuantity; +import org.hl7.fhir.r4.model.OperationDefinition; import org.hl7.fhir.r4.model.OperationDefinition.OperationParameterUse; +import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.UnsignedIntType; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -72,6 +82,7 @@ public class OperationServerR4Test { private static int ourPort; private static Server ourServer; private static IBaseResource ourNextResponse; + private static RestOperationTypeEnum ourLastRestOperation; private IGenericClient myFhirClient; @BeforeEach @@ -98,11 +109,11 @@ public class OperationServerR4Test { CapabilityStatement p = myFhirClient.fetchConformance().ofType(CapabilityStatement.class).prettyPrint().execute(); ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(p)); - List<CapabilityStatement.CapabilityStatementRestResourceOperationComponent> ops = p.getRest().get(0).getOperation(); + List<CapabilityStatement.CapabilityStatementRestResourceOperationComponent> ops = p.getRestFirstRep().getResource().stream().filter(t->t.getType().equals("Patient")).findFirst().orElseThrow(()->new IllegalArgumentException()).getOperation(); assertThat(ops.size(), greaterThan(1)); List<String> opNames = toOpNames(ops); - assertThat(opNames, containsInRelativeOrder("OP_TYPE")); + assertThat(opNames.toString(), opNames, containsInRelativeOrder("OP_TYPE")); OperationDefinition def = myFhirClient.read().resource(OperationDefinition.class).withId(ops.get(opNames.indexOf("OP_TYPE")).getDefinition()).execute(); assertEquals("OP_TYPE", def.getCode()); @@ -113,7 +124,7 @@ public class OperationServerR4Test { */ @Test public void testOperationDefinition() { - OperationDefinition def = myFhirClient.read().resource(OperationDefinition.class).withId("OperationDefinition/Patient--OP_TYPE").execute(); + OperationDefinition def = myFhirClient.read().resource(OperationDefinition.class).withId("OperationDefinition/Patient-t-OP_TYPE").execute(); ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(def)); @@ -179,8 +190,6 @@ public class OperationServerR4Test { } - - @Test public void testManualResponseWithPrimitiveParam() throws Exception { @@ -197,7 +206,6 @@ public class OperationServerR4Test { } - @Test public void testInstanceEverythingGet() throws Exception { @@ -230,7 +238,6 @@ public class OperationServerR4Test { assertEquals(RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE, ourLastRestOperation); } - @Test public void testInstanceEverythingHapiClient() { ourCtx.newRestfulGenericClient("http://localhost:" + ourPort).operation().onInstance(new IdType("Patient/123")).named("$everything").withParameters(new Parameters()).execute(); @@ -278,7 +285,7 @@ public class OperationServerR4Test { @Test public void testManualInputAndOutput() throws Exception { - byte[] bytes = new byte[]{1,2,3,4,5,6,7,8,7,6,5,4,3,2,1}; + byte[] bytes = new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 7, 6, 5, 4, 3, 2, 1}; ContentType contentType = ContentType.IMAGE_PNG; HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/$manualInputAndOutput"); @@ -295,10 +302,9 @@ public class OperationServerR4Test { } - @Test public void testManualInputAndOutputWithUrlParam() throws Exception { - byte[] bytes = new byte[]{1,2,3,4,5,6,7,8,7,6,5,4,3,2,1}; + byte[] bytes = new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 7, 6, 5, 4, 3, 2, 1}; ContentType contentType = ContentType.IMAGE_PNG; HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/$manualInputAndOutputWithParam?param1=value"); @@ -838,7 +844,7 @@ public class OperationServerR4Test { return new Bundle(); } - @Operation(name="$manualInputAndOutput", manualResponse=true, manualRequest=true) + @Operation(name = "$manualInputAndOutput", manualResponse = true, manualRequest = true) public void manualInputAndOutput(HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws IOException { String contentType = theServletRequest.getContentType(); byte[] bytes = IOUtils.toByteArray(theServletRequest.getInputStream()); @@ -850,9 +856,9 @@ public class OperationServerR4Test { theServletResponse.getOutputStream().close(); } - @Operation(name="$manualInputAndOutputWithParam", manualResponse=true, manualRequest=true) + @Operation(name = "$manualInputAndOutputWithParam", manualResponse = true, manualRequest = true) public void manualInputAndOutputWithParam( - @OperationParam(name="param1") StringType theParam1, + @OperationParam(name = "param1") StringType theParam1, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse ) throws IOException { @@ -880,7 +886,6 @@ public class OperationServerR4Test { } } - private static RestOperationTypeEnum ourLastRestOperation; public static class PlainProvider { @@ -918,10 +923,10 @@ public class OperationServerR4Test { return new SimpleBundleProvider(resources); } - @Operation(name= "$manualResponseWithPrimitiveParam", idempotent = true, global = true, manualResponse = true) + @Operation(name = "$manualResponseWithPrimitiveParam", idempotent = true, global = true, manualResponse = true) public void manualResponseWithPrimitiveParam( @IdParam IIdType theResourceId, - @OperationParam(name="path", min = 1, max = 1) IPrimitiveType<String> thePath, + @OperationParam(name = "path", min = 1, max = 1) IPrimitiveType<String> thePath, ServletRequestDetails theRequestDetails, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) { @@ -933,7 +938,7 @@ public class OperationServerR4Test { theServletResponse.setStatus(200); } - @Operation(name = "$OP_SERVER") + @Operation(name = "$OP_SERVER") public Parameters opServer( @OperationParam(name = "PARAM1") StringType theParam1, @OperationParam(name = "PARAM2") Patient theParam2 @@ -1028,12 +1033,4 @@ public class OperationServerR4Test { } - public static void main(String[] theValue) { - Parameters p = new Parameters(); - p.addParameter().setName("start").setValue(new DateTimeType("2001-01-02")); - p.addParameter().setName("end").setValue(new DateTimeType("2015-07-10")); - String inParamsStr = FhirContext.forDstu2().newXmlParser().encodeResourceToString(p); - ourLog.info(inParamsStr.replace("\"", "\\\"")); - } - } diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/RestfulServerTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/RestfulServerTest.java similarity index 67% rename from hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/RestfulServerTest.java rename to hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/RestfulServerTest.java index 95c67bec9bb..679c1ec3a6f 100644 --- a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/RestfulServerTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/RestfulServerTest.java @@ -3,7 +3,7 @@ package ca.uhn.fhir.rest.server; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; -import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.model.api.annotation.ResourceDef; import ca.uhn.fhir.rest.annotation.Create; import ca.uhn.fhir.rest.annotation.Metadata; import ca.uhn.fhir.rest.annotation.Operation; @@ -16,11 +16,10 @@ import org.hl7.fhir.instance.model.api.IBaseConformance; import org.hl7.fhir.instance.model.api.IBaseMetaType; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Patient; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Answers; -import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import javax.servlet.ServletException; @@ -33,70 +32,55 @@ import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) public class RestfulServerTest { - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private FhirContext myCtx; - private RestfulServer restfulServer; + private final FhirContext myCtx = FhirContext.forR4(); // don't use cached, we register custom resources + private RestfulServer myRestfulServer; @BeforeEach public void setUp() throws ServletException { - when(myCtx.getVersion().getVersion()).thenReturn(FhirVersionEnum.DSTU3); - when(myCtx.getVersion().getServerVersion()).thenReturn(new MyFhirVersionServer()); - - restfulServer = new RestfulServer(myCtx); - restfulServer.init(); - } - - private void mockResource(Class theClass) { - RuntimeResourceDefinition resourceDefinitionMock = mock(RuntimeResourceDefinition.class); - String className = theClass.getSimpleName(); - lenient().when(resourceDefinitionMock.getName()).thenReturn(className); - lenient().when(myCtx.getResourceDefinition(className)).thenReturn(resourceDefinitionMock); - lenient().when(myCtx.getResourceType(theClass)).thenReturn(className); + myRestfulServer = new RestfulServer(myCtx); + myRestfulServer.init(); } @Test public void testRegisterProvidersWithMethodBindings() { - mockResource(MyResource.class); - mockResource(MyResource2.class); - MyProvider provider = new MyProvider(); - restfulServer.registerProvider(provider); + myRestfulServer.registerProvider(provider); MyProvider2 provider2 = new MyProvider2(); - restfulServer.registerProvider(provider2); + myRestfulServer.registerProvider(provider2); - assertFalse(restfulServer.getProviderMethodBindings(provider).isEmpty()); - assertFalse(restfulServer.getProviderMethodBindings(provider2).isEmpty()); + assertFalse(myRestfulServer.getProviderMethodBindings(provider).isEmpty()); + assertFalse(myRestfulServer.getProviderMethodBindings(provider2).isEmpty()); - restfulServer.unregisterProvider(provider); - assertTrue(restfulServer.getProviderMethodBindings(provider).isEmpty()); - assertFalse(restfulServer.getProviderMethodBindings(provider2).isEmpty()); + myRestfulServer.unregisterProvider(provider); + assertTrue(myRestfulServer.getProviderMethodBindings(provider).isEmpty()); + assertFalse(myRestfulServer.getProviderMethodBindings(provider2).isEmpty()); } @Test public void testRegisterProviders() { //test register Plain Provider - restfulServer.registerProvider(new MyClassWithRestInterface()); - assertEquals(1, restfulServer.getPlainProviders().size()); - Object plainProvider = restfulServer.getPlainProviders().iterator().next(); + myRestfulServer.registerProvider(new MyClassWithRestInterface()); + assertEquals(1, myRestfulServer.getResourceProviders().size()); + Object plainProvider = myRestfulServer.getResourceProviders().get(0); assertTrue(plainProvider instanceof MyClassWithRestInterface); //test register Resource Provider - restfulServer.registerProvider(new MyResourceProvider()); - assertEquals(1, restfulServer.getResourceProviders().size()); - IResourceProvider resourceProvider = restfulServer.getResourceProviders().iterator().next(); + myRestfulServer.registerProvider(new MyResourceProvider()); + assertEquals(2, myRestfulServer.getResourceProviders().size()); + IResourceProvider resourceProvider = myRestfulServer.getResourceProviders().get(1); assertTrue(resourceProvider instanceof MyResourceProvider); //test unregister providers - restfulServer.unregisterProvider(plainProvider); - assertTrue(restfulServer.getPlainProviders().isEmpty()); - restfulServer.unregisterProvider(resourceProvider); - assertTrue(restfulServer.getResourceProviders().isEmpty()); + myRestfulServer.unregisterProvider(plainProvider); + assertFalse(myRestfulServer.getResourceProviders().isEmpty()); + myRestfulServer.unregisterProvider(resourceProvider); + assertTrue(myRestfulServer.getResourceProviders().isEmpty()); } @Test public void testFailRegisterInterfaceProviderWithoutRestfulMethod() { try { - restfulServer.registerProvider(new MyClassWithoutRestInterface()); + myRestfulServer.registerProvider(new MyClassWithoutRestInterface()); fail(); } catch (ConfigurationException e) { assertEquals("Did not find any annotated RESTful methods on provider class ca.uhn.fhir.rest.server.RestfulServerTest$MyClassWithoutRestInterface", e.getMessage()); @@ -108,7 +92,11 @@ public class RestfulServerTest { private static class MyClassWithoutRestInterface implements Serializable { } - private static class MyClassWithRestInterface implements MyRestInterface { + private static class MyClassWithRestInterface implements MyRestInterface, IResourceProvider { + @Override + public Class<? extends IBaseResource> getResourceType() { + return Patient.class; + } } @SuppressWarnings("unused") @@ -148,7 +136,7 @@ public class RestfulServerTest { @Override public Class<? extends IBaseResource> getResourceType() { - return IBaseResource.class; + return Patient.class; } } @@ -176,7 +164,8 @@ public class RestfulServerTest { } } - private static class MyResource implements IBaseResource { + @ResourceDef(name="MyResource") + public static class MyResource implements IBaseResource { @Override public boolean isEmpty() { @@ -230,11 +219,12 @@ public class RestfulServerTest { @Override public FhirVersionEnum getStructureFhirVersionEnum() { - return null; + return FhirVersionEnum.R4; } } - private static class MyResource2 extends MyResource { + @ResourceDef(name="MyResource2") + public static class MyResource2 extends MyResource { } } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/ServerInvalidDefinitionR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/ServerInvalidDefinitionR4Test.java index 57e485b792a..eac229a77c8 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/ServerInvalidDefinitionR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/ServerInvalidDefinitionR4Test.java @@ -2,7 +2,9 @@ package ca.uhn.fhir.rest.server; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; 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.OptionalParam; @@ -11,24 +13,35 @@ import ca.uhn.fhir.rest.annotation.Search; import ca.uhn.fhir.rest.annotation.Update; import ca.uhn.fhir.rest.annotation.Validate; import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.provider.ServerCapabilityStatementProvider; +import ca.uhn.fhir.test.utilities.server.MockServletUtil; import ca.uhn.fhir.util.TestUtil; import com.google.common.collect.Lists; import org.hamcrest.core.StringContains; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.CapabilityStatement; +import org.hl7.fhir.r4.model.DateType; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.OperationDefinition; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.StringType; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; import java.util.List; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; public class ServerInvalidDefinitionR4Test extends BaseR4ServerTest { @@ -182,9 +195,31 @@ public class ServerInvalidDefinitionR4Test extends BaseR4ServerTest { } - @AfterAll - public static void afterClassClearContext() { - TestUtil.clearAllStaticFieldsForUnitTest(); + @Test + public void testOperationOnNoTypes() throws Exception { + @SuppressWarnings("unused") + class PlainProviderWithExtendedOperationOnNoType { + + @Operation(name = "plain", idempotent = true, returnParameters = {@OperationParam(min = 1, max = 2, name = "out1", type = StringType.class)}) + public IBundleProvider everything(HttpServletRequest theServletRequest, @IdParam IdType theId, @OperationParam(name = "start") DateType theStart, + @OperationParam(name = "end") DateType theEnd) { + return null; + } + + } + + RestfulServer rs = new RestfulServer(FhirContext.forCached(FhirVersionEnum.R4)); + rs.setProviders(new PlainProviderWithExtendedOperationOnNoType()); + rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); + + try { + rs.init(MockServletUtil.createServletConfig()); + fail(); + } catch (ServletException e) { + assertEquals("Failed to initialize FHIR Restful server: Failure scanning class PlainProviderWithExtendedOperationOnNoType: @Operation method is an instance level method (it has an @IdParam parameter) but is not marked as global() and is not declared in a resource provider: everything", e.getMessage()); + } + } + } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorR4Test.java index 480568284fb..3bb305e4211 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorR4Test.java @@ -302,8 +302,37 @@ public class AuthorizationInterceptorR4Test { assertEquals(200, status.getStatusLine().getStatusCode()); assertTrue(ourHitMethod); + ourHitMethod = false; + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1/$validate"); + status = ourClient.execute(httpGet); + extractResponseAndClose(status); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); } + + /** + * A GET to the base URL isn't valid, but the interceptor should allow it + */ + @Test + public void testGetRoot() throws Exception { + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { + return new RuleBuilder() + .allowAll() + .build(); + } + }); + + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/"); + CloseableHttpResponse status = ourClient.execute(httpGet); + extractResponseAndClose(status); + assertEquals(400, status.getStatusLine().getStatusCode()); + + } + + @Test public void testAllowAllForTenant() throws Exception { ourServlet.setTenantIdentificationStrategy(new UrlBaseTenantIdentificationStrategy()); @@ -1944,7 +1973,6 @@ public class AuthorizationInterceptorR4Test { HttpGet httpGet; HttpResponse status; - String response; ourReturn = Collections.singletonList(createPatient(2)); ourHitMethod = false; @@ -1968,7 +1996,6 @@ public class AuthorizationInterceptorR4Test { HttpGet httpGet; HttpResponse status; - String response; ourReturn = Collections.singletonList(createPatient(2)); ourHitMethod = false; @@ -2188,7 +2215,6 @@ public class AuthorizationInterceptorR4Test { HttpGet httpGet; HttpResponse status; - String response; ourReturn = Collections.singletonList(new Consent().setDateTime(new Date()).setId("Consent/123")); ourHitMethod = false; @@ -2933,7 +2959,7 @@ public class AuthorizationInterceptorR4Test { HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/"); httpPost.setEntity(createFhirResourceEntity(requestBundle)); CloseableHttpResponse status = ourClient.execute(httpPost); - String resp = extractResponseAndClose(status); + extractResponseAndClose(status); assertEquals(200, status.getStatusLine().getStatusCode()); } diff --git a/hapi-fhir-structures-r5/pom.xml b/hapi-fhir-structures-r5/pom.xml index 490ba126870..3a1dbc48cb6 100644 --- a/hapi-fhir-structures-r5/pom.xml +++ b/hapi-fhir-structures-r5/pom.xml @@ -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> diff --git a/hapi-fhir-structures-r5/src/test/java/ca/uhn/fhir/rest/server/ServerCapabilityStatementProviderR5Test.java b/hapi-fhir-structures-r5/src/test/java/ca/uhn/fhir/rest/server/ServerCapabilityStatementProviderR5Test.java index 19f0ca0dbe7..d1940e3649f 100644 --- a/hapi-fhir-structures-r5/src/test/java/ca/uhn/fhir/rest/server/ServerCapabilityStatementProviderR5Test.java +++ b/hapi-fhir-structures-r5/src/test/java/ca/uhn/fhir/rest/server/ServerCapabilityStatementProviderR5Test.java @@ -125,7 +125,7 @@ public class ServerCapabilityStatementProviderR5Test { } private RequestDetails createRequestDetails(RestfulServer theServer) { - ServletRequestDetails retVal = new ServletRequestDetails(null); + ServletRequestDetails retVal = new ServletRequestDetails(); retVal.setServer(theServer); return retVal; } @@ -147,8 +147,9 @@ public class ServerCapabilityStatementProviderR5Test { String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); ourLog.info(conf); - assertEquals(1, conformance.getRest().get(0).getOperation().size()); - assertEquals("everything", conformance.getRest().get(0).getOperation().get(0).getName()); + List<CapabilityStatementRestResourceOperationComponent> operations = conformance.getRestFirstRep().getResource().stream().filter(t->t.getType().equals("Patient")).findFirst().orElseThrow(()->new IllegalArgumentException()).getOperation(); + assertEquals(1, operations.size()); + assertEquals("everything", operations.get(0).getName()); OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-everything"), createRequestDetails(rs)); validate(opDef); @@ -163,6 +164,7 @@ public class ServerCapabilityStatementProviderR5Test { RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new ProviderWithExtendedOperationReturningBundle()); + rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { }; @@ -291,56 +293,42 @@ public class ServerCapabilityStatementProviderR5Test { String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); ourLog.info(conf); + List<CapabilityStatementRestResourceOperationComponent> operations; - assertEquals(4, conformance.getRest().get(0).getOperation().size()); - List<String> operationNames = toOperationNames(conformance.getRest().get(0).getOperation()); - assertThat(operationNames, containsInAnyOrder("someOp", "validate", "someOp", "validate")); - - List<String> operationIdParts = toOperationIdParts(conformance.getRest().get(0).getOperation()); - assertThat(operationIdParts, containsInAnyOrder("Patient-i-someOp", "Encounter-i-someOp", "Patient-i-validate", "Encounter-i-validate")); + operations = conformance.getRestFirstRep().getResource().stream().filter(t->t.getType().equals("Patient")).findFirst().orElseThrow(()->new IllegalArgumentException()).getOperation(); + assertEquals(2, operations.size()); + List<String> operationNames = toOperationNames(operations); + assertThat(operationNames.toString(), operationNames, containsInAnyOrder("someOp", "validate")); + List<String> operationIdParts = toOperationIdParts(operations); + assertThat(operationIdParts.toString(), operationIdParts, containsInAnyOrder("EncounterPatient-i-someOp", "EncounterPatient-i-validate")); { - OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-someOp"), createRequestDetails(rs)); + OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/EncounterPatient-i-someOp"), createRequestDetails(rs)); validate(opDef); ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); Set<String> types = toStrings(opDef.getResource()); assertEquals("someOp", opDef.getCode()); assertEquals(true, opDef.getInstance()); assertEquals(false, opDef.getSystem()); - assertThat(types, containsInAnyOrder("Patient")); + assertThat(types, containsInAnyOrder("Patient", "Encounter")); assertEquals(2, opDef.getParameter().size()); assertEquals("someOpParam1", opDef.getParameter().get(0).getName()); assertEquals("date", opDef.getParameter().get(0).getType().toCode()); assertEquals("someOpParam2", opDef.getParameter().get(1).getName()); - assertEquals("Patient", opDef.getParameter().get(1).getType().toCode()); + assertEquals("Resource", opDef.getParameter().get(1).getType().toCode()); } { - OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Encounter-i-someOp"), createRequestDetails(rs)); - validate(opDef); - ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); - Set<String> types = toStrings(opDef.getResource()); - assertEquals("someOp", opDef.getCode()); - assertEquals(true, opDef.getInstance()); - assertEquals(false, opDef.getSystem()); - assertThat(types, containsInAnyOrder("Encounter")); - assertEquals(2, opDef.getParameter().size()); - assertEquals("someOpParam1", opDef.getParameter().get(0).getName()); - assertEquals("date", opDef.getParameter().get(0).getType().toCode()); - assertEquals("someOpParam2", opDef.getParameter().get(1).getName()); - assertEquals("Encounter", opDef.getParameter().get(1).getType().toCode()); - } - { - OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-validate"), createRequestDetails(rs)); + OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/EncounterPatient-i-validate"), createRequestDetails(rs)); validate(opDef); ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); Set<String> types = toStrings(opDef.getResource()); assertEquals("validate", opDef.getCode()); assertEquals(true, opDef.getInstance()); assertEquals(false, opDef.getSystem()); - assertThat(types, containsInAnyOrder("Patient")); + assertThat(types, containsInAnyOrder("Patient", "Encounter")); assertEquals(1, opDef.getParameter().size()); assertEquals("resource", opDef.getParameter().get(0).getName()); - assertEquals("Patient", opDef.getParameter().get(0).getType().toCode()); + assertEquals("Resource", opDef.getParameter().get(0).getType().toCode()); } } @@ -364,45 +352,6 @@ public class ServerCapabilityStatementProviderR5Test { } - @Test - public void testOperationOnNoTypes() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new PlainProviderWithExtendedOperationOnNoType()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { - @Override - public CapabilityStatement getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) { - return (CapabilityStatement) super.getServerConformance(theRequest, createRequestDetails(rs)); - } - }; - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/-is-plain"), createRequestDetails(rs)); - validate(opDef); - - assertEquals("plain", opDef.getCode()); - assertEquals(false, opDef.getAffectsState()); - assertEquals(3, opDef.getParameter().size()); - - assertTrue(opDef.getParameter().get(0).hasName()); - assertEquals("start", opDef.getParameter().get(0).getName()); - assertEquals("in", opDef.getParameter().get(0).getUse().toCode()); - assertEquals("0", opDef.getParameter().get(0).getMinElement().getValueAsString()); - assertEquals("date", opDef.getParameter().get(0).getTypeElement().getValueAsString()); - - assertEquals("out1", opDef.getParameter().get(2).getName()); - assertEquals("out", opDef.getParameter().get(2).getUse().toCode()); - assertEquals("1", opDef.getParameter().get(2).getMinElement().getValueAsString()); - assertEquals("2", opDef.getParameter().get(2).getMaxElement().getValueAsString()); - assertEquals("string", opDef.getParameter().get(2).getTypeElement().getValueAsString()); - - assertThat(opDef.getSystem(), is(true)); - assertThat(opDef.getType(), is(false)); - assertThat(opDef.getInstance(), is(true)); - } - @Test public void testProviderWithRequiredAndOptional() throws Exception { @@ -706,7 +655,7 @@ public class ServerCapabilityStatementProviderR5Test { ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(operationDefinition)); validate(operationDefinition); assertThat(operationDefinition.getCode(), is(NamedQueryPlainProvider.QUERY_NAME)); - assertThat("The operation name should be the description, if a description is set", operationDefinition.getName(), equalTo("Search_testQuery")); + assertThat("The operation name should be the description, if a description is set", operationDefinition.getName(), equalTo("TestQuery")); assertThat(operationDefinition.getStatus(), is(PublicationStatus.ACTIVE)); assertThat(operationDefinition.getKind(), is(OperationKind.QUERY)); assertThat(operationDefinition.getDescription(), is(NamedQueryPlainProvider.DESCRIPTION)); @@ -740,15 +689,15 @@ public class ServerCapabilityStatementProviderR5Test { CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance)); - CapabilityStatementRestComponent restComponent = conformance.getRest().get(0); - CapabilityStatementRestResourceOperationComponent operationComponent = restComponent.getOperation().get(0); + CapabilityStatementRestResourceComponent resource = conformance.getRestFirstRep().getResource().stream().filter(t->t.getType().equals("Patient")).findFirst().orElseThrow(()->new IllegalArgumentException()); + CapabilityStatementRestResourceOperationComponent operationComponent = resource.getOperation().get(0); String operationReference = operationComponent.getDefinition(); assertThat(operationReference, not(nullValue())); OperationDefinition operationDefinition = (OperationDefinition) sc.readOperationDefinition(new IdType(operationReference), createRequestDetails(rs)); ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(operationDefinition)); validate(operationDefinition); - assertThat("The operation name should be the code if no description is set", operationDefinition.getName(), equalTo("Search_testQuery")); + assertThat("The operation name should be the code if no description is set", operationDefinition.getName(), equalTo("TestQuery")); String patientResourceName = "Patient"; assertThat("A resource level search targets the resource of the provider it's defined in", operationDefinition.getResource().get(0).getValue(), is(patientResourceName)); assertThat(operationDefinition.getSystem(), is(false)); @@ -764,11 +713,7 @@ public class ServerCapabilityStatementProviderR5Test { assertThat(param.getMax(), is("1")); assertThat(param.getUse(), is(Enumerations.OperationParameterUse.IN)); - CapabilityStatementRestResourceComponent patientResource = restComponent.getResource().stream() - .filter(r -> patientResourceName.equals(r.getType())) - .findAny() - .get(); - assertThat("Named query parameters should not appear in the resource search params", patientResource.getSearchParam(), is(empty())); + assertThat("Named query parameters should not appear in the resource search params", resource.getSearchParam(), is(empty())); } @Test @@ -787,7 +732,7 @@ public class ServerCapabilityStatementProviderR5Test { String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); ourLog.info(conf); - List<CapabilityStatementRestResourceOperationComponent> operations = conformance.getRest().get(0).getOperation(); + List<CapabilityStatementRestResourceOperationComponent> operations = conformance.getRestFirstRep().getResource().stream().filter(t->t.getType().equals("Patient")).findFirst().orElseThrow(()->new IllegalArgumentException()).getOperation(); assertThat(operations.size(), is(1)); assertThat(operations.get(0).getName(), is(TypeLevelOperationProvider.OPERATION_NAME)); diff --git a/hapi-fhir-test-utilities/pom.xml b/hapi-fhir-test-utilities/pom.xml index 66f0f0ac9fd..609c1ab8515 100644 --- a/hapi-fhir-test-utilities/pom.xml +++ b/hapi-fhir-test-utilities/pom.xml @@ -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> @@ -60,7 +60,28 @@ <optional>true</optional> </dependency> - <!-- Jetty --> + <!-- HTMLUnit --> + <dependency> + <groupId>net.sourceforge.htmlunit</groupId> + <artifactId>htmlunit</artifactId> + <exclusions> + <exclusion> + <groupId>xml-apis</groupId> + <artifactId>xml-apis</artifactId> + </exclusion> + <exclusion> + <groupId>xerces</groupId> + <artifactId>xercesImpl</artifactId> + </exclusion> + </exclusions> + </dependency> + + <dependency> + <groupId>org.awaitility</groupId> + <artifactId>awaitility</artifactId> + </dependency> + + <!-- Jetty --> <dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-servlet</artifactId> @@ -104,10 +125,18 @@ <groupId>org.hamcrest</groupId> <artifactId>hamcrest-core</artifactId> </exclusion> + <exclusion> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + </exclusion> </exclusions> </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + </dependency> - </dependencies> + </dependencies> <build> <plugins> diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/HtmlUtil.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/HtmlUtil.java new file mode 100644 index 00000000000..700b33d9551 --- /dev/null +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/HtmlUtil.java @@ -0,0 +1,56 @@ +package ca.uhn.fhir.test.utilities; + +/*- + * #%L + * HAPI FHIR Test Utilities + * %% + * 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 com.gargoylesoftware.htmlunit.BrowserVersion; +import com.gargoylesoftware.htmlunit.StringWebResponse; +import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.html.HtmlForm; +import com.gargoylesoftware.htmlunit.html.HtmlInput; +import com.gargoylesoftware.htmlunit.html.HtmlPage; +import com.gargoylesoftware.htmlunit.html.parser.neko.HtmlUnitNekoHtmlParser; +import org.awaitility.Awaitility; + +import java.io.IOException; +import java.net.URL; + +public class HtmlUtil { + + public static HtmlPage parseAsHtml(String theRespString, URL theUrl) throws IOException { + StringWebResponse response = new StringWebResponse(theRespString, theUrl); + WebClient client = new WebClient(BrowserVersion.BEST_SUPPORTED, false, null, -1); + client.getOptions().setCssEnabled(false); + client.getOptions().setJavaScriptEnabled(false); + + final HtmlPage page = new HtmlPage(response, client.getCurrentWindow()); + HtmlUnitNekoHtmlParser htmlUnitNekoHtmlParser = new HtmlUnitNekoHtmlParser(); + htmlUnitNekoHtmlParser.parse(response, page, false); + return page; + } + + public static HtmlForm waitForForm(HtmlPage thePage, String theName) { + return Awaitility.await().until(() -> thePage.getFormByName(theName), t -> t != null); + } + + public static HtmlInput waitForInput(HtmlForm theForm, String theName) { + return Awaitility.await().until(() -> theForm.getInputByName(theName), t -> t != null); + } +} diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/JettyUtil.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/JettyUtil.java index 86d13644749..fae4d26807e 100644 --- a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/JettyUtil.java +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/JettyUtil.java @@ -28,6 +28,9 @@ import org.eclipse.jetty.server.handler.StatisticsHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + public class JettyUtil { /** @@ -43,18 +46,20 @@ public class JettyUtil { /** * Starts the given Jetty server, and configures it for graceful shutdown */ - public static void startServer(Server server) throws Exception { + public static void startServer(@Nonnull Server theServer) throws Exception { //Needed for graceful shutdown, see https://github.com/eclipse/jetty.project/issues/2076#issuecomment-353717761 - server.insertHandler(new StatisticsHandler()); - server.start(); + theServer.insertHandler(new StatisticsHandler()); + theServer.start(); } /** - * Shut down the given Jetty server, and release held resources. + * Shut down the given Jetty server, and release held resources. */ - public static void closeServer(Server server) throws Exception { - server.stop(); - server.destroy(); + public static void closeServer(@Nullable Server theServer) throws Exception { + if (theServer != null) { + theServer.stop(); + theServer.destroy(); + } } } diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/server/MockServletUtil.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/server/MockServletUtil.java new file mode 100644 index 00000000000..52c92c115ea --- /dev/null +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/server/MockServletUtil.java @@ -0,0 +1,42 @@ +package ca.uhn.fhir.test.utilities.server; + +/*- + * #%L + * HAPI FHIR Test Utilities + * %% + * 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 javax.servlet.ServletConfig; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class MockServletUtil { + + /** + * Non instantiable + */ + private MockServletUtil() { + super(); + } + + public static ServletConfig createServletConfig() { + ServletConfig sc = mock(ServletConfig.class); + when(sc.getServletContext()).thenReturn(null); + return sc; + } +} diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/server/RestfulServerExtension.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/server/RestfulServerExtension.java index e735dc37c7a..4a8b02f9131 100644 --- a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/server/RestfulServerExtension.java +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/server/RestfulServerExtension.java @@ -32,6 +32,7 @@ import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.junit.jupiter.api.extension.AfterEachCallback; @@ -54,10 +55,11 @@ public class RestfulServerExtension implements BeforeEachCallback, AfterEachCall private FhirVersionEnum myFhirVersion; private Server myServer; private RestfulServer myServlet; - private int myPort; + private int myPort = 0; private CloseableHttpClient myHttpClient; private IGenericClient myFhirClient; private List<Consumer<RestfulServer>> myConsumers = new ArrayList<>(); + private String myServletPath = "/*"; /** * Constructor @@ -94,20 +96,21 @@ public class RestfulServerExtension implements BeforeEachCallback, AfterEachCall } private void startServer() throws Exception { - myServer = new Server(0); + myServer = new Server(myPort); - ServletHandler servletHandler = new ServletHandler(); myServlet = new RestfulServer(myFhirContext); myServlet.setDefaultPrettyPrint(true); if (myProviders != null) { myServlet.registerProviders(myProviders); } ServletHolder servletHolder = new ServletHolder(myServlet); - servletHandler.addServletWithMapping(servletHolder, "/*"); myConsumers.forEach(t -> t.accept(myServlet)); - myServer.setHandler(servletHandler); + ServletContextHandler contextHandler = new ServletContextHandler(); + contextHandler.addServlet(servletHolder, myServletPath); + + myServer.setHandler(contextHandler); myServer.start(); myPort = JettyUtil.getPortForStartedServer(myServer); ourLog.info("Server has started on port {}", myPort); @@ -175,4 +178,14 @@ public class RestfulServerExtension implements BeforeEachCallback, AfterEachCall public void shutDownServer() throws Exception { JettyUtil.closeServer(myServer); } + + public RestfulServerExtension withServletPath(String theServletPath) { + myServletPath = theServletPath; + return this; + } + + public RestfulServerExtension withPort(int thePort) { + myPort = thePort; + return this; + } } diff --git a/hapi-fhir-testpage-overlay/pom.xml b/hapi-fhir-testpage-overlay/pom.xml index 072a4f9a21c..ee6382cca38 100644 --- a/hapi-fhir-testpage-overlay/pom.xml +++ b/hapi-fhir-testpage-overlay/pom.xml @@ -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> diff --git a/hapi-fhir-validation-resources-dstu2.1/pom.xml b/hapi-fhir-validation-resources-dstu2.1/pom.xml index f8cc76070d3..3af7e1b74b1 100644 --- a/hapi-fhir-validation-resources-dstu2.1/pom.xml +++ b/hapi-fhir-validation-resources-dstu2.1/pom.xml @@ -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> diff --git a/hapi-fhir-validation-resources-dstu2/pom.xml b/hapi-fhir-validation-resources-dstu2/pom.xml index a69e2ea236d..53941c421b0 100644 --- a/hapi-fhir-validation-resources-dstu2/pom.xml +++ b/hapi-fhir-validation-resources-dstu2/pom.xml @@ -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> diff --git a/hapi-fhir-validation-resources-dstu3/pom.xml b/hapi-fhir-validation-resources-dstu3/pom.xml index 0061ae4ef6e..9570b03358f 100644 --- a/hapi-fhir-validation-resources-dstu3/pom.xml +++ b/hapi-fhir-validation-resources-dstu3/pom.xml @@ -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> diff --git a/hapi-fhir-validation-resources-r4/pom.xml b/hapi-fhir-validation-resources-r4/pom.xml index ace6fe58362..188381def8a 100644 --- a/hapi-fhir-validation-resources-r4/pom.xml +++ b/hapi-fhir-validation-resources-r4/pom.xml @@ -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> diff --git a/hapi-fhir-validation-resources-r5/pom.xml b/hapi-fhir-validation-resources-r5/pom.xml index 442455129ad..79b31674df1 100644 --- a/hapi-fhir-validation-resources-r5/pom.xml +++ b/hapi-fhir-validation-resources-r5/pom.xml @@ -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> diff --git a/hapi-fhir-validation/pom.xml b/hapi-fhir-validation/pom.xml index a441397e88e..1d9dcb5397a 100644 --- a/hapi-fhir-validation/pom.xml +++ b/hapi-fhir-validation/pom.xml @@ -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> diff --git a/hapi-fhir-validation/src/test/java/ca/uhn/fhir/rest/server/RequestValidatingInterceptorDstu3Test.java b/hapi-fhir-validation/src/test/java/ca/uhn/fhir/rest/server/RequestValidatingInterceptorDstu3Test.java index af0ade61f0d..81a3dae5c27 100644 --- a/hapi-fhir-validation/src/test/java/ca/uhn/fhir/rest/server/RequestValidatingInterceptorDstu3Test.java +++ b/hapi-fhir-validation/src/test/java/ca/uhn/fhir/rest/server/RequestValidatingInterceptorDstu3Test.java @@ -19,6 +19,7 @@ import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.validation.IValidationContext; import ca.uhn.fhir.validation.IValidatorModule; import ca.uhn.fhir.validation.ResultSeverityEnum; +import com.ctc.wstx.shaded.msv_core.verifier.jaxp.DocumentBuilderFactoryImpl; import org.apache.commons.io.IOUtils; import org.apache.http.HttpResponse; import org.apache.http.client.methods.CloseableHttpResponse; @@ -45,6 +46,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; +import javax.xml.parsers.DocumentBuilderFactory; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; diff --git a/hapi-fhir-validation/src/test/java/ca/uhn/fhir/rest/server/ServerCapabilityStatementProviderR4Test.java b/hapi-fhir-validation/src/test/java/ca/uhn/fhir/rest/server/ServerCapabilityStatementProviderR4Test.java index f151466e3aa..3f503cdbb8a 100644 --- a/hapi-fhir-validation/src/test/java/ca/uhn/fhir/rest/server/ServerCapabilityStatementProviderR4Test.java +++ b/hapi-fhir-validation/src/test/java/ca/uhn/fhir/rest/server/ServerCapabilityStatementProviderR4Test.java @@ -9,6 +9,9 @@ import ca.uhn.fhir.model.primitive.InstantDt; 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.GraphQL; +import ca.uhn.fhir.rest.annotation.GraphQLQueryBody; +import ca.uhn.fhir.rest.annotation.GraphQLQueryUrl; import ca.uhn.fhir.rest.annotation.History; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.IncludeParam; @@ -23,6 +26,7 @@ import ca.uhn.fhir.rest.annotation.Update; import ca.uhn.fhir.rest.annotation.Validate; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.RequestDetails; @@ -38,14 +42,20 @@ import ca.uhn.fhir.rest.server.method.BaseMethodBinding; import ca.uhn.fhir.rest.server.method.IParameter; import ca.uhn.fhir.rest.server.method.SearchMethodBinding; import ca.uhn.fhir.rest.server.method.SearchParameter; +import ca.uhn.fhir.rest.server.provider.HashMapResourceProvider; +import ca.uhn.fhir.rest.server.provider.ProviderConstants; import ca.uhn.fhir.rest.server.provider.ServerCapabilityStatementProvider; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; +import ca.uhn.fhir.test.utilities.server.MockServletUtil; +import ca.uhn.fhir.util.HapiExtensions; import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.validation.FhirValidator; import ca.uhn.fhir.validation.ValidationResult; import com.google.common.collect.Lists; import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; +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.Bundle; import org.hl7.fhir.r4.model.CapabilityStatement; @@ -60,6 +70,7 @@ import org.hl7.fhir.r4.model.DateType; import org.hl7.fhir.r4.model.DiagnosticReport; import org.hl7.fhir.r4.model.Encounter; import org.hl7.fhir.r4.model.Enumerations.PublicationStatus; +import org.hl7.fhir.r4.model.Extension; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.OperationDefinition; @@ -68,12 +79,10 @@ import org.hl7.fhir.r4.model.OperationDefinition.OperationKind; import org.hl7.fhir.r4.model.OperationDefinition.OperationParameterUse; import org.hl7.fhir.r4.model.OperationOutcome; import org.hl7.fhir.r4.model.Patient; -import org.hl7.fhir.r4.model.StringType; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; @@ -93,6 +102,7 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; @@ -100,1345 +110,1565 @@ import static org.mockito.Mockito.when; public class ServerCapabilityStatementProviderR4Test { - public static final String PATIENT_SUB = "PatientSub"; - public static final String PATIENT_SUB_SUB = "PatientSubSub"; - public static final String PATIENT_SUB_SUB_2 = "PatientSubSub2"; - public static final String PATIENT_TRIPLE_SUB = "PatientTripleSub"; - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServerCapabilityStatementProviderR4Test.class); - private final FhirContext myCtx = FhirContext.forCached(FhirVersionEnum.R4); - private FhirValidator myValidator; - - @BeforeEach - public void before() { - myValidator = myCtx.newValidator(); - myValidator.registerValidatorModule(new FhirInstanceValidator(myCtx)); - } - - private HttpServletRequest createHttpServletRequest() { - HttpServletRequest req = mock(HttpServletRequest.class); - when(req.getRequestURI()).thenReturn("/FhirStorm/fhir/Patient/_search"); - when(req.getServletPath()).thenReturn("/fhir"); - when(req.getRequestURL()).thenReturn(new StringBuffer().append("http://fhirstorm.dyndns.org:8080/FhirStorm/fhir/Patient/_search")); - when(req.getContextPath()).thenReturn("/FhirStorm"); - return req; - } - - private ServletConfig createServletConfig() { - ServletConfig sc = mock(ServletConfig.class); - when(sc.getServletContext()).thenReturn(null); - return sc; - } - - private CapabilityStatementRestResourceComponent findRestResource(CapabilityStatement conformance, String wantResource) throws Exception { - CapabilityStatementRestResourceComponent resource = null; - for (CapabilityStatementRestResourceComponent next : conformance.getRest().get(0).getResource()) { - if (next.getType().equals(wantResource)) { - resource = next; - } - } - if (resource == null) { - throw new Exception("Could not find resource: " + wantResource); - } - return resource; - } - - @Test - public void testFormats() throws ServletException { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new ConditionalProvider()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement cs = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - List<String> formats = cs - .getFormat() - .stream() - .map(t -> t.getCode()) - .collect(Collectors.toList()); - assertThat(formats.toString(), formats, containsInAnyOrder( - "application/fhir+xml", - "xml", - "application/fhir+json", - "json", - "application/x-turtle", - "ttl" - )); - } - - - @Test - public void testConditionalOperations() throws Exception { - - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new ConditionalProvider()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); - ourLog.info(conf); - - assertEquals(2, conformance.getRest().get(0).getResource().size()); - CapabilityStatementRestResourceComponent res = conformance.getRest().get(0).getResource().get(1); - assertEquals("Patient", res.getType()); - - assertTrue(res.getConditionalCreate()); - assertEquals(ConditionalDeleteStatus.MULTIPLE, res.getConditionalDelete()); - assertTrue(res.getConditionalUpdate()); - } - - private RequestDetails createRequestDetails(RestfulServer theServer) { - ServletRequestDetails retVal = new ServletRequestDetails(null); - retVal.setServer(theServer); - retVal.setFhirServerBase("http://localhost/baseR4"); - return retVal; - } - - @Test - public void testExtendedOperationReturningBundle() throws Exception { - - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new ProviderWithExtendedOperationReturningBundle()); - rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - validate(conformance); - - assertEquals(1, conformance.getRest().get(0).getOperation().size()); - assertEquals("everything", conformance.getRest().get(0).getOperation().get(0).getName()); - - OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-everything"), createRequestDetails(rs)); - validate(opDef); - assertEquals("everything", opDef.getCode()); - assertThat(opDef.getSystem(), is(false)); - assertThat(opDef.getType(), is(false)); - assertThat(opDef.getInstance(), is(true)); - } - - @Test - public void testExtendedOperationReturningBundleOperation() throws Exception { + public static final String PATIENT_SUB = "PatientSub"; + public static final String PATIENT_SUB_SUB = "PatientSubSub"; + public static final String PATIENT_SUB_SUB_2 = "PatientSubSub2"; + public static final String PATIENT_TRIPLE_SUB = "PatientTripleSub"; + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServerCapabilityStatementProviderR4Test.class); + private final FhirContext myCtx = FhirContext.forCached(FhirVersionEnum.R4); + private FhirValidator myValidator; + + @BeforeEach + public void before() { + myValidator = myCtx.newValidator(); + myValidator.registerValidatorModule(new FhirInstanceValidator(myCtx)); + } + + private HttpServletRequest createHttpServletRequest() { + HttpServletRequest req = mock(HttpServletRequest.class); + when(req.getRequestURI()).thenReturn("/FhirStorm/fhir/Patient/_search"); + when(req.getServletPath()).thenReturn("/fhir"); + when(req.getRequestURL()).thenReturn(new StringBuffer().append("http://fhirstorm.dyndns.org:8080/FhirStorm/fhir/Patient/_search")); + when(req.getContextPath()).thenReturn("/FhirStorm"); + return req; + } + + private CapabilityStatementRestResourceComponent findRestResource(CapabilityStatement conformance, String wantResource) throws Exception { + CapabilityStatementRestResourceComponent resource = null; + for (CapabilityStatementRestResourceComponent next : conformance.getRest().get(0).getResource()) { + if (next.getType().equals(wantResource)) { + resource = next; + } + } + if (resource == null) { + throw new Exception("Could not find resource: " + wantResource); + } + return resource; + } + + @Test + public void testFormats() throws ServletException { + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new ConditionalProvider()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement cs = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + List<String> formats = cs + .getFormat() + .stream() + .map(t -> t.getCode()) + .collect(Collectors.toList()); + assertThat(formats.toString(), formats, containsInAnyOrder( + "application/fhir+xml", + "xml", + "application/fhir+json", + "json", + "application/x-turtle", + "ttl" + )); + } + + + @Test + public void testConditionalOperations() throws Exception { + + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new ConditionalProvider()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); + ourLog.info(conf); + + assertEquals(2, conformance.getRest().get(0).getResource().size()); + CapabilityStatementRestResourceComponent res = conformance.getRest().get(0).getResource().get(1); + assertEquals("Patient", res.getType()); + + assertTrue(res.getConditionalCreate()); + assertEquals(ConditionalDeleteStatus.MULTIPLE, res.getConditionalDelete()); + assertTrue(res.getConditionalUpdate()); + } + + private RequestDetails createRequestDetails(RestfulServer theServer) { + ServletRequestDetails retVal = new ServletRequestDetails(); + retVal.setServer(theServer); + retVal.setFhirServerBase("http://localhost/baseR4"); + return retVal; + } + + @Test + public void testExtendedOperationReturningBundle() throws Exception { + + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new ProviderWithExtendedOperationReturningBundle()); + rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + validate(conformance); + + CapabilityStatementRestResourceComponent patient = conformance.getRestFirstRep().getResource().stream().filter(t -> t.getType().equals("Patient")).findFirst().orElseThrow(() -> new IllegalArgumentException()); + assertEquals(1, patient.getOperation().size()); + assertEquals("everything", patient.getOperation().get(0).getName()); + assertEquals("http://localhost/baseR4/OperationDefinition/Patient-i-everything", patient.getOperation().get(0).getDefinition()); + + OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-everything"), createRequestDetails(rs)); + validate(opDef); + assertEquals("everything", opDef.getCode()); + assertThat(opDef.getSystem(), is(false)); + assertThat(opDef.getType(), is(false)); + assertThat(opDef.getInstance(), is(true)); + } - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new ProviderWithExtendedOperationReturningBundle()); + @Test + public void testExtendedOperationReturningBundleOperation() throws Exception { - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { - }; - rs.setServerConformanceProvider(sc); + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new ProviderWithExtendedOperationReturningBundle()); + rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); - rs.init(createServletConfig()); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { + }; + rs.setServerConformanceProvider(sc); - OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-everything"), createRequestDetails(rs)); - validate(opDef); + rs.init(MockServletUtil.createServletConfig()); - String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef); - ourLog.info(conf); + OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-everything"), createRequestDetails(rs)); + validate(opDef); - assertEquals("everything", opDef.getCode()); - assertEquals(false, opDef.getAffectsState()); - } + String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef); + ourLog.info(conf); - @Test - public void testInstanceHistorySupported() throws Exception { + assertEquals("everything", opDef.getCode()); + assertEquals(false, opDef.getAffectsState()); + } - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new InstanceHistoryProvider()); + @Test + public void testInstanceHistorySupported() throws Exception { - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new InstanceHistoryProvider()); - rs.init(createServletConfig()); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = validate(conformance); + rs.init(MockServletUtil.createServletConfig()); - conf = myCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(conformance); - assertThat(conf, containsString("<interaction><code value=\"" + TypeRestfulInteraction.HISTORYINSTANCE.toCode() + "\"/></interaction>")); - } + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = validate(conformance); - @Test - public void testMultiOptionalDocumentation() throws Exception { + conf = myCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(conformance); + assertThat(conf, containsString("<interaction><code value=\"" + TypeRestfulInteraction.HISTORYINSTANCE.toCode() + "\"/></interaction>")); + } - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new MultiOptionalProvider()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - boolean found = false; - Collection<ResourceBinding> resourceBindings = rs.getResourceBindings(); - for (ResourceBinding resourceBinding : resourceBindings) { - if (resourceBinding.getResourceName().equals("Patient")) { - List<BaseMethodBinding<?>> methodBindings = resourceBinding.getMethodBindings(); - SearchMethodBinding binding = (SearchMethodBinding) methodBindings.get(0); - SearchParameter param = (SearchParameter) binding.getParameters().iterator().next(); - assertEquals("The patient's identifier", param.getDescription()); - found = true; - } - } - - assertTrue(found); - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = validate(conformance); - - assertThat(conf, containsString("<documentation value=\"The patient's identifier\"/>")); - assertThat(conf, containsString("<documentation value=\"The patient's name\"/>")); - assertThat(conf, containsString("<type value=\"token\"/>")); - } - - @Test - public void testNonConditionalOperations() throws Exception { - - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new NonConditionalProvider()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - validate(conformance); - - CapabilityStatementRestResourceComponent res = conformance.getRest().get(0).getResource().get(1); - assertEquals("Patient", res.getType()); - - assertNull(res.getConditionalCreateElement().getValue()); - assertNull(res.getConditionalDeleteElement().getValue()); - assertNull(res.getConditionalUpdateElement().getValue()); - } - - /** - * See #379 - */ - @Test - public void testOperationAcrossMultipleTypes() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new MultiTypePatientProvider(), new MultiTypeEncounterProvider()); - rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - - validate(conformance); - - assertEquals(4, conformance.getRest().get(0).getOperation().size()); - List<String> operationNames = toOperationNames(conformance.getRest().get(0).getOperation()); - assertThat(operationNames, containsInAnyOrder("someOp", "validate", "someOp", "validate")); - - List<String> operationIdParts = toOperationIdParts(conformance.getRest().get(0).getOperation()); - assertThat(operationIdParts, containsInAnyOrder("Patient-i-someOp", "Encounter-i-someOp", "Patient-i-validate", "Encounter-i-validate")); - - { - OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-someOp"), createRequestDetails(rs)); - validate(opDef); - ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); - Set<String> types = toStrings(opDef.getResource()); - assertEquals("someOp", opDef.getCode()); - assertEquals(true, opDef.getInstance()); - assertEquals(false, opDef.getSystem()); - assertThat(types, containsInAnyOrder("Patient")); - assertEquals(2, opDef.getParameter().size()); - assertEquals("someOpParam1", opDef.getParameter().get(0).getName()); - assertEquals("date", opDef.getParameter().get(0).getType()); - assertEquals("someOpParam2", opDef.getParameter().get(1).getName()); - assertEquals("Patient", opDef.getParameter().get(1).getType()); - } - { - OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Encounter-i-someOp"), createRequestDetails(rs)); - validate(opDef); - ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); - Set<String> types = toStrings(opDef.getResource()); - assertEquals("someOp", opDef.getCode()); - assertEquals(true, opDef.getInstance()); - assertEquals(false, opDef.getSystem()); - assertThat(types, containsInAnyOrder("Encounter")); - assertEquals(2, opDef.getParameter().size()); - assertEquals("someOpParam1", opDef.getParameter().get(0).getName()); - assertEquals("date", opDef.getParameter().get(0).getType()); - assertEquals("someOpParam2", opDef.getParameter().get(1).getName()); - assertEquals("Encounter", opDef.getParameter().get(1).getType()); - } - { - OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-validate"), createRequestDetails(rs)); - validate(opDef); - ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); - Set<String> types = toStrings(opDef.getResource()); - assertEquals("validate", opDef.getCode()); - assertEquals(true, opDef.getInstance()); - assertEquals(false, opDef.getSystem()); - assertThat(types, containsInAnyOrder("Patient")); - assertEquals(1, opDef.getParameter().size()); - assertEquals("resource", opDef.getParameter().get(0).getName()); - assertEquals("Patient", opDef.getParameter().get(0).getType()); - } - } - - @Test - public void testOperationDocumentation() throws Exception { - - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new SearchProvider()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - - String conf = validate(conformance); - - assertThat(conf, containsString("<documentation value=\"The patient's identifier (MRN or other card number)\"/>")); - assertThat(conf, containsString("<type value=\"token\"/>")); - - } - - @Test - public void testOperationOnNoTypes() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new PlainProviderWithExtendedOperationOnNoType()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { - @Override - public CapabilityStatement getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) { - return (CapabilityStatement) super.getServerConformance(theRequest, createRequestDetails(rs)); - } - }; - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/-is-plain"), createRequestDetails(rs)); - validate(opDef); - - assertEquals("plain", opDef.getCode()); - assertEquals(false, opDef.getAffectsState()); - assertEquals(3, opDef.getParameter().size()); - - assertTrue(opDef.getParameter().get(0).hasName()); - assertEquals("start", opDef.getParameter().get(0).getName()); - assertEquals("in", opDef.getParameter().get(0).getUse().toCode()); - assertEquals("0", opDef.getParameter().get(0).getMinElement().getValueAsString()); - assertEquals("date", opDef.getParameter().get(0).getTypeElement().getValueAsString()); - - assertEquals("out1", opDef.getParameter().get(2).getName()); - assertEquals("out", opDef.getParameter().get(2).getUse().toCode()); - assertEquals("1", opDef.getParameter().get(2).getMinElement().getValueAsString()); - assertEquals("2", opDef.getParameter().get(2).getMaxElement().getValueAsString()); - assertEquals("string", opDef.getParameter().get(2).getTypeElement().getValueAsString()); - - assertThat(opDef.getSystem(), is(true)); - assertThat(opDef.getType(), is(false)); - assertThat(opDef.getInstance(), is(true)); - } - - @Test - public void testProviderWithRequiredAndOptional() throws Exception { - - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new ProviderWithRequiredAndOptional()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - validate(conformance); - - CapabilityStatementRestComponent rest = conformance.getRest().get(0); - CapabilityStatementRestResourceComponent res = rest.getResource().get(0); - assertEquals("DiagnosticReport", res.getType()); - - assertEquals("subject.identifier", res.getSearchParam().get(0).getName()); + @Test + public void testMultiOptionalDocumentation() throws Exception { + + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new MultiOptionalProvider()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + boolean found = false; + Collection<ResourceBinding> resourceBindings = rs.getResourceBindings(); + for (ResourceBinding resourceBinding : resourceBindings) { + if (resourceBinding.getResourceName().equals("Patient")) { + List<BaseMethodBinding<?>> methodBindings = resourceBinding.getMethodBindings(); + SearchMethodBinding binding = (SearchMethodBinding) methodBindings.get(0); + SearchParameter param = (SearchParameter) binding.getParameters().iterator().next(); + assertEquals("The patient's identifier", param.getDescription()); + found = true; + } + } + + assertTrue(found); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = validate(conformance); + + assertThat(conf, containsString("<documentation value=\"The patient's identifier\"/>")); + assertThat(conf, containsString("<documentation value=\"The patient's name\"/>")); + assertThat(conf, containsString("<type value=\"token\"/>")); + } + + @Test + public void testNonConditionalOperations() throws Exception { + + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new NonConditionalProvider()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + validate(conformance); + + CapabilityStatementRestResourceComponent res = conformance.getRest().get(0).getResource().get(1); + assertEquals("Patient", res.getType()); + + assertNull(res.getConditionalCreateElement().getValue()); + assertNull(res.getConditionalDeleteElement().getValue()); + assertNull(res.getConditionalUpdateElement().getValue()); + } + + + @Test + public void testOperationParameterDocumentation() throws Exception { + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new MultiTypePatientProvider(), new MultiTypeEncounterProvider()); + rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + validate(conformance); + + IdType operationId = new IdType("OperationDefinition/EncounterPatient-i-someOp"); + RequestDetails requestDetails = createRequestDetails(rs); + OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(operationId, requestDetails); + validate(opDef); + Set<String> types = toStrings(opDef.getResource()); + assertEquals("someOp", opDef.getCode()); + assertEquals(true, opDef.getInstance()); + assertEquals(false, opDef.getSystem()); + assertThat(types, containsInAnyOrder("Patient", "Encounter")); + assertEquals(2, opDef.getParameter().size()); + assertEquals("someOpParam1", opDef.getParameter().get(0).getName()); + assertEquals("date", opDef.getParameter().get(0).getType()); + assertEquals("Start description", opDef.getParameter().get(0).getDocumentation()); + + List<Extension> exampleExtensions = opDef.getParameter().get(0).getExtensionsByUrl(HapiExtensions.EXT_OP_PARAMETER_EXAMPLE_VALUE); + assertEquals(2, exampleExtensions.size()); + assertEquals("2001", exampleExtensions.get(0).getValueAsPrimitive().getValueAsString()); + assertEquals("2002", exampleExtensions.get(1).getValueAsPrimitive().getValueAsString()); + } + + + /** + * See #379 + */ + @Test + public void testOperationAcrossMultipleTypes() throws Exception { + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new MultiTypePatientProvider(), new MultiTypeEncounterProvider()); + rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + + validate(conformance); + + CapabilityStatementRestResourceComponent patient = conformance.getRestFirstRep().getResource().stream().filter(t -> t.getType().equals("Patient")).findFirst().orElseThrow(() -> new IllegalArgumentException()); + assertEquals(2, patient.getOperation().size()); + assertThat(toOperationNames(patient.getOperation()), containsInAnyOrder("someOp", "validate")); + assertThat(toOperationDefinitions(patient.getOperation()), containsInAnyOrder("http://localhost/baseR4/OperationDefinition/EncounterPatient-i-someOp", "http://localhost/baseR4/OperationDefinition/EncounterPatient-i-validate")); + + CapabilityStatementRestResourceComponent encounter = conformance.getRestFirstRep().getResource().stream().filter(t -> t.getType().equals("Encounter")).findFirst().orElseThrow(() -> new IllegalArgumentException()); + assertEquals(2, encounter.getOperation().size()); + assertThat(toOperationNames(encounter.getOperation()), containsInAnyOrder("someOp", "validate")); + assertThat(toOperationDefinitions(encounter.getOperation()).toString(), toOperationDefinitions(encounter.getOperation()), containsInAnyOrder("http://localhost/baseR4/OperationDefinition/EncounterPatient-i-someOp", "http://localhost/baseR4/OperationDefinition/EncounterPatient-i-validate")); + + + { + OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/EncounterPatient-i-someOp"), createRequestDetails(rs)); + validate(opDef); + ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); + Set<String> types = toStrings(opDef.getResource()); + assertEquals("someOp", opDef.getCode()); + assertEquals(true, opDef.getInstance()); + assertEquals(false, opDef.getSystem()); + assertThat(types, containsInAnyOrder("Patient", "Encounter")); + assertEquals(2, opDef.getParameter().size()); + assertEquals("someOpParam1", opDef.getParameter().get(0).getName()); + assertEquals("date", opDef.getParameter().get(0).getType()); + assertEquals("someOpParam2", opDef.getParameter().get(1).getName()); + assertEquals("Resource", opDef.getParameter().get(1).getType()); + } + { + OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/EncounterPatient-i-validate"), createRequestDetails(rs)); + validate(opDef); + ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); + Set<String> types = toStrings(opDef.getResource()); + assertEquals("validate", opDef.getCode()); + assertEquals(true, opDef.getInstance()); + assertEquals(false, opDef.getSystem()); + assertThat(types, containsInAnyOrder("Patient", "Encounter")); + assertEquals(1, opDef.getParameter().size()); + assertEquals("resource", opDef.getParameter().get(0).getName()); + assertEquals("Resource", opDef.getParameter().get(0).getType()); + } + } + + @Test + public void testOperationDocumentation() throws Exception { + + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new SearchProvider()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + + String conf = validate(conformance); + + assertThat(conf, containsString("<documentation value=\"The patient's identifier (MRN or other card number)\"/>")); + assertThat(conf, containsString("<type value=\"token\"/>")); + + } + + @Test + public void testProviderWithRequiredAndOptional() throws Exception { + + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new ProviderWithRequiredAndOptional()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + validate(conformance); + + CapabilityStatementRestComponent rest = conformance.getRest().get(0); + CapabilityStatementRestResourceComponent res = rest.getResource().get(0); + assertEquals("DiagnosticReport", res.getType()); + + assertEquals("subject.identifier", res.getSearchParam().get(0).getName()); // assertEquals("identifier", res.getSearchParam().get(0).getChain().get(0).getValue()); - assertEquals(DiagnosticReport.SP_CODE, res.getSearchParam().get(1).getName()); + assertEquals(DiagnosticReport.SP_CODE, res.getSearchParam().get(1).getName()); - assertEquals(DiagnosticReport.SP_DATE, res.getSearchParam().get(2).getName()); + assertEquals(DiagnosticReport.SP_DATE, res.getSearchParam().get(2).getName()); - assertEquals(1, res.getSearchInclude().size()); - assertEquals("DiagnosticReport.result", res.getSearchInclude().get(0).getValue()); - } + assertEquals(1, res.getSearchInclude().size()); + assertEquals("DiagnosticReport.result", res.getSearchInclude().get(0).getValue()); + } - @Test - public void testReadAndVReadSupported() throws Exception { + @Test + public void testReadAndVReadSupported() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new VreadProvider()); + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new VreadProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); - rs.init(createServletConfig()); + rs.init(MockServletUtil.createServletConfig()); - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = validate(conformance); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = validate(conformance); - assertThat(conf, containsString("<interaction><code value=\"vread\"/></interaction>")); - assertThat(conf, containsString("<interaction><code value=\"read\"/></interaction>")); - } + assertThat(conf, containsString("<interaction><code value=\"vread\"/></interaction>")); + assertThat(conf, containsString("<interaction><code value=\"read\"/></interaction>")); + } - @Test - public void testReadSupported() throws Exception { + @Test + public void testReadSupported() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new ReadProvider()); + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new ReadProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); - rs.init(createServletConfig()); + rs.init(MockServletUtil.createServletConfig()); - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); - ourLog.info(conf); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); + ourLog.info(conf); - conf = myCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(conformance); - assertThat(conf, not(containsString("<interaction><code value=\"vread\"/></interaction>"))); - assertThat(conf, containsString("<interaction><code value=\"read\"/></interaction>")); - } + conf = myCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(conformance); + assertThat(conf, not(containsString("<interaction><code value=\"vread\"/></interaction>"))); + assertThat(conf, containsString("<interaction><code value=\"read\"/></interaction>")); + } - @Test - public void testSearchParameterDocumentation() throws Exception { + @Test + public void testSearchParameterDocumentation() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new SearchProvider()); + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new SearchProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); - rs.init(createServletConfig()); + rs.init(MockServletUtil.createServletConfig()); - boolean found = false; - Collection<ResourceBinding> resourceBindings = rs.getResourceBindings(); - for (ResourceBinding resourceBinding : resourceBindings) { - if (resourceBinding.getResourceName().equals("Patient")) { - List<BaseMethodBinding<?>> methodBindings = resourceBinding.getMethodBindings(); - SearchMethodBinding binding = (SearchMethodBinding) methodBindings.get(0); - for (IParameter next : binding.getParameters()) { - SearchParameter param = (SearchParameter) next; - if (param.getDescription().contains("The patient's identifier (MRN or other card number")) { - found = true; - } - } - found = true; - } - } - assertTrue(found); - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + boolean found = false; + Collection<ResourceBinding> resourceBindings = rs.getResourceBindings(); + for (ResourceBinding resourceBinding : resourceBindings) { + if (resourceBinding.getResourceName().equals("Patient")) { + List<BaseMethodBinding<?>> methodBindings = resourceBinding.getMethodBindings(); + SearchMethodBinding binding = (SearchMethodBinding) methodBindings.get(0); + for (IParameter next : binding.getParameters()) { + SearchParameter param = (SearchParameter) next; + if (param.getDescription().contains("The patient's identifier (MRN or other card number")) { + found = true; + } + } + found = true; + } + } + assertTrue(found); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = validate(conformance); + String conf = validate(conformance); - assertThat(conf, containsString("<documentation value=\"The patient's identifier (MRN or other card number)\"/>")); - assertThat(conf, containsString("<type value=\"token\"/>")); + assertThat(conf, containsString("<documentation value=\"The patient's identifier (MRN or other card number)\"/>")); + assertThat(conf, containsString("<type value=\"token\"/>")); - } + } - @Test - public void testFormatIncludesSpecialNonMediaTypeFormats() throws ServletException { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new SearchProvider()); + @Test + public void testFormatIncludesSpecialNonMediaTypeFormats() throws ServletException { + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new SearchProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); - rs.init(createServletConfig()); - CapabilityStatement serverConformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + rs.init(MockServletUtil.createServletConfig()); + CapabilityStatement serverConformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - List<String> formatCodes = serverConformance.getFormat().stream().map(c -> c.getCode()).collect(Collectors.toList()); + List<String> formatCodes = serverConformance.getFormat().stream().map(c -> c.getCode()).collect(Collectors.toList()); - assertThat(formatCodes, hasItem(Constants.FORMAT_XML)); - assertThat(formatCodes, hasItem(Constants.FORMAT_JSON)); - assertThat(formatCodes, hasItem(Constants.CT_FHIR_JSON_NEW)); - assertThat(formatCodes, hasItem(Constants.CT_FHIR_XML_NEW)); - } + assertThat(formatCodes, hasItem(Constants.FORMAT_XML)); + assertThat(formatCodes, hasItem(Constants.FORMAT_JSON)); + assertThat(formatCodes, hasItem(Constants.CT_FHIR_JSON_NEW)); + assertThat(formatCodes, hasItem(Constants.CT_FHIR_XML_NEW)); + } - /** - * See #286 - */ - @Test - public void testSearchReferenceParameterDocumentation() throws Exception { + /** + * See #286 + */ + @Test + public void testSearchReferenceParameterDocumentation() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new PatientResourceProvider()); + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new PatientResourceProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); - rs.init(createServletConfig()); + rs.init(MockServletUtil.createServletConfig()); - boolean found = false; - Collection<ResourceBinding> resourceBindings = rs.getResourceBindings(); - for (ResourceBinding resourceBinding : resourceBindings) { - if (resourceBinding.getResourceName().equals("Patient")) { - List<BaseMethodBinding<?>> methodBindings = resourceBinding.getMethodBindings(); - SearchMethodBinding binding = (SearchMethodBinding) methodBindings.get(0); - SearchParameter param = (SearchParameter) binding.getParameters().get(25); - assertEquals("The organization at which this person is a patient", param.getDescription()); - found = true; - } - } - assertTrue(found); - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + boolean found = false; + Collection<ResourceBinding> resourceBindings = rs.getResourceBindings(); + for (ResourceBinding resourceBinding : resourceBindings) { + if (resourceBinding.getResourceName().equals("Patient")) { + List<BaseMethodBinding<?>> methodBindings = resourceBinding.getMethodBindings(); + SearchMethodBinding binding = (SearchMethodBinding) methodBindings.get(0); + SearchParameter param = (SearchParameter) binding.getParameters().get(25); + assertEquals("The organization at which this person is a patient", param.getDescription()); + found = true; + } + } + assertTrue(found); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = validate(conformance); + String conf = validate(conformance); - } + } - /** - * See #286 - */ - @Test - public void testSearchReferenceParameterWithWhitelistDocumentation() throws Exception { + /** + * See #286 + */ + @Test + public void testSearchReferenceParameterWithWhitelistDocumentation() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new SearchProviderWithWhitelist()); + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new SearchProviderWithWhitelist()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); - rs.init(createServletConfig()); + rs.init(MockServletUtil.createServletConfig()); - boolean found = false; - Collection<ResourceBinding> resourceBindings = rs.getResourceBindings(); - for (ResourceBinding resourceBinding : resourceBindings) { - if (resourceBinding.getResourceName().equals("Patient")) { - List<BaseMethodBinding<?>> methodBindings = resourceBinding.getMethodBindings(); - SearchMethodBinding binding = (SearchMethodBinding) methodBindings.get(0); - SearchParameter param = (SearchParameter) binding.getParameters().get(0); - assertEquals("The organization at which this person is a patient", param.getDescription()); - found = true; - } - } - assertTrue(found); - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + boolean found = false; + Collection<ResourceBinding> resourceBindings = rs.getResourceBindings(); + for (ResourceBinding resourceBinding : resourceBindings) { + if (resourceBinding.getResourceName().equals("Patient")) { + List<BaseMethodBinding<?>> methodBindings = resourceBinding.getMethodBindings(); + SearchMethodBinding binding = (SearchMethodBinding) methodBindings.get(0); + SearchParameter param = (SearchParameter) binding.getParameters().get(0); + assertEquals("The organization at which this person is a patient", param.getDescription()); + found = true; + } + } + assertTrue(found); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = validate(conformance); + String conf = validate(conformance); - CapabilityStatementRestResourceComponent resource = findRestResource(conformance, "Patient"); + CapabilityStatementRestResourceComponent resource = findRestResource(conformance, "Patient"); - CapabilityStatementRestResourceSearchParamComponent param = resource.getSearchParam().get(0); + CapabilityStatementRestResourceSearchParamComponent param = resource.getSearchParam().get(0); // assertEquals("bar", param.getChain().get(0).getValue()); // assertEquals("foo", param.getChain().get(1).getValue()); // assertEquals(2, param.getChain().size()); - } + } - @Test - public void testSearchReferenceParameterWithList() throws Exception { + @Test + public void testSearchReferenceParameterWithList() throws Exception { - RestfulServer rsNoType = new RestfulServer(myCtx) { - @Override - public RestfulServerConfiguration createConfiguration() { - RestfulServerConfiguration retVal = super.createConfiguration(); - retVal.setConformanceDate(new InstantDt("2011-02-22T11:22:33Z")); - return retVal; - } - }; - rsNoType.registerProvider(new SearchProviderWithListNoType()); - ServerCapabilityStatementProvider scNoType = new ServerCapabilityStatementProvider(rsNoType); - rsNoType.setServerConformanceProvider(scNoType); - rsNoType.init(createServletConfig()); + RestfulServer rsNoType = new RestfulServer(myCtx) { + @Override + public RestfulServerConfiguration createConfiguration() { + RestfulServerConfiguration retVal = super.createConfiguration(); + retVal.setConformanceDate(new InstantDt("2011-02-22T11:22:33Z")); + return retVal; + } + }; + rsNoType.registerProvider(new SearchProviderWithListNoType()); + ServerCapabilityStatementProvider scNoType = new ServerCapabilityStatementProvider(rsNoType); + rsNoType.setServerConformanceProvider(scNoType); + rsNoType.init(MockServletUtil.createServletConfig()); - CapabilityStatement conformance = (CapabilityStatement) scNoType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsNoType)); - conformance.setId(""); - String confNoType = validate(conformance); + CapabilityStatement conformance = (CapabilityStatement) scNoType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsNoType)); + conformance.setId(""); + String confNoType = validate(conformance); - RestfulServer rsWithType = new RestfulServer(myCtx) { - @Override - public RestfulServerConfiguration createConfiguration() { - RestfulServerConfiguration retVal = super.createConfiguration(); - retVal.setConformanceDate(new InstantDt("2011-02-22T11:22:33Z")); - return retVal; - } - }; - rsWithType.registerProvider(new SearchProviderWithListWithType()); - ServerCapabilityStatementProvider scWithType = new ServerCapabilityStatementProvider(rsWithType); - rsWithType.setServerConformanceProvider(scWithType); - rsWithType.init(createServletConfig()); + RestfulServer rsWithType = new RestfulServer(myCtx) { + @Override + public RestfulServerConfiguration createConfiguration() { + RestfulServerConfiguration retVal = super.createConfiguration(); + retVal.setConformanceDate(new InstantDt("2011-02-22T11:22:33Z")); + return retVal; + } + }; + rsWithType.registerProvider(new SearchProviderWithListWithType()); + ServerCapabilityStatementProvider scWithType = new ServerCapabilityStatementProvider(rsWithType); + rsWithType.setServerConformanceProvider(scWithType); + rsWithType.init(MockServletUtil.createServletConfig()); - CapabilityStatement conformanceWithType = (CapabilityStatement) scWithType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsWithType)); - conformanceWithType.setId(""); - String confWithType = validate(conformanceWithType); + CapabilityStatement conformanceWithType = (CapabilityStatement) scWithType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsWithType)); + conformanceWithType.setId(""); + String confWithType = validate(conformanceWithType); - assertEquals(confNoType, confWithType); - assertThat(confNoType, containsString("<date value=\"2011-02-22T11:22:33Z\"/>")); - } + assertEquals(confNoType, confWithType); + assertThat(confNoType, containsString("<date value=\"2011-02-22T11:22:33Z\"/>")); + } - @Test - public void testSystemHistorySupported() throws Exception { + @Test + public void testSystemHistorySupported() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new SystemHistoryProvider()); + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new SystemHistoryProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); - rs.init(createServletConfig()); + rs.init(MockServletUtil.createServletConfig()); - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = validate(conformance); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = validate(conformance); - assertThat(conf, containsString("<interaction><code value=\"" + SystemRestfulInteraction.HISTORYSYSTEM.toCode() + "\"/></interaction>")); - } + assertThat(conf, containsString("<interaction><code value=\"" + SystemRestfulInteraction.HISTORYSYSTEM.toCode() + "\"/></interaction>")); + } - @Test - public void testTypeHistorySupported() throws Exception { + @Test + public void testTypeHistorySupported() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new TypeHistoryProvider()); + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new TypeHistoryProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); - rs.init(createServletConfig()); + rs.init(MockServletUtil.createServletConfig()); - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = validate(conformance); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = validate(conformance); - assertThat(conf, containsString("<interaction><code value=\"" + TypeRestfulInteraction.HISTORYTYPE.toCode() + "\"/></interaction>")); - } + assertThat(conf, containsString("<interaction><code value=\"" + TypeRestfulInteraction.HISTORYTYPE.toCode() + "\"/></interaction>")); + } + + @Test + public void testStaticIncludeChains() throws Exception { + + class MyProvider implements IResourceProvider { + + @Override + public Class<DiagnosticReport> getResourceType() { + return DiagnosticReport.class; + } + + @Search + public List<IBaseResource> search(@RequiredParam(name = DiagnosticReport.SP_PATIENT + "." + Patient.SP_FAMILY) StringParam lastName, + @RequiredParam(name = DiagnosticReport.SP_PATIENT + "." + Patient.SP_GIVEN) StringParam firstName, + @RequiredParam(name = DiagnosticReport.SP_PATIENT + "." + Patient.SP_BIRTHDATE) DateParam dob, + @OptionalParam(name = DiagnosticReport.SP_DATE) DateRangeParam range) { + return null; + } + + } + + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new MyProvider()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { + }; + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement opDef = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + + validate(opDef); + + CapabilityStatementRestResourceComponent resource = opDef.getRest().get(0).getResource().get(0); + assertEquals("DiagnosticReport", resource.getType()); + List<String> searchParamNames = resource.getSearchParam().stream().map(t -> t.getName()).collect(Collectors.toList()); + assertThat(searchParamNames, containsInAnyOrder("patient.birthdate", "patient.family", "patient.given", "date")); + } + + @Test + public void testGraphQLOperation() throws Exception { + + class MyProvider { + + @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) { + throw new IllegalStateException(); + } + + @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) { + throw new IllegalStateException(); + } - @Test - public void testStaticIncludeChains() throws Exception { + } - class MyProvider implements IResourceProvider { + RestfulServer rs = new RestfulServer(myCtx); + rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); + rs.registerProvider(new MyProvider()); + rs.registerProvider(new HashMapResourceProvider<>(myCtx, Patient.class)); + rs.registerProvider(new HashMapResourceProvider<>(myCtx, Observation.class)); - @Override - public Class<DiagnosticReport> getResourceType() { - return DiagnosticReport.class; - } + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { + }; + rs.setServerConformanceProvider(sc); - @Search - public List<IBaseResource> search(@RequiredParam(name = DiagnosticReport.SP_PATIENT + "." + Patient.SP_FAMILY) StringParam lastName, - @RequiredParam(name = DiagnosticReport.SP_PATIENT + "." + Patient.SP_GIVEN) StringParam firstName, - @RequiredParam(name = DiagnosticReport.SP_PATIENT + "." + Patient.SP_BIRTHDATE) DateParam dob, - @OptionalParam(name = DiagnosticReport.SP_DATE) DateRangeParam range) { - return null; - } + rs.init(MockServletUtil.createServletConfig()); - } - - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new MyProvider()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { - }; - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement opDef = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - - validate(opDef); - - CapabilityStatementRestResourceComponent resource = opDef.getRest().get(0).getResource().get(0); - assertEquals("DiagnosticReport", resource.getType()); - List<String> searchParamNames = resource.getSearchParam().stream().map(t -> t.getName()).collect(Collectors.toList()); - assertThat(searchParamNames, containsInAnyOrder("patient.birthdate", "patient.family", "patient.given", "date")); - } + CapabilityStatement opDef = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - @Test - public void testIncludeLastUpdatedSearchParam() throws Exception { + validate(opDef); - class MyProvider implements IResourceProvider { + // On Patient Resource + CapabilityStatementRestResourceComponent resource = opDef.getRest().get(0).getResource().stream().filter(t -> t.getType().equals("Patient")).findFirst().orElseThrow(() -> new IllegalArgumentException()); + CapabilityStatementRestResourceOperationComponent graphQlOperation = resource.getOperation().stream().filter(t -> t.getName().equals("graphql")).findFirst().orElseThrow(() -> new IllegalArgumentException()); + assertEquals("graphql", graphQlOperation.getName()); + assertEquals("http://localhost/baseR4/OperationDefinition/Global-is-graphql", graphQlOperation.getDefinition()); + assertEquals("This operation invokes a GraphQL expression for fetching an joining a graph of resources, returning them in a custom format.", graphQlOperation.getDocumentation()); + + // On Patient Resource + resource = opDef.getRest().get(0).getResource().stream().filter(t -> t.getType().equals("Observation")).findFirst().orElseThrow(() -> new IllegalArgumentException()); + graphQlOperation = resource.getOperation().stream().filter(t -> t.getName().equals("graphql")).findFirst().orElseThrow(() -> new IllegalArgumentException()); + assertEquals("graphql", graphQlOperation.getName()); + assertEquals("http://localhost/baseR4/OperationDefinition/Global-is-graphql", graphQlOperation.getDefinition()); + assertEquals("This operation invokes a GraphQL expression for fetching an joining a graph of resources, returning them in a custom format.", graphQlOperation.getDocumentation()); + + // At Server Level + CapabilityStatementRestComponent rest = opDef.getRest().get(0); + graphQlOperation = rest.getOperation().stream().filter(t -> t.getName().equals("graphql")).findAny().orElseThrow(() -> new IllegalArgumentException()); + assertEquals("graphql", graphQlOperation.getName()); + assertEquals("http://localhost/baseR4/OperationDefinition/Global-is-graphql", graphQlOperation.getDefinition()); + assertEquals("This operation invokes a GraphQL expression for fetching an joining a graph of resources, returning them in a custom format.", graphQlOperation.getDocumentation()); + + // Fetch OperationDefinition + IdType id = new IdType("http://localhost/baseR4/OperationDefinition/Global-is-graphql"); + RequestDetails requestDetails = createRequestDetails(rs); + OperationDefinition operationDefinition = (OperationDefinition) sc.readOperationDefinition(id, requestDetails); + assertEquals("Graphql", operationDefinition.getName()); + assertEquals("graphql", operationDefinition.getCode()); + assertEquals("http://localhost/baseR4/OperationDefinition/Global-is-graphql", operationDefinition.getUrl()); + assertEquals("This operation invokes a GraphQL expression for fetching an joining a graph of resources, returning them in a custom format.", operationDefinition.getDescription()); + assertTrue(operationDefinition.getSystem()); + assertFalse(operationDefinition.getType()); + assertTrue(operationDefinition.getInstance()); + } + + + @Test + public void testPlainProviderGlobalSystemAndInstanceOperations() throws Exception { + + class MyProvider { + + @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, + + @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) { + throw new IllegalStateException(); + } + + @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( + @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) { + throw new IllegalStateException(); + } + + } + + RestfulServer rs = new RestfulServer(myCtx); + rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); + rs.registerProvider(new MyProvider()); + rs.registerProvider(new HashMapResourceProvider<>(myCtx, Patient.class)); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { + }; + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement opDef = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + + validate(opDef); + + // On Patient Resource + CapabilityStatementRestResourceComponent resource = opDef.getRest().get(0).getResource().stream().filter(t -> t.getType().equals("Patient")).findFirst().orElseThrow(() -> new IllegalArgumentException()); + CapabilityStatementRestResourceOperationComponent operation = resource.getOperation().stream().filter(t -> t.getName().equals("diff")).findFirst().orElseThrow(() -> new IllegalArgumentException()); + assertEquals("diff", operation.getName()); + assertEquals("http://localhost/baseR4/OperationDefinition/Global-is-diff", operation.getDefinition()); + assertEquals("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.getDocumentation()); + + // At Server Level + CapabilityStatementRestComponent rest = opDef.getRest().get(0); + operation = rest.getOperation().stream().filter(t -> t.getName().equals("diff")).findAny().orElse(null); + assertEquals("diff", operation.getName()); + assertEquals("http://localhost/baseR4/OperationDefinition/Global-is-diff", operation.getDefinition()); + assertEquals("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.getDocumentation()); + + // Fetch OperationDefinition + IdType id = new IdType("http://localhost/baseR4/OperationDefinition/Global-is-diff"); + RequestDetails requestDetails = createRequestDetails(rs); + OperationDefinition operationDefinition = (OperationDefinition) sc.readOperationDefinition(id, requestDetails); + assertEquals("Diff", operationDefinition.getName()); + assertEquals("diff", operationDefinition.getCode()); + assertEquals("http://localhost/baseR4/OperationDefinition/Global-is-diff", operationDefinition.getUrl()); + assertEquals("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.", operationDefinition.getDescription()); + assertTrue(operationDefinition.getSystem()); + assertFalse(operationDefinition.getType()); + assertTrue(operationDefinition.getInstance()); + } + + @Test + public void testResourceProviderTypeAndInstanceLevelOperations() throws Exception { + + class MyProvider implements IResourceProvider { + + @Description("This is the expunge operation") + @Operation(name = "expunge", idempotent = false, returnParameters = { + @OperationParam(name = "count", typeName = "integer") + }) + public IBaseParameters expunge( + @IdParam IIdType theIdParam, + @OperationParam(name = "limit", typeName = "integer") IPrimitiveType<Integer> theLimit, + @OperationParam(name = "deleted", typeName = "boolean") IPrimitiveType<Boolean> theExpungeDeletedResources, + @OperationParam(name = "previous", typeName = "boolean") IPrimitiveType<Boolean> theExpungeOldVersions, + RequestDetails theRequest) { + throw new UnsupportedOperationException(); + } + + @Description("This is the expunge operation") + @Operation(name = "expunge", idempotent = false, returnParameters = { + @OperationParam(name = "count", typeName = "integer") + }) + public IBaseParameters expunge( + @OperationParam(name = "limit", typeName = "integer") IPrimitiveType<Integer> theLimit, + @OperationParam(name = "deleted", typeName = "boolean") IPrimitiveType<Boolean> theExpungeDeletedResources, + @OperationParam(name = "previous", typeName = "boolean") IPrimitiveType<Boolean> theExpungeOldVersions, + RequestDetails theRequest) { + throw new UnsupportedOperationException(); + } + + @Override + public Class<? extends IBaseResource> getResourceType() { + return Patient.class; + } + } + + RestfulServer rs = new RestfulServer(myCtx); + rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); + rs.registerProvider(new MyProvider()); + rs.registerProvider(new HashMapResourceProvider<>(myCtx, Patient.class)); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { + }; + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement opDef = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + + validate(opDef); + + // On Patient Resource + CapabilityStatementRestResourceComponent resource = opDef.getRest().get(0).getResource().stream().filter(t -> t.getType().equals("Patient")).findFirst().orElseThrow(() -> new IllegalArgumentException()); + List<CapabilityStatementRestResourceOperationComponent> expungeResourceOperations = resource.getOperation().stream().filter(t -> t.getName().equals("expunge")).collect(Collectors.toList()); + assertEquals(1, expungeResourceOperations.size()); + CapabilityStatementRestResourceOperationComponent operation = expungeResourceOperations.get(0); + assertEquals("expunge", operation.getName()); + assertEquals("http://localhost/baseR4/OperationDefinition/Patient-it-expunge", operation.getDefinition()); + assertEquals("This is the expunge operation", operation.getDocumentation()); + + // At Server Level + CapabilityStatementRestComponent rest = opDef.getRest().get(0); + operation = rest.getOperation().stream().filter(t -> t.getName().equals("expunge")).findAny().orElse(null); + assertNull(operation); + + // Fetch OperationDefinition + IdType id = new IdType("http://localhost/baseR4/OperationDefinition/Patient-it-expunge"); + RequestDetails requestDetails = createRequestDetails(rs); + OperationDefinition operationDefinition = (OperationDefinition) sc.readOperationDefinition(id, requestDetails); + assertEquals("Expunge", operationDefinition.getName()); + assertEquals("expunge", operationDefinition.getCode()); + assertEquals("http://localhost/baseR4/OperationDefinition/Patient-it-expunge", operationDefinition.getUrl()); + assertEquals("This is the expunge operation", operationDefinition.getDescription()); + assertFalse(operationDefinition.getSystem()); + assertTrue(operationDefinition.getType()); + assertTrue(operationDefinition.getInstance()); + + } + + @Test + public void testIncludeLastUpdatedSearchParam() throws Exception { + + class MyProvider implements IResourceProvider { + + @Override + public Class<DiagnosticReport> getResourceType() { + return DiagnosticReport.class; + } + + @Search + public List<IBaseResource> search(@OptionalParam(name = DiagnosticReport.SP_DATE) + DateRangeParam range, + + @Description(shortDefinition = "Only return resources which were last updated as specified by the given range") + @OptionalParam(name = "_lastUpdated") + DateRangeParam theLastUpdated + ) { + return null; + } + + } + + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new MyProvider()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { + }; + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement opDef = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + + validate(opDef); + + CapabilityStatementRestResourceComponent resource = opDef.getRest().get(0).getResource().get(0); + assertEquals("DiagnosticReport", resource.getType()); + List<String> searchParamNames = resource.getSearchParam().stream().map(t -> t.getName()).collect(Collectors.toList()); + assertThat(searchParamNames, containsInAnyOrder("date", "_lastUpdated")); + } + + @Test + public void testSystemLevelNamedQueryWithParameters() throws Exception { + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new NamedQueryPlainProvider()); + rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + validate(conformance); + + CapabilityStatementRestResourceComponent patient = conformance.getRestFirstRep().getResource().stream().filter(t -> t.getType().equals("Patient")).findFirst().orElseThrow(() -> new IllegalArgumentException()); + CapabilityStatementRestResourceOperationComponent operationComponent = patient.getOperation().get(0); + assertThat(operationComponent.getName(), is(NamedQueryPlainProvider.QUERY_NAME)); + + String operationReference = operationComponent.getDefinition(); + assertThat(operationReference, not(nullValue())); + + OperationDefinition operationDefinition = (OperationDefinition) sc.readOperationDefinition(new IdType(operationReference), createRequestDetails(rs)); + ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(operationDefinition)); + validate(operationDefinition); + assertThat(operationDefinition.getCode(), is(NamedQueryPlainProvider.QUERY_NAME)); + assertThat(operationDefinition.getName(), is("TestQuery")); + assertThat(operationDefinition.getStatus(), is(PublicationStatus.ACTIVE)); + assertThat(operationDefinition.getKind(), is(OperationKind.QUERY)); + assertThat(operationDefinition.getDescription(), is(NamedQueryPlainProvider.DESCRIPTION)); + assertThat(operationDefinition.getAffectsState(), is(false)); + assertThat("A system level search has no target resources", operationDefinition.getResource(), is(empty())); + assertThat(operationDefinition.getSystem(), is(true)); + assertThat(operationDefinition.getType(), is(false)); + assertThat(operationDefinition.getInstance(), is(false)); + List<OperationDefinitionParameterComponent> parameters = operationDefinition.getParameter(); + assertThat(parameters.size(), is(1)); + OperationDefinitionParameterComponent param = parameters.get(0); + assertThat(param.getName(), is(NamedQueryPlainProvider.SP_QUANTITY)); + assertThat(param.getType(), is("string")); + assertThat(param.getSearchTypeElement().asStringValue(), is(RestSearchParameterTypeEnum.QUANTITY.getCode())); + assertThat(param.getMin(), is(1)); + assertThat(param.getMax(), is("1")); + assertThat(param.getUse(), is(OperationParameterUse.IN)); + } + + @Test + public void testResourceLevelNamedQueryWithParameters() throws Exception { + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new NamedQueryResourceProvider()); + rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + validate(conformance); + + CapabilityStatementRestResourceComponent restComponent = conformance.getRestFirstRep().getResource().stream().filter(t -> t.getType().equals("Patient")).findFirst().orElseThrow(() -> new IllegalArgumentException()); + CapabilityStatementRestResourceOperationComponent operationComponent = restComponent.getOperation().get(0); + String operationReference = operationComponent.getDefinition(); + assertThat(operationReference, not(nullValue())); + + OperationDefinition operationDefinition = (OperationDefinition) sc.readOperationDefinition(new IdType(operationReference), createRequestDetails(rs)); + ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(operationDefinition)); + validate(operationDefinition); + assertThat("The operation name should be the code if no description is set", operationDefinition.getName(), is("TestQuery")); + String patientResourceName = "Patient"; + assertThat("A resource level search targets the resource of the provider it's defined in", operationDefinition.getResource().get(0).getValue(), is(patientResourceName)); + assertThat(operationDefinition.getSystem(), is(false)); + assertThat(operationDefinition.getType(), is(true)); + assertThat(operationDefinition.getInstance(), is(false)); + List<OperationDefinitionParameterComponent> parameters = operationDefinition.getParameter(); + assertThat(parameters.size(), is(1)); + OperationDefinitionParameterComponent param = parameters.get(0); + assertThat(param.getName(), is(NamedQueryResourceProvider.SP_PARAM)); + assertThat(param.getType(), is("string")); + assertThat(param.getSearchTypeElement().asStringValue(), is(RestSearchParameterTypeEnum.STRING.getCode())); + assertThat(param.getMin(), is(0)); + assertThat(param.getMax(), is("1")); + assertThat(param.getUse(), is(OperationParameterUse.IN)); + + List<CapabilityStatementRestResourceSearchParamComponent> patientResource = restComponent.getSearchParam(); + assertThat("Named query parameters should not appear in the resource search params", patientResource, is(empty())); + } + + @Test + public void testExtendedOperationAtTypeLevel() throws Exception { + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new TypeLevelOperationProvider()); + rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + + validate(conformance); + + CapabilityStatementRestResourceComponent patient = conformance.getRestFirstRep().getResource().stream().filter(t -> t.getType().equals("Patient")).findFirst().orElseThrow(() -> new IllegalArgumentException()); + List<CapabilityStatementRestResourceOperationComponent> operations = patient.getOperation(); + assertThat(operations.size(), is(1)); + assertThat(operations.get(0).getName(), is(TypeLevelOperationProvider.OPERATION_NAME)); + + OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType(operations.get(0).getDefinition()), createRequestDetails(rs)); + validate(opDef); + assertEquals(TypeLevelOperationProvider.OPERATION_NAME, opDef.getCode()); + assertThat(opDef.getSystem(), is(false)); + assertThat(opDef.getType(), is(true)); + assertThat(opDef.getInstance(), is(false)); + } + + @Test + public void testProfiledResourceStructureDefinitionLinks() throws Exception { + RestfulServer rs = new RestfulServer(myCtx); + rs.setResourceProviders(new ProfiledPatientProvider(), new MultipleProfilesPatientProvider()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance)); + + List<CapabilityStatementRestResourceComponent> resources = conformance.getRestFirstRep().getResource(); + CapabilityStatementRestResourceComponent patientResource = resources.stream() + .filter(resource -> "Patient".equals(resource.getType())) + .findFirst().get(); + assertThat(patientResource.getProfile(), containsString(PATIENT_SUB)); + } + + @Test + public void testRevIncludes_Explicit() throws Exception { + + class PatientResourceProvider implements IResourceProvider { + + @Override + public Class<Patient> getResourceType() { + return Patient.class; + } + + @Search + public List<Patient> search(@IncludeParam(reverse = true, allow = {"Observation:foo", "Provenance:bar"}) Set<Include> theRevIncludes) { + return Collections.emptyList(); + } + + } + + class ObservationResourceProvider implements IResourceProvider { + + @Override + public Class<Observation> getResourceType() { + return Observation.class; + } + + @Search + public List<Observation> search(@OptionalParam(name = "subject") ReferenceParam theSubject) { + return Collections.emptyList(); + } + + } + + RestfulServer rs = new RestfulServer(myCtx); + rs.setResourceProviders(new PatientResourceProvider(), new ObservationResourceProvider()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + sc.setRestResourceRevIncludesEnabled(true); + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance)); + + List<CapabilityStatementRestResourceComponent> resources = conformance.getRestFirstRep().getResource(); + CapabilityStatementRestResourceComponent patientResource = resources.stream() + .filter(resource -> "Patient".equals(resource.getType())) + .findFirst().get(); + assertThat(toStrings(patientResource.getSearchRevInclude()), containsInAnyOrder("Observation:foo", "Provenance:bar")); + } + + @Test + public void testRevIncludes_Inferred() throws Exception { + + class PatientResourceProvider implements IResourceProvider { + + @Override + public Class<Patient> getResourceType() { + return Patient.class; + } + + @Search + public List<Patient> search(@IncludeParam(reverse = true) Set<Include> theRevIncludes) { + return Collections.emptyList(); + } + + } + + class ObservationResourceProvider implements IResourceProvider { + + @Override + public Class<Observation> getResourceType() { + return Observation.class; + } + + @Search + public List<Observation> search(@OptionalParam(name = "subject") ReferenceParam theSubject) { + return Collections.emptyList(); + } + + } + + RestfulServer rs = new RestfulServer(myCtx); + rs.setResourceProviders(new PatientResourceProvider(), new ObservationResourceProvider()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + sc.setRestResourceRevIncludesEnabled(true); + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance)); + + List<CapabilityStatementRestResourceComponent> resources = conformance.getRestFirstRep().getResource(); + CapabilityStatementRestResourceComponent patientResource = resources.stream() + .filter(resource -> "Patient".equals(resource.getType())) + .findFirst().get(); + assertThat(toStrings(patientResource.getSearchRevInclude()), containsInAnyOrder("Observation:subject")); + } + + private List<String> toOperationIdParts(List<CapabilityStatementRestResourceOperationComponent> theOperation) { + ArrayList<String> retVal = Lists.newArrayList(); + for (CapabilityStatementRestResourceOperationComponent next : theOperation) { + retVal.add(new IdType(next.getDefinition()).getIdPart()); + } + return retVal; + } + + private List<String> toOperationNames(List<CapabilityStatementRestResourceOperationComponent> theOperation) { + ArrayList<String> retVal = Lists.newArrayList(); + for (CapabilityStatementRestResourceOperationComponent next : theOperation) { + retVal.add(next.getName()); + } + return retVal; + } + + private List<String> toOperationDefinitions(List<CapabilityStatementRestResourceOperationComponent> theOperation) { + ArrayList<String> retVal = Lists.newArrayList(); + for (CapabilityStatementRestResourceOperationComponent next : theOperation) { + retVal.add(next.getDefinition()); + } + return retVal; + } + + private String validate(IBaseResource theResource) { + String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(theResource); + ourLog.info("Def:\n{}", conf); + + ValidationResult result = myValidator.validateWithResult(conf); + OperationOutcome operationOutcome = (OperationOutcome) result.toOperationOutcome(); + String outcome = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(operationOutcome); + ourLog.info("Outcome: {}", outcome); + + assertTrue(result.isSuccessful(), outcome); + List<OperationOutcome.OperationOutcomeIssueComponent> warningsAndErrors = operationOutcome + .getIssue() + .stream() + .filter(t -> t.getSeverity().ordinal() <= OperationOutcome.IssueSeverity.WARNING.ordinal()) // <= because this enum has a strange order + .collect(Collectors.toList()); + assertThat(outcome, warningsAndErrors, is(empty())); + + return myCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(theResource); + } + + @SuppressWarnings("unused") + public static class ConditionalProvider implements IResourceProvider { + + @Create + public MethodOutcome create(@ResourceParam Patient thePatient, @ConditionalUrlParam String theConditionalUrl) { + return null; + } + + @Delete + public MethodOutcome delete(@IdParam IdType theId, @ConditionalUrlParam(supportsMultiple = true) String theConditionalUrl) { + return null; + } + + @Override + public Class<? extends IBaseResource> getResourceType() { + return Patient.class; + } + + @Update + public MethodOutcome update(@IdParam IdType theId, @ResourceParam Patient thePatient, @ConditionalUrlParam String theConditionalUrl) { + return null; + } + + } + + @SuppressWarnings("unused") + public static class InstanceHistoryProvider implements IResourceProvider { + @Override + public Class<? extends IBaseResource> getResourceType() { + return Patient.class; + } + + @History + public List<IBaseResource> history(@IdParam IdType theId) { + return null; + } + + } + + @SuppressWarnings("unused") + public static class MultiOptionalProvider { + + @Search(type = Patient.class) + public Patient findPatient(@Description(shortDefinition = "The patient's identifier") @OptionalParam(name = Patient.SP_IDENTIFIER) TokenParam theIdentifier, + @Description(shortDefinition = "The patient's name") @OptionalParam(name = Patient.SP_NAME) StringParam theName) { + return null; + } + + } + + @SuppressWarnings("unused") + public static class MultiTypeEncounterProvider implements IResourceProvider { + + @Operation(name = "someOp") + public IBundleProvider everything(HttpServletRequest theServletRequest, @IdParam IdType theId, + @OperationParam(name = "someOpParam1") DateType theStart, @OperationParam(name = "someOpParam2") Encounter theEnd) { + return null; + } + + @Override + public Class<? extends IBaseResource> getResourceType() { + return Encounter.class; + } + + @Validate + public IBundleProvider validate(HttpServletRequest theServletRequest, @IdParam IdType theId, @ResourceParam Encounter thePatient) { + return null; + } + + } + + @SuppressWarnings("unused") + public static class MultiTypePatientProvider implements IResourceProvider { + + @Operation(name = "someOp") + public IBundleProvider everything( + HttpServletRequest theServletRequest, + + @IdParam IdType theId, + + @Description(value = "Start description", example = {"2001", "2002"}) + @OperationParam(name = "someOpParam1") DateType theStart, + + @OperationParam(name = "someOpParam2") Patient theEnd) { + return null; + } - @Override - public Class<DiagnosticReport> getResourceType() { - return DiagnosticReport.class; - } + @Override + public Class<? extends IBaseResource> getResourceType() { + return Patient.class; + } + + @Validate + public IBundleProvider validate(HttpServletRequest theServletRequest, @IdParam IdType theId, @ResourceParam Patient thePatient) { + return null; + } - @Search - public List<IBaseResource> search(@OptionalParam(name = DiagnosticReport.SP_DATE) - DateRangeParam range, + } - @Description(shortDefinition = "Only return resources which were last updated as specified by the given range") - @OptionalParam(name = "_lastUpdated") - DateRangeParam theLastUpdated - ) { - return null; - } - - } - - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new MyProvider()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { - }; - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement opDef = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - - validate(opDef); - - CapabilityStatementRestResourceComponent resource = opDef.getRest().get(0).getResource().get(0); - assertEquals("DiagnosticReport", resource.getType()); - List<String> searchParamNames = resource.getSearchParam().stream().map(t -> t.getName()).collect(Collectors.toList()); - assertThat(searchParamNames, containsInAnyOrder("date", "_lastUpdated")); - } - - @Test - public void testSystemLevelNamedQueryWithParameters() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new NamedQueryPlainProvider()); - rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - validate(conformance); - - CapabilityStatementRestComponent restComponent = conformance.getRest().get(0); - CapabilityStatementRestResourceOperationComponent operationComponent = restComponent.getOperation().get(0); - assertThat(operationComponent.getName(), is(NamedQueryPlainProvider.QUERY_NAME)); - - String operationReference = operationComponent.getDefinition(); - assertThat(operationReference, not(nullValue())); - - OperationDefinition operationDefinition = (OperationDefinition) sc.readOperationDefinition(new IdType(operationReference), createRequestDetails(rs)); - ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(operationDefinition)); - validate(operationDefinition); - assertThat(operationDefinition.getCode(), is(NamedQueryPlainProvider.QUERY_NAME)); - assertThat(operationDefinition.getName(), is("Search_" + NamedQueryPlainProvider.QUERY_NAME)); - assertThat(operationDefinition.getStatus(), is(PublicationStatus.ACTIVE)); - assertThat(operationDefinition.getKind(), is(OperationKind.QUERY)); - assertThat(operationDefinition.getDescription(), is(NamedQueryPlainProvider.DESCRIPTION)); - assertThat(operationDefinition.getAffectsState(), is(false)); - assertThat("A system level search has no target resources", operationDefinition.getResource(), is(empty())); - assertThat(operationDefinition.getSystem(), is(true)); - assertThat(operationDefinition.getType(), is(false)); - assertThat(operationDefinition.getInstance(), is(false)); - List<OperationDefinitionParameterComponent> parameters = operationDefinition.getParameter(); - assertThat(parameters.size(), is(1)); - OperationDefinitionParameterComponent param = parameters.get(0); - assertThat(param.getName(), is(NamedQueryPlainProvider.SP_QUANTITY)); - assertThat(param.getType(), is("string")); - assertThat(param.getSearchTypeElement().asStringValue(), is(RestSearchParameterTypeEnum.QUANTITY.getCode())); - assertThat(param.getMin(), is(1)); - assertThat(param.getMax(), is("1")); - assertThat(param.getUse(), is(OperationParameterUse.IN)); - } - - @Test - public void testResourceLevelNamedQueryWithParameters() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new NamedQueryResourceProvider()); - rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - validate(conformance); - - CapabilityStatementRestComponent restComponent = conformance.getRest().get(0); - CapabilityStatementRestResourceOperationComponent operationComponent = restComponent.getOperation().get(0); - String operationReference = operationComponent.getDefinition(); - assertThat(operationReference, not(nullValue())); - - OperationDefinition operationDefinition = (OperationDefinition) sc.readOperationDefinition(new IdType(operationReference), createRequestDetails(rs)); - ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(operationDefinition)); - validate(operationDefinition); - assertThat("The operation name should be the code if no description is set", operationDefinition.getName(), is("Search_" + NamedQueryResourceProvider.QUERY_NAME)); - String patientResourceName = "Patient"; - assertThat("A resource level search targets the resource of the provider it's defined in", operationDefinition.getResource().get(0).getValue(), is(patientResourceName)); - assertThat(operationDefinition.getSystem(), is(false)); - assertThat(operationDefinition.getType(), is(true)); - assertThat(operationDefinition.getInstance(), is(false)); - List<OperationDefinitionParameterComponent> parameters = operationDefinition.getParameter(); - assertThat(parameters.size(), is(1)); - OperationDefinitionParameterComponent param = parameters.get(0); - assertThat(param.getName(), is(NamedQueryResourceProvider.SP_PARAM)); - assertThat(param.getType(), is("string")); - assertThat(param.getSearchTypeElement().asStringValue(), is(RestSearchParameterTypeEnum.STRING.getCode())); - assertThat(param.getMin(), is(0)); - assertThat(param.getMax(), is("1")); - assertThat(param.getUse(), is(OperationParameterUse.IN)); - - CapabilityStatementRestResourceComponent patientResource = restComponent.getResource().stream() - .filter(r -> patientResourceName.equals(r.getType())) - .findAny().get(); - assertThat("Named query parameters should not appear in the resource search params", patientResource.getSearchParam(), is(empty())); - } - - @Test - public void testExtendedOperationAtTypeLevel() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new TypeLevelOperationProvider()); - rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - - validate(conformance); - - List<CapabilityStatementRestResourceOperationComponent> operations = conformance.getRest().get(0).getOperation(); - assertThat(operations.size(), is(1)); - assertThat(operations.get(0).getName(), is(TypeLevelOperationProvider.OPERATION_NAME)); - - OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType(operations.get(0).getDefinition()), createRequestDetails(rs)); - validate(opDef); - assertEquals(TypeLevelOperationProvider.OPERATION_NAME, opDef.getCode()); - assertThat(opDef.getSystem(), is(false)); - assertThat(opDef.getType(), is(true)); - assertThat(opDef.getInstance(), is(false)); - } - - @Test - public void testProfiledResourceStructureDefinitionLinks() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setResourceProviders(new ProfiledPatientProvider(), new MultipleProfilesPatientProvider()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance)); - - List<CapabilityStatementRestResourceComponent> resources = conformance.getRestFirstRep().getResource(); - CapabilityStatementRestResourceComponent patientResource = resources.stream() - .filter(resource -> "Patient".equals(resource.getType())) - .findFirst().get(); - assertThat(patientResource.getProfile(), containsString(PATIENT_SUB)); - } - - @Test - public void testRevIncludes_Explicit() throws Exception { - - class PatientResourceProvider implements IResourceProvider { - - @Override - public Class<Patient> getResourceType() { - return Patient.class; - } - - @Search - public List<Patient> search(@IncludeParam(reverse = true, allow = {"Observation:foo", "Provenance:bar"}) Set<Include> theRevIncludes) { - return Collections.emptyList(); - } - - } - - class ObservationResourceProvider implements IResourceProvider { - - @Override - public Class<Observation> getResourceType() { - return Observation.class; - } - - @Search - public List<Observation> search(@OptionalParam(name = "subject") ReferenceParam theSubject) { - return Collections.emptyList(); - } - - } - - RestfulServer rs = new RestfulServer(myCtx); - rs.setResourceProviders(new PatientResourceProvider(), new ObservationResourceProvider()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - sc.setRestResourceRevIncludesEnabled(true); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance)); - - List<CapabilityStatementRestResourceComponent> resources = conformance.getRestFirstRep().getResource(); - CapabilityStatementRestResourceComponent patientResource = resources.stream() - .filter(resource -> "Patient".equals(resource.getType())) - .findFirst().get(); - assertThat(toStrings(patientResource.getSearchRevInclude()), containsInAnyOrder("Observation:foo", "Provenance:bar")); - } + @SuppressWarnings("unused") + public static class NonConditionalProvider implements IResourceProvider { - @Test - public void testRevIncludes_Inferred() throws Exception { + @Create + public MethodOutcome create(@ResourceParam Patient thePatient) { + return null; + } - class PatientResourceProvider implements IResourceProvider { + @Delete + public MethodOutcome delete(@IdParam IdType theId) { + return null; + } - @Override - public Class<Patient> getResourceType() { - return Patient.class; - } + @Override + public Class<? extends IBaseResource> getResourceType() { + return Patient.class; + } - @Search - public List<Patient> search(@IncludeParam(reverse = true) Set<Include> theRevIncludes) { - return Collections.emptyList(); - } - - } - - class ObservationResourceProvider implements IResourceProvider { - - @Override - public Class<Observation> getResourceType() { - return Observation.class; - } - - @Search - public List<Observation> search(@OptionalParam(name = "subject") ReferenceParam theSubject) { - return Collections.emptyList(); - } + @Update + public MethodOutcome update(@IdParam IdType theId, @ResourceParam Patient thePatient) { + return null; + } - } - - RestfulServer rs = new RestfulServer(myCtx); - rs.setResourceProviders(new PatientResourceProvider(), new ObservationResourceProvider()); + } - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - sc.setRestResourceRevIncludesEnabled(true); - rs.setServerConformanceProvider(sc); + @SuppressWarnings("unused") + public static class ProviderWithExtendedOperationReturningBundle implements IResourceProvider { - rs.init(createServletConfig()); + @Operation(name = "everything", idempotent = true) + public IBundleProvider everything(HttpServletRequest theServletRequest, @IdParam IdType theId, @OperationParam(name = "start") DateType theStart, + @OperationParam(name = "end") DateType theEnd) { + return null; + } - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance)); - - List<CapabilityStatementRestResourceComponent> resources = conformance.getRestFirstRep().getResource(); - CapabilityStatementRestResourceComponent patientResource = resources.stream() - .filter(resource -> "Patient".equals(resource.getType())) - .findFirst().get(); - assertThat(toStrings(patientResource.getSearchRevInclude()), containsInAnyOrder("Observation:subject")); - } + @Override + public Class<? extends IBaseResource> getResourceType() { + return Patient.class; + } - private List<String> toOperationIdParts(List<CapabilityStatementRestResourceOperationComponent> theOperation) { - ArrayList<String> retVal = Lists.newArrayList(); - for (CapabilityStatementRestResourceOperationComponent next : theOperation) { - retVal.add(new IdType(next.getDefinition()).getIdPart()); - } - return retVal; - } - - private List<String> toOperationNames(List<CapabilityStatementRestResourceOperationComponent> theOperation) { - ArrayList<String> retVal = Lists.newArrayList(); - for (CapabilityStatementRestResourceOperationComponent next : theOperation) { - retVal.add(next.getName()); - } - return retVal; - } - - private String validate(IBaseResource theResource) { - String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(theResource); - ourLog.info("Def:\n{}", conf); - - ValidationResult result = myValidator.validateWithResult(conf); - OperationOutcome operationOutcome = (OperationOutcome) result.toOperationOutcome(); - String outcome = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(operationOutcome); - ourLog.info("Outcome: {}", outcome); - - assertTrue(result.isSuccessful(), outcome); - List<OperationOutcome.OperationOutcomeIssueComponent> warningsAndErrors = operationOutcome - .getIssue() - .stream() - .filter(t -> t.getSeverity().ordinal() <= OperationOutcome.IssueSeverity.WARNING.ordinal()) // <= because this enum has a strange order - .collect(Collectors.toList()); - assertThat(outcome, warningsAndErrors, is(empty())); - - return myCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(theResource); - } - - @SuppressWarnings("unused") - public static class ConditionalProvider implements IResourceProvider { - - @Create - public MethodOutcome create(@ResourceParam Patient thePatient, @ConditionalUrlParam String theConditionalUrl) { - return null; - } - - @Delete - public MethodOutcome delete(@IdParam IdType theId, @ConditionalUrlParam(supportsMultiple = true) String theConditionalUrl) { - return null; - } - - @Override - public Class<? extends IBaseResource> getResourceType() { - return Patient.class; - } - - @Update - public MethodOutcome update(@IdParam IdType theId, @ResourceParam Patient thePatient, @ConditionalUrlParam String theConditionalUrl) { - return null; - } - - } - - @SuppressWarnings("unused") - public static class InstanceHistoryProvider implements IResourceProvider { - @Override - public Class<? extends IBaseResource> getResourceType() { - return Patient.class; - } - - @History - public List<IBaseResource> history(@IdParam IdType theId) { - return null; - } - - } - - @SuppressWarnings("unused") - public static class MultiOptionalProvider { - - @Search(type = Patient.class) - public Patient findPatient(@Description(shortDefinition = "The patient's identifier") @OptionalParam(name = Patient.SP_IDENTIFIER) TokenParam theIdentifier, - @Description(shortDefinition = "The patient's name") @OptionalParam(name = Patient.SP_NAME) StringParam theName) { - return null; - } - - } - - @SuppressWarnings("unused") - public static class MultiTypeEncounterProvider implements IResourceProvider { - - @Operation(name = "someOp") - public IBundleProvider everything(HttpServletRequest theServletRequest, @IdParam IdType theId, - @OperationParam(name = "someOpParam1") DateType theStart, @OperationParam(name = "someOpParam2") Encounter theEnd) { - return null; - } - - @Override - public Class<? extends IBaseResource> getResourceType() { - return Encounter.class; - } - - @Validate - public IBundleProvider validate(HttpServletRequest theServletRequest, @IdParam IdType theId, @ResourceParam Encounter thePatient) { - return null; - } - - } - - @SuppressWarnings("unused") - public static class MultiTypePatientProvider implements IResourceProvider { - - @Operation(name = "someOp") - public IBundleProvider everything(HttpServletRequest theServletRequest, @IdParam IdType theId, - @OperationParam(name = "someOpParam1") DateType theStart, @OperationParam(name = "someOpParam2") Patient theEnd) { - return null; - } - - @Override - public Class<? extends IBaseResource> getResourceType() { - return Patient.class; - } - - @Validate - public IBundleProvider validate(HttpServletRequest theServletRequest, @IdParam IdType theId, @ResourceParam Patient thePatient) { - return null; - } - - } - - @SuppressWarnings("unused") - public static class NonConditionalProvider implements IResourceProvider { - - @Create - public MethodOutcome create(@ResourceParam Patient thePatient) { - return null; - } + } - @Delete - public MethodOutcome delete(@IdParam IdType theId) { - return null; - } - - @Override - public Class<? extends IBaseResource> getResourceType() { - return Patient.class; - } + @SuppressWarnings("unused") + public static class ProviderWithRequiredAndOptional { - @Update - public MethodOutcome update(@IdParam IdType theId, @ResourceParam Patient thePatient) { - return null; - } + @Description(shortDefinition = "This is a search for stuff!") + @Search + public List<DiagnosticReport> findDiagnosticReportsByPatient(@RequiredParam(name = DiagnosticReport.SP_SUBJECT + '.' + Patient.SP_IDENTIFIER) TokenParam thePatientId, + @OptionalParam(name = DiagnosticReport.SP_CODE) TokenOrListParam theNames, @OptionalParam(name = DiagnosticReport.SP_DATE) DateRangeParam theDateRange, + @IncludeParam(allow = {"DiagnosticReport.result"}) Set<Include> theIncludes) throws Exception { + return null; + } - } + } - @SuppressWarnings("unused") - public static class PlainProviderWithExtendedOperationOnNoType { + @SuppressWarnings("unused") + public static class ReadProvider { - @Operation(name = "plain", idempotent = true, returnParameters = {@OperationParam(min = 1, max = 2, name = "out1", type = StringType.class)}) - public IBundleProvider everything(HttpServletRequest theServletRequest, @IdParam IdType theId, @OperationParam(name = "start") DateType theStart, - @OperationParam(name = "end") DateType theEnd) { - return null; - } + @Search(type = Patient.class) + public Patient findPatient(@Description(shortDefinition = "The patient's identifier (MRN or other card number)") @RequiredParam(name = Patient.SP_IDENTIFIER) TokenParam theIdentifier) { + return null; + } - } + @Read(version = false) + public Patient readPatient(@IdParam IdType theId) { + return null; + } - @SuppressWarnings("unused") - public static class ProviderWithExtendedOperationReturningBundle implements IResourceProvider { + } - @Operation(name = "everything", idempotent = true) - public IBundleProvider everything(HttpServletRequest theServletRequest, @IdParam IdType theId, @OperationParam(name = "start") DateType theStart, - @OperationParam(name = "end") DateType theEnd) { - return null; - } + @SuppressWarnings("unused") + public static class SearchProvider { - @Override - public Class<? extends IBaseResource> getResourceType() { - return Patient.class; - } + @Search(type = Patient.class) + public Patient findPatient1(@Description(shortDefinition = "The patient's identifier (MRN or other card number)") @RequiredParam(name = Patient.SP_IDENTIFIER) TokenParam theIdentifier) { + return null; + } - } + @Search(type = Patient.class) + public Patient findPatient2( + @Description(shortDefinition = "All patients linked to the given patient") @OptionalParam(name = "link", targetTypes = {Patient.class}) ReferenceAndListParam theLink) { + return null; + } - @SuppressWarnings("unused") - public static class ProviderWithRequiredAndOptional { + } - @Description(shortDefinition = "This is a search for stuff!") - @Search - public List<DiagnosticReport> findDiagnosticReportsByPatient(@RequiredParam(name = DiagnosticReport.SP_SUBJECT + '.' + Patient.SP_IDENTIFIER) TokenParam thePatientId, - @OptionalParam(name = DiagnosticReport.SP_CODE) TokenOrListParam theNames, @OptionalParam(name = DiagnosticReport.SP_DATE) DateRangeParam theDateRange, - @IncludeParam(allow = {"DiagnosticReport.result"}) Set<Include> theIncludes) throws Exception { - return null; - } + @SuppressWarnings("unused") + public static class SearchProviderWithWhitelist { - } + @Search(type = Patient.class) + public Patient findPatient1(@Description(shortDefinition = "The organization at which this person is a patient") @RequiredParam(name = Patient.SP_ORGANIZATION, chainWhitelist = {"foo", + "bar"}) ReferenceAndListParam theIdentifier) { + return null; + } - @SuppressWarnings("unused") - public static class ReadProvider { + } - @Search(type = Patient.class) - public Patient findPatient(@Description(shortDefinition = "The patient's identifier (MRN or other card number)") @RequiredParam(name = Patient.SP_IDENTIFIER) TokenParam theIdentifier) { - return null; - } + @SuppressWarnings("unused") + public static class SearchProviderWithListNoType implements IResourceProvider { - @Read(version = false) - public Patient readPatient(@IdParam IdType theId) { - return null; - } + @Override + public Class<? extends IBaseResource> getResourceType() { + return Patient.class; + } - } - @SuppressWarnings("unused") - public static class SearchProvider { + @Search() + public List<Patient> findPatient1(@Description(shortDefinition = "The organization at which this person is a patient") @RequiredParam(name = Patient.SP_ORGANIZATION) ReferenceAndListParam theIdentifier) { + return null; + } - @Search(type = Patient.class) - public Patient findPatient1(@Description(shortDefinition = "The patient's identifier (MRN or other card number)") @RequiredParam(name = Patient.SP_IDENTIFIER) TokenParam theIdentifier) { - return null; - } + } - @Search(type = Patient.class) - public Patient findPatient2( - @Description(shortDefinition = "All patients linked to the given patient") @OptionalParam(name = "link", targetTypes = {Patient.class}) ReferenceAndListParam theLink) { - return null; - } + @SuppressWarnings("unused") + public static class SearchProviderWithListWithType implements IResourceProvider { - } + @Override + public Class<? extends IBaseResource> getResourceType() { + return Patient.class; + } - @SuppressWarnings("unused") - public static class SearchProviderWithWhitelist { - @Search(type = Patient.class) - public Patient findPatient1(@Description(shortDefinition = "The organization at which this person is a patient") @RequiredParam(name = Patient.SP_ORGANIZATION, chainWhitelist = {"foo", - "bar"}) ReferenceAndListParam theIdentifier) { - return null; - } + @Search(type = Patient.class) + public List<Patient> findPatient1(@Description(shortDefinition = "The organization at which this person is a patient") @RequiredParam(name = Patient.SP_ORGANIZATION) ReferenceAndListParam theIdentifier) { + return null; + } - } + } - @SuppressWarnings("unused") - public static class SearchProviderWithListNoType implements IResourceProvider { + public static class SystemHistoryProvider { - @Override - public Class<? extends IBaseResource> getResourceType() { - return Patient.class; - } + @History + public List<IBaseResource> history() { + return null; + } + } - @Search() - public List<Patient> findPatient1(@Description(shortDefinition = "The organization at which this person is a patient") @RequiredParam(name = Patient.SP_ORGANIZATION) ReferenceAndListParam theIdentifier) { - return null; - } + public static class TypeHistoryProvider implements IResourceProvider { - } + @Override + public Class<? extends IBaseResource> getResourceType() { + return Patient.class; + } - @SuppressWarnings("unused") - public static class SearchProviderWithListWithType implements IResourceProvider { + @History + public List<IBaseResource> history() { + return null; + } - @Override - public Class<? extends IBaseResource> getResourceType() { - return Patient.class; - } + } + @SuppressWarnings("unused") + public static class VreadProvider { - @Search(type = Patient.class) - public List<Patient> findPatient1(@Description(shortDefinition = "The organization at which this person is a patient") @RequiredParam(name = Patient.SP_ORGANIZATION) ReferenceAndListParam theIdentifier) { - return null; - } + @Search(type = Patient.class) + public Patient findPatient(@Description(shortDefinition = "The patient's identifier (MRN or other card number)") @RequiredParam(name = Patient.SP_IDENTIFIER) TokenParam theIdentifier) { + return null; + } - } + @Read(version = true) + public Patient readPatient(@IdParam IdType theId) { + return null; + } - public static class SystemHistoryProvider { + } - @History - public List<IBaseResource> history() { - return null; - } + public static class TypeLevelOperationProvider implements IResourceProvider { - } + public static final String OPERATION_NAME = "op"; - public static class TypeHistoryProvider implements IResourceProvider { + @Operation(name = OPERATION_NAME, idempotent = true) + public IBundleProvider op() { + return null; + } - @Override - public Class<? extends IBaseResource> getResourceType() { - return Patient.class; - } + @Override + public Class<? extends IBaseResource> getResourceType() { + return Patient.class; + } - @History - public List<IBaseResource> history() { - return null; - } + } - } + public static class NamedQueryPlainProvider { - @SuppressWarnings("unused") - public static class VreadProvider { + public static final String QUERY_NAME = "testQuery"; + public static final String DESCRIPTION = "A query description"; + public static final String SP_QUANTITY = "quantity"; - @Search(type = Patient.class) - public Patient findPatient(@Description(shortDefinition = "The patient's identifier (MRN or other card number)") @RequiredParam(name = Patient.SP_IDENTIFIER) TokenParam theIdentifier) { - return null; - } + @Search(queryName = QUERY_NAME, type = Patient.class) + @Description(formalDefinition = DESCRIPTION) + public Bundle findAllGivenParameter(@RequiredParam(name = SP_QUANTITY) QuantityParam quantity) { + return null; + } + } - @Read(version = true) - public Patient readPatient(@IdParam IdType theId) { - return null; - } + public static class NamedQueryResourceProvider implements IResourceProvider { - } + public static final String QUERY_NAME = "testQuery"; + public static final String SP_PARAM = "param"; - public static class TypeLevelOperationProvider implements IResourceProvider { + @Override + public Class<? extends IBaseResource> getResourceType() { + return Patient.class; + } - public static final String OPERATION_NAME = "op"; + @Search(queryName = QUERY_NAME) + public Bundle findAllGivenParameter(@OptionalParam(name = SP_PARAM) StringParam param) { + return null; + } - @Operation(name = OPERATION_NAME, idempotent = true) - public IBundleProvider op() { - return null; - } + } - @Override - public Class<? extends IBaseResource> getResourceType() { - return Patient.class; - } + public static class ProfiledPatientProvider implements IResourceProvider { - } + @Override + public Class<? extends IBaseResource> getResourceType() { + return PatientSubSub2.class; + } - public static class NamedQueryPlainProvider { + @Search + public List<PatientSubSub2> find() { + return null; + } + } - public static final String QUERY_NAME = "testQuery"; - public static final String DESCRIPTION = "A query description"; - public static final String SP_QUANTITY = "quantity"; + public static class MultipleProfilesPatientProvider implements IResourceProvider { - @Search(queryName = QUERY_NAME) - @Description(formalDefinition = DESCRIPTION) - public Bundle findAllGivenParameter(@RequiredParam(name = SP_QUANTITY) QuantityParam quantity) { - return null; - } - } + @Override + public Class<? extends IBaseResource> getResourceType() { + return PatientSubSub.class; + } - public static class NamedQueryResourceProvider implements IResourceProvider { + @Read(type = PatientTripleSub.class) + public PatientTripleSub read(@IdParam IdType theId) { + return null; + } - public static final String QUERY_NAME = "testQuery"; - public static final String SP_PARAM = "param"; + } - @Override - public Class<? extends IBaseResource> getResourceType() { - return Patient.class; - } + @ResourceDef(id = PATIENT_SUB) + public static class PatientSub extends Patient { + } - @Search(queryName = QUERY_NAME) - public Bundle findAllGivenParameter(@OptionalParam(name = SP_PARAM) StringParam param) { - return null; - } + @ResourceDef(id = PATIENT_SUB_SUB) + public static class PatientSubSub extends PatientSub { + } - } + @ResourceDef(id = PATIENT_SUB_SUB_2) + public static class PatientSubSub2 extends PatientSub { + } - public static class ProfiledPatientProvider implements IResourceProvider { + @ResourceDef(id = PATIENT_TRIPLE_SUB) + public static class PatientTripleSub extends PatientSubSub { + } - @Override - public Class<? extends IBaseResource> getResourceType() { - return PatientSubSub2.class; - } + private static Set<String> toStrings(Collection<? extends IPrimitiveType> theType) { + HashSet<String> retVal = new HashSet<String>(); + for (IPrimitiveType next : theType) { + retVal.add(next.getValueAsString()); + } + return retVal; + } - @Search - public List<PatientSubSub2> find() { - return null; - } - } - - public static class MultipleProfilesPatientProvider implements IResourceProvider { - - @Override - public Class<? extends IBaseResource> getResourceType() { - return PatientSubSub.class; - } - - @Read(type = PatientTripleSub.class) - public PatientTripleSub read(@IdParam IdType theId) { - return null; - } - - } - - @ResourceDef(id = PATIENT_SUB) - public static class PatientSub extends Patient { - } - - @ResourceDef(id = PATIENT_SUB_SUB) - public static class PatientSubSub extends PatientSub { - } - - @ResourceDef(id = PATIENT_SUB_SUB_2) - public static class PatientSubSub2 extends PatientSub { - } - - @ResourceDef(id = PATIENT_TRIPLE_SUB) - public static class PatientTripleSub extends PatientSubSub { - } - - private static Set<String> toStrings(Collection<? extends IPrimitiveType> theType) { - HashSet<String> retVal = new HashSet<String>(); - for (IPrimitiveType next : theType) { - retVal.add(next.getValueAsString()); - } - return retVal; - } - - @AfterAll - public static void afterClassClearContext() { - TestUtil.clearAllStaticFieldsForUnitTest(); - } + @AfterAll + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/SchemaValidationDstu3Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/SchemaValidationDstu3Test.java index a87dd300e02..dcc4b7287f4 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/SchemaValidationDstu3Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/SchemaValidationDstu3Test.java @@ -13,6 +13,10 @@ import static org.junit.jupiter.api.Assertions.assertFalse; public class SchemaValidationDstu3Test { + static { + + } + private static FhirContext ourCtx = FhirContext.forDstu3(); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SchemaValidationDstu3Test.class); diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/SchemaValidationR4Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/SchemaValidationR4Test.java index f3794a85181..1d870ed32ac 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/SchemaValidationR4Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/SchemaValidationR4Test.java @@ -5,40 +5,62 @@ import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.validation.FhirValidator; import ca.uhn.fhir.validation.ValidationResult; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.SAXParserFactory; +import javax.xml.transform.TransformerFactory; +import javax.xml.xpath.XPathFactory; + +import java.security.CodeSource; +import java.text.MessageFormat; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.junit.jupiter.api.Assertions.assertFalse; public class SchemaValidationR4Test { - + private static final Logger ourLog = LoggerFactory.getLogger(SchemaValidationR4Test.class); private static FhirContext ourCtx = FhirContext.forDstu3(); - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SchemaValidationR4Test.class); + @BeforeEach + public void before() { + ourLog.info(getJaxpImplementationInfo("DocumentBuilderFactory", DocumentBuilderFactory.newInstance().getClass())); + ourLog.info(getJaxpImplementationInfo("XPathFactory", XPathFactory.newInstance().getClass())); + ourLog.info(getJaxpImplementationInfo("TransformerFactory", TransformerFactory.newInstance().getClass())); + ourLog.info(getJaxpImplementationInfo("SAXParserFactory", SAXParserFactory.newInstance().getClass())); + + // The following code can be used to force the built in schema parser to be used + // System.setProperty("jaxp.debug", "1"); + // System.setProperty("javax.xml.validation.SchemaFactory:http://www.w3.org/2001/XMLSchema", "com.sun.org.apache.xerces.internal.jaxp.validation.XMLSchemaFactory"); + } /** * See #339 - * + * <p> * https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Processing */ @Test public void testXxe() { //@formatter:off String input = - "<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n" + - "<!DOCTYPE foo [ \n" + - "<!ELEMENT foo ANY >\n" + - "<!ENTITY xxe SYSTEM \"file:///etc/passwd\" >]>" + - "<Patient xmlns=\"http://hl7.org/fhir\">" + - "<text>" + - "<status value=\"generated\"/>" + - "<div xmlns=\"http://www.w3.org/1999/xhtml\">TEXT &xxe; TEXT</div>\n" + + "<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n" + + "<!DOCTYPE foo [ \n" + + "<!ELEMENT foo ANY >\n" + + "<!ENTITY xxe SYSTEM \"file:///etc/passwd\" >]>" + + "<Patient xmlns=\"http://hl7.org/fhir\">" + + "<text>" + + "<status value=\"generated\"/>" + + "<div xmlns=\"http://www.w3.org/1999/xhtml\">TEXT &xxe; TEXT</div>\n" + "</text>" + - "<address>" + - "<line value=\"FOO\"/>" + + "<address>" + + "<line value=\"FOO\"/>" + "</address>" + - "</Patient>"; + "</Patient>"; //@formatter:on FhirValidator val = ourCtx.newValidator(); @@ -49,11 +71,26 @@ public class SchemaValidationR4Test { String encoded = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(result.toOperationOutcome()); ourLog.info(encoded); + /* + * If this starts failing, check if xerces (or another simiar library) has slipped in + * to the classpath as a dependency. The logs in the @Before method should include this: + * DocumentBuilderFactory implementation: com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl loaded from: Java Runtime + */ + assertFalse(result.isSuccessful()); assertThat(encoded, containsString("passwd")); assertThat(encoded, containsString("accessExternalDTD")); } + private static String getJaxpImplementationInfo(String componentName, Class componentClass) { + CodeSource source = componentClass.getProtectionDomain().getCodeSource(); + return MessageFormat.format( + "{0} implementation: {1} loaded from: {2}", + componentName, + componentClass.getName(), + source == null ? "Java Runtime" : source.getLocation()); + } + @AfterAll public static void afterClassClearContext() { TestUtil.clearAllStaticFieldsForUnitTest(); diff --git a/hapi-tinder-plugin/pom.xml b/hapi-tinder-plugin/pom.xml index 1da19fdaf24..6be3a8a978f 100644 --- a/hapi-tinder-plugin/pom.xml +++ b/hapi-tinder-plugin/pom.xml @@ -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> @@ -58,37 +58,37 @@ <dependency> <groupId>ca.uhn.hapi.fhir</groupId> <artifactId>hapi-fhir-structures-dstu3</artifactId> - <version>5.4.0-PRE7-SNAPSHOT</version> + <version>5.4.0-PRE8-SNAPSHOT</version> </dependency> <dependency> <groupId>ca.uhn.hapi.fhir</groupId> <artifactId>hapi-fhir-structures-hl7org-dstu2</artifactId> - <version>5.4.0-PRE7-SNAPSHOT</version> + <version>5.4.0-PRE8-SNAPSHOT</version> </dependency> <dependency> <groupId>ca.uhn.hapi.fhir</groupId> <artifactId>hapi-fhir-structures-r4</artifactId> - <version>5.4.0-PRE7-SNAPSHOT</version> + <version>5.4.0-PRE8-SNAPSHOT</version> </dependency> <dependency> <groupId>ca.uhn.hapi.fhir</groupId> <artifactId>hapi-fhir-structures-r5</artifactId> - <version>5.4.0-PRE7-SNAPSHOT</version> + <version>5.4.0-PRE8-SNAPSHOT</version> </dependency> <dependency> <groupId>ca.uhn.hapi.fhir</groupId> <artifactId>hapi-fhir-validation-resources-dstu2</artifactId> - <version>5.4.0-PRE7-SNAPSHOT</version> + <version>5.4.0-PRE8-SNAPSHOT</version> </dependency> <dependency> <groupId>ca.uhn.hapi.fhir</groupId> <artifactId>hapi-fhir-validation-resources-dstu3</artifactId> - <version>5.4.0-PRE7-SNAPSHOT</version> + <version>5.4.0-PRE8-SNAPSHOT</version> </dependency> <dependency> <groupId>ca.uhn.hapi.fhir</groupId> <artifactId>hapi-fhir-validation-resources-r4</artifactId> - <version>5.4.0-PRE7-SNAPSHOT</version> + <version>5.4.0-PRE8-SNAPSHOT</version> </dependency> <dependency> <groupId>org.apache.velocity</groupId> diff --git a/hapi-tinder-test/pom.xml b/hapi-tinder-test/pom.xml index 2f98895c12d..247b0328ab6 100644 --- a/hapi-tinder-test/pom.xml +++ b/hapi-tinder-test/pom.xml @@ -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> diff --git a/pom.xml b/pom.xml index 109923b7ecc..38b66b8cffa 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ <groupId>ca.uhn.hapi.fhir</groupId> <artifactId>hapi-fhir</artifactId> <packaging>pom</packaging> - <version>5.4.0-PRE7-SNAPSHOT</version> + <version>5.4.0-PRE8-SNAPSHOT</version> <name>HAPI-FHIR</name> <description>An open-source implementation of the FHIR specification in Java.</description> <url>https://hapifhir.io</url> @@ -780,12 +780,13 @@ <jaxb_api_version>2.3.1</jaxb_api_version> <jaxb_core_version>2.3.0.1</jaxb_core_version> <jaxb_runtime_version>3.0.0</jaxb_runtime_version> - <jena_version>3.16.0</jena_version> + <jena_version>3.17.0</jena_version> <jersey_version>3.0.0</jersey_version> <!-- 9.4.17 seems to have issues --> <jetty_version>9.4.39.v20210325</jetty_version> <jsr305_version>3.0.2</jsr305_version> <junit_version>5.7.1</junit_version> + <flexmark_version>0.50.40</flexmark_version> <flyway_version>6.5.4</flyway_version> <hibernate_version>5.4.30.Final</hibernate_version> <hibernate_search_version>6.0.2.Final</hibernate_search_version> @@ -962,6 +963,21 @@ <artifactId>javax.mail</artifactId> <version>1.6.1</version> </dependency> + <dependency> + <groupId>com.vladsch.flexmark</groupId> + <artifactId>flexmark</artifactId> + <version>${flexmark_version}</version> + </dependency> + <dependency> + <groupId>com.vladsch.flexmark</groupId> + <artifactId>flexmark-ext-tables</artifactId> + <version>${flexmark_version}</version> + </dependency> + <dependency> + <groupId>com.vladsch.flexmark</groupId> + <artifactId>flexmark-profile-pegdown</artifactId> + <version>${flexmark_version}</version> + </dependency> <dependency> <groupId>commons-cli</groupId> <artifactId>commons-cli</artifactId> @@ -1107,13 +1123,28 @@ <dependency> <groupId>io.swagger</groupId> <artifactId>swagger-annotations</artifactId> - <version>1.6.1</version> + <version>1.6.2</version> + </dependency> + <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> + <version>2.1.7</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.20</version> </dependency> + <dependency> + <groupId>net.sourceforge.htmlunit</groupId> + <artifactId>htmlunit</artifactId> + <version>2.49.1</version> + </dependency> <dependency> <groupId>net.sf.json-lib</groupId> <artifactId>json-lib</artifactId> @@ -1517,6 +1548,13 @@ <artifactId>javassist</artifactId> <version>3.22.0-GA</version> </dependency> + <dependency> + <groupId>org.junit</groupId> + <artifactId>junit-bom</artifactId> + <version>${junit_version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> @@ -1544,7 +1582,7 @@ <dependency> <groupId>org.junit-pioneer</groupId> <artifactId>junit-pioneer</artifactId> - <version>1.1.0</version> + <version>1.3.8</version> </dependency> <dependency> <groupId>org.mariadb.jdbc</groupId> @@ -1737,6 +1775,11 @@ <artifactId>popper.js</artifactId> <version>1.16.1</version> </dependency> + <dependency> + <groupId>org.webjars</groupId> + <artifactId>swagger-ui</artifactId> + <version>3.46.0</version> + </dependency> <dependency> <groupId>org.xmlunit</groupId> <artifactId>xmlunit-core</artifactId> @@ -1745,19 +1788,19 @@ <dependency> <groupId>org.testcontainers</groupId> <artifactId>testcontainers</artifactId> - <version>1.15.1</version> + <version>1.15.3</version> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>elasticsearch</artifactId> - <version>1.15.1</version> + <version>1.15.3</version> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>junit-jupiter</artifactId> - <version>1.15.1</version> + <version>1.15.3</version> <scope>test</scope> </dependency> <dependency> @@ -1923,7 +1966,7 @@ <runOrder>random</runOrder> <argLine>@{argLine} ${surefire_jvm_args}</argLine> <forkCount>1.0C</forkCount> - <trimStackTrace>false</trimStackTrace> + <trimStackTrace>true</trimStackTrace> </configuration> </plugin> <plugin> @@ -2101,7 +2144,7 @@ <dependency> <groupId>com.puppycrawl.tools</groupId> <artifactId>checkstyle</artifactId> - <version>8.29</version> + <version>8.41.1</version> </dependency> </dependencies> <configuration> @@ -2188,6 +2231,9 @@ <copy todir="target/site/apidocs-server-mdm"> <fileset dir="hapi-fhir-server-mdm/target/site/apidocs"/> </copy> + <copy todir="target/site/apidocs-server-openapi"> + <fileset dir="hapi-fhir-server-mdm/target/site/openapi"/> + </copy> <copy todir="target/site/xref-jpaserver"> <fileset dir="hapi-fhir-jpaserver-base/target/site/xref"/> </copy> @@ -2593,6 +2639,7 @@ <module>hapi-fhir-client</module> <module>hapi-fhir-server</module> <module>hapi-fhir-server-mdm</module> + <module>hapi-fhir-server-openapi</module> <module>hapi-fhir-converter</module> <module>hapi-fhir-validation</module> <!--<module>hapi-fhir-narrativegenerator</module>--> diff --git a/restful-server-example/pom.xml b/restful-server-example/pom.xml index ee4e9459316..dfb8121f1a7 100644 --- a/restful-server-example/pom.xml +++ b/restful-server-example/pom.xml @@ -8,7 +8,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> diff --git a/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml b/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml index 757146f0f1c..5308513a49d 100644 --- a/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml +++ b/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml @@ -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> diff --git a/tests/hapi-fhir-base-test-mindeps-client/pom.xml b/tests/hapi-fhir-base-test-mindeps-client/pom.xml index e295bb2a92c..4c39a242c15 100644 --- a/tests/hapi-fhir-base-test-mindeps-client/pom.xml +++ b/tests/hapi-fhir-base-test-mindeps-client/pom.xml @@ -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> diff --git a/tests/hapi-fhir-base-test-mindeps-server/pom.xml b/tests/hapi-fhir-base-test-mindeps-server/pom.xml index bde4aa9378f..429c414d48a 100644 --- a/tests/hapi-fhir-base-test-mindeps-server/pom.xml +++ b/tests/hapi-fhir-base-test-mindeps-server/pom.xml @@ -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>