Configurable summary mode (#5944)

* Fix #5871 - Configurable summary mode

* Add docs

* Spotless

* Add javadoc
This commit is contained in:
James Agnew 2024-05-30 11:47:48 -04:00 committed by GitHub
parent ac3a5e2ad2
commit ab0b62706a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 835 additions and 245 deletions

View File

@ -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<String> myDontStripVersionsFromReferencesAtPaths = Collections.emptySet();
private boolean myOverrideResourceIdWithBundleEntryFullUrl = true;
private boolean myAutoContainReferenceTargetsWithNoId = true;
private Set<String> myEncodeElementsForSummaryMode = null;
private Set<String> myDontEncodeElementsForSummaryMode = null;
/**
* If set to {@literal true} (which is the default), contained resources may be specified by
@ -143,7 +147,7 @@ public class ParserOptions {
if (thePaths == null) {
setDontStripVersionsFromReferencesAtPaths((List<String>) null);
} else {
setDontStripVersionsFromReferencesAtPaths(Arrays.asList(thePaths));
setDontStripVersionsFromReferencesAtPaths(CollectionUtil.newSet(thePaths));
}
return this;
}
@ -205,4 +209,119 @@ public class ParserOptions {
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:
* <ul>
* <li><b>Patient.maritalStatus</b> - Encode the entire maritalStatus CodeableConcept, even though Patient.maritalStatus is not a summary element</li>
* <li><b>Patient.maritalStatus.text</b> - Encode only the text component of the patient's maritalStatus</li>
* <li><b>*.text</b> - Encode the text element on any resource (only the very first position may contain a
* wildcard)</li>
* </ul>
*
* @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:
* <ul>
* <li><b>Patient.maritalStatus</b> - Encode the entire maritalStatus CodeableConcept, even though Patient.maritalStatus is not a summary element</li>
* <li><b>Patient.maritalStatus.text</b> - Encode only the text component of the patient's maritalStatus</li>
* <li><b>*.text</b> - Encode the text element on any resource (only the very first position may contain a
* wildcard)</li>
* </ul>
*
* @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<String> theEncodeElements) {
Set<String> 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 <code>null</code>
*
* @since 7.4.0
*/
@Nullable
public Set<String> 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:
* <ul>
* <li><b>Patient.name</b> - Do not include the patient's name</li>
* <li><b>Patient.name.family</b> - Do not include the patient's family name</li>
* <li><b>*.name</b> - Do not include the name element on any resource type</li>
* </ul>
*
* @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:
* <ul>
* <li><b>Patient.name</b> - Do not include the patient's name</li>
* <li><b>Patient.name.family</b> - Do not include the patient's family name</li>
* <li><b>*.name</b> - Do not include the name element on any resource type</li>
* </ul>
*
* @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<String> theDontEncodeElements) {
Set<String> 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 <code>null</code>
* @since 7.4.0
*/
@Nullable
public Set<String> getDontEncodeElementsForSummaryMode() {
return myDontEncodeElementsForSummaryMode;
}
}

View File

