diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/ParserOptions.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/ParserOptions.java index 0a7aaa5892b..4115c7e49c8 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/ParserOptions.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/ParserOptions.java @@ -20,8 +20,10 @@ package ca.uhn.fhir.context; import ca.uhn.fhir.parser.IParser; +import ca.uhn.fhir.util.CollectionUtil; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -42,6 +44,8 @@ public class ParserOptions { private Set myDontStripVersionsFromReferencesAtPaths = Collections.emptySet(); private boolean myOverrideResourceIdWithBundleEntryFullUrl = true; private boolean myAutoContainReferenceTargetsWithNoId = true; + private Set myEncodeElementsForSummaryMode = null; + private Set myDontEncodeElementsForSummaryMode = null; /** * If set to {@literal true} (which is the default), contained resources may be specified by @@ -143,7 +147,7 @@ public ParserOptions setDontStripVersionsFromReferencesAtPaths(String... thePath if (thePaths == null) { setDontStripVersionsFromReferencesAtPaths((List) null); } else { - setDontStripVersionsFromReferencesAtPaths(Arrays.asList(thePaths)); + setDontStripVersionsFromReferencesAtPaths(CollectionUtil.newSet(thePaths)); } return this; } @@ -205,4 +209,119 @@ public ParserOptions setOverrideResourceIdWithBundleEntryFullUrl( myOverrideResourceIdWithBundleEntryFullUrl = theOverrideResourceIdWithBundleEntryFullUrl; return this; } + + /** + * This option specifies one or more elements that should be included when the parser is encoding + * a resource in {@link IParser#setSummaryMode(boolean) summary mode}, even if the element is not + * a part of the base FHIR specification's list of summary elements. Examples of valid values + * include: + *
    + *
  • Patient.maritalStatus - Encode the entire maritalStatus CodeableConcept, even though Patient.maritalStatus is not a summary element
  • + *
  • Patient.maritalStatus.text - Encode only the text component of the patient's maritalStatus
  • + *
  • *.text - Encode the text element on any resource (only the very first position may contain a + * wildcard)
  • + *
+ * + * @see IParser#setSummaryMode(boolean) + * @see IParser#setEncodeElements(Set) Can be used to specify these values for an individual parser instance. + * @since 7.4.0 + */ + @SuppressWarnings({"UnusedReturnValue"}) + @Nonnull + public ParserOptions setEncodeElementsForSummaryMode(@Nonnull String... theEncodeElements) { + return setEncodeElementsForSummaryMode(CollectionUtil.newSet(theEncodeElements)); + } + + /** + * This option specifies one or more elements that should be included when the parser is encoding + * a resource in {@link IParser#setSummaryMode(boolean) summary mode}, even if the element is not + * a part of the base FHIR specification's list of summary elements. Examples of valid values + * include: + *
    + *
  • Patient.maritalStatus - Encode the entire maritalStatus CodeableConcept, even though Patient.maritalStatus is not a summary element
  • + *
  • Patient.maritalStatus.text - Encode only the text component of the patient's maritalStatus
  • + *
  • *.text - Encode the text element on any resource (only the very first position may contain a + * wildcard)
  • + *
+ * + * @see IParser#setSummaryMode(boolean) + * @see IParser#setEncodeElements(Set) Can be used to specify these values for an individual parser instance. + * @since 7.4.0 + */ + @Nonnull + public ParserOptions setEncodeElementsForSummaryMode(@Nullable Collection theEncodeElements) { + Set encodeElements = null; + if (theEncodeElements != null && !theEncodeElements.isEmpty()) { + encodeElements = new HashSet<>(theEncodeElements); + } + myEncodeElementsForSummaryMode = encodeElements; + return this; + } + + /** + * @return Returns the values provided to {@link #setEncodeElementsForSummaryMode(Collection)} + * or null + * + * @since 7.4.0 + */ + @Nullable + public Set getEncodeElementsForSummaryMode() { + return myEncodeElementsForSummaryMode; + } + + /** + * This option specifies one or more elements that should be excluded when the parser is encoding + * a resource in {@link IParser#setSummaryMode(boolean) summary mode}, even if the element is + * a part of the base FHIR specification's list of summary elements. Examples of valid values + * include: + *
    + *
  • Patient.name - Do not include the patient's name
  • + *
  • Patient.name.family - Do not include the patient's family name
  • + *
  • *.name - Do not include the name element on any resource type
  • + *
+ * + * @see IParser#setSummaryMode(boolean) + * @see IParser#setDontEncodeElements(Collection) Can be used to specify these values for an individual parser instance. + * @since 7.4.0 + */ + @SuppressWarnings({"UnusedReturnValue"}) + @Nonnull + public ParserOptions setDontEncodeElementsForSummaryMode(@Nonnull String... theEncodeElements) { + return setDontEncodeElementsForSummaryMode(CollectionUtil.newSet(theEncodeElements)); + } + + /** + * This option specifies one or more elements that should be excluded when the parser is encoding + * a resource in {@link IParser#setSummaryMode(boolean) summary mode}, even if the element is + * a part of the base FHIR specification's list of summary elements. Examples of valid values + * include: + *
    + *
  • Patient.name - Do not include the patient's name
  • + *
  • Patient.name.family - Do not include the patient's family name
  • + *
  • *.name - Do not include the name element on any resource type
  • + *
+ * + * @see IParser#setSummaryMode(boolean) + * @see IParser#setDontEncodeElements(Collection) Can be used to specify these values for an individual parser instance. + * @since 7.4.0 + */ + @Nonnull + public ParserOptions setDontEncodeElementsForSummaryMode(@Nullable Collection theDontEncodeElements) { + Set dontEncodeElements = null; + if (theDontEncodeElements != null && !theDontEncodeElements.isEmpty()) { + dontEncodeElements = new HashSet<>(theDontEncodeElements); + } + myDontEncodeElementsForSummaryMode = dontEncodeElements; + return this; + } + + /** + * @return Returns the values provided to {@link #setDontEncodeElementsForSummaryMode(Collection)} + * or null + * @since 7.4.0 + */ + @Nullable + public Set getDontEncodeElementsForSummaryMode() { + return myDontEncodeElementsForSummaryMode; + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java index 9dbea4a3d7b..f0559373e0a 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java @@ -27,6 +27,7 @@ import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.context.ParserOptions; import ca.uhn.fhir.context.RuntimeChildChoiceDefinition; import ca.uhn.fhir.context.RuntimeChildContainedResources; import ca.uhn.fhir.context.RuntimeChildNarrativeDefinition; @@ -42,6 +43,7 @@ import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.util.BundleUtil; +import ca.uhn.fhir.util.CollectionUtil; import ca.uhn.fhir.util.FhirTerser; import ca.uhn.fhir.util.MetaUtil; import ca.uhn.fhir.util.UrlUtil; @@ -106,9 +108,8 @@ public abstract class BaseParser implements IParser { private FhirTerser.ContainedResources myContainedResources; private boolean myEncodeElementsAppliesToChildResourcesOnly; private final FhirContext myContext; - private List myDontEncodeElements; - private List myEncodeElements; - private Set myEncodeElementsAppliesToResourceTypes; + private Collection myDontEncodeElements; + private Collection myEncodeElements; private IIdType myEncodeForceResourceId; private IParserErrorHandler myErrorHandler; private boolean myOmitResourceId; @@ -131,52 +132,15 @@ protected FhirContext getContext() { return myContext; } - List getDontEncodeElements() { - return myDontEncodeElements; - } - @Override public IParser setDontEncodeElements(Collection theDontEncodeElements) { - if (theDontEncodeElements == null || theDontEncodeElements.isEmpty()) { - myDontEncodeElements = null; - } else { - myDontEncodeElements = - theDontEncodeElements.stream().map(EncodeContextPath::new).collect(Collectors.toList()); - } + myDontEncodeElements = theDontEncodeElements; return this; } - List getEncodeElements() { - return myEncodeElements; - } - @Override public IParser setEncodeElements(Set theEncodeElements) { - - if (theEncodeElements == null || theEncodeElements.isEmpty()) { - myEncodeElements = null; - myEncodeElementsAppliesToResourceTypes = null; - } else { - myEncodeElements = - theEncodeElements.stream().map(EncodeContextPath::new).collect(Collectors.toList()); - - myEncodeElementsAppliesToResourceTypes = new HashSet<>(); - for (String next : myEncodeElements.stream() - .map(t -> t.getPath().get(0).getName()) - .collect(Collectors.toList())) { - if (next.startsWith("*")) { - myEncodeElementsAppliesToResourceTypes = null; - break; - } - int dotIdx = next.indexOf('.'); - if (dotIdx == -1) { - myEncodeElementsAppliesToResourceTypes.add(next); - } else { - myEncodeElementsAppliesToResourceTypes.add(next.substring(0, dotIdx)); - } - } - } - + myEncodeElements = theEncodeElements; return this; } @@ -298,7 +262,7 @@ public String encodeResourceToString(IBaseResource theResource) throws DataForma @Override public final void encodeResourceToWriter(IBaseResource theResource, Writer theWriter) throws IOException, DataFormatException { - EncodeContext encodeContext = new EncodeContext(); + EncodeContext encodeContext = new EncodeContext(this, myContext.getParserOptions()); encodeResourceToWriter(theResource, theWriter, encodeContext); } @@ -321,7 +285,7 @@ public void encodeToWriter(IBase theElement, Writer theWriter) throws DataFormat } else if (theElement instanceof IPrimitiveType) { theWriter.write(((IPrimitiveType) theElement).getValueAsString()); } else { - EncodeContext encodeContext = new EncodeContext(); + EncodeContext encodeContext = new EncodeContext(this, myContext.getParserOptions()); encodeToWriter(theElement, theWriter, encodeContext); } } @@ -628,7 +592,7 @@ private boolean isStripVersionsFromReferences( return false; } - Set dontStripVersionsFromReferencesAtPaths = myDontStripVersionsFromReferencesAtPaths; + Set dontStripVersionsFromReferencesAtPaths = getDontStripVersionsFromReferencesAtPaths(); if (dontStripVersionsFromReferencesAtPaths != null && !dontStripVersionsFromReferencesAtPaths.isEmpty() && theCompositeChildElement.anyPathMatches(dontStripVersionsFromReferencesAtPaths)) { @@ -923,7 +887,7 @@ protected boolean shouldAddSubsettedTag(EncodeContext theEncodeContext) { if (isSuppressNarratives()) { return true; } - if (myEncodeElements != null) { + if (theEncodeContext.myEncodeElementPaths != null) { if (isEncodeElementsAppliesToChildResourcesOnly() && theEncodeContext.getResourcePath().size() < 2) { return false; @@ -933,8 +897,8 @@ protected boolean shouldAddSubsettedTag(EncodeContext theEncodeContext) { .getResourcePath() .get(theEncodeContext.getResourcePath().size() - 1) .getName(); - return myEncodeElementsAppliesToResourceTypes == null - || myEncodeElementsAppliesToResourceTypes.contains(currentResourceName); + return theEncodeContext.myEncodeElementsAppliesToResourceTypes == null + || theEncodeContext.myEncodeElementsAppliesToResourceTypes.contains(currentResourceName); } return false; @@ -945,14 +909,15 @@ protected boolean shouldEncodeResourceId(IBaseResource theResource, EncodeContex if (isOmitResourceId() && theEncodeContext.getPath().size() == 1) { retVal = false; } else { - if (myDontEncodeElements != null) { + if (theEncodeContext.myDontEncodeElementPaths != null) { String resourceName = myContext.getResourceType(theResource); - if (myDontEncodeElements.stream().anyMatch(t -> t.equalsPath(resourceName + ".id"))) { + if (theEncodeContext.myDontEncodeElementPaths.stream() + .anyMatch(t -> t.equalsPath(resourceName + ".id"))) { retVal = false; - } else if (myDontEncodeElements.stream().anyMatch(t -> t.equalsPath("*.id"))) { + } else if (theEncodeContext.myDontEncodeElementPaths.stream().anyMatch(t -> t.equalsPath("*.id"))) { retVal = false; } else if (theEncodeContext.getResourcePath().size() == 1 - && myDontEncodeElements.stream().anyMatch(t -> t.equalsPath("id"))) { + && theEncodeContext.myDontEncodeElementPaths.stream().anyMatch(t -> t.equalsPath("id"))) { retVal = false; } } @@ -963,19 +928,22 @@ protected boolean shouldEncodeResourceId(IBaseResource theResource, EncodeContex /** * Used for DSTU2 only */ - protected boolean shouldEncodeResourceMeta(IResource theResource) { - return shouldEncodePath(theResource, "meta"); + protected boolean shouldEncodeResourceMeta(IResource theResource, EncodeContext theEncodeContext) { + return shouldEncodePath(theResource, "meta", theEncodeContext); } /** * Used for DSTU2 only */ - protected boolean shouldEncodePath(IResource theResource, String thePath) { - if (myDontEncodeElements != null) { + protected boolean shouldEncodePath(IResource theResource, String thePath, EncodeContext theEncodeContext) { + if (theEncodeContext.myDontEncodeElementPaths != null) { String resourceName = myContext.getResourceType(theResource); - if (myDontEncodeElements.stream().anyMatch(t -> t.equalsPath(resourceName + "." + thePath))) { + if (theEncodeContext.myDontEncodeElementPaths.stream() + .anyMatch(t -> t.equalsPath(resourceName + "." + thePath))) { return false; - } else return myDontEncodeElements.stream().noneMatch(t -> t.equalsPath("*." + thePath)); + } else { + return theEncodeContext.myDontEncodeElementPaths.stream().noneMatch(t -> t.equalsPath("*." + thePath)); + } } return true; } @@ -994,16 +962,16 @@ protected void throwExceptionForUnknownChildType( b.append(" but this is not a valid type for this element"); if (nextChild instanceof RuntimeChildChoiceDefinition) { RuntimeChildChoiceDefinition choice = (RuntimeChildChoiceDefinition) nextChild; - b.append(" - Expected one of: " + choice.getValidChildTypes()); + b.append(" - Expected one of: ").append(choice.getValidChildTypes()); } throw new DataFormatException(Msg.code(1831) + b); } throw new DataFormatException(Msg.code(1832) + nextChild + " has no child of type " + theType); } - protected boolean shouldEncodeResource(String theName) { - if (myDontEncodeElements != null) { - for (EncodeContextPath next : myDontEncodeElements) { + protected boolean shouldEncodeResource(String theName, EncodeContext theEncodeContext) { + if (theEncodeContext.myDontEncodeElementPaths != null) { + for (EncodeContextPath next : theEncodeContext.myDontEncodeElementPaths) { if (next.equalsPath(theName)) { return false; } @@ -1012,11 +980,6 @@ protected boolean shouldEncodeResource(String theName) { return true; } - protected boolean isFhirVersionLessThanOrEqualTo(FhirVersionEnum theFhirVersionEnum) { - final FhirVersionEnum apiFhirVersion = myContext.getVersion().getVersion(); - return theFhirVersionEnum == apiFhirVersion || apiFhirVersion.isOlderThan(theFhirVersionEnum); - } - protected void containResourcesInReferences(IBaseResource theResource) { /* @@ -1043,7 +1006,7 @@ protected void containResourcesInReferences(IBaseResource theResource) { myContainedResources = getContext().newTerser().containResources(theResource); } - class ChildNameAndDef { + static class ChildNameAndDef { private final BaseRuntimeElementDefinition myChildDef; private final String myChildName; @@ -1066,10 +1029,40 @@ public String getChildName() { * EncodeContext is a shared state object that is passed around the * encode process */ - public class EncodeContext extends EncodeContextPath { + class EncodeContext extends EncodeContextPath { private final Map> myCompositeChildrenCache = new HashMap<>(); + private final List myEncodeElementPaths; + private final Set myEncodeElementsAppliesToResourceTypes; + private final List myDontEncodeElementPaths; + + public EncodeContext(BaseParser theParser, ParserOptions theParserOptions) { + Collection encodeElements = theParser.myEncodeElements; + Collection dontEncodeElements = theParser.myDontEncodeElements; + if (isSummaryMode()) { + encodeElements = CollectionUtil.nullSafeUnion( + encodeElements, theParserOptions.getEncodeElementsForSummaryMode()); + dontEncodeElements = CollectionUtil.nullSafeUnion( + dontEncodeElements, theParserOptions.getDontEncodeElementsForSummaryMode()); + } + + if (encodeElements == null || encodeElements.isEmpty()) { + myEncodeElementPaths = null; + } else { + myEncodeElementPaths = + encodeElements.stream().map(EncodeContextPath::new).collect(Collectors.toList()); + } + if (dontEncodeElements == null || dontEncodeElements.isEmpty()) { + myDontEncodeElementPaths = null; + } else { + myDontEncodeElementPaths = + dontEncodeElements.stream().map(EncodeContextPath::new).collect(Collectors.toList()); + } + + myEncodeElementsAppliesToResourceTypes = + ParserUtil.determineApplicableResourceTypesForTerserPaths(myEncodeElementPaths); + } - public Map> getCompositeChildrenCache() { + private Map> getCompositeChildrenCache() { return myCompositeChildrenCache; } } @@ -1161,14 +1154,14 @@ private StringBuilder buildPath() { } private boolean checkIfParentShouldBeEncodedAndBuildPath() { - List encodeElements = myEncodeElements; + List encodeElements = myEncodeContext.myEncodeElementPaths; String currentResourceName = myEncodeContext .getResourcePath() .get(myEncodeContext.getResourcePath().size() - 1) .getName(); - if (myEncodeElementsAppliesToResourceTypes != null - && !myEncodeElementsAppliesToResourceTypes.contains(currentResourceName)) { + if (myEncodeContext.myEncodeElementsAppliesToResourceTypes != null + && !myEncodeContext.myEncodeElementsAppliesToResourceTypes.contains(currentResourceName)) { encodeElements = null; } @@ -1194,7 +1187,7 @@ && shouldAddSubsettedTag(myEncodeContext)) { } private boolean checkIfParentShouldNotBeEncodedAndBuildPath() { - return checkIfPathMatchesForEncoding(myDontEncodeElements, false); + return checkIfPathMatchesForEncoding(myEncodeContext.myDontEncodeElementPaths, false); } private boolean checkIfPathMatchesForEncoding( @@ -1256,16 +1249,7 @@ public CompositeChildElement getParent() { public boolean shouldBeEncoded(boolean theContainedResource) { boolean retVal = true; - if (myEncodeElements != null) { - retVal = checkIfParentShouldBeEncodedAndBuildPath(); - } - if (retVal && myDontEncodeElements != null) { - retVal = !checkIfParentShouldNotBeEncodedAndBuildPath(); - } - if (theContainedResource) { - retVal = !notEncodeForContainedResource.contains(myDef.getElementName()); - } - if (retVal && isSummaryMode() && (getDef() == null || !getDef().isSummary())) { + if (isSummaryMode() && (getDef() == null || !getDef().isSummary())) { String resourceName = myEncodeContext.getLeafResourceName(); // Technically the spec says we shouldn't include extensions in CapabilityStatement // but we will do so because there are people who depend on this behaviour, at least @@ -1280,6 +1264,15 @@ public boolean shouldBeEncoded(boolean theContainedResource) { retVal = false; } } + if (myEncodeContext.myEncodeElementPaths != null) { + retVal = checkIfParentShouldBeEncodedAndBuildPath(); + } + if (retVal && myEncodeContext.myDontEncodeElementPaths != null) { + retVal = !checkIfParentShouldNotBeEncodedAndBuildPath(); + } + if (theContainedResource) { + retVal = !notEncodeForContainedResource.contains(myDef.getElementName()); + } return retVal; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/IParser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/IParser.java index e32b52e6269..76cefb82b08 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/IParser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/IParser.java @@ -24,6 +24,9 @@ import ca.uhn.fhir.context.ParserOptions; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.rest.api.EncodingEnum; +import ca.uhn.fhir.util.CollectionUtil; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -106,6 +109,7 @@ public interface IParser { /** * When encoding, force this resource ID to be encoded as the resource ID */ + @SuppressWarnings("UnusedReturnValue") IParser setEncodeForceResourceId(IIdType theForceResourceId); /** @@ -155,7 +159,7 @@ public interface IParser { * ID will not have an ID. *

* If the resource being encoded is a Bundle or Parameters resource, this setting only applies to the - * outer resource being encoded, not any resources contained wihthin. + * outer resource being encoded, not any resources contained within. *

* * @param theOmitResourceId Should resource IDs be omitted @@ -172,7 +176,7 @@ public interface IParser { * links. In that case, this value should be set to false. * * @return Returns the parser instance's configuration setting for stripping versions from resource references when - * encoding. This method will retun null if no value is set, in which case + * encoding. This method will return null if no value is set, in which case * the value from the {@link ParserOptions} will be used (default is true) * @see ParserOptions */ @@ -199,13 +203,29 @@ public interface IParser { /** * Is the parser in "summary mode"? See {@link #setSummaryMode(boolean)} for information * - * @see {@link #setSummaryMode(boolean)} for information + * @see #setSummaryMode(boolean) for information */ boolean isSummaryMode(); /** * If set to true (default is false) only elements marked by the FHIR specification as * being "summary elements" will be included. + *

+ * It is possible to modify the default summary mode element inclusions + * for this parser instance by invoking {@link #setEncodeElements(Set)} + * or {@link #setDontEncodeElements(Collection)}. It is also possible to + * modify the default summary mode element inclusions for all parsers + * generated for a given {@link FhirContext} by accessing + * {@link FhirContext#getParserOptions()} followed by + * {@link ParserOptions#setEncodeElementsForSummaryMode(Collection)} and/or + * {@link ParserOptions#setDontEncodeElementsForSummaryMode(Collection)}. + *

+ *

+ * For compatibility reasons with other frameworks, when encoding a + * CapabilityStatement resource in summary mode, extensions + * are always encoded, even though the FHIR Specification does not consider + * them to be summary elements. + *

* * @return Returns a reference to this parser so that method calls can be chained together */ @@ -287,16 +307,48 @@ T parseResource(Class theResourceType, InputStream * wildcard) * *

+ * Note: If {@link #setSummaryMode(boolean)} is set to true, then any + * elements specified using this method will be excluded even if they are + * summary elements. + *

+ *

* DSTU2 note: Note that values including meta, such as Patient.meta - * will work for DSTU2 parsers, but values with subelements on meta such + * will work for DSTU2 parsers, but values with sub-elements on meta such * as Patient.meta.lastUpdated will only work in * DSTU3+ mode. *

* - * @param theDontEncodeElements The elements to encode + * @param theDontEncodeElements The elements to not encode, or null * @see #setEncodeElements(Set) + * @see ParserOptions#setDontEncodeElementsForSummaryMode(Collection) + */ + IParser setDontEncodeElements(@Nullable Collection theDontEncodeElements); + + /** + * If provided, specifies the elements which should NOT be encoded. Valid values for this + * field would include: + *
    + *
  • Patient - Don't encode patient and all its children
  • + *
  • Patient.name - Don't encode the patient's name
  • + *
  • Patient.name.family - Don't encode the patient's family name
  • + *
  • *.text - Don't encode the text element on any resource (only the very first position may contain a + * wildcard)
  • + *
+ *

+ * DSTU2 note: Note that values including meta, such as Patient.meta + * will work for DSTU2 parsers, but values with sub-elements on meta such + * as Patient.meta.lastUpdated will only work in + * DSTU3+ mode. + *

+ * + * @param theDontEncodeElements The elements to not encode. Can be an empty list, but must not be null. + * @see #setDontEncodeElements(Collection) + * @see ParserOptions#setDontEncodeElementsForSummaryMode(Collection) + * @since 7.4.0 */ - IParser setDontEncodeElements(Collection theDontEncodeElements); + default IParser setDontEncodeElements(@Nonnull String... theDontEncodeElements) { + return setDontEncodeElements(CollectionUtil.newSet(theDontEncodeElements)); + } /** * If provided, specifies the elements which should be encoded, to the exclusion of all others. Valid values for this @@ -309,11 +361,44 @@ T parseResource(Class theResourceType, InputStream * wildcard) *
  • *.(mandatory) - This is a special case which causes any mandatory fields (min > 0) to be encoded
  • * + *

    + * Note: If {@link #setSummaryMode(boolean)} is set to true, then any + * elements specified using this method will be included even if they are not + * summary elements. + *

    * - * @param theEncodeElements The elements to encode + * @param theEncodeElements The elements to encode, or null * @see #setDontEncodeElements(Collection) + * @see #setEncodeElements(String...) + * @see ParserOptions#setEncodeElementsForSummaryMode(Collection) + */ + IParser setEncodeElements(@Nullable Set theEncodeElements); + + /** + * If provided, specifies the elements which should be encoded, to the exclusion of all others. Valid values for this + * field would include: + *
      + *
    • Patient - Encode patient and all its children
    • + *
    • Patient.name - Encode only the patient's name
    • + *
    • Patient.name.family - Encode only the patient's family name
    • + *
    • *.text - Encode the text element on any resource (only the very first position may contain a + * wildcard)
    • + *
    • *.(mandatory) - This is a special case which causes any mandatory fields (min > 0) to be encoded
    • + *
    + *

    + * Note: If {@link #setSummaryMode(boolean)} is set to true, then any + * elements specified using this method will be included even if they are not + * summary elements. + *

    + * + * @param theEncodeElements The elements to encode. Can be an empty list, but must not be null. + * @since 7.4.0 + * @see #setEncodeElements(Set) + * @see ParserOptions#setEncodeElementsForSummaryMode(String...) */ - IParser setEncodeElements(Set theEncodeElements); + default IParser setEncodeElements(@Nonnull String... theEncodeElements) { + return setEncodeElements(CollectionUtil.newSet(theEncodeElements)); + } /** * If set to true (default is false), the values supplied @@ -351,7 +436,7 @@ T parseResource(Class theResourceType, InputStream * Sets the server's base URL used by this parser. If a value is set, resource references will be turned into * relative references if they are provided as absolute URLs but have a base matching the given base. * - * @param theUrl The base URL, e.g. "http://example.com/base" + * @param theUrl The base URL, e.g. "http://example.com/base" * @return Returns an instance of this parser so that method calls can be chained together */ IParser setServerBaseUrl(String theUrl); @@ -378,7 +463,7 @@ T parseResource(Class theResourceType, InputStream /** * Returns the value supplied to {@link IParser#setDontStripVersionsFromReferencesAtPaths(String...)} * or null if no value has been set for this parser (in which case the default from - * the {@link ParserOptions} will be used} + * the {@link ParserOptions} will be used). * * @see #setDontStripVersionsFromReferencesAtPaths(String...) * @see #setStripVersionsFromReferences(Boolean) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java index e9e89ff286d..0b25304ee5f 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java @@ -284,6 +284,8 @@ private void encodeChildElementToStreamWriter( throws IOException { switch (theChildDef.getChildType()) { + case EXTENSION_DECLARED: + break; case ID_DATATYPE: { IIdType value = (IIdType) theNextValue; String encodedValue = "id".equals(theChildName) ? value.getIdPart() : value.getValue(); @@ -797,7 +799,7 @@ private void encodeCompositeElementChildrenToStreamWriter( private boolean isSupportsFhirComment() { if (myIsSupportsFhirComment == null) { - myIsSupportsFhirComment = isFhirVersionLessThanOrEqualTo(FhirVersionEnum.DSTU2_1); + myIsSupportsFhirComment = !getContext().getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU2_1); } return myIsSupportsFhirComment; } @@ -836,7 +838,7 @@ public void encodeResourceToJsonLikeWriter(IBaseResource theResource, BaseJsonLi + theResource.getStructureFhirVersionEnum()); } - EncodeContext encodeContext = new EncodeContext(); + EncodeContext encodeContext = new EncodeContext(this, getContext().getParserOptions()); String resourceName = getContext().getResourceType(theResource); encodeContext.pushPath(resourceName, true); doEncodeResourceToJsonLikeWriter(theResource, theJsonLikeWriter, encodeContext); @@ -887,7 +889,7 @@ private void encodeResourceToJsonStreamWriter( EncodeContext theEncodeContext) throws IOException { - if (!super.shouldEncodeResource(theResDef.getName())) { + if (!super.shouldEncodeResource(theResDef.getName(), theEncodeContext)) { return; } @@ -973,15 +975,15 @@ private void parseMetaForDSTU2( } List, Object>> extensionMetadataKeys = getExtensionMetadataKeys(resource); - if (super.shouldEncodeResourceMeta(resource) + if (super.shouldEncodeResourceMeta(resource, theEncodeContext) && (ElementUtil.isEmpty(versionIdPart, updated, securityLabels, tags, profiles) == false) || !extensionMetadataKeys.isEmpty()) { beginObject(theEventWriter, "meta"); - if (shouldEncodePath(resource, "meta.versionId")) { + if (shouldEncodePath(resource, "meta.versionId", theEncodeContext)) { writeOptionalTagWithTextNode(theEventWriter, "versionId", versionIdPart); } - if (shouldEncodePath(resource, "meta.lastUpdated")) { + if (shouldEncodePath(resource, "meta.lastUpdated", theEncodeContext)) { writeOptionalTagWithTextNode(theEventWriter, "lastUpdated", updated); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/ParserUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/ParserUtil.java new file mode 100644 index 00000000000..eea920b7cf8 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/ParserUtil.java @@ -0,0 +1,38 @@ +package ca.uhn.fhir.parser; + +import ca.uhn.fhir.parser.path.EncodeContextPath; +import jakarta.annotation.Nullable; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class ParserUtil { + + /** Non instantiable */ + private ParserUtil() {} + + public static @Nullable Set determineApplicableResourceTypesForTerserPaths( + @Nullable List encodeElements) { + Set encodeElementsAppliesToResourceTypes = null; + if (encodeElements != null) { + encodeElementsAppliesToResourceTypes = new HashSet<>(); + for (String next : encodeElements.stream() + .map(t -> t.getPath().get(0).getName()) + .collect(Collectors.toList())) { + if (next.startsWith("*")) { + encodeElementsAppliesToResourceTypes = null; + break; + } + int dotIdx = next.indexOf('.'); + if (dotIdx == -1) { + encodeElementsAppliesToResourceTypes.add(next); + } else { + encodeElementsAppliesToResourceTypes.add(next.substring(0, dotIdx)); + } + } + } + return encodeElementsAppliesToResourceTypes; + } +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/RDFParser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/RDFParser.java index 05a3e13c3d4..b75ee9c897f 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/RDFParser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/RDFParser.java @@ -345,12 +345,12 @@ private Model encodeChildElementToStreamWriter( final BaseRuntimeElementDefinition childDef, final boolean includedResource, final CompositeChildElement parent, - final EncodeContext encodeContext, + final EncodeContext theEncodeContext, final Integer cardinalityIndex) { String childGenericName = childDefinition.getElementName(); - encodeContext.pushPath(childGenericName, false); + theEncodeContext.pushPath(childGenericName, false); try { if (element == null || element.isEmpty()) { @@ -412,8 +412,8 @@ private Model encodeChildElementToStreamWriter( rdfModel, extensionResource, false, - new CompositeChildElement(resDef, encodeContext), - encodeContext); + new CompositeChildElement(resDef, theEncodeContext), + theEncodeContext); } } } @@ -429,7 +429,7 @@ private Model encodeChildElementToStreamWriter( String idPredicate = null; if (element instanceof IBaseResource) { idPredicate = FHIR_NS + RESOURCE_ID; - IIdType resourceId = processResourceID((IBaseResource) element, encodeContext); + IIdType resourceId = processResourceID((IBaseResource) element, theEncodeContext); if (resourceId != null) { idString = resourceId.getIdPart(); } @@ -444,7 +444,7 @@ private Model encodeChildElementToStreamWriter( rdfModel.createProperty(idPredicate), createFhirValueBlankNode(rdfModel, idString)); } rdfModel = encodeCompositeElementToStreamWriter( - resource, element, rdfModel, rdfResource, includedResource, parent, encodeContext); + resource, element, rdfModel, rdfResource, includedResource, parent, theEncodeContext); break; } case CONTAINED_RESOURCE_LIST: @@ -465,7 +465,7 @@ private Model encodeChildElementToStreamWriter( rdfModel, true, super.fixContainedResourceId(resourceId.getValue()), - encodeContext, + theEncodeContext, false, containedResource); } @@ -474,13 +474,14 @@ private Model encodeChildElementToStreamWriter( case RESOURCE: { IBaseResource baseResource = (IBaseResource) element; String resourceName = getContext().getResourceType(baseResource); - if (!super.shouldEncodeResource(resourceName)) { + if (!super.shouldEncodeResource(resourceName, theEncodeContext)) { break; } - encodeContext.pushPath(resourceName, true); - IIdType resourceId = processResourceID(resource, encodeContext); - encodeResourceToRDFStreamWriter(resource, rdfModel, false, resourceId, encodeContext, false, null); - encodeContext.popPath(); + theEncodeContext.pushPath(resourceName, true); + IIdType resourceId = processResourceID(resource, theEncodeContext); + encodeResourceToRDFStreamWriter( + resource, rdfModel, false, resourceId, theEncodeContext, false, null); + theEncodeContext.popPath(); break; } case PRIMITIVE_XHTML: @@ -502,7 +503,7 @@ private Model encodeChildElementToStreamWriter( } } } finally { - encodeContext.popPath(); + theEncodeContext.popPath(); } return rdfModel; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/XmlParser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/XmlParser.java index a7f9257d787..7a5aaa021bf 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/XmlParser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/XmlParser.java @@ -372,7 +372,7 @@ private void encodeChildElementToStreamWriter( case RESOURCE: { IBaseResource resource = (IBaseResource) theElement; String resourceName = getContext().getResourceType(resource); - if (!super.shouldEncodeResource(resourceName)) { + if (!super.shouldEncodeResource(resourceName, theEncodeContext)) { break; } theEventWriter.writeStartElement(theChildName); @@ -736,14 +736,14 @@ private void encodeResourceToXmlStreamWriter( TagList tags = getMetaTagsForEncoding((resource), theEncodeContext); - if (super.shouldEncodeResourceMeta(resource) + if (super.shouldEncodeResourceMeta(resource, theEncodeContext) && ElementUtil.isEmpty(versionIdPart, updated, securityLabels, tags, profiles) == false) { theEventWriter.writeStartElement("meta"); - if (shouldEncodePath(resource, "meta.versionId")) { + if (shouldEncodePath(resource, "meta.versionId", theEncodeContext)) { writeOptionalTagWithValue(theEventWriter, "versionId", versionIdPart); } if (updated != null) { - if (shouldEncodePath(resource, "meta.lastUpdated")) { + if (shouldEncodePath(resource, "meta.lastUpdated", theEncodeContext)) { writeOptionalTagWithValue(theEventWriter, "lastUpdated", updated.getValueAsString()); } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/CollectionUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/CollectionUtil.java index 9c57c40c1b4..65c3ca4e7af 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/CollectionUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/CollectionUtil.java @@ -19,17 +19,76 @@ */ package ca.uhn.fhir.util; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import org.apache.commons.collections4.CollectionUtils; + +import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.Set; +import static java.util.Collections.unmodifiableCollection; + public class CollectionUtil { - public static Set newSet(T... theValues) { - HashSet retVal = new HashSet(); + /** + * Non instantiable + */ + private CollectionUtil() { + // nothing + } - for (T t : theValues) { - retVal.add(t); + /** + * Returns an immutable union of both collections. If either or both arguments are + * null they will be treated as an empty collection, meaning + * that even if both arguments are null, an empty immutable + * collection will be returned. + *

    + * DO NOT use this method if the underlying collections can be changed + * after calling this method, as the behaviour is indeterminate. + *

    + * + * @param theCollection0 The first set in the union, or null. + * @param theCollection1 The second set in the union, or null. + * @return Returns a union of both collections. Will not return null ever. + * @since 7.4.0 + */ + @Nonnull + public static Collection nullSafeUnion( + @Nullable Collection theCollection0, @Nullable Collection theCollection1) { + Collection collection0 = theCollection0; + if (collection0 != null && collection0.isEmpty()) { + collection0 = null; + } + Collection collection1 = theCollection1; + if (collection1 != null && collection1.isEmpty()) { + collection1 = null; + } + if (collection0 == null && collection1 == null) { + return Collections.emptySet(); } - return retVal; + if (collection0 == null) { + return unmodifiableCollection(collection1); + } + if (collection1 == null) { + return unmodifiableCollection(collection0); + } + return CollectionUtils.union(collection0, collection1); + } + + /** + * This method is equivalent to Set.of(...) but is kept here + * and used instead of that method because Set.of is not present on Android + * SDKs (at least up to 29). + *

    + * Sets returned by this method are unmodifiable. + *

    + */ + @SuppressWarnings("unchecked") + public static Set newSet(T... theValues) { + HashSet retVal = new HashSet<>(); + Collections.addAll(retVal, theValues); + return Collections.unmodifiableSet(retVal); } } diff --git a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/SearchParameter.java b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/SearchParameter.java index 5bbafbef11e..f5446841504 100644 --- a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/SearchParameter.java +++ b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/SearchParameter.java @@ -90,8 +90,8 @@ public class SearchParameter extends BaseQueryParameter { private static final String EMPTY_STRING = ""; - private static HashMap> ourParamQualifiers; - private static HashMap, RestSearchParameterTypeEnum> ourParamTypes; + private static final HashMap> ourParamQualifiers; + private static final HashMap, RestSearchParameterTypeEnum> ourParamTypes; static final String QUALIFIER_ANY_TYPE = ":*"; static { diff --git a/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/Parser.java b/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/Parser.java index eefcae59fc6..ab0c2cbbe48 100644 --- a/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/Parser.java +++ b/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/Parser.java @@ -20,15 +20,18 @@ package ca.uhn.hapi.fhir.docs; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.ParserOptions; import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.parser.IParser; import com.google.common.collect.Sets; +import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.Patient; import java.io.IOException; public class Parser { + @SuppressWarnings("unused") public static void main(String[] args) throws DataFormatException, IOException { { @@ -117,6 +120,36 @@ public static void main(String[] args) throws DataFormatException, IOException { System.out.println(serialized); // END SNIPPET: encodingConfig } + { + // Create a FHIR context + FhirContext ctx = FhirContext.forR4(); + Patient patient = new Patient(); + patient.addName().setFamily("Simpson").addGiven("James"); + + // START SNIPPET: encodingSummary + // Create a parser + IParser parser = ctx.newJsonParser(); + + // Instruct the parser to only include summary elements + parser.setSummaryMode(true); + + // If you need to, you can instruct the parser to override + // the default summary elements by adding and/or removing + // elements from the list of elements it will include. This + // is typically not needed, but it's shown here in case you + // need to do this: + // Include a non-summary element in the summary view. + parser.setEncodeElements("Patient.maritalStatus"); + // Exclude a summary element even though it would normally + // be included. + parser.setDontEncodeElements("Patient.name"); + + // Serialize it + String serialized = parser.encodeResourceToString(patient); + System.out.println(serialized); + // END SNIPPET: encodingSummary + } + { // START SNIPPET: disableStripVersions FhirContext ctx = FhirContext.forR4(); @@ -148,5 +181,37 @@ public static void main(String[] args) throws DataFormatException, IOException { // END SNIPPET: disableStripVersionsField } + + { + IBaseResource patient = new Patient(); + + // START SNIPPET: globalParserConfig + FhirContext ctx = FhirContext.forR4(); + + // Request the ParserOptions, which store global config + // settings applied to all parsers coming from the given + // context. + ParserOptions parserOptions = ctx.getParserOptions(); + + // Never strip resource reference versions for the following + // paths + parserOptions.setDontStripVersionsFromReferencesAtPaths( + "AuditEvent.entity.reference", "Patient.managingOrganization"); + + // Never strip any resource reference versions (setting this + // to false would make the setting above redundant since this + // setting applies to all paths) + parserOptions.setStripVersionsFromReferences(false); + + // Even in summary mode, always include extensions on the + // root of Patient resources. + parserOptions.setEncodeElementsForSummaryMode("Patient.extension"); + + // Create a parser and encode, with the global config applied. + IParser parser = ctx.newJsonParser(); + String encoded = parser.encodeResourceToString(patient); + // END SNIPPET: globalParserConfig + + } } } diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_4_0/5871-allow-overriding-parser-summary-elements.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_4_0/5871-allow-overriding-parser-summary-elements.yaml new file mode 100644 index 00000000000..101b1fbc298 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_4_0/5871-allow-overriding-parser-summary-elements.yaml @@ -0,0 +1,7 @@ +--- +type: add +issue: 5871 +title: "When encoding resources in summary mode, it is now possible to override the + built-in list of summary elements, by adding additional elements and/or by + removing elements from the default list. This can be done for an individual parser + instance, or globally using the ParserOptions object available from the FhirContext." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/model/parsers.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/model/parsers.md index f1b6699eb01..34312447c4f 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/model/parsers.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/model/parsers.md @@ -26,16 +26,39 @@ The following example shows a JSON Parser being used to serialize a FHIR resourc By default, the parser will output in condensed form, with no newlines or indenting. This is good for machine-to-machine communication since it reduces the amount of data to be transferred but it is harder to read. To enable pretty printed output: +When using the [HAPI FHIR Server](../server_plain/), pretty printing can be requested by adding the parameter _pretty=true to the request. + ```java {{snippet:classpath:/ca/uhn/hapi/fhir/docs/Parser.java|encodingPretty}} ``` ## Encoding Configuration -There are plenty of other options too that can be used to control the output by the parser. A few examples are shown below. See the [IParser](/apidocs/hapi-fhir-base/ca/uhn/fhir/parser/IParser.html) JavaDoc for more information. +There are plenty of other options too, that can be used to control the output by the parser. A few examples are shown below. See the [IParser](/apidocs/hapi-fhir-base/ca/uhn/fhir/parser/IParser.html) JavaDoc for more information. ```java {{snippet:classpath:/ca/uhn/hapi/fhir/docs/Parser.java|encodingConfig}} ``` - +## Summary Mode + +For each resource type, the FHIR specification defines a collection of elements which are considered "summary elements". These are marked on the individual resource views using a Sigma (Σ) symbol next to the element names. See the [Patient Resource Definition](https://hl7.org/fhir/patient.html) for an example, looking for +this symbol on the page. + +If the parser is configured as shown below, only the summary mode elements will be included in the encoded resource. + +When using the [HAPI FHIR Server](../server_plain/), summary mode can be requested by adding the parameter _summary=true to the request. + +```java +{{snippet:classpath:/ca/uhn/hapi/fhir/docs/Parser.java|encodingSummary}} +``` + + + +# Global Parser Configuration + +It is possible to configure a number of parser settings globally for a given FhirContext, meaning that they will apply to all parsers that are created by that context. This is especially useful for [HAPI FHIR Clients](../client/) and [HAPI FHIR Servers](../server_plain/), where parsers are created by the client/server internally using the given FhirContext. + +```java +{{snippet:classpath:/ca/uhn/hapi/fhir/docs/Parser.java|globalParserConfig}} +``` diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/util/CollectionUtilTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/util/CollectionUtilTest.java new file mode 100644 index 00000000000..13e0426ebce --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/util/CollectionUtilTest.java @@ -0,0 +1,27 @@ +package ca.uhn.fhir.util; + +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Set; + +import static ca.uhn.fhir.util.CollectionUtil.nullSafeUnion; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; + +class CollectionUtilTest { + + @Test + void testNullSafeUnion() { + assertThat(nullSafeUnion(null, null), empty()); + assertThat(nullSafeUnion(Set.of(), Set.of()), empty()); + assertThat(nullSafeUnion(Set.of("A"), null), containsInAnyOrder("A")); + assertThat(nullSafeUnion(Set.of("A"), Set.of()), containsInAnyOrder("A")); + assertThat(nullSafeUnion(null, Set.of("B")), containsInAnyOrder("B")); + assertThat(nullSafeUnion(Set.of(), Set.of("B")), containsInAnyOrder("B")); + assertThat(nullSafeUnion(Set.of("A"), Set.of("B")), containsInAnyOrder("A", "B")); + assertThat(nullSafeUnion(List.of("A"), Set.of("B")), containsInAnyOrder("A", "B")); + } + +} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchParameter.java index 89d16be4e83..a5c23032939 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchParameter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchParameter.java @@ -74,7 +74,6 @@ import ca.uhn.fhir.rest.param.binder.StringBinder; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.util.CollectionUtil; import ca.uhn.fhir.util.ReflectionUtil; import org.apache.commons.lang3.builder.ToStringBuilder; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -105,30 +104,27 @@ public class SearchParameter extends BaseQueryParameter { ourParamTypes.put(StringParam.class, RestSearchParameterTypeEnum.STRING); ourParamTypes.put(StringOrListParam.class, RestSearchParameterTypeEnum.STRING); ourParamTypes.put(StringAndListParam.class, RestSearchParameterTypeEnum.STRING); - ourParamQualifiers.put( - RestSearchParameterTypeEnum.STRING, - CollectionUtil.newSet( - Constants.PARAMQUALIFIER_STRING_EXACT, - Constants.PARAMQUALIFIER_STRING_CONTAINS, - Constants.PARAMQUALIFIER_MISSING, - EMPTY_STRING)); + ourParamQualifiers.put(RestSearchParameterTypeEnum.STRING, Set.of(new String[] { + Constants.PARAMQUALIFIER_STRING_EXACT, + Constants.PARAMQUALIFIER_STRING_CONTAINS, + Constants.PARAMQUALIFIER_MISSING, + EMPTY_STRING + })); ourParamTypes.put(UriParam.class, RestSearchParameterTypeEnum.URI); ourParamTypes.put(UriOrListParam.class, RestSearchParameterTypeEnum.URI); ourParamTypes.put(UriAndListParam.class, RestSearchParameterTypeEnum.URI); // TODO: are these right for URI? - ourParamQualifiers.put( - RestSearchParameterTypeEnum.URI, - CollectionUtil.newSet( - Constants.PARAMQUALIFIER_STRING_EXACT, Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING)); + ourParamQualifiers.put(RestSearchParameterTypeEnum.URI, Set.of(new String[] { + Constants.PARAMQUALIFIER_STRING_EXACT, Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING + })); ourParamTypes.put(TokenParam.class, RestSearchParameterTypeEnum.TOKEN); ourParamTypes.put(TokenOrListParam.class, RestSearchParameterTypeEnum.TOKEN); ourParamTypes.put(TokenAndListParam.class, RestSearchParameterTypeEnum.TOKEN); - ourParamQualifiers.put( - RestSearchParameterTypeEnum.TOKEN, - CollectionUtil.newSet( - Constants.PARAMQUALIFIER_TOKEN_TEXT, Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING)); + ourParamQualifiers.put(RestSearchParameterTypeEnum.TOKEN, Set.of(new String[] { + Constants.PARAMQUALIFIER_TOKEN_TEXT, Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING + })); ourParamTypes.put(DateParam.class, RestSearchParameterTypeEnum.DATE); ourParamTypes.put(DateOrListParam.class, RestSearchParameterTypeEnum.DATE); @@ -136,35 +132,35 @@ public class SearchParameter extends BaseQueryParameter { ourParamTypes.put(DateRangeParam.class, RestSearchParameterTypeEnum.DATE); ourParamQualifiers.put( RestSearchParameterTypeEnum.DATE, - CollectionUtil.newSet(Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING)); + Set.of(new String[] {Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING})); ourParamTypes.put(QuantityParam.class, RestSearchParameterTypeEnum.QUANTITY); ourParamTypes.put(QuantityOrListParam.class, RestSearchParameterTypeEnum.QUANTITY); ourParamTypes.put(QuantityAndListParam.class, RestSearchParameterTypeEnum.QUANTITY); ourParamQualifiers.put( RestSearchParameterTypeEnum.QUANTITY, - CollectionUtil.newSet(Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING)); + Set.of(new String[] {Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING})); ourParamTypes.put(NumberParam.class, RestSearchParameterTypeEnum.NUMBER); ourParamTypes.put(NumberOrListParam.class, RestSearchParameterTypeEnum.NUMBER); ourParamTypes.put(NumberAndListParam.class, RestSearchParameterTypeEnum.NUMBER); ourParamQualifiers.put( RestSearchParameterTypeEnum.NUMBER, - CollectionUtil.newSet(Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING)); + Set.of(new String[] {Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING})); ourParamTypes.put(ReferenceParam.class, RestSearchParameterTypeEnum.REFERENCE); ourParamTypes.put(ReferenceOrListParam.class, RestSearchParameterTypeEnum.REFERENCE); ourParamTypes.put(ReferenceAndListParam.class, RestSearchParameterTypeEnum.REFERENCE); // --vvvv-- no empty because that gets added from OptionalParam#chainWhitelist ourParamQualifiers.put( - RestSearchParameterTypeEnum.REFERENCE, CollectionUtil.newSet(Constants.PARAMQUALIFIER_MISSING)); + RestSearchParameterTypeEnum.REFERENCE, Set.of(new String[] {Constants.PARAMQUALIFIER_MISSING})); ourParamTypes.put(CompositeParam.class, RestSearchParameterTypeEnum.COMPOSITE); ourParamTypes.put(CompositeOrListParam.class, RestSearchParameterTypeEnum.COMPOSITE); ourParamTypes.put(CompositeAndListParam.class, RestSearchParameterTypeEnum.COMPOSITE); ourParamQualifiers.put( RestSearchParameterTypeEnum.COMPOSITE, - CollectionUtil.newSet(Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING)); + Set.of(new String[] {Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING})); ourParamTypes.put(HasParam.class, RestSearchParameterTypeEnum.HAS); ourParamTypes.put(HasOrListParam.class, RestSearchParameterTypeEnum.HAS); @@ -174,7 +170,7 @@ public class SearchParameter extends BaseQueryParameter { ourParamTypes.put(SpecialOrListParam.class, RestSearchParameterTypeEnum.SPECIAL); ourParamTypes.put(SpecialAndListParam.class, RestSearchParameterTypeEnum.SPECIAL); ourParamQualifiers.put( - RestSearchParameterTypeEnum.SPECIAL, CollectionUtil.newSet(Constants.PARAMQUALIFIER_MISSING)); + RestSearchParameterTypeEnum.SPECIAL, Set.of(new String[] {Constants.PARAMQUALIFIER_MISSING})); } private List> myCompositeTypes = Collections.emptyList(); diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/JsonParserDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/JsonParserDstu3Test.java index ba8ddac128b..aaebdd90b79 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/JsonParserDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/JsonParserDstu3Test.java @@ -1160,99 +1160,6 @@ public void testEncodeParametersWithId() { assertThat(enc, containsString("\"valueId\": \"1\"")); } - @Test - public void testEncodeSummary() { - Patient patient = new Patient(); - patient.setId("Patient/1/_history/1"); - patient.getText().setDivAsString("
    THE DIV
    "); - patient.addName().setFamily("FAMILY"); - patient.addPhoto().setTitle("green"); - patient.getMaritalStatus().addCoding().setCode("D"); - - ourLog.debug(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient)); - - String encoded = ourCtx.newJsonParser().setPrettyPrint(true).setSummaryMode(true).encodeResourceToString(patient); - ourLog.info(encoded); - - assertThat(encoded, containsString("Patient")); - assertThat(encoded, stringContainsInOrder("\"tag\"", "\"system\": \"" + ca.uhn.fhir.rest.api.Constants.TAG_SUBSETTED_SYSTEM_DSTU3 + "\",", "\"code\": \"" + ca.uhn.fhir.rest.api.Constants.TAG_SUBSETTED_CODE + "\"")); - assertThat(encoded, not(containsString("THE DIV"))); - assertThat(encoded, containsString("family")); - assertThat(encoded, not(containsString("maritalStatus"))); - } - - /** - * We specifically include extensions on CapabilityStatment even in - * summary mode, since this is behaviour that people depend on - */ - @Test - public void testEncodeSummaryCapabilityStatementExtensions() { - - CapabilityStatement cs = new CapabilityStatement(); - CapabilityStatement.CapabilityStatementRestComponent rest = cs.addRest(); - rest.setMode(CapabilityStatement.RestfulCapabilityMode.CLIENT); - rest.getSecurity() - .addExtension() - .setUrl("http://foo") - .setValue(new StringType("bar")); - - cs.getVersionElement().addExtension() - .setUrl("http://goo") - .setValue(new StringType("ber")); - - String encoded = ourCtx.newJsonParser().setSummaryMode(true).setPrettyPrint(true).setPrettyPrint(true).encodeResourceToString(cs); - ourLog.info(encoded); - - assertThat(encoded, (containsString("http://foo"))); - assertThat(encoded, (containsString("bar"))); - assertThat(encoded, (containsString("http://goo"))); - assertThat(encoded, (containsString("ber"))); - } - - @Test - public void testEncodeSummaryPatientExtensions() { - - Patient cs = new Patient(); - Address address = cs.addAddress(); - address.setCity("CITY"); - address - .addExtension() - .setUrl("http://foo") - .setValue(new StringType("bar")); - address.getCityElement().addExtension() - .setUrl("http://goo") - .setValue(new StringType("ber")); - - String encoded = ourCtx.newJsonParser().setSummaryMode(true).setPrettyPrint(true).setPrettyPrint(true).encodeResourceToString(cs); - ourLog.info(encoded); - - assertThat(encoded, not(containsString("http://foo"))); - assertThat(encoded, not(containsString("bar"))); - assertThat(encoded, not(containsString("http://goo"))); - assertThat(encoded, not(containsString("ber"))); - } - - @Test - public void testEncodeSummary2() { - Patient patient = new Patient(); - patient.setId("Patient/1/_history/1"); - patient.getText().setDivAsString("
    THE DIV
    "); - patient.addName().setFamily("FAMILY"); - patient.getMaritalStatus().addCoding().setCode("D"); - - patient.getMeta().addTag().setSystem("foo").setCode("bar"); - - String encoded = ourCtx.newJsonParser().setPrettyPrint(true).setSummaryMode(true).encodeResourceToString(patient); - ourLog.info(encoded); - - assertThat(encoded, containsString("Patient")); - assertThat(encoded, stringContainsInOrder("\"tag\"", "\"system\": \"foo\",", "\"code\": \"bar\"", "\"system\": \"" + ca.uhn.fhir.rest.api.Constants.TAG_SUBSETTED_SYSTEM_DSTU3 + "\"", - "\"code\": \"" + ca.uhn.fhir.rest.api.Constants.TAG_SUBSETTED_CODE + "\"")); - assertThat(encoded, not(containsString("THE DIV"))); - assertThat(encoded, containsString("family")); - assertThat(encoded, not(containsString("maritalStatus"))); - } - /** * See #205 */ diff --git a/hapi-fhir-structures-r5/src/test/java/ca/uhn/fhir/parser/JsonParserSummaryModeR5Test.java b/hapi-fhir-structures-r5/src/test/java/ca/uhn/fhir/parser/JsonParserSummaryModeR5Test.java new file mode 100644 index 00000000000..b1b0bce1d7c --- /dev/null +++ b/hapi-fhir-structures-r5/src/test/java/ca/uhn/fhir/parser/JsonParserSummaryModeR5Test.java @@ -0,0 +1,269 @@ +package ca.uhn.fhir.parser; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.api.Constants; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r5.model.Address; +import org.hl7.fhir.r5.model.CapabilityStatement; +import org.hl7.fhir.r5.model.Patient; +import org.hl7.fhir.r5.model.StringType; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.stringContainsInOrder; + +public class JsonParserSummaryModeR5Test { + private static final Logger ourLog = LoggerFactory.getLogger(JsonParserSummaryModeR5Test.class); + + private static final FhirContext ourCtx = FhirContext.forR5Cached(); + + @Test + public void testEncodeSummary() { + Patient patient = new Patient(); + patient.setId("Patient/1/_history/1"); + patient.getText().setDivAsString("
    THE DIV
    "); + patient.addName().setFamily("FAMILY"); + patient.addPhoto().setTitle("green"); + patient.getMaritalStatus().addCoding().setCode("D"); + + ourLog.debug(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient)); + + String encoded = ourCtx.newJsonParser().setPrettyPrint(true).setSummaryMode(true).encodeResourceToString(patient); + ourLog.info(encoded); + + assertThat(encoded, containsString("Patient")); + assertThat(encoded, stringContainsInOrder("\"tag\"", "\"system\": \"" + Constants.TAG_SUBSETTED_SYSTEM_R4 + "\",", "\"code\": \"" + ca.uhn.fhir.rest.api.Constants.TAG_SUBSETTED_CODE + "\"")); + assertThat(encoded, not(containsString("THE DIV"))); + assertThat(encoded, containsString("family")); + assertThat(encoded, not(containsString("maritalStatus"))); + } + + @Test + public void testEncodeSummary2() { + Patient patient = new Patient(); + patient.setId("Patient/1/_history/1"); + patient.getText().setDivAsString("
    THE DIV
    "); + patient.addName().setFamily("FAMILY"); + patient.getMaritalStatus().addCoding().setCode("D"); + + patient.getMeta().addTag().setSystem("foo").setCode("bar"); + + String encoded = ourCtx.newJsonParser().setPrettyPrint(true).setSummaryMode(true).encodeResourceToString(patient); + ourLog.info(encoded); + + assertThat(encoded, containsString("Patient")); + assertThat(encoded, stringContainsInOrder("\"tag\"", "\"system\": \"foo\",", "\"code\": \"bar\"", "\"system\": \"" + ca.uhn.fhir.rest.api.Constants.TAG_SUBSETTED_SYSTEM_R4 + "\"", + "\"code\": \"" + ca.uhn.fhir.rest.api.Constants.TAG_SUBSETTED_CODE + "\"")); + assertThat(encoded, not(containsString("THE DIV"))); + assertThat(encoded, containsString("family")); + assertThat(encoded, not(containsString("maritalStatus"))); + } + + /** + * We specifically include extensions on CapabilityStatment even in + * summary mode, since this is behaviour that people depend on + */ + @Test + public void testEncodeSummaryCapabilityStatementExtensions() { + CapabilityStatement cs = createCapabilityStatementWithExtensions(); + + IParser parser = ourCtx.newJsonParser(); + parser.setSummaryMode(true); + parser.setPrettyPrint(true); + parser.setPrettyPrint(true); + String encoded = parser.encodeResourceToString(cs); + ourLog.info(encoded); + + assertThat(encoded, (containsString("\"rest\""))); + assertThat(encoded, (containsString("http://foo"))); + assertThat(encoded, (containsString("bar"))); + assertThat(encoded, (containsString("http://goo"))); + assertThat(encoded, (containsString("ber"))); + } + + /** + * We specifically include extensions on CapabilityStatment even in + * summary mode, since this is behaviour that people depend on + */ + @Test + public void testEncodeSummaryCapabilityStatementExtensions_ExplicitlyExcludeExtensions() { + CapabilityStatement cs = createCapabilityStatementWithExtensions(); + + IParser parser = ourCtx.newJsonParser(); + parser.setSummaryMode(true); + parser.setPrettyPrint(true); + parser.setPrettyPrint(true); + parser.setDontEncodeElements( + "CapabilityStatement.version.extension", + "CapabilityStatement.rest.security.extension" + ); + String encoded = parser.encodeResourceToString(cs); + ourLog.info(encoded); + + assertThat(encoded, (containsString("\"rest\""))); + assertThat(encoded, not(containsString("http://foo"))); + assertThat(encoded, not(containsString("bar"))); + assertThat(encoded, not(containsString("http://goo"))); + assertThat(encoded, not(containsString("ber"))); + } + + + @Test + public void testDontIncludeExtensions() { + Patient cs = createPatientWithVariousFieldsAndExtensions(); + + IParser parser = ourCtx.newJsonParser(); + parser.setSummaryMode(true); + parser.setPrettyPrint(true); + String encoded = parser.encodeResourceToString(cs); + ourLog.info(encoded); + + assertThat(encoded, containsString("\"id\": \"1\"")); + assertThat(encoded, containsString("\"versionId\": \"1\"")); + assertThat(encoded, containsString("\"city\": \"CITY\"")); + assertThat(encoded, not(containsString("http://foo"))); + assertThat(encoded, not(containsString("bar"))); + assertThat(encoded, not(containsString("http://goo"))); + assertThat(encoded, not(containsString("ber"))); + assertThat(encoded, not(containsString("http://fog"))); + assertThat(encoded, not(containsString("baz"))); + assertThat(encoded, not(containsString("Married to work"))); + } + + @Test + public void testForceInclude() { + Patient cs = createPatientWithVariousFieldsAndExtensions(); + + IParser parser = ourCtx.newJsonParser(); + parser.setEncodeElements("Patient.maritalStatus", "Patient.address.city.extension"); + parser.setSummaryMode(true); + parser.setPrettyPrint(true); + String encoded = parser.encodeResourceToString(cs); + ourLog.info(encoded); + + assertThat(encoded, containsString("\"id\": \"1\"")); + assertThat(encoded, containsString("\"versionId\": \"1\"")); + assertThat(encoded, containsString("\"city\": \"CITY\"")); + assertThat(encoded, not(containsString("http://foo"))); + assertThat(encoded, not(containsString("bar"))); + assertThat(encoded, not(containsString("http://fog"))); + assertThat(encoded, not(containsString("baz"))); + assertThat(encoded, containsString("http://goo")); + assertThat(encoded, containsString("ber")); + assertThat(encoded, containsString("Married to work")); + } + + @Test + public void testForceInclude_UsingStar() { + Patient cs = createPatientWithVariousFieldsAndExtensions(); + + IParser parser = ourCtx.newJsonParser(); + parser.setEncodeElements("*.maritalStatus", "*.address.city.extension"); + parser.setSummaryMode(true); + parser.setPrettyPrint(true); + String encoded = parser.encodeResourceToString(cs); + ourLog.info(encoded); + + assertThat(encoded, containsString("\"id\": \"1\"")); + assertThat(encoded, containsString("\"versionId\": \"1\"")); + assertThat(encoded, containsString("\"city\": \"CITY\"")); + assertThat(encoded, not(containsString("http://foo"))); + assertThat(encoded, not(containsString("bar"))); + assertThat(encoded, not(containsString("http://fog"))); + assertThat(encoded, not(containsString("baz"))); + assertThat(encoded, containsString("http://goo")); + assertThat(encoded, containsString("ber")); + assertThat(encoded, containsString("Married to work")); + } + + @Test + public void testForceInclude_ViaDefaultConfig() { + Patient cs = createPatientWithVariousFieldsAndExtensions(); + + FhirContext ctx = FhirContext.forR5(); + ctx.getParserOptions().setEncodeElementsForSummaryMode("Patient.maritalStatus", "Patient.address.city.extension"); + ctx.getParserOptions().setDontEncodeElementsForSummaryMode("Patient.id"); + + IParser parser = ctx.newJsonParser(); + parser.setSummaryMode(true); + parser.setPrettyPrint(true); + String encoded = parser.encodeResourceToString(cs); + ourLog.info(encoded); + + assertThat(encoded, not(containsString("\"id\": \"1\""))); + assertThat(encoded, containsString("\"versionId\": \"1\"")); + assertThat(encoded, containsString("\"city\": \"CITY\"")); + assertThat(encoded, not(containsString("http://foo"))); + assertThat(encoded, not(containsString("bar"))); + assertThat(encoded, not(containsString("http://fog"))); + assertThat(encoded, not(containsString("baz"))); + assertThat(encoded, containsString("http://goo")); + assertThat(encoded, containsString("ber")); + assertThat(encoded, containsString("Married to work")); + } + + @Test + public void testParserOptionsDontIncludeForSummaryModeDoesntApplyIfNotUsingSummaryMode() { + Patient cs = createPatientWithVariousFieldsAndExtensions(); + + FhirContext ctx = FhirContext.forR5(); + ctx.getParserOptions().setEncodeElementsForSummaryMode("Patient.maritalStatus", "Patient.address.city.extension"); + ctx.getParserOptions().setDontEncodeElementsForSummaryMode("Patient.id"); + + IParser parser = ctx.newJsonParser(); + parser.setSummaryMode(false); + parser.setPrettyPrint(true); + String encoded = parser.encodeResourceToString(cs); + ourLog.info(encoded); + + assertThat(encoded, containsString("\"id\": \"1\"")); + assertThat(encoded, containsString("\"versionId\": \"1\"")); + assertThat(encoded, containsString("\"city\": \"CITY\"")); + assertThat(encoded, containsString("http://foo")); + assertThat(encoded, containsString("bar")); + assertThat(encoded, containsString("http://fog")); + assertThat(encoded, containsString("baz")); + assertThat(encoded, containsString("http://goo")); + assertThat(encoded, containsString("ber")); + assertThat(encoded, containsString("Married to work")); + } + + private static @Nonnull CapabilityStatement createCapabilityStatementWithExtensions() { + CapabilityStatement cs = new CapabilityStatement(); + CapabilityStatement.CapabilityStatementRestComponent rest = cs.addRest(); + rest.setMode(CapabilityStatement.RestfulCapabilityMode.CLIENT); + rest.getSecurity() + .addExtension() + .setUrl("http://foo") + .setValue(new StringType("bar")); + + cs.getVersionElement().addExtension() + .setUrl("http://goo") + .setValue(new StringType("ber")); + return cs; + } + + private static @Nonnull Patient createPatientWithVariousFieldsAndExtensions() { + Patient retVal = new Patient(); + retVal.setId("Patient/1/_history/1"); + retVal.getMaritalStatus().setText("Married to work"); + retVal.addExtension() + .setUrl("http://fog") + .setValue(new StringType("baz")); + Address address = retVal.addAddress(); + address.setCity("CITY"); + address + .addExtension() + .setUrl("http://foo") + .setValue(new StringType("bar")); + address.getCityElement().addExtension() + .setUrl("http://goo") + .setValue(new StringType("ber")); + return retVal; + } + +} diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/jpa/conformance/DateSearchTestCase.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/jpa/conformance/DateSearchTestCase.java index ccaef5cd2f0..5b7cdd4398e 100644 --- a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/jpa/conformance/DateSearchTestCase.java +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/jpa/conformance/DateSearchTestCase.java @@ -19,7 +19,6 @@ */ package ca.uhn.fhir.jpa.conformance; -import ca.uhn.fhir.util.CollectionUtil; import jakarta.annotation.Nonnull; import org.junit.jupiter.params.provider.Arguments; @@ -127,7 +126,7 @@ public static List compactCases() { */ @Nonnull static List expandPrefixCases(Reader theSource, String theFileName) { - Set supportedPrefixes = CollectionUtil.newSet("eq", "ge", "gt", "le", "lt", "ne"); + Set supportedPrefixes = Set.of(new String[] {"eq", "ge", "gt", "le", "lt", "ne"}); // expand these into individual tests for each prefix. LineNumberReader lineNumberReader = new LineNumberReader(theSource);