diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index bdc13ab4116..13987de4279 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -59,6 +59,8 @@ stages:
# module: hapi-fhir-jpaserver-base
- name: hapi_fhir_jpaserver_elastic_test_utilities
module: hapi-fhir-jpaserver-elastic-test-utilities
+ - name: hapi_fhir_jpaserver_ips
+ module: hapi-fhir-jpaserver-ips
- name: hapi_fhir_jpaserver_mdm
module: hapi-fhir-jpaserver-mdm
- name: hapi_fhir_jpaserver_model
diff --git a/hapi-deployable-pom/pom.xml b/hapi-deployable-pom/pom.xml
index b13edcc35da..6be19192e0c 100644
--- a/hapi-deployable-pom/pom.xml
+++ b/hapi-deployable-pom/pom.xml
@@ -4,7 +4,7 @@
ca.uhn.hapi.fhirhapi-fhir
- 6.3.12-SNAPSHOT
+ 6.3.13-SNAPSHOT../pom.xml
diff --git a/hapi-fhir-android/pom.xml b/hapi-fhir-android/pom.xml
index 1e2050bce79..b426255dc0c 100644
--- a/hapi-fhir-android/pom.xml
+++ b/hapi-fhir-android/pom.xml
@@ -5,7 +5,7 @@
ca.uhn.hapi.fhirhapi-deployable-pom
- 6.3.12-SNAPSHOT
+ 6.3.13-SNAPSHOT../hapi-deployable-pom/pom.xml
diff --git a/hapi-fhir-base/pom.xml b/hapi-fhir-base/pom.xml
index 746e6e664c1..77391678ee0 100644
--- a/hapi-fhir-base/pom.xml
+++ b/hapi-fhir-base/pom.xml
@@ -5,7 +5,7 @@
ca.uhn.hapi.fhirhapi-deployable-pom
- 6.3.12-SNAPSHOT
+ 6.3.13-SNAPSHOT../hapi-deployable-pom/pom.xml
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/fhirpath/IFhirPath.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/fhirpath/IFhirPath.java
index 38d7600cdfa..66ed7fbf455 100644
--- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/fhirpath/IFhirPath.java
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/fhirpath/IFhirPath.java
@@ -22,6 +22,7 @@ package ca.uhn.fhir.fhirpath;
import org.hl7.fhir.instance.model.api.IBase;
+import javax.annotation.Nonnull;
import java.util.List;
import java.util.Optional;
@@ -52,4 +53,15 @@ public interface IFhirPath {
* Parses the expression and throws an exception if it can not parse correctly
*/
void parse(String theExpression) throws Exception;
+
+
+ /**
+ * This method can be used optionally to supply an evaluation context for the
+ * FHIRPath evaluator instance. The context can be used to supply data needed by
+ * specific functions, e.g. allowing the resolve() function to
+ * fetch referenced resources.
+ *
+ * @since 6.4.0
+ */
+ void setEvaluationContext(@Nonnull IFhirPathEvaluationContext theEvaluationContext);
}
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/fhirpath/IFhirPathEvaluationContext.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/fhirpath/IFhirPathEvaluationContext.java
new file mode 100644
index 00000000000..b7feaf6d3bd
--- /dev/null
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/fhirpath/IFhirPathEvaluationContext.java
@@ -0,0 +1,41 @@
+package ca.uhn.fhir.fhirpath;
+
+/*-
+ * #%L
+ * HAPI FHIR - Core Library
+ * %%
+ * Copyright (C) 2014 - 2023 Smile CDR, Inc.
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import org.hl7.fhir.instance.model.api.IBase;
+import org.hl7.fhir.instance.model.api.IIdType;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+public interface IFhirPathEvaluationContext {
+
+ /**
+ * Evaluates the resolve() function and returns the target
+ * of the resolution.
+ *
+ * @param theReference The reference
+ * @param theContext The entity containing the reference. Note that this will be null for FHIR versions R4 and below.
+ */
+ default IBase resolveReference(@Nonnull IIdType theReference, @Nullable IBase theContext) {
+ return null;
+ }
+}
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/Include.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/Include.java
index a39edf7b2ba..2a9e81ceaac 100644
--- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/Include.java
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/Include.java
@@ -37,7 +37,7 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
* upgrading servers.
*
*
- * Note on thrwead safety: This class is not thread safe.
+ * Note on thread safety: This class is not thread safe.
*
+ */
+ public CustomThymeleafNarrativeGenerator(String... theNarrativePropertyFiles) {
+ this();
+ setPropertyFile(theNarrativePropertyFiles);
+ }
/**
* Create a new narrative generator
- *
- * @param thePropertyFile
- * The name of the property file, in one of the following formats:
- *
- *
file:/path/to/file/file.properties
- *
classpath:/com/package/file.properties
- *
+ *
+ * @param theNarrativePropertyFiles The name of the property file, in one of the following formats:
+ *
+ *
file:/path/to/file/file.properties
+ *
classpath:/com/package/file.properties
+ *
*/
- public CustomThymeleafNarrativeGenerator(String... thePropertyFile) {
- super();
- setPropertyFile(thePropertyFile);
+ public CustomThymeleafNarrativeGenerator(List theNarrativePropertyFiles) {
+ this(theNarrativePropertyFiles.toArray(new String[0]));
+ }
+
+ @Override
+ public NarrativeTemplateManifest getManifest() {
+ NarrativeTemplateManifest retVal = myManifest;
+ if (myManifest == null) {
+ Validate.isTrue(myPropertyFile != null, "Neither a property file or a manifest has been provided");
+ retVal = NarrativeTemplateManifest.forManifestFileLocation(myPropertyFile);
+ setManifest(retVal);
+ }
+ return retVal;
+ }
+
+ public void setManifest(NarrativeTemplateManifest theManifest) {
+ myManifest = theManifest;
}
/**
* Set the property file to use
- *
- * @param thePropertyFile
- * The name of the property file, in one of the following formats:
- *
- *
file:/path/to/file/file.properties
- *
classpath:/com/package/file.properties
- *
+ *
+ * @param thePropertyFile The name of the property file, in one of the following formats:
+ *
+ *
file:/path/to/file/file.properties
+ *
classpath:/com/package/file.properties
+ *
*/
public void setPropertyFile(String... thePropertyFile) {
Validate.notNull(thePropertyFile, "Property file can not be null");
myPropertyFile = Arrays.asList(thePropertyFile);
+ myManifest = null;
}
-
- @Override
- public List getPropertyFile() {
- return myPropertyFile;
- }
-
}
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGenerator.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGenerator.java
index c921ace1d3c..0aff3ad66b0 100644
--- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGenerator.java
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGenerator.java
@@ -20,6 +20,8 @@ package ca.uhn.fhir.narrative;
* #L%
*/
+import ca.uhn.fhir.narrative2.NarrativeTemplateManifest;
+
import java.util.ArrayList;
import java.util.List;
@@ -29,23 +31,29 @@ public class DefaultThymeleafNarrativeGenerator extends BaseThymeleafNarrativeGe
static final String HAPISERVER_NARRATIVES_PROPERTIES = "classpath:ca/uhn/fhir/narrative/narratives-hapiserver.properties";
private boolean myUseHapiServerConformanceNarrative;
+ private volatile NarrativeTemplateManifest myManifest;
public DefaultThymeleafNarrativeGenerator() {
super();
}
@Override
- protected List getPropertyFile() {
- List retVal = new ArrayList();
- retVal.add(NARRATIVES_PROPERTIES);
- if (myUseHapiServerConformanceNarrative) {
- retVal.add(HAPISERVER_NARRATIVES_PROPERTIES);
+ protected NarrativeTemplateManifest getManifest() {
+ NarrativeTemplateManifest retVal = myManifest;
+ if (retVal == null) {
+ List propertyFiles = new ArrayList<>();
+ propertyFiles.add(NARRATIVES_PROPERTIES);
+ if (myUseHapiServerConformanceNarrative) {
+ propertyFiles.add(HAPISERVER_NARRATIVES_PROPERTIES);
+ }
+ retVal = NarrativeTemplateManifest.forManifestFileLocation(propertyFiles);
+ myManifest = retVal;
}
return retVal;
}
/**
- * If set to true (default is false) a special custom narrative for the Conformance resource will be provided, which is designed to be used with HAPI {@link RestfulServer}
+ * If set to true (default is false) a special custom narrative for the Conformance resource will be provided, which is designed to be used with HAPI FHIR Server
* instances. This narrative provides a friendly search page which can assist users of the service.
*/
public void setUseHapiServerConformanceNarrative(boolean theValue) {
@@ -53,7 +61,7 @@ public class DefaultThymeleafNarrativeGenerator extends BaseThymeleafNarrativeGe
}
/**
- * If set to true (default is false) a special custom narrative for the Conformance resource will be provided, which is designed to be used with HAPI {@link RestfulServer}
+ * If set to true (default is false) a special custom narrative for the Conformance resource will be provided, which is designed to be used with HAPI FHIR Server
* instances. This narrative provides a friendly search page which can assist users of the service.
*/
public boolean isUseHapiServerConformanceNarrative() {
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/INarrativeGenerator.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/INarrativeGenerator.java
index 618512635c1..754adf3c6e9 100644
--- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/INarrativeGenerator.java
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/INarrativeGenerator.java
@@ -35,4 +35,8 @@ public interface INarrativeGenerator {
*/
boolean populateResourceNarrative(FhirContext theFhirContext, IBaseResource theResource);
+ /**
+ * Generates the narrative for the given resource and returns it as a string
+ */
+ String generateResourceNarrative(FhirContext theFhirContext, IBaseResource theResource);
}
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/BaseNarrativeGenerator.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/BaseNarrativeGenerator.java
index 003591e08e8..6319731ea26 100644
--- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/BaseNarrativeGenerator.java
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/BaseNarrativeGenerator.java
@@ -28,10 +28,15 @@ import ca.uhn.fhir.fhirpath.IFhirPath;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.narrative.INarrativeGenerator;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
+import ca.uhn.fhir.util.Logs;
+import ch.qos.logback.classic.spi.LogbackServiceProvider;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.INarrative;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import javax.annotation.Nullable;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
@@ -43,29 +48,46 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
public abstract class BaseNarrativeGenerator implements INarrativeGenerator {
- private INarrativeTemplateManifest myManifest;
-
- public INarrativeTemplateManifest getManifest() {
- return myManifest;
- }
-
- public void setManifest(INarrativeTemplateManifest theManifest) {
- myManifest = theManifest;
- }
-
@Override
public boolean populateResourceNarrative(FhirContext theFhirContext, IBaseResource theResource) {
- List templateOpt = getTemplateForElement(theFhirContext, theResource);
- if (templateOpt.size() > 0) {
- applyTemplate(theFhirContext, templateOpt.get(0), theResource);
+ INarrativeTemplate template = selectTemplate(theFhirContext, theResource);
+ if (template != null) {
+ applyTemplate(theFhirContext, template, theResource);
return true;
}
return false;
}
- private List getTemplateForElement(FhirContext theFhirContext, IBase theElement) {
- return myManifest.getTemplateByElement(theFhirContext, getStyle(), theElement);
+ @Nullable
+ private INarrativeTemplate selectTemplate(FhirContext theFhirContext, IBaseResource theResource) {
+ List templates = getTemplateForElement(theFhirContext, theResource);
+ INarrativeTemplate template = null;
+ if (templates.isEmpty()) {
+ Logs.getNarrativeGenerationTroubleshootingLog().debug("No templates match for resource of type {}", theResource.getClass());
+ } else {
+ if (templates.size() > 1) {
+ Logs.getNarrativeGenerationTroubleshootingLog().debug("Multiple templates match for resource of type {} - Picking first from: {}", theResource.getClass(), templates);
+ }
+ template = templates.get(0);
+ Logs.getNarrativeGenerationTroubleshootingLog().debug("Selected template: {}", template);
+ }
+ return template;
+ }
+
+ @Override
+ public String generateResourceNarrative(FhirContext theFhirContext, IBaseResource theResource) {
+ INarrativeTemplate template = selectTemplate(theFhirContext, theResource);
+ if (template != null) {
+ String narrative = applyTemplate(theFhirContext, template, (IBase)theResource);
+ return cleanWhitespace(narrative);
+ }
+
+ return null;
+ }
+
+ protected List getTemplateForElement(FhirContext theFhirContext, IBase theElement) {
+ return getManifest().getTemplateByElement(theFhirContext, getStyle(), theElement);
}
private boolean applyTemplate(FhirContext theFhirContext, INarrativeTemplate theTemplate, IBaseResource theResource) {
@@ -208,4 +230,7 @@ public abstract class BaseNarrativeGenerator implements INarrativeGenerator {
}
return b.toString();
}
+
+ protected abstract NarrativeTemplateManifest getManifest();
+
}
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/INarrativeTemplateManifest.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/INarrativeTemplateManifest.java
index 9b2530bd831..5f7b2a9d582 100644
--- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/INarrativeTemplateManifest.java
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/INarrativeTemplateManifest.java
@@ -23,13 +23,17 @@ package ca.uhn.fhir.narrative2;
import ca.uhn.fhir.context.FhirContext;
import org.hl7.fhir.instance.model.api.IBase;
+import javax.annotation.Nonnull;
+import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
public interface INarrativeTemplateManifest {
- List getTemplateByResourceName(FhirContext theFhirContext, EnumSet theStyles, String theResourceName);
+ List getTemplateByResourceName(@Nonnull FhirContext theFhirContext, @Nonnull EnumSet theStyles, @Nonnull String theResourceName, @Nonnull Collection theProfiles);
- List getTemplateByName(FhirContext theFhirContext, EnumSet theStyles, String theName);
+ List getTemplateByName(@Nonnull FhirContext theFhirContext, @Nonnull EnumSet theStyles, @Nonnull String theName);
- List getTemplateByElement(FhirContext theFhirContext, EnumSet theStyles, IBase theElementValue);
+ List getTemplateByElement(@Nonnull FhirContext theFhirContext, @Nonnull EnumSet theStyles, @Nonnull IBase theElementValue);
+
+ List getTemplateByFragmentName(@Nonnull FhirContext theFhirContext, @Nonnull EnumSet theStyles, @Nonnull String theFragmentName);
}
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeTemplate.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeTemplate.java
index c03c3cd75b1..688606d9f4d 100644
--- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeTemplate.java
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeTemplate.java
@@ -20,30 +20,46 @@ package ca.uhn.fhir.narrative2;
* #L%
*/
-import ca.uhn.fhir.i18n.Msg;
-import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
import org.hl7.fhir.instance.model.api.IBase;
-import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
public class NarrativeTemplate implements INarrativeTemplate {
+ private final Set myAppliesToProfiles = new HashSet<>();
+ private final Set myAppliesToResourceTypes = new HashSet<>();
+ private final Set myAppliesToDataTypes = new HashSet<>();
+ private final Set> myAppliesToClasses = new HashSet<>();
+ private final Set myAppliesToFragmentNames = new HashSet<>();
private String myTemplateFileName;
- private Set myAppliesToProfiles = new HashSet<>();
- private Set myAppliesToResourceTypes = new HashSet<>();
- private Set myAppliesToDataTypes = new HashSet<>();
- private Set> myAppliesToClasses = new HashSet<>();
private TemplateTypeEnum myTemplateType = TemplateTypeEnum.THYMELEAF;
private String myContextPath;
private String myTemplateName;
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.SIMPLE_STYLE)
+ .append("name", myTemplateName)
+ .append("fileName", myTemplateFileName)
+ .toString();
+ }
+
public Set getAppliesToDataTypes() {
return Collections.unmodifiableSet(myAppliesToDataTypes);
}
+ public Set getAppliesToFragmentNames() {
+ return Collections.unmodifiableSet(myAppliesToFragmentNames);
+ }
+
+ void addAppliesToFragmentName(String theAppliesToFragmentName) {
+ myAppliesToFragmentNames.add(theAppliesToFragmentName);
+ }
+
@Override
public String getContextPath() {
return myContextPath;
@@ -109,11 +125,7 @@ public class NarrativeTemplate implements INarrativeTemplate {
@Override
public String getTemplateText() {
- try {
- return NarrativeTemplateManifest.loadResource(getTemplateFileName());
- } catch (IOException e) {
- throw new InternalErrorException(Msg.code(1866) + e);
- }
+ return NarrativeTemplateManifest.loadResource(getTemplateFileName());
}
void addAppliesToDatatype(String theDataType) {
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeTemplateManifest.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeTemplateManifest.java
index 7201b0d6fb4..e6b0cd80972 100644
--- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeTemplateManifest.java
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeTemplateManifest.java
@@ -23,31 +23,28 @@ package ca.uhn.fhir.narrative2;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.Msg;
-import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
+import ca.uhn.fhir.util.ClasspathUtil;
import com.google.common.base.Charsets;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Multimaps;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseResource;
+import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import javax.annotation.Nonnull;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
-import java.io.InputStream;
import java.io.StringReader;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.EnumSet;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Properties;
+import java.util.*;
+import java.util.function.Consumer;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@@ -55,36 +52,42 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
private static final Logger ourLog = LoggerFactory.getLogger(NarrativeTemplateManifest.class);
- private final Map> myResourceTypeToTemplate;
- private final Map> myDatatypeToTemplate;
- private final Map> myNameToTemplate;
- private final Map> myClassToTemplate;
+ private final ListMultimap myResourceTypeToTemplate;
+ private final ListMultimap myDatatypeToTemplate;
+ private final ListMultimap myNameToTemplate;
+ private final ListMultimap myFragmentNameToTemplate;
+ private final ListMultimap myClassToTemplate;
private final int myTemplateCount;
private NarrativeTemplateManifest(Collection theTemplates) {
- Map> resourceTypeToTemplate = new HashMap<>();
- Map> datatypeToTemplate = new HashMap<>();
- Map> nameToTemplate = new HashMap<>();
- Map> classToTemplate = new HashMap<>();
+ ListMultimap resourceTypeToTemplate = ArrayListMultimap.create();
+ ListMultimap datatypeToTemplate = ArrayListMultimap.create();
+ ListMultimap nameToTemplate = ArrayListMultimap.create();
+ ListMultimap classToTemplate = ArrayListMultimap.create();
+ ListMultimap fragmentNameToTemplate = ArrayListMultimap.create();
for (NarrativeTemplate nextTemplate : theTemplates) {
- nameToTemplate.computeIfAbsent(nextTemplate.getTemplateName(), t -> new ArrayList<>()).add(nextTemplate);
+ nameToTemplate.put(nextTemplate.getTemplateName(), nextTemplate);
for (String nextResourceType : nextTemplate.getAppliesToResourceTypes()) {
- resourceTypeToTemplate.computeIfAbsent(nextResourceType.toUpperCase(), t -> new ArrayList<>()).add(nextTemplate);
+ resourceTypeToTemplate.put(nextResourceType.toUpperCase(), nextTemplate);
}
for (String nextDataType : nextTemplate.getAppliesToDataTypes()) {
- datatypeToTemplate.computeIfAbsent(nextDataType.toUpperCase(), t -> new ArrayList<>()).add(nextTemplate);
+ datatypeToTemplate.put(nextDataType.toUpperCase(), nextTemplate);
}
for (Class extends IBase> nextAppliesToClass : nextTemplate.getAppliesToClasses()) {
- classToTemplate.computeIfAbsent(nextAppliesToClass.getName(), t -> new ArrayList<>()).add(nextTemplate);
+ classToTemplate.put(nextAppliesToClass.getName(), nextTemplate);
+ }
+ for (String nextFragmentName : nextTemplate.getAppliesToFragmentNames()) {
+ fragmentNameToTemplate.put(nextFragmentName, nextTemplate);
}
}
myTemplateCount = theTemplates.size();
- myClassToTemplate = makeImmutable(classToTemplate);
- myNameToTemplate = makeImmutable(nameToTemplate);
- myResourceTypeToTemplate = makeImmutable(resourceTypeToTemplate);
- myDatatypeToTemplate = makeImmutable(datatypeToTemplate);
+ myClassToTemplate = Multimaps.unmodifiableListMultimap(classToTemplate);
+ myNameToTemplate = Multimaps.unmodifiableListMultimap(nameToTemplate);
+ myResourceTypeToTemplate = Multimaps.unmodifiableListMultimap(resourceTypeToTemplate);
+ myDatatypeToTemplate = Multimaps.unmodifiableListMultimap(datatypeToTemplate);
+ myFragmentNameToTemplate = Multimaps.unmodifiableListMultimap(fragmentNameToTemplate);
}
public int getNamedTemplateCount() {
@@ -92,35 +95,56 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
}
@Override
- public List getTemplateByResourceName(FhirContext theFhirContext, EnumSet theStyles, String theResourceName) {
- return getFromMap(theStyles, theResourceName.toUpperCase(), myResourceTypeToTemplate);
+ public List getTemplateByResourceName(@Nonnull FhirContext theFhirContext, @Nonnull EnumSet theStyles, @Nonnull String theResourceName, @Nonnull Collection theProfiles) {
+ return getFromMap(theStyles, theResourceName.toUpperCase(), myResourceTypeToTemplate, theProfiles);
}
@Override
- public List getTemplateByName(FhirContext theFhirContext, EnumSet theStyles, String theName) {
- return getFromMap(theStyles, theName, myNameToTemplate);
+ public List getTemplateByName(@Nonnull FhirContext theFhirContext, @Nonnull EnumSet theStyles, @Nonnull String theName) {
+ return getFromMap(theStyles, theName, myNameToTemplate, Collections.emptyList());
}
@Override
- public List getTemplateByElement(FhirContext theFhirContext, EnumSet theStyles, IBase theElement) {
- List retVal = getFromMap(theStyles, theElement.getClass().getName(), myClassToTemplate);
+ public List getTemplateByFragmentName(@Nonnull FhirContext theFhirContext, @Nonnull EnumSet theStyles, @Nonnull String theFragmentName) {
+ return getFromMap(theStyles, theFragmentName, myFragmentNameToTemplate, Collections.emptyList());
+ }
+
+ @SuppressWarnings("PatternVariableCanBeUsed")
+ @Override
+ public List getTemplateByElement(@Nonnull FhirContext theFhirContext, @Nonnull EnumSet theStyles, @Nonnull IBase theElement) {
+ List retVal = Collections.emptyList();
+
+ if (theElement instanceof IBaseResource) {
+ IBaseResource resource = (IBaseResource) theElement;
+ String resourceName = theFhirContext.getResourceDefinition(resource).getName();
+ List profiles = resource
+ .getMeta()
+ .getProfile()
+ .stream()
+ .filter(Objects::nonNull)
+ .map(IPrimitiveType::getValueAsString)
+ .filter(StringUtils::isNotBlank)
+ .collect(Collectors.toList());
+ retVal = getTemplateByResourceName(theFhirContext, theStyles, resourceName, profiles);
+ }
+
if (retVal.isEmpty()) {
- if (theElement instanceof IBaseResource) {
- String resourceName = theFhirContext.getResourceDefinition((IBaseResource) theElement).getName();
- retVal = getTemplateByResourceName(theFhirContext, theStyles, resourceName);
- } else {
- String datatypeName = theFhirContext.getElementDefinition(theElement.getClass()).getName();
- retVal = getFromMap(theStyles, datatypeName.toUpperCase(), myDatatypeToTemplate);
- }
+ retVal = getFromMap(theStyles, theElement.getClass().getName(), myClassToTemplate, Collections.emptyList());
+ }
+
+ if (retVal.isEmpty()) {
+ String datatypeName = theFhirContext.getElementDefinition(theElement.getClass()).getName();
+ retVal = getFromMap(theStyles, datatypeName.toUpperCase(), myDatatypeToTemplate, Collections.emptyList());
}
return retVal;
}
- public static NarrativeTemplateManifest forManifestFileLocation(String... thePropertyFilePaths) throws IOException {
+
+ public static NarrativeTemplateManifest forManifestFileLocation(String... thePropertyFilePaths) {
return forManifestFileLocation(Arrays.asList(thePropertyFilePaths));
}
- public static NarrativeTemplateManifest forManifestFileLocation(Collection thePropertyFilePaths) throws IOException {
+ public static NarrativeTemplateManifest forManifestFileLocation(Collection thePropertyFilePaths) {
ourLog.debug("Loading narrative properties file(s): {}", thePropertyFilePaths);
List manifestFileContents = new ArrayList<>(thePropertyFilePaths.size());
@@ -132,18 +156,23 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
return forManifestFileContents(manifestFileContents);
}
- public static NarrativeTemplateManifest forManifestFileContents(String... theResources) throws IOException {
+ public static NarrativeTemplateManifest forManifestFileContents(String... theResources) {
return forManifestFileContents(Arrays.asList(theResources));
}
- public static NarrativeTemplateManifest forManifestFileContents(Collection theResources) throws IOException {
- List templates = new ArrayList<>();
- for (String next : theResources) {
- templates.addAll(loadProperties(next));
+ public static NarrativeTemplateManifest forManifestFileContents(Collection theResources) {
+ try {
+ List templates = new ArrayList<>();
+ for (String next : theResources) {
+ templates.addAll(loadProperties(next));
+ }
+ return new NarrativeTemplateManifest(templates);
+ } catch (IOException e) {
+ throw new InternalErrorException(Msg.code(1808) + e);
}
- return new NarrativeTemplateManifest(templates);
}
+ @SuppressWarnings("unchecked")
private static Collection loadProperties(String theManifestText) throws IOException {
Map nameToTemplate = new HashMap<>();
@@ -164,7 +193,7 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
try {
nextTemplate.addAppliesToClass((Class extends IBase>) Class.forName(className));
} catch (ClassNotFoundException theE) {
- throw new InternalErrorException(Msg.code(1867) + "Could not find class " + className + " declared in narative manifest");
+ throw new InternalErrorException(Msg.code(1867) + "Could not find class " + className + " declared in narrative manifest");
}
}
} else if (nextKey.endsWith(".profile")) {
@@ -174,18 +203,13 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
}
} else if (nextKey.endsWith(".resourceType")) {
String resourceType = file.getProperty(nextKey);
- Arrays
- .stream(resourceType.split(","))
- .map(t -> t.trim())
- .filter(t -> isNotBlank(t))
- .forEach(t -> nextTemplate.addAppliesToResourceType(t));
+ parseValuesAndAddToMap(resourceType, nextTemplate::addAppliesToResourceType);
+ } else if (nextKey.endsWith(".fragmentName")) {
+ String resourceType = file.getProperty(nextKey);
+ parseValuesAndAddToMap(resourceType, nextTemplate::addAppliesToFragmentName);
} else if (nextKey.endsWith(".dataType")) {
String dataType = file.getProperty(nextKey);
- Arrays
- .stream(dataType.split(","))
- .map(t -> t.trim())
- .filter(t -> isNotBlank(t))
- .forEach(t -> nextTemplate.addAppliesToDatatype(t));
+ parseValuesAndAddToMap(dataType, nextTemplate::addAppliesToDatatype);
} else if (nextKey.endsWith(".style")) {
String templateTypeName = file.getProperty(nextKey).toUpperCase();
TemplateTypeEnum templateType = TemplateTypeEnum.valueOf(templateTypeName);
@@ -203,8 +227,8 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
ourLog.debug("Ignoring title property as narrative generator no longer generates titles: {}", nextKey);
} else {
throw new ConfigurationException(Msg.code(1868) + "Invalid property name: " + nextKey
- + " - the key must end in one of the expected extensions "
- + "'.profile', '.resourceType', '.dataType', '.style', '.contextPath', '.narrative', '.title'");
+ + " - the key must end in one of the expected extensions "
+ + "'.profile', '.resourceType', '.dataType', '.style', '.contextPath', '.narrative', '.title'");
}
}
@@ -212,44 +236,39 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
return nameToTemplate.values();
}
- static String loadResource(String name) throws IOException {
- if (name.startsWith("classpath:")) {
- String cpName = name.substring("classpath:".length());
- try (InputStream resource = DefaultThymeleafNarrativeGenerator.class.getResourceAsStream(cpName)) {
- if (resource == null) {
- try (InputStream resource2 = DefaultThymeleafNarrativeGenerator.class.getResourceAsStream("/" + cpName)) {
- if (resource2 == null) {
- throw new IOException(Msg.code(1869) + "Can not find '" + cpName + "' on classpath");
- }
- return IOUtils.toString(resource2, Charsets.UTF_8);
- }
- }
- return IOUtils.toString(resource, Charsets.UTF_8);
- }
- } else if (name.startsWith("file:")) {
- File file = new File(name.substring("file:".length()));
+ private static void parseValuesAndAddToMap(String resourceType, Consumer addAppliesToResourceType) {
+ Arrays
+ .stream(resourceType.split(","))
+ .map(String::trim)
+ .filter(StringUtils::isNotBlank)
+ .forEach(addAppliesToResourceType);
+ }
+
+ static String loadResource(String theName) {
+ if (theName.startsWith("classpath:")) {
+ return ClasspathUtil.loadResource(theName);
+ } else if (theName.startsWith("file:")) {
+ File file = new File(theName.substring("file:".length()));
if (file.exists() == false) {
- throw new IOException(Msg.code(1870) + "File not found: " + file.getAbsolutePath());
+ throw new InternalErrorException(Msg.code(1870) + "File not found: " + file.getAbsolutePath());
}
try (FileInputStream inputStream = new FileInputStream(file)) {
return IOUtils.toString(inputStream, Charsets.UTF_8);
+ } catch (IOException e) {
+ throw new InternalErrorException(Msg.code(1869) + e.getMessage(), e);
}
} else {
- throw new IOException(Msg.code(1871) + "Invalid resource name: '" + name + "' (must start with classpath: or file: )");
+ throw new InternalErrorException(Msg.code(1871) + "Invalid resource name: '" + theName + "' (must start with classpath: or file: )");
}
}
- private static List getFromMap(EnumSet theStyles, T theKey, Map> theMap) {
+ private static List getFromMap(EnumSet theStyles, T theKey, ListMultimap theMap, Collection theProfiles) {
return theMap
- .getOrDefault(theKey, Collections.emptyList())
- .stream()
- .filter(t -> theStyles.contains(t.getTemplateType()))
- .collect(Collectors.toList());
- }
-
- private static Map> makeImmutable(Map> theStyleToResourceTypeToTemplate) {
- theStyleToResourceTypeToTemplate.replaceAll((key, value) -> Collections.unmodifiableList(value));
- return Collections.unmodifiableMap(theStyleToResourceTypeToTemplate);
+ .get(theKey)
+ .stream()
+ .filter(t -> theStyles.contains(t.getTemplateType()))
+ .filter(t -> theProfiles.isEmpty() || t.getAppliesToProfiles().stream().anyMatch(theProfiles::contains))
+ .collect(Collectors.toList());
}
}
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NullNarrativeGenerator.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NullNarrativeGenerator.java
index 5b37cf3b60a..3ff366724ce 100644
--- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NullNarrativeGenerator.java
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NullNarrativeGenerator.java
@@ -29,4 +29,9 @@ public class NullNarrativeGenerator implements INarrativeGenerator {
public boolean populateResourceNarrative(FhirContext theFhirContext, IBaseResource theResource) {
return false;
}
+
+ @Override
+ public String generateResourceNarrative(FhirContext theFhirContext, IBaseResource theResource) {
+ return null;
+ }
}
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/ThymeleafNarrativeGenerator.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/ThymeleafNarrativeGenerator.java
deleted file mode 100644
index 7fc96d179cf..00000000000
--- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/ThymeleafNarrativeGenerator.java
+++ /dev/null
@@ -1,209 +0,0 @@
-package ca.uhn.fhir.narrative2;
-
-/*-
- * #%L
- * HAPI FHIR - Core Library
- * %%
- * Copyright (C) 2014 - 2023 Smile CDR, Inc.
- * %%
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- * #L%
- */
-
-import ca.uhn.fhir.context.FhirContext;
-import ca.uhn.fhir.i18n.Msg;
-import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
-import org.hl7.fhir.instance.model.api.IBase;
-import org.thymeleaf.IEngineConfiguration;
-import org.thymeleaf.TemplateEngine;
-import org.thymeleaf.cache.AlwaysValidCacheEntryValidity;
-import org.thymeleaf.cache.ICacheEntryValidity;
-import org.thymeleaf.context.Context;
-import org.thymeleaf.context.ITemplateContext;
-import org.thymeleaf.engine.AttributeName;
-import org.thymeleaf.messageresolver.IMessageResolver;
-import org.thymeleaf.model.IProcessableElementTag;
-import org.thymeleaf.processor.IProcessor;
-import org.thymeleaf.processor.element.AbstractAttributeTagProcessor;
-import org.thymeleaf.processor.element.AbstractElementTagProcessor;
-import org.thymeleaf.processor.element.IElementTagStructureHandler;
-import org.thymeleaf.standard.StandardDialect;
-import org.thymeleaf.standard.expression.IStandardExpression;
-import org.thymeleaf.standard.expression.IStandardExpressionParser;
-import org.thymeleaf.standard.expression.StandardExpressions;
-import org.thymeleaf.templatemode.TemplateMode;
-import org.thymeleaf.templateresolver.DefaultTemplateResolver;
-import org.thymeleaf.templateresource.ITemplateResource;
-import org.thymeleaf.templateresource.StringTemplateResource;
-
-import java.util.EnumSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import static org.apache.commons.lang3.StringUtils.isNotBlank;
-
-public class ThymeleafNarrativeGenerator extends BaseNarrativeGenerator {
-
- private IMessageResolver myMessageResolver;
-
- /**
- * Constructor
- */
- public ThymeleafNarrativeGenerator() {
- super();
- }
-
- private TemplateEngine getTemplateEngine(FhirContext theFhirContext) {
- TemplateEngine engine = new TemplateEngine();
- ProfileResourceResolver resolver = new ProfileResourceResolver(theFhirContext);
- engine.setTemplateResolver(resolver);
- if (myMessageResolver != null) {
- engine.setMessageResolver(myMessageResolver);
- }
- StandardDialect dialect = new StandardDialect() {
- @Override
- public Set getProcessors(String theDialectPrefix) {
- Set retVal = super.getProcessors(theDialectPrefix);
- retVal.add(new NarrativeTagProcessor(theFhirContext, theDialectPrefix));
- retVal.add(new NarrativeAttributeProcessor(theDialectPrefix, theFhirContext));
- return retVal;
- }
-
- };
-
- engine.setDialect(dialect);
- return engine;
- }
-
- @Override
- protected String applyTemplate(FhirContext theFhirContext, INarrativeTemplate theTemplate, IBase theTargetContext) {
-
- Context context = new Context();
- context.setVariable("resource", theTargetContext);
- context.setVariable("context", theTargetContext);
- context.setVariable("fhirVersion", theFhirContext.getVersion().getVersion().name());
-
- return getTemplateEngine(theFhirContext).process(theTemplate.getTemplateName(), context);
- }
-
-
- @Override
- protected EnumSet getStyle() {
- return EnumSet.of(TemplateTypeEnum.THYMELEAF);
- }
-
- private String applyTemplateWithinTag(FhirContext theFhirContext, ITemplateContext theTemplateContext, String theName, String theElement) {
- IEngineConfiguration configuration = theTemplateContext.getConfiguration();
- IStandardExpressionParser expressionParser = StandardExpressions.getExpressionParser(configuration);
- final IStandardExpression expression = expressionParser.parseExpression(theTemplateContext, theElement);
- Object elementValueObj = expression.execute(theTemplateContext);
- final IBase elementValue = (IBase) elementValueObj;
- if (elementValue == null) {
- return "";
- }
-
- List templateOpt;
- if (isNotBlank(theName)) {
- templateOpt = getManifest().getTemplateByName(theFhirContext, getStyle(), theName);
- if (templateOpt.isEmpty()) {
- throw new InternalErrorException(Msg.code(1863) + "Unknown template name: " + theName);
- }
- } else {
- templateOpt = getManifest().getTemplateByElement(theFhirContext, getStyle(), elementValue);
- if (templateOpt.isEmpty()) {
- throw new InternalErrorException(Msg.code(1864) + "No template for type: " + elementValue.getClass());
- }
- }
-
- return applyTemplate(theFhirContext, templateOpt.get(0), elementValue);
- }
-
- public void setMessageResolver(IMessageResolver theMessageResolver) {
- myMessageResolver = theMessageResolver;
- }
-
-
- private class ProfileResourceResolver extends DefaultTemplateResolver {
- private final FhirContext myFhirContext;
-
- private ProfileResourceResolver(FhirContext theFhirContext) {
- myFhirContext = theFhirContext;
- }
-
- @Override
- protected boolean computeResolvable(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map theTemplateResolutionAttributes) {
- return getManifest().getTemplateByName(myFhirContext, getStyle(), theTemplate).size() > 0;
- }
-
- @Override
- protected TemplateMode computeTemplateMode(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map theTemplateResolutionAttributes) {
- return TemplateMode.XML;
- }
-
- @Override
- protected ITemplateResource computeTemplateResource(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map theTemplateResolutionAttributes) {
- return getManifest()
- .getTemplateByName(myFhirContext, getStyle(), theTemplate)
- .stream()
- .findFirst()
- .map(t -> new StringTemplateResource(t.getTemplateText()))
- .orElseThrow(() -> new IllegalArgumentException("Unknown template: " + theTemplate));
- }
-
- @Override
- protected ICacheEntryValidity computeValidity(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map theTemplateResolutionAttributes) {
- return AlwaysValidCacheEntryValidity.INSTANCE;
- }
- }
-
- private class NarrativeTagProcessor extends AbstractElementTagProcessor {
-
- private final FhirContext myFhirContext;
-
- NarrativeTagProcessor(FhirContext theFhirContext, String dialectPrefix) {
- super(TemplateMode.XML, dialectPrefix, "narrative", true, null, true, 0);
- myFhirContext = theFhirContext;
- }
-
- @Override
- protected void doProcess(ITemplateContext theTemplateContext, IProcessableElementTag theTag, IElementTagStructureHandler theStructureHandler) {
- String name = theTag.getAttributeValue("th:name");
- String element = theTag.getAttributeValue("th:element");
-
- String appliedTemplate = applyTemplateWithinTag(myFhirContext, theTemplateContext, name, element);
- theStructureHandler.replaceWith(appliedTemplate, false);
- }
- }
-
- /**
- * This is a thymeleaf extension that allows people to do things like
- *
- */
- private class NarrativeAttributeProcessor extends AbstractAttributeTagProcessor {
-
- private final FhirContext myFhirContext;
-
- NarrativeAttributeProcessor(String theDialectPrefix, FhirContext theFhirContext) {
- super(TemplateMode.XML, theDialectPrefix, null, false, "narrative", true, 0, true);
- myFhirContext = theFhirContext;
- }
-
- @Override
- protected void doProcess(ITemplateContext theContext, IProcessableElementTag theTag, AttributeName theAttributeName, String theAttributeValue, IElementTagStructureHandler theStructureHandler) {
- String text = applyTemplateWithinTag(myFhirContext, theContext, null, theAttributeValue);
- theStructureHandler.setBody(text, false);
- }
-
- }
-}
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleBuilder.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleBuilder.java
index 3521803c23e..4fbf6261fb3 100644
--- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleBuilder.java
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleBuilder.java
@@ -35,6 +35,8 @@ import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.util.Date;
import java.util.Objects;
/**
@@ -144,8 +146,9 @@ public class BundleBuilder {
/**
* Adds a FHIRPatch patch bundle to the transaction
+ *
* @param theTarget The target resource ID to patch
- * @param thePatch The FHIRPath Parameters resource
+ * @param thePatch The FHIRPath Parameters resource
* @since 6.3.0
*/
public PatchBuilder addTransactionFhirPatchEntry(IIdType theTarget, IBaseParameters thePatch) {
@@ -162,10 +165,10 @@ public class BundleBuilder {
* Adds a FHIRPatch patch bundle to the transaction. This method is intended for conditional PATCH operations. If you
* know the ID of the resource you wish to patch, use {@link #addTransactionFhirPatchEntry(IIdType, IBaseParameters)}
* instead.
- *
+ *
* @param thePatch The FHIRPath Parameters resource
- * @since 6.3.0
* @see #addTransactionFhirPatchEntry(IIdType, IBaseParameters)
+ * @since 6.3.0
*/
public PatchBuilder addTransactionFhirPatchEntry(IBaseParameters thePatch) {
IPrimitiveType> url = addAndPopulateTransactionBundleEntryRequest(thePatch, null, null, "PATCH");
@@ -324,6 +327,14 @@ public class BundleBuilder {
addEntryAndReturnRequest(theResource, theResource.getIdElement().getValue());
}
+ /**
+ * Adds an entry for a Document bundle type
+ */
+ public void addDocumentEntry(IBaseResource theResource) {
+ setType("document");
+ addEntryAndReturnRequest(theResource, theResource.getIdElement().getValue());
+ }
+
/**
* Creates new entry and adds it to the bundle
*
@@ -460,6 +471,30 @@ public class BundleBuilder {
setBundleField("type", theType);
}
+ /**
+ * Adds an identifier to Bundle.identifier
+ *
+ * @param theSystem The system
+ * @param theValue The value
+ * @since 6.4.0
+ */
+ public void setIdentifier(@Nullable String theSystem, @Nullable String theValue) {
+ FhirTerser terser = myContext.newTerser();
+ IBase identifier = terser.addElement(myBundle, "identifier");
+ terser.setElement(identifier, "system", theSystem);
+ terser.setElement(identifier, "value", theValue);
+ }
+
+ /**
+ * Sets the timestamp in Bundle.timestamp
+ *
+ * @since 6.4.0
+ */
+ public void setTimestamp(@Nonnull IPrimitiveType theTimestamp) {
+ FhirTerser terser = myContext.newTerser();
+ terser.setElement(myBundle, "Bundle.timestamp", theTimestamp.getValueAsString());
+ }
+
public class DeleteBuilder extends BaseOperationBuilder {
@@ -486,7 +521,7 @@ public class BundleBuilder {
public class CreateBuilder extends BaseOperationBuilder {
private final IBase myRequest;
- CreateBuilder(IBase theRequest) {
+ CreateBuilder(IBase theRequest) {
myRequest = theRequest;
}
@@ -509,7 +544,7 @@ public class BundleBuilder {
/**
* Returns a reference to the BundleBuilder instance.
- *
+ *
* Calling this method has no effect at all, it is only
* provided for easy method chaning if you want to build
* your bundle as a single fluent call.
@@ -527,7 +562,7 @@ public class BundleBuilder {
private final IPrimitiveType> myUrl;
- BaseOperationBuilderWithConditionalUrl(IPrimitiveType> theUrl) {
+ BaseOperationBuilderWithConditionalUrl(IPrimitiveType> theUrl) {
myUrl = theUrl;
}
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ClasspathUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ClasspathUtil.java
index 12d7ef2043e..1df46e78737 100644
--- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ClasspathUtil.java
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ClasspathUtil.java
@@ -118,9 +118,24 @@ public class ClasspathUtil {
return loadResource(theClasspath, streamTransform);
}
+ /**
+ * Load a classpath resource, throw an {@link InternalErrorException} if not found
+ *
+ * @since 6.4.0
+ */
+ @Nonnull
+ public static T loadCompressedResource(FhirContext theCtx, Class theType, String theClasspath) {
+ String resource = loadCompressedResource(theClasspath);
+ return parseResource(theCtx, theType, resource);
+ }
+
@Nonnull
public static T loadResource(FhirContext theCtx, Class theType, String theClasspath) {
String raw = loadResource(theClasspath);
+ return parseResource(theCtx, theType, raw);
+ }
+
+ private static T parseResource(FhirContext theCtx, Class theType, String raw) {
return EncodingEnum.detectEncodingNoDefault(raw).newParser(theCtx).parseResource(theType, raw);
}
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/CompositionBuilder.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/CompositionBuilder.java
new file mode 100644
index 00000000000..49589864e55
--- /dev/null
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/CompositionBuilder.java
@@ -0,0 +1,174 @@
+package ca.uhn.fhir.util;
+
+/*-
+ * #%L
+ * HAPI FHIR - Core Library
+ * %%
+ * Copyright (C) 2014 - 2023 Smile CDR, Inc.
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import ca.uhn.fhir.context.FhirContext;
+import ca.uhn.fhir.context.RuntimeResourceDefinition;
+import org.hl7.fhir.instance.model.api.IBase;
+import org.hl7.fhir.instance.model.api.IBaseCoding;
+import org.hl7.fhir.instance.model.api.IBaseReference;
+import org.hl7.fhir.instance.model.api.IBaseResource;
+import org.hl7.fhir.instance.model.api.IIdType;
+import org.hl7.fhir.instance.model.api.IPrimitiveType;
+
+import javax.annotation.Nonnull;
+import java.util.Date;
+
+/**
+ * This class can be used to generate Composition resources in
+ * a version independent way.
+ *
+ * @since 6.4.0
+ */
+public class CompositionBuilder {
+
+ private final FhirContext myCtx;
+ private final IBaseResource myComposition;
+ private final RuntimeResourceDefinition myCompositionDef;
+ private final FhirTerser myTerser;
+
+ public CompositionBuilder(@Nonnull FhirContext theFhirContext) {
+ myCtx = theFhirContext;
+ myCompositionDef = myCtx.getResourceDefinition("Composition");
+ myTerser = myCtx.newTerser();
+ myComposition = myCompositionDef.newInstance();
+ }
+
+ @SuppressWarnings("unchecked")
+ public T getComposition() {
+ return (T) myComposition;
+ }
+
+ /**
+ * Add a value to Composition.author
+ */
+ public void addAuthor(IIdType theAuthorId) {
+ IBaseReference reference = myTerser.addElement(myComposition, "Composition.author");
+ reference.setReference(theAuthorId.getValue());
+ }
+
+ /**
+ * Set a value in Composition.status
+ */
+ public void setStatus(String theStatusCode) {
+ myTerser.setElement(myComposition, "Composition.status", theStatusCode);
+ }
+
+
+ /**
+ * Set a value in Composition.subject
+ */
+ public void setSubject(IIdType theSubject) {
+ myTerser.setElement(myComposition, "Composition.subject.reference", theSubject.getValue());
+ }
+
+ /**
+ * Add a Coding to Composition.type.coding
+ */
+ public void addTypeCoding(String theSystem, String theCode, String theDisplay) {
+ IBaseCoding coding = myTerser.addElement(myComposition, "Composition.type.coding");
+ coding.setCode(theCode);
+ coding.setSystem(theSystem);
+ coding.setDisplay(theDisplay);
+ }
+
+ /**
+ * Set a value in Composition.date
+ */
+ public void setDate(IPrimitiveType theDate) {
+ myTerser.setElement(myComposition, "Composition.date", theDate.getValueAsString());
+ }
+
+ /**
+ * Set a value in Composition.title
+ */
+ public void setTitle(String theTitle) {
+ myTerser.setElement(myComposition, "Composition.title", theTitle);
+ }
+
+ /**
+ * Set a value in Composition.confidentiality
+ */
+ public void setConfidentiality(String theConfidentiality) {
+ myTerser.setElement(myComposition, "Composition.confidentiality", theConfidentiality);
+ }
+
+ /**
+ * Set a value in Composition.id
+ */
+ public void setId(IIdType theId) {
+ myComposition.setId(theId.getValue());
+ }
+
+ public SectionBuilder addSection() {
+ IBase section = myTerser.addElement(myComposition, "Composition.section");
+ return new SectionBuilder(section);
+ }
+
+ public class SectionBuilder {
+
+ private final IBase mySection;
+
+ /**
+ * Constructor
+ */
+ private SectionBuilder(IBase theSection) {
+ mySection = theSection;
+ }
+
+ /**
+ * Sets the section title
+ */
+ public void setTitle(String theTitle) {
+ myTerser.setElement(mySection, "title", theTitle);
+ }
+
+ /**
+ * Add a coding to section.code
+ */
+ public void addCodeCoding(String theSystem, String theCode, String theDisplay) {
+ IBaseCoding coding = myTerser.addElement(mySection, "code.coding");
+ coding.setCode(theCode);
+ coding.setSystem(theSystem);
+ coding.setDisplay(theDisplay);
+ }
+
+ /**
+ * Adds a reference to entry.reference
+ */
+ public void addEntry(IIdType theReference) {
+ IBaseReference entry = myTerser.addElement(mySection, "entry");
+ entry.setReference(theReference.getValue());
+ }
+
+ /**
+ * Adds narrative text to the section
+ */
+ public void setText(String theStatus, String theDivHtml) {
+ IBase text = myTerser.addElement(mySection, "text");
+ myTerser.setElement(text, "status", theStatus);
+ myTerser.setElement(text, "div", theDivHtml);
+ }
+ }
+
+
+}
+
diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/batch/log/Logs.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/Logs.java
similarity index 74%
rename from hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/batch/log/Logs.java
rename to hapi-fhir-base/src/main/java/ca/uhn/fhir/util/Logs.java
index 6432ae69de7..4da6fa01edb 100644
--- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/batch/log/Logs.java
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/Logs.java
@@ -1,8 +1,8 @@
-package ca.uhn.fhir.jpa.batch.log;
+package ca.uhn.fhir.util;
/*-
* #%L
- * HAPI FHIR Storage api
+ * HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
@@ -25,8 +25,13 @@ import org.slf4j.LoggerFactory;
public class Logs {
private static final Logger ourBatchTroubleshootingLog = LoggerFactory.getLogger("ca.uhn.fhir.log.batch_troubleshooting");
+ private static final Logger ourNarrativeGenerationTroubleshootingLog = LoggerFactory.getLogger("ca.uhn.fhir.log.narrative_generation_troubleshooting");
public static Logger getBatchTroubleshootingLog() {
return ourBatchTroubleshootingLog;
}
+
+ public static Logger getNarrativeGenerationTroubleshootingLog() {
+ return ourBatchTroubleshootingLog;
+ }
}
diff --git a/hapi-fhir-bom/pom.xml b/hapi-fhir-bom/pom.xml
index 8a8554894a7..6ee77e7c3af 100644
--- a/hapi-fhir-bom/pom.xml
+++ b/hapi-fhir-bom/pom.xml
@@ -4,14 +4,14 @@
4.0.0ca.uhn.hapi.fhirhapi-fhir-bom
- 6.3.12-SNAPSHOT
+ 6.3.13-SNAPSHOTpomHAPI FHIR BOMca.uhn.hapi.fhirhapi-deployable-pom
- 6.3.12-SNAPSHOT
+ 6.3.13-SNAPSHOT../hapi-deployable-pom/pom.xml
@@ -77,6 +77,11 @@
hapi-fhir-jpaserver-elastic-test-utilities${project.version}
+
+ ${project.groupId}
+ hapi-fhir-jpaserver-ips
+ ${project.version}
+ ${project.groupId}hapi-fhir-jpaserver-mdm
diff --git a/hapi-fhir-checkstyle/pom.xml b/hapi-fhir-checkstyle/pom.xml
index b08a7edf751..b85d38797be 100644
--- a/hapi-fhir-checkstyle/pom.xml
+++ b/hapi-fhir-checkstyle/pom.xml
@@ -5,7 +5,7 @@
ca.uhn.hapi.fhirhapi-fhir
- 6.3.12-SNAPSHOT
+ 6.3.13-SNAPSHOT../pom.xml
diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml
index 31c4fa52bb1..945799435ba 100644
--- a/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml
+++ b/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml
@@ -4,7 +4,7 @@
ca.uhn.hapi.fhirhapi-deployable-pom
- 6.3.12-SNAPSHOT
+ 6.3.13-SNAPSHOT../../hapi-deployable-pom/pom.xml
diff --git a/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml
index dd76d6eef33..15fd8f03efe 100644
--- a/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml
+++ b/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml
@@ -6,7 +6,7 @@
ca.uhn.hapi.fhirhapi-fhir-cli
- 6.3.12-SNAPSHOT
+ 6.3.13-SNAPSHOT../pom.xml
diff --git a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml
index 1f15b94df7e..af41b578c7e 100644
--- a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml
+++ b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml
@@ -6,7 +6,7 @@
ca.uhn.hapi.fhirhapi-deployable-pom
- 6.3.12-SNAPSHOT
+ 6.3.13-SNAPSHOT../../hapi-deployable-pom
diff --git a/hapi-fhir-cli/pom.xml b/hapi-fhir-cli/pom.xml
index 0b11c25bc7f..520224f7cd4 100644
--- a/hapi-fhir-cli/pom.xml
+++ b/hapi-fhir-cli/pom.xml
@@ -5,7 +5,7 @@
ca.uhn.hapi.fhirhapi-fhir
- 6.3.12-SNAPSHOT
+ 6.3.13-SNAPSHOT../pom.xml
diff --git a/hapi-fhir-client-okhttp/pom.xml b/hapi-fhir-client-okhttp/pom.xml
index 097affebf5d..8befbdd4228 100644
--- a/hapi-fhir-client-okhttp/pom.xml
+++ b/hapi-fhir-client-okhttp/pom.xml
@@ -4,7 +4,7 @@
ca.uhn.hapi.fhirhapi-deployable-pom
- 6.3.12-SNAPSHOT
+ 6.3.13-SNAPSHOT../hapi-deployable-pom/pom.xml
diff --git a/hapi-fhir-client/pom.xml b/hapi-fhir-client/pom.xml
index 55b19a9789f..a80ba2c725d 100644
--- a/hapi-fhir-client/pom.xml
+++ b/hapi-fhir-client/pom.xml
@@ -4,7 +4,7 @@
ca.uhn.hapi.fhirhapi-deployable-pom
- 6.3.12-SNAPSHOT
+ 6.3.13-SNAPSHOT../hapi-deployable-pom/pom.xml
diff --git a/hapi-fhir-converter/pom.xml b/hapi-fhir-converter/pom.xml
index 3d448d1c47d..988f1031258 100644
--- a/hapi-fhir-converter/pom.xml
+++ b/hapi-fhir-converter/pom.xml
@@ -5,7 +5,7 @@
ca.uhn.hapi.fhirhapi-deployable-pom
- 6.3.12-SNAPSHOT
+ 6.3.13-SNAPSHOT../hapi-deployable-pom/pom.xml
diff --git a/hapi-fhir-dist/pom.xml b/hapi-fhir-dist/pom.xml
index fea8a625d05..3a31155caa9 100644
--- a/hapi-fhir-dist/pom.xml
+++ b/hapi-fhir-dist/pom.xml
@@ -5,7 +5,7 @@
ca.uhn.hapi.fhirhapi-fhir
- 6.3.12-SNAPSHOT
+ 6.3.13-SNAPSHOT../pom.xml
diff --git a/hapi-fhir-docs/pom.xml b/hapi-fhir-docs/pom.xml
index 0d5907f97b4..90acbc2a826 100644
--- a/hapi-fhir-docs/pom.xml
+++ b/hapi-fhir-docs/pom.xml
@@ -5,7 +5,7 @@
ca.uhn.hapi.fhirhapi-deployable-pom
- 6.3.12-SNAPSHOT
+ 6.3.13-SNAPSHOT../hapi-deployable-pom/pom.xml
@@ -60,6 +60,11 @@
hapi-fhir-server-openapi${project.version}
+
+ ca.uhn.hapi.fhir
+ hapi-fhir-test-utilities
+ ${project.version}
+ com.fasterxml.jackson.dataformat
diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4438-add-composition-builder.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4438-add-composition-builder.yaml
new file mode 100644
index 00000000000..872de97dbd8
--- /dev/null
+++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4438-add-composition-builder.yaml
@@ -0,0 +1,6 @@
+---
+type: add
+issue: 4438
+title: "A new utility called `CompositionBuilder` has been added. This class can be used to
+ generate Composition resources in a version-independent way, similar to the function
+ of `BundleUtil` for Bundle resources."
diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4438-add-fhirpath-evaluation-conetxt.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4438-add-fhirpath-evaluation-conetxt.yaml
new file mode 100644
index 00000000000..ce4c10f7d67
--- /dev/null
+++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4438-add-fhirpath-evaluation-conetxt.yaml
@@ -0,0 +1,5 @@
+---
+type: add
+issue: 4438
+title: "The FhirPath evaluator framework now allows for an optional evaluation context object
+ which can be used to supply external data such as responses to the `resolve()` function."
diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4438-add-ips-generator.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4438-add-ips-generator.yaml
new file mode 100644
index 00000000000..f4d6109f3d9
--- /dev/null
+++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4438-add-ips-generator.yaml
@@ -0,0 +1,9 @@
+---
+type: add
+issue: 4438
+title: "A new experimental API has been added for automated generation of
+ International Patient Summary (IPS) documents in the JPA server. This module
+ is based on the excellent work of Rio Bennin and Panayiotis Savva of the
+ University of Cyprus and was completed at FHIR Connectathon 34 in
+ Henderson Nevada.
+"
diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4438-add-thymeleaf-fragment-support.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4438-add-thymeleaf-fragment-support.yaml
new file mode 100644
index 00000000000..e1eed4fbe66
--- /dev/null
+++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4438-add-thymeleaf-fragment-support.yaml
@@ -0,0 +1,5 @@
+---
+type: add
+issue: 4438
+title: "The Thymeleaf narrative generator can now declare template fragments in separate files
+ so that they can be reused across multiple templates."
diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties
index 5ff1884ce0d..0f0e29c3c5a 100644
--- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties
+++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties
@@ -65,6 +65,7 @@ page.server_jpa.diff=Diff Operation
page.server_jpa.lastn=LastN Operation
page.server_jpa.elastic=Lucene/Elasticsearch Indexing
page.server_jpa.terminology=Terminology
+page.server_jpa.ips=International Patient Summary (IPS)
section.server_jpa_mdm.title=JPA Server: MDM
page.server_jpa_mdm.mdm=MDM Getting Started
diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/model/narrative_generation.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/model/narrative_generation.md
index 36fa258c477..68fccf51f7e 100644
--- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/model/narrative_generation.md
+++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/model/narrative_generation.md
@@ -97,3 +97,38 @@ Finally, use the [CustomThymeleafNarrativeGenerator](/hapi-fhir/apidocs/hapi-fhi
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/NarrativeGenerator.java|gen}}
```
+# Fragments Expressions in Thyemleaf Templates
+
+Thymeleaf has a concept called Fragments, which allow reusable template portions that can be imported anywhere you need them. It can be helpful to put these fragment definitions in their own file. For example, the following property file declares a template and a fragment:
+
+```properties
+{{snippet:classpath:ca/uhn/fhir/narrative/narrative-with-fragment.properties}}
+```
+
+The following template declares a fragment (this is `narrative-with-fragment-child.html` in the example above):
+
+```html
+{{snippet:classpath:ca/uhn/fhir/narrative/narrative-with-fragment-child.html}}
+```
+
+And the following template uses it (this is `narrative-with-fragment-child.html` in the example above):
+
+```html
+{{snippet:classpath:ca/uhn/fhir/narrative/narrative-with-fragment-parent.html}}
+```
+
+
+# FHIRPath Expressions in Thyemleaf Templates
+
+Thymeleaf templates can incorporate FHIRPath expressions using the `#fhirpath` expression object.
+
+This object has the following methods:
+
+* evaluateFirst(input, pathExpression) – This method returns the first element matched on `input` by the path expression, or _null_ if nothing matches.
+* evaluate(input, pathExpression) – This method returns a Java List of elements matched on `input` by the path expression, or an empty list if nothing matches.
+
+For example:
+
+```html
+{{snippet:classpath:ca/uhn/fhir/narrative/narratives-with-fhirpath-evaluate-single-primitive.html}}
+```
diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/elastic.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/elastic.md
index fd1a24eb0bd..9c8211067d5 100644
--- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/elastic.md
+++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/elastic.md
@@ -104,3 +104,14 @@ Note: This does not support the $meta-add or $meta-delete operations. Full reind
when this option is enabled after resources have been indexed.
This **experimental** feature is enabled via the `setStoreResourceInHSearchIndex()` option of DaoConfig.
+
+# Synchronous Writes
+
+ElasticSearch writes are asynchronous by default. This means that when writing to an ElasticSearch instance (independent of HAPI FHIR), the data you write will not be available to subsequent reads for a short period of time while the indexes synchronize.
+
+ElasticSearch states that this behaviour leads to better overall performance and throughput on the system.
+
+This can cause issues, particularly in unit tests where data is being examined shortly after it is written.
+
+You can force synchronous writing to them in HAPI FHIR JPA by setting the Hibernate Search [synchronization strategy](https://docs.jboss.org/hibernate/stable/search/reference/en-US/html_single/#mapper-orm-indexing-automatic-synchronization). This setting is internally setting the ElasticSearch [refresh=wait_for](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-refresh.html) option. Be warned that this will have a negative impact on overall performance. THE HAPI FHIR TEAM has not tried to quantify this impact but the ElasticSearch docs seem to make a fairly big deal about it.
+
diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/ips.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/ips.md
new file mode 100644
index 00000000000..67cfe65c68b
--- /dev/null
+++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/ips.md
@@ -0,0 +1,63 @@
+# International Patient Summary (IPS) Generator
+
+The International Patient Summary (IPS) is an international collaborative effort to develop a specification for a health record summary extract. It is specified in the standards EN 17269 and ISO 27269, and supported in FHIR through the [International Patient Summary Implementation Guide](http://hl7.org/fhir/uv/ips/).
+
+In FHIR, an IPS is expressed as a [FHIR Document](https://www.hl7.org/fhir/documents.html). The HAPI FHIR JPA server supports the automated generation of IPS documents through an extensible and customizable engine which implements the [`$summary`](http://hl7.org/fhir/uv/ips/OperationDefinition-summary.html) operation.
+
+# Overview
+
+
+
+The IPS Generator uses FHIR resources stored in your repository as its input. The algorithm for determining which resources to include and how to construct the mandatory narrative is customizable and extensible, with a default algorithm included.
+
+
+
+# Generation Strategy
+
+A user supplied strategy class is used to determine various properties of the IPS. This class must implement the `IIpsGenerationStrategy` interface. A default implementation called `DefaultIpsGenerationStrategy` is included. You may use this default implementation, use a subclassed version of it that adds additional logic, or use en entirely new implementation.
+
+The generation strategy also supplies the [Section Registry](#section-registry) and [Narrative Templates](#narrative-templates) implementations, so it can be considered the central part of your IPS configuration.
+
+* JavaDoc: [IIpsGenerationStrategy](/hapi-fhir/apidocs/hapi-fhir-jpaserver-ips/ca/uhn/fhir/jpa/ips/api/IIpsGenerationStrategy.html)
+* Source Code: [IIpsGenerationStrategy.java](https://github.com/hapifhir/hapi-fhir/blob/master/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IIpsGenerationStrategy.java)
+* JavaDoc: [DefaultIpsGenerationStrategy](/hapi-fhir/apidocs/hapi-fhir-jpaserver-ips/ca/uhn/fhir/jpa/ips/strategy/DefaultIpsGenerationStrategy.html)
+* Source Code: [DefaultIpsGenerationStrategy.java](https://github.com/hapifhir/hapi-fhir/blob/master/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/DefaultIpsGenerationStrategy.java)
+
+
+
+
+# Section Registry
+
+The IPS SectionRegistry class defines the sections that will be included in your IPS. Out of the box, the standard IPS sections are all included. See the [IG homepage](http://hl7.org/fhir/uv/ips/) for a list of the standard sections.
+
+* JavaDoc: [SectionRegistry](/hapi-fhir/apidocs/hapi-fhir-jpaserver-ips/ca/uhn/fhir/jpa/ips/api/SectionRegistry.html)
+* Source Code: [SectionRegistry.java](https://github.com/hapifhir/hapi-fhir/blob/master/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/SectionRegistry.java)
+
+
+
+
+# Narrative Templates
+
+The IPS Document includes a [Composition](http://hl7.org/fhir/composition.html) resource, and this composition must include a populated narrative for each section containing the relevant clinical details for the section.
+
+The IPS generator uses HAPI FHIR [Narrative Generation](/hapi-fhir/docs/model/narrative_generation.html) to achieve this.
+
+Narrative templates for individual sections will be supplied a Bundle resource containing only the matched resources for the individual section as entries (ie. the Composition itself will not be present and no other resources will be present). So, for example, when generating the _Allergies / Intolerances_ IPS section narrative, the input to the narrative generator will be a _Bundle_ resource containing only _AllergyIntolerance_ resources.
+
+The narrative properties file should contain definitions using the profile URL of the individual section (as defined in the [section registry](#section-registry)) as the `.profile` qualifier. For example:
+
+```properties
+ips-allergyintolerance.resourceType=Bundle
+ips-allergyintolerance.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/AllergiesAndIntolerances-uv-ips
+ips-allergyintolerance.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/allergyintolerance.html
+```
+
+Built-in Narrative Templates:
+* Source Code: [ca.uhn.fhir.jpa.ips.narrative](https://github.com/hapifhir/hapi-fhir/blob/master/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/). Note the following:
+ * Default properties file: [ips-narratives.properties](https://github.com/hapifhir/hapi-fhir/blob/master/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/ips-narratives.properties)
+ * Example template for Allergies section: [allergyintolerance.html](https://github.com/hapifhir/hapi-fhir/blob/master/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/allergyintolerance.html)
+ * Fragments file containing common fragments used in multiple templates: [utility-fragments.html](https://github.com/hapifhir/hapi-fhir/blob/master/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/utility-fragments.html)
+
+# Credits
+
+This module is based on the excellent work of Rio Bennin and Panayiotis Savva of the University of Cyprus.
diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/ips/overview.svg b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/ips/overview.svg
new file mode 100644
index 00000000000..2a3acf91c94
--- /dev/null
+++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/ips/overview.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/hapi-fhir-jacoco/pom.xml b/hapi-fhir-jacoco/pom.xml
index c4bb66238c1..9fd26de5ce9 100644
--- a/hapi-fhir-jacoco/pom.xml
+++ b/hapi-fhir-jacoco/pom.xml
@@ -11,7 +11,7 @@
ca.uhn.hapi.fhirhapi-deployable-pom
- 6.3.12-SNAPSHOT
+ 6.3.13-SNAPSHOT../hapi-deployable-pom/pom.xml
@@ -181,6 +181,11 @@
hapi-fhir-jpaserver-model${project.version}
+
+ ca.uhn.hapi.fhir
+ hapi-fhir-jpaserver-ips
+ ${project.version}
+ ca.uhn.hapi.fhirhapi-fhir-jpaserver-mdm
diff --git a/hapi-fhir-jaxrsserver-base/pom.xml b/hapi-fhir-jaxrsserver-base/pom.xml
index 9e6f22375db..d5ded8302c0 100644
--- a/hapi-fhir-jaxrsserver-base/pom.xml
+++ b/hapi-fhir-jaxrsserver-base/pom.xml
@@ -4,7 +4,7 @@
ca.uhn.hapi.fhirhapi-deployable-pom
- 6.3.12-SNAPSHOT
+ 6.3.13-SNAPSHOT../hapi-deployable-pom/pom.xml
diff --git a/hapi-fhir-jpa/pom.xml b/hapi-fhir-jpa/pom.xml
index a2137f38b88..aace7e6de97 100644
--- a/hapi-fhir-jpa/pom.xml
+++ b/hapi-fhir-jpa/pom.xml
@@ -5,7 +5,7 @@
ca.uhn.hapi.fhirhapi-deployable-pom
- 6.3.12-SNAPSHOT
+ 6.3.13-SNAPSHOT../hapi-deployable-pom/pom.xml4.0.0
diff --git a/hapi-fhir-jpaserver-base/pom.xml b/hapi-fhir-jpaserver-base/pom.xml
index 9f32dc19aef..58f104cd3ed 100644
--- a/hapi-fhir-jpaserver-base/pom.xml
+++ b/hapi-fhir-jpaserver-base/pom.xml
@@ -5,7 +5,7 @@
ca.uhn.hapi.fhirhapi-deployable-pom
- 6.3.12-SNAPSHOT
+ 6.3.13-SNAPSHOT../hapi-deployable-pom/pom.xml
@@ -376,6 +376,12 @@
${project.version}test
+
+ ca.uhn.hapi.fhir
+ hapi-fhir-test-utilities
+ ${project.version}
+ test
+ org.jetbrains
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/batch2/JpaJobPersistenceImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/batch2/JpaJobPersistenceImpl.java
index c53b37d045f..f6ecea647be 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/batch2/JpaJobPersistenceImpl.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/batch2/JpaJobPersistenceImpl.java
@@ -29,7 +29,7 @@ import ca.uhn.fhir.batch2.model.MarkWorkChunkAsErrorRequest;
import ca.uhn.fhir.batch2.model.StatusEnum;
import ca.uhn.fhir.batch2.model.WorkChunk;
import ca.uhn.fhir.batch2.models.JobInstanceFetchRequest;
-import ca.uhn.fhir.jpa.batch.log.Logs;
+import ca.uhn.fhir.util.Logs;
import ca.uhn.fhir.jpa.dao.data.IBatch2JobInstanceRepository;
import ca.uhn.fhir.jpa.dao.data.IBatch2WorkChunkRepository;
import ca.uhn.fhir.jpa.entity.Batch2JobInstanceEntity;
@@ -39,7 +39,6 @@ import ca.uhn.fhir.model.api.PagingIterator;
import org.apache.commons.collections4.ListUtils;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/svc/BulkDataImportSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/svc/BulkDataImportSvcImpl.java
index 0bebb4f459f..8c4467b295f 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/svc/BulkDataImportSvcImpl.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/svc/BulkDataImportSvcImpl.java
@@ -24,7 +24,7 @@ import ca.uhn.fhir.batch2.api.IJobCoordinator;
import ca.uhn.fhir.batch2.importpull.models.Batch2BulkImportPullJobParameters;
import ca.uhn.fhir.batch2.model.JobInstanceStartRequest;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
-import ca.uhn.fhir.jpa.batch.log.Logs;
+import ca.uhn.fhir.util.Logs;
import ca.uhn.fhir.jpa.bulk.imprt.api.IBulkDataImportSvc;
import ca.uhn.fhir.jpa.bulk.imprt.model.ActivateJobResult;
import ca.uhn.fhir.jpa.bulk.imprt.model.BulkImportJobFileJson;
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/PersistedJpaBundleProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/PersistedJpaBundleProvider.java
index c36928583f1..52ac51e2cad 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/PersistedJpaBundleProvider.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/PersistedJpaBundleProvider.java
@@ -105,7 +105,6 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
private MemoryCacheService myMemoryCacheService;
@Autowired
private IJpaStorageResourceParser myJpaStorageResourceParser;
-
/*
* Non autowired fields (will be different for every instance
* of this class, since it's a prototype
@@ -114,7 +113,6 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
private String myUuid;
private SearchCacheStatusEnum myCacheStatus;
private RequestPartitionId myRequestPartitionId;
-
/**
* Constructor
*/
@@ -223,7 +221,6 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
return myTxService.withRequest(myRequest).execute(() -> toResourceList(sb, pidsSubList));
}
-
/**
* Returns false if the entity can't be found
*/
diff --git a/hapi-fhir-jpaserver-elastic-test-utilities/pom.xml b/hapi-fhir-jpaserver-elastic-test-utilities/pom.xml
index 7b769a6a173..2d2952de95e 100644
--- a/hapi-fhir-jpaserver-elastic-test-utilities/pom.xml
+++ b/hapi-fhir-jpaserver-elastic-test-utilities/pom.xml
@@ -6,7 +6,7 @@
ca.uhn.hapi.fhirhapi-deployable-pom
- 6.3.12-SNAPSHOT
+ 6.3.13-SNAPSHOT../hapi-deployable-pom/pom.xml
diff --git a/hapi-fhir-jpaserver-ips/pom.xml b/hapi-fhir-jpaserver-ips/pom.xml
new file mode 100644
index 00000000000..3283bee3887
--- /dev/null
+++ b/hapi-fhir-jpaserver-ips/pom.xml
@@ -0,0 +1,48 @@
+
+ 4.0.0
+
+ ca.uhn.hapi.fhir
+ hapi-deployable-pom
+ 6.3.13-SNAPSHOT
+ ../hapi-deployable-pom/pom.xml
+
+
+ hapi-fhir-jpaserver-ips
+ jar
+ HAPI FHIR JPA Server - International Patient Summary (IPS)
+
+
+
+ ca.uhn.hapi.fhir
+ hapi-fhir-jpaserver-base
+ ${project.version}
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ provided
+
+
+
+
+ ca.uhn.hapi.fhir
+ hapi-fhir-jpaserver-test-utilities
+ ${project.version}
+ test
+
+
+ ca.uhn.hapi.fhir
+ hapi-fhir-test-utilities
+ ${project.version}
+ test
+
+
+ net.sourceforge.htmlunit
+ htmlunit
+ test
+
+
+
+
diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IIpsGenerationStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IIpsGenerationStrategy.java
new file mode 100644
index 00000000000..196aceb3ed1
--- /dev/null
+++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IIpsGenerationStrategy.java
@@ -0,0 +1,136 @@
+package ca.uhn.fhir.jpa.ips.api;
+
+/*-
+ * #%L
+ * HAPI FHIR JPA Server - International Patient Summary (IPS)
+ * %%
+ * Copyright (C) 2014 - 2023 Smile CDR, Inc.
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
+import ca.uhn.fhir.model.api.Include;
+import org.hl7.fhir.instance.model.api.IBaseResource;
+import org.hl7.fhir.instance.model.api.IIdType;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * This interface is the primary configuration and strategy provider for the
+ * HAPI FHIR International Patient Summary (IPS) generator.
+ *
+ * Note that this API will almost certainly change as more real-world experience is
+ * gained with the IPS generator.
+ */
+public interface IIpsGenerationStrategy {
+
+ /**
+ * Provides a registry which defines the various sections that will be
+ * included when generating an IPS. It can be subclassed and customized
+ * as needed in order to add, change, or remove sections.
+ */
+ SectionRegistry getSectionRegistry();
+
+ /**
+ * Provides a list of configuration property files for the IPS narrative generator.
+ *
+ * Entries should be of the format classpath:path/to/file.properties
+ *
+ *
+ * If more than one file is provided, the files will be evaluated in order. Therefore you
+ * might choose to include a custom file, followed by
+ * {@link ca.uhn.fhir.jpa.ips.strategy.DefaultIpsGenerationStrategy#DEFAULT_IPS_NARRATIVES_PROPERTIES}
+ * in order to fall back to the default templates for any sections you have not
+ * provided an explicit template for.
+ *
+ */
+ List getNarrativePropertyFiles();
+
+ /**
+ * Create and return a new Organization resource representing.
+ * the author of the IPS document. This method will be called once per IPS
+ * in order to
+ */
+ IBaseResource createAuthor();
+
+ /**
+ * Create and return a title for the composition document.
+ *
+ * @param theContext The associated context for the specific IPS document being generated.
+ */
+ String createTitle(IpsContext theContext);
+
+ /**
+ * Create and return a confidentiality code for the composition document. Must be a valid
+ * code for the element Composition.confidentiality
+ *
+ * @param theIpsContext The associated context for the specific IPS document being generated.
+ */
+ String createConfidentiality(IpsContext theIpsContext);
+
+ /**
+ * This method is used to determine the resource ID to assign to a resource that
+ * will be added to the IPS document Bundle. Implementations will probably either
+ * return the resource ID as-is, or generate a placeholder UUID to replace it with.
+ *
+ * @param theIpsContext The associated context for the specific IPS document being
+ * generated. Note that this will be null when
+ * massaging the ID of the subject (Patient) resource, but will
+ * be populated for all subsequent calls for a given IPS
+ * document generation.
+ * @param theResource The resource to massage the resource ID for
+ * @return An ID to assign to the resource
+ */
+ IIdType massageResourceId(@Nullable IpsContext theIpsContext, @Nonnull IBaseResource theResource);
+
+ /**
+ * This method can manipulate the {@link SearchParameterMap} that will
+ * be used to find candidate resources for the given IPS section. The map will already have
+ * a subject/patient parameter added to it. The map provided in {@literal theSearchParameterMap}
+ * will contain a subject/patient reference, but no other parameters. This method can add other
+ * parameters.
+ *
+ * For example, for a Vital Signs section, the implementation might add a parameter indicating
+ * the parameter category=vital-signs.
+ *
+ * @param theIpsSectionContext The context, which indicates the IPS section and the resource type
+ * being searched for.
+ * @param theSearchParameterMap The map to manipulate.
+ */
+ void massageResourceSearch(IpsContext.IpsSectionContext theIpsSectionContext, SearchParameterMap theSearchParameterMap);
+
+ /**
+ * Return a set of Include directives to be added to the resource search
+ * for resources to include for a given IPS section. These include statements will
+ * be added to the same {@link SearchParameterMap} provided to
+ * {@link #massageResourceSearch(IpsContext.IpsSectionContext, SearchParameterMap)}.
+ * This is a separate method in order to make subclassing easier.
+ *
+ * @param theIpsSectionContext The context, which indicates the IPS section and the resource type
+ * being searched for.
+ */
+ @Nonnull
+ Set provideResourceSearchIncludes(IpsContext.IpsSectionContext theIpsSectionContext);
+
+ /**
+ * This method will be called for each found resource candidate for inclusion in the
+ * IPS document. The strategy can decide whether to include it or not.
+ */
+ boolean shouldInclude(IpsContext.IpsSectionContext theIpsSectionContext, IBaseResource theCandidate);
+
+}
diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IpsContext.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IpsContext.java
new file mode 100644
index 00000000000..118a9722726
--- /dev/null
+++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IpsContext.java
@@ -0,0 +1,86 @@
+package ca.uhn.fhir.jpa.ips.api;
+
+/*-
+ * #%L
+ * HAPI FHIR JPA Server - International Patient Summary (IPS)
+ * %%
+ * Copyright (C) 2014 - 2023 Smile CDR, Inc.
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import org.hl7.fhir.instance.model.api.IBaseResource;
+import org.hl7.fhir.instance.model.api.IIdType;
+
+public class IpsContext {
+
+ private final IBaseResource mySubject;
+ private final IIdType mySubjectId;
+
+ /**
+ * Constructor
+ *
+ * @param theSubject The subject Patient resource for the IPS being generated
+ * @param theSubjectId The original ID for {@literal theSubject}, which may not match the current ID if {@link IIpsGenerationStrategy#massageResourceId(IpsContext, IBaseResource)} has modified it
+ */
+ public IpsContext(IBaseResource theSubject, IIdType theSubjectId) {
+ mySubject = theSubject;
+ mySubjectId = theSubjectId;
+ }
+
+ /**
+ * Returns the subject Patient resource for the IPS being generated. Note that
+ * the {@literal Resource.id} value may not match the ID of the resource stored in the
+ * repository if {@link IIpsGenerationStrategy#massageResourceId(IpsContext, IBaseResource)} has
+ * returned a different ID. Use {@link #getSubjectId()} if you want the originally stored ID.
+ *
+ * @see #getSubjectId() for the originally stored ID.
+ */
+ public IBaseResource getSubject() {
+ return mySubject;
+ }
+
+ /**
+ * Returns the ID of the subject for the given IPS. This value should match the
+ * ID which was originally fetched from the repository.
+ */
+ public IIdType getSubjectId() {
+ return mySubjectId;
+ }
+
+ public IpsSectionContext newSectionContext(IpsSectionEnum theSection, String theResourceType) {
+ return new IpsSectionContext(mySubject, mySubjectId, theSection, theResourceType);
+ }
+
+ public static class IpsSectionContext extends IpsContext {
+
+ private final IpsSectionEnum mySection;
+ private final String myResourceType;
+
+ private IpsSectionContext(IBaseResource theSubject, IIdType theSubjectId, IpsSectionEnum theSection, String theResourceType) {
+ super(theSubject, theSubjectId);
+ mySection = theSection;
+ myResourceType = theResourceType;
+ }
+
+ public String getResourceType() {
+ return myResourceType;
+ }
+
+ public IpsSectionEnum getSection() {
+ return mySection;
+ }
+ }
+
+}
diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IpsSectionEnum.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IpsSectionEnum.java
new file mode 100644
index 00000000000..31b0fc02c9e
--- /dev/null
+++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IpsSectionEnum.java
@@ -0,0 +1,38 @@
+package ca.uhn.fhir.jpa.ips.api;
+
+/*-
+ * #%L
+ * HAPI FHIR JPA Server - International Patient Summary (IPS)
+ * %%
+ * Copyright (C) 2014 - 2023 Smile CDR, Inc.
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+public enum IpsSectionEnum {
+ ALLERGY_INTOLERANCE,
+ MEDICATION_SUMMARY,
+ PROBLEM_LIST,
+ IMMUNIZATIONS,
+ PROCEDURES,
+ MEDICAL_DEVICES,
+ DIAGNOSTIC_RESULTS,
+ VITAL_SIGNS,
+ ILLNESS_HISTORY,
+ PREGNANCY,
+ SOCIAL_HISTORY,
+ FUNCTIONAL_STATUS,
+ PLAN_OF_CARE,
+ ADVANCE_DIRECTIVES
+ }
diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/SectionRegistry.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/SectionRegistry.java
new file mode 100644
index 00000000000..1b4fc7f3544
--- /dev/null
+++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/SectionRegistry.java
@@ -0,0 +1,422 @@
+package ca.uhn.fhir.jpa.ips.api;
+
+/*-
+ * #%L
+ * HAPI FHIR JPA Server - International Patient Summary (IPS)
+ * %%
+ * Copyright (C) 2014 - 2023 Smile CDR, Inc.
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import org.apache.commons.lang3.Validate;
+import org.hl7.fhir.instance.model.api.IBaseResource;
+import org.hl7.fhir.instance.model.api.IIdType;
+import org.hl7.fhir.r4.model.AllergyIntolerance;
+import org.hl7.fhir.r4.model.CodeableConcept;
+import org.hl7.fhir.r4.model.Coding;
+import org.hl7.fhir.r4.model.Condition;
+import org.hl7.fhir.r4.model.MedicationStatement;
+import org.hl7.fhir.r4.model.Reference;
+import org.hl7.fhir.r4.model.ResourceType;
+
+import javax.annotation.Nullable;
+import javax.annotation.PostConstruct;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * This class is the registry for sections for the IPS document. It can be extended
+ * and customized if you wish to add / remove / change sections.
+ *
+ * By default, all standard sections in the
+ * base IPS specification IG
+ * are included. You can customize this to remove sections, or to add new ones
+ * as permitted by the IG.
+ *
+ *
+ * To customize the sections, you may override the {@link #addSections()} method
+ * in order to add new sections or remove them. You may also override individual
+ * section methods such as {@link #addSectionAllergyIntolerance()} or
+ * {@link #addSectionAdvanceDirectives()}.
+ *
+ */
+public class SectionRegistry {
+
+ private final ArrayList mySections = new ArrayList<>();
+ private List> myGlobalCustomizers = new ArrayList<>();
+
+ /**
+ * Constructor
+ */
+ public SectionRegistry() {
+ super();
+ }
+
+ /**
+ * This method should be automatically called by the Spring context. It initializes
+ * the registry.
+ */
+ @PostConstruct
+ public final void initialize() {
+ Validate.isTrue(mySections.isEmpty(), "Sections are already initialized");
+ addSections();
+ }
+
+ public boolean isInitialized() {
+ return !mySections.isEmpty();
+ }
+
+ /**
+ * Add the various sections to the registry in order. This method can be overridden for
+ * customization.
+ */
+ protected void addSections() {
+ addSectionAllergyIntolerance();
+ addSectionMedicationSummary();
+ addSectionProblemList();
+ addSectionImmunizations();
+ addSectionProcedures();
+ addSectionMedicalDevices();
+ addSectionDiagnosticResults();
+ addSectionVitalSigns();
+ addSectionPregnancy();
+ addSectionSocialHistory();
+ addSectionIllnessHistory();
+ addSectionFunctionalStatus();
+ addSectionPlanOfCare();
+ addSectionAdvanceDirectives();
+ }
+
+ protected void addSectionAllergyIntolerance() {
+ addSection(IpsSectionEnum.ALLERGY_INTOLERANCE)
+ .withTitle("Allergies and Intolerances")
+ .withSectionCode("48765-2")
+ .withSectionDisplay("Allergies and Adverse Reactions")
+ .withResourceTypes(ResourceType.AllergyIntolerance.name())
+ .withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/AllergiesAndIntolerances-uv-ips")
+ .withNoInfoGenerator(new AllergyIntoleranceNoInfoR4Generator())
+ .build();
+ }
+
+ protected void addSectionMedicationSummary() {
+ addSection(IpsSectionEnum.MEDICATION_SUMMARY)
+ .withTitle("Medication List")
+ .withSectionCode("10160-0")
+ .withSectionDisplay("Medication List")
+ .withResourceTypes(
+ ResourceType.MedicationStatement.name(),
+ ResourceType.MedicationRequest.name(),
+ ResourceType.MedicationAdministration.name(),
+ ResourceType.MedicationDispense.name()
+ )
+ .withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/MedicationSummary-uv-ips")
+ .withNoInfoGenerator(new MedicationNoInfoR4Generator())
+ .build();
+ }
+
+ protected void addSectionProblemList() {
+ addSection(IpsSectionEnum.PROBLEM_LIST)
+ .withTitle("Problem List")
+ .withSectionCode("11450-4")
+ .withSectionDisplay("Problem List")
+ .withResourceTypes(ResourceType.Condition.name())
+ .withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/ProblemList-uv-ips")
+ .withNoInfoGenerator(new ProblemNoInfoR4Generator())
+ .build();
+ }
+
+ protected void addSectionImmunizations() {
+ addSection(IpsSectionEnum.IMMUNIZATIONS)
+ .withTitle("History of Immunizations")
+ .withSectionCode("11369-6")
+ .withSectionDisplay("History of Immunizations")
+ .withResourceTypes(ResourceType.Immunization.name())
+ .withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/Immunizations-uv-ips")
+ .build();
+ }
+
+ protected void addSectionProcedures() {
+ addSection(IpsSectionEnum.PROCEDURES)
+ .withTitle("History of Procedures")
+ .withSectionCode("47519-4")
+ .withSectionDisplay("History of Procedures")
+ .withResourceTypes(ResourceType.Procedure.name())
+ .withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/HistoryOfProcedures-uv-ips")
+ .build();
+ }
+
+ protected void addSectionMedicalDevices() {
+ addSection(IpsSectionEnum.MEDICAL_DEVICES)
+ .withTitle("Medical Devices")
+ .withSectionCode("46240-8")
+ .withSectionDisplay("Medical Devices")
+ .withResourceTypes(ResourceType.DeviceUseStatement.name())
+ .withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/MedicalDevices-uv-ips")
+ .build();
+ }
+
+ protected void addSectionDiagnosticResults() {
+ addSection(IpsSectionEnum.DIAGNOSTIC_RESULTS)
+ .withTitle("Diagnostic Results")
+ .withSectionCode("30954-2")
+ .withSectionDisplay("Diagnostic Results")
+ .withResourceTypes(ResourceType.DiagnosticReport.name(), ResourceType.Observation.name())
+ .withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/DiagnosticResults-uv-ips")
+ .build();
+ }
+
+ protected void addSectionVitalSigns() {
+ addSection(IpsSectionEnum.VITAL_SIGNS)
+ .withTitle("Vital Signs")
+ .withSectionCode("8716-3")
+ .withSectionDisplay("Vital Signs")
+ .withResourceTypes(ResourceType.Observation.name())
+ .withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/VitalSigns-uv-ips")
+ .build();
+ }
+
+ protected void addSectionPregnancy() {
+ addSection(IpsSectionEnum.PREGNANCY)
+ .withTitle("Pregnancy Information")
+ .withSectionCode("10162-6")
+ .withSectionDisplay("Pregnancy Information")
+ .withResourceTypes(ResourceType.Observation.name())
+ .withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/Pregnancy-uv-ips")
+ .build();
+ }
+
+ protected void addSectionSocialHistory() {
+ addSection(IpsSectionEnum.SOCIAL_HISTORY)
+ .withTitle("Social History")
+ .withSectionCode("29762-2")
+ .withSectionDisplay("Social History")
+ .withResourceTypes(ResourceType.Observation.name())
+ .withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/SocialHistory-uv-ips")
+ .build();
+ }
+
+ protected void addSectionIllnessHistory() {
+ addSection(IpsSectionEnum.ILLNESS_HISTORY)
+ .withTitle("History of Past Illness")
+ .withSectionCode("11348-0")
+ .withSectionDisplay("History of Past Illness")
+ .withResourceTypes(ResourceType.Condition.name())
+ .withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/PastHistoryOfIllnesses-uv-ips")
+ .build();
+ }
+
+ protected void addSectionFunctionalStatus() {
+ addSection(IpsSectionEnum.FUNCTIONAL_STATUS)
+ .withTitle("Functional Status")
+ .withSectionCode("47420-5")
+ .withSectionDisplay("Functional Status")
+ .withResourceTypes(ResourceType.ClinicalImpression.name())
+ .withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/FunctionalStatus-uv-ips")
+ .build();
+ }
+
+ protected void addSectionPlanOfCare() {
+ addSection(IpsSectionEnum.PLAN_OF_CARE)
+ .withTitle("Plan of Care")
+ .withSectionCode("18776-5")
+ .withSectionDisplay("Plan of Care")
+ .withResourceTypes(ResourceType.CarePlan.name())
+ .withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/PlanOfCare-uv-ips")
+ .build();
+ }
+
+ protected void addSectionAdvanceDirectives() {
+ addSection(IpsSectionEnum.ADVANCE_DIRECTIVES)
+ .withTitle("Advance Directives")
+ .withSectionCode("42349-0")
+ .withSectionDisplay("Advance Directives")
+ .withResourceTypes(ResourceType.Consent.name())
+ .withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/AdvanceDirectives-uv-ips")
+ .build();
+ }
+
+ private SectionBuilder addSection(IpsSectionEnum theSectionEnum) {
+ return new SectionBuilder(theSectionEnum);
+ }
+
+ public SectionRegistry addGlobalCustomizer(Consumer theGlobalCustomizer) {
+ Validate.notNull(theGlobalCustomizer, "theGlobalCustomizer must not be null");
+ myGlobalCustomizers.add(theGlobalCustomizer);
+ return this;
+ }
+
+ public List getSections() {
+ Validate.isTrue(isInitialized(), "Section registry has not been initialized");
+ return Collections.unmodifiableList(mySections);
+ }
+
+ public Section getSection(IpsSectionEnum theSectionEnum) {
+ return getSections().stream().filter(t -> t.getSectionEnum() == theSectionEnum).findFirst().orElseThrow(() -> new IllegalArgumentException("No section for type: " + theSectionEnum));
+ }
+
+
+ public interface INoInfoGenerator {
+
+ /**
+ * Generate an appropriate no-info resource. The resource does not need to have an ID populated,
+ * although it can if it is a resource found in the repository.
+ */
+ IBaseResource generate(IIdType theSubjectId);
+
+ }
+
+ public class SectionBuilder {
+
+ private final IpsSectionEnum mySectionEnum;
+ private String myTitle;
+ private String mySectionCode;
+ private String mySectionDisplay;
+ private List myResourceTypes;
+ private String myProfile;
+ private INoInfoGenerator myNoInfoGenerator;
+
+ public SectionBuilder(IpsSectionEnum theSectionEnum) {
+ mySectionEnum = theSectionEnum;
+ }
+
+ public SectionBuilder withTitle(String theTitle) {
+ Validate.notBlank(theTitle);
+ myTitle = theTitle;
+ return this;
+ }
+
+ public SectionBuilder withSectionCode(String theSectionCode) {
+ Validate.notBlank(theSectionCode);
+ mySectionCode = theSectionCode;
+ return this;
+ }
+
+ public SectionBuilder withSectionDisplay(String theSectionDisplay) {
+ Validate.notBlank(theSectionDisplay);
+ mySectionDisplay = theSectionDisplay;
+ return this;
+ }
+
+ public SectionBuilder withResourceTypes(String... theResourceTypes) {
+ Validate.isTrue(theResourceTypes.length > 0);
+ myResourceTypes = Arrays.asList(theResourceTypes);
+ return this;
+ }
+
+ public SectionBuilder withProfile(String theProfile) {
+ Validate.notBlank(theProfile);
+ myProfile = theProfile;
+ return this;
+ }
+
+ public SectionBuilder withNoInfoGenerator(INoInfoGenerator theNoInfoGenerator) {
+ myNoInfoGenerator = theNoInfoGenerator;
+ return this;
+ }
+
+ public void build() {
+ myGlobalCustomizers.forEach(t -> t.accept(this));
+ mySections.add(new Section(mySectionEnum, myTitle, mySectionCode, mySectionDisplay, myResourceTypes, myProfile, myNoInfoGenerator));
+ }
+ }
+
+ private static class AllergyIntoleranceNoInfoR4Generator implements INoInfoGenerator {
+ @Override
+ public IBaseResource generate(IIdType theSubjectId) {
+ AllergyIntolerance allergy = new AllergyIntolerance();
+ allergy.setCode(new CodeableConcept().addCoding(new Coding().setCode("no-allergy-info").setSystem("http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips").setDisplay("No information about allergies")))
+ .setPatient(new Reference(theSubjectId))
+ .setClinicalStatus(new CodeableConcept().addCoding(new Coding().setCode("active").setSystem("http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical")));
+ return allergy;
+ }
+ }
+
+ private static class MedicationNoInfoR4Generator implements INoInfoGenerator {
+ @Override
+ public IBaseResource generate(IIdType theSubjectId) {
+ MedicationStatement medication = new MedicationStatement();
+ // setMedicationCodeableConcept is not available
+ medication.setMedication(new CodeableConcept().addCoding(new Coding().setCode("no-medication-info").setSystem("http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips").setDisplay("No information about medications")))
+ .setSubject(new Reference(theSubjectId))
+ .setStatus(MedicationStatement.MedicationStatementStatus.UNKNOWN);
+ // .setEffective(new Period().addExtension().setUrl("http://hl7.org/fhir/StructureDefinition/data-absent-reason").setValue((new Coding().setCode("not-applicable"))))
+ return medication;
+ }
+ }
+
+ private static class ProblemNoInfoR4Generator implements INoInfoGenerator {
+ @Override
+ public IBaseResource generate(IIdType theSubjectId) {
+ Condition condition = new Condition();
+ condition.setCode(new CodeableConcept().addCoding(new Coding().setCode("no-problem-info").setSystem("http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips").setDisplay("No information about problems")))
+ .setSubject(new Reference(theSubjectId))
+ .setClinicalStatus(new CodeableConcept().addCoding(new Coding().setCode("active").setSystem("http://terminology.hl7.org/CodeSystem/condition-clinical")));
+ return condition;
+ }
+ }
+
+ public static class Section {
+
+ private final IpsSectionEnum mySectionEnum;
+ private final String myTitle;
+ private final String mySectionCode;
+ private final String mySectionDisplay;
+ private final List myResourceTypes;
+ private final String myProfile;
+ private final INoInfoGenerator myNoInfoGenerator;
+
+ public Section(IpsSectionEnum theSectionEnum, String theTitle, String theSectionCode, String theSectionDisplay, List theResourceTypes, String theProfile, INoInfoGenerator theNoInfoGenerator) {
+ mySectionEnum = theSectionEnum;
+ myTitle = theTitle;
+ mySectionCode = theSectionCode;
+ mySectionDisplay = theSectionDisplay;
+ myResourceTypes = Collections.unmodifiableList(new ArrayList<>(theResourceTypes));
+ myProfile = theProfile;
+ myNoInfoGenerator = theNoInfoGenerator;
+ }
+
+ @Nullable
+ public INoInfoGenerator getNoInfoGenerator() {
+ return myNoInfoGenerator;
+ }
+
+ public List getResourceTypes() {
+ return myResourceTypes;
+ }
+
+ public String getProfile() {
+ return myProfile;
+ }
+
+ public IpsSectionEnum getSectionEnum() {
+ return mySectionEnum;
+ }
+
+ public String getTitle() {
+ return myTitle;
+ }
+
+ public String getSectionCode() {
+ return mySectionCode;
+ }
+
+ public String getSectionDisplay() {
+ return mySectionDisplay;
+ }
+ }
+}
diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/generator/IIpsGeneratorSvc.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/generator/IIpsGeneratorSvc.java
new file mode 100644
index 00000000000..42a6ab9a395
--- /dev/null
+++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/generator/IIpsGeneratorSvc.java
@@ -0,0 +1,41 @@
+package ca.uhn.fhir.jpa.ips.generator;
+
+/*-
+ * #%L
+ * HAPI FHIR JPA Server - International Patient Summary (IPS)
+ * %%
+ * Copyright (C) 2014 - 2023 Smile CDR, Inc.
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import ca.uhn.fhir.rest.api.server.RequestDetails;
+import ca.uhn.fhir.rest.param.TokenParam;
+import org.hl7.fhir.instance.model.api.IBaseBundle;
+import org.hl7.fhir.instance.model.api.IIdType;
+
+public interface IIpsGeneratorSvc {
+
+ /**
+ * Generates an IPS document and returns the complete document bundle
+ * for the given patient by ID
+ */
+ IBaseBundle generateIps(RequestDetails theRequestDetails, IIdType thePatientId);
+
+ /**
+ * Generates an IPS document and returns the complete document bundle
+ * for the given patient by identifier
+ */
+ IBaseBundle generateIps(RequestDetails theRequestDetails, TokenParam thePatientIdentifier);
+}
diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImpl.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImpl.java
new file mode 100644
index 00000000000..a3afa800395
--- /dev/null
+++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImpl.java
@@ -0,0 +1,562 @@
+package ca.uhn.fhir.jpa.ips.generator;
+
+/*-
+ * #%L
+ * HAPI FHIR JPA Server - International Patient Summary (IPS)
+ * %%
+ * Copyright (C) 2014 - 2023 Smile CDR, Inc.
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import ca.uhn.fhir.context.FhirContext;
+import ca.uhn.fhir.context.RuntimeResourceDefinition;
+import ca.uhn.fhir.fhirpath.IFhirPathEvaluationContext;
+import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
+import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
+import ca.uhn.fhir.jpa.ips.api.IIpsGenerationStrategy;
+import ca.uhn.fhir.jpa.ips.api.IpsContext;
+import ca.uhn.fhir.jpa.ips.api.IpsSectionEnum;
+import ca.uhn.fhir.jpa.ips.api.SectionRegistry;
+import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
+import ca.uhn.fhir.model.api.Include;
+import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
+import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum;
+import ca.uhn.fhir.narrative.CustomThymeleafNarrativeGenerator;
+import ca.uhn.fhir.rest.api.server.IBundleProvider;
+import ca.uhn.fhir.rest.api.server.RequestDetails;
+import ca.uhn.fhir.rest.param.ReferenceParam;
+import ca.uhn.fhir.rest.param.TokenParam;
+import ca.uhn.fhir.util.BundleBuilder;
+import ca.uhn.fhir.util.CompositionBuilder;
+import ca.uhn.fhir.util.ResourceReferenceInfo;
+import ca.uhn.fhir.util.ValidateUtil;
+import org.hl7.fhir.instance.model.api.IBase;
+import org.hl7.fhir.instance.model.api.IBaseBundle;
+import org.hl7.fhir.instance.model.api.IBaseExtension;
+import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
+import org.hl7.fhir.instance.model.api.IBaseResource;
+import org.hl7.fhir.instance.model.api.IIdType;
+import org.hl7.fhir.instance.model.api.IPrimitiveType;
+import org.hl7.fhir.r4.model.Bundle;
+import org.hl7.fhir.r4.model.Composition;
+import org.hl7.fhir.r4.model.IdType;
+import org.hl7.fhir.r4.model.InstantType;
+import org.hl7.fhir.r4.model.Patient;
+import org.hl7.fhir.r4.model.Resource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+
+import static ca.uhn.fhir.jpa.term.api.ITermLoaderSvc.LOINC_URI;
+import static org.apache.commons.lang3.StringUtils.isNotBlank;
+
+public class IpsGeneratorSvcImpl implements IIpsGeneratorSvc {
+
+ public static final int CHUNK_SIZE = 10;
+ private static final Logger ourLog = LoggerFactory.getLogger(IpsGeneratorSvcImpl.class);
+ private final IIpsGenerationStrategy myGenerationStrategy;
+ private final DaoRegistry myDaoRegistry;
+ private final FhirContext myFhirContext;
+
+ /**
+ * Constructor
+ */
+ public IpsGeneratorSvcImpl(FhirContext theFhirContext, IIpsGenerationStrategy theGenerationStrategy, DaoRegistry theDaoRegistry) {
+ myGenerationStrategy = theGenerationStrategy;
+ myDaoRegistry = theDaoRegistry;
+ myFhirContext = theFhirContext;
+ }
+
+ @Override
+ public IBaseBundle generateIps(RequestDetails theRequestDetails, IIdType thePatientId) {
+ IBaseResource patient = myDaoRegistry
+ .getResourceDao("Patient")
+ .read(thePatientId, theRequestDetails);
+
+ return generateIpsForPatient(theRequestDetails, patient);
+ }
+
+ @Override
+ public IBaseBundle generateIps(RequestDetails theRequestDetails, TokenParam thePatientIdentifier) {
+ SearchParameterMap searchParameterMap = new SearchParameterMap()
+ .setLoadSynchronousUpTo(2)
+ .add(Patient.SP_IDENTIFIER, thePatientIdentifier);
+ IBundleProvider searchResults = myDaoRegistry
+ .getResourceDao("Patient")
+ .search(searchParameterMap, theRequestDetails);
+
+ ValidateUtil.isTrueOrThrowInvalidRequest(searchResults.sizeOrThrowNpe() > 0, "No Patient could be found matching given identifier");
+ ValidateUtil.isTrueOrThrowInvalidRequest(searchResults.sizeOrThrowNpe() == 1, "Multiple Patient resources were found matching given identifier");
+
+ IBaseResource patient = searchResults.getResources(0, 1).get(0);
+
+ return generateIpsForPatient(theRequestDetails, patient);
+ }
+
+ private IBaseBundle generateIpsForPatient(RequestDetails theRequestDetails, IBaseResource thePatient) {
+ IIdType originalSubjectId = myFhirContext.getVersion().newIdType().setValue(thePatient.getIdElement().getValue());
+ massageResourceId(null, thePatient);
+ IpsContext context = new IpsContext(thePatient, originalSubjectId);
+
+ IBaseResource author = myGenerationStrategy.createAuthor();
+ massageResourceId(context, author);
+
+ CompositionBuilder compositionBuilder = createComposition(thePatient, context, author);
+
+ ResourceInclusionCollection globalResourcesToInclude = determineInclusions(theRequestDetails, originalSubjectId, context, compositionBuilder);
+
+ IBaseResource composition = compositionBuilder.getComposition();
+
+ // Create the narrative for the Composition itself
+ CustomThymeleafNarrativeGenerator generator = newNarrativeGenerator(globalResourcesToInclude);
+ generator.populateResourceNarrative(myFhirContext, composition);
+
+ return createCompositionDocument(thePatient, author, composition, globalResourcesToInclude);
+ }
+
+ private IBaseBundle createCompositionDocument(IBaseResource thePatient, IBaseResource author, IBaseResource composition, ResourceInclusionCollection theResourcesToInclude) {
+ BundleBuilder bundleBuilder = new BundleBuilder(myFhirContext);
+ bundleBuilder.setType(Bundle.BundleType.DOCUMENT.toCode());
+ bundleBuilder.setIdentifier("urn:ietf:rfc:4122", UUID.randomUUID().toString());
+ bundleBuilder.setTimestamp(InstantType.now());
+
+ // Add composition to document
+ bundleBuilder.addDocumentEntry(composition);
+
+ // Add subject to document
+ bundleBuilder.addDocumentEntry(thePatient);
+
+ // Add inclusion candidates
+ for (IBaseResource next : theResourcesToInclude.getResources()) {
+ bundleBuilder.addDocumentEntry(next);
+ }
+
+ // Add author to document
+ bundleBuilder.addDocumentEntry(author);
+
+ return bundleBuilder.getBundle();
+ }
+
+ @Nonnull
+ private ResourceInclusionCollection determineInclusions(RequestDetails theRequestDetails, IIdType originalSubjectId, IpsContext context, CompositionBuilder theCompositionBuilder) {
+ ResourceInclusionCollection globalResourcesToInclude = new ResourceInclusionCollection();
+ SectionRegistry sectionRegistry = myGenerationStrategy.getSectionRegistry();
+ for (SectionRegistry.Section nextSection : sectionRegistry.getSections()) {
+ determineInclusionsForSection(theRequestDetails, originalSubjectId, context, theCompositionBuilder, globalResourcesToInclude, nextSection);
+ }
+ return globalResourcesToInclude;
+ }
+
+ private void determineInclusionsForSection(RequestDetails theRequestDetails, IIdType theOriginalSubjectId, IpsContext theIpsContext, CompositionBuilder theCompositionBuilder, ResourceInclusionCollection theGlobalResourcesToInclude, SectionRegistry.Section theSection) {
+ ResourceInclusionCollection sectionResourcesToInclude = new ResourceInclusionCollection();
+ for (String nextResourceType : theSection.getResourceTypes()) {
+
+ SearchParameterMap searchParameterMap = new SearchParameterMap();
+ String subjectSp = determinePatientCompartmentSearchParameterName(nextResourceType);
+ searchParameterMap.add(subjectSp, new ReferenceParam(theOriginalSubjectId));
+
+ IpsSectionEnum sectionEnum = theSection.getSectionEnum();
+ IpsContext.IpsSectionContext ipsSectionContext = theIpsContext.newSectionContext(sectionEnum, nextResourceType);
+ myGenerationStrategy.massageResourceSearch(ipsSectionContext, searchParameterMap);
+
+ Set includes = myGenerationStrategy.provideResourceSearchIncludes(ipsSectionContext);
+ includes.forEach(searchParameterMap::addInclude);
+
+ IFhirResourceDao> dao = myDaoRegistry.getResourceDao(nextResourceType);
+ IBundleProvider searchResult = dao.search(searchParameterMap, theRequestDetails);
+ for (int startIndex = 0; ; startIndex += CHUNK_SIZE) {
+ int endIndex = startIndex + CHUNK_SIZE;
+ List resources = searchResult.getResources(startIndex, endIndex);
+ if (resources.isEmpty()) {
+ break;
+ }
+
+ for (IBaseResource nextCandidate : resources) {
+
+ boolean include;
+
+ if (ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(nextCandidate) == BundleEntrySearchModeEnum.INCLUDE) {
+ include = true;
+ } else {
+ include = myGenerationStrategy.shouldInclude(ipsSectionContext, nextCandidate);
+ }
+
+ if (include) {
+
+ String originalResourceId = nextCandidate.getIdElement().toUnqualifiedVersionless().getValue();
+
+ // Check if we already have this resource included so that we don't
+ // include it twice
+ IBaseResource previouslyExistingResource = theGlobalResourcesToInclude.getResourceByOriginalId(originalResourceId);
+ if (previouslyExistingResource != null) {
+ BundleEntrySearchModeEnum candidateSearchEntryMode = ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(nextCandidate);
+ if (candidateSearchEntryMode == BundleEntrySearchModeEnum.MATCH) {
+ ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(previouslyExistingResource, BundleEntrySearchModeEnum.MATCH);
+ }
+
+ nextCandidate = previouslyExistingResource;
+ sectionResourcesToInclude.addResourceIfNotAlreadyPresent(nextCandidate, originalResourceId);
+ } else {
+ IIdType id = myGenerationStrategy.massageResourceId(theIpsContext, nextCandidate);
+ nextCandidate.setId(id);
+ theGlobalResourcesToInclude.addResourceIfNotAlreadyPresent(nextCandidate, originalResourceId);
+ sectionResourcesToInclude.addResourceIfNotAlreadyPresent(nextCandidate, originalResourceId);
+ }
+ }
+
+ }
+
+ }
+
+ /*
+ * Update any references within the added candidates - This is important
+ * because we might be replacing resource IDs before including them in
+ * the summary, so we need to also update the references to those
+ * resources.
+ */
+ for (IBaseResource nextResource : sectionResourcesToInclude.getResources()) {
+ List references = myFhirContext.newTerser().getAllResourceReferences(nextResource);
+ for (ResourceReferenceInfo nextReference : references) {
+ String existingReference = nextReference.getResourceReference().getReferenceElement().getValue();
+ if (isNotBlank(existingReference)) {
+ existingReference = new IdType(existingReference).toUnqualifiedVersionless().getValue();
+ String replacement = theGlobalResourcesToInclude.getIdSubstitution(existingReference);
+ if (isNotBlank(replacement) && !replacement.equals(existingReference)) {
+ nextReference.getResourceReference().setReference(replacement);
+ }
+ }
+ }
+ }
+
+ }
+
+ if (sectionResourcesToInclude.isEmpty() && theSection.getNoInfoGenerator() != null) {
+ IBaseResource noInfoResource = theSection.getNoInfoGenerator().generate(theIpsContext.getSubjectId());
+ String id = IdType.newRandomUuid().getValue();
+ if (noInfoResource.getIdElement().isEmpty()) {
+ noInfoResource.setId(id);
+ }
+ ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(noInfoResource, BundleEntrySearchModeEnum.MATCH);
+ theGlobalResourcesToInclude.addResourceIfNotAlreadyPresent(noInfoResource, noInfoResource.getIdElement().toUnqualifiedVersionless().getValue());
+ sectionResourcesToInclude.addResourceIfNotAlreadyPresent(noInfoResource, id);
+ }
+
+ addSection(theSection, theCompositionBuilder, sectionResourcesToInclude, theGlobalResourcesToInclude);
+ }
+
+ @SuppressWarnings("unchecked")
+ private void addSection(SectionRegistry.Section theSection, CompositionBuilder theCompositionBuilder, ResourceInclusionCollection theResourcesToInclude, ResourceInclusionCollection theGlobalResourcesToInclude) {
+
+ CompositionBuilder.SectionBuilder sectionBuilder = theCompositionBuilder.addSection();
+
+ sectionBuilder.setTitle(theSection.getTitle());
+ sectionBuilder.addCodeCoding(LOINC_URI, theSection.getSectionCode(), theSection.getSectionDisplay());
+
+ for (IBaseResource next : theResourcesToInclude.getResources()) {
+
+ IBaseExtension, ?> narrativeLink = ((IBaseHasExtensions) next).addExtension();
+ narrativeLink.setUrl("http://hl7.org/fhir/StructureDefinition/NarrativeLink");
+ String narrativeLinkValue = theCompositionBuilder.getComposition().getIdElement().getValue()
+ + "#"
+ + myFhirContext.getResourceType(next)
+ + "-"
+ + next.getIdElement().getValue();
+ IPrimitiveType narrativeLinkUri = (IPrimitiveType) myFhirContext.getElementDefinition("uri").newInstance();
+ narrativeLinkUri.setValueAsString(narrativeLinkValue);
+ narrativeLink.setValue(narrativeLinkUri);
+
+ sectionBuilder.addEntry(next.getIdElement());
+ }
+
+ String narrative = createSectionNarrative(theSection, theResourcesToInclude, theGlobalResourcesToInclude);
+ sectionBuilder.setText("generated", narrative);
+ }
+
+ private CompositionBuilder createComposition(IBaseResource thePatient, IpsContext context, IBaseResource author) {
+ CompositionBuilder compositionBuilder = new CompositionBuilder(myFhirContext);
+ compositionBuilder.setId(IdType.newRandomUuid());
+
+ compositionBuilder.setStatus(Composition.CompositionStatus.FINAL.toCode());
+ compositionBuilder.setSubject(thePatient.getIdElement().toUnqualifiedVersionless());
+ compositionBuilder.addTypeCoding("http://loinc.org", "60591-5", "Patient Summary Document");
+ compositionBuilder.setDate(InstantType.now());
+ compositionBuilder.setTitle(myGenerationStrategy.createTitle(context));
+ compositionBuilder.setConfidentiality(myGenerationStrategy.createConfidentiality(context));
+ compositionBuilder.addAuthor(author.getIdElement());
+
+ return compositionBuilder;
+ }
+
+ private String determinePatientCompartmentSearchParameterName(String theResourceType) {
+ RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(theResourceType);
+ return resourceDef.getSearchParamsForCompartmentName("Patient").get(0).getName();
+ }
+
+ private void massageResourceId(IpsContext theIpsContext, IBaseResource theResource) {
+ IIdType id = myGenerationStrategy.massageResourceId(theIpsContext, theResource);
+ theResource.setId(id);
+ }
+
+ private String createSectionNarrative(SectionRegistry.Section theSection, ResourceInclusionCollection theResources, ResourceInclusionCollection theGlobalResourceCollection) {
+ CustomThymeleafNarrativeGenerator generator = newNarrativeGenerator(theGlobalResourceCollection);
+
+ Bundle bundle = new Bundle();
+ for (IBaseResource resource : theResources.getResources()) {
+ BundleEntrySearchModeEnum searchMode = ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(resource);
+ if (searchMode == BundleEntrySearchModeEnum.MATCH) {
+ bundle.addEntry().setResource((Resource) resource);
+ }
+ }
+ String profile = theSection.getProfile();
+ bundle.getMeta().addProfile(profile);
+
+ // Generate the narrative
+ return generator.generateResourceNarrative(myFhirContext, bundle);
+ }
+
+ @Nonnull
+ private CustomThymeleafNarrativeGenerator newNarrativeGenerator(ResourceInclusionCollection theGlobalResourceCollection) {
+ List narrativePropertyFiles = myGenerationStrategy.getNarrativePropertyFiles();
+ CustomThymeleafNarrativeGenerator generator = new CustomThymeleafNarrativeGenerator(narrativePropertyFiles);
+ generator.setFhirPathEvaluationContext(new IFhirPathEvaluationContext() {
+ @Override
+ public IBase resolveReference(@Nonnull IIdType theReference, @Nullable IBase theContext) {
+ IBaseResource resource = theGlobalResourceCollection.getResourceById(theReference);
+ return resource;
+ }
+ });
+ return generator;
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+/*
+
+
+
+
+
+
+ private static HashMap> hashPrimaries(List resourceList) {
+ HashMap> iPSResourceMap = new HashMap>();
+
+ for (Resource resource : resourceList) {
+ for (PatientSummary.IPSSection iPSSection : PatientSummary.IPSSection.values()) {
+ if ( SectionTypes.get(iPSSection).contains(resource.getResourceType()) ) {
+ if ( !(resource.getResourceType() == ResourceType.Observation) || isObservationinSection(iPSSection, (Observation) resource)) {
+ if (iPSResourceMap.get(iPSSection) == null) {
+ iPSResourceMap.put(iPSSection, new ArrayList());
+ }
+ iPSResourceMap.get(iPSSection).add(resource);
+ }
+ }
+ }
+ }
+
+ return iPSResourceMap;
+ }
+
+
+
+ private static HashMap> filterPrimaries(HashMap> sectionPrimaries) {
+ HashMap> filteredPrimaries = new HashMap>();
+ for ( PatientSummary.IPSSection section : sectionPrimaries.keySet() ) {
+ List filteredList = new ArrayList();
+ for (Resource resource : sectionPrimaries.get(section)) {
+ if (passesFilter(section, resource)) {
+ filteredList.add(resource);
+ }
+ }
+ if (filteredList.size() > 0) {
+ filteredPrimaries.put(section, filteredList);
+ }
+ }
+ return filteredPrimaries;
+ }
+
+ private static List pruneResources(Patient patient, List resources, HashMap> sectionPrimaries, FhirContext ctx) {
+ List resourceIds = new ArrayList();
+ List followedIds = new ArrayList();
+
+ HashMap resourcesById = new HashMap();
+ for (Resource resource : resources) {
+ resourcesById.put(resource.getIdElement().getIdPart(), resource);
+ }
+ String patientId = patient.getIdElement().getIdPart();
+ resourcesById.put(patientId, patient);
+
+ recursivePrune(patientId, resourceIds, followedIds, resourcesById, ctx);
+
+ for (PatientSummary.IPSSection section : sectionPrimaries.keySet()) {
+ for (Resource resource : sectionPrimaries.get(section)) {
+ String resourceId = resource.getIdElement().getIdPart();
+ recursivePrune(resourceId, resourceIds, followedIds, resourcesById, ctx);
+ }
+ }
+
+ List prunedResources = new ArrayList();
+
+ for (Resource resource : resources) {
+ if (resourceIds.contains(resource.getIdElement().getIdPart())) {
+ prunedResources.add(resource);
+ }
+ }
+
+ return prunedResources;
+ }
+
+ private static Void recursivePrune(String resourceId, List resourceIds, List followedIds, HashMap resourcesById, FhirContext ctx) {
+ if (!resourceIds.contains(resourceId)) {
+ resourceIds.add(resourceId);
+ }
+
+ Resource resource = resourcesById.get(resourceId);
+ if (resource != null) {
+ ctx.newTerser().getAllResourceReferences(resource).stream()
+ .map( r -> r.getResourceReference().getReferenceElement().getIdPart() )
+ .forEach( id -> {
+ if (!followedIds.contains(id)) {
+ followedIds.add(id);
+ recursivePrune(id, resourceIds, followedIds, resourcesById, ctx);
+ }
+ });
+ }
+
+ return null;
+ }
+
+ private static List addLinkToResources(List resources, HashMap> sectionPrimaries, Composition composition) {
+ List linkedResources = new ArrayList();
+ HashMap valueUrls = new HashMap();
+
+ String url = "http://hl7.org/fhir/StructureDefinition/NarrativeLink";
+ String valueUrlBase = composition.getId() + "#";
+
+ for (PatientSummary.IPSSection section : sectionPrimaries.keySet()) {
+ String profile = SectionProfiles.get(section);
+ String[] arr = profile.split("/");
+ String profileName = arr[arr.length - 1];
+ String sectionValueUrlBase = valueUrlBase + profileName.split("-uv-")[0];
+
+ for (Resource resource : sectionPrimaries.get(section)) {
+ String valueUrl = sectionValueUrlBase + "-" + resource.getIdElement().getIdPart();
+ valueUrls.put(resource.getIdElement().getIdPart(), valueUrl);
+ }
+ }
+
+ for (Resource resource : resources) {
+ if (valueUrls.containsKey(resource.getIdElement().getIdPart())) {
+ String valueUrl = valueUrls.get(resource.getIdElement().getIdPart());
+ Extension extension = new Extension();
+ extension.setUrl(url);
+ extension.setValue(new UriType(valueUrl));
+ DomainResource domainResource = (DomainResource) resource;
+ domainResource.addExtension(extension);
+ resource = (Resource) domainResource;
+ }
+ linkedResources.add(resource);
+ }
+
+ return linkedResources;
+ }
+
+ private static HashMap createNarratives(HashMap> sectionPrimaries, List resources, FhirContext ctx) {
+ HashMap hashedNarratives = new HashMap();
+
+ for (PatientSummary.IPSSection section : sectionPrimaries.keySet()) {
+ String narrative = createSectionNarrative(section, resources, ctx);
+ hashedNarratives.put(section, narrative);
+ }
+
+ return hashedNarratives;
+ }
+
+
+
+
+*/
+
+
+ private static class ResourceInclusionCollection {
+
+ private final List myResources = new ArrayList<>();
+ private final Map myIdToResource = new HashMap<>();
+ private final Map myOriginalIdToNewId = new HashMap<>();
+
+ public List getResources() {
+ return myResources;
+ }
+
+ /**
+ * @param theOriginalResourceId Must be an unqualified versionless ID
+ */
+ public void addResourceIfNotAlreadyPresent(IBaseResource theResource, String theOriginalResourceId) {
+ assert theOriginalResourceId.matches("([A-Z][a-z]([A-Za-z]+)/[a-zA-Z0-9._-]+)|(urn:uuid:[0-9a-z-]+)") : "Not an unqualified versionless ID: " + theOriginalResourceId;
+
+ String resourceId = theResource.getIdElement().toUnqualifiedVersionless().getValue();
+ if (myIdToResource.containsKey(resourceId)) {
+ return;
+ }
+
+ myResources.add(theResource);
+ myIdToResource.put(resourceId, theResource);
+ myOriginalIdToNewId.put(theOriginalResourceId, resourceId);
+ }
+
+ public String getIdSubstitution(String theExistingReference) {
+ return myOriginalIdToNewId.get(theExistingReference);
+ }
+
+ public IBaseResource getResourceById(IIdType theReference) {
+ return myIdToResource.get(theReference.toUnqualifiedVersionless().getValue());
+ }
+
+ @Nullable
+ public IBaseResource getResourceByOriginalId(String theOriginalResourceId) {
+ String newResourceId = myOriginalIdToNewId.get(theOriginalResourceId);
+ if (newResourceId != null) {
+ return myIdToResource.get(newResourceId);
+ }
+ return null;
+ }
+
+ public boolean isEmpty() {
+ return myResources.isEmpty();
+ }
+ }
+
+
+}
diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/provider/IpsOperationProvider.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/provider/IpsOperationProvider.java
new file mode 100644
index 00000000000..8371430dfeb
--- /dev/null
+++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/provider/IpsOperationProvider.java
@@ -0,0 +1,75 @@
+package ca.uhn.fhir.jpa.ips.provider;
+
+/*-
+ * #%L
+ * HAPI FHIR JPA Server - International Patient Summary (IPS)
+ * %%
+ * Copyright (C) 2014 - 2023 Smile CDR, Inc.
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import ca.uhn.fhir.jpa.ips.generator.IIpsGeneratorSvc;
+import ca.uhn.fhir.jpa.model.util.JpaConstants;
+import ca.uhn.fhir.model.api.annotation.Description;
+import ca.uhn.fhir.model.valueset.BundleTypeEnum;
+import ca.uhn.fhir.rest.annotation.IdParam;
+import ca.uhn.fhir.rest.annotation.Operation;
+import ca.uhn.fhir.rest.annotation.OperationParam;
+import ca.uhn.fhir.rest.api.server.RequestDetails;
+import ca.uhn.fhir.rest.param.TokenParam;
+import org.hl7.fhir.instance.model.api.IBaseBundle;
+import org.hl7.fhir.instance.model.api.IIdType;
+
+public class IpsOperationProvider {
+
+ private final IIpsGeneratorSvc myIpsGeneratorSvc;
+
+ /**
+ * Constructor
+ */
+ public IpsOperationProvider(IIpsGeneratorSvc theIpsGeneratorSvc) {
+ myIpsGeneratorSvc = theIpsGeneratorSvc;
+ }
+
+
+ /**
+ * Patient/123/$summary
+ */
+ @Operation(name = JpaConstants.OPERATION_SUMMARY, idempotent = true, bundleType = BundleTypeEnum.DOCUMENT, typeName = "Patient", canonicalUrl = JpaConstants.SUMMARY_OPERATION_URL)
+ public IBaseBundle patientInstanceSummary(
+ @IdParam
+ IIdType thePatientId,
+
+ RequestDetails theRequestDetails
+ ) {
+ return myIpsGeneratorSvc.generateIps(theRequestDetails, thePatientId);
+ }
+
+ /**
+ * /Patient/$summary?identifier=foo|bar
+ */
+ @Operation(name = JpaConstants.OPERATION_SUMMARY, idempotent = true, bundleType = BundleTypeEnum.DOCUMENT, typeName = "Patient", canonicalUrl = JpaConstants.SUMMARY_OPERATION_URL)
+ public IBaseBundle patientTypeSummary(
+
+ @Description(shortDefinition = "When the logical id of the patient is not used, servers MAY choose to support patient selection based on provided identifier")
+ @OperationParam(name = "identifier", min = 0, max = 1)
+ TokenParam thePatientIdentifier,
+
+ RequestDetails theRequestDetails
+ ) {
+ return myIpsGeneratorSvc.generateIps(theRequestDetails, thePatientIdentifier);
+ }
+
+}
diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/DefaultIpsGenerationStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/DefaultIpsGenerationStrategy.java
new file mode 100644
index 00000000000..a97df7705ab
--- /dev/null
+++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/DefaultIpsGenerationStrategy.java
@@ -0,0 +1,365 @@
+package ca.uhn.fhir.jpa.ips.strategy;
+
+/*-
+ * #%L
+ * HAPI FHIR JPA Server - International Patient Summary (IPS)
+ * %%
+ * Copyright (C) 2014 - 2023 Smile CDR, Inc.
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import ca.uhn.fhir.jpa.ips.api.IIpsGenerationStrategy;
+import ca.uhn.fhir.jpa.ips.api.IpsContext;
+import ca.uhn.fhir.jpa.ips.api.SectionRegistry;
+import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
+import ca.uhn.fhir.model.api.Include;
+import ca.uhn.fhir.rest.param.TokenOrListParam;
+import ca.uhn.fhir.rest.param.TokenParam;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import org.hl7.fhir.instance.model.api.IBaseResource;
+import org.hl7.fhir.instance.model.api.IIdType;
+import org.hl7.fhir.r4.model.*;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import static ca.uhn.fhir.jpa.term.api.ITermLoaderSvc.LOINC_URI;
+
+@SuppressWarnings({"EnhancedSwitchMigration", "HttpUrlsUsage"})
+public class DefaultIpsGenerationStrategy implements IIpsGenerationStrategy {
+
+ public static final String DEFAULT_IPS_NARRATIVES_PROPERTIES = "classpath:ca/uhn/fhir/jpa/ips/narrative/ips-narratives.properties";
+ private SectionRegistry mySectionRegistry;
+
+ /**
+ * Constructor
+ */
+ public DefaultIpsGenerationStrategy() {
+ setSectionRegistry(new SectionRegistry());
+ }
+
+ @Override
+ public SectionRegistry getSectionRegistry() {
+ return mySectionRegistry;
+ }
+
+ public void setSectionRegistry(SectionRegistry theSectionRegistry) {
+ if (!theSectionRegistry.isInitialized()) {
+ theSectionRegistry.initialize();
+ }
+ mySectionRegistry = theSectionRegistry;
+ }
+
+ @Override
+ public List getNarrativePropertyFiles() {
+ return Lists.newArrayList(
+ DEFAULT_IPS_NARRATIVES_PROPERTIES
+ );
+ }
+
+ @Override
+ public IBaseResource createAuthor() {
+ Organization organization = new Organization();
+ organization.setName("eHealthLab - University of Cyprus")
+ .addAddress(new Address()
+ .addLine("1 University Avenue")
+ .setCity("Nicosia")
+ .setPostalCode("2109")
+ .setCountry("CY"))
+ .setId(IdType.newRandomUuid());
+ return organization;
+ }
+
+ @Override
+ public String createTitle(IpsContext theContext) {
+ return "Patient Summary as of " + DateTimeFormatter.ofPattern("MM/dd/yyyy").format(LocalDate.now());
+ }
+
+ @Override
+ public String createConfidentiality(IpsContext theIpsContext) {
+ return Composition.DocumentConfidentiality.N.toCode();
+ }
+
+ @Override
+ public IIdType massageResourceId(@Nullable IpsContext theIpsContext, @Nonnull IBaseResource theResource) {
+ return IdType.newRandomUuid();
+ }
+
+ @Override
+ public void massageResourceSearch(IpsContext.IpsSectionContext theIpsSectionContext, SearchParameterMap theSearchParameterMap) {
+ switch (theIpsSectionContext.getSection()) {
+ case ALLERGY_INTOLERANCE:
+ case PROBLEM_LIST:
+ case IMMUNIZATIONS:
+ case PROCEDURES:
+ case MEDICAL_DEVICES:
+ case ILLNESS_HISTORY:
+ case FUNCTIONAL_STATUS:
+ return;
+ case VITAL_SIGNS:
+ if (theIpsSectionContext.getResourceType().equals(ResourceType.Observation.name())) {
+ theSearchParameterMap.add(Observation.SP_CATEGORY, new TokenOrListParam()
+ .addOr(new TokenParam("http://terminology.hl7.org/CodeSystem/observation-category", "vital-signs"))
+ );
+ return;
+ }
+ break;
+ case SOCIAL_HISTORY:
+ if (theIpsSectionContext.getResourceType().equals(ResourceType.Observation.name())) {
+ theSearchParameterMap.add(Observation.SP_CATEGORY, new TokenOrListParam()
+ .addOr(new TokenParam("http://terminology.hl7.org/CodeSystem/observation-category", "social-history"))
+ );
+ return;
+ }
+ break;
+ case DIAGNOSTIC_RESULTS:
+ if (theIpsSectionContext.getResourceType().equals(ResourceType.DiagnosticReport.name())) {
+ return;
+ } else if (theIpsSectionContext.getResourceType().equals(ResourceType.Observation.name())) {
+ theSearchParameterMap.add(Observation.SP_CATEGORY, new TokenOrListParam()
+ .addOr(new TokenParam("http://terminology.hl7.org/CodeSystem/observation-category", "laboratory"))
+ );
+ return;
+ }
+ break;
+ case PREGNANCY:
+ if (theIpsSectionContext.getResourceType().equals(ResourceType.Observation.name())) {
+ theSearchParameterMap.add(Observation.SP_CODE, new TokenOrListParam()
+ .addOr(new TokenParam(LOINC_URI, "82810-3"))
+ .addOr(new TokenParam(LOINC_URI, "11636-8"))
+ .addOr(new TokenParam(LOINC_URI, "11637-6"))
+ .addOr(new TokenParam(LOINC_URI, "11638-4"))
+ .addOr(new TokenParam(LOINC_URI, "11639-2"))
+ .addOr(new TokenParam(LOINC_URI, "11640-0"))
+ .addOr(new TokenParam(LOINC_URI, "11612-9"))
+ .addOr(new TokenParam(LOINC_URI, "11613-7"))
+ .addOr(new TokenParam(LOINC_URI, "11614-5"))
+ .addOr(new TokenParam(LOINC_URI, "33065-4"))
+ );
+ return;
+ }
+ break;
+ case MEDICATION_SUMMARY:
+ if (theIpsSectionContext.getResourceType().equals(ResourceType.MedicationStatement.name())) {
+ theSearchParameterMap.add(MedicationStatement.SP_STATUS, new TokenOrListParam()
+ .addOr(new TokenParam(MedicationStatement.MedicationStatementStatus.ACTIVE.getSystem(), MedicationStatement.MedicationStatementStatus.ACTIVE.toCode()))
+ .addOr(new TokenParam(MedicationStatement.MedicationStatementStatus.INTENDED.getSystem(), MedicationStatement.MedicationStatementStatus.INTENDED.toCode()))
+ .addOr(new TokenParam(MedicationStatement.MedicationStatementStatus.UNKNOWN.getSystem(), MedicationStatement.MedicationStatementStatus.UNKNOWN.toCode()))
+ .addOr(new TokenParam(MedicationStatement.MedicationStatementStatus.ONHOLD.getSystem(), MedicationStatement.MedicationStatementStatus.ONHOLD.toCode()))
+ );
+ return;
+ } else if (theIpsSectionContext.getResourceType().equals(ResourceType.MedicationRequest.name())) {
+ theSearchParameterMap.add(MedicationRequest.SP_STATUS, new TokenOrListParam()
+ .addOr(new TokenParam(MedicationRequest.MedicationRequestStatus.ACTIVE.getSystem(), MedicationRequest.MedicationRequestStatus.ACTIVE.toCode()))
+ .addOr(new TokenParam(MedicationRequest.MedicationRequestStatus.UNKNOWN.getSystem(), MedicationRequest.MedicationRequestStatus.UNKNOWN.toCode()))
+ .addOr(new TokenParam(MedicationRequest.MedicationRequestStatus.ONHOLD.getSystem(), MedicationRequest.MedicationRequestStatus.ONHOLD.toCode()))
+ );
+ return;
+ } else if (theIpsSectionContext.getResourceType().equals(ResourceType.MedicationAdministration.name())) {
+ theSearchParameterMap.add(MedicationAdministration.SP_STATUS, new TokenOrListParam()
+ .addOr(new TokenParam(MedicationAdministration.MedicationAdministrationStatus.INPROGRESS.getSystem(), MedicationAdministration.MedicationAdministrationStatus.INPROGRESS.toCode()))
+ .addOr(new TokenParam(MedicationAdministration.MedicationAdministrationStatus.UNKNOWN.getSystem(), MedicationAdministration.MedicationAdministrationStatus.UNKNOWN.toCode()))
+ .addOr(new TokenParam(MedicationAdministration.MedicationAdministrationStatus.ONHOLD.getSystem(), MedicationAdministration.MedicationAdministrationStatus.ONHOLD.toCode()))
+ );
+ return;
+ } else if (theIpsSectionContext.getResourceType().equals(ResourceType.MedicationDispense.name())) {
+ theSearchParameterMap.add(MedicationDispense.SP_STATUS, new TokenOrListParam()
+ .addOr(new TokenParam(MedicationDispense.MedicationDispenseStatus.INPROGRESS.getSystem(), MedicationDispense.MedicationDispenseStatus.INPROGRESS.toCode()))
+ .addOr(new TokenParam(MedicationDispense.MedicationDispenseStatus.UNKNOWN.getSystem(), MedicationDispense.MedicationDispenseStatus.UNKNOWN.toCode()))
+ .addOr(new TokenParam(MedicationDispense.MedicationDispenseStatus.ONHOLD.getSystem(), MedicationDispense.MedicationDispenseStatus.ONHOLD.toCode()))
+ );
+ return;
+ }
+ break;
+ case PLAN_OF_CARE:
+ if (theIpsSectionContext.getResourceType().equals(ResourceType.CarePlan.name())) {
+ theSearchParameterMap.add(CarePlan.SP_STATUS, new TokenOrListParam()
+ .addOr(new TokenParam(CarePlan.CarePlanStatus.ACTIVE.getSystem(), CarePlan.CarePlanStatus.ACTIVE.toCode()))
+ .addOr(new TokenParam(CarePlan.CarePlanStatus.ONHOLD.getSystem(), CarePlan.CarePlanStatus.ONHOLD.toCode()))
+ .addOr(new TokenParam(CarePlan.CarePlanStatus.UNKNOWN.getSystem(), CarePlan.CarePlanStatus.UNKNOWN.toCode()))
+ );
+ return;
+ }
+ break;
+ case ADVANCE_DIRECTIVES:
+ if (theIpsSectionContext.getResourceType().equals(ResourceType.Consent.name())) {
+ theSearchParameterMap.add(Consent.SP_STATUS, new TokenOrListParam()
+ .addOr(new TokenParam(Consent.ConsentState.ACTIVE.getSystem(), Consent.ConsentState.ACTIVE.toCode()))
+ );
+ return;
+ }
+ break;
+ }
+
+ // Shouldn't happen: This means none of the above switches handled the Section+resourceType combination
+ assert false : "Don't know how to handle " + theIpsSectionContext.getSection() + "/" + theIpsSectionContext.getResourceType();
+ }
+
+ @Nonnull
+ @Override
+ public Set provideResourceSearchIncludes(IpsContext.IpsSectionContext theIpsSectionContext) {
+ switch (theIpsSectionContext.getSection()) {
+ case MEDICATION_SUMMARY:
+ if (ResourceType.MedicationStatement.name().equals(theIpsSectionContext.getResourceType())) {
+ return Sets.newHashSet(
+ MedicationStatement.INCLUDE_MEDICATION
+ );
+ }
+ if (ResourceType.MedicationRequest.name().equals(theIpsSectionContext.getResourceType())) {
+ return Sets.newHashSet(
+ MedicationRequest.INCLUDE_MEDICATION
+ );
+ }
+ if (ResourceType.MedicationAdministration.name().equals(theIpsSectionContext.getResourceType())) {
+ return Sets.newHashSet(
+ MedicationAdministration.INCLUDE_MEDICATION
+ );
+ }
+ if (ResourceType.MedicationDispense.name().equals(theIpsSectionContext.getResourceType())) {
+ return Sets.newHashSet(
+ MedicationDispense.INCLUDE_MEDICATION
+ );
+ }
+ break;
+ case MEDICAL_DEVICES:
+ if (ResourceType.DeviceUseStatement.name().equals(theIpsSectionContext.getResourceType())) {
+ return Sets.newHashSet(
+ DeviceUseStatement.INCLUDE_DEVICE
+ );
+ }
+ break;
+ case ALLERGY_INTOLERANCE:
+ case PROBLEM_LIST:
+ case IMMUNIZATIONS:
+ case PROCEDURES:
+ case DIAGNOSTIC_RESULTS:
+ case VITAL_SIGNS:
+ case ILLNESS_HISTORY:
+ case PREGNANCY:
+ case SOCIAL_HISTORY:
+ case FUNCTIONAL_STATUS:
+ case PLAN_OF_CARE:
+ case ADVANCE_DIRECTIVES:
+ break;
+ }
+ return Collections.emptySet();
+ }
+
+ @SuppressWarnings("EnhancedSwitchMigration")
+ @Override
+ public boolean shouldInclude(IpsContext.IpsSectionContext theIpsSectionContext, IBaseResource theCandidate) {
+
+ switch (theIpsSectionContext.getSection()) {
+ case MEDICATION_SUMMARY:
+ case PLAN_OF_CARE:
+ case ADVANCE_DIRECTIVES:
+ return true;
+ case ALLERGY_INTOLERANCE:
+ if (theIpsSectionContext.getResourceType().equals(ResourceType.AllergyIntolerance.name())) {
+ AllergyIntolerance allergyIntolerance = (AllergyIntolerance) theCandidate;
+ return !allergyIntolerance.getClinicalStatus().hasCoding("http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical", "inactive")
+ && !allergyIntolerance.getClinicalStatus().hasCoding("http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical", "resolved")
+ && !allergyIntolerance.getVerificationStatus().hasCoding("http://terminology.hl7.org/CodeSystem/allergyintolerance-verification", "entered-in-error");
+ }
+ break;
+ case PROBLEM_LIST:
+ if (theIpsSectionContext.getResourceType().equals(ResourceType.Condition.name())) {
+ Condition prob = (Condition) theCandidate;
+ return !prob.getClinicalStatus().hasCoding("http://terminology.hl7.org/CodeSystem/condition-clinical", "inactive")
+ && !prob.getClinicalStatus().hasCoding("http://terminology.hl7.org/CodeSystem/condition-clinical", "resolved")
+ && !prob.getVerificationStatus().hasCoding("http://terminology.hl7.org/CodeSystem/condition-ver-status", "entered-in-error");
+ }
+ break;
+ case IMMUNIZATIONS:
+ if (theIpsSectionContext.getResourceType().equals(ResourceType.Immunization.name())) {
+ Immunization immunization = (Immunization) theCandidate;
+ return immunization.getStatus() != Immunization.ImmunizationStatus.ENTEREDINERROR;
+ }
+ break;
+ case PROCEDURES:
+ if (theIpsSectionContext.getResourceType().equals(ResourceType.Procedure.name())) {
+ Procedure proc = (Procedure) theCandidate;
+ return proc.getStatus() != Procedure.ProcedureStatus.ENTEREDINERROR
+ && proc.getStatus() != Procedure.ProcedureStatus.NOTDONE;
+ }
+ break;
+ case MEDICAL_DEVICES:
+ if (theIpsSectionContext.getResourceType().equals(ResourceType.DeviceUseStatement.name())) {
+ DeviceUseStatement deviceUseStatement = (DeviceUseStatement) theCandidate;
+ return deviceUseStatement.getStatus() != DeviceUseStatement.DeviceUseStatementStatus.ENTEREDINERROR;
+ }
+ return true;
+ case DIAGNOSTIC_RESULTS:
+ if (theIpsSectionContext.getResourceType().equals(ResourceType.DiagnosticReport.name())) {
+ return true;
+ }
+ if (theIpsSectionContext.getResourceType().equals(ResourceType.Observation.name())) {
+ // code filtering not yet applied
+ Observation observation = (Observation) theCandidate;
+ return (observation.getStatus() != Observation.ObservationStatus.PRELIMINARY);
+ }
+ break;
+ case VITAL_SIGNS:
+ if (theIpsSectionContext.getResourceType().equals(ResourceType.Observation.name())) {
+ // code filtering not yet applied
+ Observation observation = (Observation) theCandidate;
+ return (observation.getStatus() != Observation.ObservationStatus.PRELIMINARY);
+ }
+ break;
+ case ILLNESS_HISTORY:
+ if (theIpsSectionContext.getResourceType().equals(ResourceType.Condition.name())) {
+ Condition prob = (Condition) theCandidate;
+ if (prob.getVerificationStatus().hasCoding("http://terminology.hl7.org/CodeSystem/condition-ver-status", "entered-in-error")) {
+ return false;
+ } else {
+ return prob.getClinicalStatus().hasCoding("http://terminology.hl7.org/CodeSystem/condition-clinical", "inactive")
+ || prob.getClinicalStatus().hasCoding("http://terminology.hl7.org/CodeSystem/condition-clinical", "resolved")
+ || prob.getClinicalStatus().hasCoding("http://terminology.hl7.org/CodeSystem/condition-clinical", "remission");
+ }
+ }
+ break;
+ case PREGNANCY:
+ if (theIpsSectionContext.getResourceType().equals(ResourceType.Observation.name())) {
+ // code filtering not yet applied
+ Observation observation = (Observation) theCandidate;
+ return (observation.getStatus() != Observation.ObservationStatus.PRELIMINARY);
+ }
+ break;
+ case SOCIAL_HISTORY:
+ if (theIpsSectionContext.getResourceType().equals(ResourceType.Observation.name())) {
+ // code filtering not yet applied
+ Observation observation = (Observation) theCandidate;
+ return (observation.getStatus() != Observation.ObservationStatus.PRELIMINARY);
+ }
+ break;
+ case FUNCTIONAL_STATUS:
+ if (theIpsSectionContext.getResourceType().equals(ResourceType.ClinicalImpression.name())) {
+ ClinicalImpression clinicalImpression = (ClinicalImpression) theCandidate;
+ return clinicalImpression.getStatus() != ClinicalImpression.ClinicalImpressionStatus.INPROGRESS
+ && clinicalImpression.getStatus() != ClinicalImpression.ClinicalImpressionStatus.ENTEREDINERROR;
+ }
+ break;
+ }
+
+ return true;
+ }
+
+}
diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/advancedirectives.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/advancedirectives.html
new file mode 100644
index 00000000000..7ab1f745ad4
--- /dev/null
+++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/advancedirectives.html
@@ -0,0 +1,37 @@
+
+
+