diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/DefaultProfileValidationSupport.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/DefaultProfileValidationSupport.java index bf5e7be37d4..66a3dcfc34b 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/DefaultProfileValidationSupport.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/DefaultProfileValidationSupport.java @@ -148,6 +148,12 @@ public class DefaultProfileValidationSupport implements IValidationSupport { return toList(provideStructureDefinitionMap()); } + @Nullable + @Override + public List fetchAllNonBaseStructureDefinitions() { + return null; + } + @Override public IBaseResource fetchCodeSystem(String theSystem) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java index 8b08e8d8708..3ab90dd4ec7 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java @@ -36,11 +36,13 @@ import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.function.Supplier; import java.util.stream.Collectors; +import static org.apache.commons.lang3.StringUtils.defaultString; import static org.apache.commons.lang3.StringUtils.isNotBlank; /** @@ -109,6 +111,32 @@ public interface IValidationSupport { return null; } + /** + * Load and return all possible structure definitions aside from resource definitions themselves + */ + @Nullable + default List fetchAllNonBaseStructureDefinitions() { + List retVal = fetchAllStructureDefinitions(); + if (retVal != null) { + List newList = new ArrayList<>(retVal.size()); + for (T next : retVal) { + String url = defaultString(getFhirContext().newTerser().getSinglePrimitiveValueOrNull(next, "url")); + if (url.startsWith("http://hl7.org/fhir/StructureDefinition/")) { + String lastPart = url.substring("http://hl7.org/fhir/StructureDefinition/".length()); + if (getFhirContext().getResourceTypes().contains(lastPart)) { + continue; + } + } + + newList.add(next); + } + + retVal = newList; + } + + return retVal; + } + /** * Fetch a code system by ID * diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java index d5e9aa9a35b..b36bc90a43f 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java @@ -222,6 +222,53 @@ public enum Pointcut implements IPointcut { "ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException" ), + /** + * Server Hook: + * This method is immediately before the handling method is selected. Interceptors may make changes + * to the request that can influence which handler will ultimately be called. + *

+ * Hooks may accept the following parameters: + *

    + *
  • + * ca.uhn.fhir.rest.api.server.RequestDetails - A bean containing details about the request that is about to be processed, including details such as the + * resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been + * pulled out of the servlet request. + * Note that the bean properties are not all guaranteed to be populated at the time this hook is called. + *
  • + *
  • + * ca.uhn.fhir.rest.server.servlet.ServletRequestDetails - A bean containing details about the request that is about to be processed, including details such as the + * resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been + * pulled out of the servlet request. This parameter is identical to the RequestDetails parameter above but will + * only be populated when operating in a RestfulServer implementation. It is provided as a convenience. + *
  • + *
  • + * javax.servlet.http.HttpServletRequest - The servlet request, when running in a servlet environment + *
  • + *
  • + * javax.servlet.http.HttpServletResponse - The servlet response, when running in a servlet environment + *
  • + *
+ *

+ * Hook methods may return true or void if processing should continue normally. + * This is generally the right thing to do. + * If your interceptor is providing an HTTP response rather than letting HAPI handle the response normally, you + * must return false. In this case, no further processing will occur and no further interceptors + * will be called. + *

+ *

+ * Hook methods may also throw {@link AuthenticationException} if they would like. This exception may be thrown + * to indicate that the interceptor has detected an unauthorized access + * attempt. If thrown, processing will stop and an HTTP 401 will be returned to the client. + * + * @since 5.4.0 + */ + SERVER_INCOMING_REQUEST_PRE_HANDLER_SELECTED(boolean.class, + "ca.uhn.fhir.rest.api.server.RequestDetails", + "ca.uhn.fhir.rest.server.servlet.ServletRequestDetails", + "javax.servlet.http.HttpServletRequest", + "javax.servlet.http.HttpServletResponse" + ), + /** * Server Hook: * This method is called just before the actual implementing server method is invoked. diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Read.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Read.java index 91f2fe2b593..fd71606d45e 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Read.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Read.java @@ -55,9 +55,18 @@ public @interface Read { // NB: Read, Search (maybe others) share this annotation, so update the javadocs everywhere Class 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 ""; + /** * If set to true (default is false), this method supports vread operation as well as read */ boolean version() default false; - } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java index 93d957299d2..a7604ec3a02 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java @@ -131,6 +131,9 @@ public class Constants { public static final String HEADER_LOCATION_LC = HEADER_LOCATION.toLowerCase(); public static final String HEADER_ORIGIN = "Origin"; public static final String HEADER_PREFER = "Prefer"; + public static final String HEADER_PREFER_HANDLING = "handling"; + public static final String HEADER_PREFER_HANDLING_STRICT = "strict"; + public static final String HEADER_PREFER_HANDLING_LENIENT = "lenient"; public static final String HEADER_PREFER_RETURN = "return"; public static final String HEADER_PREFER_RETURN_MINIMAL = "minimal"; public static final String HEADER_PREFER_RETURN_REPRESENTATION = "representation"; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/PreferHandlingEnum.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/PreferHandlingEnum.java new file mode 100644 index 00000000000..f668dfccd3c --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/PreferHandlingEnum.java @@ -0,0 +1,54 @@ +package ca.uhn.fhir.rest.api; + +/*- + * #%L + * HAPI FHIR - Core Library + * %% + * 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 java.util.HashMap; + +/** + * Represents values for "handling" value as provided in the the FHIR Search Spec. + */ +public enum PreferHandlingEnum { + + STRICT(Constants.HEADER_PREFER_HANDLING_STRICT), LENIENT(Constants.HEADER_PREFER_HANDLING_LENIENT); + + private static HashMap ourValues; + private String myHeaderValue; + + PreferHandlingEnum(String theHeaderValue) { + myHeaderValue = theHeaderValue; + } + + public String getHeaderValue() { + return myHeaderValue; + } + + public static PreferHandlingEnum fromHeaderValue(String theHeaderValue) { + if (ourValues == null) { + HashMap values = new HashMap<>(); + for (PreferHandlingEnum next : PreferHandlingEnum.values()) { + values.put(next.getHeaderValue(), next); + } + ourValues = values; + } + return ourValues.get(theHeaderValue); + } + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/PreferHeader.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/PreferHeader.java index 9b6a17f20d3..3468c35d128 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/PreferHeader.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/PreferHeader.java @@ -26,9 +26,10 @@ public class PreferHeader { private PreferReturnEnum myReturn; private boolean myRespondAsync; + private PreferHandlingEnum myHanding; - public @Nullable - PreferReturnEnum getReturn() { + @Nullable + public PreferReturnEnum getReturn() { return myReturn; } @@ -46,4 +47,12 @@ public class PreferHeader { return this; } + @Nullable + public PreferHandlingEnum getHanding() { + return myHanding; + } + + public void setHanding(PreferHandlingEnum theHanding) { + myHanding = theHanding; + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/PreferReturnEnum.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/PreferReturnEnum.java index a0552d25de5..b37139cc44e 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/PreferReturnEnum.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/PreferReturnEnum.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.rest.api; * #L% */ +import javax.annotation.Nullable; import java.util.HashMap; /** @@ -27,7 +28,7 @@ import java.util.HashMap; */ public enum PreferReturnEnum { - REPRESENTATION("representation"), MINIMAL("minimal"), OPERATION_OUTCOME("OperationOutcome"); + REPRESENTATION(Constants.HEADER_PREFER_RETURN_REPRESENTATION), MINIMAL(Constants.HEADER_PREFER_RETURN_MINIMAL), OPERATION_OUTCOME(Constants.HEADER_PREFER_RETURN_OPERATION_OUTCOME); private static HashMap ourValues; private String myHeaderValue; @@ -40,6 +41,7 @@ public enum PreferReturnEnum { return myHeaderValue; } + @Nullable public static PreferReturnEnum fromHeaderValue(String theHeaderValue) { if (ourValues == null) { HashMap values = new HashMap<>(); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/RestOperationTypeEnum.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/RestOperationTypeEnum.java index 7ae7e217791..4a8d5725c4e 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/RestOperationTypeEnum.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/RestOperationTypeEnum.java @@ -25,17 +25,22 @@ import java.util.HashMap; import java.util.Map; import ca.uhn.fhir.util.CoverageIgnore; +import org.apache.commons.lang3.Validate; + +import javax.annotation.Nonnull; @CoverageIgnore public enum RestOperationTypeEnum { - ADD_TAGS("add-tags"), + BATCH("batch", true, false, false), - DELETE_TAGS("delete-tags"), + ADD_TAGS("add-tags", false, false, true), - GET_TAGS("get-tags"), + DELETE_TAGS("delete-tags", false, false, true), - GET_PAGE("get-page"), + GET_TAGS("get-tags", false, true, true), + + GET_PAGE("get-page", false, false, false), /** * @@ -43,111 +48,111 @@ public enum RestOperationTypeEnum { * change as the GraphQL interface matures * */ - GRAPHQL_REQUEST("graphql-request"), + GRAPHQL_REQUEST("graphql-request", false, false, false), /** * E.g. $everything, $validate, etc. */ - EXTENDED_OPERATION_SERVER("extended-operation-server"), + EXTENDED_OPERATION_SERVER("extended-operation-server", false, false, false), /** * E.g. $everything, $validate, etc. */ - EXTENDED_OPERATION_TYPE("extended-operation-type"), + EXTENDED_OPERATION_TYPE("extended-operation-type", false, false, false), /** * E.g. $everything, $validate, etc. */ - EXTENDED_OPERATION_INSTANCE("extended-operation-instance"), + EXTENDED_OPERATION_INSTANCE("extended-operation-instance", false, false, false), /** * Code Value: create */ - CREATE("create"), + CREATE("create", false, true, false), /** * Code Value: delete */ - DELETE("delete"), + DELETE("delete", false, false, true), /** * Code Value: history-instance */ - HISTORY_INSTANCE("history-instance"), + HISTORY_INSTANCE("history-instance", false, false, true), /** * Code Value: history-system */ - HISTORY_SYSTEM("history-system"), + HISTORY_SYSTEM("history-system", true, false, false), /** * Code Value: history-type */ - HISTORY_TYPE("history-type"), + HISTORY_TYPE("history-type", false, true, false), /** * Code Value: read */ - READ("read"), + READ("read", false, false, true), /** * Code Value: search-system */ - SEARCH_SYSTEM("search-system"), + SEARCH_SYSTEM("search-system", true, false, false), /** * Code Value: search-type */ - SEARCH_TYPE("search-type"), + SEARCH_TYPE("search-type", false, true, false), /** * Code Value: transaction */ - TRANSACTION("transaction"), + TRANSACTION("transaction", true, false, false), /** * Code Value: update */ - UPDATE("update"), + UPDATE("update", false, false, true), /** * Code Value: validate */ - VALIDATE("validate"), + VALIDATE("validate", false, true, true), /** * Code Value: vread */ - VREAD("vread"), + VREAD("vread", false, false, true), /** * Load the server's metadata */ - METADATA("metadata"), + METADATA("metadata", false, false, false), /** * $meta-add extended operation */ - META_ADD("$meta-add"), + META_ADD("$meta-add", false, false, false), /** * $meta-add extended operation */ - META("$meta"), + META("$meta", false, false, false), /** * $meta-delete extended operation */ - META_DELETE("$meta-delete"), + META_DELETE("$meta-delete", false, false, false), /** * Patch operation */ - PATCH("patch"), + PATCH("patch", false, false, true), ; - private static Map CODE_TO_ENUM = new HashMap(); + private static final Map CODE_TO_ENUM = new HashMap(); /** * Identifier for this Value Set: http://hl7.org/fhir/vs/type-restful-operation @@ -166,27 +171,45 @@ public enum RestOperationTypeEnum { } private final String myCode; + private final boolean mySystemLevel; + private final boolean myTypeLevel; + private final boolean myInstanceLevel; /** * Constructor */ - RestOperationTypeEnum(String theCode) { + RestOperationTypeEnum(@Nonnull String theCode, boolean theSystemLevel, boolean theTypeLevel, boolean theInstanceLevel) { myCode = theCode; + mySystemLevel = theSystemLevel; + myTypeLevel = theTypeLevel; + myInstanceLevel = theInstanceLevel; } /** * Returns the enumerated value associated with this code */ - public RestOperationTypeEnum forCode(String theCode) { - RestOperationTypeEnum retVal = CODE_TO_ENUM.get(theCode); - return retVal; + public RestOperationTypeEnum forCode(@Nonnull String theCode) { + Validate.notNull(theCode, "theCode must not be null"); + return CODE_TO_ENUM.get(theCode); } /** * Returns the code associated with this enumerated value */ + @Nonnull public String getCode() { return myCode; } + public boolean isSystemLevel() { + return mySystemLevel; + } + + public boolean isTypeLevel() { + return myTypeLevel; + } + + public boolean isInstanceLevel() { + return myInstanceLevel; + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ClasspathUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ClasspathUtil.java index f778771bcbc..6c1def2fdef 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ClasspathUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ClasspathUtil.java @@ -61,7 +61,9 @@ public class ClasspathUtil { public static InputStream loadResourceAsStream(String theClasspath) { InputStream retVal = ClasspathUtil.class.getResourceAsStream(theClasspath); if (retVal == null) { - if (!theClasspath.startsWith("/")) { + if (theClasspath.startsWith("/")) { + retVal = ClasspathUtil.class.getResourceAsStream(theClasspath.substring(1)); + } else { retVal = ClasspathUtil.class.getResourceAsStream("/" + theClasspath); } if (retVal == null) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java index a7334f04707..8cc1c9be1de 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java @@ -21,6 +21,7 @@ import ca.uhn.fhir.model.base.composite.BaseResourceReferenceDt; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.StringDt; import ca.uhn.fhir.parser.DataFormatException; +import com.google.common.collect.Lists; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBase; @@ -34,6 +35,8 @@ import org.hl7.fhir.instance.model.api.IDomainResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -42,6 +45,7 @@ import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; @@ -317,6 +321,14 @@ public class FhirTerser { return retVal.get(0); } + public Optional getSinglePrimitiveValue(IBase theTarget, String thePath) { + return getSingleValue(theTarget, thePath, IPrimitiveType.class).map(t->t.getValueAsString()); + } + + public String getSinglePrimitiveValueOrNull(IBase theTarget, String thePath) { + return getSingleValue(theTarget, thePath, IPrimitiveType.class).map(t->t.getValueAsString()).orElse(null); + } + public Optional getSingleValue(IBase theTarget, String thePath, Class theWantedType) { return Optional.ofNullable(getSingleValueOrNull(theTarget, thePath, theWantedType)); } @@ -671,10 +683,8 @@ public class FhirTerser { parts.add(thePath.substring(currentStart)); - if (theElementDef instanceof RuntimeResourceDefinition) { - if (parts.size() > 0 && parts.get(0).equals(theElementDef.getName())) { - parts = parts.subList(1, parts.size()); - } + if (parts.size() > 0 && parts.get(0).equals(theElementDef.getName())) { + parts = parts.subList(1, parts.size()); } if (parts.size() < 1) { @@ -1168,6 +1178,194 @@ public class FhirTerser { return containedResources; } + /** + * Adds and returns a new element at the given path within the given structure. The paths used here + * are not FHIRPath expressions but instead just simple dot-separated path expressions. + *

+ * Only the last entry in the path is always created, existing repetitions of elements before + * the final dot are returned if they exists (although they are created if they do not). For example, + * given the path Patient.name.given, a new repetition of given is always + * added to the first (index 0) repetition of the name. If an index-0 repetition of name + * already exists, it is added to. If one does not exist, it if created and then added to. + *

+ *

+ * If the last element in the path refers to a non-repeatable element that is already present and + * is not empty, a {@link DataFormatException} error will be thrown. + *

+ * + * @param theTarget The element to add to. This will often be a {@link IBaseResource resource} + * instance, but does not need to be. + * @param thePath The path. + * @return The newly added element + * @throws DataFormatException If the path is invalid or does not end with either a repeatable element, or + * an element that is non-repeatable but not already populated. + */ + @SuppressWarnings("unchecked") + @Nonnull + public T addElement(@Nonnull IBase theTarget, @Nonnull String thePath) { + return (T) doAddElement(theTarget, thePath, 1).get(0); + } + + @SuppressWarnings("unchecked") + private List doAddElement(IBase theTarget, String thePath, int theElementsToAdd) { + if (theElementsToAdd == 0) { + return Collections.emptyList(); + } + + IBase target = theTarget; + BaseRuntimeElementCompositeDefinition def = (BaseRuntimeElementCompositeDefinition) myContext.getElementDefinition(target.getClass()); + List parts = parsePath(def, thePath); + + for (int i = 0, partsSize = parts.size(); ; i++) { + String nextPart = parts.get(i); + boolean lastPart = i == partsSize - 1; + + BaseRuntimeChildDefinition nextChild = def.getChildByName(nextPart); + if (nextChild == null) { + throw new DataFormatException("Invalid path " + thePath + ": Element of type " + def.getName() + " has no child named " + nextPart + ". Valid names: " + def.getChildrenAndExtension().stream().map(t -> t.getElementName()).sorted().collect(Collectors.joining(", "))); + } + + List childValues = nextChild.getAccessor().getValues(target); + IBase childValue; + if (childValues.size() > 0 && !lastPart) { + childValue = childValues.get(0); + } else { + + if (lastPart) { + if (!childValues.isEmpty()) { + if (theElementsToAdd == -1) { + return (List) Collections.singletonList(childValues.get(0)); + } else if (nextChild.getMax() == 1 && !childValues.get(0).isEmpty()) { + throw new DataFormatException("Element at path " + thePath + " is not repeatable and not empty"); + } else if (nextChild.getMax() == 1 && childValues.get(0).isEmpty()) { + return (List) Collections.singletonList(childValues.get(0)); + } + } + } + + BaseRuntimeElementDefinition elementDef = nextChild.getChildByName(nextPart); + childValue = elementDef.newInstance(nextChild.getInstanceConstructorArguments()); + nextChild.getMutator().addValue(target, childValue); + + if (lastPart) { + if (theElementsToAdd == 1 || theElementsToAdd == -1) { + return (List) Collections.singletonList(childValue); + } else { + if (nextChild.getMax() == 1) { + throw new DataFormatException("Can not add multiple values at path " + thePath + ": Element does not repeat"); + } + + List values = (List) Lists.newArrayList(childValue); + for (int j = 1; j < theElementsToAdd; j++) { + childValue = elementDef.newInstance(nextChild.getInstanceConstructorArguments()); + nextChild.getMutator().addValue(target, childValue); + values.add((T) childValue); + } + + return values; + } + } + + } + + target = childValue; + + if (!lastPart) { + BaseRuntimeElementDefinition nextDef = myContext.getElementDefinition(target.getClass()); + if (!(nextDef instanceof BaseRuntimeElementCompositeDefinition)) { + throw new DataFormatException("Invalid path " + thePath + ": Element of type " + def.getName() + " has no child named " + nextPart + " (this is a primitive type)"); + } + def = (BaseRuntimeElementCompositeDefinition) nextDef; + } + } + + } + + /** + * Adds and returns a new element at the given path within the given structure. The paths used here + * are not FHIRPath expressions but instead just simple dot-separated path expressions. + *

+ * This method follows all of the same semantics as {@link #addElement(IBase, String)} but it + * requires the path to point to an element with a primitive datatype and set the value of + * the datatype to the given value. + *

+ * + * @param theTarget The element to add to. This will often be a {@link IBaseResource resource} + * instance, but does not need to be. + * @param thePath The path. + * @param theValue The value to set, or null. + * @return The newly added element + * @throws DataFormatException If the path is invalid or does not end with either a repeatable element, or + * an element that is non-repeatable but not already populated. + */ + @SuppressWarnings("unchecked") + @Nonnull + public T addElement(@Nonnull IBase theTarget, @Nonnull String thePath, @Nullable String theValue) { + T value = (T) doAddElement(theTarget, thePath, 1).get(0); + if (!(value instanceof IPrimitiveType)) { + throw new DataFormatException("Element at path " + thePath + " is not a primitive datatype. Found: " + myContext.getElementDefinition(value.getClass()).getName()); + } + + ((IPrimitiveType) value).setValueAsString(theValue); + + return value; + } + + + /** + * Adds and returns a new element at the given path within the given structure. The paths used here + * are not FHIRPath expressions but instead just simple dot-separated path expressions. + *

+ * This method follows all of the same semantics as {@link #addElement(IBase, String)} but it + * requires the path to point to an element with a primitive datatype and set the value of + * the datatype to the given value. + *

+ * + * @param theTarget The element to add to. This will often be a {@link IBaseResource resource} + * instance, but does not need to be. + * @param thePath The path. + * @param theValue The value to set, or null. + * @return The newly added element + * @throws DataFormatException If the path is invalid or does not end with either a repeatable element, or + * an element that is non-repeatable but not already populated. + */ + @SuppressWarnings("unchecked") + @Nonnull + public T setElement(@Nonnull IBase theTarget, @Nonnull String thePath, @Nullable String theValue) { + T value = (T) doAddElement(theTarget, thePath, -1).get(0); + if (!(value instanceof IPrimitiveType)) { + throw new DataFormatException("Element at path " + thePath + " is not a primitive datatype. Found: " + myContext.getElementDefinition(value.getClass()).getName()); + } + + ((IPrimitiveType) value).setValueAsString(theValue); + + return value; + } + + + /** + * This method has the same semantics as {@link #addElement(IBase, String, String)} but adds + * a collection of primitives instead of a single one. + * + * @param theTarget The element to add to. This will often be a {@link IBaseResource resource} + * instance, but does not need to be. + * @param thePath The path. + * @param theValues The values to set, or null. + */ + public void addElements(IBase theTarget, String thePath, Collection theValues) { + List targets = doAddElement(theTarget, thePath, theValues.size()); + Iterator valuesIter = theValues.iterator(); + for (IBase target : targets) { + + if (!(target instanceof IPrimitiveType)) { + throw new DataFormatException("Element at path " + thePath + " is not a primitive datatype. Found: " + myContext.getElementDefinition(target.getClass()).getName()); + } + + ((IPrimitiveType) target).setValueAsString(valuesIter.next()); + } + + } + public enum OptionsEnum { 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 4033ba37f32..c5b1b9832b3 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 @@ -94,7 +94,7 @@ public class SchemaBaseValidator implements IValidatorModule { validator.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); validator.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); } catch (SAXNotRecognizedException ex) { - ourLog.warn("Jaxp 1.5 Support not found.", ex); + ourLog.debug("Jaxp 1.5 Support not found.", ex); } validator.validate(new StreamSource(new StringReader(encodedResource))); diff --git a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java index 71ae933f231..dc7d8cff645 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java +++ b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java @@ -22,12 +22,12 @@ package ca.uhn.fhir.jpa.demo; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.binstore.BinaryAccessProvider; -import ca.uhn.fhir.jpa.binstore.BinaryStorageInterceptor; import ca.uhn.fhir.jpa.config.BaseConfig; import ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor; import ca.uhn.fhir.jpa.provider.JpaConformanceProviderDstu2; @@ -35,7 +35,7 @@ import ca.uhn.fhir.jpa.provider.JpaSystemProviderDstu2; import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider; import ca.uhn.fhir.jpa.provider.dstu3.JpaConformanceProviderDstu3; import ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3; -import ca.uhn.fhir.jpa.provider.r4.JpaConformanceProviderR4; +import ca.uhn.fhir.jpa.provider.JpaCapabilityStatementProvider; import ca.uhn.fhir.jpa.provider.r4.JpaSystemProviderR4; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.model.dstu2.composite.MetaDt; @@ -135,8 +135,9 @@ public class JpaServerDemo extends RestfulServer { } else if (fhirVersion == FhirVersionEnum.R4) { IFhirSystemDao systemDao = myAppCtx .getBean("mySystemDaoR4", IFhirSystemDao.class); - JpaConformanceProviderR4 confProvider = new JpaConformanceProviderR4(this, systemDao, - myAppCtx.getBean(DaoConfig.class), myAppCtx.getBean(ISearchParamRegistry.class)); + IValidationSupport validationSupport = myAppCtx.getBean(IValidationSupport.class); + JpaCapabilityStatementProvider confProvider = new JpaCapabilityStatementProvider(this, systemDao, + myAppCtx.getBean(DaoConfig.class), myAppCtx.getBean(ISearchParamRegistry.class), validationSupport); confProvider.setImplementationDescription("Example Server"); setServerConformanceProvider(confProvider); } else { diff --git a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/BaseMethodBinding.java b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/BaseMethodBinding.java index bb5368704c1..334407a42bb 100644 --- a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/BaseMethodBinding.java +++ b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/BaseMethodBinding.java @@ -41,6 +41,8 @@ import ca.uhn.fhir.rest.client.impl.BaseHttpClientInvocation; import ca.uhn.fhir.rest.server.exceptions.*; import ca.uhn.fhir.util.ReflectionUtil; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + public abstract class BaseMethodBinding implements IClientResponseHandler { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseMethodBinding.class); 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 72f88194b42..e682175e0fb 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 @@ -22,6 +22,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.server.RestfulServer; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.interceptor.*; @@ -245,4 +246,28 @@ public class ServletExamples { } } + + // START SNIPPET: preferHandling + @WebServlet(urlPatterns = { "/fhir/*" }, displayName = "FHIR Server") + public class RestfulServerWithPreferHandling extends RestfulServer { + + @Override + protected void initialize() throws ServletException { + + // Create an interceptor + SearchPreferHandlingInterceptor interceptor = new SearchPreferHandlingInterceptor(); + + // Optionally you can change the default behaviour for when the Prefer + // header is not found in the request or does not have a handling + // directive + interceptor.setDefaultBehaviour(PreferHandlingEnum.LENIENT); + + // Register the interceptor + registerInterceptor(interceptor); + + } + // END SNIPPET: preferHandling + } + + } diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/support-contained-searches.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2441-support-contained-searches.yaml similarity index 100% rename from hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/support-contained-searches.yaml rename to hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2441-support-contained-searches.yaml diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2506-add-prefer-handling-interceptor.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2506-add-prefer-handling-interceptor.yaml new file mode 100644 index 00000000000..b556771ca3f --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2506-add-prefer-handling-interceptor.yaml @@ -0,0 +1,5 @@ +--- +type: add +issue: 2506 +title: "A new server interceptor has been added that allows servers to implement lenient search mode, + where unknown search parameters are ignored if an optional HTTP Prefer header is provided." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2506-list-profiles-in-capabilitystatement.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2506-list-profiles-in-capabilitystatement.yaml new file mode 100644 index 00000000000..92df47111cb --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2506-list-profiles-in-capabilitystatement.yaml @@ -0,0 +1,5 @@ +--- +type: add +issue: 2506 +title: "The server generated CapabilityStatment will now include supported Profile declarations + for FHIR R4+." 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 78406b55b7e..f5177609a67 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 @@ -151,6 +151,24 @@ The following example shows how to register this interceptor within a HAPI FHIR **See Also:** The [Repository Validating Interceptor](/docs/validation/repository_validating_interceptor.html) provides a different and potentially more powerful way of validating data when paired with a HAPI FHIR JPA Server. + + +# Search: Allow Lenient Searching + +By default, HAPI FHIR applies strict search parameter validation. This means that FHIR search requests will fail if the search contains search parameters (any parameter that does not begin with an underscore) that are not known to the server. + +* [SearchPreferHandlingInterceptor JavaDoc](/apidocs/hapi-fhir-server/ca/uhn/fhir/rest/server/interceptor/SearchPreferHandlingInterceptor.html) +* [SearchPreferHandlingInterceptor Source](https://github.com/hapifhir/hapi-fhir/blob/master/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/SearchPreferHandlingInterceptor.java) + +The SearchPreferHandlingInterceptor looks for a header of the form `Prefer: handling=lenient` or `Prefer: handling=strict` as described in the [FHIR Search Specification](http://hl7.org/fhir/search.html#errors) and treats it appropriately. A non-strict can also optionally be set. + +The following example shows how to register this interceptor within a HAPI FHIR REST server. + +```java +{{snippet:classpath:/ca/uhn/hapi/fhir/docs/ServletExamples.java|preferHandling}} +``` + + # Security: CORS HAPI FHIR includes an interceptor which can be used to implement CORS support on your server. See [Server CORS Documentation](/docs/security/cors.html#cors_interceptor) for information on how to use this interceptor. @@ -281,7 +299,7 @@ The RepositoryValidatingInterceptor can be used to enforce validation rules on d # Data Standardization -```StandardizingInterceptor``` handles data standardization (s13n) requirements. This interceptor applies standardization rules on all FHIR primitives as configured in the ```s13n.json``` file that should be made available on the classpath. This file contains FHIRPath definitions together with the standardizers that should be applied to that path. It comes with six per-build standardizers: NAME_FAMILY, NAME_GIVEN, EMAIL, TITLE, PHONE and TEXT. Custom standardizers can be developed by implementing ```ca.uhn.fhir.rest.server.interceptor.s13n.standardizers.IStandardizer``` interface. +`StandardizingInterceptor` handles data standardization (s13n) requirements. This interceptor applies standardization rules on all FHIR primitives as configured in the `s13n.json` file that should be made available on the classpath. This file contains FHIRPath definitions together with the standardizers that should be applied to that path. It comes with six per-build standardizers: NAME_FAMILY, NAME_GIVEN, EMAIL, TITLE, PHONE and TEXT. Custom standardizers can be developed by implementing `ca.uhn.fhir.rest.server.interceptor.s13n.standardizers.IStandardizer` interface. A sample configuration file can be found below: @@ -303,20 +321,20 @@ A sample configuration file can be found below: } ``` -Standardization can be disabled for a given request by providing ```HAPI-Standardization-Disabled: *``` request header. Header value can be any string, it is the presence of the header that disables the s13n. +Standardization can be disabled for a given request by providing `HAPI-Standardization-Disabled: *` request header. Header value can be any string, it is the presence of the header that disables the s13n. # Validation: Address Validation -```AddressValidatingInterceptor``` takes care of validation of addresses on all incoming resources through a 3rd party address validation service. Before a resource containing an Address field is stored, this interceptor invokes address validation service and then stores validation results as an extension on the address with ```https://hapifhir.org/AddressValidation/``` URL. +`AddressValidatingInterceptor` takes care of validation of addresses on all incoming resources through a 3rd party address validation service. Before a resource containing an Address field is stored, this interceptor invokes address validation service and then stores validation results as an extension on the address with `https://hapifhir.org/AddressValidation/` URL. -This interceptor is configured in ```address-validation.properties``` file that should be made available on the classpath. This file must contain ```validator.class``` property, which defines a fully qualified class implementing ```ca.uhn.fhir.rest.server.interceptor.validation.address.IAddressValidator``` interface. The specified implementation must provide service-specific logic for validating an Address instance. An example implementation can be found in ```ca.uhn.fhir.rest.server.interceptor.validation.address.impl.LoquateAddressValidator``` class which validates addresses by using Loquate Data Cleanse service. +This interceptor is configured in `address-validation.properties` file that should be made available on the classpath. This file must contain `validator.class` property, which defines a fully qualified class implementing `ca.uhn.fhir.rest.server.interceptor.validation.address.IAddressValidator` interface. The specified implementation must provide service-specific logic for validating an Address instance. An example implementation can be found in `ca.uhn.fhir.rest.server.interceptor.validation.address.impl.LoquateAddressValidator` class which validates addresses by using Loquate Data Cleanse service. -Address validation can be disabled for a given request by providing ```HAPI-Address-Validation-Disabled: *``` request header. Header value can be any string, it is the presence of the header that disables the validation. +Address validation can be disabled for a given request by providing `HAPI-Address-Validation-Disabled: *` request header. Header value can be any string, it is the presence of the header that disables the validation. # Validation: Field-Level Validation -```FieldValidatingInterceptor``` allows validating primitive fields on various FHIR resources. It expects validation rules to be provided via ```field-validation-rules.json``` file that should be available on the classpath. JSON in this file defines a mapping of FHIRPath expressions to validators that should be applied to those fields. Custom validators that implement ```ca.uhn.fhir.rest.server.interceptor.validation.fields.IValidator``` interface can be provided. +`FieldValidatingInterceptor` allows validating primitive fields on various FHIR resources. It expects validation rules to be provided via `field-validation-rules.json` file that should be available on the classpath. JSON in this file defines a mapping of FHIRPath expressions to validators that should be applied to those fields. Custom validators that implement `ca.uhn.fhir.rest.server.interceptor.validation.fields.IValidator` interface can be provided. ```json { @@ -325,4 +343,4 @@ Address validation can be disabled for a given request by providing ```HAPI-Addr } ``` -Field validation can be disabled for a given request by providing ```HAPI-Field-Validation-Disabled: *``` request header. Header value can be any string, it is the presence of the header that disables the validation. +Field validation can be disabled for a given request by providing `HAPI-Field-Validation-Disabled: *` request header. Header value can be any string, it is the presence of the header that disables the validation. diff --git a/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/AbstractJaxRsConformanceProvider.java b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/AbstractJaxRsConformanceProvider.java index ec70a50bbef..87fc3f4581e 100644 --- a/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/AbstractJaxRsConformanceProvider.java +++ b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/AbstractJaxRsConformanceProvider.java @@ -33,9 +33,11 @@ import javax.ws.rs.*; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import ca.uhn.fhir.rest.server.provider.ServerCapabilityStatementProvider; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.dstu2.hapi.rest.server.ServerConformanceProvider; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.CapabilityStatement; import org.slf4j.LoggerFactory; import ca.uhn.fhir.context.*; @@ -137,8 +139,8 @@ public abstract class AbstractJaxRsConformanceProvider extends AbstractJaxRsProv FhirVersionEnum fhirContextVersion = super.getFhirContext().getVersion().getVersion(); switch (fhirContextVersion) { case R4: - org.hl7.fhir.r4.hapi.rest.server.ServerCapabilityStatementProvider r4ServerCapabilityStatementProvider = new org.hl7.fhir.r4.hapi.rest.server.ServerCapabilityStatementProvider(serverConfiguration); - myR4CapabilityStatement = r4ServerCapabilityStatementProvider.getServerConformance(null, null); + ServerCapabilityStatementProvider r4ServerCapabilityStatementProvider = new ServerCapabilityStatementProvider(getFhirContext(), serverConfiguration); + myR4CapabilityStatement = (CapabilityStatement) r4ServerCapabilityStatementProvider.getServerConformance(null, null); break; case DSTU3: org.hl7.fhir.dstu3.hapi.rest.server.ServerCapabilityStatementProvider dstu3ServerCapabilityStatementProvider = new org.hl7.fhir.dstu3.hapi.rest.server.ServerCapabilityStatementProvider(serverConfiguration); @@ -226,7 +228,7 @@ public abstract class AbstractJaxRsConformanceProvider extends AbstractJaxRsProv } /** - * This method will add a provider to the conformance. This method is almost an exact copy of {@link ca.uhn.fhir.rest.server.RestfulServer#findResourceMethods(Object, Class)} } + * This method will add a provider to the conformance. This method is almost an exact copy of {@link ca.uhn.fhir.rest.server.RestfulServer#findResourceMethods(Object)} * * @param theProvider * an instance of the provider interface diff --git a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IFhirSystemDao.java b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IFhirSystemDao.java index f382f6a7642..2d810788d76 100644 --- a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IFhirSystemDao.java +++ b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IFhirSystemDao.java @@ -46,7 +46,7 @@ public interface IFhirSystemDao extends IDao { /** * Returns a cached count of resources using a cache that regularly - * refreshes in the background. This method will never + * refreshes in the background. This method will never block, and may return null if nothing is in the cache. */ @Nullable Map getResourceCountsFromCache(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupport.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupport.java index d3a1fe2adf1..f1819cbbbb7 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupport.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupport.java @@ -50,6 +50,7 @@ import javax.annotation.Nullable; import javax.annotation.PostConstruct; import javax.transaction.Transactional; import java.util.Arrays; +import java.util.List; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; @@ -104,6 +105,13 @@ public class JpaPersistedResourceValidationSupport implements IValidationSupport return fetchResource(myStructureDefinitionType, theUrl); } + @SuppressWarnings("unchecked") + @Nullable + @Override + public List fetchAllStructureDefinitions() { + IBundleProvider search = myDaoRegistry.getResourceDao("StructureDefinition").search(new SearchParameterMap().setLoadSynchronousUpTo(1000)); + return (List) search.getResources(0, 1000); + } @Override @SuppressWarnings({"unchecked", "unused"}) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/INpmPackageVersionResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/INpmPackageVersionResourceDao.java index 67a45b6a360..40c8956c3be 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/INpmPackageVersionResourceDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/INpmPackageVersionResourceDao.java @@ -30,6 +30,9 @@ import org.springframework.data.repository.query.Param; public interface INpmPackageVersionResourceDao extends JpaRepository { + @Query("SELECT e FROM NpmPackageVersionResourceEntity e WHERE e.myResourceType = :resourceType AND e.myFhirVersion = :fhirVersion AND e.myPackageVersion.myCurrentVersion = true") + Slice findCurrentVersionByResourceType(Pageable thePage, @Param("fhirVersion") FhirVersionEnum theFhirVersion, @Param("resourceType") String theResourceType); + @Query("SELECT e FROM NpmPackageVersionResourceEntity e WHERE e.myCanonicalUrl = :url AND e.myFhirVersion = :fhirVersion AND e.myPackageVersion.myCurrentVersion = true") Slice findCurrentVersionByCanonicalUrl(Pageable thePage, @Param("fhirVersion") FhirVersionEnum theFhirVersion, @Param("url") String theCanonicalUrl); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/IHapiPackageCacheManager.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/IHapiPackageCacheManager.java index eb6ab9c2900..64db93e961a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/IHapiPackageCacheManager.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/IHapiPackageCacheManager.java @@ -28,6 +28,7 @@ import org.hl7.fhir.utilities.npm.NpmPackage; import java.io.IOException; import java.util.Date; +import java.util.List; public interface IHapiPackageCacheManager extends IPackageCacheManager { @@ -43,6 +44,8 @@ public interface IHapiPackageCacheManager extends IPackageCacheManager { PackageDeleteOutcomeJson uninstallPackage(String thePackageId, String theVersion); + List loadPackageAssetsByType(FhirVersionEnum theFhirVersion, String theResourceType); + class PackageContents { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/JpaPackageCache.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/JpaPackageCache.java index 0be0b6c1256..6c64b331a17 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/JpaPackageCache.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/JpaPackageCache.java @@ -65,6 +65,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.support.TransactionTemplate; @@ -467,16 +468,20 @@ public class JpaPackageCache extends BasePackageCacheManager implements IHapiPac return null; } else { NpmPackageVersionResourceEntity contents = slice.getContent().get(0); - ResourcePersistentId binaryPid = new ResourcePersistentId(contents.getResourceBinary().getId()); - IBaseBinary binary = getBinaryDao().readByPid(binaryPid); - byte[] resourceContentsBytes = BinaryUtil.getOrCreateData(myCtx, binary).getValue(); - String resourceContents = new String(resourceContentsBytes, StandardCharsets.UTF_8); - - FhirContext packageContext = getFhirContext(contents.getFhirVersion()); - return EncodingEnum.detectEncoding(resourceContents).newParser(packageContext).parseResource(resourceContents); + return loadPackageEntity(contents); } } + private IBaseResource loadPackageEntity(NpmPackageVersionResourceEntity contents) { + ResourcePersistentId binaryPid = new ResourcePersistentId(contents.getResourceBinary().getId()); + IBaseBinary binary = getBinaryDao().readByPid(binaryPid); + byte[] resourceContentsBytes = BinaryUtil.getOrCreateData(myCtx, binary).getValue(); + String resourceContents = new String(resourceContentsBytes, StandardCharsets.UTF_8); + + FhirContext packageContext = getFhirContext(contents.getFhirVersion()); + return EncodingEnum.detectEncoding(resourceContents).newParser(packageContext).parseResource(resourceContents); + } + @Override @Transactional public NpmPackageMetadataJson loadPackageMetadata(String thePackageId) { @@ -641,6 +646,14 @@ public class JpaPackageCache extends BasePackageCacheManager implements IHapiPac return retVal; } + @Override + @Transactional + public List loadPackageAssetsByType(FhirVersionEnum theFhirVersion, String theResourceType) { +// List outcome = myPackageVersionResourceDao.findAll(); + Slice outcome = myPackageVersionResourceDao.findCurrentVersionByResourceType(PageRequest.of(0, 1000), theFhirVersion, theResourceType); + return outcome.stream().map(t->loadPackageEntity(t)).collect(Collectors.toList()); + } + private void deleteAndExpungeResourceBinary(IIdType theResourceBinaryId, ExpungeOptions theOptions) { if (myPartitionSettings.isPartitioningEnabled()) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupport.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupport.java index 4f4ccdf49ab..0504be64f6e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupport.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupport.java @@ -27,6 +27,7 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.springframework.beans.factory.annotation.Autowired; import javax.annotation.Nullable; +import java.util.List; public class NpmJpaValidationSupport implements IValidationSupport { @@ -68,4 +69,14 @@ public class NpmJpaValidationSupport implements IValidationSupport { } return null; } + + + @SuppressWarnings("unchecked") + @Nullable + @Override + public List fetchAllStructureDefinitions() { + FhirVersionEnum fhirVersion = myFhirContext.getVersion().getVersion(); + return (List) myHapiPackageCacheManager.loadPackageAssetsByType(fhirVersion, "StructureDefinition"); + } + } 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 new file mode 100644 index 00000000000..99ebd33fc37 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaCapabilityStatementProvider.java @@ -0,0 +1,143 @@ +package ca.uhn.fhir.jpa.provider; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * 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.context.support.IValidationSupport; +import ca.uhn.fhir.jpa.api.config.DaoConfig; +import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.provider.ServerCapabilityStatementProvider; +import ca.uhn.fhir.util.CoverageIgnore; +import ca.uhn.fhir.util.ExtensionConstants; +import ca.uhn.fhir.util.ExtensionUtil; +import ca.uhn.fhir.util.FhirTerser; +import org.apache.commons.lang3.Validate; +import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IBaseConformance; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.CapabilityStatement.ConditionalDeleteStatus; +import org.hl7.fhir.r4.model.CapabilityStatement.ResourceVersionPolicy; +import org.hl7.fhir.r4.model.Meta; + +import javax.annotation.Nonnull; +import java.util.Map; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +/** + * R4+ Only + */ +public class JpaCapabilityStatementProvider extends ServerCapabilityStatementProvider { + + private final FhirContext myContext; + private DaoConfig myDaoConfig; + private String myImplementationDescription; + private boolean myIncludeResourceCounts; + private IFhirSystemDao mySystemDao; + + /** + * Constructor + */ + public JpaCapabilityStatementProvider(@Nonnull RestfulServer theRestfulServer, @Nonnull IFhirSystemDao theSystemDao, @Nonnull DaoConfig theDaoConfig, @Nonnull ISearchParamRegistry theSearchParamRegistry, IValidationSupport theValidationSupport) { + super(theRestfulServer, theSearchParamRegistry, theValidationSupport); + + Validate.notNull(theRestfulServer); + Validate.notNull(theSystemDao); + Validate.notNull(theDaoConfig); + Validate.notNull(theSearchParamRegistry); + + myContext = theRestfulServer.getFhirContext(); + mySystemDao = theSystemDao; + myDaoConfig = theDaoConfig; + setIncludeResourceCounts(true); + } + + @Override + protected void postProcess(FhirTerser theTerser, IBaseConformance theCapabilityStatement) { + super.postProcess(theTerser, theCapabilityStatement); + + if (isNotBlank(myImplementationDescription)) { + theTerser.setElement(theCapabilityStatement, "implementation.description", myImplementationDescription); + } + } + + @Override + protected void postProcessRest(FhirTerser theTerser, IBase theRest) { + super.postProcessRest(theTerser, theRest); + + if (myDaoConfig.getSupportedSubscriptionTypes().contains(org.hl7.fhir.dstu2.model.Subscription.SubscriptionChannelType.WEBSOCKET)) { + if (isNotBlank(myDaoConfig.getWebsocketContextPath())) { + ExtensionUtil.setExtension(myContext, theRest, Constants.CAPABILITYSTATEMENT_WEBSOCKET_URL, "uri", myDaoConfig.getWebsocketContextPath()); + } + } + + } + + @Override + protected void postProcessRestResource(FhirTerser theTerser, IBase theResource, String theResourceName) { + super.postProcessRestResource(theTerser, theResource, theResourceName); + + theTerser.addElement(theResource, "versioning", ResourceVersionPolicy.VERSIONEDUPDATE.toCode()); + + if (myDaoConfig.isAllowMultipleDelete()) { + theTerser.addElement(theResource, "conditionalDelete", ConditionalDeleteStatus.MULTIPLE.toCode()); + } else { + theTerser.addElement(theResource, "conditionalDelete", ConditionalDeleteStatus.SINGLE.toCode()); + } + + // Add resource counts + if (myIncludeResourceCounts) { + Map counts = mySystemDao.getResourceCountsFromCache(); + if (counts != null) { + Long count = counts.get(theResourceName); + if (count != null) { + ExtensionUtil.setExtension(myContext, theResource, ExtensionConstants.CONF_RESOURCE_COUNT, "decimal", Long.toString(count)); + } + } + } + + } + + public boolean isIncludeResourceCounts() { + return myIncludeResourceCounts; + } + + public void setIncludeResourceCounts(boolean theIncludeResourceCounts) { + myIncludeResourceCounts = theIncludeResourceCounts; + } + + public void setDaoConfig(DaoConfig myDaoConfig) { + this.myDaoConfig = myDaoConfig; + } + + @CoverageIgnore + public void setImplementationDescription(String theImplDesc) { + myImplementationDescription = theImplDesc; + } + + @CoverageIgnore + public void setSystemDao(IFhirSystemDao mySystemDao) { + this.mySystemDao = mySystemDao; + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/JpaConformanceProviderR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/JpaConformanceProviderR4.java deleted file mode 100644 index a7d48135eb7..00000000000 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/JpaConformanceProviderR4.java +++ /dev/null @@ -1,225 +0,0 @@ -package ca.uhn.fhir.jpa.provider.r4; - -/* - * #%L - * HAPI FHIR JPA Server - * %% - * 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.RuntimeResourceDefinition; -import ca.uhn.fhir.context.RuntimeSearchParam; -import ca.uhn.fhir.jpa.api.config.DaoConfig; -import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; -import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; -import ca.uhn.fhir.rest.api.Constants; -import ca.uhn.fhir.rest.api.server.RequestDetails; -import ca.uhn.fhir.rest.server.RestfulServer; -import ca.uhn.fhir.util.CoverageIgnore; -import ca.uhn.fhir.util.ExtensionConstants; -import org.apache.commons.lang3.Validate; -import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.CapabilityStatement; -import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestComponent; -import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestResourceComponent; -import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent; -import org.hl7.fhir.r4.model.CapabilityStatement.ConditionalDeleteStatus; -import org.hl7.fhir.r4.model.CapabilityStatement.ResourceVersionPolicy; -import org.hl7.fhir.r4.model.DecimalType; -import org.hl7.fhir.r4.model.Enumerations.SearchParamType; -import org.hl7.fhir.r4.model.Extension; -import org.hl7.fhir.r4.model.Meta; -import org.hl7.fhir.r4.model.UriType; - -import javax.annotation.Nonnull; -import javax.servlet.http.HttpServletRequest; -import java.util.Collection; -import java.util.Collections; -import java.util.Map; - -import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; -import static org.apache.commons.lang3.StringUtils.isNotBlank; - -public class JpaConformanceProviderR4 extends org.hl7.fhir.r4.hapi.rest.server.ServerCapabilityStatementProvider { - - private volatile CapabilityStatement myCachedValue; - private DaoConfig myDaoConfig; - private ISearchParamRegistry mySearchParamRegistry; - private String myImplementationDescription; - private boolean myIncludeResourceCounts; - private RestfulServer myRestfulServer; - private IFhirSystemDao mySystemDao; - - /** - * Constructor - */ - @CoverageIgnore - public JpaConformanceProviderR4() { - super(); - super.setCache(false); - setIncludeResourceCounts(true); - } - - /** - * Constructor - */ - public JpaConformanceProviderR4(@Nonnull RestfulServer theRestfulServer, @Nonnull IFhirSystemDao theSystemDao, @Nonnull DaoConfig theDaoConfig, @Nonnull ISearchParamRegistry theSearchParamRegistry) { - super(theRestfulServer); - - Validate.notNull(theRestfulServer); - Validate.notNull(theSystemDao); - Validate.notNull(theDaoConfig); - Validate.notNull(theSearchParamRegistry); - - myRestfulServer = theRestfulServer; - mySystemDao = theSystemDao; - myDaoConfig = theDaoConfig; - super.setCache(false); - setIncludeResourceCounts(true); - setSearchParamRegistry(theSearchParamRegistry); - } - - public void setSearchParamRegistry(ISearchParamRegistry theSearchParamRegistry) { - mySearchParamRegistry = theSearchParamRegistry; - } - - @Override - public CapabilityStatement getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) { - CapabilityStatement retVal = myCachedValue; - - Map counts = null; - if (myIncludeResourceCounts) { - counts = mySystemDao.getResourceCountsFromCache(); - } - counts = defaultIfNull(counts, Collections.emptyMap()); - - retVal = super.getServerConformance(theRequest, theRequestDetails); - for (CapabilityStatementRestComponent nextRest : retVal.getRest()) { - - for (CapabilityStatementRestResourceComponent nextResource : nextRest.getResource()) { - - nextResource.setVersioning(ResourceVersionPolicy.VERSIONEDUPDATE); - - ConditionalDeleteStatus conditionalDelete = nextResource.getConditionalDelete(); - if (conditionalDelete == ConditionalDeleteStatus.MULTIPLE && myDaoConfig.isAllowMultipleDelete() == false) { - nextResource.setConditionalDelete(ConditionalDeleteStatus.SINGLE); - } - - // Add resource counts - Long count = counts.get(nextResource.getTypeElement().getValueAsString()); - if (count != null) { - nextResource.addExtension(new Extension(ExtensionConstants.CONF_RESOURCE_COUNT, new DecimalType(count))); - } - - nextResource.getSearchParam().clear(); - String resourceName = nextResource.getType(); - RuntimeResourceDefinition resourceDef = myRestfulServer.getFhirContext().getResourceDefinition(resourceName); - Collection searchParams = mySearchParamRegistry.getSearchParamsByResourceType(resourceDef); - for (RuntimeSearchParam runtimeSp : searchParams) { - CapabilityStatementRestResourceSearchParamComponent confSp = nextResource.addSearchParam(); - - confSp.setName(runtimeSp.getName()); - confSp.setDocumentation(runtimeSp.getDescription()); - confSp.setDefinition(runtimeSp.getUri()); - switch (runtimeSp.getParamType()) { - case COMPOSITE: - confSp.setType(SearchParamType.COMPOSITE); - break; - case DATE: - confSp.setType(SearchParamType.DATE); - break; - case NUMBER: - confSp.setType(SearchParamType.NUMBER); - break; - case QUANTITY: - confSp.setType(SearchParamType.QUANTITY); - break; - case REFERENCE: - confSp.setType(SearchParamType.REFERENCE); - break; - case STRING: - confSp.setType(SearchParamType.STRING); - break; - case TOKEN: - confSp.setType(SearchParamType.TOKEN); - break; - case URI: - confSp.setType(SearchParamType.URI); - break; - case SPECIAL: - confSp.setType(SearchParamType.SPECIAL); - break; - case HAS: - // Shouldn't happen - break; - } - - } - - } - } - - if (myDaoConfig.getSupportedSubscriptionTypes().contains(org.hl7.fhir.dstu2.model.Subscription.SubscriptionChannelType.WEBSOCKET)) { - if (isNotBlank(myDaoConfig.getWebsocketContextPath())) { - Extension websocketExtension = new Extension(); - websocketExtension.setUrl(Constants.CAPABILITYSTATEMENT_WEBSOCKET_URL); - websocketExtension.setValue(new UriType(myDaoConfig.getWebsocketContextPath())); - retVal.getRestFirstRep().addExtension(websocketExtension); - } - } - - massage(retVal); - - retVal.getImplementation().setDescription(myImplementationDescription); - myCachedValue = retVal; - return retVal; - } - - public boolean isIncludeResourceCounts() { - return myIncludeResourceCounts; - } - - public void setIncludeResourceCounts(boolean theIncludeResourceCounts) { - myIncludeResourceCounts = theIncludeResourceCounts; - } - - /** - * Subclasses may override - */ - protected void massage(CapabilityStatement theStatement) { - // nothing - } - - public void setDaoConfig(DaoConfig myDaoConfig) { - this.myDaoConfig = myDaoConfig; - } - - @CoverageIgnore - public void setImplementationDescription(String theImplDesc) { - myImplementationDescription = theImplDesc; - } - - @Override - public void setRestfulServer(RestfulServer theRestfulServer) { - this.myRestfulServer = theRestfulServer; - super.setRestfulServer(theRestfulServer); - } - - @CoverageIgnore - public void setSystemDao(IFhirSystemDao mySystemDao) { - this.mySystemDao = mySystemDao; - } -} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r5/JpaConformanceProviderR5.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r5/JpaConformanceProviderR5.java deleted file mode 100644 index e2970598e04..00000000000 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r5/JpaConformanceProviderR5.java +++ /dev/null @@ -1,215 +0,0 @@ -package ca.uhn.fhir.jpa.provider.r5; - -/* - * #%L - * HAPI FHIR JPA Server - * %% - * 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.RuntimeResourceDefinition; -import ca.uhn.fhir.context.RuntimeSearchParam; -import ca.uhn.fhir.jpa.api.config.DaoConfig; -import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; -import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; -import ca.uhn.fhir.rest.api.Constants; -import ca.uhn.fhir.rest.api.server.RequestDetails; -import ca.uhn.fhir.rest.server.RestfulServer; -import ca.uhn.fhir.util.CoverageIgnore; -import ca.uhn.fhir.util.ExtensionConstants; -import org.hl7.fhir.r5.model.Bundle; -import org.hl7.fhir.r5.model.CapabilityStatement; -import org.hl7.fhir.r5.model.CapabilityStatement.CapabilityStatementRestComponent; -import org.hl7.fhir.r5.model.CapabilityStatement.CapabilityStatementRestResourceComponent; -import org.hl7.fhir.r5.model.CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent; -import org.hl7.fhir.r5.model.CapabilityStatement.ConditionalDeleteStatus; -import org.hl7.fhir.r5.model.CapabilityStatement.ResourceVersionPolicy; -import org.hl7.fhir.r5.model.DecimalType; -import org.hl7.fhir.r5.model.Enumerations.SearchParamType; -import org.hl7.fhir.r5.model.Extension; -import org.hl7.fhir.r5.model.Meta; -import org.hl7.fhir.r5.model.UriType; - -import javax.servlet.http.HttpServletRequest; -import java.util.Collection; -import java.util.Collections; -import java.util.Map; - -import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; -import static org.apache.commons.lang3.StringUtils.isNotBlank; - -public class JpaConformanceProviderR5 extends org.hl7.fhir.r5.hapi.rest.server.ServerCapabilityStatementProvider { - - private volatile CapabilityStatement myCachedValue; - private DaoConfig myDaoConfig; - private ISearchParamRegistry mySearchParamRegistry; - private String myImplementationDescription; - private boolean myIncludeResourceCounts; - private RestfulServer myRestfulServer; - private IFhirSystemDao mySystemDao; - - /** - * Constructor - */ - @CoverageIgnore - public JpaConformanceProviderR5() { - super(); - setIncludeResourceCounts(true); - } - - /** - * Constructor - */ - public JpaConformanceProviderR5(RestfulServer theRestfulServer, IFhirSystemDao theSystemDao, DaoConfig theDaoConfig, ISearchParamRegistry theSearchParamRegistry) { - super(); - myRestfulServer = theRestfulServer; - mySystemDao = theSystemDao; - myDaoConfig = theDaoConfig; - setIncludeResourceCounts(true); - setSearchParamRegistry(theSearchParamRegistry); - } - - public void setSearchParamRegistry(ISearchParamRegistry theSearchParamRegistry) { - mySearchParamRegistry = theSearchParamRegistry; - } - - @Override - public CapabilityStatement getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) { - CapabilityStatement retVal = myCachedValue; - - Map counts = null; - if (myIncludeResourceCounts) { - counts = mySystemDao.getResourceCountsFromCache(); - } - counts = defaultIfNull(counts, Collections.emptyMap()); - - retVal = super.getServerConformance(theRequest, theRequestDetails); - for (CapabilityStatementRestComponent nextRest : retVal.getRest()) { - - for (CapabilityStatementRestResourceComponent nextResource : nextRest.getResource()) { - - nextResource.setVersioning(ResourceVersionPolicy.VERSIONEDUPDATE); - - ConditionalDeleteStatus conditionalDelete = nextResource.getConditionalDelete(); - if (conditionalDelete == ConditionalDeleteStatus.MULTIPLE && myDaoConfig.isAllowMultipleDelete() == false) { - nextResource.setConditionalDelete(ConditionalDeleteStatus.SINGLE); - } - - // Add resource counts - Long count = counts.get(nextResource.getTypeElement().getValueAsString()); - if (count != null) { - nextResource.addExtension(new Extension(ExtensionConstants.CONF_RESOURCE_COUNT, new DecimalType(count))); - } - - nextResource.getSearchParam().clear(); - String resourceName = nextResource.getType(); - RuntimeResourceDefinition resourceDef = myRestfulServer.getFhirContext().getResourceDefinition(resourceName); - Collection searchParams = mySearchParamRegistry.getSearchParamsByResourceType(resourceDef); - for (RuntimeSearchParam runtimeSp : searchParams) { - CapabilityStatementRestResourceSearchParamComponent confSp = nextResource.addSearchParam(); - - confSp.setName(runtimeSp.getName()); - confSp.setDocumentation(runtimeSp.getDescription()); - confSp.setDefinition(runtimeSp.getUri()); - switch (runtimeSp.getParamType()) { - case COMPOSITE: - confSp.setType(SearchParamType.COMPOSITE); - break; - case DATE: - confSp.setType(SearchParamType.DATE); - break; - case NUMBER: - confSp.setType(SearchParamType.NUMBER); - break; - case QUANTITY: - confSp.setType(SearchParamType.QUANTITY); - break; - case REFERENCE: - confSp.setType(SearchParamType.REFERENCE); - break; - case STRING: - confSp.setType(SearchParamType.STRING); - break; - case TOKEN: - confSp.setType(SearchParamType.TOKEN); - break; - case URI: - confSp.setType(SearchParamType.URI); - break; - case SPECIAL: - confSp.setType(SearchParamType.SPECIAL); - break; - case HAS: - // Shouldn't happen - break; - } - - } - - } - } - - if (myDaoConfig.getSupportedSubscriptionTypes().contains(org.hl7.fhir.dstu2.model.Subscription.SubscriptionChannelType.WEBSOCKET)) { - if (isNotBlank(myDaoConfig.getWebsocketContextPath())) { - Extension websocketExtension = new Extension(); - websocketExtension.setUrl(Constants.CAPABILITYSTATEMENT_WEBSOCKET_URL); - websocketExtension.setValue(new UriType(myDaoConfig.getWebsocketContextPath())); - retVal.getRestFirstRep().addExtension(websocketExtension); - } - } - - massage(retVal); - - retVal.getImplementation().setDescription(myImplementationDescription); - myCachedValue = retVal; - return retVal; - } - - public boolean isIncludeResourceCounts() { - return myIncludeResourceCounts; - } - - public void setIncludeResourceCounts(boolean theIncludeResourceCounts) { - myIncludeResourceCounts = theIncludeResourceCounts; - } - - /** - * Subclasses may override - */ - protected void massage(CapabilityStatement theStatement) { - // nothing - } - - public void setDaoConfig(DaoConfig myDaoConfig) { - this.myDaoConfig = myDaoConfig; - } - - @CoverageIgnore - public void setImplementationDescription(String theImplDesc) { - myImplementationDescription = theImplDesc; - } - - @Override - public void setRestfulServer(RestfulServer theRestfulServer) { - this.myRestfulServer = theRestfulServer; - super.setRestfulServer(theRestfulServer); - } - - @CoverageIgnore - public void setSystemDao(IFhirSystemDao mySystemDao) { - this.mySystemDao = mySystemDao; - } -} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java index 50379f0f0fb..5f8e41307e1 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java @@ -64,6 +64,7 @@ import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.packages.IPackageInstallerSvc; import ca.uhn.fhir.jpa.partition.IPartitionLookupSvc; import ca.uhn.fhir.jpa.provider.r4.BaseJpaResourceProviderObservationR4; import ca.uhn.fhir.jpa.provider.r4.JpaSystemProviderR4; @@ -93,6 +94,8 @@ import ca.uhn.fhir.rest.server.BasePagingProvider; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.provider.ResourceProviderFactory; import ca.uhn.fhir.test.utilities.ITestDataBuilder; +import ca.uhn.fhir.util.ClasspathUtil; +import ca.uhn.fhir.util.ResourceUtil; import ca.uhn.fhir.util.UrlUtil; import ca.uhn.fhir.validation.FhirValidator; import ca.uhn.fhir.validation.ValidationResult; @@ -199,6 +202,8 @@ public abstract class BaseJpaR4Test extends BaseJpaTest implements ITestDataBuil private static IValidationSupport ourJpaValidationSupportChainR4; private static IFhirResourceDaoValueSet ourValueSetDao; + @Autowired + protected IPackageInstallerSvc myPackageInstallerSvc; @Autowired protected ITermConceptMappingSvc myConceptMappingSvc; @Autowired @@ -591,13 +596,7 @@ public abstract class BaseJpaR4Test extends BaseJpaTest implements ITestDataBuil } protected T loadResourceFromClasspath(Class type, String resourceName) throws IOException { - InputStream stream = FhirResourceDaoDstu2SearchNoFtTest.class.getResourceAsStream(resourceName); - if (stream == null) { - fail("Unable to load resource: " + resourceName); - } - String string = IOUtils.toString(stream, "UTF-8"); - IParser newJsonParser = EncodingEnum.detectEncodingNoDefault(string).newParser(myFhirCtx); - return newJsonParser.parseResource(type, string); + return ClasspathUtil.loadResource(myFhirCtx, type, resourceName); } protected void validate(IBaseResource theResource) { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java index b48e81b7654..94a11c017fa 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java @@ -146,7 +146,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { myCaptureQueriesListener.clear(); myObservationDao.validate(obs, null, null, null, null, null, null); myCaptureQueriesListener.logSelectQueriesForCurrentThread(); - assertEquals(10, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); + assertEquals(12, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); assertEquals(0, myCaptureQueriesListener.getUpdateQueriesForCurrentThread().size()); assertEquals(0, myCaptureQueriesListener.getInsertQueriesForCurrentThread().size()); assertEquals(0, myCaptureQueriesListener.getDeleteQueriesForCurrentThread().size()); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/interceptor/SearchPreferHandlingInterceptorJpaTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/interceptor/SearchPreferHandlingInterceptorJpaTest.java new file mode 100644 index 00000000000..071641fe3a2 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/interceptor/SearchPreferHandlingInterceptorJpaTest.java @@ -0,0 +1,126 @@ +package ca.uhn.fhir.jpa.interceptor; + +import ca.uhn.fhir.jpa.provider.r4.BaseResourceProviderR4Test; +import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; +import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum; +import ca.uhn.fhir.rest.annotation.OptionalParam; +import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.gclient.StringClientParam; +import ca.uhn.fhir.rest.param.TokenAndListParam; +import ca.uhn.fhir.rest.server.IResourceProvider; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.interceptor.SearchPreferHandlingInterceptor; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.HumanName; +import org.hl7.fhir.r4.model.Patient; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +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.fail; + +public class SearchPreferHandlingInterceptorJpaTest extends BaseResourceProviderR4Test { + + private SearchPreferHandlingInterceptor mySvc; + + @Override + @BeforeEach + public void before() throws Exception { + super.before(); + + mySvc = new SearchPreferHandlingInterceptor(mySearchParamRegistry); + ourRestServer.registerInterceptor(mySvc); + } + + @Override + @AfterEach + public void after() throws Exception { + super.after(); + + ourRestServer.unregisterInterceptor(mySvc); + } + + + @Test + public void testSearchWithInvalidParam_NoHeader() { + try { + myClient + .search() + .forResource(Patient.class) + .where(new StringClientParam("foo").matches().value("bar")) + .prettyPrint() + .returnBundle(Bundle.class) + .encodedJson() + .execute(); + fail(); + } catch (InvalidRequestException e) { + assertThat(e.getMessage(), containsString("Unknown search parameter \"foo\" for resource type \"Patient\". Valid search parameters for this search are: [_id, _language, active, address, address-city, address-country, address-postalcode, address-state, address-use, birthdate, death-date, deceased, email, family, gender, general-practitioner, given, identifier, language, link, name, organization, phone, phonetic, telecom]")); + } + + } + + @Test + public void testSearchWithInvalidParam_StrictHeader() { + try { + myClient + .search() + .forResource(Patient.class) + .where(new StringClientParam("foo").matches().value("bar")) + .withAdditionalHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_HANDLING + "=" + Constants.HEADER_PREFER_HANDLING_STRICT) + .prettyPrint() + .returnBundle(Bundle.class) + .encodedJson() + .execute(); + fail(); + } catch (InvalidRequestException e) { + assertThat(e.getMessage(), containsString("Unknown search parameter \"foo\" for resource type \"Patient\". Valid search parameters for this search are: [_id, _language, active, address, address-city, address-country, address-postalcode, address-state, address-use, birthdate, death-date, deceased, email, family, gender, general-practitioner, given, identifier, language, link, name, organization, phone, phonetic, telecom]")); + } + + } + + @Test + public void testSearchWithInvalidParam_UnrelatedPreferHeader() { + try { + myClient + .search() + .forResource(Patient.class) + .where(new StringClientParam("foo").matches().value("bar")) + .withAdditionalHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RETURN + "=" + Constants.HEADER_PREFER_RETURN_REPRESENTATION) + .prettyPrint() + .returnBundle(Bundle.class) + .encodedJson() + .execute(); + fail(); + } catch (InvalidRequestException e) { + assertThat(e.getMessage(), containsString("Unknown search parameter \"foo\" for resource type \"Patient\". Valid search parameters for this search are: [_id, _language, active, address, address-city, address-country, address-postalcode, address-state, address-use, birthdate, death-date, deceased, email, family, gender, general-practitioner, given, identifier, language, link, name, organization, phone, phonetic, telecom]")); + } + + } + + @Test + public void testSearchWithInvalidParam_LenientHeader() { + Bundle outcome = myClient + .search() + .forResource(Patient.class) + .where(new StringClientParam("foo").matches().value("bar")) + .and(Patient.IDENTIFIER.exactly().codes("BLAH")) + .prettyPrint() + .returnBundle(Bundle.class) + .withAdditionalHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_HANDLING + "=" + Constants.HEADER_PREFER_HANDLING_LENIENT) + .encodedJson() + .execute(); + assertEquals(0, outcome.getTotal()); + + assertEquals(ourServerBase + "/Patient?_format=json&_pretty=true&identifier=BLAH", outcome.getLink(Constants.LINK_SELF).getUrl()); + } + + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/NpmR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/NpmR4Test.java index eeaa4eac020..05940239896 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/NpmR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/NpmR4Test.java @@ -78,8 +78,6 @@ public class NpmR4Test extends BaseJpaR4Test { private static final Logger ourLog = LoggerFactory.getLogger(NpmR4Test.class); @Autowired - public IPackageInstallerSvc igInstaller; - @Autowired private IHapiPackageCacheManager myPackageCacheManager; @Autowired private NpmJpaValidationSupport myNpmJpaValidationSupport; @@ -140,7 +138,7 @@ public class NpmR4Test extends BaseJpaR4Test { .setVersion("3.1.0") .setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_AND_INSTALL) .setFetchDependencies(true); - igInstaller.install(spec); + myPackageInstallerSvc.install(spec); runInTransaction(()->{ SearchParameterMap map = SearchParameterMap.newSynchronous(SearchParameter.SP_BASE, new TokenParam("NamingSystem")); @@ -154,7 +152,7 @@ public class NpmR4Test extends BaseJpaR4Test { } }); - igInstaller.install(spec); + myPackageInstallerSvc.install(spec); } @@ -164,7 +162,7 @@ public class NpmR4Test extends BaseJpaR4Test { myFakeNpmServlet.myResponses.put("/nictiz.fhir.nl.stu3.questionnaires/1.0.2", bytes); PackageInstallationSpec spec = new PackageInstallationSpec().setName("nictiz.fhir.nl.stu3.questionnaires").setVersion("1.0.2").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY); - igInstaller.install(spec); + myPackageInstallerSvc.install(spec); // Be sure no further communication with the server JettyUtil.closeServer(myServer); @@ -203,7 +201,7 @@ public class NpmR4Test extends BaseJpaR4Test { myFakeNpmServlet.myResponses.put("/hl7.fhir.uv.shorthand/0.12.0", bytes); PackageInstallationSpec spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.12.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_AND_INSTALL); - PackageInstallOutcomeJson outcome = igInstaller.install(spec); + PackageInstallOutcomeJson outcome = myPackageInstallerSvc.install(spec); assertEquals(1, outcome.getResourcesInstalled().get("CodeSystem")); // Be sure no further communication with the server @@ -273,7 +271,7 @@ public class NpmR4Test extends BaseJpaR4Test { resourceList.add("Organization"); PackageInstallationSpec spec = new PackageInstallationSpec().setName("test-organizations").setVersion("1.0.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_AND_INSTALL); spec.setInstallResourceTypes(resourceList); - PackageInstallOutcomeJson outcome = igInstaller.install(spec); + PackageInstallOutcomeJson outcome = myPackageInstallerSvc.install(spec); assertEquals(3, outcome.getResourcesInstalled().get("Organization")); // Be sure no further communication with the server @@ -310,7 +308,7 @@ public class NpmR4Test extends BaseJpaR4Test { resourceList.add("Organization"); PackageInstallationSpec spec = new PackageInstallationSpec().setName("test-organizations").setVersion("1.0.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_AND_INSTALL); spec.setInstallResourceTypes(resourceList); - PackageInstallOutcomeJson outcome = igInstaller.install(spec); + PackageInstallOutcomeJson outcome = myPackageInstallerSvc.install(spec); assertEquals(3, outcome.getResourcesInstalled().get("Organization")); // Be sure no further communication with the server @@ -350,7 +348,7 @@ public class NpmR4Test extends BaseJpaR4Test { PackageInstallationSpec spec = new PackageInstallationSpec().setName("test-organizations").setVersion("1.0.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_AND_INSTALL); spec.setInstallResourceTypes(resourceList); try { - PackageInstallOutcomeJson outcome = igInstaller.install(spec); + PackageInstallOutcomeJson outcome = myPackageInstallerSvc.install(spec); fail(); } catch (ImplementationGuideInstallationException theE) { assertThat(theE.getMessage(), containsString("Resources in a package must have a url or identifier to be loaded by the package installer.")); @@ -370,7 +368,7 @@ public class NpmR4Test extends BaseJpaR4Test { resourceList.add("ImplementationGuide"); PackageInstallationSpec spec = new PackageInstallationSpec().setName("test-ig").setVersion("1.0.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_AND_INSTALL); spec.setInstallResourceTypes(resourceList); - PackageInstallOutcomeJson outcome = igInstaller.install(spec); + PackageInstallOutcomeJson outcome = myPackageInstallerSvc.install(spec); assertEquals(1, outcome.getResourcesInstalled().get("ImplementationGuide")); // Be sure no further communication with the server @@ -393,7 +391,7 @@ public class NpmR4Test extends BaseJpaR4Test { myFakeNpmServlet.myResponses.put("/hl7.fhir.uv.shorthand/0.11.1", bytes); PackageInstallationSpec spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.11.1").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_AND_INSTALL); - PackageInstallOutcomeJson outcome = igInstaller.install(spec); + PackageInstallOutcomeJson outcome = myPackageInstallerSvc.install(spec); assertEquals(0, outcome.getResourcesInstalled().size(), outcome.getResourcesInstalled().toString()); } @@ -408,11 +406,11 @@ public class NpmR4Test extends BaseJpaR4Test { PackageInstallOutcomeJson outcome; PackageInstallationSpec spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.12.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_AND_INSTALL); - outcome = igInstaller.install(spec); + outcome = myPackageInstallerSvc.install(spec); assertEquals(1, outcome.getResourcesInstalled().get("CodeSystem")); - igInstaller.install(spec); - outcome = igInstaller.install(spec); + myPackageInstallerSvc.install(spec); + outcome = myPackageInstallerSvc.install(spec); assertEquals(null, outcome.getResourcesInstalled().get("CodeSystem")); // Ensure that we loaded the contents @@ -430,7 +428,7 @@ public class NpmR4Test extends BaseJpaR4Test { myFakeNpmServlet.myResponses.put("/UK.Core.r4/1.1.0", bytes); PackageInstallationSpec spec = new PackageInstallationSpec().setName("UK.Core.r4").setVersion("1.1.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_AND_INSTALL); - igInstaller.install(spec); + myPackageInstallerSvc.install(spec); // Be sure no further communication with the server JettyUtil.closeServer(myServer); @@ -450,9 +448,9 @@ public class NpmR4Test extends BaseJpaR4Test { myFakeNpmServlet.myResponses.put("/hl7.fhir.uv.shorthand/0.11.1", loadClasspathBytes("/packages/hl7.fhir.uv.shorthand-0.11.1.tgz")); PackageInstallationSpec spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.12.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY); - igInstaller.install(spec); + myPackageInstallerSvc.install(spec); spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.11.1").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY); - igInstaller.install(spec); + myPackageInstallerSvc.install(spec); runInTransaction(() -> { NpmPackageMetadataJson metadata = myPackageCacheManager.loadPackageMetadata("hl7.fhir.uv.shorthand"); @@ -484,18 +482,18 @@ public class NpmR4Test extends BaseJpaR4Test { PackageInstallationSpec spec; spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.12.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY); - PackageInstallOutcomeJson outcome = igInstaller.install(spec); + PackageInstallOutcomeJson outcome = myPackageInstallerSvc.install(spec); ourLog.info("Install messages:\n * {}", outcome.getMessage().stream().collect(Collectors.joining("\n * "))); assertThat(outcome.getMessage(), hasItem("Marking package hl7.fhir.uv.shorthand#0.12.0 as current version")); assertThat(outcome.getMessage(), hasItem("Indexing CodeSystem Resource[package/CodeSystem-shorthand-code-system.json] with URL: http://hl7.org/fhir/uv/shorthand/CodeSystem/shorthand-code-system|0.12.0")); spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.11.1").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY); - outcome = igInstaller.install(spec); + outcome = myPackageInstallerSvc.install(spec); ourLog.info("Install messages:\n * {}", outcome.getMessage().stream().collect(Collectors.joining("\n * "))); assertThat(outcome.getMessage(), not(hasItem("Marking package hl7.fhir.uv.shorthand#0.11.1 as current version"))); spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.11.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY); - igInstaller.install(spec); + myPackageInstallerSvc.install(spec); NpmPackage pkg; @@ -519,7 +517,7 @@ public class NpmR4Test extends BaseJpaR4Test { // Install older version PackageInstallationSpec spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.11.1").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY); - igInstaller.install(spec); + myPackageInstallerSvc.install(spec); // Older version is current runInTransaction(() -> { @@ -535,7 +533,7 @@ public class NpmR4Test extends BaseJpaR4Test { // Now install newer version spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.12.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY); - igInstaller.install(spec); + myPackageInstallerSvc.install(spec); // Newer version is current runInTransaction(() -> { @@ -562,7 +560,7 @@ public class NpmR4Test extends BaseJpaR4Test { // Install newer version PackageInstallationSpec spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.12.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY); - igInstaller.install(spec); + myPackageInstallerSvc.install(spec); runInTransaction(() -> { @@ -578,7 +576,7 @@ public class NpmR4Test extends BaseJpaR4Test { // Install older version spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.11.1").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY); - igInstaller.install(spec); + myPackageInstallerSvc.install(spec); // Newer version is still current runInTransaction(() -> { @@ -604,7 +602,7 @@ public class NpmR4Test extends BaseJpaR4Test { // Install PackageInstallationSpec spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.12.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY); - igInstaller.install(spec); + myPackageInstallerSvc.install(spec); runInTransaction(() -> { NpmPackageVersionEntity versionEntity = myPackageVersionDao.findByPackageIdAndVersion("hl7.fhir.uv.shorthand", "0.12.0").orElseThrow(() -> new IllegalArgumentException()); @@ -613,7 +611,7 @@ public class NpmR4Test extends BaseJpaR4Test { // Install same again spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.12.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY); - igInstaller.install(spec); + myPackageInstallerSvc.install(spec); runInTransaction(() -> { NpmPackageVersionEntity versionEntity = myPackageVersionDao.findByPackageIdAndVersion("hl7.fhir.uv.shorthand", "0.12.0").orElseThrow(() -> new IllegalArgumentException()); @@ -634,7 +632,7 @@ public class NpmR4Test extends BaseJpaR4Test { // Install older version PackageInstallationSpec spec = new PackageInstallationSpec().setName("test-exchange.fhir.us.com").setVersion("2.1.1").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_AND_INSTALL); - igInstaller.install(spec); + myPackageInstallerSvc.install(spec); IBundleProvider spSearch = mySearchParameterDao.search(SearchParameterMap.newSynchronous("code", new TokenParam("network-id"))); assertEquals(1, spSearch.sizeOrThrowNpe()); @@ -657,7 +655,7 @@ public class NpmR4Test extends BaseJpaR4Test { // Install newer version spec = new PackageInstallationSpec().setName("test-exchange.fhir.us.com").setVersion("2.1.2").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_AND_INSTALL); - igInstaller.install(spec); + myPackageInstallerSvc.install(spec); spSearch = mySearchParameterDao.search(SearchParameterMap.newSynchronous("code", new TokenParam("network-id"))); assertEquals(1, spSearch.sizeOrThrowNpe()); @@ -675,9 +673,9 @@ public class NpmR4Test extends BaseJpaR4Test { byte[] contents0120 = loadClasspathBytes("/packages/hl7.fhir.uv.shorthand-0.12.0.tgz"); PackageInstallationSpec spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.11.1").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY).setPackageContents(contents0111); - igInstaller.install(spec); + myPackageInstallerSvc.install(spec); spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.12.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY).setPackageContents(contents0120); - igInstaller.install(spec); + myPackageInstallerSvc.install(spec); assertArrayEquals(contents0111, myPackageCacheManager.loadPackageContents("hl7.fhir.uv.shorthand", "0.11.1").getBytes()); @@ -699,9 +697,9 @@ public class NpmR4Test extends BaseJpaR4Test { myFakeNpmServlet.myResponses.put("/hl7.fhir.uv.shorthand/0.11.1", loadClasspathBytes("/packages/hl7.fhir.uv.shorthand-0.11.1.tgz")); myFakeNpmServlet.myResponses.put("/hl7.fhir.uv.shorthand/0.11.0", loadClasspathBytes("/packages/hl7.fhir.uv.shorthand-0.11.0.tgz")); - igInstaller.install(new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.12.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY)); - igInstaller.install(new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.11.1").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY)); - igInstaller.install(new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.11.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY)); + myPackageInstallerSvc.install(new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.12.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY)); + myPackageInstallerSvc.install(new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.11.1").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY)); + myPackageInstallerSvc.install(new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.11.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY)); runInTransaction(() -> { Slice versions = myPackageVersionResourceDao.findCurrentVersionByCanonicalUrl(Pageable.unpaged(), FhirVersionEnum.R4, "http://hl7.org/fhir/uv/shorthand/ValueSet/shorthand-instance-tags"); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java index 969959dea84..3a5a676dcff 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java @@ -7,12 +7,12 @@ import ca.uhn.fhir.jpa.dao.data.IPartitionDao; import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test; import ca.uhn.fhir.jpa.provider.DiffProvider; import ca.uhn.fhir.jpa.provider.GraphQLProvider; +import ca.uhn.fhir.jpa.provider.JpaCapabilityStatementProvider; import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryImpl; import ca.uhn.fhir.jpa.subscription.match.config.WebsocketDispatcherConfig; import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionLoader; -import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionMatcherInterceptor; import ca.uhn.fhir.jpa.util.ResourceCountCache; import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; import ca.uhn.fhir.parser.StrictErrorHandler; @@ -23,7 +23,6 @@ import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.interceptor.CorsInterceptor; import ca.uhn.fhir.test.utilities.JettyUtil; -import ca.uhn.fhir.util.TestUtil; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; @@ -38,7 +37,6 @@ import org.hl7.fhir.r4.model.Patient; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; -import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.context.ContextLoader; import org.springframework.web.context.WebApplicationContext; @@ -57,8 +55,6 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.slf4j.LoggerFactory.getLogger; public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test { - private static final Logger ourLog = getLogger(BaseResourceProviderR4Test.class); - protected static IValidationSupport myValidationSupport; protected static CloseableHttpClient ourHttpClient; @@ -155,8 +151,9 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test { ourRestServer.registerInterceptor(corsInterceptor); ourSearchParamRegistry = myAppCtx.getBean(SearchParamRegistryImpl.class); + IValidationSupport validationSupport = myAppCtx.getBean(IValidationSupport.class); - JpaConformanceProviderR4 confProvider = new JpaConformanceProviderR4(ourRestServer, mySystemDao, myDaoConfig, ourSearchParamRegistry); + JpaCapabilityStatementProvider confProvider = new JpaCapabilityStatementProvider(ourRestServer, mySystemDao, myDaoConfig, ourSearchParamRegistry, validationSupport); confProvider.setImplementationDescription("THIS IS THE DESC"); ourRestServer.setServerConformanceProvider(confProvider); @@ -169,8 +166,6 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test { myValidationSupport = wac.getBean(IValidationSupport.class); mySearchCoordinatorSvc = wac.getBean(ISearchCoordinatorSvc.class); - confProvider.setSearchParamRegistry(ourSearchParamRegistry); - myFhirCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER); myFhirCtx.getRestfulClientFactory().setSocketTimeout(400000); @@ -191,6 +186,13 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test { } } + protected static void clearRestfulServer() throws Exception { + if (ourServer != null) { + JettyUtil.closeServer(ourServer); + } + ourServer = null; + } + protected boolean shouldLogClient() { return true; } 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 new file mode 100644 index 00000000000..23e0bff37e4 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ServerCapabilityStatementProviderJpaR4Test.java @@ -0,0 +1,144 @@ +package ca.uhn.fhir.jpa.provider.r4; + +import ca.uhn.fhir.jpa.packages.PackageInstallationSpec; +import org.hl7.fhir.r4.model.CapabilityStatement; +import org.hl7.fhir.r4.model.Enumerations; +import org.hl7.fhir.r4.model.SearchParameter; +import org.hl7.fhir.r4.model.StructureDefinition; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasItems; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ServerCapabilityStatementProviderJpaR4Test extends BaseResourceProviderR4Test { + + private static final Logger ourLog = LoggerFactory.getLogger(ServerCapabilityStatementProviderJpaR4Test.class); + + @Test + public void testCorrectResourcesReflected() { + CapabilityStatement cs = myClient.capabilities().ofType(CapabilityStatement.class).execute(); + + List resourceTypes = cs.getRest().get(0).getResource().stream().map(t -> t.getType()).collect(Collectors.toList()); + assertThat(resourceTypes, hasItems("Patient", "Observation", "SearchParameter")); + } + + @Test + public void testCustomSearchParamsReflected() { + SearchParameter fooSp = new SearchParameter(); + fooSp.addBase("Patient"); + fooSp.setCode("foo"); + fooSp.setUrl("http://acme.com/foo"); + fooSp.setType(org.hl7.fhir.r4.model.Enumerations.SearchParamType.TOKEN); + fooSp.setTitle("FOO SP"); + fooSp.setDescription("This is a search param!"); + fooSp.setExpression("Patient.gender"); + fooSp.setXpathUsage(org.hl7.fhir.r4.model.SearchParameter.XPathUsageType.NORMAL); + fooSp.setStatus(org.hl7.fhir.r4.model.Enumerations.PublicationStatus.ACTIVE); + mySearchParameterDao.create(fooSp); + mySearchParamRegistry.forceRefresh(); + + CapabilityStatement cs = myClient.capabilities().ofType(CapabilityStatement.class).execute(); + + List fooSearchParams = findSearchParams(cs, "Patient", "foo"); + assertEquals(1, fooSearchParams.size()); + assertEquals("foo", fooSearchParams.get(0).getName()); + assertEquals("http://acme.com/foo", fooSearchParams.get(0).getDefinition()); + assertEquals("This is a search param!", fooSearchParams.get(0).getDocumentation()); + assertEquals(Enumerations.SearchParamType.TOKEN, fooSearchParams.get(0).getType()); + + } + + @Test + public void testRegisteredProfilesReflected_StoredInServer() throws IOException { + StructureDefinition sd = loadResourceFromClasspath(StructureDefinition.class, "/r4/StructureDefinition-kfdrc-patient.json"); + myStructureDefinitionDao.update(sd); + StructureDefinition sd2 = loadResourceFromClasspath(StructureDefinition.class, "/r4/StructureDefinition-kfdrc-patient-no-phi.json"); + myStructureDefinitionDao.update(sd2); + + CapabilityStatement cs = myClient.capabilities().ofType(CapabilityStatement.class).execute(); + + List supportedProfiles = findSupportedProfiles(cs, "Patient"); + assertThat(supportedProfiles.toString(), supportedProfiles, containsInAnyOrder( + "http://fhir.kids-first.io/StructureDefinition/kfdrc-patient", + "http://fhir.kids-first.io/StructureDefinition/kfdrc-patient-no-phi" + )); + } + + /** + * Universal profiles like vitalsigns should not be excluded + */ + @Test + public void testRegisteredProfilesReflected_Universal() throws IOException { + StructureDefinition sd = loadResourceFromClasspath(StructureDefinition.class, "/r4/r4-create-structuredefinition-vital-signs.json"); + ourLog.info("Stored SD to ID: {}", myStructureDefinitionDao.update(sd).getId()); + + CapabilityStatement cs = myClient.capabilities().ofType(CapabilityStatement.class).execute(); + + List supportedProfiles = findSupportedProfiles(cs, "Observation"); + assertThat(supportedProfiles.toString(), supportedProfiles, containsInAnyOrder( + "http://hl7.org/fhir/StructureDefinition/vitalsigns" + )); + } + + @Test + public void testRegisteredProfilesReflected_StoredInPackageRegistry() throws IOException { + byte[] bytes = loadClasspathBytes("/packages/UK.Core.r4-1.1.0.tgz"); + PackageInstallationSpec spec = new PackageInstallationSpec() + .setName("UK.Core.r4") + .setVersion("1.1.0") + .setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY) + .setPackageContents(bytes); + myPackageInstallerSvc.install(spec); + + CapabilityStatement cs = myClient.capabilities().ofType(CapabilityStatement.class).execute(); + + List supportedProfiles = findSupportedProfiles(cs, "Patient"); + assertThat(supportedProfiles.toString(), supportedProfiles, containsInAnyOrder( + "https://fhir.nhs.uk/R4/StructureDefinition/UKCore-Patient" + )); + } + + @Nonnull + private List findSupportedProfiles(CapabilityStatement theCapabilityStatement, String theResourceType) { + assertEquals(1, theCapabilityStatement.getRest().size()); + return theCapabilityStatement + .getRest() + .get(0) + .getResource() + .stream() + .filter(t -> t.getType().equals(theResourceType)) + .findFirst() + .orElseThrow(() -> new IllegalStateException()) + .getSupportedProfile() + .stream() + .map(t -> t.getValue()) + .collect(Collectors.toList()); + } + + @Nonnull + private List findSearchParams(CapabilityStatement theCapabilityStatement, String theResourceType, String theParamName) { + assertEquals(1, theCapabilityStatement.getRest().size()); + return theCapabilityStatement + .getRest() + .get(0) + .getResource() + .stream() + .filter(t -> t.getType().equals(theResourceType)) + .findFirst() + .orElseThrow(() -> new IllegalStateException()) + .getSearchParam() + .stream() + .filter(t -> t.getName().equals(theParamName)) + .collect(Collectors.toList()); + } + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r5/BaseResourceProviderR5Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r5/BaseResourceProviderR5Test.java index 0f24f202834..f83c8723fa1 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r5/BaseResourceProviderR5Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r5/BaseResourceProviderR5Test.java @@ -5,6 +5,7 @@ import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc; import ca.uhn.fhir.jpa.dao.r5.BaseJpaR5Test; import ca.uhn.fhir.jpa.provider.GraphQLProvider; +import ca.uhn.fhir.jpa.provider.JpaCapabilityStatementProvider; import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryImpl; @@ -19,10 +20,8 @@ import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum; import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; import ca.uhn.fhir.rest.server.RestfulServer; -import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; import ca.uhn.fhir.rest.server.interceptor.CorsInterceptor; import ca.uhn.fhir.test.utilities.JettyUtil; -import ca.uhn.fhir.util.TestUtil; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; @@ -110,8 +109,11 @@ public abstract class BaseResourceProviderR5Test extends BaseJpaR5Test { ourRestServer.registerProviders(mySystemProvider, myTerminologyUploaderProvider); ourRestServer.registerProvider(myAppCtx.getBean(GraphQLProvider.class)); + IValidationSupport validationSupport = myAppCtx.getBean(IValidationSupport.class); - JpaConformanceProviderR5 confProvider = new JpaConformanceProviderR5(ourRestServer, mySystemDao, myDaoConfig, ourSearchParamRegistry); + ourSearchParamRegistry = myAppCtx.getBean(SearchParamRegistryImpl.class); + + JpaCapabilityStatementProvider confProvider = new JpaCapabilityStatementProvider(ourRestServer, mySystemDao, myDaoConfig, ourSearchParamRegistry, validationSupport); confProvider.setImplementationDescription("THIS IS THE DESC"); ourRestServer.setServerConformanceProvider(confProvider); @@ -166,11 +168,9 @@ public abstract class BaseResourceProviderR5Test extends BaseJpaR5Test { WebApplicationContext wac = WebApplicationContextUtils.getWebApplicationContext(subsServletHolder.getServlet().getServletConfig().getServletContext()); myValidationSupport = wac.getBean(IValidationSupport.class); mySearchCoordinatorSvc = wac.getBean(ISearchCoordinatorSvc.class); - ourSearchParamRegistry = wac.getBean(SearchParamRegistryImpl.class); ourSubscriptionMatcherInterceptor = wac.getBean(SubscriptionMatcherInterceptor.class); myFhirCtx.getRestfulClientFactory().setSocketTimeout(5000000); - confProvider.setSearchParamRegistry(ourSearchParamRegistry); PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); HttpClientBuilder builder = HttpClientBuilder.create(); diff --git a/hapi-fhir-jpaserver-base/src/test/resources/r4/r4-create-structuredefinition-vital-signs.json b/hapi-fhir-jpaserver-base/src/test/resources/r4/r4-create-structuredefinition-vital-signs.json new file mode 100644 index 00000000000..d854346aa33 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/resources/r4/r4-create-structuredefinition-vital-signs.json @@ -0,0 +1,3122 @@ +{ + "resourceType" : "StructureDefinition", + "id" : "vitalsigns", + "text" : { + "status" : "generated", + "div" : "
to do
" + }, + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/structuredefinition-summary", + "valueMarkdown" : "\"#### Complete Summary of the Mandatory Requirements\n\n1. One status in `Observation.status`which has a [required](http://build.fhir.org/terminologies.html#extensible) binding to:\n - [ObservationStatus] value set.\n1. A category in `Observation.category` which must have:\n - a fixed `Observation.category.coding.system`=\"http://terminology.hl7.org/CodeSystem/observation-category\"\n - a fixed `Observation.category.coding.code`= \"vital-signs\"\n\n1. A code in `Observation.code`\n - a fixed `Observation.code.coding.system`= \"http://loinc.org\"\"\n - a LOINC code in `Observation.code.coding.code` which has an [extensible](http://build.fhir.org/terminologies.html#extensible) binding to:\n - [Vital Signs] value set.\n \n1. One patient in `Observation.subject`\n1. A date and time in `effectiveDateTime` or `effectivePeriod`\n1. Either one `Observation.value[x]` or, if there is no value, one code in `Observation.DataAbsentReason`\n - if a vital sign measure then:\n - One numeric value in Observation.valueQuantity.value\n - a fixed Observation.valueQuantity.system=\"http://unitsofmeasure.org\"\n - a UCUM unit code in Observation.valueQuantity.code which has an required binding to the [Vital Signs Units] value set.\n - Observation.DataAbsentReason is bound to [Observation Value\n Absent Reason] value set.\n\n1. When using a panel code to group component observations (Note: See\n the comments regarding blood pressure in the table above), one or\n more `Observation.component.code` each of which must have:\n - a fixed\n `Observation.component.code.coding.system` =\"\"http://loinc.org\"\"\n - a LOINC code in `Observation.code.coding.code` which has an [extensible] binding to:\n - [Vital Signs Units] value set.\n\n1. Either one `Observation.component.valueQuantity` or, if there is\n no value, one code in `Observation.component.DataAbsentReason`\n - Observation.component.DataAbsentReason is bound to [Observation\n Value Absent Reason] value set.\n\n1. When using a panel code to group observations, one or more reference\n to Vitals Signs Observations in `Observation.related.target`\n - a fixed `Observation.related.type`= \"has-member\"\"\n\n [Vital Signs]: valueset-observation-vitalsignresult.html\n [Vital Signs Units]: valueset-ucum-vitals-common.html\n [extensible]: terminologies.html#extensible\n [ObservationStatus]: valueset-observation-status.html\n [Observation Value Absent Reason]: valueset-data-absent-reason.html\n[required]: terminologies.html#required\"" + }, + { + "url" : "http://hl7.org/fhir/StructureDefinition/structuredefinition-fmm", + "valueInteger" : 5 + }, + { + "url" : "http://hl7.org/fhir/StructureDefinition/structuredefinition-wg", + "valueCode" : "oo" + }, + { + "url" : "http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status", + "valueCode" : "trial-use" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/vitalsigns", + "version" : "4.0.1", + "name" : "observation-vitalsigns", + "title" : "Vital Signs Profile", + "status" : "draft", + "experimental" : false, + "date" : "2016-03-25", + "publisher" : "Health Level Seven International (Orders and Observations Workgroup)", + "contact" : [{ + "telecom" : [{ + "system" : "url", + "value" : "http://www.hl7.org/Special/committees/orders/index.cfm Orders and Observations" + }] + }], + "description" : "FHIR Vital Signs Profile", + "fhirVersion" : "4.0.1", + "mapping" : [{ + "identity" : "workflow", + "uri" : "http://hl7.org/fhir/workflow", + "name" : "Workflow Pattern" + }, + { + "identity" : "sct-concept", + "uri" : "http://snomed.info/conceptdomain", + "name" : "SNOMED CT Concept Domain Binding" + }, + { + "identity" : "v2", + "uri" : "http://hl7.org/v2", + "name" : "HL7 v2 Mapping" + }, + { + "identity" : "rim", + "uri" : "http://hl7.org/v3", + "name" : "RIM Mapping" + }, + { + "identity" : "w5", + "uri" : "http://hl7.org/fhir/fivews", + "name" : "FiveWs Pattern Mapping" + }, + { + "identity" : "sct-attr", + "uri" : "http://snomed.org/attributebinding", + "name" : "SNOMED CT Attribute Binding" + }], + "kind" : "resource", + "abstract" : false, + "type" : "Observation", + "baseDefinition" : "http://hl7.org/fhir/StructureDefinition/Observation", + "derivation" : "constraint", + "snapshot" : { + "element" : [{ + "id" : "Observation", + "path" : "Observation", + "short" : "FHIR Vital Signs Profile", + "definition" : "The FHIR Vitals Signs profile sets minimum expectations for the Observation Resource to record, search and fetch the vital signs associated with a patient.", + "comment" : "Used for simple observations such as device measurements, laboratory atomic results, vital signs, height, weight, smoking status, comments, etc. Other resources are used to provide context for observations such as laboratory reports, etc.", + "alias" : ["Vital Signs", + "Measurement", + "Results", + "Tests"], + "min" : 0, + "max" : "*", + "base" : { + "path" : "Observation", + "min" : 0, + "max" : "*" + }, + "constraint" : [{ + "key" : "dom-2", + "severity" : "error", + "human" : "If the resource is contained in another resource, it SHALL NOT contain nested Resources", + "expression" : "contained.contained.empty()", + "xpath" : "not(parent::f:contained and f:contained)", + "source" : "http://hl7.org/fhir/StructureDefinition/DomainResource" + }, + { + "key" : "dom-3", + "severity" : "error", + "human" : "If the resource is contained in another resource, it SHALL be referred to from elsewhere in the resource or SHALL refer to the containing resource", + "expression" : "contained.where((('#'+id in (%resource.descendants().reference | %resource.descendants().as(canonical) | %resource.descendants().as(uri) | %resource.descendants().as(url))) or descendants().where(reference = '#').exists() or descendants().where(as(canonical) = '#').exists() or descendants().where(as(canonical) = '#').exists()).not()).trace('unmatched', id).empty()", + "xpath" : "not(exists(for $id in f:contained/*/f:id/@value return $contained[not(parent::*/descendant::f:reference/@value=concat('#', $contained/*/id/@value) or descendant::f:reference[@value='#'])]))", + "source" : "http://hl7.org/fhir/StructureDefinition/DomainResource" + }, + { + "key" : "dom-4", + "severity" : "error", + "human" : "If a resource is contained in another resource, it SHALL NOT have a meta.versionId or a meta.lastUpdated", + "expression" : "contained.meta.versionId.empty() and contained.meta.lastUpdated.empty()", + "xpath" : "not(exists(f:contained/*/f:meta/f:versionId)) and not(exists(f:contained/*/f:meta/f:lastUpdated))", + "source" : "http://hl7.org/fhir/StructureDefinition/DomainResource" + }, + { + "key" : "dom-5", + "severity" : "error", + "human" : "If a resource is contained in another resource, it SHALL NOT have a security label", + "expression" : "contained.meta.security.empty()", + "xpath" : "not(exists(f:contained/*/f:meta/f:security))", + "source" : "http://hl7.org/fhir/StructureDefinition/DomainResource" + }, + { + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-bestpractice", + "valueBoolean" : true + }, + { + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-bestpractice-explanation", + "valueMarkdown" : "When a resource has no narrative, only systems that fully understand the data can display the resource to a human safely. Including a human readable representation in the resource makes for a much more robust eco-system and cheaper handling of resources by intermediary systems. Some ecosystems restrict distribution of resources to only those systems that do fully understand the resources, and as a consequence implementers may believe that the narrative is superfluous. However experience shows that such eco-systems often open up to new participants over time." + }], + "key" : "dom-6", + "severity" : "warning", + "human" : "A resource should have narrative for robust management", + "expression" : "text.`div`.exists()", + "xpath" : "exists(f:text/h:div)", + "source" : "http://hl7.org/fhir/StructureDefinition/DomainResource" + }, + { + "key" : "obs-6", + "severity" : "error", + "human" : "dataAbsentReason SHALL only be present if Observation.value[x] is not present", + "expression" : "dataAbsentReason.empty() or value.empty()", + "xpath" : "not(exists(f:dataAbsentReason)) or (not(exists(*[starts-with(local-name(.), 'value')])))", + "source" : "http://hl7.org/fhir/StructureDefinition/Observation" + }, + { + "key" : "obs-7", + "severity" : "error", + "human" : "If Observation.code is the same as an Observation.component.code then the value element associated with the code SHALL NOT be present", + "expression" : "value.empty() or component.code.where(coding.intersect(%resource.code.coding).exists()).empty()", + "xpath" : "not(f:*[starts-with(local-name(.), 'value')] and (for $coding in f:code/f:coding return f:component/f:code/f:coding[f:code/@value=$coding/f:code/@value] [f:system/@value=$coding/f:system/@value]))", + "source" : "http://hl7.org/fhir/StructureDefinition/Observation" + }, + { + "key" : "vs-2", + "severity" : "error", + "human" : "If there is no component or hasMember element then either a value[x] or a data absent reason must be present.", + "expression" : "(component.empty() and hasMember.empty()) implies (dataAbsentReason.exists() or value.exists())", + "xpath" : "f:component or f:memberOF or f:*[starts-with(local-name(.), 'value')] or f:dataAbsentReason" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "rim", + "map" : "Entity. Role, or Act" + }, + { + "identity" : "workflow", + "map" : "Event" + }, + { + "identity" : "sct-concept", + "map" : "< 363787002 |Observable entity|" + }, + { + "identity" : "v2", + "map" : "OBX" + }, + { + "identity" : "rim", + "map" : "Observation[classCode=OBS, moodCode=EVN]" + }] + }, + { + "id" : "Observation.id", + "path" : "Observation.id", + "short" : "Logical id of this artifact", + "definition" : "The logical id of the resource, as used in the URL for the resource. Once assigned, this value never changes.", + "comment" : "The only time that a resource does not have an id is when it is being submitted to the server using a create operation.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Resource.id", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/structuredefinition-fhir-type", + "valueUrl" : "string" + }], + "code" : "http://hl7.org/fhirpath/System.String" + }], + "isModifier" : false, + "isSummary" : true + }, + { + "id" : "Observation.meta", + "path" : "Observation.meta", + "short" : "Metadata about the resource", + "definition" : "The metadata about the resource. This is content that is maintained by the infrastructure. Changes to the content might not always be associated with version changes to the resource.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Resource.meta", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "Meta" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : true + }, + { + "id" : "Observation.implicitRules", + "path" : "Observation.implicitRules", + "short" : "A set of rules under which this content was created", + "definition" : "A reference to a set of rules that were followed when the resource was constructed, and which must be understood when processing the content. Often, this is a reference to an implementation guide that defines the special rules along with other profiles etc.", + "comment" : "Asserting this rule set restricts the content to be only understood by a limited set of trading partners. This inherently limits the usefulness of the data in the long term. However, the existing health eco-system is highly fractured, and not yet ready to define, collect, and exchange data in a generally computable sense. Wherever possible, implementers and/or specification writers should avoid using this element. Often, when used, the URL is a reference to an implementation guide that defines these special rules as part of it's narrative along with other profiles, value sets, etc.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Resource.implicitRules", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "uri" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : true, + "isModifierReason" : "This element is labeled as a modifier because the implicit rules may provide additional knowledge about the resource that modifies it's meaning or interpretation", + "isSummary" : true + }, + { + "id" : "Observation.language", + "path" : "Observation.language", + "short" : "Language of the resource content", + "definition" : "The base language in which the resource is written.", + "comment" : "Language is provided to support indexing and accessibility (typically, services such as text to speech use the language tag). The html language tag in the narrative applies to the narrative. The language tag on the resource may be used to specify the language of other presentations generated from the data in the resource. Not all the content has to be in the base language. The Resource.language should not be assumed to apply to the narrative automatically. If a language is specified, it should it also be specified on the div element in the html (see rules in HTML5 for information about the relationship between xml:lang and the html lang attribute).", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Resource.language", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "code" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : false, + "binding" : { + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet", + "valueCanonical" : "http://hl7.org/fhir/ValueSet/all-languages" + }, + { + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString" : "Language" + }, + { + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-isCommonBinding", + "valueBoolean" : true + }], + "strength" : "preferred", + "description" : "A human language.", + "valueSet" : "http://hl7.org/fhir/ValueSet/languages" + } + }, + { + "id" : "Observation.text", + "path" : "Observation.text", + "short" : "Text summary of the resource, for human interpretation", + "definition" : "A human-readable narrative that contains a summary of the resource and can be used to represent the content of the resource to a human. The narrative need not encode all the structured data, but is required to contain sufficient detail to make it \"clinically safe\" for a human to just read the narrative. Resource definitions may define what content should be represented in the narrative to ensure clinical safety.", + "comment" : "Contained resources do not have narrative. Resources that are not contained SHOULD have a narrative. In some cases, a resource may only have text with little or no additional discrete data (as long as all minOccurs=1 elements are satisfied). This may be necessary for data from legacy systems where information is captured as a \"text blob\" or where text is additionally entered raw or narrated and encoded information is added later.", + "alias" : ["narrative", + "html", + "xhtml", + "display"], + "min" : 0, + "max" : "1", + "base" : { + "path" : "DomainResource.text", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "Narrative" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "rim", + "map" : "Act.text?" + }] + }, + { + "id" : "Observation.contained", + "path" : "Observation.contained", + "short" : "Contained, inline Resources", + "definition" : "These resources do not have an independent existence apart from the resource that contains them - they cannot be identified independently, and nor can they have their own independent transaction scope.", + "comment" : "This should never be done when the content can be identified properly, as once identification is lost, it is extremely difficult (and context dependent) to restore it again. Contained resources may have profiles and tags In their meta elements, but SHALL NOT have security labels.", + "alias" : ["inline resources", + "anonymous resources", + "contained resources"], + "min" : 0, + "max" : "*", + "base" : { + "path" : "DomainResource.contained", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "Resource" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "rim", + "map" : "N/A" + }] + }, + { + "id" : "Observation.extension", + "path" : "Observation.extension", + "short" : "Additional content defined by implementations", + "definition" : "May be used to represent additional information that is not part of the basic definition of the resource. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension.", + "comment" : "There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.", + "alias" : ["extensions", + "user content"], + "min" : 0, + "max" : "*", + "base" : { + "path" : "DomainResource.extension", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "Extension" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }, + { + "key" : "ext-1", + "severity" : "error", + "human" : "Must have either extensions or value[x], not both", + "expression" : "extension.exists() != value.exists()", + "xpath" : "exists(f:extension)!=exists(f:*[starts-with(local-name(.), \"value\")])", + "source" : "http://hl7.org/fhir/StructureDefinition/Extension" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "rim", + "map" : "N/A" + }] + }, + { + "id" : "Observation.modifierExtension", + "path" : "Observation.modifierExtension", + "short" : "Extensions that cannot be ignored", + "definition" : "May be used to represent additional information that is not part of the basic definition of the resource and that modifies the understanding of the element that contains it and/or the understanding of the containing element's descendants. Usually modifier elements provide negation or qualification. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer is allowed to define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. Applications processing a resource are required to check for modifier extensions.\n\nModifier extensions SHALL NOT change the meaning of any elements on Resource or DomainResource (including cannot change the meaning of modifierExtension itself).", + "comment" : "There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.", + "requirements" : "Modifier extensions allow for extensions that *cannot* be safely ignored to be clearly distinguished from the vast majority of extensions which can be safely ignored. This promotes interoperability by eliminating the need for implementers to prohibit the presence of extensions. For further information, see the [definition of modifier extensions](http://hl7.org/fhir/extensibility.html#modifierExtension).", + "alias" : ["extensions", + "user content"], + "min" : 0, + "max" : "*", + "base" : { + "path" : "DomainResource.modifierExtension", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "Extension" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }, + { + "key" : "ext-1", + "severity" : "error", + "human" : "Must have either extensions or value[x], not both", + "expression" : "extension.exists() != value.exists()", + "xpath" : "exists(f:extension)!=exists(f:*[starts-with(local-name(.), \"value\")])", + "source" : "http://hl7.org/fhir/StructureDefinition/Extension" + }], + "isModifier" : true, + "isModifierReason" : "Modifier extensions are expected to modify the meaning or interpretation of the resource that contains them", + "isSummary" : false, + "mapping" : [{ + "identity" : "rim", + "map" : "N/A" + }] + }, + { + "id" : "Observation.identifier", + "path" : "Observation.identifier", + "short" : "Business Identifier for observation", + "definition" : "A unique identifier assigned to this observation.", + "requirements" : "Allows observations to be distinguished and referenced.", + "min" : 0, + "max" : "*", + "base" : { + "path" : "Observation.identifier", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "Identifier" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : true, + "mapping" : [{ + "identity" : "workflow", + "map" : "Event.identifier" + }, + { + "identity" : "w5", + "map" : "FiveWs.identifier" + }, + { + "identity" : "v2", + "map" : "OBX.21 For OBX segments from systems without OBX-21 support a combination of ORC/OBR and OBX must be negotiated between trading partners to uniquely identify the OBX segment. Depending on how V2 has been implemented each of these may be an option: 1) OBR-3 + OBX-3 + OBX-4 or 2) OBR-3 + OBR-4 + OBX-3 + OBX-4 or 2) some other way to uniquely ID the OBR/ORC + OBX-3 + OBX-4." + }, + { + "identity" : "rim", + "map" : "id" + }] + }, + { + "id" : "Observation.basedOn", + "path" : "Observation.basedOn", + "short" : "Fulfills plan, proposal or order", + "definition" : "A plan, proposal or order that is fulfilled in whole or in part by this event. For example, a MedicationRequest may require a patient to have laboratory test performed before it is dispensed.", + "requirements" : "Allows tracing of authorization for the event and tracking whether proposals/recommendations were acted upon.", + "alias" : ["Fulfills"], + "min" : 0, + "max" : "*", + "base" : { + "path" : "Observation.basedOn", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "Reference", + "targetProfile" : ["http://hl7.org/fhir/StructureDefinition/CarePlan", + "http://hl7.org/fhir/StructureDefinition/DeviceRequest", + "http://hl7.org/fhir/StructureDefinition/ImmunizationRecommendation", + "http://hl7.org/fhir/StructureDefinition/MedicationRequest", + "http://hl7.org/fhir/StructureDefinition/NutritionOrder", + "http://hl7.org/fhir/StructureDefinition/ServiceRequest"] + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : true, + "mapping" : [{ + "identity" : "workflow", + "map" : "Event.basedOn" + }, + { + "identity" : "v2", + "map" : "ORC" + }, + { + "identity" : "rim", + "map" : ".inboundRelationship[typeCode=COMP].source[moodCode=EVN]" + }] + }, + { + "id" : "Observation.partOf", + "path" : "Observation.partOf", + "short" : "Part of referenced event", + "definition" : "A larger event of which this particular Observation is a component or step. For example, an observation as part of a procedure.", + "comment" : "To link an Observation to an Encounter use `encounter`. See the [Notes](http://hl7.org/fhir/observation.html#obsgrouping) below for guidance on referencing another Observation.", + "alias" : ["Container"], + "min" : 0, + "max" : "*", + "base" : { + "path" : "Observation.partOf", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "Reference", + "targetProfile" : ["http://hl7.org/fhir/StructureDefinition/MedicationAdministration", + "http://hl7.org/fhir/StructureDefinition/MedicationDispense", + "http://hl7.org/fhir/StructureDefinition/MedicationStatement", + "http://hl7.org/fhir/StructureDefinition/Procedure", + "http://hl7.org/fhir/StructureDefinition/Immunization", + "http://hl7.org/fhir/StructureDefinition/ImagingStudy"] + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : true, + "mapping" : [{ + "identity" : "workflow", + "map" : "Event.partOf" + }, + { + "identity" : "v2", + "map" : "Varies by domain" + }, + { + "identity" : "rim", + "map" : ".outboundRelationship[typeCode=FLFS].target" + }] + }, + { + "id" : "Observation.status", + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/structuredefinition-display-hint", + "valueString" : "default: final" + }], + "path" : "Observation.status", + "short" : "registered | preliminary | final | amended +", + "definition" : "The status of the result value.", + "comment" : "This element is labeled as a modifier because the status contains codes that mark the resource as not currently valid.", + "requirements" : "Need to track the status of individual results. Some results are finalized before the whole report is finalized.", + "min" : 1, + "max" : "1", + "base" : { + "path" : "Observation.status", + "min" : 1, + "max" : "1" + }, + "type" : [{ + "code" : "code" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "mustSupport" : true, + "isModifier" : true, + "isModifierReason" : "This element is labeled as a modifier because it is a status element that contains status entered-in-error which means that the resource should not be treated as valid", + "isSummary" : true, + "binding" : { + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString" : "Status" + }], + "strength" : "required", + "valueSet" : "http://hl7.org/fhir/ValueSet/observation-status|4.0.1" + }, + "mapping" : [{ + "identity" : "workflow", + "map" : "Event.status" + }, + { + "identity" : "w5", + "map" : "FiveWs.status" + }, + { + "identity" : "sct-concept", + "map" : "< 445584004 |Report by finality status|" + }, + { + "identity" : "v2", + "map" : "OBX-11" + }, + { + "identity" : "rim", + "map" : "status Amended & Final are differentiated by whether it is the subject of a ControlAct event with a type of \"revise\"" + }] + }, + { + "id" : "Observation.category", + "path" : "Observation.category", + "slicing" : { + "discriminator" : [{ + "type" : "value", + "path" : "coding.code" + }, + { + "type" : "value", + "path" : "coding.system" + }], + "ordered" : false, + "rules" : "open" + }, + "short" : "Classification of type of observation", + "definition" : "A code that classifies the general type of observation being made.", + "comment" : "In addition to the required category valueset, this element allows various categorization schemes based on the owner’s definition of the category and effectively multiple categories can be used at once. The level of granularity is defined by the category concepts in the value set.", + "requirements" : "Used for filtering what observations are retrieved and displayed.", + "min" : 1, + "max" : "*", + "base" : { + "path" : "Observation.category", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "CodeableConcept" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "mustSupport" : true, + "isModifier" : false, + "isSummary" : false, + "binding" : { + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString" : "ObservationCategory" + }], + "strength" : "preferred", + "description" : "Codes for high level observation categories.", + "valueSet" : "http://hl7.org/fhir/ValueSet/observation-category" + }, + "mapping" : [{ + "identity" : "w5", + "map" : "FiveWs.class" + }, + { + "identity" : "rim", + "map" : ".outboundRelationship[typeCode=\"COMP].target[classCode=\"LIST\", moodCode=\"EVN\"].code" + }] + }, + { + "id" : "Observation.category:VSCat", + "path" : "Observation.category", + "sliceName" : "VSCat", + "short" : "Classification of type of observation", + "definition" : "A code that classifies the general type of observation being made.", + "comment" : "In addition to the required category valueset, this element allows various categorization schemes based on the owner’s definition of the category and effectively multiple categories can be used at once. The level of granularity is defined by the category concepts in the value set.", + "requirements" : "Used for filtering what observations are retrieved and displayed.", + "min" : 1, + "max" : "1", + "base" : { + "path" : "Observation.category", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "CodeableConcept" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "mustSupport" : true, + "isModifier" : false, + "isSummary" : false, + "binding" : { + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString" : "ObservationCategory" + }], + "strength" : "preferred", + "description" : "Codes for high level observation categories.", + "valueSet" : "http://hl7.org/fhir/ValueSet/observation-category" + }, + "mapping" : [{ + "identity" : "w5", + "map" : "FiveWs.class" + }, + { + "identity" : "rim", + "map" : ".outboundRelationship[typeCode=\"COMP].target[classCode=\"LIST\", moodCode=\"EVN\"].code" + }] + }, + { + "id" : "Observation.category:VSCat.id", + "path" : "Observation.category.id", + "representation" : ["xmlAttr"], + "short" : "Unique id for inter-element referencing", + "definition" : "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Element.id", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/structuredefinition-fhir-type", + "valueUrl" : "string" + }], + "code" : "http://hl7.org/fhirpath/System.String" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "rim", + "map" : "n/a" + }] + }, + { + "id" : "Observation.category:VSCat.extension", + "path" : "Observation.category.extension", + "slicing" : { + "discriminator" : [{ + "type" : "value", + "path" : "url" + }], + "description" : "Extensions are always sliced by (at least) url", + "rules" : "open" + }, + "short" : "Additional content defined by implementations", + "definition" : "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension.", + "comment" : "There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.", + "alias" : ["extensions", + "user content"], + "min" : 0, + "max" : "*", + "base" : { + "path" : "Element.extension", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "Extension" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }, + { + "key" : "ext-1", + "severity" : "error", + "human" : "Must have either extensions or value[x], not both", + "expression" : "extension.exists() != value.exists()", + "xpath" : "exists(f:extension)!=exists(f:*[starts-with(local-name(.), \"value\")])", + "source" : "http://hl7.org/fhir/StructureDefinition/Extension" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "rim", + "map" : "n/a" + }] + }, + { + "id" : "Observation.category:VSCat.coding", + "path" : "Observation.category.coding", + "short" : "Code defined by a terminology system", + "definition" : "A reference to a code defined by a terminology system.", + "comment" : "Codes may be defined very casually in enumerations, or code lists, up to very formal definitions such as SNOMED CT - see the HL7 v3 Core Principles for more information. Ordering of codings is undefined and SHALL NOT be used to infer meaning. Generally, at most only one of the coding values will be labeled as UserSelected = true.", + "requirements" : "Allows for alternative encodings within a code system, and translations to other code systems.", + "min" : 1, + "max" : "*", + "base" : { + "path" : "CodeableConcept.coding", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "Coding" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "mustSupport" : true, + "isModifier" : false, + "isSummary" : true, + "mapping" : [{ + "identity" : "v2", + "map" : "C*E.1-8, C*E.10-22" + }, + { + "identity" : "rim", + "map" : "union(., ./translation)" + }, + { + "identity" : "orim", + "map" : "fhir:CodeableConcept.coding rdfs:subPropertyOf dt:CD.coding" + }] + }, + { + "id" : "Observation.category:VSCat.coding.id", + "path" : "Observation.category.coding.id", + "representation" : ["xmlAttr"], + "short" : "Unique id for inter-element referencing", + "definition" : "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Element.id", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/structuredefinition-fhir-type", + "valueUrl" : "string" + }], + "code" : "http://hl7.org/fhirpath/System.String" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "rim", + "map" : "n/a" + }] + }, + { + "id" : "Observation.category:VSCat.coding.extension", + "path" : "Observation.category.coding.extension", + "slicing" : { + "discriminator" : [{ + "type" : "value", + "path" : "url" + }], + "description" : "Extensions are always sliced by (at least) url", + "rules" : "open" + }, + "short" : "Additional content defined by implementations", + "definition" : "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension.", + "comment" : "There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.", + "alias" : ["extensions", + "user content"], + "min" : 0, + "max" : "*", + "base" : { + "path" : "Element.extension", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "Extension" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }, + { + "key" : "ext-1", + "severity" : "error", + "human" : "Must have either extensions or value[x], not both", + "expression" : "extension.exists() != value.exists()", + "xpath" : "exists(f:extension)!=exists(f:*[starts-with(local-name(.), \"value\")])", + "source" : "http://hl7.org/fhir/StructureDefinition/Extension" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "rim", + "map" : "n/a" + }] + }, + { + "id" : "Observation.category:VSCat.coding.system", + "path" : "Observation.category.coding.system", + "short" : "Identity of the terminology system", + "definition" : "The identification of the code system that defines the meaning of the symbol in the code.", + "comment" : "The URI may be an OID (urn:oid:...) or a UUID (urn:uuid:...). OIDs and UUIDs SHALL be references to the HL7 OID registry. Otherwise, the URI should come from HL7's list of FHIR defined special URIs or it should reference to some definition that establishes the system clearly and unambiguously.", + "requirements" : "Need to be unambiguous about the source of the definition of the symbol.", + "min" : 1, + "max" : "1", + "base" : { + "path" : "Coding.system", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "uri" + }], + "fixedUri" : "http://terminology.hl7.org/CodeSystem/observation-category", + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "mustSupport" : true, + "isModifier" : false, + "isSummary" : true, + "mapping" : [{ + "identity" : "v2", + "map" : "C*E.3" + }, + { + "identity" : "rim", + "map" : "./codeSystem" + }, + { + "identity" : "orim", + "map" : "fhir:Coding.system rdfs:subPropertyOf dt:CDCoding.codeSystem" + }] + }, + { + "id" : "Observation.category:VSCat.coding.version", + "path" : "Observation.category.coding.version", + "short" : "Version of the system - if relevant", + "definition" : "The version of the code system which was used when choosing this code. Note that a well-maintained code system does not need the version reported, because the meaning of codes is consistent across versions. However this cannot consistently be assured, and when the meaning is not guaranteed to be consistent, the version SHOULD be exchanged.", + "comment" : "Where the terminology does not clearly define what string should be used to identify code system versions, the recommendation is to use the date (expressed in FHIR date format) on which that version was officially published as the version date.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Coding.version", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "string" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : true, + "mapping" : [{ + "identity" : "v2", + "map" : "C*E.7" + }, + { + "identity" : "rim", + "map" : "./codeSystemVersion" + }, + { + "identity" : "orim", + "map" : "fhir:Coding.version rdfs:subPropertyOf dt:CDCoding.codeSystemVersion" + }] + }, + { + "id" : "Observation.category:VSCat.coding.code", + "path" : "Observation.category.coding.code", + "short" : "Symbol in syntax defined by the system", + "definition" : "A symbol in syntax defined by the system. The symbol may be a predefined code or an expression in a syntax defined by the coding system (e.g. post-coordination).", + "requirements" : "Need to refer to a particular code in the system.", + "min" : 1, + "max" : "1", + "base" : { + "path" : "Coding.code", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "code" + }], + "fixedCode" : "vital-signs", + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "mustSupport" : true, + "isModifier" : false, + "isSummary" : true, + "mapping" : [{ + "identity" : "v2", + "map" : "C*E.1" + }, + { + "identity" : "rim", + "map" : "./code" + }, + { + "identity" : "orim", + "map" : "fhir:Coding.code rdfs:subPropertyOf dt:CDCoding.code" + }] + }, + { + "id" : "Observation.category:VSCat.coding.display", + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-translatable", + "valueBoolean" : true + }], + "path" : "Observation.category.coding.display", + "short" : "Representation defined by the system", + "definition" : "A representation of the meaning of the code in the system, following the rules of the system.", + "requirements" : "Need to be able to carry a human-readable meaning of the code for readers that do not know the system.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Coding.display", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "string" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : true, + "mapping" : [{ + "identity" : "v2", + "map" : "C*E.2 - but note this is not well followed" + }, + { + "identity" : "rim", + "map" : "CV.displayName" + }, + { + "identity" : "orim", + "map" : "fhir:Coding.display rdfs:subPropertyOf dt:CDCoding.displayName" + }] + }, + { + "id" : "Observation.category:VSCat.coding.userSelected", + "path" : "Observation.category.coding.userSelected", + "short" : "If this coding was chosen directly by the user", + "definition" : "Indicates that this coding was chosen by a user directly - e.g. off a pick list of available items (codes or displays).", + "comment" : "Amongst a set of alternatives, a directly chosen code is the most appropriate starting point for new translations. There is some ambiguity about what exactly 'directly chosen' implies, and trading partner agreement may be needed to clarify the use of this element and its consequences more completely.", + "requirements" : "This has been identified as a clinical safety criterium - that this exact system/code pair was chosen explicitly, rather than inferred by the system based on some rules or language processing.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Coding.userSelected", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "boolean" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : true, + "mapping" : [{ + "identity" : "v2", + "map" : "Sometimes implied by being first" + }, + { + "identity" : "rim", + "map" : "CD.codingRationale" + }, + { + "identity" : "orim", + "map" : "fhir:Coding.userSelected fhir:mapsTo dt:CDCoding.codingRationale. fhir:Coding.userSelected fhir:hasMap fhir:Coding.userSelected.map. fhir:Coding.userSelected.map a fhir:Map; fhir:target dt:CDCoding.codingRationale. fhir:Coding.userSelected\\#true a [ fhir:source \"true\"; fhir:target dt:CDCoding.codingRationale\\#O ]" + }] + }, + { + "id" : "Observation.category:VSCat.text", + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-translatable", + "valueBoolean" : true + }], + "path" : "Observation.category.text", + "short" : "Plain text representation of the concept", + "definition" : "A human language representation of the concept as seen/selected/uttered by the user who entered the data and/or which represents the intended meaning of the user.", + "comment" : "Very often the text is the same as a displayName of one of the codings.", + "requirements" : "The codes from the terminologies do not always capture the correct meaning with all the nuances of the human using them, or sometimes there is no appropriate code at all. In these cases, the text is used to capture the full meaning of the source.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "CodeableConcept.text", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "string" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : true, + "mapping" : [{ + "identity" : "v2", + "map" : "C*E.9. But note many systems use C*E.2 for this" + }, + { + "identity" : "rim", + "map" : "./originalText[mediaType/code=\"text/plain\"]/data" + }, + { + "identity" : "orim", + "map" : "fhir:CodeableConcept.text rdfs:subPropertyOf dt:CD.originalText" + }] + }, + { + "id" : "Observation.code", + "path" : "Observation.code", + "short" : "Coded Responses from C-CDA Vital Sign Results", + "definition" : "Coded Responses from C-CDA Vital Sign Results.", + "comment" : "*All* code-value and, if present, component.code-component.value pairs need to be taken into account to correctly understand the meaning of the observation.", + "requirements" : "5. SHALL contain exactly one [1..1] code, where the @code SHOULD be selected from ValueSet HITSP Vital Sign Result Type 2.16.840.1.113883.3.88.12.80.62 DYNAMIC (CONF:7301).", + "alias" : ["Name"], + "min" : 1, + "max" : "1", + "base" : { + "path" : "Observation.code", + "min" : 1, + "max" : "1" + }, + "type" : [{ + "code" : "CodeableConcept" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "mustSupport" : true, + "isModifier" : false, + "isSummary" : true, + "binding" : { + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString" : "VitalSigns" + }], + "strength" : "extensible", + "description" : "This identifies the vital sign result type.", + "valueSet" : "http://hl7.org/fhir/ValueSet/observation-vitalsignresult" + }, + "mapping" : [{ + "identity" : "workflow", + "map" : "Event.code" + }, + { + "identity" : "w5", + "map" : "FiveWs.what[x]" + }, + { + "identity" : "sct-concept", + "map" : "< 363787002 |Observable entity| OR < 386053000 |Evaluation procedure|" + }, + { + "identity" : "v2", + "map" : "OBX-3" + }, + { + "identity" : "rim", + "map" : "code" + }, + { + "identity" : "sct-attr", + "map" : "116680003 |Is a|" + }] + }, + { + "id" : "Observation.subject", + "path" : "Observation.subject", + "short" : "Who and/or what the observation is about", + "definition" : "The patient, or group of patients, location, or device this observation is about and into whose record the observation is placed. If the actual focus of the observation is different from the subject (or a sample of, part, or region of the subject), the `focus` element or the `code` itself specifies the actual focus of the observation.", + "comment" : "One would expect this element to be a cardinality of 1..1. The only circumstance in which the subject can be missing is when the observation is made by a device that does not know the patient. In this case, the observation SHALL be matched to a patient through some context/channel matching technique, and at this point, the observation should be updated.", + "requirements" : "Observations have no value if you don't know who or what they're about.", + "min" : 1, + "max" : "1", + "base" : { + "path" : "Observation.subject", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "Reference", + "targetProfile" : ["http://hl7.org/fhir/StructureDefinition/Patient"] + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "mustSupport" : true, + "isModifier" : false, + "isSummary" : true, + "mapping" : [{ + "identity" : "workflow", + "map" : "Event.subject" + }, + { + "identity" : "w5", + "map" : "FiveWs.subject[x]" + }, + { + "identity" : "v2", + "map" : "PID-3" + }, + { + "identity" : "rim", + "map" : "participation[typeCode=RTGT] " + }, + { + "identity" : "w5", + "map" : "FiveWs.subject" + }] + }, + { + "id" : "Observation.focus", + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status", + "valueCode" : "trial-use" + }], + "path" : "Observation.focus", + "short" : "What the observation is about, when it is not about the subject of record", + "definition" : "The actual focus of an observation when it is not the patient of record representing something or someone associated with the patient such as a spouse, parent, fetus, or donor. For example, fetus observations in a mother's record. The focus of an observation could also be an existing condition, an intervention, the subject's diet, another observation of the subject, or a body structure such as tumor or implanted device. An example use case would be using the Observation resource to capture whether the mother is trained to change her child's tracheostomy tube. In this example, the child is the patient of record and the mother is the focus.", + "comment" : "Typically, an observation is made about the subject - a patient, or group of patients, location, or device - and the distinction between the subject and what is directly measured for an observation is specified in the observation code itself ( e.g., \"Blood Glucose\") and does not need to be represented separately using this element. Use `specimen` if a reference to a specimen is required. If a code is required instead of a resource use either `bodysite` for bodysites or the standard extension [focusCode](http://hl7.org/fhir/extension-observation-focuscode.html).", + "min" : 0, + "max" : "*", + "base" : { + "path" : "Observation.focus", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "Reference", + "targetProfile" : ["http://hl7.org/fhir/StructureDefinition/Resource"] + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : true, + "mapping" : [{ + "identity" : "w5", + "map" : "FiveWs.subject[x]" + }, + { + "identity" : "v2", + "map" : "OBX-3" + }, + { + "identity" : "rim", + "map" : "participation[typeCode=SBJ]" + }, + { + "identity" : "w5", + "map" : "FiveWs.subject" + }] + }, + { + "id" : "Observation.encounter", + "path" : "Observation.encounter", + "short" : "Healthcare event during which this observation is made", + "definition" : "The healthcare event (e.g. a patient and healthcare provider interaction) during which this observation is made.", + "comment" : "This will typically be the encounter the event occurred within, but some events may be initiated prior to or after the official completion of an encounter but still be tied to the context of the encounter (e.g. pre-admission laboratory tests).", + "requirements" : "For some observations it may be important to know the link between an observation and a particular encounter.", + "alias" : ["Context"], + "min" : 0, + "max" : "1", + "base" : { + "path" : "Observation.encounter", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "Reference", + "targetProfile" : ["http://hl7.org/fhir/StructureDefinition/Encounter"] + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : true, + "mapping" : [{ + "identity" : "workflow", + "map" : "Event.context" + }, + { + "identity" : "w5", + "map" : "FiveWs.context" + }, + { + "identity" : "v2", + "map" : "PV1" + }, + { + "identity" : "rim", + "map" : "inboundRelationship[typeCode=COMP].source[classCode=ENC, moodCode=EVN]" + }] + }, + { + "id" : "Observation.effective[x]", + "path" : "Observation.effective[x]", + "short" : "Often just a dateTime for Vital Signs", + "definition" : "Often just a dateTime for Vital Signs.", + "comment" : "At least a date should be present unless this observation is a historical report. For recording imprecise or \"fuzzy\" times (For example, a blood glucose measurement taken \"after breakfast\") use the [Timing](http://hl7.org/fhir/datatypes.html#timing) datatype which allow the measurement to be tied to regular life events.", + "requirements" : "Knowing when an observation was deemed true is important to its relevance as well as determining trends.", + "alias" : ["Occurrence"], + "min" : 1, + "max" : "1", + "base" : { + "path" : "Observation.effective[x]", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "dateTime" + }, + { + "code" : "Period" + }], + "condition" : ["vs-1"], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }, + { + "key" : "vs-1", + "severity" : "error", + "human" : "if Observation.effective[x] is dateTime and has a value then that value shall be precise to the day", + "expression" : "($this as dateTime).toString().length() >= 8", + "xpath" : "f:effectiveDateTime[matches(@value, '^\\d{4}-\\d{2}-\\d{2}')]" + }], + "mustSupport" : true, + "isModifier" : false, + "isSummary" : true, + "mapping" : [{ + "identity" : "workflow", + "map" : "Event.occurrence[x]" + }, + { + "identity" : "w5", + "map" : "FiveWs.done[x]" + }, + { + "identity" : "v2", + "map" : "OBX-14, and/or OBX-19 after v2.4 (depends on who observation made)" + }, + { + "identity" : "rim", + "map" : "effectiveTime" + }] + }, + { + "id" : "Observation.issued", + "path" : "Observation.issued", + "short" : "Date/Time this version was made available", + "definition" : "The date and time this version of the observation was made available to providers, typically after the results have been reviewed and verified.", + "comment" : "For Observations that don’t require review and verification, it may be the same as the [`lastUpdated` ](http://hl7.org/fhir/resource-definitions.html#Meta.lastUpdated) time of the resource itself. For Observations that do require review and verification for certain updates, it might not be the same as the `lastUpdated` time of the resource itself due to a non-clinically significant update that doesn’t require the new version to be reviewed and verified again.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Observation.issued", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "instant" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : true, + "mapping" : [{ + "identity" : "w5", + "map" : "FiveWs.recorded" + }, + { + "identity" : "v2", + "map" : "OBR.22 (or MSH.7), or perhaps OBX-19 (depends on who observation made)" + }, + { + "identity" : "rim", + "map" : "participation[typeCode=AUT].time" + }] + }, + { + "id" : "Observation.performer", + "path" : "Observation.performer", + "short" : "Who is responsible for the observation", + "definition" : "Who was responsible for asserting the observed value as \"true\".", + "requirements" : "May give a degree of confidence in the observation and also indicates where follow-up questions should be directed.", + "min" : 0, + "max" : "*", + "base" : { + "path" : "Observation.performer", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "Reference", + "targetProfile" : ["http://hl7.org/fhir/StructureDefinition/Practitioner", + "http://hl7.org/fhir/StructureDefinition/PractitionerRole", + "http://hl7.org/fhir/StructureDefinition/Organization", + "http://hl7.org/fhir/StructureDefinition/CareTeam", + "http://hl7.org/fhir/StructureDefinition/Patient", + "http://hl7.org/fhir/StructureDefinition/RelatedPerson"] + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : true, + "mapping" : [{ + "identity" : "workflow", + "map" : "Event.performer.actor" + }, + { + "identity" : "w5", + "map" : "FiveWs.actor" + }, + { + "identity" : "v2", + "map" : "OBX.15 / (Practitioner) OBX-16, PRT-5:PRT-4='RO' / (Device) OBX-18 , PRT-10:PRT-4='EQUIP' / (Organization) OBX-23, PRT-8:PRT-4='PO'" + }, + { + "identity" : "rim", + "map" : "participation[typeCode=PRF]" + }] + }, + { + "id" : "Observation.value[x]", + "path" : "Observation.value[x]", + "short" : "Vital Signs value are recorded using the Quantity data type. For supporting observations such as Cuff size could use other datatypes such as CodeableConcept.", + "definition" : "Vital Signs value are recorded using the Quantity data type. For supporting observations such as Cuff size could use other datatypes such as CodeableConcept.", + "comment" : "An observation may have; 1) a single value here, 2) both a value and a set of related or component values, or 3) only a set of related or component values. If a value is present, the datatype for this element should be determined by Observation.code. A CodeableConcept with just a text would be used instead of a string if the field was usually coded, or if the type associated with the Observation.code defines a coded value. For additional guidance, see the [Notes section](http://hl7.org/fhir/observation.html#notes) below.", + "requirements" : "9. SHALL contain exactly one [1..1] value with @xsi:type=\"PQ\" (CONF:7305).", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Observation.value[x]", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "Quantity" + }, + { + "code" : "CodeableConcept" + }, + { + "code" : "string" + }, + { + "code" : "boolean" + }, + { + "code" : "integer" + }, + { + "code" : "Range" + }, + { + "code" : "Ratio" + }, + { + "code" : "SampledData" + }, + { + "code" : "time" + }, + { + "code" : "dateTime" + }, + { + "code" : "Period" + }], + "condition" : ["obs-7", + "vs-2"], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "mustSupport" : true, + "isModifier" : false, + "isSummary" : true, + "mapping" : [{ + "identity" : "sct-concept", + "map" : "< 441742003 |Evaluation finding|" + }, + { + "identity" : "v2", + "map" : "OBX.2, OBX.5, OBX.6" + }, + { + "identity" : "rim", + "map" : "value" + }, + { + "identity" : "sct-attr", + "map" : "363714003 |Interprets|" + }] + }, + { + "id" : "Observation.dataAbsentReason", + "path" : "Observation.dataAbsentReason", + "short" : "Why the result is missing", + "definition" : "Provides a reason why the expected value in the element Observation.value[x] is missing.", + "comment" : "Null or exceptional values can be represented two ways in FHIR Observations. One way is to simply include them in the value set and represent the exceptions in the value. For example, measurement values for a serology test could be \"detected\", \"not detected\", \"inconclusive\", or \"specimen unsatisfactory\". \n\nThe alternate way is to use the value element for actual observations and use the explicit dataAbsentReason element to record exceptional values. For example, the dataAbsentReason code \"error\" could be used when the measurement was not completed. Note that an observation may only be reported if there are values to report. For example differential cell counts values may be reported only when > 0. Because of these options, use-case agreements are required to interpret general observations for null or exceptional values.", + "requirements" : "For many results it is necessary to handle exceptional values in measurements.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Observation.dataAbsentReason", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "CodeableConcept" + }], + "condition" : ["obs-6", + "vs-2"], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "mustSupport" : true, + "isModifier" : false, + "isSummary" : false, + "binding" : { + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString" : "ObservationValueAbsentReason" + }], + "strength" : "extensible", + "description" : "Codes specifying why the result (`Observation.value[x]`) is missing.", + "valueSet" : "http://hl7.org/fhir/ValueSet/data-absent-reason" + }, + "mapping" : [{ + "identity" : "v2", + "map" : "N/A" + }, + { + "identity" : "rim", + "map" : "value.nullFlavor" + }] + }, + { + "id" : "Observation.interpretation", + "path" : "Observation.interpretation", + "short" : "High, low, normal, etc.", + "definition" : "A categorical assessment of an observation value. For example, high, low, normal.", + "comment" : "Historically used for laboratory results (known as 'abnormal flag' ), its use extends to other use cases where coded interpretations are relevant. Often reported as one or more simple compact codes this element is often placed adjacent to the result value in reports and flow sheets to signal the meaning/normalcy status of the result.", + "requirements" : "For some results, particularly numeric results, an interpretation is necessary to fully understand the significance of a result.", + "alias" : ["Abnormal Flag"], + "min" : 0, + "max" : "*", + "base" : { + "path" : "Observation.interpretation", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "CodeableConcept" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : false, + "binding" : { + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString" : "ObservationInterpretation" + }], + "strength" : "extensible", + "description" : "Codes identifying interpretations of observations.", + "valueSet" : "http://hl7.org/fhir/ValueSet/observation-interpretation" + }, + "mapping" : [{ + "identity" : "sct-concept", + "map" : "< 260245000 |Findings values|" + }, + { + "identity" : "v2", + "map" : "OBX-8" + }, + { + "identity" : "rim", + "map" : "interpretationCode" + }, + { + "identity" : "sct-attr", + "map" : "363713009 |Has interpretation|" + }] + }, + { + "id" : "Observation.note", + "path" : "Observation.note", + "short" : "Comments about the observation", + "definition" : "Comments about the observation or the results.", + "comment" : "May include general statements about the observation, or statements about significant, unexpected or unreliable results values, or information about its source when relevant to its interpretation.", + "requirements" : "Need to be able to provide free text additional information.", + "min" : 0, + "max" : "*", + "base" : { + "path" : "Observation.note", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "Annotation" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "v2", + "map" : "NTE.3 (partner NTE to OBX, or sometimes another (child?) OBX)" + }, + { + "identity" : "rim", + "map" : "subjectOf.observationEvent[code=\"annotation\"].value" + }] + }, + { + "id" : "Observation.bodySite", + "path" : "Observation.bodySite", + "short" : "Observed body part", + "definition" : "Indicates the site on the subject's body where the observation was made (i.e. the target site).", + "comment" : "Only used if not implicit in code found in Observation.code. In many systems, this may be represented as a related observation instead of an inline component. \n\nIf the use case requires BodySite to be handled as a separate resource (e.g. to identify and track separately) then use the standard extension[ bodySite](http://hl7.org/fhir/extension-bodysite.html).", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Observation.bodySite", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "CodeableConcept" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : false, + "binding" : { + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString" : "BodySite" + }], + "strength" : "example", + "description" : "Codes describing anatomical locations. May include laterality.", + "valueSet" : "http://hl7.org/fhir/ValueSet/body-site" + }, + "mapping" : [{ + "identity" : "sct-concept", + "map" : "< 123037004 |Body structure|" + }, + { + "identity" : "v2", + "map" : "OBX-20" + }, + { + "identity" : "rim", + "map" : "targetSiteCode" + }, + { + "identity" : "sct-attr", + "map" : "718497002 |Inherent location|" + }] + }, + { + "id" : "Observation.method", + "path" : "Observation.method", + "short" : "How it was done", + "definition" : "Indicates the mechanism used to perform the observation.", + "comment" : "Only used if not implicit in code for Observation.code.", + "requirements" : "In some cases, method can impact results and is thus used for determining whether results can be compared or determining significance of results.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Observation.method", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "CodeableConcept" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : false, + "binding" : { + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString" : "ObservationMethod" + }], + "strength" : "example", + "description" : "Methods for simple observations.", + "valueSet" : "http://hl7.org/fhir/ValueSet/observation-methods" + }, + "mapping" : [{ + "identity" : "v2", + "map" : "OBX-17" + }, + { + "identity" : "rim", + "map" : "methodCode" + }] + }, + { + "id" : "Observation.specimen", + "path" : "Observation.specimen", + "short" : "Specimen used for this observation", + "definition" : "The specimen that was used when this observation was made.", + "comment" : "Should only be used if not implicit in code found in `Observation.code`. Observations are not made on specimens themselves; they are made on a subject, but in many cases by the means of a specimen. Note that although specimens are often involved, they are not always tracked and reported explicitly. Also note that observation resources may be used in contexts that track the specimen explicitly (e.g. Diagnostic Report).", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Observation.specimen", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "Reference", + "targetProfile" : ["http://hl7.org/fhir/StructureDefinition/Specimen"] + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "sct-concept", + "map" : "< 123038009 |Specimen|" + }, + { + "identity" : "v2", + "map" : "SPM segment" + }, + { + "identity" : "rim", + "map" : "participation[typeCode=SPC].specimen" + }, + { + "identity" : "sct-attr", + "map" : "704319004 |Inherent in|" + }] + }, + { + "id" : "Observation.device", + "path" : "Observation.device", + "short" : "(Measurement) Device", + "definition" : "The device used to generate the observation data.", + "comment" : "Note that this is not meant to represent a device involved in the transmission of the result, e.g., a gateway. Such devices may be documented using the Provenance resource where relevant.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Observation.device", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "Reference", + "targetProfile" : ["http://hl7.org/fhir/StructureDefinition/Device", + "http://hl7.org/fhir/StructureDefinition/DeviceMetric"] + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "sct-concept", + "map" : "< 49062001 |Device|" + }, + { + "identity" : "v2", + "map" : "OBX-17 / PRT -10" + }, + { + "identity" : "rim", + "map" : "participation[typeCode=DEV]" + }, + { + "identity" : "sct-attr", + "map" : "424226004 |Using device|" + }] + }, + { + "id" : "Observation.referenceRange", + "path" : "Observation.referenceRange", + "short" : "Provides guide for interpretation", + "definition" : "Guidance on how to interpret the value by comparison to a normal or recommended range. Multiple reference ranges are interpreted as an \"OR\". In other words, to represent two distinct target populations, two `referenceRange` elements would be used.", + "comment" : "Most observations only have one generic reference range. Systems MAY choose to restrict to only supplying the relevant reference range based on knowledge about the patient (e.g., specific to the patient's age, gender, weight and other factors), but this might not be possible or appropriate. Whenever more than one reference range is supplied, the differences between them SHOULD be provided in the reference range and/or age properties.", + "requirements" : "Knowing what values are considered \"normal\" can help evaluate the significance of a particular result. Need to be able to provide multiple reference ranges for different contexts.", + "min" : 0, + "max" : "*", + "base" : { + "path" : "Observation.referenceRange", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "BackboneElement" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }, + { + "key" : "obs-3", + "severity" : "error", + "human" : "Must have at least a low or a high or text", + "expression" : "low.exists() or high.exists() or text.exists()", + "xpath" : "(exists(f:low) or exists(f:high)or exists(f:text))" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "v2", + "map" : "OBX.7" + }, + { + "identity" : "rim", + "map" : "outboundRelationship[typeCode=REFV]/target[classCode=OBS, moodCode=EVN]" + }] + }, + { + "id" : "Observation.referenceRange.id", + "path" : "Observation.referenceRange.id", + "representation" : ["xmlAttr"], + "short" : "Unique id for inter-element referencing", + "definition" : "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Element.id", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/structuredefinition-fhir-type", + "valueUrl" : "string" + }], + "code" : "http://hl7.org/fhirpath/System.String" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "rim", + "map" : "n/a" + }] + }, + { + "id" : "Observation.referenceRange.extension", + "path" : "Observation.referenceRange.extension", + "short" : "Additional content defined by implementations", + "definition" : "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension.", + "comment" : "There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.", + "alias" : ["extensions", + "user content"], + "min" : 0, + "max" : "*", + "base" : { + "path" : "Element.extension", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "Extension" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }, + { + "key" : "ext-1", + "severity" : "error", + "human" : "Must have either extensions or value[x], not both", + "expression" : "extension.exists() != value.exists()", + "xpath" : "exists(f:extension)!=exists(f:*[starts-with(local-name(.), \"value\")])", + "source" : "http://hl7.org/fhir/StructureDefinition/Extension" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "rim", + "map" : "n/a" + }] + }, + { + "id" : "Observation.referenceRange.modifierExtension", + "path" : "Observation.referenceRange.modifierExtension", + "short" : "Extensions that cannot be ignored even if unrecognized", + "definition" : "May be used to represent additional information that is not part of the basic definition of the element and that modifies the understanding of the element in which it is contained and/or the understanding of the containing element's descendants. Usually modifier elements provide negation or qualification. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. Applications processing a resource are required to check for modifier extensions.\n\nModifier extensions SHALL NOT change the meaning of any elements on Resource or DomainResource (including cannot change the meaning of modifierExtension itself).", + "comment" : "There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.", + "requirements" : "Modifier extensions allow for extensions that *cannot* be safely ignored to be clearly distinguished from the vast majority of extensions which can be safely ignored. This promotes interoperability by eliminating the need for implementers to prohibit the presence of extensions. For further information, see the [definition of modifier extensions](http://hl7.org/fhir/extensibility.html#modifierExtension).", + "alias" : ["extensions", + "user content", + "modifiers"], + "min" : 0, + "max" : "*", + "base" : { + "path" : "BackboneElement.modifierExtension", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "Extension" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }, + { + "key" : "ext-1", + "severity" : "error", + "human" : "Must have either extensions or value[x], not both", + "expression" : "extension.exists() != value.exists()", + "xpath" : "exists(f:extension)!=exists(f:*[starts-with(local-name(.), \"value\")])", + "source" : "http://hl7.org/fhir/StructureDefinition/Extension" + }], + "isModifier" : true, + "isModifierReason" : "Modifier extensions are expected to modify the meaning or interpretation of the element that contains them", + "isSummary" : true, + "mapping" : [{ + "identity" : "rim", + "map" : "N/A" + }] + }, + { + "id" : "Observation.referenceRange.low", + "path" : "Observation.referenceRange.low", + "short" : "Low Range, if relevant", + "definition" : "The value of the low bound of the reference range. The low bound of the reference range endpoint is inclusive of the value (e.g. reference range is >=5 - <=9). If the low bound is omitted, it is assumed to be meaningless (e.g. reference range is <=2.3).", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Observation.referenceRange.low", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "Quantity", + "profile" : ["http://hl7.org/fhir/StructureDefinition/SimpleQuantity"] + }], + "condition" : ["obs-3"], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "v2", + "map" : "OBX-7" + }, + { + "identity" : "rim", + "map" : "value:IVL_PQ.low" + }] + }, + { + "id" : "Observation.referenceRange.high", + "path" : "Observation.referenceRange.high", + "short" : "High Range, if relevant", + "definition" : "The value of the high bound of the reference range. The high bound of the reference range endpoint is inclusive of the value (e.g. reference range is >=5 - <=9). If the high bound is omitted, it is assumed to be meaningless (e.g. reference range is >= 2.3).", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Observation.referenceRange.high", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "Quantity", + "profile" : ["http://hl7.org/fhir/StructureDefinition/SimpleQuantity"] + }], + "condition" : ["obs-3"], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "v2", + "map" : "OBX-7" + }, + { + "identity" : "rim", + "map" : "value:IVL_PQ.high" + }] + }, + { + "id" : "Observation.referenceRange.type", + "path" : "Observation.referenceRange.type", + "short" : "Reference range qualifier", + "definition" : "Codes to indicate the what part of the targeted reference population it applies to. For example, the normal or therapeutic range.", + "comment" : "This SHOULD be populated if there is more than one range. If this element is not present then the normal range is assumed.", + "requirements" : "Need to be able to say what kind of reference range this is - normal, recommended, therapeutic, etc., - for proper interpretation.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Observation.referenceRange.type", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "CodeableConcept" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : false, + "binding" : { + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString" : "ObservationRangeMeaning" + }], + "strength" : "preferred", + "description" : "Code for the meaning of a reference range.", + "valueSet" : "http://hl7.org/fhir/ValueSet/referencerange-meaning" + }, + "mapping" : [{ + "identity" : "sct-concept", + "map" : "< 260245000 |Findings values| OR \r< 365860008 |General clinical state finding| \rOR \r< 250171008 |Clinical history or observation findings| OR \r< 415229000 |Racial group| OR \r< 365400002 |Finding of puberty stage| OR\r< 443938003 |Procedure carried out on subject|" + }, + { + "identity" : "v2", + "map" : "OBX-10" + }, + { + "identity" : "rim", + "map" : "interpretationCode" + }] + }, + { + "id" : "Observation.referenceRange.appliesTo", + "path" : "Observation.referenceRange.appliesTo", + "short" : "Reference range population", + "definition" : "Codes to indicate the target population this reference range applies to. For example, a reference range may be based on the normal population or a particular sex or race. Multiple `appliesTo` are interpreted as an \"AND\" of the target populations. For example, to represent a target population of African American females, both a code of female and a code for African American would be used.", + "comment" : "This SHOULD be populated if there is more than one range. If this element is not present then the normal population is assumed.", + "requirements" : "Need to be able to identify the target population for proper interpretation.", + "min" : 0, + "max" : "*", + "base" : { + "path" : "Observation.referenceRange.appliesTo", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "CodeableConcept" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : false, + "binding" : { + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString" : "ObservationRangeType" + }], + "strength" : "example", + "description" : "Codes identifying the population the reference range applies to.", + "valueSet" : "http://hl7.org/fhir/ValueSet/referencerange-appliesto" + }, + "mapping" : [{ + "identity" : "sct-concept", + "map" : "< 260245000 |Findings values| OR \r< 365860008 |General clinical state finding| \rOR \r< 250171008 |Clinical history or observation findings| OR \r< 415229000 |Racial group| OR \r< 365400002 |Finding of puberty stage| OR\r< 443938003 |Procedure carried out on subject|" + }, + { + "identity" : "v2", + "map" : "OBX-10" + }, + { + "identity" : "rim", + "map" : "interpretationCode" + }] + }, + { + "id" : "Observation.referenceRange.age", + "path" : "Observation.referenceRange.age", + "short" : "Applicable age range, if relevant", + "definition" : "The age at which this reference range is applicable. This is a neonatal age (e.g. number of weeks at term) if the meaning says so.", + "requirements" : "Some analytes vary greatly over age.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Observation.referenceRange.age", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "Range" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "rim", + "map" : "outboundRelationship[typeCode=PRCN].targetObservationCriterion[code=\"age\"].value" + }] + }, + { + "id" : "Observation.referenceRange.text", + "path" : "Observation.referenceRange.text", + "short" : "Text based reference range in an observation", + "definition" : "Text based reference range in an observation which may be used when a quantitative range is not appropriate for an observation. An example would be a reference value of \"Negative\" or a list or table of \"normals\".", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Observation.referenceRange.text", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "string" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "v2", + "map" : "OBX-7" + }, + { + "identity" : "rim", + "map" : "value:ST" + }] + }, + { + "id" : "Observation.hasMember", + "path" : "Observation.hasMember", + "short" : "Used when reporting vital signs panel components", + "definition" : "Used when reporting vital signs panel components.", + "comment" : "When using this element, an observation will typically have either a value or a set of related resources, although both may be present in some cases. For a discussion on the ways Observations can assembled in groups together, see [Notes](http://hl7.org/fhir/observation.html#obsgrouping) below. Note that a system may calculate results from [QuestionnaireResponse](http://hl7.org/fhir/questionnaireresponse.html) into a final score and represent the score as an Observation.", + "min" : 0, + "max" : "*", + "base" : { + "path" : "Observation.hasMember", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "Reference", + "targetProfile" : ["http://hl7.org/fhir/StructureDefinition/QuestionnaireResponse", + "http://hl7.org/fhir/StructureDefinition/MolecularSequence", + "http://hl7.org/fhir/StructureDefinition/vitalsigns"] + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : true, + "mapping" : [{ + "identity" : "v2", + "map" : "Relationships established by OBX-4 usage" + }, + { + "identity" : "rim", + "map" : "outBoundRelationship" + }] + }, + { + "id" : "Observation.derivedFrom", + "path" : "Observation.derivedFrom", + "short" : "Related measurements the observation is made from", + "definition" : "The target resource that represents a measurement from which this observation value is derived. For example, a calculated anion gap or a fetal measurement based on an ultrasound image.", + "comment" : "All the reference choices that are listed in this element can represent clinical observations and other measurements that may be the source for a derived value. The most common reference will be another Observation. For a discussion on the ways Observations can assembled in groups together, see [Notes](http://hl7.org/fhir/observation.html#obsgrouping) below.", + "min" : 0, + "max" : "*", + "base" : { + "path" : "Observation.derivedFrom", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "Reference", + "targetProfile" : ["http://hl7.org/fhir/StructureDefinition/DocumentReference", + "http://hl7.org/fhir/StructureDefinition/ImagingStudy", + "http://hl7.org/fhir/StructureDefinition/Media", + "http://hl7.org/fhir/StructureDefinition/QuestionnaireResponse", + "http://hl7.org/fhir/StructureDefinition/MolecularSequence", + "http://hl7.org/fhir/StructureDefinition/vitalsigns"] + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : true, + "mapping" : [{ + "identity" : "v2", + "map" : "Relationships established by OBX-4 usage" + }, + { + "identity" : "rim", + "map" : ".targetObservation" + }] + }, + { + "id" : "Observation.component", + "path" : "Observation.component", + "short" : "Used when reporting systolic and diastolic blood pressure.", + "definition" : "Used when reporting systolic and diastolic blood pressure.", + "comment" : "For a discussion on the ways Observations can be assembled in groups together see [Notes](http://hl7.org/fhir/observation.html#notes) below.", + "requirements" : "Component observations share the same attributes in the Observation resource as the primary observation and are always treated a part of a single observation (they are not separable). However, the reference range for the primary observation value is not inherited by the component values and is required when appropriate for each component observation.", + "min" : 0, + "max" : "*", + "base" : { + "path" : "Observation.component", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "BackboneElement" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }, + { + "key" : "vs-3", + "severity" : "error", + "human" : "If there is no a value a data absent reason must be present", + "expression" : "value.exists() or dataAbsentReason.exists()", + "xpath" : "f:*[starts-with(local-name(.), 'value')] or f:dataAbsentReason" + }], + "mustSupport" : true, + "isModifier" : false, + "isSummary" : true, + "mapping" : [{ + "identity" : "v2", + "map" : "containment by OBX-4?" + }, + { + "identity" : "rim", + "map" : "outBoundRelationship[typeCode=COMP]" + }] + }, + { + "id" : "Observation.component.id", + "path" : "Observation.component.id", + "representation" : ["xmlAttr"], + "short" : "Unique id for inter-element referencing", + "definition" : "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Element.id", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/structuredefinition-fhir-type", + "valueUrl" : "string" + }], + "code" : "http://hl7.org/fhirpath/System.String" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "rim", + "map" : "n/a" + }] + }, + { + "id" : "Observation.component.extension", + "path" : "Observation.component.extension", + "short" : "Additional content defined by implementations", + "definition" : "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension.", + "comment" : "There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.", + "alias" : ["extensions", + "user content"], + "min" : 0, + "max" : "*", + "base" : { + "path" : "Element.extension", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "Extension" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }, + { + "key" : "ext-1", + "severity" : "error", + "human" : "Must have either extensions or value[x], not both", + "expression" : "extension.exists() != value.exists()", + "xpath" : "exists(f:extension)!=exists(f:*[starts-with(local-name(.), \"value\")])", + "source" : "http://hl7.org/fhir/StructureDefinition/Extension" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "rim", + "map" : "n/a" + }] + }, + { + "id" : "Observation.component.modifierExtension", + "path" : "Observation.component.modifierExtension", + "short" : "Extensions that cannot be ignored even if unrecognized", + "definition" : "May be used to represent additional information that is not part of the basic definition of the element and that modifies the understanding of the element in which it is contained and/or the understanding of the containing element's descendants. Usually modifier elements provide negation or qualification. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. Applications processing a resource are required to check for modifier extensions.\n\nModifier extensions SHALL NOT change the meaning of any elements on Resource or DomainResource (including cannot change the meaning of modifierExtension itself).", + "comment" : "There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.", + "requirements" : "Modifier extensions allow for extensions that *cannot* be safely ignored to be clearly distinguished from the vast majority of extensions which can be safely ignored. This promotes interoperability by eliminating the need for implementers to prohibit the presence of extensions. For further information, see the [definition of modifier extensions](http://hl7.org/fhir/extensibility.html#modifierExtension).", + "alias" : ["extensions", + "user content", + "modifiers"], + "min" : 0, + "max" : "*", + "base" : { + "path" : "BackboneElement.modifierExtension", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "Extension" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }, + { + "key" : "ext-1", + "severity" : "error", + "human" : "Must have either extensions or value[x], not both", + "expression" : "extension.exists() != value.exists()", + "xpath" : "exists(f:extension)!=exists(f:*[starts-with(local-name(.), \"value\")])", + "source" : "http://hl7.org/fhir/StructureDefinition/Extension" + }], + "isModifier" : true, + "isModifierReason" : "Modifier extensions are expected to modify the meaning or interpretation of the element that contains them", + "isSummary" : true, + "mapping" : [{ + "identity" : "rim", + "map" : "N/A" + }] + }, + { + "id" : "Observation.component.code", + "path" : "Observation.component.code", + "short" : "Type of component observation (code / type)", + "definition" : "Describes what was observed. Sometimes this is called the observation \"code\".", + "comment" : "*All* code-value and component.code-component.value pairs need to be taken into account to correctly understand the meaning of the observation.", + "requirements" : "Knowing what kind of observation is being made is essential to understanding the observation.", + "min" : 1, + "max" : "1", + "base" : { + "path" : "Observation.component.code", + "min" : 1, + "max" : "1" + }, + "type" : [{ + "code" : "CodeableConcept" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "mustSupport" : true, + "isModifier" : false, + "isSummary" : true, + "binding" : { + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString" : "VitalSigns" + }], + "strength" : "extensible", + "description" : "This identifies the vital sign result type.", + "valueSet" : "http://hl7.org/fhir/ValueSet/observation-vitalsignresult" + }, + "mapping" : [{ + "identity" : "w5", + "map" : "FiveWs.what[x]" + }, + { + "identity" : "sct-concept", + "map" : "< 363787002 |Observable entity| OR \r< 386053000 |Evaluation procedure|" + }, + { + "identity" : "v2", + "map" : "OBX-3" + }, + { + "identity" : "rim", + "map" : "code" + }] + }, + { + "id" : "Observation.component.value[x]", + "path" : "Observation.component.value[x]", + "short" : "Vital Sign Value recorded with UCUM", + "definition" : "Vital Sign Value recorded with UCUM.", + "comment" : "Used when observation has a set of component observations. An observation may have both a value (e.g. an Apgar score) and component observations (the observations from which the Apgar score was derived). If a value is present, the datatype for this element should be determined by Observation.code. A CodeableConcept with just a text would be used instead of a string if the field was usually coded, or if the type associated with the Observation.code defines a coded value. For additional guidance, see the [Notes section](http://hl7.org/fhir/observation.html#notes) below.", + "requirements" : "9. SHALL contain exactly one [1..1] value with @xsi:type=\"PQ\" (CONF:7305).", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Observation.component.value[x]", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "Quantity" + }, + { + "code" : "CodeableConcept" + }, + { + "code" : "string" + }, + { + "code" : "boolean" + }, + { + "code" : "integer" + }, + { + "code" : "Range" + }, + { + "code" : "Ratio" + }, + { + "code" : "SampledData" + }, + { + "code" : "time" + }, + { + "code" : "dateTime" + }, + { + "code" : "Period" + }], + "condition" : ["vs-3"], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "mustSupport" : true, + "isModifier" : false, + "isSummary" : true, + "binding" : { + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString" : "VitalSignsUnits" + }], + "strength" : "required", + "description" : "Common UCUM units for recording Vital Signs.", + "valueSet" : "http://hl7.org/fhir/ValueSet/ucum-vitals-common|4.0.1" + }, + "mapping" : [{ + "identity" : "sct-concept", + "map" : "363714003 |Interprets| < 441742003 |Evaluation finding|" + }, + { + "identity" : "v2", + "map" : "OBX.2, OBX.5, OBX.6" + }, + { + "identity" : "rim", + "map" : "value" + }, + { + "identity" : "sct-attr", + "map" : "363714003 |Interprets|" + }] + }, + { + "id" : "Observation.component.dataAbsentReason", + "path" : "Observation.component.dataAbsentReason", + "short" : "Why the component result is missing", + "definition" : "Provides a reason why the expected value in the element Observation.component.value[x] is missing.", + "comment" : "\"Null\" or exceptional values can be represented two ways in FHIR Observations. One way is to simply include them in the value set and represent the exceptions in the value. For example, measurement values for a serology test could be \"detected\", \"not detected\", \"inconclusive\", or \"test not done\". \n\nThe alternate way is to use the value element for actual observations and use the explicit dataAbsentReason element to record exceptional values. For example, the dataAbsentReason code \"error\" could be used when the measurement was not completed. Because of these options, use-case agreements are required to interpret general observations for exceptional values.", + "requirements" : "For many results it is necessary to handle exceptional values in measurements.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Observation.component.dataAbsentReason", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "CodeableConcept" + }], + "condition" : ["obs-6", + "vs-3"], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "mustSupport" : true, + "isModifier" : false, + "isSummary" : false, + "binding" : { + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString" : "ObservationValueAbsentReason" + }], + "strength" : "extensible", + "description" : "Codes specifying why the result (`Observation.value[x]`) is missing.", + "valueSet" : "http://hl7.org/fhir/ValueSet/data-absent-reason" + }, + "mapping" : [{ + "identity" : "v2", + "map" : "N/A" + }, + { + "identity" : "rim", + "map" : "value.nullFlavor" + }] + }, + { + "id" : "Observation.component.interpretation", + "path" : "Observation.component.interpretation", + "short" : "High, low, normal, etc.", + "definition" : "A categorical assessment of an observation value. For example, high, low, normal.", + "comment" : "Historically used for laboratory results (known as 'abnormal flag' ), its use extends to other use cases where coded interpretations are relevant. Often reported as one or more simple compact codes this element is often placed adjacent to the result value in reports and flow sheets to signal the meaning/normalcy status of the result.", + "requirements" : "For some results, particularly numeric results, an interpretation is necessary to fully understand the significance of a result.", + "alias" : ["Abnormal Flag"], + "min" : 0, + "max" : "*", + "base" : { + "path" : "Observation.component.interpretation", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "CodeableConcept" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : false, + "binding" : { + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString" : "ObservationInterpretation" + }], + "strength" : "extensible", + "description" : "Codes identifying interpretations of observations.", + "valueSet" : "http://hl7.org/fhir/ValueSet/observation-interpretation" + }, + "mapping" : [{ + "identity" : "sct-concept", + "map" : "< 260245000 |Findings values|" + }, + { + "identity" : "v2", + "map" : "OBX-8" + }, + { + "identity" : "rim", + "map" : "interpretationCode" + }, + { + "identity" : "sct-attr", + "map" : "363713009 |Has interpretation|" + }] + }, + { + "id" : "Observation.component.referenceRange", + "path" : "Observation.component.referenceRange", + "short" : "Provides guide for interpretation of component result", + "definition" : "Guidance on how to interpret the value by comparison to a normal or recommended range.", + "comment" : "Most observations only have one generic reference range. Systems MAY choose to restrict to only supplying the relevant reference range based on knowledge about the patient (e.g., specific to the patient's age, gender, weight and other factors), but this might not be possible or appropriate. Whenever more than one reference range is supplied, the differences between them SHOULD be provided in the reference range and/or age properties.", + "requirements" : "Knowing what values are considered \"normal\" can help evaluate the significance of a particular result. Need to be able to provide multiple reference ranges for different contexts.", + "min" : 0, + "max" : "*", + "base" : { + "path" : "Observation.component.referenceRange", + "min" : 0, + "max" : "*" + }, + "contentReference" : "#Observation.referenceRange", + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "v2", + "map" : "OBX.7" + }, + { + "identity" : "rim", + "map" : "outboundRelationship[typeCode=REFV]/target[classCode=OBS, moodCode=EVN]" + }] + }] + }, + "differential" : { + "element" : [{ + "id" : "Observation", + "path" : "Observation", + "short" : "FHIR Vital Signs Profile", + "definition" : "The FHIR Vitals Signs profile sets minimum expectations for the Observation Resource to record, search and fetch the vital signs associated with a patient.", + "alias" : ["Vital Signs", + "Measurement", + "Results", + "Tests"], + "min" : 0, + "max" : "*", + "constraint" : [{ + "key" : "vs-2", + "severity" : "error", + "human" : "If there is no component or hasMember element then either a value[x] or a data absent reason must be present.", + "expression" : "(component.empty() and hasMember.empty()) implies (dataAbsentReason.exists() or value.exists())", + "xpath" : "f:component or f:memberOF or f:*[starts-with(local-name(.), 'value')] or f:dataAbsentReason" + }] + }, + { + "id" : "Observation.status", + "path" : "Observation.status", + "min" : 1, + "max" : "1", + "type" : [{ + "code" : "code" + }], + "mustSupport" : true, + "binding" : { + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString" : "Status" + }], + "strength" : "required", + "valueSet" : "http://hl7.org/fhir/ValueSet/observation-status|4.0.1" + } + }, + { + "id" : "Observation.category", + "path" : "Observation.category", + "slicing" : { + "discriminator" : [{ + "type" : "value", + "path" : "coding.code" + }, + { + "type" : "value", + "path" : "coding.system" + }], + "ordered" : false, + "rules" : "open" + }, + "min" : 1, + "max" : "*", + "type" : [{ + "code" : "CodeableConcept" + }], + "mustSupport" : true + }, + { + "id" : "Observation.category:VSCat", + "path" : "Observation.category", + "sliceName" : "VSCat", + "min" : 1, + "max" : "1", + "type" : [{ + "code" : "CodeableConcept" + }], + "mustSupport" : true + }, + { + "id" : "Observation.category:VSCat.coding", + "path" : "Observation.category.coding", + "min" : 1, + "max" : "*", + "type" : [{ + "code" : "Coding" + }], + "mustSupport" : true + }, + { + "id" : "Observation.category:VSCat.coding.system", + "path" : "Observation.category.coding.system", + "min" : 1, + "max" : "1", + "type" : [{ + "code" : "uri" + }], + "fixedUri" : "http://terminology.hl7.org/CodeSystem/observation-category", + "mustSupport" : true + }, + { + "id" : "Observation.category:VSCat.coding.code", + "path" : "Observation.category.coding.code", + "min" : 1, + "max" : "1", + "type" : [{ + "code" : "code" + }], + "fixedCode" : "vital-signs", + "mustSupport" : true + }, + { + "id" : "Observation.code", + "path" : "Observation.code", + "short" : "Coded Responses from C-CDA Vital Sign Results", + "definition" : "Coded Responses from C-CDA Vital Sign Results.", + "requirements" : "5. SHALL contain exactly one [1..1] code, where the @code SHOULD be selected from ValueSet HITSP Vital Sign Result Type 2.16.840.1.113883.3.88.12.80.62 DYNAMIC (CONF:7301).", + "min" : 1, + "max" : "1", + "type" : [{ + "code" : "CodeableConcept" + }], + "mustSupport" : true, + "binding" : { + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString" : "VitalSigns" + }], + "strength" : "extensible", + "description" : "This identifies the vital sign result type.", + "valueSet" : "http://hl7.org/fhir/ValueSet/observation-vitalsignresult" + } + }, + { + "id" : "Observation.subject", + "path" : "Observation.subject", + "min" : 1, + "max" : "1", + "type" : [{ + "code" : "Reference", + "targetProfile" : ["http://hl7.org/fhir/StructureDefinition/Patient"] + }], + "mustSupport" : true + }, + { + "id" : "Observation.effective[x]", + "path" : "Observation.effective[x]", + "short" : "Often just a dateTime for Vital Signs", + "definition" : "Often just a dateTime for Vital Signs.", + "min" : 1, + "max" : "1", + "type" : [{ + "code" : "dateTime" + }, + { + "code" : "Period" + }], + "condition" : ["vs-1"], + "constraint" : [{ + "key" : "vs-1", + "severity" : "error", + "human" : "if Observation.effective[x] is dateTime and has a value then that value shall be precise to the day", + "expression" : "($this as dateTime).toString().length() >= 8", + "xpath" : "f:effectiveDateTime[matches(@value, '^\\d{4}-\\d{2}-\\d{2}')]" + }], + "mustSupport" : true + }, + { + "id" : "Observation.value[x]", + "path" : "Observation.value[x]", + "short" : "Vital Signs value are recorded using the Quantity data type. For supporting observations such as Cuff size could use other datatypes such as CodeableConcept.", + "definition" : "Vital Signs value are recorded using the Quantity data type. For supporting observations such as Cuff size could use other datatypes such as CodeableConcept.", + "requirements" : "9. SHALL contain exactly one [1..1] value with @xsi:type=\"PQ\" (CONF:7305).", + "min" : 0, + "max" : "1", + "condition" : ["vs-2"], + "mustSupport" : true + }, + { + "id" : "Observation.dataAbsentReason", + "path" : "Observation.dataAbsentReason", + "min" : 0, + "max" : "1", + "type" : [{ + "code" : "CodeableConcept" + }], + "condition" : ["vs-2"], + "mustSupport" : true + }, + { + "id" : "Observation.hasMember", + "path" : "Observation.hasMember", + "short" : "Used when reporting vital signs panel components", + "definition" : "Used when reporting vital signs panel components.", + "type" : [{ + "code" : "Reference", + "targetProfile" : ["http://hl7.org/fhir/StructureDefinition/QuestionnaireResponse", + "http://hl7.org/fhir/StructureDefinition/MolecularSequence", + "http://hl7.org/fhir/StructureDefinition/vitalsigns"] + }] + }, + { + "id" : "Observation.derivedFrom", + "path" : "Observation.derivedFrom", + "type" : [{ + "code" : "Reference", + "targetProfile" : ["http://hl7.org/fhir/StructureDefinition/DocumentReference", + "http://hl7.org/fhir/StructureDefinition/ImagingStudy", + "http://hl7.org/fhir/StructureDefinition/Media", + "http://hl7.org/fhir/StructureDefinition/QuestionnaireResponse", + "http://hl7.org/fhir/StructureDefinition/MolecularSequence", + "http://hl7.org/fhir/StructureDefinition/vitalsigns"] + }] + }, + { + "id" : "Observation.component", + "path" : "Observation.component", + "short" : "Used when reporting systolic and diastolic blood pressure.", + "definition" : "Used when reporting systolic and diastolic blood pressure.", + "constraint" : [{ + "key" : "vs-3", + "severity" : "error", + "human" : "If there is no a value a data absent reason must be present", + "expression" : "value.exists() or dataAbsentReason.exists()", + "xpath" : "f:*[starts-with(local-name(.), 'value')] or f:dataAbsentReason" + }], + "mustSupport" : true + }, + { + "id" : "Observation.component.code", + "path" : "Observation.component.code", + "min" : 1, + "max" : "1", + "type" : [{ + "code" : "CodeableConcept" + }], + "mustSupport" : true, + "binding" : { + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString" : "VitalSigns" + }], + "strength" : "extensible", + "description" : "This identifies the vital sign result type.", + "valueSet" : "http://hl7.org/fhir/ValueSet/observation-vitalsignresult" + } + }, + { + "id" : "Observation.component.value[x]", + "path" : "Observation.component.value[x]", + "short" : "Vital Sign Value recorded with UCUM", + "definition" : "Vital Sign Value recorded with UCUM.", + "requirements" : "9. SHALL contain exactly one [1..1] value with @xsi:type=\"PQ\" (CONF:7305).", + "min" : 0, + "max" : "1", + "condition" : ["vs-3"], + "mustSupport" : true, + "binding" : { + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString" : "VitalSignsUnits" + }], + "strength" : "required", + "description" : "Common UCUM units for recording Vital Signs.", + "valueSet" : "http://hl7.org/fhir/ValueSet/ucum-vitals-common|4.0.1" + } + }, + { + "id" : "Observation.component.dataAbsentReason", + "path" : "Observation.component.dataAbsentReason", + "min" : 0, + "max" : "1", + "type" : [{ + "code" : "CodeableConcept" + }], + "condition" : ["vs-3"], + "mustSupport" : true + }] + } +} diff --git a/hapi-fhir-jpaserver-mdm/pom.xml b/hapi-fhir-jpaserver-mdm/pom.xml index f763c5f3bf0..4f63e20b3f2 100644 --- a/hapi-fhir-jpaserver-mdm/pom.xml +++ b/hapi-fhir-jpaserver-mdm/pom.xml @@ -69,7 +69,11 @@ spring-boot-starter-test test - + + javax.servlet + javax.servlet-api + + diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmSubmitterConfig.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmSubmitterConfig.java index b95834bcb5e..dd840b3189d 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmSubmitterConfig.java +++ b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmSubmitterConfig.java @@ -32,7 +32,7 @@ import ca.uhn.fhir.jpa.mdm.svc.MdmGoldenResourceDeletingSvc; import ca.uhn.fhir.jpa.mdm.svc.MdmSearchParamSvc; import ca.uhn.fhir.jpa.mdm.svc.MdmSubmitSvcImpl; import ca.uhn.fhir.jpa.subscription.channel.api.IChannelFactory; -import ca.uhn.fhir.mdm.util.MessageHelper; +import ca.uhn.fhir.rest.server.util.ISearchParamRetriever; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; @@ -51,8 +51,8 @@ public class MdmSubmitterConfig { } @Bean - MdmRuleValidator mdmRuleValidator(FhirContext theFhirContext) { - return new MdmRuleValidator(theFhirContext, mdmSearchParamSvc()); + MdmRuleValidator mdmRuleValidator(FhirContext theFhirContext, ISearchParamRetriever theSearchParamRetriever) { + return new MdmRuleValidator(theFhirContext, theSearchParamRetriever); } @Bean diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmSearchParamSvc.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmSearchParamSvc.java index 6b7afd413f7..4d8184c8a5f 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmSearchParamSvc.java +++ b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmSearchParamSvc.java @@ -31,7 +31,6 @@ import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorService; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; -import ca.uhn.fhir.rest.server.util.ISearchParamRetriever; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.instance.model.api.IBaseResource; import org.springframework.beans.factory.annotation.Autowired; @@ -41,7 +40,7 @@ import javax.annotation.Nullable; import java.util.List; @Service -public class MdmSearchParamSvc implements ISearchParamRetriever { +public class MdmSearchParamSvc { @Autowired FhirContext myFhirContext; @Autowired @@ -66,18 +65,12 @@ public class MdmSearchParamSvc implements ISearchParamRetriever { return mySearchParamExtractorService.extractParamValuesAsStrings(activeSearchParam, theResource); } - @Override - public RuntimeSearchParam getActiveSearchParam(String theResourceName, String theParamName) { - return mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName); - } - /** * Given a source type, and a criteria string of the shape name=x&birthDate=y, generate a {@link SearchParameterMap} * that represents this query. * * @param theSourceType the resource type to execute the search on - * @param theCriteria the string search criteria. - * + * @param theCriteria the string search criteria. * @return the generated SearchParameterMap, or an empty one if there is no criteria. */ public SearchParameterMap getSearchParameterMapFromCriteria(String theSourceType, @Nullable String theCriteria) { @@ -91,17 +84,18 @@ public class MdmSearchParamSvc implements ISearchParamRetriever { } public ISearchBuilder generateSearchBuilderForType(String theSourceType) { - IFhirResourceDao resourceDao = myDaoRegistry.getResourceDao(theSourceType); - return mySearchBuilderFactory.newSearchBuilder(resourceDao, theSourceType, resourceDao.getResourceType()); - } + IFhirResourceDao resourceDao = myDaoRegistry.getResourceDao(theSourceType); + return mySearchBuilderFactory.newSearchBuilder(resourceDao, theSourceType, resourceDao.getResourceType()); + } /** * Will return true if the types match, or the search param type is '*', otherwise false. + * * @param theSearchParamType * @param theResourceType * @return */ - public boolean searchParamTypeIsValidForResourceType(String theSearchParamType, String theResourceType) { - return theSearchParamType.equalsIgnoreCase(theResourceType) || theSearchParamType.equalsIgnoreCase("*"); - } + public boolean searchParamTypeIsValidForResourceType(String theSearchParamType, String theResourceType) { + return theSearchParamType.equalsIgnoreCase(theResourceType) || theSearchParamType.equalsIgnoreCase("*"); + } } diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/JpaRuntimeSearchParam.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/JpaRuntimeSearchParam.java index dde298b5d2f..8dd99d34106 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/JpaRuntimeSearchParam.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/JpaRuntimeSearchParam.java @@ -26,7 +26,12 @@ 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 java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import static org.apache.commons.lang3.StringUtils.isNotBlank; @@ -38,22 +43,12 @@ public class JpaRuntimeSearchParam extends RuntimeSearchParam { /** * Constructor */ - public JpaRuntimeSearchParam(IIdType theId, String theUri, String theName, String theDescription, String thePath, RestSearchParameterTypeEnum theParamType, Set theProvidesMembershipInCompartments, Set theTargets, RuntimeSearchParamStatusEnum theStatus, boolean theUnique, List theComponents, Collection> theBase) { - super(theId, theUri, theName, theDescription, thePath, theParamType, createCompositeList(theParamType), theProvidesMembershipInCompartments, theTargets, theStatus, toStrings(theBase)); + public JpaRuntimeSearchParam(IIdType theId, String theUri, String theName, String theDescription, String thePath, RestSearchParameterTypeEnum theParamType, Set theProvidesMembershipInCompartments, Set theTargets, RuntimeSearchParamStatusEnum theStatus, boolean theUnique, List theComponents, Collection theBase) { + super(theId, theUri, theName, theDescription, thePath, theParamType, createCompositeList(theParamType), theProvidesMembershipInCompartments, theTargets, theStatus, theBase); myUnique = theUnique; myComponents = Collections.unmodifiableList(theComponents); } - private static Collection toStrings(Collection> theBase) { - HashSet retVal = new HashSet<>(); - for (IPrimitiveType next : theBase) { - if (isNotBlank(next.getValueAsString())) { - retVal.add(next.getValueAsString()); - } - } - return retVal; - } - public List getComponents() { return myComponents; } @@ -62,14 +57,6 @@ public class JpaRuntimeSearchParam extends RuntimeSearchParam { return myUnique; } - private static ArrayList createCompositeList(RestSearchParameterTypeEnum theParamType) { - if (theParamType == RestSearchParameterTypeEnum.COMPOSITE) { - return new ArrayList<>(); - } else { - return null; - } - } - public static class Component { private final String myExpression; private final IBaseReference myReference; @@ -89,5 +76,13 @@ public class JpaRuntimeSearchParam extends RuntimeSearchParam { } } + private static ArrayList createCompositeList(RestSearchParameterTypeEnum theParamType) { + if (theParamType == RestSearchParameterTypeEnum.COMPOSITE) { + return new ArrayList<>(); + } else { + return null; + } + } + } diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/ISearchParamRegistry.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/ISearchParamRegistry.java index 6b7073d428d..7f8dbf07ffd 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/ISearchParamRegistry.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/ISearchParamRegistry.java @@ -25,24 +25,19 @@ import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.context.phonetic.IPhoneticEncoder; import ca.uhn.fhir.jpa.cache.ResourceChangeResult; import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam; +import ca.uhn.fhir.rest.server.util.ISearchParamRetriever; import java.util.Collection; import java.util.List; -import java.util.Map; import java.util.Set; -public interface ISearchParamRegistry { +public interface ISearchParamRegistry extends ISearchParamRetriever { /** * Request that the cache be refreshed now, in the current thread */ void forceRefresh(); - /** - * @return Returns {@literal null} if no match - */ - RuntimeSearchParam getActiveSearchParam(String theResourceName, String theParamName); - /** * @return the number of search parameter entries changed */ @@ -50,8 +45,6 @@ public interface ISearchParamRegistry { ReadOnlySearchParamCache getActiveSearchParams(); - Map getActiveSearchParams(String theResourceName); - List getActiveUniqueSearchParams(String theResourceName, Set theParamNames); List getActiveUniqueSearchParams(String theResourceName); diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParameterCanonicalizer.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParameterCanonicalizer.java index 76d765b8148..d134a8c7ba9 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParameterCanonicalizer.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParameterCanonicalizer.java @@ -28,10 +28,13 @@ import ca.uhn.fhir.model.api.ExtensionDt; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.util.DatatypeUtil; +import ca.uhn.fhir.util.ExtensionUtil; +import ca.uhn.fhir.util.FhirTerser; import ca.uhn.fhir.util.HapiExtensions; import org.apache.commons.lang3.EnumUtils; import org.hl7.fhir.dstu3.model.Extension; import org.hl7.fhir.dstu3.model.SearchParameter; +import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseDatatype; import org.hl7.fhir.instance.model.api.IBaseExtension; import org.hl7.fhir.instance.model.api.IBaseHasExtensions; @@ -47,8 +50,10 @@ import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; @@ -74,10 +79,8 @@ public class SearchParameterCanonicalizer { retVal = canonicalizeSearchParameterDstu3((org.hl7.fhir.dstu3.model.SearchParameter) theSearchParameter); break; case R4: - retVal = canonicalizeSearchParameterR4((org.hl7.fhir.r4.model.SearchParameter) theSearchParameter); - break; case R5: - retVal = canonicalizeSearchParameterR5((org.hl7.fhir.r5.model.SearchParameter) theSearchParameter); + retVal = canonicalizeSearchParameterR4Plus(theSearchParameter); break; case DSTU2_HL7ORG: case DSTU2_1: @@ -161,7 +164,7 @@ public class SearchParameterCanonicalizer { List components = Collections.emptyList(); Collection> base = Collections.singletonList(theNextSp.getBaseElement()); - return new JpaRuntimeSearchParam(id, uri, name, description, path, paramType, providesMembershipInCompartments, targets, status, unique, components, base); + return new JpaRuntimeSearchParam(id, uri, name, description, path, paramType, providesMembershipInCompartments, targets, status, unique, components, toStrings(base)); } private JpaRuntimeSearchParam canonicalizeSearchParameterDstu3(org.hl7.fhir.dstu3.model.SearchParameter theNextSp) { @@ -244,66 +247,63 @@ public class SearchParameterCanonicalizer { components.add(new JpaRuntimeSearchParam.Component(next.getExpression(), next.getDefinition())); } - return new JpaRuntimeSearchParam(id, uri, name, description, path, paramType, providesMembershipInCompartments, targets, status, unique, components, theNextSp.getBase()); + return new JpaRuntimeSearchParam(id, uri, name, description, path, paramType, providesMembershipInCompartments, targets, status, unique, components, toStrings(theNextSp.getBase())); } - private JpaRuntimeSearchParam canonicalizeSearchParameterR4(org.hl7.fhir.r4.model.SearchParameter theNextSp) { - String name = theNextSp.getCode(); - String description = theNextSp.getDescription(); - String path = theNextSp.getExpression(); + private JpaRuntimeSearchParam canonicalizeSearchParameterR4Plus(IBaseResource theNextSp) { + FhirTerser terser = myFhirContext.newTerser(); + String name = terser.getSinglePrimitiveValueOrNull(theNextSp, "code"); + String description = terser.getSinglePrimitiveValueOrNull(theNextSp, "description"); + String path = terser.getSinglePrimitiveValueOrNull(theNextSp, "expression"); + List base = terser.getValues(theNextSp, "base", IPrimitiveType.class).stream().map(t -> t.getValueAsString()).collect(Collectors.toList()); + RestSearchParameterTypeEnum paramType = null; RuntimeSearchParam.RuntimeSearchParamStatusEnum status = null; - switch (theNextSp.getType()) { - case COMPOSITE: + switch (terser.getSinglePrimitiveValue(theNextSp, "type").orElse("")) { + case "composite": paramType = RestSearchParameterTypeEnum.COMPOSITE; break; - case DATE: + case "date": paramType = RestSearchParameterTypeEnum.DATE; break; - case NUMBER: + case "number": paramType = RestSearchParameterTypeEnum.NUMBER; break; - case QUANTITY: + case "quantity": paramType = RestSearchParameterTypeEnum.QUANTITY; break; - case REFERENCE: + case "reference": paramType = RestSearchParameterTypeEnum.REFERENCE; break; - case STRING: + case "string": paramType = RestSearchParameterTypeEnum.STRING; break; - case TOKEN: + case "token": paramType = RestSearchParameterTypeEnum.TOKEN; break; - case URI: + case "uri": paramType = RestSearchParameterTypeEnum.URI; break; - case SPECIAL: + case "special": paramType = RestSearchParameterTypeEnum.SPECIAL; break; - case NULL: + } + switch (terser.getSinglePrimitiveValue(theNextSp, "status").orElse("")) { + case "active": + status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE; + break; + case "draft": + status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.DRAFT; + break; + case "retired": + status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.RETIRED; + break; + case "unknown": + status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.UNKNOWN; break; } - if (theNextSp.getStatus() != null) { - switch (theNextSp.getStatus()) { - case ACTIVE: - status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE; - break; - case DRAFT: - status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.DRAFT; - break; - case RETIRED: - status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.RETIRED; - break; - case UNKNOWN: - status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.UNKNOWN; - break; - case NULL: - break; - } - } Set providesMembershipInCompartments = Collections.emptySet(); - Set targets = DatatypeUtil.toStringSet(theNextSp.getTarget()); + Set targets = terser.getValues(theNextSp, "target", IPrimitiveType.class).stream().map(t -> t.getValueAsString()).collect(Collectors.toSet()); if (isBlank(name) || isBlank(path) || paramType == null) { if (paramType != RestSearchParameterTypeEnum.COMPOSITE) { @@ -312,111 +312,29 @@ public class SearchParameterCanonicalizer { } IIdType id = theNextSp.getIdElement(); - String uri = ""; + String uri = terser.getSinglePrimitiveValueOrNull(theNextSp, "url"); boolean unique = false; - List uniqueExts = theNextSp.getExtensionsByUrl(HapiExtensions.EXT_SP_UNIQUE); - if (uniqueExts.size() > 0) { - IPrimitiveType uniqueExtsValuePrimitive = uniqueExts.get(0).getValueAsPrimitive(); - if (uniqueExtsValuePrimitive != null) { - if ("true".equalsIgnoreCase(uniqueExtsValuePrimitive.getValueAsString())) { - unique = true; - } - } + String value = ((IBaseHasExtensions) theNextSp).getExtension() + .stream() + .filter(e -> HapiExtensions.EXT_SP_UNIQUE.equals(e.getUrl())) + .filter(t->t.getValue() instanceof IPrimitiveType) + .map(t->(IPrimitiveType)t.getValue()) + .map(t->t.getValueAsString()) + .findFirst() + .orElse(""); + if ("true".equalsIgnoreCase(value)) { + unique = true; } List components = new ArrayList<>(); - for (org.hl7.fhir.r4.model.SearchParameter.SearchParameterComponentComponent next : theNextSp.getComponent()) { - components.add(new JpaRuntimeSearchParam.Component(next.getExpression(), new Reference(next.getDefinition()))); + for (IBase next : terser.getValues(theNextSp, "component")) { + String expression = terser.getSinglePrimitiveValueOrNull(next, "expression"); + String definition = terser.getSinglePrimitiveValueOrNull(next, "definition"); + components.add(new JpaRuntimeSearchParam.Component(expression, new Reference(definition))); } - return new JpaRuntimeSearchParam(id, uri, name, description, path, paramType, providesMembershipInCompartments, targets, status, unique, components, theNextSp.getBase()); - } - - private JpaRuntimeSearchParam canonicalizeSearchParameterR5(org.hl7.fhir.r5.model.SearchParameter theNextSp) { - String name = theNextSp.getCode(); - String description = theNextSp.getDescription(); - String path = theNextSp.getExpression(); - RestSearchParameterTypeEnum paramType = null; - RuntimeSearchParam.RuntimeSearchParamStatusEnum status = null; - switch (theNextSp.getType()) { - case COMPOSITE: - paramType = RestSearchParameterTypeEnum.COMPOSITE; - break; - case DATE: - paramType = RestSearchParameterTypeEnum.DATE; - break; - case NUMBER: - paramType = RestSearchParameterTypeEnum.NUMBER; - break; - case QUANTITY: - paramType = RestSearchParameterTypeEnum.QUANTITY; - break; - case REFERENCE: - paramType = RestSearchParameterTypeEnum.REFERENCE; - break; - case STRING: - paramType = RestSearchParameterTypeEnum.STRING; - break; - case TOKEN: - paramType = RestSearchParameterTypeEnum.TOKEN; - break; - case URI: - paramType = RestSearchParameterTypeEnum.URI; - break; - case SPECIAL: - paramType = RestSearchParameterTypeEnum.SPECIAL; - break; - case NULL: - break; - } - if (theNextSp.getStatus() != null) { - switch (theNextSp.getStatus()) { - case ACTIVE: - status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE; - break; - case DRAFT: - status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.DRAFT; - break; - case RETIRED: - status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.RETIRED; - break; - case UNKNOWN: - status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.UNKNOWN; - break; - case NULL: - break; - } - } - Set providesMembershipInCompartments = Collections.emptySet(); - Set targets = DatatypeUtil.toStringSet(theNextSp.getTarget()); - - if (isBlank(name) || isBlank(path) || paramType == null) { - if (paramType != RestSearchParameterTypeEnum.COMPOSITE) { - return null; - } - } - - IIdType id = theNextSp.getIdElement(); - String uri = ""; - boolean unique = false; - - List uniqueExts = theNextSp.getExtensionsByUrl(HapiExtensions.EXT_SP_UNIQUE); - if (uniqueExts.size() > 0) { - IPrimitiveType uniqueExtsValuePrimitive = uniqueExts.get(0).getValueAsPrimitive(); - if (uniqueExtsValuePrimitive != null) { - if ("true".equalsIgnoreCase(uniqueExtsValuePrimitive.getValueAsString())) { - unique = true; - } - } - } - - List components = new ArrayList<>(); - for (org.hl7.fhir.r5.model.SearchParameter.SearchParameterComponentComponent next : theNextSp.getComponent()) { - components.add(new JpaRuntimeSearchParam.Component(next.getExpression(), new org.hl7.fhir.r5.model.Reference(next.getDefinition()))); - } - - return new JpaRuntimeSearchParam(id, uri, name, description, path, paramType, providesMembershipInCompartments, targets, status, unique, components, theNextSp.getBase()); + return new JpaRuntimeSearchParam(id, uri, name, description, path, paramType, providesMembershipInCompartments, targets, status, unique, components, base); } @@ -449,4 +367,16 @@ public class SearchParameterCanonicalizer { } } } + + private static Collection toStrings(Collection> theBase) { + HashSet retVal = new HashSet<>(); + for (IPrimitiveType next : theBase) { + if (isNotBlank(next.getValueAsString())) { + retVal.add(next.getValueAsString()); + } + } + return retVal; + } + + } 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 2f2d052420f..5fa02d56807 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 @@ -1,6 +1,7 @@ package ca.uhn.fhirtest; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; @@ -9,14 +10,13 @@ import ca.uhn.fhir.jpa.bulk.provider.BulkDataExportProvider; import ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor; import ca.uhn.fhir.jpa.provider.DiffProvider; import ca.uhn.fhir.jpa.provider.GraphQLProvider; +import ca.uhn.fhir.jpa.provider.JpaCapabilityStatementProvider; import ca.uhn.fhir.jpa.provider.JpaConformanceProviderDstu2; import ca.uhn.fhir.jpa.provider.JpaSystemProviderDstu2; import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider; import ca.uhn.fhir.jpa.provider.dstu3.JpaConformanceProviderDstu3; import ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3; -import ca.uhn.fhir.jpa.provider.r4.JpaConformanceProviderR4; import ca.uhn.fhir.jpa.provider.r4.JpaSystemProviderR4; -import ca.uhn.fhir.jpa.provider.r5.JpaConformanceProviderR5; import ca.uhn.fhir.jpa.provider.r5.JpaSystemProviderR5; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; @@ -145,7 +145,8 @@ public class TestRestfulServer extends RestfulServer { providers.add(myAppCtx.getBean("mySystemProviderR4", JpaSystemProviderR4.class)); systemDao = myAppCtx.getBean("mySystemDaoR4", IFhirSystemDao.class); etagSupport = ETagSupportEnum.ENABLED; - JpaConformanceProviderR4 confProvider = new JpaConformanceProviderR4(this, systemDao, myAppCtx.getBean(DaoConfig.class), myAppCtx.getBean(ISearchParamRegistry.class)); + 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)); @@ -164,8 +165,8 @@ public class TestRestfulServer extends RestfulServer { providers.add(myAppCtx.getBean("mySystemProviderR5", JpaSystemProviderR5.class)); systemDao = myAppCtx.getBean("mySystemDaoR5", IFhirSystemDao.class); etagSupport = ETagSupportEnum.ENABLED; - JpaConformanceProviderR5 confProvider = new JpaConformanceProviderR5(this, systemDao, myAppCtx.getBean(DaoConfig.class), myAppCtx.getBean(ISearchParamRegistry.class)); - confProvider.setImplementationDescription(implDesc); + IValidationSupport validationSupport = myAppCtx.getBean(IValidationSupport.class); + JpaCapabilityStatementProvider confProvider = new JpaCapabilityStatementProvider(this, systemDao, myAppCtx.getBean(DaoConfig.class), myAppCtx.getBean(ISearchParamRegistry.class), validationSupport); setServerConformanceProvider(confProvider); providers.add(myAppCtx.getBean(TerminologyUploaderProvider.class)); providers.add(myAppCtx.getBean(GraphQLProvider.class)); 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 51591ed0fec..f488c3e7ec1 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 @@ -1013,11 +1013,14 @@ public class RestfulServer extends HttpServlet implements IRestfulServer resourceMethod = determineResourceMethod(requestDetails, requestPath); @@ -1063,14 +1078,16 @@ public class RestfulServer extends HttpServlet implements IRestfulServer resourceBindings; private List> serverBindings; - private Map> resourceNameToSharedSupertype; + private Map> resourceNameToSharedSupertype; private String implementationDescription; private String serverVersion = VersionUtil.getVersion(); private String serverName = "HAPI FHIR"; @@ -92,15 +108,15 @@ public class RestfulServerConfiguration { this.serverBindings = theServerBindings; return this; } - - public Map> getNameToSharedSupertype() { - return resourceNameToSharedSupertype; - } - public RestfulServerConfiguration setNameToSharedSupertype(Map> resourceNameToSharedSupertype) { - this.resourceNameToSharedSupertype = resourceNameToSharedSupertype; - return this; - } + public Map> getNameToSharedSupertype() { + return resourceNameToSharedSupertype; + } + + public RestfulServerConfiguration setNameToSharedSupertype(Map> resourceNameToSharedSupertype) { + this.resourceNameToSharedSupertype = resourceNameToSharedSupertype; + return this; + } /** * Get the implementationDescription @@ -343,4 +359,94 @@ public class RestfulServerConfiguration { return retVal.toString(); } + @Override + public RuntimeSearchParam getActiveSearchParam(String theResourceName, String theParamName) { + return getActiveSearchParams(theResourceName).get(theParamName); + } + + @Override + public Map getActiveSearchParams(String theResourceName) { + + Map retVal = new LinkedHashMap<>(); + + collectMethodBindings() + .getOrDefault(theResourceName, Collections.emptyList()) + .stream() + .filter(t -> t.getResourceName().equals(theResourceName)) + .filter(t -> t instanceof SearchMethodBinding) + .map(t -> (SearchMethodBinding) t) + .filter(t -> t.getQueryName() == null) + .forEach(t -> createRuntimeBinding(retVal, t)); + + return retVal; + } + + private void createRuntimeBinding(Map theMapToPopulate, SearchMethodBinding theSearchMethodBinding) { + + List parameters = theSearchMethodBinding + .getParameters() + .stream() + .filter(t -> t instanceof SearchParameter) + .map(t -> (SearchParameter) t) + .sorted(SearchParameterComparator.INSTANCE) + .collect(Collectors.toList()); + + for (SearchParameter nextParameter : parameters) { + + String nextParamName = nextParameter.getName(); + + String nextParamUnchainedName = nextParamName; + if (nextParamName.contains(".")) { + nextParamUnchainedName = nextParamName.substring(0, nextParamName.indexOf('.')); + } + + String nextParamDescription = nextParameter.getDescription(); + + /* + * If the parameter has no description, default to the one from the resource + */ + if (StringUtils.isBlank(nextParamDescription)) { + RuntimeResourceDefinition def = getFhirContext().getResourceDefinition(theSearchMethodBinding.getResourceName()); + RuntimeSearchParam paramDef = def.getSearchParam(nextParamUnchainedName); + if (paramDef != null) { + nextParamDescription = paramDef.getDescription(); + } + } + + if (theMapToPopulate.containsKey(nextParamUnchainedName)) { + continue; + } + + IIdType id = getFhirContext().getVersion().newIdType().setValue("SearchParameter/" + theSearchMethodBinding.getResourceName() + "-" + nextParamName); + String uri = null; + String description = nextParamDescription; + String path = null; + RestSearchParameterTypeEnum type = nextParameter.getParamType(); + List compositeOf = Collections.emptyList(); + Set providesMembershipInCompartments = Collections.emptySet(); + Set targets = Collections.emptySet(); + RuntimeSearchParam.RuntimeSearchParamStatusEnum status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE; + Collection base = Collections.singletonList(theSearchMethodBinding.getResourceName()); + RuntimeSearchParam param = new RuntimeSearchParam(id, uri, nextParamName, description, path, type, compositeOf, providesMembershipInCompartments, targets, status, base); + theMapToPopulate.put(nextParamName, param); + + } + + } + + + private static class SearchParameterComparator implements Comparator { + private static final SearchParameterComparator INSTANCE = new SearchParameterComparator(); + + @Override + public int compare(SearchParameter theO1, SearchParameter theO2) { + if (theO1.isRequired() == theO2.isRequired()) { + return theO1.getName().compareTo(theO2.getName()); + } + if (theO1.isRequired()) { + return -1; + } + return 1; + } + } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerUtils.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerUtils.java index ad808df0047..07d4b8c41cc 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerUtils.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerUtils.java @@ -33,6 +33,7 @@ import ca.uhn.fhir.rest.api.BundleLinks; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.DeleteCascadeModeEnum; import ca.uhn.fhir.rest.api.EncodingEnum; +import ca.uhn.fhir.rest.api.PreferHandlingEnum; import ca.uhn.fhir.rest.api.PreferHeader; import ca.uhn.fhir.rest.api.PreferReturnEnum; import ca.uhn.fhir.rest.api.RequestTypeEnum; @@ -795,7 +796,7 @@ public class RestfulServerUtils { PreferHeader retVal = new PreferHeader(); if (isNotBlank(theValue)) { - StringTokenizer tok = new StringTokenizer(theValue, ";"); + StringTokenizer tok = new StringTokenizer(theValue, ";,"); while (tok.hasMoreTokens()) { String next = trim(tok.nextToken()); int eqIndex = next.indexOf('='); @@ -812,15 +813,14 @@ public class RestfulServerUtils { if (key.equals(Constants.HEADER_PREFER_RETURN)) { - if (value.length() < 2) { - continue; - } - if ('"' == value.charAt(0) && '"' == value.charAt(value.length() - 1)) { - value = value.substring(1, value.length() - 1); - } - + value = cleanUpValue(value); retVal.setReturn(PreferReturnEnum.fromHeaderValue(value)); + } else if (key.equals(Constants.HEADER_PREFER_HANDLING)) { + + value = cleanUpValue(value); + retVal.setHanding(PreferHandlingEnum.fromHeaderValue(value)); + } else if (key.equals(Constants.HEADER_PREFER_RESPOND_ASYNC)) { retVal.setRespondAsync(true); @@ -836,6 +836,16 @@ public class RestfulServerUtils { return retVal; } + private static String cleanUpValue(String value) { + if (value.length() < 2) { + value = ""; + } + if ('"' == value.charAt(0) && '"' == value.charAt(value.length() - 1)) { + value = value.substring(1, value.length() - 1); + } + return value; + } + public static boolean prettyPrintResponse(IRestfulServerDefaults theServer, RequestDetails theRequest) { Map requestParams = theRequest.getParameters(); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/SearchPreferHandlingInterceptor.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/SearchPreferHandlingInterceptor.java new file mode 100644 index 00000000000..d4dab8ed04b --- /dev/null +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/SearchPreferHandlingInterceptor.java @@ -0,0 +1,151 @@ +package ca.uhn.fhir.rest.server.interceptor; + +/*- + * #%L + * HAPI FHIR - Server Framework + * %% + * 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.RuntimeSearchParam; +import ca.uhn.fhir.i18n.HapiLocalizer; +import ca.uhn.fhir.interceptor.api.Hook; +import ca.uhn.fhir.interceptor.api.Interceptor; +import ca.uhn.fhir.interceptor.api.Pointcut; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.PreferHandlingEnum; +import ca.uhn.fhir.rest.api.PreferHeader; +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.RestfulServerUtils; +import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.method.SearchMethodBinding; +import ca.uhn.fhir.rest.server.util.ISearchParamRetriever; +import org.apache.commons.lang3.Validate; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.HashMap; +import java.util.List; +import java.util.stream.Collectors; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +/** + * @since 5.4.0 + */ +@Interceptor +public class SearchPreferHandlingInterceptor { + + @Nonnull + private PreferHandlingEnum myDefaultBehaviour; + @Nullable + private ISearchParamRetriever mySearchParamRetriever; + + /** + * Constructor that uses the {@link RestfulServer} itself to determine + * the allowable search params. + */ + public SearchPreferHandlingInterceptor() { + setDefaultBehaviour(PreferHandlingEnum.STRICT); + } + + /** + * Constructor that uses a dedicated {@link ISearchParamRetriever} instance. This is mainly + * intended for the JPA server. + */ + public SearchPreferHandlingInterceptor(ISearchParamRetriever theSearchParamRetriever) { + this(); + mySearchParamRetriever = theSearchParamRetriever; + } + + @Hook(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLER_SELECTED) + public void incomingRequestPostProcessed(RequestDetails theRequestDetails, HttpServletRequest theRequest, HttpServletResponse theResponse) throws AuthenticationException { + if (!SearchMethodBinding.isPlainSearchRequest(theRequestDetails)) { + return; + } + + String preferHeader = theRequestDetails.getHeader(Constants.HEADER_PREFER); + PreferHandlingEnum handling = null; + if (isNotBlank(preferHeader)) { + PreferHeader parsedPreferHeader = RestfulServerUtils.parsePreferHeader((IRestfulServer) theRequestDetails.getServer(), preferHeader); + handling = parsedPreferHeader.getHanding(); + } + + // Default behaviour + if (handling == null) { + handling = getDefaultBehaviour(); + } + + removeUnwantedParams(handling, theRequestDetails); + } + + private void removeUnwantedParams(PreferHandlingEnum theHandling, RequestDetails theRequestDetails) { + + ISearchParamRetriever searchParamRetriever = mySearchParamRetriever; + if (searchParamRetriever == null) { + searchParamRetriever = ((RestfulServer) theRequestDetails.getServer()).createConfiguration(); + } + + String resourceName = theRequestDetails.getResourceName(); + HashMap newMap = null; + for (String paramName : theRequestDetails.getParameters().keySet()) { + if (paramName.startsWith("_")) { + continue; + } + + RuntimeSearchParam activeSearchParam = searchParamRetriever.getActiveSearchParam(resourceName, paramName); + if (activeSearchParam == null) { + + if (theHandling == PreferHandlingEnum.LENIENT) { + + if (newMap == null) { + newMap = new HashMap<>(theRequestDetails.getParameters()); + } + + newMap.remove(paramName); + + } else { + + // Strict handling + List allowedParams = searchParamRetriever.getActiveSearchParams(resourceName).keySet().stream().sorted().distinct().collect(Collectors.toList()); + HapiLocalizer localizer = theRequestDetails.getFhirContext().getLocalizer(); + String msg = localizer.getMessage("ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.invalidSearchParameter", paramName, resourceName, allowedParams); + throw new InvalidRequestException(msg); + + } + } + + } + + if (newMap != null) { + theRequestDetails.setParameters(newMap); + } + } + + public PreferHandlingEnum getDefaultBehaviour() { + return myDefaultBehaviour; + } + + public void setDefaultBehaviour(@Nonnull PreferHandlingEnum theDefaultBehaviour) { + Validate.notNull(theDefaultBehaviour, "theDefaultBehaviour must not be null"); + myDefaultBehaviour = theDefaultBehaviour; + } +} 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 f89ccc8025a..8eee8146d3a 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 @@ -55,6 +55,8 @@ import java.util.Set; import java.util.TreeSet; import java.util.stream.Collectors; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + public abstract class BaseMethodBinding { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseMethodBinding.class); @@ -379,7 +381,11 @@ public abstract class BaseMethodBinding { Class returnTypeFromAnnotation = IBaseResource.class; if (read != null) { - returnTypeFromAnnotation = read.type(); + if (isNotBlank(read.typeName())) { + returnTypeFromAnnotation = theContext.getResourceDefinition(read.typeName()).getImplementingClass(); + } else { + returnTypeFromAnnotation = read.type(); + } } else if (search != null) { returnTypeFromAnnotation = search.type(); } else if (history != null) { 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 7fd3d2ec7b7..cdb569d43e4 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 @@ -148,29 +148,18 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding { @Override public MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequest) { + if (!mightBeSearchRequest(theRequest)) { + return MethodMatchEnum.NONE; + } + if (theRequest.getId() != null && myIdParamIndex == null) { ourLog.trace("Method {} doesn't match because ID is not null: {}", getMethod(), theRequest.getId()); return MethodMatchEnum.NONE; } - if (theRequest.getRequestType() == RequestTypeEnum.GET && theRequest.getOperation() != null && !Constants.PARAM_SEARCH.equals(theRequest.getOperation())) { - ourLog.trace("Method {} doesn't match because request type is GET but operation is not null: {}", theRequest.getId(), theRequest.getOperation()); - return MethodMatchEnum.NONE; - } - if (theRequest.getRequestType() == RequestTypeEnum.POST && !Constants.PARAM_SEARCH.equals(theRequest.getOperation())) { - ourLog.trace("Method {} doesn't match because request type is POST but operation is not _search: {}", theRequest.getId(), theRequest.getOperation()); - return MethodMatchEnum.NONE; - } - if (theRequest.getRequestType() != RequestTypeEnum.GET && theRequest.getRequestType() != RequestTypeEnum.POST) { - ourLog.trace("Method {} doesn't match because request type is {}", getMethod(), theRequest.getRequestType()); - return MethodMatchEnum.NONE; - } if (!StringUtils.equals(myCompartmentName, theRequest.getCompartmentName())) { ourLog.trace("Method {} doesn't match because it is for compartment {} but request is compartment {}", getMethod(), myCompartmentName, theRequest.getCompartmentName()); return MethodMatchEnum.NONE; } - if (theRequest.getParameters().get(Constants.PARAM_PAGINGACTION) != null) { - return MethodMatchEnum.NONE; - } if (myQueryName != null) { String[] queryNameValues = theRequest.getParameters().get(Constants.PARAM_QUERY); @@ -271,6 +260,38 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding { return retVal; } + /** + * Is this request a request for a normal search - Ie. not a named search, nor a compartment + * search, just a plain old search. + * + * @since 5.4.0 + */ + public static boolean isPlainSearchRequest(RequestDetails theRequest) { + if (theRequest.getId() != null) { + return false; + } + if (isNotBlank(theRequest.getCompartmentName())) { + return false; + } + return mightBeSearchRequest(theRequest); + } + + private static boolean mightBeSearchRequest(RequestDetails theRequest) { + if (theRequest.getRequestType() == RequestTypeEnum.GET && theRequest.getOperation() != null && !Constants.PARAM_SEARCH.equals(theRequest.getOperation())) { + return false; + } + if (theRequest.getRequestType() == RequestTypeEnum.POST && !Constants.PARAM_SEARCH.equals(theRequest.getOperation())) { + return false; + } + if (theRequest.getRequestType() != RequestTypeEnum.GET && theRequest.getRequestType() != RequestTypeEnum.POST) { + return false; + } + if (theRequest.getParameters().get(Constants.PARAM_PAGINGACTION) != null) { + return false; + } + return true; + } + @Override public IBundleProvider invokeServer(IRestfulServer theServer, RequestDetails theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException { if (myIdParamIndex != null) { 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 new file mode 100644 index 00000000000..51b688771af --- /dev/null +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ServerCapabilityStatementProvider.java @@ -0,0 +1,615 @@ +package ca.uhn.fhir.rest.server.provider; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.context.support.IValidationSupport; +import ca.uhn.fhir.context.support.ValidationSupportContext; +import ca.uhn.fhir.model.primitive.InstantDt; +import ca.uhn.fhir.parser.DataFormatException; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.Metadata; +import ca.uhn.fhir.rest.annotation.Read; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; +import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.server.Bindings; +import ca.uhn.fhir.rest.server.IServerConformanceProvider; +import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.RestfulServerConfiguration; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import ca.uhn.fhir.rest.server.method.BaseMethodBinding; +import ca.uhn.fhir.rest.server.method.IParameter; +import ca.uhn.fhir.rest.server.method.OperationMethodBinding; +import ca.uhn.fhir.rest.server.method.OperationMethodBinding.ReturnType; +import ca.uhn.fhir.rest.server.method.OperationParameter; +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.ISearchParamRetriever; +import ca.uhn.fhir.util.FhirTerser; +import com.google.common.collect.TreeMultimap; +import org.hl7.fhir.instance.model.api.IBase; +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.hl7.fhir.instance.model.api.IPrimitiveType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; + +import static org.apache.commons.lang3.StringUtils.defaultString; +import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +/* + * #%L + * HAPI FHIR - Server Framework + * %% + * 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% + */ + +/** + * Server FHIR Provider which serves the conformance statement for a RESTful server implementation + *

+ * This class is version independent, but will only work on servers supporting FHIR R4+ (as this was + * the first FHIR release where CapabilityStatement was a normative resource) + */ +public class ServerCapabilityStatementProvider implements IServerConformanceProvider { + + private static final Logger ourLog = LoggerFactory.getLogger(ServerCapabilityStatementProvider.class); + private final FhirContext myContext; + private final RestfulServer myServer; + private final ISearchParamRetriever mySearchParamRetriever; + private final RestfulServerConfiguration myServerConfiguration; + private final IValidationSupport myValidationSupport; + private String myPublisher = "Not provided"; + + /** + * Constructor + */ + public ServerCapabilityStatementProvider(RestfulServer theServer) { + myServer = theServer; + myContext = theServer.getFhirContext(); + mySearchParamRetriever = null; + myServerConfiguration = null; + myValidationSupport = null; + } + + /** + * Constructor + */ + public ServerCapabilityStatementProvider(FhirContext theContext, RestfulServerConfiguration theServerConfiguration) { + myContext = theContext; + myServerConfiguration = theServerConfiguration; + mySearchParamRetriever = null; + myServer = null; + myValidationSupport = null; + } + + /** + * Constructor + */ + public ServerCapabilityStatementProvider(RestfulServer theRestfulServer, ISearchParamRetriever theSearchParamRetriever, IValidationSupport theValidationSupport) { + myContext = theRestfulServer.getFhirContext(); + mySearchParamRetriever = theSearchParamRetriever; + myServer = theRestfulServer; + myServerConfiguration = null; + myValidationSupport = theValidationSupport; + } + + private void checkBindingForSystemOps(FhirTerser theTerser, IBase theRest, Set 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 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 resourceTypeToSupportedProfiles = getSupportedProfileMultimap(terser); + + 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()); + terser.addElement(retVal, "format", Constants.CT_FHIR_XML_NEW); + terser.addElement(retVal, "format", Constants.CT_FHIR_JSON_NEW); + terser.addElement(retVal, "format", Constants.FORMAT_JSON); + terser.addElement(retVal, "format", Constants.FORMAT_XML); + terser.addElement(retVal, "status", "active"); + + IBase rest = terser.addElement(retVal, "rest"); + terser.addElement(rest, "mode", "server"); + + Set systemOps = new HashSet<>(); + Set operationNames = new HashSet<>(); + + Map>> resourceToMethods = configuration.collectMethodBindings(); + Map> resourceNameToSharedSupertype = configuration.getNameToSharedSupertype(); + + for (Entry>> nextEntry : resourceToMethods.entrySet()) { + + if (nextEntry.getKey().isEmpty() == false) { + Set resourceOps = new HashSet<>(); + Set resourceIncludes = new HashSet<>(); + IBase resource = terser.addElement(rest, "resource"); + String resourceName = nextEntry.getKey(); + + 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; + 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 { + + resourceIncludes.addAll(methodBinding.getIncludes()); + + } + } 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); + } + } + + } + + ISearchParamRetriever searchParamRetriever = mySearchParamRetriever; + if (searchParamRetriever == null && myServerConfiguration != null) { + searchParamRetriever = myServerConfiguration; + } else if (searchParamRetriever == null) { + searchParamRetriever = myServer.createConfiguration(); + } + + Map searchParams = searchParamRetriever.getActiveSearchParams(resourceName); + if (searchParams != null) { + 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); + } + } + + if (resourceIncludes.isEmpty()) { + for (String nextInclude : searchParams.values().stream().filter(t -> t.getParamType() == RestSearchParameterTypeEnum.REFERENCE).map(t -> t.getName()).sorted().collect(Collectors.toList())) { + terser.addElement(resource, "searchInclude", nextInclude); + } + } + + } + + for (String supportedProfile : resourceTypeToSupportedProfiles.get(resourceName)) { + terser.addElement(resource, "supportedProfile", supportedProfile); + } + + for (String resourceInclude : resourceIncludes) { + terser.addElement(resource, "searchInclude", resourceInclude); + } + + } 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 getSupportedProfileMultimap(FhirTerser terser) { + TreeMultimap resourceTypeToSupportedProfiles = TreeMultimap.create(); + if (myValidationSupport != null) { + List 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)) { + next = myValidationSupport.generateSnapshot(new ValidationSupportContext(myValidationSupport), next, null, null, null); + if (next != null) { + resourceType = terser.getSinglePrimitiveValueOrNull(next, "snapshot.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 operationBindings = bindings.getOperationNameToBindings().get(theId.getIdPart()); + if (operationBindings != null && !operationBindings.isEmpty()) { + return readOperationDefinitionForOperation(operationBindings); + } + List searchBindings = bindings.getSearchNameToBindings().get(theId.getIdPart()); + if (searchBindings != null && !searchBindings.isEmpty()) { + return readOperationDefinitionForNamedSearch(searchBindings); + } + throw new ResourceNotFoundException(theId); + } + + private IBaseResource readOperationDefinitionForNamedSearch(List 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 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 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 resourceNames = new TreeSet<>(); + Set inParams = new HashSet<>(); + Set 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 + } + +} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/BaseServerCapabilityStatementProvider.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/BaseServerCapabilityStatementProvider.java index 9ec272580c3..c9c116c28e6 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/BaseServerCapabilityStatementProvider.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/BaseServerCapabilityStatementProvider.java @@ -26,6 +26,8 @@ import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.RestfulServerConfiguration; import org.apache.commons.lang3.Validate; +import javax.annotation.Nullable; + public class BaseServerCapabilityStatementProvider { private RestfulServerConfiguration myConfiguration; @@ -39,7 +41,7 @@ public class BaseServerCapabilityStatementProvider { } - protected RestfulServerConfiguration getServerConfiguration(RequestDetails theRequestDetails) { + protected RestfulServerConfiguration getServerConfiguration(@Nullable RequestDetails theRequestDetails) { RestfulServerConfiguration retVal; if (theRequestDetails != null && theRequestDetails.getServer() instanceof RestfulServer) { retVal = ((RestfulServer) theRequestDetails.getServer()).createConfiguration(); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/ISearchParamRetriever.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/ISearchParamRetriever.java index 69f12333db5..2bec515dbc2 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/ISearchParamRetriever.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/ISearchParamRetriever.java @@ -22,9 +22,17 @@ package ca.uhn.fhir.rest.server.util; import ca.uhn.fhir.context.RuntimeSearchParam; +import java.util.Map; + public interface ISearchParamRetriever { /** * @return Returns {@literal null} if no match */ RuntimeSearchParam getActiveSearchParam(String theResourceName, String theParamName); + + /** + * @return Returns all active search params for the given resource + */ + Map getActiveSearchParams(String theResourceName); + } diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/RestfulServerUtilsTest.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/RestfulServerUtilsTest.java index b8250f2eb52..6e22f74fb6c 100644 --- a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/RestfulServerUtilsTest.java +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/RestfulServerUtilsTest.java @@ -1,5 +1,6 @@ package ca.uhn.fhir.rest.server; +import ca.uhn.fhir.rest.api.PreferHandlingEnum; import ca.uhn.fhir.rest.api.PreferHeader; import ca.uhn.fhir.rest.api.PreferReturnEnum; import org.junit.jupiter.api.Test; @@ -28,4 +29,28 @@ public class RestfulServerUtilsTest{ assertEquals(null, header.getReturn()); assertTrue(header.getRespondAsync()); } + + @Test + public void testParseHandlingLenient() { + PreferHeader header = RestfulServerUtils.parsePreferHeader(null,"handling=lenient"); + assertEquals(null, header.getReturn()); + assertFalse(header.getRespondAsync()); + assertEquals(PreferHandlingEnum.LENIENT, header.getHanding()); + } + + @Test + public void testParseHandlingLenientAndReturnRepresentation_CommaSeparatd() { + PreferHeader header = RestfulServerUtils.parsePreferHeader(null,"handling=lenient, return=representation"); + assertEquals(PreferReturnEnum.REPRESENTATION, header.getReturn()); + assertFalse(header.getRespondAsync()); + assertEquals(PreferHandlingEnum.LENIENT, header.getHanding()); + } + + @Test + public void testParseHandlingLenientAndReturnRepresentation_SemicolonSeparatd() { + PreferHeader header = RestfulServerUtils.parsePreferHeader(null,"handling=lenient; return=representation"); + assertEquals(PreferReturnEnum.REPRESENTATION, header.getReturn()); + assertFalse(header.getRespondAsync()); + assertEquals(PreferHandlingEnum.LENIENT, header.getHanding()); + } } diff --git a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/ctx/FhirServerR4.java b/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/ctx/FhirServerR4.java index 6557858b62a..62b47b29dea 100644 --- a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/ctx/FhirServerR4.java +++ b/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/ctx/FhirServerR4.java @@ -2,9 +2,10 @@ package org.hl7.fhir.r4.hapi.ctx; import ca.uhn.fhir.rest.api.server.IFhirVersionServer; import ca.uhn.fhir.rest.server.RestfulServer; -import org.hl7.fhir.r4.hapi.rest.server.ServerCapabilityStatementProvider; +import ca.uhn.fhir.rest.server.provider.ServerCapabilityStatementProvider; public class FhirServerR4 implements IFhirVersionServer { + @Override public ServerCapabilityStatementProvider createServerConformanceProvider(RestfulServer theServer) { return new ServerCapabilityStatementProvider(theServer); diff --git a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/rest/server/ServerCapabilityStatementProvider.java b/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/rest/server/ServerCapabilityStatementProvider.java deleted file mode 100644 index fd72529be18..00000000000 --- a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/rest/server/ServerCapabilityStatementProvider.java +++ /dev/null @@ -1,578 +0,0 @@ -package org.hl7.fhir.r4.hapi.rest.server; - -import ca.uhn.fhir.context.FhirVersionEnum; -import ca.uhn.fhir.context.RuntimeResourceDefinition; -import ca.uhn.fhir.context.RuntimeSearchParam; -import ca.uhn.fhir.parser.DataFormatException; -import ca.uhn.fhir.rest.annotation.IdParam; -import ca.uhn.fhir.rest.annotation.Metadata; -import ca.uhn.fhir.rest.annotation.Read; -import ca.uhn.fhir.rest.api.Constants; -import ca.uhn.fhir.rest.api.server.RequestDetails; -import ca.uhn.fhir.rest.server.Bindings; -import ca.uhn.fhir.rest.server.IServerConformanceProvider; -import ca.uhn.fhir.rest.server.RestfulServer; -import ca.uhn.fhir.rest.server.RestfulServerConfiguration; -import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; -import ca.uhn.fhir.rest.server.method.*; -import ca.uhn.fhir.rest.server.method.SearchParameter; -import ca.uhn.fhir.rest.server.method.OperationMethodBinding.ReturnType; -import ca.uhn.fhir.rest.server.util.BaseServerCapabilityStatementProvider; -import org.apache.commons.lang3.StringUtils; -import org.hl7.fhir.exceptions.FHIRException; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.instance.model.api.IPrimitiveType; -import org.hl7.fhir.r4.model.*; -import org.hl7.fhir.r4.model.CapabilityStatement.*; -import org.hl7.fhir.r4.model.Enumerations.PublicationStatus; -import org.hl7.fhir.r4.model.OperationDefinition.OperationDefinitionParameterComponent; -import org.hl7.fhir.r4.model.OperationDefinition.OperationKind; -import org.hl7.fhir.r4.model.OperationDefinition.OperationParameterUse; - -import javax.servlet.ServletContext; -import javax.servlet.http.HttpServletRequest; -import java.util.*; -import java.util.Map.Entry; - -import static org.apache.commons.lang3.StringUtils.isBlank; -import static org.apache.commons.lang3.StringUtils.isNotBlank; - -import ca.uhn.fhir.context.FhirContext; - -/* - * #%L - * HAPI FHIR Structures - DSTU2 (FHIR v1.0.0) - * %% - * Copyright (C) 2014 - 2015 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -/** - * Server FHIR Provider which serves the conformance statement for a RESTful server implementation - * - *

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

- */ -public class ServerCapabilityStatementProvider extends BaseServerCapabilityStatementProvider implements IServerConformanceProvider { - - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServerCapabilityStatementProvider.class); - private String myPublisher = "Not provided"; - - /** - * No-arg constructor and setter so that the ServerConformanceProvider can be Spring-wired with the RestfulService avoiding the potential reference cycle that would happen. - */ - public ServerCapabilityStatementProvider() { - super(); - } - - /** - * Constructor - * - * @deprecated Use no-args constructor instead. Deprecated in 4.0.0 - */ - @Deprecated - public ServerCapabilityStatementProvider(RestfulServer theRestfulServer) { - this(); - } - - /** - * Constructor - This is intended only for JAX-RS server - */ - public ServerCapabilityStatementProvider(RestfulServerConfiguration theServerConfiguration) { - super(theServerConfiguration); - } - - private void checkBindingForSystemOps(CapabilityStatementRestComponent rest, Set systemOps, BaseMethodBinding nextMethodBinding) { - if (nextMethodBinding.getRestOperationType() != null) { - String sysOpCode = nextMethodBinding.getRestOperationType().getCode(); - if (sysOpCode != null) { - SystemRestfulInteraction sysOp; - try { - sysOp = SystemRestfulInteraction.fromCode(sysOpCode); - } catch (FHIRException e) { - return; - } - if (sysOp == null) { - return; - } - if (systemOps.contains(sysOp) == false) { - systemOps.add(sysOp); - rest.addInteraction().setCode(sysOp); - } - } - } - } - - private DateTimeType conformanceDate(RequestDetails theRequestDetails) { - IPrimitiveType buildDate = getServerConfiguration(theRequestDetails).getConformanceDate(); - if (buildDate != null && buildDate.getValue() != null) { - try { - return new DateTimeType(buildDate.getValueAsString()); - } catch (DataFormatException e) { - // fall through - } - } - return DateTimeType.now(); - } - - - /** - * 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; - } - - @SuppressWarnings("EnumSwitchStatementWhichMissesCases") - @Override - @Metadata - public CapabilityStatement getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) { - - RestfulServerConfiguration configuration = getServerConfiguration(theRequestDetails); - Bindings bindings = configuration.provideBindings(); - - CapabilityStatement retVal = new CapabilityStatement(); - - retVal.setPublisher(myPublisher); - retVal.setDateElement(conformanceDate(theRequestDetails)); - retVal.setFhirVersion(Enumerations.FHIRVersion.fromCode(FhirVersionEnum.R4.getFhirVersionString())); - - ServletContext servletContext = (ServletContext) (theRequest == null ? null : theRequest.getAttribute(RestfulServer.SERVLET_CONTEXT_ATTRIBUTE)); - String serverBase = configuration.getServerAddressStrategy().determineServerBase(servletContext, theRequest); - retVal - .getImplementation() - .setUrl(serverBase) - .setDescription(configuration.getImplementationDescription()); - - retVal.setKind(CapabilityStatementKind.INSTANCE); - retVal.getSoftware().setName(configuration.getServerName()); - retVal.getSoftware().setVersion(configuration.getServerVersion()); - retVal.addFormat(Constants.CT_FHIR_XML_NEW); - retVal.addFormat(Constants.CT_FHIR_JSON_NEW); - retVal.addFormat(Constants.FORMAT_JSON); - retVal.addFormat(Constants.FORMAT_XML); - retVal.setStatus(PublicationStatus.ACTIVE); - - CapabilityStatementRestComponent rest = retVal.addRest(); - rest.setMode(RestfulCapabilityMode.SERVER); - - Set systemOps = new HashSet<>(); - Set operationNames = new HashSet<>(); - - Map>> resourceToMethods = configuration.collectMethodBindings(); - Map> resourceNameToSharedSupertype = configuration.getNameToSharedSupertype(); - for (Entry>> nextEntry : resourceToMethods.entrySet()) { - - if (nextEntry.getKey().isEmpty() == false) { - Set resourceOps = new HashSet<>(); - CapabilityStatementRestResourceComponent resource = rest.addResource(); - String resourceName = nextEntry.getKey(); - - RuntimeResourceDefinition def; - FhirContext context = configuration.getFhirContext(); - if (resourceNameToSharedSupertype.containsKey(resourceName)) { - def = context.getResourceDefinition(resourceNameToSharedSupertype.get(resourceName)); - } else { - def = context.getResourceDefinition(resourceName); - } - resource.getTypeElement().setValue(def.getName()); - resource.getProfileElement().setValue((def.getResourceProfile(serverBase))); - - TreeSet includes = new TreeSet<>(); - - // Map nameToSearchParam = new HashMap(); - for (BaseMethodBinding nextMethodBinding : nextEntry.getValue()) { - nextMethodBinding.getRestOperationType(); - String resOpCode = nextMethodBinding.getRestOperationType().getCode(); - if (resOpCode != null) { - TypeRestfulInteraction resOp; - try { - resOp = TypeRestfulInteraction.fromCode(resOpCode); - } catch (Exception e) { - resOp = null; - } - if (resOp != null) { - if (resourceOps.contains(resOp) == false) { - resourceOps.add(resOp); - resource.addInteraction().setCode(resOp); - } - if ("vread".equals(resOpCode)) { - // vread implies read - resOp = TypeRestfulInteraction.READ; - if (resourceOps.contains(resOp) == false) { - resourceOps.add(resOp); - resource.addInteraction().setCode(resOp); - } - } - - if (nextMethodBinding.isSupportsConditional()) { - switch (resOp) { - case CREATE: - resource.setConditionalCreate(true); - break; - case DELETE: - if (nextMethodBinding.isSupportsConditionalMultiple()) { - resource.setConditionalDelete(ConditionalDeleteStatus.MULTIPLE); - } else { - resource.setConditionalDelete(ConditionalDeleteStatus.SINGLE); - } - break; - case UPDATE: - resource.setConditionalUpdate(true); - break; - default: - break; - } - } - } - } - - checkBindingForSystemOps(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)) { - rest.addOperation().setName(methodBinding.getQueryName()).setDefinition((getOperationDefinitionPrefix(theRequestDetails) + "OperationDefinition/" + queryName)); - } - } else { - handleNamelessSearchMethodBinding(resource, def, includes, (SearchMethodBinding) nextMethodBinding, theRequestDetails); - } - } else if (nextMethodBinding instanceof OperationMethodBinding) { - OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; - String opName = bindings.getOperationBindingToName().get(methodBinding); - if (operationNames.add(opName)) { - // Only add each operation (by name) once - rest.addOperation().setName(methodBinding.getName().substring(1)).setDefinition((getOperationDefinitionPrefix(theRequestDetails) + "OperationDefinition/" + opName)); - } - } - - resource.getInteraction().sort(new Comparator() { - @Override - public int compare(ResourceInteractionComponent theO1, ResourceInteractionComponent theO2) { - TypeRestfulInteraction o1 = theO1.getCode(); - TypeRestfulInteraction o2 = theO2.getCode(); - if (o1 == null && o2 == null) { - return 0; - } - if (o1 == null) { - return 1; - } - if (o2 == null) { - return -1; - } - return o1.ordinal() - o2.ordinal(); - } - }); - - } - - for (String nextInclude : includes) { - resource.addSearchInclude(nextInclude); - } - } else { - for (BaseMethodBinding nextMethodBinding : nextEntry.getValue()) { - checkBindingForSystemOps(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); - rest.addOperation().setName(methodBinding.getName().substring(1)).setDefinition((getOperationDefinitionPrefix(theRequestDetails) + "OperationDefinition/" + opName)); - } - } - } - } - } - - return retVal; - } - - protected String getOperationDefinitionPrefix(RequestDetails theRequestDetails) { - if (theRequestDetails == null) { - return ""; - } - return theRequestDetails.getServerBaseForRequest() + "/"; - } - - private void handleNamelessSearchMethodBinding(CapabilityStatementRestResourceComponent resource, RuntimeResourceDefinition def, TreeSet includes, - SearchMethodBinding searchMethodBinding, RequestDetails theRequestDetails) { - includes.addAll(searchMethodBinding.getIncludes()); - - List params = searchMethodBinding.getParameters(); - List searchParameters = new ArrayList<>(); - for (IParameter nextParameter : params) { - if ((nextParameter instanceof SearchParameter)) { - searchParameters.add((SearchParameter) nextParameter); - } - } - sortSearchParameters(searchParameters); - if (!searchParameters.isEmpty()) { - - Set paramNames = new HashSet<>(); - for (SearchParameter nextParameter : searchParameters) { - - if (nextParameter.getParamType() == null) { - ourLog.warn("SearchParameter {}:{} does not declare a type - Not exporting in CapabilityStatement", def.getName(), nextParameter.getName()); - continue; - } - - String nextParamName = nextParameter.getName(); - - String nextParamUnchainedName = nextParamName; - if (nextParamName.contains(".")) { - nextParamUnchainedName = nextParamName.substring(0, nextParamName.indexOf('.')); - } - - if (!paramNames.add(nextParamUnchainedName)) { - continue; - } - - String nextParamDescription = nextParameter.getDescription(); - - /* - * If the parameter has no description, default to the one from the resource - */ - if (StringUtils.isBlank(nextParamDescription)) { - RuntimeSearchParam paramDef = def.getSearchParam(nextParamUnchainedName); - if (paramDef != null) { - nextParamDescription = paramDef.getDescription(); - } - } - - - CapabilityStatementRestResourceSearchParamComponent param = resource.addSearchParam(); - String typeCode = nextParameter.getParamType().getCode(); - param.getTypeElement().setValueAsString(typeCode); - param.setName(nextParamUnchainedName); - param.setDocumentation(nextParamDescription); - - } - } - } - - - @Read(type = OperationDefinition.class) - public OperationDefinition readOperationDefinition(@IdParam IdType theId, RequestDetails theRequestDetails) { - if (theId == null || theId.hasIdPart() == false) { - throw new ResourceNotFoundException(theId); - } - RestfulServerConfiguration configuration = getServerConfiguration(theRequestDetails); - Bindings bindings = configuration.provideBindings(); - - List operationBindings = bindings.getOperationNameToBindings().get(theId.getIdPart()); - if (operationBindings != null && !operationBindings.isEmpty()) { - return readOperationDefinitionForOperation(operationBindings); - } - List searchBindings = bindings.getSearchNameToBindings().get(theId.getIdPart()); - if (searchBindings != null && !searchBindings.isEmpty()) { - return readOperationDefinitionForNamedSearch(searchBindings); - } - throw new ResourceNotFoundException(theId); - } - - private OperationDefinition readOperationDefinitionForNamedSearch(List bindings) { - OperationDefinition op = new OperationDefinition(); - op.setStatus(PublicationStatus.ACTIVE); - op.setKind(OperationKind.QUERY); - op.setAffectsState(false); - - op.setSystem(false); - op.setType(false); - op.setInstance(false); - - Set inParams = new HashSet<>(); - - for (SearchMethodBinding binding : bindings) { - if (isNotBlank(binding.getDescription())) { - op.setDescription(binding.getDescription()); - } - if (isBlank(binding.getResourceProviderResourceName())) { - op.setSystem(true); - } else { - op.setType(true); - op.addResourceElement().setValue(binding.getResourceProviderResourceName()); - } - op.setCode(binding.getQueryName()); - for (IParameter nextParamUntyped : binding.getParameters()) { - if (nextParamUntyped instanceof SearchParameter) { - SearchParameter nextParam = (SearchParameter) nextParamUntyped; - if (!inParams.add(nextParam.getName())) { - continue; - } - OperationDefinitionParameterComponent param = op.addParameter(); - param.setUse(OperationParameterUse.IN); - param.setType("string"); - param.getSearchTypeElement().setValueAsString(nextParam.getParamType().getCode()); - param.setMin(nextParam.isRequired() ? 1 : 0); - param.setMax("1"); - param.setName(nextParam.getName()); - - - } - } - - if (isBlank(op.getName())) { - if (isNotBlank(op.getDescription())) { - op.setName(op.getDescription()); - } else { - op.setName(op.getCode()); - } - } - } - - return op; - } - - private OperationDefinition readOperationDefinitionForOperation(List bindings) { - OperationDefinition op = new OperationDefinition(); - op.setStatus(PublicationStatus.ACTIVE); - op.setKind(OperationKind.OPERATION); - op.setAffectsState(false); - - // We reset these to true below if we find a binding that can handle the level - op.setSystem(false); - op.setType(false); - op.setInstance(false); - - Set inParams = new HashSet<>(); - Set outParams = new HashSet<>(); - - for (OperationMethodBinding sharedDescription : bindings) { - if (isNotBlank(sharedDescription.getDescription())) { - op.setDescription(sharedDescription.getDescription()); - } - if (sharedDescription.isCanOperateAtInstanceLevel()) { - op.setInstance(true); - } - if (sharedDescription.isCanOperateAtServerLevel()) { - op.setSystem(true); - } - if (sharedDescription.isCanOperateAtTypeLevel()) { - op.setType(true); - } - if (!sharedDescription.isIdempotent()) { - op.setAffectsState(!sharedDescription.isIdempotent()); - } - op.setCode(sharedDescription.getName().substring(1)); - if (sharedDescription.isCanOperateAtInstanceLevel()) { - op.setInstance(sharedDescription.isCanOperateAtInstanceLevel()); - } - if (sharedDescription.isCanOperateAtServerLevel()) { - op.setSystem(sharedDescription.isCanOperateAtServerLevel()); - } - if (isNotBlank(sharedDescription.getResourceName())) { - op.addResourceElement().setValue(sharedDescription.getResourceName()); - } - - for (IParameter nextParamUntyped : sharedDescription.getParameters()) { - if (nextParamUntyped instanceof OperationParameter) { - OperationParameter nextParam = (OperationParameter) nextParamUntyped; - OperationDefinitionParameterComponent param = op.addParameter(); - if (!inParams.add(nextParam.getName())) { - continue; - } - param.setUse(OperationParameterUse.IN); - if (nextParam.getParamType() != null) { - param.setType(nextParam.getParamType()); - } - if (nextParam.getSearchParamType() != null) { - param.getSearchTypeElement().setValueAsString(nextParam.getSearchParamType()); - } - param.setMin(nextParam.getMin()); - param.setMax(nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax())); - param.setName(nextParam.getName()); - } - } - - for (ReturnType nextParam : sharedDescription.getReturnParams()) { - if (!outParams.add(nextParam.getName())) { - continue; - } - OperationDefinitionParameterComponent param = op.addParameter(); - param.setUse(OperationParameterUse.OUT); - if (nextParam.getType() != null) { - param.setType(nextParam.getType()); - } - param.setMin(nextParam.getMin()); - param.setMax(nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax())); - param.setName(nextParam.getName()); - } - } - - if (isBlank(op.getName())) { - if (isNotBlank(op.getDescription())) { - op.setName(op.getDescription()); - } else { - op.setName(op.getCode()); - } - } - - if (op.hasSystem() == false) { - op.setSystem(false); - } - if (op.hasInstance() == false) { - op.setInstance(false); - } - - return op; - } - - /** - * Sets the cache property (default is true). If set to true, the same response will be returned for each invocation. - *

- * See the class documentation for an important note if you are extending this class - *

- * - * @deprecated Since 4.0.0 - This method no longer does anything - */ - @Deprecated - public ServerCapabilityStatementProvider setCache(boolean theCache) { - return this; - } - - @Override - public void setRestfulServer(RestfulServer theRestfulServer) { - // ignore - } - - private void sortSearchParameters(List searchParameters) { - Collections.sort(searchParameters, new Comparator() { - @Override - public int compare(SearchParameter theO1, SearchParameter theO2) { - if (theO1.isRequired() == theO2.isRequired()) { - return theO1.getName().compareTo(theO2.getName()); - } - if (theO1.isRequired()) { - return -1; - } - return 1; - } - }); - } -} diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/ReadDstu3Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/ReadR4Test.java similarity index 63% rename from hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/ReadDstu3Test.java rename to hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/ReadR4Test.java index 41107427ee2..dd76ccb61de 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/ReadDstu3Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/ReadR4Test.java @@ -1,31 +1,32 @@ package ca.uhn.fhir.rest.server; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Read; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.client.MyPatientWithExtensions; -import ca.uhn.fhir.test.utilities.JettyUtil; +import ca.uhn.fhir.test.utilities.server.RestfulServerExtension; import ca.uhn.fhir.util.DateUtils; -import ca.uhn.fhir.util.TestUtil; import org.apache.commons.io.IOUtils; -import org.apache.http.HttpResponse; 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.apache.http.impl.conn.PoolingHttpClientConnectionManager; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.servlet.ServletHandler; -import org.eclipse.jetty.servlet.ServletHolder; -import org.hl7.fhir.dstu3.model.DateType; -import org.hl7.fhir.dstu3.model.IdType; -import org.hl7.fhir.dstu3.model.Patient; -import org.junit.jupiter.api.Test; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.DateType; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Patient; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeUnit; @@ -34,30 +35,34 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.stringContainsInOrder; import static org.junit.jupiter.api.Assertions.assertEquals; -public class ReadDstu3Test { +public class ReadR4Test { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ReadR4Test.class); private static CloseableHttpClient ourClient; + private final FhirContext myCtx = FhirContext.forCached(FhirVersionEnum.R4); + @RegisterExtension + public RestfulServerExtension myRestfulServerExtension = new RestfulServerExtension(myCtx); + private int myPort; - private static FhirContext ourCtx = FhirContext.forDstu3(); - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ReadDstu3Test.class); - private static int ourPort; - private static Server ourServer; + @BeforeEach + public void before() { + myPort = myRestfulServerExtension.getPort(); + } @Test public void testRead() throws Exception { + myRestfulServerExtension.getRestfulServer().registerProvider(new PatientProvider()); - HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/2?_format=xml&_pretty=true"); - HttpResponse status = ourClient.execute(httpGet); + HttpGet httpGet = new HttpGet("http://localhost:" + myPort + "/Patient/2?_format=xml&_pretty=true"); + try (CloseableHttpResponse status = ourClient.execute(httpGet)) { - String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); - IOUtils.closeQuietly(status.getEntity().getContent()); + String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info("Response was:\n{}", responseContent); - ourLog.info("Response was:\n{}", responseContent); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertEquals(null, status.getFirstHeader(Constants.HEADER_LOCATION)); + assertEquals("http://localhost:" + myPort + "/Patient/2/_history/2", status.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue()); - assertEquals(200, status.getStatusLine().getStatusCode()); - assertEquals(null, status.getFirstHeader(Constants.HEADER_LOCATION)); - assertEquals("http://localhost:" + ourPort + "/Patient/2/_history/2", status.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue()); - - assertThat(responseContent, stringContainsInOrder( + assertThat(responseContent, stringContainsInOrder( "", " ", " ", @@ -67,14 +72,44 @@ public class ReadDstu3Test { " ", " ", "")); + } + } + + @Test + public void testReadUsingPlainProvider() throws Exception { + myRestfulServerExtension.getRestfulServer().registerProvider(new PlainGenericPatientProvider()); + + HttpGet httpGet = new HttpGet("http://localhost:" + myPort + "/Patient/2?_format=xml&_pretty=true"); + try (CloseableHttpResponse status = ourClient.execute(httpGet)) { + + String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info("Response was:\n{}", responseContent); + + assertEquals(200, status.getStatusLine().getStatusCode()); + assertEquals(null, status.getFirstHeader(Constants.HEADER_LOCATION)); + assertEquals("http://localhost:" + myPort + "/Patient/2/_history/2", status.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue()); + + assertThat(responseContent, stringContainsInOrder( + "", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "")); + } } @Test public void testInvalidQueryParamsInRead() throws Exception { + myRestfulServerExtension.getRestfulServer().registerProvider(new PatientProvider()); + CloseableHttpResponse status; HttpGet httpGet; - httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/2?_contained=both&_format=xml&_pretty=true"); + httpGet = new HttpGet("http://localhost:" + myPort + "/Patient/2?_contained=both&_format=xml&_pretty=true"); status = ourClient.execute(httpGet); try (InputStream inputStream = status.getEntity().getContent()) { assertEquals(400, status.getStatusLine().getStatusCode()); @@ -91,7 +126,7 @@ public class ReadDstu3Test { )); } - httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/2?_containedType=contained&_format=xml&_pretty=true"); + httpGet = new HttpGet("http://localhost:" + myPort + "/Patient/2?_containedType=contained&_format=xml&_pretty=true"); status = ourClient.execute(httpGet); try (InputStream inputStream = status.getEntity().getContent()) { assertEquals(400, status.getStatusLine().getStatusCode()); @@ -108,7 +143,7 @@ public class ReadDstu3Test { )); } - httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/2?_count=10&_format=xml&_pretty=true"); + httpGet = new HttpGet("http://localhost:" + myPort + "/Patient/2?_count=10&_format=xml&_pretty=true"); status = ourClient.execute(httpGet); try (InputStream inputStream = status.getEntity().getContent()) { assertEquals(400, status.getStatusLine().getStatusCode()); @@ -125,7 +160,7 @@ public class ReadDstu3Test { )); } - httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/2?_include=Patient:organization&_format=xml&_pretty=true"); + httpGet = new HttpGet("http://localhost:" + myPort + "/Patient/2?_include=Patient:organization&_format=xml&_pretty=true"); status = ourClient.execute(httpGet); try (InputStream inputStream = status.getEntity().getContent()) { assertEquals(400, status.getStatusLine().getStatusCode()); @@ -142,7 +177,7 @@ public class ReadDstu3Test { )); } - httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/2?_revinclude=Provenance:target&_format=xml&_pretty=true"); + httpGet = new HttpGet("http://localhost:" + myPort + "/Patient/2?_revinclude=Provenance:target&_format=xml&_pretty=true"); status = ourClient.execute(httpGet); try (InputStream inputStream = status.getEntity().getContent()) { assertEquals(400, status.getStatusLine().getStatusCode()); @@ -159,7 +194,7 @@ public class ReadDstu3Test { )); } - httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/2?_sort=family&_format=xml&_pretty=true"); + httpGet = new HttpGet("http://localhost:" + myPort + "/Patient/2?_sort=family&_format=xml&_pretty=true"); status = ourClient.execute(httpGet); try (InputStream inputStream = status.getEntity().getContent()) { assertEquals(400, status.getStatusLine().getStatusCode()); @@ -176,7 +211,7 @@ public class ReadDstu3Test { )); } - httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/2?_total=accurate&_format=xml&_pretty=true"); + httpGet = new HttpGet("http://localhost:" + myPort + "/Patient/2?_total=accurate&_format=xml&_pretty=true"); status = ourClient.execute(httpGet); try (InputStream inputStream = status.getEntity().getContent()) { assertEquals(400, status.getStatusLine().getStatusCode()); @@ -196,77 +231,39 @@ public class ReadDstu3Test { @Test public void testIfModifiedSince() throws Exception { + myRestfulServerExtension.getRestfulServer().registerProvider(new PatientProvider()); - CloseableHttpResponse status; HttpGet httpGet; // Fixture was last modified at 2012-01-01T12:12:12Z // thus it hasn't changed after the later time of 2012-01-01T13:00:00Z // so we expect a 304 (Not Modified) - httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/2"); + httpGet = new HttpGet("http://localhost:" + myPort + "/Patient/2"); httpGet.addHeader(Constants.HEADER_IF_MODIFIED_SINCE, DateUtils.formatDate(new InstantDt("2012-01-01T13:00:00Z").getValue())); - status = ourClient.execute(httpGet); - try { + try (CloseableHttpResponse status = ourClient.execute(httpGet)) { assertEquals(304, status.getStatusLine().getStatusCode()); - } finally { - IOUtils.closeQuietly(status); } // Fixture was last modified at 2012-01-01T12:12:12Z // thus it hasn't changed after the same time of 2012-01-01T12:12:12Z // so we expect a 304 (Not Modified) - httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/2"); + httpGet = new HttpGet("http://localhost:" + myPort + "/Patient/2"); httpGet.addHeader(Constants.HEADER_IF_MODIFIED_SINCE, DateUtils.formatDate(new InstantDt("2012-01-01T12:12:12Z").getValue())); - status = ourClient.execute(httpGet); - try { + try (CloseableHttpResponse status = ourClient.execute(httpGet)) { assertEquals(304, status.getStatusLine().getStatusCode()); - } finally { - IOUtils.closeQuietly(status); } // Fixture was last modified at 2012-01-01T12:12:12Z // thus it has changed after the earlier time of 2012-01-01T10:00:00Z // so we expect a 200 - httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/2"); + httpGet = new HttpGet("http://localhost:" + myPort + "/Patient/2"); httpGet.addHeader(Constants.HEADER_IF_MODIFIED_SINCE, DateUtils.formatDate(new InstantDt("2012-01-01T10:00:00Z").getValue())); - status = ourClient.execute(httpGet); - try { + try (CloseableHttpResponse status = ourClient.execute(httpGet)) { assertEquals(200, status.getStatusLine().getStatusCode()); - } finally { - IOUtils.closeQuietly(status); } } - @AfterAll - public static void afterClassClearContext() throws Exception { - JettyUtil.closeServer(ourServer); - TestUtil.clearAllStaticFieldsForUnitTest(); - } - - @BeforeAll - public static void beforeClass() throws Exception { - ourServer = new Server(0); - - PatientProvider patientProvider = new PatientProvider(); - - ServletHandler proxyHandler = new ServletHandler(); - RestfulServer servlet = new RestfulServer(ourCtx); - - servlet.setResourceProviders(patientProvider); - ServletHolder servletHolder = new ServletHolder(servlet); - proxyHandler.addServletWithMapping(servletHolder, "/*"); - ourServer.setHandler(proxyHandler); - JettyUtil.startServer(ourServer); - ourPort = JettyUtil.getPortForStartedServer(ourServer); - - PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); - HttpClientBuilder builder = HttpClientBuilder.create(); - builder.setConnectionManager(connectionManager); - ourClient = builder.build(); - - } - public static class PatientProvider implements IResourceProvider { @Override @@ -288,4 +285,33 @@ public class ReadDstu3Test { } + public static class PlainGenericPatientProvider { + + @Read(version = true, typeName = "Patient") + public IBaseResource read(@IdParam IIdType theIdParam) { + MyPatientWithExtensions p0 = new MyPatientWithExtensions(); + p0.getMeta().getLastUpdatedElement().setValueAsString("2012-01-01T12:12:12Z"); + p0.setId(theIdParam); + if (theIdParam.hasVersionIdPart() == false) { + p0.setIdElement(p0.getIdElement().withVersion("2")); + } + p0.setDateExt(new DateType("2011-01-01")); + return p0; + } + + } + + + @BeforeAll + public static void beforeClass() throws Exception { + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); + HttpClientBuilder builder = HttpClientBuilder.create(); + builder.setConnectionManager(connectionManager); + ourClient = builder.build(); + } + + @AfterAll + public static void afterClass() throws IOException { + ourClient.close(); + } } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/SearchPreferHandlingInterceptorTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/SearchPreferHandlingInterceptorTest.java new file mode 100644 index 00000000000..012b2e30c76 --- /dev/null +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/SearchPreferHandlingInterceptorTest.java @@ -0,0 +1,151 @@ +package ca.uhn.fhir.rest.server; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; +import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum; +import ca.uhn.fhir.rest.annotation.OptionalParam; +import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.EncodingEnum; +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.gclient.StringClientParam; +import ca.uhn.fhir.rest.param.TokenAndListParam; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.interceptor.SearchPreferHandlingInterceptor; +import ca.uhn.fhir.test.utilities.server.RestfulServerExtension; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.HumanName; +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.RegisterExtension; + +import java.util.ArrayList; +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.fail; + +public class SearchPreferHandlingInterceptorTest { + + private final FhirContext myCtx = FhirContext.forCached(FhirVersionEnum.R4); + @RegisterExtension + public RestfulServerExtension myRestfulServerExtension = new RestfulServerExtension(myCtx) + .registerProvider(new DummyPatientResourceProvider()) + .withServer(t -> t.setDefaultResponseEncoding(EncodingEnum.JSON)) + .withServer(t -> t.setPagingProvider(new FifoMemoryPagingProvider(10))) + .registerInterceptor(new SearchPreferHandlingInterceptor()); + private int myPort; + private IGenericClient myClient; + + @BeforeEach + public void before() { + myClient = myRestfulServerExtension.getFhirClient(); + myPort = myRestfulServerExtension.getPort(); + } + + + @Test + public void testSearchWithInvalidParam_NoHeader() { + try { + myClient + .search() + .forResource(Patient.class) + .where(new StringClientParam("foo").matches().value("bar")) + .prettyPrint() + .returnBundle(Bundle.class) + .encodedJson() + .execute(); + fail(); + } catch (InvalidRequestException e) { + assertThat(e.getMessage(), containsString("Unknown search parameter \"foo\" for resource type \"Patient\". Valid search parameters for this search are: [identifier]")); + } + + } + + @Test + public void testSearchWithInvalidParam_StrictHeader() { + try { + myClient + .search() + .forResource(Patient.class) + .where(new StringClientParam("foo").matches().value("bar")) + .withAdditionalHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_HANDLING + "=" + Constants.HEADER_PREFER_HANDLING_STRICT) + .prettyPrint() + .returnBundle(Bundle.class) + .encodedJson() + .execute(); + fail(); + } catch (InvalidRequestException e) { + assertThat(e.getMessage(), containsString("Unknown search parameter \"foo\" for resource type \"Patient\". Valid search parameters for this search are: [identifier]")); + } + + } + + @Test + public void testSearchWithInvalidParam_UnrelatedPreferHeader() { + try { + myClient + .search() + .forResource(Patient.class) + .where(new StringClientParam("foo").matches().value("bar")) + .withAdditionalHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RETURN + "=" + Constants.HEADER_PREFER_RETURN_REPRESENTATION) + .prettyPrint() + .returnBundle(Bundle.class) + .encodedJson() + .execute(); + fail(); + } catch (InvalidRequestException e) { + assertThat(e.getMessage(), containsString("Unknown search parameter \"foo\" for resource type \"Patient\". Valid search parameters for this search are: [identifier]")); + } + + } + + @Test + public void testSearchWithInvalidParam_LenientHeader() { + Bundle outcome = myClient + .search() + .forResource(Patient.class) + .where(new StringClientParam("foo").matches().value("bar")) + .and(Patient.IDENTIFIER.exactly().codes("BLAH")) + .prettyPrint() + .returnBundle(Bundle.class) + .withAdditionalHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_HANDLING + "=" + Constants.HEADER_PREFER_HANDLING_LENIENT) + .encodedJson() + .execute(); + assertEquals(200, outcome.getTotal()); + assertEquals("http://localhost:" + myPort + "/Patient?_format=json&_pretty=true&identifier=BLAH", outcome.getLink(Constants.LINK_SELF).getUrl()); + } + + public static class DummyPatientResourceProvider implements IResourceProvider { + + @Override + public Class getResourceType() { + return Patient.class; + } + + @SuppressWarnings("rawtypes") + @Search() + public List search( + @OptionalParam(name = Patient.SP_IDENTIFIER) TokenAndListParam theIdentifiers) { + ArrayList retVal = new ArrayList<>(); + + for (int i = 0; i < 200; i++) { + Patient patient = new Patient(); + patient.getIdElement().setValue("Patient/" + i + "/_history/222"); + ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(patient, BundleEntrySearchModeEnum.INCLUDE.getCode()); + patient.addName(new HumanName().setFamily("FAMILY")); + patient.setActive(true); + retVal.add(patient); + } + return retVal; + } + + } + + +} diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/SearchR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/SearchR4Test.java index 3e8809188aa..147f77a192a 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/SearchR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/SearchR4Test.java @@ -1,6 +1,7 @@ package ca.uhn.fhir.rest.server; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum; @@ -17,6 +18,7 @@ import ca.uhn.fhir.rest.gclient.StringClientParam; import ca.uhn.fhir.rest.param.TokenAndListParam; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.test.utilities.JettyUtil; +import ca.uhn.fhir.test.utilities.server.RestfulServerExtension; import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.util.UrlUtil; import ca.uhn.fhir.validation.FhirValidator; @@ -43,6 +45,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -63,17 +66,22 @@ public class SearchR4Test { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchR4Test.class); private static CloseableHttpClient ourClient; - private static FhirContext ourCtx = FhirContext.forR4(); private static TokenAndListParam ourIdentifiers; private static String ourLastMethod; - private static int ourPort; - - private static Server ourServer; + private final FhirContext myCtx = FhirContext.forCached(FhirVersionEnum.R4); + @RegisterExtension + public RestfulServerExtension myRestfulServerExtension = new RestfulServerExtension(myCtx) + .registerProvider(new DummyPatientResourceProvider()) + .registerProvider(new DummyMedicationRequestResourceProvider()) + .withServer(t -> t.setDefaultResponseEncoding(EncodingEnum.JSON)) + .withServer(t -> t.setPagingProvider(new FifoMemoryPagingProvider(10))); + private int myPort; @BeforeEach public void before() { ourLastMethod = null; ourIdentifiers = null; + myPort = myRestfulServerExtension.getPort(); } private Bundle executeSearchAndValidateHasLinkNext(HttpGet httpGet, EncodingEnum theExpectEncoding) throws IOException { @@ -93,7 +101,7 @@ public class SearchR4Test { assertEquals(200, status.getStatusLine().getStatusCode()); EncodingEnum ct = EncodingEnum.forContentType(status.getEntity().getContentType().getValue().replaceAll(";.*", "").trim()); assertEquals(theExpectEncoding, ct); - bundle = ct.newParser(ourCtx).parseResource(Bundle.class, responseContent); + bundle = ct.newParser(myCtx).parseResource(Bundle.class, responseContent); validate(bundle); } return bundle; @@ -104,7 +112,7 @@ public class SearchR4Test { */ @Test public void testPageRequestCantTriggerSearchAccidentally() throws Exception { - HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?" + Constants.PARAM_PAGINGACTION + "=12345"); + HttpGet httpGet = new HttpGet("http://localhost:" + myPort + "/Patient?" + Constants.PARAM_PAGINGACTION + "=12345"); try (CloseableHttpResponse status = ourClient.execute(httpGet)) { String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); ourLog.info(responseContent); @@ -119,7 +127,7 @@ public class SearchR4Test { */ @Test public void testSummaryCount() throws Exception { - HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?identifier=foo%7Cbar&"+Constants.PARAM_SUMMARY + "=" + SummaryEnum.COUNT.getCode()); + HttpGet httpGet = new HttpGet("http://localhost:" + myPort + "/Patient?identifier=foo%7Cbar&" + Constants.PARAM_SUMMARY + "=" + SummaryEnum.COUNT.getCode()); Bundle bundle = executeSearch(httpGet, EncodingEnum.JSON); ourLog.info(toJson(bundle)); assertEquals(200, bundle.getTotal()); @@ -128,7 +136,6 @@ public class SearchR4Test { } - @Test public void testPagingPreservesElements() throws Exception { HttpGet httpGet; @@ -137,7 +144,7 @@ public class SearchR4Test { String linkSelf; // Initial search - httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?identifier=foo%7Cbar&_elements=name&_elements:exclude=birthDate,active"); + httpGet = new HttpGet("http://localhost:" + myPort + "/Patient?identifier=foo%7Cbar&_elements=name&_elements:exclude=birthDate,active"); bundle = executeSearchAndValidateHasLinkNext(httpGet, EncodingEnum.JSON); assertThat(toJson(bundle), not(containsString("\"active\""))); linkSelf = bundle.getLink(Constants.LINK_SELF).getUrl(); @@ -182,7 +189,7 @@ public class SearchR4Test { Bundle bundle; // No include specified - httpGet = new HttpGet("http://localhost:" + ourPort + "/MedicationRequest"); + httpGet = new HttpGet("http://localhost:" + myPort + "/MedicationRequest"); bundle = executeAndReturnBundle(httpGet); assertEquals(1, bundle.getEntry().size()); } @@ -196,7 +203,7 @@ public class SearchR4Test { Bundle bundle; // * include specified - httpGet = new HttpGet("http://localhost:" + ourPort + "/MedicationRequest?_include=" + UrlUtil.escapeUrlParam("*")); + httpGet = new HttpGet("http://localhost:" + myPort + "/MedicationRequest?_include=" + UrlUtil.escapeUrlParam("*")); bundle = executeAndReturnBundle(httpGet); assertEquals(2, bundle.getEntry().size()); } @@ -210,7 +217,7 @@ public class SearchR4Test { Bundle bundle; // MedicationRequest:medication include specified - httpGet = new HttpGet("http://localhost:" + ourPort + "/MedicationRequest?_include=" + UrlUtil.escapeUrlParam(MedicationRequest.INCLUDE_MEDICATION.getValue())); + httpGet = new HttpGet("http://localhost:" + myPort + "/MedicationRequest?_include=" + UrlUtil.escapeUrlParam(MedicationRequest.INCLUDE_MEDICATION.getValue())); bundle = executeAndReturnBundle(httpGet); assertEquals(2, bundle.getEntry().size()); @@ -221,7 +228,7 @@ public class SearchR4Test { try (CloseableHttpResponse status = ourClient.execute(theHttpGet)) { String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); assertEquals(200, status.getStatusLine().getStatusCode()); - bundle = ourCtx.newJsonParser().parseResource(Bundle.class, responseContent); + bundle = myCtx.newJsonParser().parseResource(Bundle.class, responseContent); } return bundle; } @@ -233,7 +240,7 @@ public class SearchR4Test { Bundle bundle; // Initial search - httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?identifier=foo%7Cbar&_format=" + Constants.CT_FHIR_JSON_NEW); + httpGet = new HttpGet("http://localhost:" + myPort + "/Patient?identifier=foo%7Cbar&_format=" + Constants.CT_FHIR_JSON_NEW); bundle = executeSearchAndValidateHasLinkNext(httpGet, EncodingEnum.JSON); linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl(); assertThat(linkNext, containsString("_format=" + UrlUtil.escapeUrlParam(Constants.CT_FHIR_JSON_NEW))); @@ -265,7 +272,7 @@ public class SearchR4Test { Bundle bundle; // Initial search - httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?identifier=foo%7Cbar&_format=json"); + httpGet = new HttpGet("http://localhost:" + myPort + "/Patient?identifier=foo%7Cbar&_format=json"); bundle = executeSearchAndValidateHasLinkNext(httpGet, EncodingEnum.JSON); assertThat(toJson(bundle), containsString("active")); linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl(); @@ -298,7 +305,7 @@ public class SearchR4Test { Bundle bundle; // Initial search - httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?identifier=foo%7Cbar"); + httpGet = new HttpGet("http://localhost:" + myPort + "/Patient?identifier=foo%7Cbar"); bundle = executeSearchAndValidateHasLinkNext(httpGet, EncodingEnum.JSON); linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl(); assertThat(linkNext, not(containsString("_format"))); @@ -330,7 +337,7 @@ public class SearchR4Test { Bundle bundle; // Initial search - httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?identifier=foo%7Cbar"); + httpGet = new HttpGet("http://localhost:" + myPort + "/Patient?identifier=foo%7Cbar"); httpGet.addHeader(Constants.HEADER_ACCEPT, "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8"); bundle = executeSearchAndValidateHasLinkNext(httpGet, EncodingEnum.XML); linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl(); @@ -366,7 +373,7 @@ public class SearchR4Test { Bundle bundle; // Initial search - httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?identifier=foo%7Cbar&_format=xml"); + httpGet = new HttpGet("http://localhost:" + myPort + "/Patient?identifier=foo%7Cbar&_format=xml"); bundle = executeSearchAndValidateHasLinkNext(httpGet, EncodingEnum.XML); linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl(); assertThat(linkNext, containsString("_format=xml")); @@ -393,11 +400,11 @@ public class SearchR4Test { @Test public void testSearchNormal() throws Exception { - HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?identifier=foo%7Cbar&_pretty=true"); + HttpGet httpGet = new HttpGet("http://localhost:" + myPort + "/Patient?identifier=foo%7Cbar&_pretty=true"); try (CloseableHttpResponse status = ourClient.execute(httpGet)) { String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); ourLog.info(responseContent); - validate(ourCtx.newJsonParser().parseResource(responseContent)); + validate(myCtx.newJsonParser().parseResource(responseContent)); assertEquals(200, status.getStatusLine().getStatusCode()); assertEquals("search", ourLastMethod); @@ -410,7 +417,7 @@ public class SearchR4Test { @Test public void testRequestIdGeneratedAndReturned() throws Exception { - HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?identifier=foo%7Cbar&_pretty=true"); + HttpGet httpGet = new HttpGet("http://localhost:" + myPort + "/Patient?identifier=foo%7Cbar&_pretty=true"); try (CloseableHttpResponse status = ourClient.execute(httpGet)) { assertEquals(200, status.getStatusLine().getStatusCode()); String requestId = status.getFirstHeader(Constants.HEADER_REQUEST_ID).getValue(); @@ -420,7 +427,7 @@ public class SearchR4Test { @Test public void testRequestIdSuppliedAndReturned() throws Exception { - HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?identifier=foo%7Cbar&_pretty=true"); + HttpGet httpGet = new HttpGet("http://localhost:" + myPort + "/Patient?identifier=foo%7Cbar&_pretty=true"); httpGet.addHeader(Constants.HEADER_REQUEST_ID, "help im a bug"); try (CloseableHttpResponse status = ourClient.execute(httpGet)) { assertEquals(200, status.getStatusLine().getStatusCode()); @@ -431,7 +438,7 @@ public class SearchR4Test { @Test public void testRequestIdSuppliedAndReturned_Invalid() throws Exception { - HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?identifier=foo%7Cbar&_pretty=true"); + HttpGet httpGet = new HttpGet("http://localhost:" + myPort + "/Patient?identifier=foo%7Cbar&_pretty=true"); httpGet.addHeader(Constants.HEADER_REQUEST_ID, "help i'm a bug"); try (CloseableHttpResponse status = ourClient.execute(httpGet)) { assertEquals(200, status.getStatusLine().getStatusCode()); @@ -442,13 +449,13 @@ public class SearchR4Test { @Test public void testSearchWithInvalidChain() throws Exception { - HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?identifier.chain=foo%7Cbar"); + HttpGet httpGet = new HttpGet("http://localhost:" + myPort + "/Patient?identifier.chain=foo%7Cbar"); try (CloseableHttpResponse status = ourClient.execute(httpGet)) { String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); ourLog.info(responseContent); assertEquals(400, status.getStatusLine().getStatusCode()); - OperationOutcome oo = (OperationOutcome) ourCtx.newJsonParser().parseResource(responseContent); + OperationOutcome oo = (OperationOutcome) myCtx.newJsonParser().parseResource(responseContent); assertEquals( "Invalid search parameter \"identifier.chain\". Parameter contains a chain (.chain) and chains are not supported for this parameter (chaining is only allowed on reference parameters)", oo.getIssueFirstRep().getDiagnostics()); @@ -458,7 +465,7 @@ public class SearchR4Test { @Test public void testSearchWithPostAndInvalidParameters() { - IGenericClient client = ourCtx.newRestfulGenericClient("http://localhost:" + ourPort); + IGenericClient client = myCtx.newRestfulGenericClient("http://localhost:" + myPort); LoggingInterceptor interceptor = new LoggingInterceptor(); interceptor.setLogRequestSummary(true); interceptor.setLogRequestBody(true); @@ -485,14 +492,14 @@ public class SearchR4Test { } private String toJson(Bundle theBundle) { - return ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(theBundle); + return myCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(theBundle); } protected void validate(IBaseResource theResource) { - FhirValidator validatorModule = ourCtx.newValidator(); + FhirValidator validatorModule = myCtx.newValidator(); ValidationResult result = validatorModule.validateWithResult(theResource); if (!result.isSuccessful()) { - fail(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(result.toOperationOutcome())); + fail(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(result.toOperationOutcome())); } } @@ -560,34 +567,15 @@ public class SearchR4Test { @AfterAll public static void afterClassClearContext() throws Exception { - JettyUtil.closeServer(ourServer); - TestUtil.clearAllStaticFieldsForUnitTest(); + ourClient.close(); } @BeforeAll public static void beforeClass() throws Exception { - ourServer = new Server(0); - - DummyPatientResourceProvider patientProvider = new DummyPatientResourceProvider(); - DummyMedicationRequestResourceProvider medRequestProvider = new DummyMedicationRequestResourceProvider(); - - ServletHandler proxyHandler = new ServletHandler(); - RestfulServer servlet = new RestfulServer(ourCtx); - servlet.setDefaultResponseEncoding(EncodingEnum.JSON); - servlet.setPagingProvider(new FifoMemoryPagingProvider(10)); - - servlet.setResourceProviders(patientProvider, medRequestProvider); - ServletHolder servletHolder = new ServletHolder(servlet); - proxyHandler.addServletWithMapping(servletHolder, "/*"); - ourServer.setHandler(proxyHandler); - JettyUtil.startServer(ourServer); - ourPort = JettyUtil.getPortForStartedServer(ourServer); - PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); HttpClientBuilder builder = HttpClientBuilder.create(); builder.setConnectionManager(connectionManager); ourClient = builder.build(); - } } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/FhirTerserR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/FhirTerserR4Test.java index 40be4456066..d44b17812dc 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/FhirTerserR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/FhirTerserR4Test.java @@ -6,6 +6,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.model.api.annotation.Block; import ca.uhn.fhir.parser.DataFormatException; +import com.google.common.collect.Lists; import org.hamcrest.Matchers; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseExtension; @@ -67,7 +68,116 @@ import static org.mockito.Mockito.when; public class FhirTerserR4Test { private static final Logger ourLog = LoggerFactory.getLogger(FhirTerserR4Test.class); - private FhirContext myCtx = FhirContext.forCached(FhirVersionEnum.R4); + private final FhirContext myCtx = FhirContext.forCached(FhirVersionEnum.R4); + + @Test + public void testAddElement() { + Patient patient = new Patient(); + IBase family = myCtx.newTerser().addElement(patient, "Patient.name.family"); + + assertEquals(1, patient.getName().size()); + assertSame(family, patient.getName().get(0).getFamilyElement()); + } + + @Test + public void testAddElementWithValue() { + Patient patient = new Patient(); + IBase family = myCtx.newTerser().addElement(patient, "Patient.name.family", "FOO"); + + assertEquals(1, patient.getName().size()); + assertSame(family, patient.getName().get(0).getFamilyElement()); + assertEquals("FOO", patient.getName().get(0).getFamilyElement().getValue()); + } + + @Test + public void testAddElementWithValue_NonPrimitivePath() { + Patient patient = new Patient(); + + try { + myCtx.newTerser().addElement(patient, "Patient.name", "FOO"); + fail(); + } catch (DataFormatException e) { + assertEquals("Element at path Patient.name is not a primitive datatype. Found: HumanName", e.getMessage()); + } + + } + + @Test + public void testAddElements_NonRepeatingPath() { + Patient patient = new Patient(); + + try { + myCtx.newTerser().addElements(patient, "Patient.name.family", Lists.newArrayList("FOO", "BAR")); + fail(); + } catch (DataFormatException e) { + assertEquals("Can not add multiple values at path Patient.name.family: Element does not repeat", e.getMessage()); + } + + } + + @Test + public void testAddElementReusesExisting() { + Patient patient = new Patient(); + StringType existingFamily = patient.addName().getFamilyElement(); + patient.addName().setFamily("FAM2"); + patient.addName().setFamily("FAM3"); + + IBase family = myCtx.newTerser().addElement(patient, "Patient.name.family"); + + assertEquals(3, patient.getName().size()); + assertTrue(existingFamily == patient.getName().get(0).getFamilyElement()); + assertSame(family, patient.getName().get(0).getFamilyElement()); + } + + @Test + public void testAddElementCantReuseExistingBecauseItIsNotEmpty() { + Patient patient = new Patient(); + patient.addName().setFamily("FAM1"); + patient.addName().setFamily("FAM2"); + patient.addName().setFamily("FAM3"); + + try { + myCtx.newTerser().addElement(patient, "Patient.name.family"); + fail(); + } catch (DataFormatException e) { + assertEquals("Element at path Patient.name.family is not repeatable and not empty", e.getMessage()); + } + } + + @Test + public void testAddElementInvalidPath() { + Patient patient = new Patient(); + + // So much foo.... + + try { + myCtx.newTerser().addElement(patient, "foo"); + fail(); + } catch (DataFormatException e) { + assertEquals("Invalid path foo: Element of type Patient has no child named foo. Valid names: active, address, birthDate, communication, contact, contained, deceased, extension, gender, generalPractitioner, id, identifier, implicitRules, language, link, managingOrganization, maritalStatus, meta, modifierExtension, multipleBirth, name, photo, telecom, text", e.getMessage()); + } + + try { + myCtx.newTerser().addElement(patient, "Patient.foo"); + fail(); + } catch (DataFormatException e) { + assertEquals("Invalid path Patient.foo: Element of type Patient has no child named foo. Valid names: active, address, birthDate, communication, contact, contained, deceased, extension, gender, generalPractitioner, id, identifier, implicitRules, language, link, managingOrganization, maritalStatus, meta, modifierExtension, multipleBirth, name, photo, telecom, text", e.getMessage()); + } + + try { + myCtx.newTerser().addElement(patient, "Patient.name.foo"); + fail(); + } catch (DataFormatException e) { + assertEquals("Invalid path Patient.name.foo: Element of type HumanName has no child named foo. Valid names: extension, family, given, id, period, prefix, suffix, text, use", e.getMessage()); + } + + try { + myCtx.newTerser().addElement(patient, "Patient.name.family.foo"); + fail(); + } catch (DataFormatException e) { + assertEquals("Invalid path Patient.name.family.foo: Element of type HumanName has no child named family (this is a primitive type)", e.getMessage()); + } + } @Test public void testContainResourcesWithModify() { diff --git a/hapi-fhir-structures-r5/src/main/java/org/hl7/fhir/r5/hapi/ctx/FhirServerR5.java b/hapi-fhir-structures-r5/src/main/java/org/hl7/fhir/r5/hapi/ctx/FhirServerR5.java index 318ad9e578a..5c181fa1984 100644 --- a/hapi-fhir-structures-r5/src/main/java/org/hl7/fhir/r5/hapi/ctx/FhirServerR5.java +++ b/hapi-fhir-structures-r5/src/main/java/org/hl7/fhir/r5/hapi/ctx/FhirServerR5.java @@ -2,12 +2,12 @@ package org.hl7.fhir.r5.hapi.ctx; import ca.uhn.fhir.rest.api.server.IFhirVersionServer; import ca.uhn.fhir.rest.server.RestfulServer; -import org.hl7.fhir.r5.hapi.rest.server.ServerCapabilityStatementProvider; +import ca.uhn.fhir.rest.server.provider.ServerCapabilityStatementProvider; public class FhirServerR5 implements IFhirVersionServer { - @Override - public ServerCapabilityStatementProvider createServerConformanceProvider(RestfulServer theServer) { - return new ServerCapabilityStatementProvider(); - } + @Override + public ServerCapabilityStatementProvider createServerConformanceProvider(RestfulServer theServer) { + return new ServerCapabilityStatementProvider(theServer); + } } diff --git a/hapi-fhir-structures-r5/src/main/java/org/hl7/fhir/r5/hapi/rest/server/ServerCapabilityStatementProvider.java b/hapi-fhir-structures-r5/src/main/java/org/hl7/fhir/r5/hapi/rest/server/ServerCapabilityStatementProvider.java deleted file mode 100644 index 5fd5eaa56fb..00000000000 --- a/hapi-fhir-structures-r5/src/main/java/org/hl7/fhir/r5/hapi/rest/server/ServerCapabilityStatementProvider.java +++ /dev/null @@ -1,606 +0,0 @@ -package org.hl7.fhir.r5.hapi.rest.server; - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.FhirVersionEnum; -import ca.uhn.fhir.context.RuntimeResourceDefinition; -import ca.uhn.fhir.context.RuntimeSearchParam; -import ca.uhn.fhir.parser.DataFormatException; -import ca.uhn.fhir.rest.annotation.IdParam; -import ca.uhn.fhir.rest.annotation.Metadata; -import ca.uhn.fhir.rest.annotation.Read; -import ca.uhn.fhir.rest.api.Constants; -import ca.uhn.fhir.rest.api.server.RequestDetails; -import ca.uhn.fhir.rest.server.Bindings; -import ca.uhn.fhir.rest.server.IServerConformanceProvider; -import ca.uhn.fhir.rest.server.RestfulServer; -import ca.uhn.fhir.rest.server.RestfulServerConfiguration; -import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; -import ca.uhn.fhir.rest.server.method.BaseMethodBinding; -import ca.uhn.fhir.rest.server.method.IParameter; -import ca.uhn.fhir.rest.server.method.OperationMethodBinding; -import ca.uhn.fhir.rest.server.method.OperationMethodBinding.ReturnType; -import ca.uhn.fhir.rest.server.method.OperationParameter; -import ca.uhn.fhir.rest.server.method.SearchMethodBinding; -import ca.uhn.fhir.rest.server.method.SearchParameter; -import ca.uhn.fhir.rest.server.util.BaseServerCapabilityStatementProvider; -import org.apache.commons.lang3.StringUtils; -import org.hl7.fhir.exceptions.FHIRException; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.instance.model.api.IPrimitiveType; -import org.hl7.fhir.r5.model.CapabilityStatement; -import org.hl7.fhir.r5.model.CapabilityStatement.CapabilityStatementRestComponent; -import org.hl7.fhir.r5.model.CapabilityStatement.CapabilityStatementRestResourceComponent; -import org.hl7.fhir.r5.model.CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent; -import org.hl7.fhir.r5.model.CapabilityStatement.ConditionalDeleteStatus; -import org.hl7.fhir.r5.model.CapabilityStatement.ResourceInteractionComponent; -import org.hl7.fhir.r5.model.CapabilityStatement.SystemRestfulInteraction; -import org.hl7.fhir.r5.model.CapabilityStatement.TypeRestfulInteraction; -import org.hl7.fhir.r5.model.DateTimeType; -import org.hl7.fhir.r5.model.Enumerations; -import org.hl7.fhir.r5.model.Enumerations.PublicationStatus; -import org.hl7.fhir.r5.model.IdType; -import org.hl7.fhir.r5.model.OperationDefinition; -import org.hl7.fhir.r5.model.OperationDefinition.OperationDefinitionParameterComponent; -import org.hl7.fhir.r5.model.OperationDefinition.OperationKind; -import org.hl7.fhir.r5.model.ResourceType; - -import javax.servlet.ServletContext; -import javax.servlet.http.HttpServletRequest; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.TreeSet; - -import static org.apache.commons.lang3.StringUtils.isBlank; -import static org.apache.commons.lang3.StringUtils.isNotBlank; - -/* - * #%L - * HAPI FHIR Structures - DSTU2 (FHIR v1.0.0) - * %% - * Copyright (C) 2014 - 2015 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -/** - * Server FHIR Provider which serves the conformance statement for a RESTful server implementation - * - *

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

- */ -public class ServerCapabilityStatementProvider extends BaseServerCapabilityStatementProvider implements IServerConformanceProvider { - - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServerCapabilityStatementProvider.class); - private String myPublisher = "Not provided"; - - /** - * No-arg constructor and setter so that the ServerConformanceProvider can be Spring-wired with the RestfulService avoiding the potential reference cycle that would happen. - */ - public ServerCapabilityStatementProvider() { - super(); - } - - /** - * Constructor - This is intended only for JAX-RS server - */ - public ServerCapabilityStatementProvider(RestfulServerConfiguration theServerConfiguration) { - super(theServerConfiguration); - } - - private void checkBindingForSystemOps(CapabilityStatementRestComponent rest, Set systemOps, BaseMethodBinding nextMethodBinding) { - if (nextMethodBinding.getRestOperationType() != null) { - String sysOpCode = nextMethodBinding.getRestOperationType().getCode(); - if (sysOpCode != null) { - SystemRestfulInteraction sysOp; - try { - sysOp = SystemRestfulInteraction.fromCode(sysOpCode); - } catch (FHIRException e) { - return; - } - if (sysOp == null) { - return; - } - if (systemOps.contains(sysOp) == false) { - systemOps.add(sysOp); - rest.addInteraction().setCode(sysOp); - } - } - } - } - - - private DateTimeType conformanceDate(RequestDetails theRequestDetails) { - IPrimitiveType buildDate = getServerConfiguration(theRequestDetails).getConformanceDate(); - if (buildDate != null && buildDate.getValue() != null) { - try { - return new DateTimeType(buildDate.getValueAsString()); - } catch (DataFormatException e) { - // fall through - } - } - return DateTimeType.now(); - } - - - /** - * 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; - } - - @SuppressWarnings("EnumSwitchStatementWhichMissesCases") - @Override - @Metadata - public CapabilityStatement getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) { - - RestfulServerConfiguration configuration = getServerConfiguration(theRequestDetails); - Bindings bindings = configuration.provideBindings(); - - CapabilityStatement retVal = new CapabilityStatement(); - - retVal.setPublisher(myPublisher); - retVal.setDateElement(conformanceDate(theRequestDetails)); - retVal.setFhirVersion(Enumerations.FHIRVersion.fromCode(FhirVersionEnum.R5.getFhirVersionString())); - - ServletContext servletContext = (ServletContext) (theRequest == null ? null : theRequest.getAttribute(RestfulServer.SERVLET_CONTEXT_ATTRIBUTE)); - String serverBase = configuration.getServerAddressStrategy().determineServerBase(servletContext, theRequest); - retVal - .getImplementation() - .setUrl(serverBase) - .setDescription(configuration.getImplementationDescription()); - - retVal.setKind(Enumerations.CapabilityStatementKind.INSTANCE); - retVal.getSoftware().setName(configuration.getServerName()); - retVal.getSoftware().setVersion(configuration.getServerVersion()); - retVal.addFormat(Constants.CT_FHIR_XML_NEW); - retVal.addFormat(Constants.CT_FHIR_JSON_NEW); - retVal.addFormat(Constants.FORMAT_JSON); - retVal.addFormat(Constants.FORMAT_XML); - retVal.setStatus(PublicationStatus.ACTIVE); - - CapabilityStatementRestComponent rest = retVal.addRest(); - rest.setMode(Enumerations.RestfulCapabilityMode.SERVER); - - Set systemOps = new HashSet<>(); - Set operationNames = new HashSet<>(); - - Map>> resourceToMethods = configuration.collectMethodBindings(); - Map> resourceNameToSharedSupertype = configuration.getNameToSharedSupertype(); - for (Entry>> nextEntry : resourceToMethods.entrySet()) { - - if (nextEntry.getKey().isEmpty() == false) { - Set resourceOps = new HashSet<>(); - CapabilityStatementRestResourceComponent resource = rest.addResource(); - String resourceName = nextEntry.getKey(); - RuntimeResourceDefinition def; - FhirContext context = configuration.getFhirContext(); - if (resourceNameToSharedSupertype.containsKey(resourceName)) { - def = context.getResourceDefinition(resourceNameToSharedSupertype.get(resourceName)); - } else { - def = context.getResourceDefinition(resourceName); - } - resource.getTypeElement().setValue(def.getName()); - resource.getProfileElement().setValue((def.getResourceProfile(serverBase))); - - TreeSet includes = new TreeSet<>(); - - // Map nameToSearchParam = new HashMap(); - for (BaseMethodBinding nextMethodBinding : nextEntry.getValue()) { - if (nextMethodBinding.getRestOperationType() != null) { - String resOpCode = nextMethodBinding.getRestOperationType().getCode(); - if (resOpCode != null) { - TypeRestfulInteraction resOp; - try { - resOp = TypeRestfulInteraction.fromCode(resOpCode); - } catch (Exception e) { - resOp = null; - } - if (resOp != null) { - if (resourceOps.contains(resOp) == false) { - resourceOps.add(resOp); - resource.addInteraction().setCode(resOp); - } - if ("vread".equals(resOpCode)) { - // vread implies read - resOp = TypeRestfulInteraction.READ; - if (resourceOps.contains(resOp) == false) { - resourceOps.add(resOp); - resource.addInteraction().setCode(resOp); - } - } - - if (nextMethodBinding.isSupportsConditional()) { - switch (resOp) { - case CREATE: - resource.setConditionalCreate(true); - break; - case DELETE: - if (nextMethodBinding.isSupportsConditionalMultiple()) { - resource.setConditionalDelete(ConditionalDeleteStatus.MULTIPLE); - } else { - resource.setConditionalDelete(ConditionalDeleteStatus.SINGLE); - } - break; - case UPDATE: - resource.setConditionalUpdate(true); - break; - default: - break; - } - } - } - } - } - - checkBindingForSystemOps(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)) { - rest.addOperation().setName(methodBinding.getQueryName()).setDefinition((getOperationDefinitionPrefix(theRequestDetails) + "OperationDefinition/" + queryName)); - } - } else { - handleNamelessSearchMethodBinding(resource, def, includes, (SearchMethodBinding) nextMethodBinding, theRequestDetails); - } - } else if (nextMethodBinding instanceof OperationMethodBinding) { - OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; - String opName = bindings.getOperationBindingToName().get(methodBinding); - if (operationNames.add(opName)) { - // Only add each operation (by name) once - rest.addOperation().setName(methodBinding.getName().substring(1)).setDefinition((getOperationDefinitionPrefix(theRequestDetails) + "OperationDefinition/" + opName)); - } - } - - resource.getInteraction().sort(new Comparator() { - @Override - public int compare(ResourceInteractionComponent theO1, ResourceInteractionComponent theO2) { - TypeRestfulInteraction o1 = theO1.getCode(); - TypeRestfulInteraction o2 = theO2.getCode(); - if (o1 == null && o2 == null) { - return 0; - } - if (o1 == null) { - return 1; - } - if (o2 == null) { - return -1; - } - return o1.ordinal() - o2.ordinal(); - } - }); - - } - - for (String nextInclude : includes) { - resource.addSearchInclude(nextInclude); - } - } else { - for (BaseMethodBinding nextMethodBinding : nextEntry.getValue()) { - checkBindingForSystemOps(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); - rest.addOperation().setName(methodBinding.getName().substring(1)).setDefinition((getOperationDefinitionPrefix(theRequestDetails) + "OperationDefinition/" + opName)); - } - } - } - } - } - - return retVal; - } - - protected String getOperationDefinitionPrefix(RequestDetails theRequestDetails) { - if (theRequestDetails == null) { - return ""; - } - return theRequestDetails.getServerBaseForRequest() + "/"; - } - - private void handleNamelessSearchMethodBinding(CapabilityStatementRestResourceComponent resource, RuntimeResourceDefinition def, TreeSet includes, - SearchMethodBinding searchMethodBinding, RequestDetails theRequestDetails) { - includes.addAll(searchMethodBinding.getIncludes()); - - List params = searchMethodBinding.getParameters(); - List searchParameters = new ArrayList<>(); - for (IParameter nextParameter : params) { - if ((nextParameter instanceof SearchParameter)) { - searchParameters.add((SearchParameter) nextParameter); - } - } - sortSearchParameters(searchParameters); - if (!searchParameters.isEmpty()) { - // boolean allOptional = searchParameters.get(0).isRequired() == false; - // - // OperationDefinition query = null; - // if (!allOptional) { - // RestOperation operation = rest.addOperation(); - // query = new OperationDefinition(); - // operation.setDefinition(new ResourceReferenceDt(query)); - // query.getDescriptionElement().setValue(searchMethodBinding.getDescription()); - // query.addUndeclaredExtension(false, ExtensionConstants.QUERY_RETURN_TYPE, new CodeDt(resourceName)); - // for (String nextInclude : searchMethodBinding.getIncludes()) { - // query.addUndeclaredExtension(false, ExtensionConstants.QUERY_ALLOWED_INCLUDE, new StringDt(nextInclude)); - // } - // } - - for (SearchParameter nextParameter : searchParameters) { - - String nextParamName = nextParameter.getName(); - - String chain = null; - String nextParamUnchainedName = nextParamName; - if (nextParamName.contains(".")) { - chain = nextParamName.substring(nextParamName.indexOf('.') + 1); - nextParamUnchainedName = nextParamName.substring(0, nextParamName.indexOf('.')); - } - - String nextParamDescription = nextParameter.getDescription(); - - /* - * If the parameter has no description, default to the one from the resource - */ - if (StringUtils.isBlank(nextParamDescription)) { - RuntimeSearchParam paramDef = def.getSearchParam(nextParamUnchainedName); - if (paramDef != null) { - nextParamDescription = paramDef.getDescription(); - } - } - - CapabilityStatementRestResourceSearchParamComponent param = resource.addSearchParam(); - param.setName(nextParamUnchainedName); - -// if (StringUtils.isNotBlank(chain)) { -// param.addChain(chain); -// } -// -// if (nextParameter.getParamType() == RestSearchParameterTypeEnum.REFERENCE) { -// for (String nextWhitelist : new TreeSet(nextParameter.getQualifierWhitelist())) { -// if (nextWhitelist.startsWith(".")) { -// param.addChain(nextWhitelist.substring(1)); -// } -// } -// } - - param.setDocumentation(nextParamDescription); - if (nextParameter.getParamType() != null) { - param.getTypeElement().setValueAsString(nextParameter.getParamType().getCode()); - } - for (Class nextTarget : nextParameter.getDeclaredTypes()) { - RuntimeResourceDefinition targetDef = getServerConfiguration(theRequestDetails).getFhirContext().getResourceDefinition(nextTarget); - if (targetDef != null) { - ResourceType code; - try { - code = ResourceType.fromCode(targetDef.getName()); - } catch (FHIRException e) { - code = null; - } -// if (code != null) { -// param.addTarget(targetDef.getName()); -// } - } - } - } - } - } - - - @Read(type = OperationDefinition.class) - public OperationDefinition readOperationDefinition(@IdParam IdType theId, RequestDetails theRequestDetails) { - if (theId == null || theId.hasIdPart() == false) { - throw new ResourceNotFoundException(theId); - } - RestfulServerConfiguration configuration = getServerConfiguration(theRequestDetails); - Bindings bindings = configuration.provideBindings(); - - List operationBindings = bindings.getOperationNameToBindings().get(theId.getIdPart()); - if (operationBindings != null && !operationBindings.isEmpty()) { - return readOperationDefinitionForOperation(operationBindings); - } - List searchBindings = bindings.getSearchNameToBindings().get(theId.getIdPart()); - if (searchBindings != null && !searchBindings.isEmpty()) { - return readOperationDefinitionForNamedSearch(searchBindings); - } - throw new ResourceNotFoundException(theId); - } - - private OperationDefinition readOperationDefinitionForNamedSearch(List bindings) { - OperationDefinition op = new OperationDefinition(); - op.setStatus(PublicationStatus.ACTIVE); - op.setKind(OperationKind.QUERY); - op.setAffectsState(false); - - op.setSystem(false); - op.setType(false); - op.setInstance(false); - - Set inParams = new HashSet<>(); - - for (SearchMethodBinding binding : bindings) { - if (isNotBlank(binding.getDescription())) { - op.setDescription(binding.getDescription()); - } - if (isBlank(binding.getResourceProviderResourceName())) { - op.setSystem(true); - } else { - op.setType(true); - op.addResourceElement().setValue(binding.getResourceProviderResourceName()); - } - op.setCode(binding.getQueryName()); - for (IParameter nextParamUntyped : binding.getParameters()) { - if (nextParamUntyped instanceof SearchParameter) { - SearchParameter nextParam = (SearchParameter) nextParamUntyped; - if (!inParams.add(nextParam.getName())) { - continue; - } - OperationDefinitionParameterComponent param = op.addParameter(); - param.setUse(Enumerations.OperationParameterUse.IN); - param.setType(Enumerations.FHIRAllTypes.STRING); - param.getSearchTypeElement().setValueAsString(nextParam.getParamType().getCode()); - param.setMin(nextParam.isRequired() ? 1 : 0); - param.setMax("1"); - param.setName(nextParam.getName()); - } - } - - if (isBlank(op.getName())) { - if (isNotBlank(op.getDescription())) { - op.setName(op.getDescription()); - } else { - op.setName(op.getCode()); - } - } - } - - return op; - } - - private OperationDefinition readOperationDefinitionForOperation(List bindings) { - OperationDefinition op = new OperationDefinition(); - op.setStatus(PublicationStatus.ACTIVE); - op.setKind(OperationKind.OPERATION); - op.setAffectsState(false); - - // We reset these to true below if we find a binding that can handle the level - op.setSystem(false); - op.setType(false); - op.setInstance(false); - - Set inParams = new HashSet<>(); - Set outParams = new HashSet<>(); - - for (OperationMethodBinding sharedDescription : bindings) { - if (isNotBlank(sharedDescription.getDescription())) { - op.setDescription(sharedDescription.getDescription()); - } - if (sharedDescription.isCanOperateAtInstanceLevel()) { - op.setInstance(true); - } - if (sharedDescription.isCanOperateAtServerLevel()) { - op.setSystem(true); - } - if (sharedDescription.isCanOperateAtTypeLevel()) { - op.setType(true); - } - if (!sharedDescription.isIdempotent()) { - op.setAffectsState(!sharedDescription.isIdempotent()); - } - op.setCode(sharedDescription.getName().substring(1)); - if (sharedDescription.isCanOperateAtInstanceLevel()) { - op.setInstance(sharedDescription.isCanOperateAtInstanceLevel()); - } - if (sharedDescription.isCanOperateAtServerLevel()) { - op.setSystem(sharedDescription.isCanOperateAtServerLevel()); - } - if (isNotBlank(sharedDescription.getResourceName())) { - op.addResourceElement().setValue(sharedDescription.getResourceName()); - } - - for (IParameter nextParamUntyped : sharedDescription.getParameters()) { - if (nextParamUntyped instanceof OperationParameter) { - OperationParameter nextParam = (OperationParameter) nextParamUntyped; - OperationDefinitionParameterComponent param = op.addParameter(); - if (!inParams.add(nextParam.getName())) { - continue; - } - param.setUse(Enumerations.OperationParameterUse.IN); - if (nextParam.getParamType() != null) { - param.setType(Enumerations.FHIRAllTypes.fromCode(nextParam.getParamType())); - } - if (nextParam.getSearchParamType() != null) { - param.getSearchTypeElement().setValueAsString(nextParam.getSearchParamType()); - } - param.setMin(nextParam.getMin()); - param.setMax(nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax())); - param.setName(nextParam.getName()); - } - } - - for (ReturnType nextParam : sharedDescription.getReturnParams()) { - if (!outParams.add(nextParam.getName())) { - continue; - } - OperationDefinitionParameterComponent param = op.addParameter(); - param.setUse(Enumerations.OperationParameterUse.OUT); - if (nextParam.getType() != null) { - param.setType(Enumerations.FHIRAllTypes.fromCode(nextParam.getType())); - } - param.setMin(nextParam.getMin()); - param.setMax(nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax())); - param.setName(nextParam.getName()); - } - } - - if (isBlank(op.getName())) { - if (isNotBlank(op.getDescription())) { - op.setName(op.getDescription()); - } else { - op.setName(op.getCode()); - } - } - - if (op.hasSystem() == false) { - op.setSystem(false); - } - if (op.hasInstance() == false) { - op.setInstance(false); - } - - return op; - } - - @Override - public void setRestfulServer(RestfulServer theRestfulServer) { - // ignore - } - - private void sortSearchParameters(List searchParameters) { - Collections.sort(searchParameters, new Comparator() { - @Override - public int compare(SearchParameter theO1, SearchParameter theO2) { - if (theO1.isRequired() == theO2.isRequired()) { - return theO1.getName().compareTo(theO2.getName()); - } - if (theO1.isRequired()) { - return -1; - } - return 1; - } - }); - } -} 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 1780bc2df1e..091892c2db8 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 @@ -1,6 +1,7 @@ package ca.uhn.fhir.rest.server; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.api.annotation.Description; import ca.uhn.fhir.model.api.annotation.ResourceDef; @@ -21,12 +22,12 @@ 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.ServerCapabilityStatementProvider; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.validation.ValidationResult; import com.google.common.collect.Lists; import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.r5.hapi.rest.server.ServerCapabilityStatementProvider; import org.hl7.fhir.r5.model.*; import org.hl7.fhir.r5.model.CapabilityStatement.CapabilityStatementRestComponent; import org.hl7.fhir.r5.model.CapabilityStatement.CapabilityStatementRestResourceComponent; @@ -56,6 +57,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; @@ -68,13 +70,9 @@ import static org.mockito.Mockito.when; public class ServerCapabilityStatementProviderR5Test { - private static FhirContext ourCtx; + private final FhirContext myCtx = FhirContext.forCached(FhirVersionEnum.R5); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServerCapabilityStatementProviderR5Test.class); - static { - ourCtx = FhirContext.forR5(); - } - private HttpServletRequest createHttpServletRequest() { HttpServletRequest req = mock(HttpServletRequest.class); when(req.getRequestURI()).thenReturn("/FhirStorm/fhir/Patient/_search"); @@ -106,16 +104,16 @@ public class ServerCapabilityStatementProviderR5Test { @Test public void testConditionalOperations() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new ConditionalProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); ourLog.info(conf); CapabilityStatementRestResourceComponent res = conformance.getRest().get(0).getResource().get(1); @@ -135,24 +133,24 @@ public class ServerCapabilityStatementProviderR5Test { @Test public void testExtendedOperationReturningBundle() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new ProviderWithExtendedOperationReturningBundle()); rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); + 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()); - OperationDefinition opDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-everything"), createRequestDetails(rs)); + OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-everything"), createRequestDetails(rs)); validate(opDef); assertEquals("everything", opDef.getCode()); assertThat(opDef.getSystem(), is(false)); @@ -163,19 +161,19 @@ public class ServerCapabilityStatementProviderR5Test { @Test public void testExtendedOperationReturningBundleOperation() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new ProviderWithExtendedOperationReturningBundle()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider() { + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { }; rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - OperationDefinition opDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-everything"), createRequestDetails(rs)); + OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-everything"), createRequestDetails(rs)); validate(opDef); - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef); + String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef); ourLog.info(conf); assertEquals("everything", opDef.getCode()); @@ -185,32 +183,32 @@ public class ServerCapabilityStatementProviderR5Test { @Test public void testInstanceHistorySupported() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new InstanceHistoryProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); ourLog.info(conf); - conf = ourCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(conformance); + conf = myCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(conformance); assertThat(conf, containsString("")); } @Test public void testFormatIncludesSpecialNonMediaTypeFormats() throws ServletException { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new SearchProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - CapabilityStatement serverConformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + CapabilityStatement serverConformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); List formatCodes = serverConformance.getFormat().stream().map(c -> c.getCode()).collect(Collectors.toList());; @@ -224,10 +222,10 @@ public class ServerCapabilityStatementProviderR5Test { @Test public void testMultiOptionalDocumentation() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new MultiOptionalProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); @@ -245,8 +243,8 @@ public class ServerCapabilityStatementProviderR5Test { } assertTrue(found); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); ourLog.info(conf); assertThat(conf, containsString("")); @@ -257,16 +255,16 @@ public class ServerCapabilityStatementProviderR5Test { @Test public void testNonConditionalOperations() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new NonConditionalProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); ourLog.info(conf); CapabilityStatementRestResourceComponent res = conformance.getRest().get(0).getResource().get(1); @@ -280,18 +278,18 @@ public class ServerCapabilityStatementProviderR5Test { /** See #379 */ @Test public void testOperationAcrossMultipleTypes() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new MultiTypePatientProvider(), new MultiTypeEncounterProvider()); rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); + String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); ourLog.info(conf); assertEquals(4, conformance.getRest().get(0).getOperation().size()); @@ -302,9 +300,9 @@ public class ServerCapabilityStatementProviderR5Test { assertThat(operationIdParts, containsInAnyOrder("Patient-i-someOp", "Encounter-i-someOp", "Patient-i-validate", "Encounter-i-validate")); { - OperationDefinition opDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-someOp"), createRequestDetails(rs)); + OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-someOp"), createRequestDetails(rs)); validate(opDef); - ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); + ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); Set types = toStrings(opDef.getResource()); assertEquals("someOp", opDef.getCode()); assertEquals(true, opDef.getInstance()); @@ -317,9 +315,9 @@ public class ServerCapabilityStatementProviderR5Test { assertEquals("Patient", opDef.getParameter().get(1).getType().toCode()); } { - OperationDefinition opDef = sc.readOperationDefinition(new IdType("OperationDefinition/Encounter-i-someOp"), createRequestDetails(rs)); + OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Encounter-i-someOp"), createRequestDetails(rs)); validate(opDef); - ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); + ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); Set types = toStrings(opDef.getResource()); assertEquals("someOp", opDef.getCode()); assertEquals(true, opDef.getInstance()); @@ -332,9 +330,9 @@ public class ServerCapabilityStatementProviderR5Test { assertEquals("Encounter", opDef.getParameter().get(1).getType().toCode()); } { - OperationDefinition opDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-validate"), createRequestDetails(rs)); + OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-validate"), createRequestDetails(rs)); validate(opDef); - ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); + ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); Set types = toStrings(opDef.getResource()); assertEquals("validate", opDef.getCode()); assertEquals(true, opDef.getInstance()); @@ -349,17 +347,17 @@ public class ServerCapabilityStatementProviderR5Test { @Test public void testOperationDocumentation() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new SearchProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); + String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); assertThat(conf, containsString("")); assertThat(conf, containsString("")); @@ -368,20 +366,20 @@ public class ServerCapabilityStatementProviderR5Test { @Test public void testOperationOnNoTypes() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new PlainProviderWithExtendedOperationOnNoType()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider() { + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { @Override public CapabilityStatement getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) { - return super.getServerConformance(theRequest, createRequestDetails(rs)); + return (CapabilityStatement) super.getServerConformance(theRequest, createRequestDetails(rs)); } }; rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - OperationDefinition opDef = sc.readOperationDefinition(new IdType("OperationDefinition/-is-plain"), createRequestDetails(rs)); + OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/-is-plain"), createRequestDetails(rs)); validate(opDef); assertEquals("plain", opDef.getCode()); @@ -408,24 +406,23 @@ public class ServerCapabilityStatementProviderR5Test { @Test public void testProviderWithRequiredAndOptional() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new ProviderWithRequiredAndOptional()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); ourLog.info(conf); CapabilityStatementRestComponent rest = conformance.getRest().get(0); CapabilityStatementRestResourceComponent res = rest.getResource().get(0); assertEquals("DiagnosticReport", res.getType()); - assertEquals(DiagnosticReport.SP_SUBJECT, res.getSearchParam().get(0).getName()); -// assertEquals("identifier", res.getSearchParam().get(0).getChain().get(0).getValue()); + assertEquals("subject.identifier", res.getSearchParam().get(0).getName()); assertEquals(DiagnosticReport.SP_CODE, res.getSearchParam().get(1).getName()); @@ -438,19 +435,19 @@ public class ServerCapabilityStatementProviderR5Test { @Test public void testReadAndVReadSupported() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new VreadProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); ourLog.info(conf); - conf = ourCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(conformance); + conf = myCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(conformance); assertThat(conf, containsString("")); assertThat(conf, containsString("")); } @@ -458,19 +455,19 @@ public class ServerCapabilityStatementProviderR5Test { @Test public void testReadSupported() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new ReadProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); ourLog.info(conf); - conf = ourCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(conformance); + conf = myCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(conformance); assertThat(conf, not(containsString(""))); assertThat(conf, containsString("")); } @@ -478,10 +475,10 @@ public class ServerCapabilityStatementProviderR5Test { @Test public void testSearchParameterDocumentation() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new SearchProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); @@ -502,9 +499,9 @@ public class ServerCapabilityStatementProviderR5Test { } } assertTrue(found); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); + String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); ourLog.info(conf); assertThat(conf, containsString("")); @@ -518,10 +515,10 @@ public class ServerCapabilityStatementProviderR5Test { @Test public void testSearchReferenceParameterDocumentation() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new PatientResourceProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); @@ -538,9 +535,9 @@ public class ServerCapabilityStatementProviderR5Test { } } assertTrue(found); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); + String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); ourLog.info(conf); } @@ -551,10 +548,10 @@ public class ServerCapabilityStatementProviderR5Test { @Test public void testSearchReferenceParameterWithWhitelistDocumentation() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new SearchProviderWithWhitelist()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); @@ -571,9 +568,9 @@ public class ServerCapabilityStatementProviderR5Test { } } assertTrue(found); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); + String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); ourLog.info(conf); CapabilityStatementRestResourceComponent resource = findRestResource(conformance, "Patient"); @@ -587,7 +584,7 @@ public class ServerCapabilityStatementProviderR5Test { @Test public void testSearchReferenceParameterWithList() throws Exception { - RestfulServer rsNoType = new RestfulServer(ourCtx){ + RestfulServer rsNoType = new RestfulServer(myCtx){ @Override public RestfulServerConfiguration createConfiguration() { RestfulServerConfiguration retVal = super.createConfiguration(); @@ -596,15 +593,15 @@ public class ServerCapabilityStatementProviderR5Test { } }; rsNoType.registerProvider(new SearchProviderWithListNoType()); - ServerCapabilityStatementProvider scNoType = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider scNoType = new ServerCapabilityStatementProvider(rsNoType); rsNoType.setServerConformanceProvider(scNoType); rsNoType.init(createServletConfig()); - CapabilityStatement conformance = scNoType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsNoType)); - String confNoType = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); + CapabilityStatement conformance = (CapabilityStatement) scNoType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsNoType)); + String confNoType = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); ourLog.info(confNoType); - RestfulServer rsWithType = new RestfulServer(ourCtx){ + RestfulServer rsWithType = new RestfulServer(myCtx){ @Override public RestfulServerConfiguration createConfiguration() { RestfulServerConfiguration retVal = super.createConfiguration(); @@ -613,12 +610,12 @@ public class ServerCapabilityStatementProviderR5Test { } }; rsWithType.registerProvider(new SearchProviderWithListWithType()); - ServerCapabilityStatementProvider scWithType = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider scWithType = new ServerCapabilityStatementProvider(rsWithType); rsWithType.setServerConformanceProvider(scWithType); rsWithType.init(createServletConfig()); - CapabilityStatement conformanceWithType = scWithType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsWithType)); - String confWithType = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformanceWithType); + CapabilityStatement conformanceWithType = (CapabilityStatement) scWithType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsWithType)); + String confWithType = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformanceWithType); ourLog.info(confWithType); assertEquals(confNoType, confWithType); @@ -628,38 +625,38 @@ public class ServerCapabilityStatementProviderR5Test { @Test public void testSystemHistorySupported() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new SystemHistoryProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); ourLog.info(conf); - conf = ourCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(conformance); + conf = myCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(conformance); assertThat(conf, containsString("")); } @Test public void testTypeHistorySupported() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new TypeHistoryProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); ourLog.info(conf); - conf = ourCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(conformance); + conf = myCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(conformance); assertThat(conf, containsString("")); } @@ -667,34 +664,34 @@ public class ServerCapabilityStatementProviderR5Test { @Disabled public void testValidateGeneratedStatement() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new MultiOptionalProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance)); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance)); - ValidationResult result = ourCtx.newValidator().validateWithResult(conformance); + ValidationResult result = myCtx.newValidator().validateWithResult(conformance); assertTrue(result.isSuccessful(), result.getMessages().toString()); } @Test public void testSystemLevelNamedQueryWithParameters() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new NamedQueryPlainProvider()); rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance)); + 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); @@ -703,11 +700,11 @@ public class ServerCapabilityStatementProviderR5Test { String operationReference = operationComponent.getDefinition(); assertThat(operationReference, not(nullValue())); - OperationDefinition operationDefinition = sc.readOperationDefinition(new IdType(operationReference), createRequestDetails(rs)); - ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(operationDefinition)); + 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("The operation name should be the description, if a description is set", operationDefinition.getName(), is(NamedQueryPlainProvider.DESCRIPTION)); + assertThat("The operation name should be the description, if a description is set", operationDefinition.getName(), equalTo("Search_testQuery")); assertThat(operationDefinition.getStatus(), is(PublicationStatus.ACTIVE)); assertThat(operationDefinition.getKind(), is(OperationKind.QUERY)); assertThat(operationDefinition.getDescription(), is(NamedQueryPlainProvider.DESCRIPTION)); @@ -729,27 +726,27 @@ public class ServerCapabilityStatementProviderR5Test { @Test public void testResourceLevelNamedQueryWithParameters() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new NamedQueryResourceProvider()); rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance)); + 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); String operationReference = operationComponent.getDefinition(); assertThat(operationReference, not(nullValue())); - OperationDefinition operationDefinition = sc.readOperationDefinition(new IdType(operationReference), createRequestDetails(rs)); - ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(operationDefinition)); + 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(NamedQueryResourceProvider.QUERY_NAME)); + assertThat("The operation name should be the code if no description is set", operationDefinition.getName(), equalTo("Search_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)); @@ -767,31 +764,32 @@ public class ServerCapabilityStatementProviderR5Test { CapabilityStatementRestResourceComponent patientResource = restComponent.getResource().stream() .filter(r -> patientResourceName.equals(r.getType())) - .findAny().get(); + .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(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new TypeLevelOperationProvider()); rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); + String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); ourLog.info(conf); List operations = conformance.getRest().get(0).getOperation(); assertThat(operations.size(), is(1)); assertThat(operations.get(0).getName(), is(TypeLevelOperationProvider.OPERATION_NAME)); - OperationDefinition opDef = sc.readOperationDefinition(new IdType(operations.get(0).getDefinition()), createRequestDetails(rs)); + 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)); @@ -801,16 +799,16 @@ public class ServerCapabilityStatementProviderR5Test { @Test public void testProfiledResourceStructureDefinitionLinks() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setResourceProviders(new ProfiledPatientProvider(), new MultipleProfilesPatientProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance)); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance)); List resources = conformance.getRestFirstRep().getResource(); CapabilityStatementRestResourceComponent patientResource = resources.stream() @@ -844,7 +842,7 @@ public class ServerCapabilityStatementProviderR5Test { } private void validate(OperationDefinition theOpDef) { - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(theOpDef); + String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(theOpDef); ourLog.info("Def: {}", conf); } 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 ef1accd27fb..28f504dd0f4 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 @@ -40,19 +40,24 @@ import org.junit.jupiter.api.extension.ExtensionContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; public class RestfulServerExtension implements BeforeEachCallback, AfterEachCallback { private static final Logger ourLog = LoggerFactory.getLogger(RestfulServerExtension.class); private FhirContext myFhirContext; - private Object[] myProviders; + private List myProviders = new ArrayList<>(); private FhirVersionEnum myFhirVersion; private Server myServer; private RestfulServer myServlet; private int myPort; private CloseableHttpClient myHttpClient; private IGenericClient myFhirClient; + private List> myConsumers = new ArrayList<>(); /** * Constructor @@ -60,7 +65,9 @@ public class RestfulServerExtension implements BeforeEachCallback, AfterEachCall public RestfulServerExtension(FhirContext theFhirContext, Object... theProviders) { Validate.notNull(theFhirContext); myFhirContext = theFhirContext; - myProviders = theProviders; + if (theProviders != null) { + myProviders = new ArrayList<>(Arrays.asList(theProviders)); + } } /** @@ -98,6 +105,8 @@ public class RestfulServerExtension implements BeforeEachCallback, AfterEachCall ServletHolder servletHolder = new ServletHolder(myServlet); servletHandler.addServletWithMapping(servletHolder, "/*"); + myConsumers.forEach(t -> t.accept(myServlet)); + myServer.setHandler(servletHandler); myServer.start(); myPort = JettyUtil.getPortForStartedServer(myServer); @@ -140,4 +149,26 @@ public class RestfulServerExtension implements BeforeEachCallback, AfterEachCall createContextIfNeeded(); startServer(); } + + public RestfulServerExtension registerProvider(Object theProvider) { + if (myServlet != null) { + myServlet.registerProvider(theProvider); + } else { + myProviders.add(theProvider); + } + return this; + } + + public RestfulServerExtension withServer(Consumer theConsumer) { + if (myServlet != null) { + theConsumer.accept(myServlet); + } else { + myConsumers.add(theConsumer); + } + return this; + } + + public RestfulServerExtension registerInterceptor(Object theInterceptor) { + return withServer(t -> t.getInterceptorService().registerInterceptor(theInterceptor)); + } } diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/BaseValidationSupportWrapper.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/BaseValidationSupportWrapper.java index 3bec90fd887..fb92318e462 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/BaseValidationSupportWrapper.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/BaseValidationSupportWrapper.java @@ -10,6 +10,7 @@ import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBaseResource; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.List; /** @@ -39,6 +40,12 @@ public class BaseValidationSupportWrapper extends BaseValidationSupport { return myWrap.fetchAllConformanceResources(); } + @Nullable + @Override + public List fetchAllNonBaseStructureDefinitions() { + return myWrap.fetchAllNonBaseStructureDefinitions(); + } + @Override public List fetchAllStructureDefinitions() { return myWrap.fetchAllStructureDefinitions(); diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java index 43fbfff5c6c..45e5cdb174c 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java @@ -84,6 +84,12 @@ public class CachingValidationSupport extends BaseValidationSupportWrapper imple return loadFromCache(myCache, key, t -> super.fetchAllStructureDefinitions()); } + @Override + public List fetchAllNonBaseStructureDefinitions() { + String key = "fetchAllNonBaseStructureDefinitions"; + return loadFromCache(myCache, key, t -> super.fetchAllNonBaseStructureDefinitions()); + } + @Override public T fetchResource(@Nullable Class theClass, String theUri) { return loadFromCache(myCache, "fetchResource " + theClass + " " + theUri, diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChain.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChain.java index 10ce6294421..68e01621741 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChain.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChain.java @@ -15,9 +15,12 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType; import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Set; +import java.util.function.Function; +import static org.apache.commons.lang3.StringUtils.defaultString; import static org.apache.commons.lang3.StringUtils.isBlank; public class ValidationSupportChain implements IValidationSupport { @@ -182,10 +185,19 @@ public class ValidationSupportChain implements IValidationSupport { @Override public List fetchAllStructureDefinitions() { + return doFetchStructureDefinitions(t->t.fetchAllStructureDefinitions()); + } + + @Override + public List fetchAllNonBaseStructureDefinitions() { + return doFetchStructureDefinitions(t->t.fetchAllNonBaseStructureDefinitions()); + } + + private List doFetchStructureDefinitions(Function> theFunction) { ArrayList retVal = new ArrayList<>(); Set urls = new HashSet<>(); for (IValidationSupport nextSupport : myChain) { - List allStructureDefinitions = nextSupport.fetchAllStructureDefinitions(); + List allStructureDefinitions = theFunction.apply(nextSupport); if (allStructureDefinitions != null) { for (IBaseResource next : allStructureDefinitions) { diff --git a/hapi-fhir-validation/src/test/java/ca/uhn/fhir/rest/server/PatientResourceProvider.java b/hapi-fhir-validation/src/test/java/ca/uhn/fhir/rest/server/PatientResourceProvider.java new file mode 100644 index 00000000000..f9092dab8c4 --- /dev/null +++ b/hapi-fhir-validation/src/test/java/ca/uhn/fhir/rest/server/PatientResourceProvider.java @@ -0,0 +1,191 @@ + +package ca.uhn.fhir.rest.server; + +import ca.uhn.fhir.model.api.Include; +import ca.uhn.fhir.model.api.annotation.Description; +import ca.uhn.fhir.rest.annotation.Count; +import ca.uhn.fhir.rest.annotation.IncludeParam; +import ca.uhn.fhir.rest.annotation.OptionalParam; +import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.annotation.Sort; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.SortSpec; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.param.DateRangeParam; +import ca.uhn.fhir.rest.param.ReferenceAndListParam; +import ca.uhn.fhir.rest.param.StringAndListParam; +import ca.uhn.fhir.rest.param.TokenAndListParam; +import ca.uhn.fhir.rest.param.UriAndListParam; +import org.hl7.fhir.r4.model.Organization; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Practitioner; + +import java.util.Set; + +// import ca.uhn.fhir.model.dstu.resource.Binary; +// import ca.uhn.fhir.model.dstu2.resource.Bundle; +// import ca.uhn.fhir.model.api.Bundle; + + +public class PatientResourceProvider implements IResourceProvider + { + + @Override + public Class getResourceType() { + return Patient.class; + } + + @Search() + public IBundleProvider search( + javax.servlet.http.HttpServletRequest theServletRequest, + + @Description(shortDefinition="The resource identity") + @OptionalParam(name="_id") + StringAndListParam theId, + + @Description(shortDefinition="The resource language") + @OptionalParam(name="_language") + StringAndListParam theResourceLanguage, + + @Description(shortDefinition="Search the contents of the resource's data using a fulltext search") + @OptionalParam(name=Constants.PARAM_CONTENT) + StringAndListParam theFtContent, + + @Description(shortDefinition="Search the contents of the resource's narrative using a fulltext search") + @OptionalParam(name=Constants.PARAM_TEXT) + StringAndListParam theFtText, + + @Description(shortDefinition="Search for resources which have the given tag") + @OptionalParam(name=Constants.PARAM_TAG) + TokenAndListParam theSearchForTag, + + @Description(shortDefinition="Search for resources which have the given security labels") + @OptionalParam(name=Constants.PARAM_SECURITY) + TokenAndListParam theSearchForSecurity, + + @Description(shortDefinition="Search for resources which have the given profile") + @OptionalParam(name=Constants.PARAM_PROFILE) + UriAndListParam theSearchForProfile, + + + @Description(shortDefinition="A patient identifier") + @OptionalParam(name="identifier") + TokenAndListParam theIdentifier, + + @Description(shortDefinition="A portion of either family or given name of the patient") + @OptionalParam(name="name") + StringAndListParam theName, + + @Description(shortDefinition="A portion of the family name of the patient") + @OptionalParam(name="family") + StringAndListParam theFamily, + + @Description(shortDefinition="A portion of the given name of the patient") + @OptionalParam(name="given") + StringAndListParam theGiven, + + @Description(shortDefinition="A portion of either family or given name using some kind of phonetic matching algorithm") + @OptionalParam(name="phonetic") + StringAndListParam thePhonetic, + + @Description(shortDefinition="The value in any kind of telecom details of the patient") + @OptionalParam(name="telecom") + TokenAndListParam theTelecom, + + @Description(shortDefinition="A value in a phone contact") + @OptionalParam(name="phone") + TokenAndListParam thePhone, + + @Description(shortDefinition="A value in an email contact") + @OptionalParam(name="email") + TokenAndListParam theEmail, + + @Description(shortDefinition="An address in any kind of address/part of the patient") + @OptionalParam(name="address") + StringAndListParam theAddress, + + @Description(shortDefinition="A city specified in an address") + @OptionalParam(name="address-city") + StringAndListParam theAddress_city, + + @Description(shortDefinition="A state specified in an address") + @OptionalParam(name="address-state") + StringAndListParam theAddress_state, + + @Description(shortDefinition="A postalCode specified in an address") + @OptionalParam(name="address-postalcode") + StringAndListParam theAddress_postalcode, + + @Description(shortDefinition="A country specified in an address") + @OptionalParam(name="address-country") + StringAndListParam theAddress_country, + + @Description(shortDefinition="A use code specified in an address") + @OptionalParam(name="address-use") + TokenAndListParam theAddress_use, + + @Description(shortDefinition="Gender of the patient") + @OptionalParam(name="gender") + TokenAndListParam theGender, + + @Description(shortDefinition="Language code (irrespective of use value)") + @OptionalParam(name="language") + TokenAndListParam theLanguage, + + @Description(shortDefinition="The patient's date of birth") + @OptionalParam(name="birthdate") + DateRangeParam theBirthdate, + + @Description(shortDefinition="The organization at which this person is a patient") + @OptionalParam(name="organization", targetTypes={ Organization.class } ) + ReferenceAndListParam theOrganization, + + @Description(shortDefinition="Patient's nominated care provider, could be a care manager, not the organization that manages the record") + @OptionalParam(name="careprovider", targetTypes={ Organization.class , Practitioner.class } ) + ReferenceAndListParam theCareprovider, + + @Description(shortDefinition="Whether the patient record is active") + @OptionalParam(name="active") + TokenAndListParam theActive, + + @Description(shortDefinition="The species for animal patients") + @OptionalParam(name="animal-species") + TokenAndListParam theAnimal_species, + + @Description(shortDefinition="The breed for animal patients") + @OptionalParam(name="animal-breed") + TokenAndListParam theAnimal_breed, + + @Description(shortDefinition="All patients linked to the given patient") + @OptionalParam(name="link", targetTypes={ Patient.class } ) + ReferenceAndListParam theLink, + + @Description(shortDefinition="This patient has been marked as deceased, or as a death date entered") + @OptionalParam(name="deceased") + TokenAndListParam theDeceased, + + @Description(shortDefinition="The date of death has been provided and satisfies this search value") + @OptionalParam(name="deathdate") + DateRangeParam theDeathdate, + + @IncludeParam(reverse=true) + Set theRevIncludes, + @Description(shortDefinition="Only return resources which were last updated as specified by the given range") + @OptionalParam(name="_lastUpdated") + DateRangeParam theLastUpdated, + + @IncludeParam(allow= { + "Patient:careprovider" , "Patient:link" , "Patient:organization" , "Patient:careprovider" , "Patient:link" , "Patient:organization" , "Patient:careprovider" , "Patient:link" , "Patient:organization" , "*" + }) + Set theIncludes, + + @Sort + SortSpec theSort, + + @Count + Integer theCount + ) { + return null; + } + +} diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/ServerCapabilityStatementProviderR4Test.java b/hapi-fhir-validation/src/test/java/ca/uhn/fhir/rest/server/ServerCapabilityStatementProviderR4Test.java similarity index 79% rename from hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/ServerCapabilityStatementProviderR4Test.java rename to hapi-fhir-validation/src/test/java/ca/uhn/fhir/rest/server/ServerCapabilityStatementProviderR4Test.java index 67aa2155d86..2f17ac44cea 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/ServerCapabilityStatementProviderR4Test.java +++ b/hapi-fhir-validation/src/test/java/ca/uhn/fhir/rest/server/ServerCapabilityStatementProviderR4Test.java @@ -1,6 +1,7 @@ package ca.uhn.fhir.rest.server; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.api.annotation.Description; import ca.uhn.fhir.model.api.annotation.ResourceDef; @@ -36,14 +37,14 @@ 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.ServerCapabilityStatementProvider; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 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.eclipse.jetty.server.Server; +import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.r4.hapi.rest.server.ServerCapabilityStatementProvider; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.CapabilityStatement; import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestComponent; @@ -63,9 +64,11 @@ import org.hl7.fhir.r4.model.OperationDefinition; import org.hl7.fhir.r4.model.OperationDefinition.OperationDefinitionParameterComponent; 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; @@ -99,14 +102,13 @@ public class ServerCapabilityStatementProviderR4Test { 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 static FhirContext ourCtx; - private static FhirValidator ourValidator; + private final FhirContext myCtx = FhirContext.forCached(FhirVersionEnum.R4); + private FhirValidator myValidator; - static { - ourCtx = FhirContext.forR4(); - ourValidator = ourCtx.newValidator(); - ourValidator.setValidateAgainstStandardSchema(true); - ourValidator.setValidateAgainstStandardSchematron(true); + @BeforeEach + public void before() { + myValidator = myCtx.newValidator(); + myValidator.registerValidatorModule(new FhirInstanceValidator(myCtx)); } private HttpServletRequest createHttpServletRequest() { @@ -140,18 +142,19 @@ public class ServerCapabilityStatementProviderR4Test { @Test public void testConditionalOperations() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new ConditionalProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); + 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()); @@ -170,24 +173,22 @@ public class ServerCapabilityStatementProviderR4Test { @Test public void testExtendedOperationReturningBundle() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new ProviderWithExtendedOperationReturningBundle()); rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); - ourLog.info(conf); + 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 = sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-everything"), createRequestDetails(rs)); + OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-everything"), createRequestDetails(rs)); validate(opDef); assertEquals("everything", opDef.getCode()); assertThat(opDef.getSystem(), is(false)); @@ -198,19 +199,19 @@ public class ServerCapabilityStatementProviderR4Test { @Test public void testExtendedOperationReturningBundleOperation() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new ProviderWithExtendedOperationReturningBundle()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider() { + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { }; rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - OperationDefinition opDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-everything"), createRequestDetails(rs)); + OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-everything"), createRequestDetails(rs)); validate(opDef); - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef); + String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef); ourLog.info(conf); assertEquals("everything", opDef.getCode()); @@ -220,29 +221,28 @@ public class ServerCapabilityStatementProviderR4Test { @Test public void testInstanceHistorySupported() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new InstanceHistoryProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); - ourLog.info(conf); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = validate(conformance); - conf = ourCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(conformance); + conf = myCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(conformance); assertThat(conf, containsString("")); } @Test public void testMultiOptionalDocumentation() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new MultiOptionalProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); @@ -260,9 +260,8 @@ public class ServerCapabilityStatementProviderR4Test { } assertTrue(found); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); - ourLog.info(conf); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = validate(conformance); assertThat(conf, containsString("")); assertThat(conf, containsString("")); @@ -272,17 +271,16 @@ public class ServerCapabilityStatementProviderR4Test { @Test public void testNonConditionalOperations() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new NonConditionalProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); - ourLog.info(conf); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + validate(conformance); CapabilityStatementRestResourceComponent res = conformance.getRest().get(0).getResource().get(1); assertEquals("Patient", res.getType()); @@ -297,19 +295,18 @@ public class ServerCapabilityStatementProviderR4Test { */ @Test public void testOperationAcrossMultipleTypes() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new MultiTypePatientProvider(), new MultiTypeEncounterProvider()); rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); - ourLog.info(conf); + validate(conformance); assertEquals(4, conformance.getRest().get(0).getOperation().size()); List operationNames = toOperationNames(conformance.getRest().get(0).getOperation()); @@ -319,9 +316,9 @@ public class ServerCapabilityStatementProviderR4Test { assertThat(operationIdParts, containsInAnyOrder("Patient-i-someOp", "Encounter-i-someOp", "Patient-i-validate", "Encounter-i-validate")); { - OperationDefinition opDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-someOp"), createRequestDetails(rs)); + OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-someOp"), createRequestDetails(rs)); validate(opDef); - ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); + ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); Set types = toStrings(opDef.getResource()); assertEquals("someOp", opDef.getCode()); assertEquals(true, opDef.getInstance()); @@ -334,9 +331,9 @@ public class ServerCapabilityStatementProviderR4Test { assertEquals("Patient", opDef.getParameter().get(1).getType()); } { - OperationDefinition opDef = sc.readOperationDefinition(new IdType("OperationDefinition/Encounter-i-someOp"), createRequestDetails(rs)); + OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Encounter-i-someOp"), createRequestDetails(rs)); validate(opDef); - ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); + ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); Set types = toStrings(opDef.getResource()); assertEquals("someOp", opDef.getCode()); assertEquals(true, opDef.getInstance()); @@ -349,9 +346,9 @@ public class ServerCapabilityStatementProviderR4Test { assertEquals("Encounter", opDef.getParameter().get(1).getType()); } { - OperationDefinition opDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-validate"), createRequestDetails(rs)); + OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-validate"), createRequestDetails(rs)); validate(opDef); - ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); + ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); Set types = toStrings(opDef.getResource()); assertEquals("validate", opDef.getCode()); assertEquals(true, opDef.getInstance()); @@ -366,17 +363,17 @@ public class ServerCapabilityStatementProviderR4Test { @Test public void testOperationDocumentation() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new SearchProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); + String conf = validate(conformance); assertThat(conf, containsString("")); assertThat(conf, containsString("")); @@ -385,20 +382,20 @@ public class ServerCapabilityStatementProviderR4Test { @Test public void testOperationOnNoTypes() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new PlainProviderWithExtendedOperationOnNoType()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider() { + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { @Override public CapabilityStatement getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) { - return super.getServerConformance(theRequest, createRequestDetails(rs)); + return (CapabilityStatement) super.getServerConformance(theRequest, createRequestDetails(rs)); } }; rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - OperationDefinition opDef = sc.readOperationDefinition(new IdType("OperationDefinition/-is-plain"), createRequestDetails(rs)); + OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/-is-plain"), createRequestDetails(rs)); validate(opDef); assertEquals("plain", opDef.getCode()); @@ -425,23 +422,22 @@ public class ServerCapabilityStatementProviderR4Test { @Test public void testProviderWithRequiredAndOptional() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new ProviderWithRequiredAndOptional()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); - ourLog.info(conf); + 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(DiagnosticReport.SP_SUBJECT, res.getSearchParam().get(0).getName()); + 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()); @@ -455,19 +451,17 @@ public class ServerCapabilityStatementProviderR4Test { @Test public void testReadAndVReadSupported() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new VreadProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); - ourLog.info(conf); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = validate(conformance); - conf = ourCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(conformance); assertThat(conf, containsString("")); assertThat(conf, containsString("")); } @@ -475,19 +469,19 @@ public class ServerCapabilityStatementProviderR4Test { @Test public void testReadSupported() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new ReadProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); ourLog.info(conf); - conf = ourCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(conformance); + conf = myCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(conformance); assertThat(conf, not(containsString(""))); assertThat(conf, containsString("")); } @@ -495,10 +489,10 @@ public class ServerCapabilityStatementProviderR4Test { @Test public void testSearchParameterDocumentation() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new SearchProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); @@ -519,10 +513,9 @@ public class ServerCapabilityStatementProviderR4Test { } } assertTrue(found); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); - ourLog.info(conf); + String conf = validate(conformance); assertThat(conf, containsString("")); assertThat(conf, containsString("")); @@ -532,14 +525,14 @@ public class ServerCapabilityStatementProviderR4Test { @Test public void testFormatIncludesSpecialNonMediaTypeFormats() throws ServletException { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new SearchProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - CapabilityStatement serverConformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + CapabilityStatement serverConformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); List formatCodes = serverConformance.getFormat().stream().map(c -> c.getCode()).collect(Collectors.toList()); @@ -555,10 +548,10 @@ public class ServerCapabilityStatementProviderR4Test { @Test public void testSearchReferenceParameterDocumentation() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new PatientResourceProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); @@ -575,10 +568,9 @@ public class ServerCapabilityStatementProviderR4Test { } } assertTrue(found); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); - ourLog.info(conf); + String conf = validate(conformance); } @@ -588,10 +580,10 @@ public class ServerCapabilityStatementProviderR4Test { @Test public void testSearchReferenceParameterWithWhitelistDocumentation() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new SearchProviderWithWhitelist()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); @@ -608,10 +600,9 @@ public class ServerCapabilityStatementProviderR4Test { } } assertTrue(found); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); - ourLog.info(conf); + String conf = validate(conformance); CapabilityStatementRestResourceComponent resource = findRestResource(conformance, "Patient"); @@ -624,7 +615,7 @@ public class ServerCapabilityStatementProviderR4Test { @Test public void testSearchReferenceParameterWithList() throws Exception { - RestfulServer rsNoType = new RestfulServer(ourCtx) { + RestfulServer rsNoType = new RestfulServer(myCtx) { @Override public RestfulServerConfiguration createConfiguration() { RestfulServerConfiguration retVal = super.createConfiguration(); @@ -633,15 +624,14 @@ public class ServerCapabilityStatementProviderR4Test { } }; rsNoType.registerProvider(new SearchProviderWithListNoType()); - ServerCapabilityStatementProvider scNoType = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider scNoType = new ServerCapabilityStatementProvider(rsNoType); rsNoType.setServerConformanceProvider(scNoType); rsNoType.init(createServletConfig()); - CapabilityStatement conformance = scNoType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsNoType)); - String confNoType = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); - ourLog.info(confNoType); + CapabilityStatement conformance = (CapabilityStatement) scNoType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsNoType)); + String confNoType = validate(conformance); - RestfulServer rsWithType = new RestfulServer(ourCtx) { + RestfulServer rsWithType = new RestfulServer(myCtx) { @Override public RestfulServerConfiguration createConfiguration() { RestfulServerConfiguration retVal = super.createConfiguration(); @@ -650,13 +640,12 @@ public class ServerCapabilityStatementProviderR4Test { } }; rsWithType.registerProvider(new SearchProviderWithListWithType()); - ServerCapabilityStatementProvider scWithType = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider scWithType = new ServerCapabilityStatementProvider(rsWithType); rsWithType.setServerConformanceProvider(scWithType); rsWithType.init(createServletConfig()); - CapabilityStatement conformanceWithType = scWithType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsWithType)); - String confWithType = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformanceWithType); - ourLog.info(confWithType); + CapabilityStatement conformanceWithType = (CapabilityStatement) scWithType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsWithType)); + String confWithType = validate(conformanceWithType); assertEquals(confNoType, confWithType); assertThat(confNoType, containsString("")); @@ -665,38 +654,34 @@ public class ServerCapabilityStatementProviderR4Test { @Test public void testSystemHistorySupported() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new SystemHistoryProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); - ourLog.info(conf); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = validate(conformance); - conf = ourCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(conformance); assertThat(conf, containsString("")); } @Test public void testTypeHistorySupported() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new TypeHistoryProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); - ourLog.info(conf); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = validate(conformance); - conf = ourCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(conformance); assertThat(conf, containsString("")); } @@ -720,39 +705,38 @@ public class ServerCapabilityStatementProviderR4Test { } - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new MyProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider() { + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { }; rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - CapabilityStatement opDef = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + CapabilityStatement opDef = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef); - ourLog.info(conf); + validate(opDef); CapabilityStatementRestResourceComponent resource = opDef.getRest().get(0).getResource().get(0); assertEquals("DiagnosticReport", resource.getType()); List searchParamNames = resource.getSearchParam().stream().map(t -> t.getName()).collect(Collectors.toList()); - assertThat(searchParamNames, containsInAnyOrder("patient", "date")); + assertThat(searchParamNames, containsInAnyOrder("patient.birthdate", "patient.family", "patient.given", "date")); } @Test public void testSystemLevelNamedQueryWithParameters() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new NamedQueryPlainProvider()); rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance)); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + validate(conformance); CapabilityStatementRestComponent restComponent = conformance.getRest().get(0); CapabilityStatementRestResourceOperationComponent operationComponent = restComponent.getOperation().get(0); @@ -761,11 +745,11 @@ public class ServerCapabilityStatementProviderR4Test { String operationReference = operationComponent.getDefinition(); assertThat(operationReference, not(nullValue())); - OperationDefinition operationDefinition = sc.readOperationDefinition(new IdType(operationReference), createRequestDetails(rs)); - ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(operationDefinition)); + 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("The operation name should be the description, if a description is set", operationDefinition.getName(), is(NamedQueryPlainProvider.DESCRIPTION)); + 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)); @@ -787,27 +771,27 @@ public class ServerCapabilityStatementProviderR4Test { @Test public void testResourceLevelNamedQueryWithParameters() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new NamedQueryResourceProvider()); rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance)); + 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 = sc.readOperationDefinition(new IdType(operationReference), createRequestDetails(rs)); - ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(operationDefinition)); + 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(NamedQueryResourceProvider.QUERY_NAME)); + 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)); @@ -831,25 +815,24 @@ public class ServerCapabilityStatementProviderR4Test { @Test public void testExtendedOperationAtTypeLevel() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new TypeLevelOperationProvider()); rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); - ourLog.info(conf); + validate(conformance); List operations = conformance.getRest().get(0).getOperation(); assertThat(operations.size(), is(1)); assertThat(operations.get(0).getName(), is(TypeLevelOperationProvider.OPERATION_NAME)); - OperationDefinition opDef = sc.readOperationDefinition(new IdType(operations.get(0).getDefinition()), createRequestDetails(rs)); + 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)); @@ -859,16 +842,16 @@ public class ServerCapabilityStatementProviderR4Test { @Test public void testProfiledResourceStructureDefinitionLinks() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); + RestfulServer rs = new RestfulServer(myCtx); rs.setResourceProviders(new ProfiledPatientProvider(), new MultipleProfilesPatientProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); rs.setServerConformanceProvider(sc); rs.init(createServletConfig()); - CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance)); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance)); List resources = conformance.getRestFirstRep().getResource(); CapabilityStatementRestResourceComponent patientResource = resources.stream() @@ -901,15 +884,24 @@ public class ServerCapabilityStatementProviderR4Test { return retVal; } - private void validate(OperationDefinition theOpDef) { - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(theOpDef); - ourLog.info("Def: {}", conf); + private String validate(IBaseResource theResource) { + String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(theResource); + ourLog.info("Def:\n{}", conf); - ValidationResult result = ourValidator.validateWithResult(theOpDef); - String outcome = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(result.toOperationOutcome()); + 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 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")