@ -27,6 +27,7 @@ import ca.uhn.fhir.context.BaseRuntimeElementDefinition.ChildTypeEnum;
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.parser.path.EncodeContextPath;
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<EncodeContextPath> myDontEncodeElements;
private List<EncodeContextPath> myEncodeElements;
private Set<String> myEncodeElementsAppliesToResourceTypes;
private Collection<String> myDontEncodeElements;
private Collection<String> myEncodeElements;
private IIdType myEncodeForceResourceId;
private IParserErrorHandler myErrorHandler;
private boolean myOmitResourceId;
@ -131,52 +132,15 @@ public abstract class BaseParser implements IParser {
return myContext;
}
List<EncodeContextPath> getDontEncodeElements() {
return myDontEncodeElements;
}
@Override
public IParser setDontEncodeElements(Collection<String> theDontEncodeElements) {
if (theDontEncodeElements == null || theDontEncodeElements.isEmpty()) {
myDontEncodeElements = null;
} else {
myDontEncodeElements =
theDontEncodeElements.stream().map(EncodeContextPath::new).collect(Collectors.toList());
}
myDontEncodeElements = theDontEncodeElements;
return this;
}
List<EncodeContextPath> getEncodeElements() {
return myEncodeElements;
}
@Override
public IParser setEncodeElements(Set<String> 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 abstract class BaseParser implements IParser {
@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 abstract class BaseParser implements IParser {
} 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 @@ public abstract class BaseParser implements IParser {
return false;
}
Set<String> dontStripVersionsFromReferencesAtPaths = myDontStripVersionsFromReferencesAtPaths;
Set<String> dontStripVersionsFromReferencesAtPaths = getDontStripVersionsFromReferencesAtPaths();
if (dontStripVersionsFromReferencesAtPaths != null
&& !dontStripVersionsFromReferencesAtPaths.isEmpty()
&& theCompositeChildElement.anyPathMatches(dontStripVersionsFromReferencesAtPaths)) {
@ -923,7 +887,7 @@ public abstract class BaseParser implements IParser {
if (isSuppressNarratives()) {
return true;
}
if (myEncodeElements != null) {
if (theEncodeContext.myEncodeElementPaths != null) {
if (isEncodeElementsAppliesToChildResourcesOnly()
&& theEncodeContext.getResourcePath().size() < 2) {
return false;
@ -933,8 +897,8 @@ public abstract class BaseParser implements IParser {
.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 @@ public abstract class BaseParser implements IParser {
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 @@ public abstract class BaseParser implements IParser {
/**
* 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 @@ public abstract class BaseParser implements IParser {
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 @@ public abstract class BaseParser implements IParser {
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 @@ public abstract class BaseParser implements IParser {
myContainedResources = getContext().newTerser().containResources(theResource);
}
class ChildNameAndDef {
static class ChildNameAndDef {
private final BaseRuntimeElementDefinition<?> myChildDef;
private final String myChildName;
@ -1066,10 +1029,40 @@ public abstract class BaseParser implements IParser {
* 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<Key, List<BaseParser.CompositeChildElement>> myCompositeChildrenCache = new HashMap<>();
private final List<EncodeContextPath> myEncodeElementPaths;
private final Set<String> myEncodeElementsAppliesToResourceTypes;
private final List<EncodeContextPath> myDontEncodeElementPaths;
public Map<Key, List<BaseParser.CompositeChildElement>> getCompositeChildrenCache() {
public EncodeContext(BaseParser theParser, ParserOptions theParserOptions) {
Collection<String> encodeElements = theParser.myEncodeElements;
Collection<String> 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);
}
private Map<Key, List<BaseParser.CompositeChildElement>> getCompositeChildrenCache() {
return myCompositeChildrenCache;
}
}
@ -1161,14 +1154,14 @@ public abstract class BaseParser implements IParser {
}
private boolean checkIfParentShouldBeEncodedAndBuildPath() {
List<EncodeContextPath> encodeElements = myEncodeElements;
List<EncodeContextPath> 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 @@ public abstract class BaseParser implements IParser {
}
private boolean checkIfParentShouldNotBeEncodedAndBuildPath() {
return checkIfPathMatchesForEncoding(myDontEncodeElements, false);
return checkIfPathMatchesForEncoding(myEncodeContext.myDontEncodeElementPaths, false);
}
private boolean checkIfPathMatchesForEncoding(
@ -1256,16 +1249,7 @@ public abstract class BaseParser implements IParser {
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 abstract class BaseParser implements IParser {
retVal = false;
}
}
if (myEncodeContext.myEncodeElementPaths != null) {
retVal = checkIfParentShouldBeEncodedAndBuildPath();
}
if (retVal && myEncodeContext.myDontEncodeElementPaths != null) {
retVal = !checkIfParentShouldNotBeEncodedAndBuildPath();
}
if (theContainedResource) {
retVal = !notEncodeForContainedResource.contains(myDef.getElementName());
}
return retVal;
}

View File

@ -24,6 +24,9 @@ import ca.uhn.fhir.context.FhirContext;
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.
* <p>
* 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.
* </p>
*
* @param theOmitResourceId Should resource IDs be omitted
@ -172,7 +176,7 @@ public interface IParser {
* links. In that case, this value should be set to <code>false</code>.
*
* @return Returns the parser instance's configuration setting for stripping versions from resource references when
* encoding. This method will retun <code>null</code> if no value is set, in which case
* encoding. This method will return <code>null</code> if no value is set, in which case
* the value from the {@link ParserOptions} will be used (default is <code>true</code>)
* @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 <code>true</code> (default is <code>false</code>) only elements marked by the FHIR specification as
* being "summary elements" will be included.
* <p>
* 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)}.
* </p>
* <p>
* For compatibility reasons with other frameworks, when encoding a
* <code>CapabilityStatement</code> resource in summary mode, extensions
* are always encoded, even though the FHIR Specification does not consider
* them to be summary elements.
* </p>
*
* @return Returns a reference to <code>this</code> parser so that method calls can be chained together
*/
@ -287,16 +307,48 @@ public interface IParser {
* wildcard)</li>
* </ul>
* <p>
* Note: If {@link #setSummaryMode(boolean)} is set to <code>true</code>, then any
* elements specified using this method will be excluded even if they are
* summary elements.
* </p>
* <p>
* DSTU2 note: Note that values including meta, such as <code>Patient.meta</code>
* 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 <code>Patient.meta.lastUpdated</code> will only work in
* DSTU3+ mode.
* </p>
*
* @param theDontEncodeElements The elements to encode
* @param theDontEncodeElements The elements to not encode, or <code>null</code>
* @see #setEncodeElements(Set)
* @see ParserOptions#setDontEncodeElementsForSummaryMode(Collection)
*/
IParser setDontEncodeElements(Collection<String> theDontEncodeElements);
IParser setDontEncodeElements(@Nullable Collection<String> theDontEncodeElements);
/**
* If provided, specifies the elements which should NOT be encoded. Valid values for this
* field would include:
* <ul>
* <li><b>Patient</b> - Don't encode patient and all its children</li>
* <li><b>Patient.name</b> - Don't encode the patient's name</li>
* <li><b>Patient.name.family</b> - Don't encode the patient's family name</li>
* <li><b>*.text</b> - Don't encode the text element on any resource (only the very first position may contain a
* wildcard)</li>
* </ul>
* <p>
* DSTU2 note: Note that values including meta, such as <code>Patient.meta</code>
* will work for DSTU2 parsers, but values with sub-elements on meta such
* as <code>Patient.meta.lastUpdated</code> will only work in
* DSTU3+ mode.
* </p>
*
* @param theDontEncodeElements The elements to not encode. Can be an empty list, but must not be <code>null</code>.
* @see #setDontEncodeElements(Collection)
* @see ParserOptions#setDontEncodeElementsForSummaryMode(Collection)
* @since 7.4.0
*/
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 @@ public interface IParser {
* wildcard)</li>
* <li><b>*.(mandatory)</b> - This is a special case which causes any mandatory fields (min > 0) to be encoded</li>
* </ul>
* <p>
* Note: If {@link #setSummaryMode(boolean)} is set to <code>true</code>, then any
* elements specified using this method will be included even if they are not
* summary elements.
* </p>
*
* @param theEncodeElements The elements to encode
* @param theEncodeElements The elements to encode, or <code>null</code>
* @see #setDontEncodeElements(Collection)
* @see #setEncodeElements(String...)
* @see ParserOptions#setEncodeElementsForSummaryMode(Collection)
*/
IParser setEncodeElements(Set<String> theEncodeElements);
IParser setEncodeElements(@Nullable Set<String> theEncodeElements);
/**
* If provided, specifies the elements which should be encoded, to the exclusion of all others. Valid values for this
* field would include:
* <ul>
* <li><b>Patient</b> - Encode patient and all its children</li>
* <li><b>Patient.name</b> - Encode only the patient's name</li>
* <li><b>Patient.name.family</b> - Encode only the patient's family name</li>
* <li><b>*.text</b> - Encode the text element on any resource (only the very first position may contain a
* wildcard)</li>
* <li><b>*.(mandatory)</b> - This is a special case which causes any mandatory fields (min > 0) to be encoded</li>
* </ul>
* <p>
* Note: If {@link #setSummaryMode(boolean)} is set to <code>true</code>, then any
* elements specified using this method will be included even if they are not
* summary elements.
* </p>
*
* @param theEncodeElements The elements to encode. Can be an empty list, but must not be <code>null</code>.
* @since 7.4.0
* @see #setEncodeElements(Set)
* @see ParserOptions#setEncodeElementsForSummaryMode(String...)
*/
default IParser setEncodeElements(@Nonnull String... theEncodeElements) {
return setEncodeElements(CollectionUtil.newSet(theEncodeElements));
}
/**
* If set to <code>true</code> (default is false), the values supplied
@ -351,7 +436,7 @@ public interface IParser {
* 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. "<a href="http://example.com/base">http://example.com/base</a>"
* @return Returns an instance of <code>this</code> parser so that method calls can be chained together
*/
IParser setServerBaseUrl(String theUrl);
@ -378,7 +463,7 @@ public interface IParser {
/**
* Returns the value supplied to {@link IParser#setDontStripVersionsFromReferencesAtPaths(String...)}
* or <code>null</code> 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)

View File

@ -284,6 +284,8 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
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 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
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 class JsonParser extends BaseParser implements IJsonLikeParser {
+ 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 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
EncodeContext theEncodeContext)
throws IOException {
if (!super.shouldEncodeResource(theResDef.getName())) {
if (!super.shouldEncodeResource(theResDef.getName(), theEncodeContext)) {
return;
}
@ -973,15 +975,15 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
}
List<Map.Entry<ResourceMetadataKeyEnum<?>, 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);
}

View File

@ -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<String> determineApplicableResourceTypesForTerserPaths(
@Nullable List<EncodeContextPath> encodeElements) {
Set<String> 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;
}
}

View File

@ -345,12 +345,12 @@ public class RDFParser extends BaseParser {
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 @@ public class RDFParser extends BaseParser {
rdfModel,
extensionResource,
false,
new CompositeChildElement(resDef, encodeContext),
encodeContext);
new CompositeChildElement(resDef, theEncodeContext),
theEncodeContext);
}
}
}
@ -429,7 +429,7 @@ public class RDFParser extends BaseParser {
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 @@ public class RDFParser extends BaseParser {
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 @@ public class RDFParser extends BaseParser {
rdfModel,
true,
super.fixContainedResourceId(resourceId.getValue()),
encodeContext,
theEncodeContext,
false,
containedResource);
}
@ -474,13 +474,14 @@ public class RDFParser extends BaseParser {
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 @@ public class RDFParser extends BaseParser {
}
}
} finally {
encodeContext.popPath();
theEncodeContext.popPath();
}
return rdfModel;

View File

@ -372,7 +372,7 @@ public class XmlParser extends BaseParser {
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 @@ public class XmlParser extends BaseParser {
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());
}
}

View File

@ -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 <T> Set<T> newSet(T... theValues) {
HashSet<T> retVal = new HashSet<T>();
/**
* 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
* <code>null</code> they will be treated as an empty collection, meaning
* that even if both arguments are <code>null</code>, an empty immutable
* collection will be returned.
* <p>
* DO NOT use this method if the underlying collections can be changed
* after calling this method, as the behaviour is indeterminate.
* </p>
*
* @param theCollection0 The first set in the union, or <code>null</code>.
* @param theCollection1 The second set in the union, or <code>null</code>.
* @return Returns a union of both collections. Will not return <code>null</code> ever.
* @since 7.4.0
*/
@Nonnull
public static <T> Collection<T> nullSafeUnion(
@Nullable Collection<T> theCollection0, @Nullable Collection<T> theCollection1) {
Collection<T> collection0 = theCollection0;
if (collection0 != null && collection0.isEmpty()) {
collection0 = null;
}
return retVal;
Collection<T> collection1 = theCollection1;
if (collection1 != null && collection1.isEmpty()) {
collection1 = null;
}
if (collection0 == null && collection1 == null) {
return Collections.emptySet();
}
if (collection0 == null) {
return unmodifiableCollection(collection1);
}
if (collection1 == null) {
return unmodifiableCollection(collection0);
}
return CollectionUtils.union(collection0, collection1);
}
/**
* This method is equivalent to <code>Set.of(...)</code> but is kept here
* and used instead of that method because Set.of is not present on Android
* SDKs (at least up to 29).
* <p>
* Sets returned by this method are unmodifiable.
* </p>
*/
@SuppressWarnings("unchecked")
public static <T> Set<T> newSet(T... theValues) {
HashSet<T> retVal = new HashSet<>();
Collections.addAll(retVal, theValues);
return Collections.unmodifiableSet(retVal);
}
}

View File

@ -90,8 +90,8 @@ import java.util.Set;
public class SearchParameter extends BaseQueryParameter {
private static final String EMPTY_STRING = "";
private static HashMap<RestSearchParameterTypeEnum, Set<String>> ourParamQualifiers;
private static HashMap<Class<?>, RestSearchParameterTypeEnum> ourParamTypes;
private static final HashMap<RestSearchParameterTypeEnum, Set<String>> ourParamQualifiers;
private static final HashMap<Class<?>, RestSearchParameterTypeEnum> ourParamTypes;
static final String QUALIFIER_ANY_TYPE = ":*";
static {

View File

@ -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 class Parser {
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 class Parser {
// 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
}
}
}

View File

@ -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."

View File

@ -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 <code>_pretty=true</code> 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 (&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 <code>_summary=true</code> to the request.
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/Parser.java|encodingSummary}}
```
<a name="parser-options"/>
# 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}}
```

View File

@ -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"));
}
}

View File

@ -74,7 +74,6 @@ import ca.uhn.fhir.rest.param.binder.QueryParameterTypeBinder;
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<Class<? extends IQueryParameterType>> myCompositeTypes = Collections.emptyList();

View File

@ -1160,99 +1160,6 @@ public class JsonParserDstu3Test {
assertThat(enc, containsString("\"valueId\": \"1\""));
}
@Test
public void testEncodeSummary() {
Patient patient = new Patient();
patient.setId("Patient/1/_history/1");
patient.getText().setDivAsString("<div>THE DIV</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("<div>THE DIV</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
*/

View File

@ -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("<div>THE DIV</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("<div>THE DIV</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;
}
}

View File

@ -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 class DateSearchTestCase {
*/
@Nonnull
static List<DateSearchTestCase> expandPrefixCases(Reader theSource, String theFileName) {
Set<String> supportedPrefixes = CollectionUtil.newSet("eq", "ge", "gt", "le", "lt", "ne");
Set<String> supportedPrefixes = Set.of(new String[] {"eq", "ge", "gt", "le", "lt", "ne"});
// expand these into individual tests for each prefix.
LineNumberReader lineNumberReader = new LineNumberReader(theSource);