Add IPS Generator (#4438)

* Begin IPS refactor

* Commit work so far

* Fix test bug

* Fix typo

* Fix typo

* Narrative generator cleanup

* Narrative generator working

* Add test for bad reference in narrative

* Tests passing

* Start docs

* Tests passing

* Cleanup

* Update cyangelog

* Doc tweaks

* Version bump

* Address review comments

* Address review comments

* Build fix

* Cleanup

* Compile fix

* Test fix

* Test fix

* Version bump

* Build update

* Test fix

* Test fix

* Add one utility method

* Add doc
This commit is contained in:
James Agnew 2023-01-24 12:07:36 -05:00 committed by GitHub
parent 320a0d895f
commit d584e14048
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
186 changed files with 4984 additions and 556 deletions

View File

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

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.3.12-SNAPSHOT</version>
<version>6.3.13-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.12-SNAPSHOT</version>
<version>6.3.13-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.12-SNAPSHOT</version>
<version>6.3.13-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -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 <code>resolve()</code> function to
* fetch referenced resources.
*
* @since 6.4.0
*/
void setEvaluationContext(@Nonnull IFhirPathEvaluationContext theEvaluationContext);
}

View File

@ -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 <code>resolve()</code> function and returns the target
* of the resolution.
*
* @param theReference The reference
* @param theContext The entity containing the reference. Note that this will be <code>null</code> for FHIR versions R4 and below.
*/
default IBase resolveReference(@Nonnull IIdType theReference, @Nullable IBase theContext) {
return null;
}
}

View File

@ -37,7 +37,7 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
* upgrading servers.
* </p>
* <p>
* Note on thrwead safety: This class is not thread safe.
* Note on thread safety: This class is not thread safe.
* </p>
*/
public class Include implements Serializable {

View File

@ -21,18 +21,55 @@ package ca.uhn.fhir.narrative;
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.fhirpath.IFhirPath;
import ca.uhn.fhir.fhirpath.IFhirPathEvaluationContext;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.narrative2.NarrativeTemplateManifest;
import ca.uhn.fhir.narrative2.ThymeleafNarrativeGenerator;
import ca.uhn.fhir.narrative2.BaseNarrativeGenerator;
import ca.uhn.fhir.narrative2.INarrativeTemplate;
import ca.uhn.fhir.narrative2.TemplateTypeEnum;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import org.hl7.fhir.instance.model.api.IBaseResource;
import com.google.common.collect.Sets;
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.IExpressionContext;
import org.thymeleaf.context.ITemplateContext;
import org.thymeleaf.dialect.IDialect;
import org.thymeleaf.dialect.IExpressionObjectDialect;
import org.thymeleaf.engine.AttributeName;
import org.thymeleaf.expression.IExpressionObjectFactory;
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.templateresolver.ITemplateResolver;
import org.thymeleaf.templateresource.ITemplateResource;
import org.thymeleaf.templateresource.StringTemplateResource;
import java.io.IOException;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
public abstract class BaseThymeleafNarrativeGenerator extends ThymeleafNarrativeGenerator {
import static org.apache.commons.lang3.StringUtils.isNotBlank;
private boolean myInitialized;
public abstract class BaseThymeleafNarrativeGenerator extends BaseNarrativeGenerator {
public static final String FHIRPATH = "fhirpath";
private IMessageResolver myMessageResolver;
private IFhirPathEvaluationContext myFhirPathEvaluationContext;
/**
* Constructor
@ -41,32 +78,244 @@ public abstract class BaseThymeleafNarrativeGenerator extends ThymeleafNarrative
super();
}
public void setFhirPathEvaluationContext(IFhirPathEvaluationContext theFhirPathEvaluationContext) {
myFhirPathEvaluationContext = theFhirPathEvaluationContext;
}
private TemplateEngine getTemplateEngine(FhirContext theFhirContext) {
TemplateEngine engine = new TemplateEngine();
ITemplateResolver resolver = new NarrativeTemplateResolver(theFhirContext);
engine.setTemplateResolver(resolver);
if (myMessageResolver != null) {
engine.setMessageResolver(myMessageResolver);
}
StandardDialect dialect = new StandardDialect() {
@Override
public Set<IProcessor> getProcessors(String theDialectPrefix) {
Set<IProcessor> retVal = super.getProcessors(theDialectPrefix);
retVal.add(new NarrativeTagProcessor(theFhirContext, theDialectPrefix));
retVal.add(new NarrativeAttributeProcessor(theDialectPrefix, theFhirContext));
return retVal;
}
};
engine.setDialect(dialect);
engine.addDialect(new NarrativeGeneratorDialect(theFhirContext));
return engine;
}
@Override
public boolean populateResourceNarrative(FhirContext theFhirContext, IBaseResource theResource) {
if (!myInitialized) {
initialize();
}
super.populateResourceNarrative(theFhirContext, theResource);
return false;
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);
}
protected abstract List<String> getPropertyFile();
private synchronized void initialize() {
if (myInitialized) {
return;
}
List<String> propFileName = getPropertyFile();
try {
NarrativeTemplateManifest manifest = NarrativeTemplateManifest.forManifestFileLocation(propFileName);
setManifest(manifest);
} catch (IOException e) {
throw new InternalErrorException(Msg.code(1808) + e);
}
myInitialized = true;
@Override
protected EnumSet<TemplateTypeEnum> 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<INarrativeTemplate> 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 NarrativeTemplateResolver extends DefaultTemplateResolver {
private final FhirContext myFhirContext;
private NarrativeTemplateResolver(FhirContext theFhirContext) {
myFhirContext = theFhirContext;
}
@Override
protected boolean computeResolvable(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map<String, Object> theTemplateResolutionAttributes) {
if (theOwnerTemplate == null) {
return getManifest().getTemplateByName(myFhirContext, getStyle(), theTemplate).size() > 0;
} else {
return getManifest().getTemplateByFragmentName(myFhirContext, getStyle(), theTemplate).size() > 0;
}
}
@Override
protected TemplateMode computeTemplateMode(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map<String, Object> theTemplateResolutionAttributes) {
return TemplateMode.XML;
}
@Override
protected ITemplateResource computeTemplateResource(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map<String, Object> theTemplateResolutionAttributes) {
if (theOwnerTemplate == null) {
return getManifest()
.getTemplateByName(myFhirContext, getStyle(), theTemplate)
.stream()
.findFirst()
.map(t -> new StringTemplateResource(t.getTemplateText()))
.orElseThrow(() -> new IllegalArgumentException("Unknown template: " + theTemplate));
} else {
return getManifest()
.getTemplateByFragmentName(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<String, Object> 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
* <th:block th:narrative="${result}"/>
*/
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);
}
}
private class NarrativeGeneratorDialect implements IDialect, IExpressionObjectDialect {
private final FhirContext myFhirContext;
public NarrativeGeneratorDialect(FhirContext theFhirContext) {
myFhirContext = theFhirContext;
}
@Override
public String getName() {
return "NarrativeGeneratorDialect";
}
@Override
public IExpressionObjectFactory getExpressionObjectFactory() {
return new NarrativeGeneratorExpressionObjectFactory(myFhirContext);
}
}
private class NarrativeGeneratorExpressionObjectFactory implements IExpressionObjectFactory {
private final FhirContext myFhirContext;
public NarrativeGeneratorExpressionObjectFactory(FhirContext theFhirContext) {
myFhirContext = theFhirContext;
}
@Override
public Set<String> getAllExpressionObjectNames() {
return Sets.newHashSet(FHIRPATH);
}
@Override
public Object buildObject(IExpressionContext context, String expressionObjectName) {
if (FHIRPATH.equals(expressionObjectName)) {
return new NarrativeGeneratorFhirPathExpressionObject(myFhirContext);
}
return null;
}
@Override
public boolean isCacheable(String expressionObjectName) {
return false;
}
}
private class NarrativeGeneratorFhirPathExpressionObject {
private final FhirContext myFhirContext;
public NarrativeGeneratorFhirPathExpressionObject(FhirContext theFhirContext) {
myFhirContext = theFhirContext;
}
public IBase evaluateFirst(IBase theInput, String theExpression) {
IFhirPath fhirPath = newFhirPath();
Optional<IBase> output = fhirPath.evaluateFirst(theInput, theExpression, IBase.class);
return output.orElse(null);
}
public List<IBase> evaluate(IBase theInput, String theExpression) {
IFhirPath fhirPath = newFhirPath();
return fhirPath.evaluate(theInput, theExpression, IBase.class);
}
private IFhirPath newFhirPath() {
IFhirPath fhirPath = myFhirContext.newFhirPath();
if (myFhirPathEvaluationContext != null) {
fhirPath.setEvaluationContext(myFhirPathEvaluationContext);
}
return fhirPath;
}
}
}

View File

@ -20,48 +20,80 @@ package ca.uhn.fhir.narrative;
* #L%
*/
import ca.uhn.fhir.narrative2.NarrativeTemplateManifest;
import org.apache.commons.lang3.Validate;
import java.util.Arrays;
import java.util.List;
import org.apache.commons.lang3.Validate;
public class CustomThymeleafNarrativeGenerator extends BaseThymeleafNarrativeGenerator {
private List<String> myPropertyFile;
private volatile List<String> myPropertyFile;
private volatile NarrativeTemplateManifest myManifest;
/**
* Constructor. If this constructor is used you must explicitly call
* {@link #setManifest(NarrativeTemplateManifest)} to provide a template
* manifest before using the generator.
*/
public CustomThymeleafNarrativeGenerator() {
super();
}
/**
* Create a new narrative generator
*
* @param theNarrativePropertyFiles The name of the property file, in one of the following formats:
* <ul>
* <li>file:/path/to/file/file.properties</li>
* <li>classpath:/com/package/file.properties</li>
* </ul>
*/
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:
* <ul>
* <li>file:/path/to/file/file.properties</li>
* <li>classpath:/com/package/file.properties</li>
* </ul>
*
* @param theNarrativePropertyFiles The name of the property file, in one of the following formats:
* <ul>
* <li>file:/path/to/file/file.properties</li>
* <li>classpath:/com/package/file.properties</li>
* </ul>
*/
public CustomThymeleafNarrativeGenerator(String... thePropertyFile) {
super();
setPropertyFile(thePropertyFile);
public CustomThymeleafNarrativeGenerator(List<String> 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:
* <ul>
* <li>file:/path/to/file/file.properties</li>
* <li>classpath:/com/package/file.properties</li>
* </ul>
*
* @param thePropertyFile The name of the property file, in one of the following formats:
* <ul>
* <li>file:/path/to/file/file.properties</li>
* <li>classpath:/com/package/file.properties</li>
* </ul>
*/
public void setPropertyFile(String... thePropertyFile) {
Validate.notNull(thePropertyFile, "Property file can not be null");
myPropertyFile = Arrays.asList(thePropertyFile);
myManifest = null;
}
@Override
public List<String> getPropertyFile() {
return myPropertyFile;
}
}

View File

@ -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<String> getPropertyFile() {
List<String> retVal = new ArrayList<String>();
retVal.add(NARRATIVES_PROPERTIES);
if (myUseHapiServerConformanceNarrative) {
retVal.add(HAPISERVER_NARRATIVES_PROPERTIES);
protected NarrativeTemplateManifest getManifest() {
NarrativeTemplateManifest retVal = myManifest;
if (retVal == null) {
List<String> 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 <code>true</code> (default is <code>false</code>) a special custom narrative for the Conformance resource will be provided, which is designed to be used with HAPI {@link RestfulServer}
* If set to <code>true</code> (default is <code>false</code>) 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 <code>true</code> (default is <code>false</code>) a special custom narrative for the Conformance resource will be provided, which is designed to be used with HAPI {@link RestfulServer}
* If set to <code>true</code> (default is <code>false</code>) 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() {

View File

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

View File

@ -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<INarrativeTemplate> 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<INarrativeTemplate> getTemplateForElement(FhirContext theFhirContext, IBase theElement) {
return myManifest.getTemplateByElement(theFhirContext, getStyle(), theElement);
@Nullable
private INarrativeTemplate selectTemplate(FhirContext theFhirContext, IBaseResource theResource) {
List<INarrativeTemplate> 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<INarrativeTemplate> 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();
}

View File

@ -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<INarrativeTemplate> getTemplateByResourceName(FhirContext theFhirContext, EnumSet<TemplateTypeEnum> theStyles, String theResourceName);
List<INarrativeTemplate> getTemplateByResourceName(@Nonnull FhirContext theFhirContext, @Nonnull EnumSet<TemplateTypeEnum> theStyles, @Nonnull String theResourceName, @Nonnull Collection<String> theProfiles);
List<INarrativeTemplate> getTemplateByName(FhirContext theFhirContext, EnumSet<TemplateTypeEnum> theStyles, String theName);
List<INarrativeTemplate> getTemplateByName(@Nonnull FhirContext theFhirContext, @Nonnull EnumSet<TemplateTypeEnum> theStyles, @Nonnull String theName);
List<INarrativeTemplate> getTemplateByElement(FhirContext theFhirContext, EnumSet<TemplateTypeEnum> theStyles, IBase theElementValue);
List<INarrativeTemplate> getTemplateByElement(@Nonnull FhirContext theFhirContext, @Nonnull EnumSet<TemplateTypeEnum> theStyles, @Nonnull IBase theElementValue);
List<INarrativeTemplate> getTemplateByFragmentName(@Nonnull FhirContext theFhirContext, @Nonnull EnumSet<TemplateTypeEnum> theStyles, @Nonnull String theFragmentName);
}

View File

@ -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<String> myAppliesToProfiles = new HashSet<>();
private final Set<String> myAppliesToResourceTypes = new HashSet<>();
private final Set<String> myAppliesToDataTypes = new HashSet<>();
private final Set<Class<? extends IBase>> myAppliesToClasses = new HashSet<>();
private final Set<String> myAppliesToFragmentNames = new HashSet<>();
private String myTemplateFileName;
private Set<String> myAppliesToProfiles = new HashSet<>();
private Set<String> myAppliesToResourceTypes = new HashSet<>();
private Set<String> myAppliesToDataTypes = new HashSet<>();
private Set<Class<? extends IBase>> 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<String> getAppliesToDataTypes() {
return Collections.unmodifiableSet(myAppliesToDataTypes);
}
public Set<String> 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) {

View File

@ -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<String, List<NarrativeTemplate>> myResourceTypeToTemplate;
private final Map<String, List<NarrativeTemplate>> myDatatypeToTemplate;
private final Map<String, List<NarrativeTemplate>> myNameToTemplate;
private final Map<String, List<NarrativeTemplate>> myClassToTemplate;
private final ListMultimap<String, NarrativeTemplate> myResourceTypeToTemplate;
private final ListMultimap<String, NarrativeTemplate> myDatatypeToTemplate;
private final ListMultimap<String, NarrativeTemplate> myNameToTemplate;
private final ListMultimap<String, NarrativeTemplate> myFragmentNameToTemplate;
private final ListMultimap<String, NarrativeTemplate> myClassToTemplate;
private final int myTemplateCount;
private NarrativeTemplateManifest(Collection<NarrativeTemplate> theTemplates) {
Map<String, List<NarrativeTemplate>> resourceTypeToTemplate = new HashMap<>();
Map<String, List<NarrativeTemplate>> datatypeToTemplate = new HashMap<>();
Map<String, List<NarrativeTemplate>> nameToTemplate = new HashMap<>();
Map<String, List<NarrativeTemplate>> classToTemplate = new HashMap<>();
ListMultimap<String, NarrativeTemplate> resourceTypeToTemplate = ArrayListMultimap.create();
ListMultimap<String, NarrativeTemplate> datatypeToTemplate = ArrayListMultimap.create();
ListMultimap<String, NarrativeTemplate> nameToTemplate = ArrayListMultimap.create();
ListMultimap<String, NarrativeTemplate> classToTemplate = ArrayListMultimap.create();
ListMultimap<String, NarrativeTemplate> 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<INarrativeTemplate> getTemplateByResourceName(FhirContext theFhirContext, EnumSet<TemplateTypeEnum> theStyles, String theResourceName) {
return getFromMap(theStyles, theResourceName.toUpperCase(), myResourceTypeToTemplate);
public List<INarrativeTemplate> getTemplateByResourceName(@Nonnull FhirContext theFhirContext, @Nonnull EnumSet<TemplateTypeEnum> theStyles, @Nonnull String theResourceName, @Nonnull Collection<String> theProfiles) {
return getFromMap(theStyles, theResourceName.toUpperCase(), myResourceTypeToTemplate, theProfiles);
}
@Override
public List<INarrativeTemplate> getTemplateByName(FhirContext theFhirContext, EnumSet<TemplateTypeEnum> theStyles, String theName) {
return getFromMap(theStyles, theName, myNameToTemplate);
public List<INarrativeTemplate> getTemplateByName(@Nonnull FhirContext theFhirContext, @Nonnull EnumSet<TemplateTypeEnum> theStyles, @Nonnull String theName) {
return getFromMap(theStyles, theName, myNameToTemplate, Collections.emptyList());
}
@Override
public List<INarrativeTemplate> getTemplateByElement(FhirContext theFhirContext, EnumSet<TemplateTypeEnum> theStyles, IBase theElement) {
List<INarrativeTemplate> retVal = getFromMap(theStyles, theElement.getClass().getName(), myClassToTemplate);
public List<INarrativeTemplate> getTemplateByFragmentName(@Nonnull FhirContext theFhirContext, @Nonnull EnumSet<TemplateTypeEnum> theStyles, @Nonnull String theFragmentName) {
return getFromMap(theStyles, theFragmentName, myFragmentNameToTemplate, Collections.emptyList());
}
@SuppressWarnings("PatternVariableCanBeUsed")
@Override
public List<INarrativeTemplate> getTemplateByElement(@Nonnull FhirContext theFhirContext, @Nonnull EnumSet<TemplateTypeEnum> theStyles, @Nonnull IBase theElement) {
List<INarrativeTemplate> retVal = Collections.emptyList();
if (theElement instanceof IBaseResource) {
IBaseResource resource = (IBaseResource) theElement;
String resourceName = theFhirContext.getResourceDefinition(resource).getName();
List<String> 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<String> thePropertyFilePaths) throws IOException {
public static NarrativeTemplateManifest forManifestFileLocation(Collection<String> thePropertyFilePaths) {
ourLog.debug("Loading narrative properties file(s): {}", thePropertyFilePaths);
List<String> 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<String> theResources) throws IOException {
List<NarrativeTemplate> templates = new ArrayList<>();
for (String next : theResources) {
templates.addAll(loadProperties(next));
public static NarrativeTemplateManifest forManifestFileContents(Collection<String> theResources) {
try {
List<NarrativeTemplate> 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<NarrativeTemplate> loadProperties(String theManifestText) throws IOException {
Map<String, NarrativeTemplate> 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<String> 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 <T> List<INarrativeTemplate> getFromMap(EnumSet<TemplateTypeEnum> theStyles, T theKey, Map<T, List<NarrativeTemplate>> theMap) {
private static <T> List<INarrativeTemplate> getFromMap(EnumSet<TemplateTypeEnum> theStyles, T theKey, ListMultimap<T, NarrativeTemplate> theMap, Collection<String> theProfiles) {
return theMap
.getOrDefault(theKey, Collections.emptyList())
.stream()
.filter(t -> theStyles.contains(t.getTemplateType()))
.collect(Collectors.toList());
}
private static <T> Map<T, List<NarrativeTemplate>> makeImmutable(Map<T, List<NarrativeTemplate>> 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());
}
}

View File

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

View File

@ -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<IProcessor> getProcessors(String theDialectPrefix) {
Set<IProcessor> 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<TemplateTypeEnum> 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<INarrativeTemplate> 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<String, Object> theTemplateResolutionAttributes) {
return getManifest().getTemplateByName(myFhirContext, getStyle(), theTemplate).size() > 0;
}
@Override
protected TemplateMode computeTemplateMode(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map<String, Object> theTemplateResolutionAttributes) {
return TemplateMode.XML;
}
@Override
protected ITemplateResource computeTemplateResource(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map<String, Object> 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<String, Object> 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
* <th:block th:narrative="${result}"/>
*/
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);
}
}
}

View File

@ -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 <code>Bundle.identifier</code>
*
* @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 <code>Bundle.timestamp</code>
*
* @since 6.4.0
*/
public void setTimestamp(@Nonnull IPrimitiveType<Date> 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.
*
* <p>
* 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;
}

View File

@ -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 extends IBaseResource> T loadCompressedResource(FhirContext theCtx, Class<T> theType, String theClasspath) {
String resource = loadCompressedResource(theClasspath);
return parseResource(theCtx, theType, resource);
}
@Nonnull
public static <T extends IBaseResource> T loadResource(FhirContext theCtx, Class<T> theType, String theClasspath) {
String raw = loadResource(theClasspath);
return parseResource(theCtx, theType, raw);
}
private static <T extends IBaseResource> T parseResource(FhirContext theCtx, Class<T> theType, String raw) {
return EncodingEnum.detectEncodingNoDefault(raw).newParser(theCtx).parseResource(theType, raw);
}

View File

@ -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 <code>Composition</code> 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 extends IBaseResource> T getComposition() {
return (T) myComposition;
}
/**
* Add a value to <code>Composition.author</code>
*/
public void addAuthor(IIdType theAuthorId) {
IBaseReference reference = myTerser.addElement(myComposition, "Composition.author");
reference.setReference(theAuthorId.getValue());
}
/**
* Set a value in <code>Composition.status</code>
*/
public void setStatus(String theStatusCode) {
myTerser.setElement(myComposition, "Composition.status", theStatusCode);
}
/**
* Set a value in <code>Composition.subject</code>
*/
public void setSubject(IIdType theSubject) {
myTerser.setElement(myComposition, "Composition.subject.reference", theSubject.getValue());
}
/**
* Add a Coding to <code>Composition.type.coding</code>
*/
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 <code>Composition.date</code>
*/
public void setDate(IPrimitiveType<Date> theDate) {
myTerser.setElement(myComposition, "Composition.date", theDate.getValueAsString());
}
/**
* Set a value in <code>Composition.title</code>
*/
public void setTitle(String theTitle) {
myTerser.setElement(myComposition, "Composition.title", theTitle);
}
/**
* Set a value in <code>Composition.confidentiality</code>
*/
public void setConfidentiality(String theConfidentiality) {
myTerser.setElement(myComposition, "Composition.confidentiality", theConfidentiality);
}
/**
* Set a value in <code>Composition.id</code>
*/
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);
}
}
}

View File

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

View File

@ -4,14 +4,14 @@
<modelVersion>4.0.0</modelVersion>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-bom</artifactId>
<version>6.3.12-SNAPSHOT</version>
<version>6.3.13-SNAPSHOT</version>
<packaging>pom</packaging>
<name>HAPI FHIR BOM</name>
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.12-SNAPSHOT</version>
<version>6.3.13-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
@ -77,6 +77,11 @@
<artifactId>hapi-fhir-jpaserver-elastic-test-utilities</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>hapi-fhir-jpaserver-ips</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>hapi-fhir-jpaserver-mdm</artifactId>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.3.12-SNAPSHOT</version>
<version>6.3.13-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.12-SNAPSHOT</version>
<version>6.3.13-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-cli</artifactId>
<version>6.3.12-SNAPSHOT</version>
<version>6.3.13-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.12-SNAPSHOT</version>
<version>6.3.13-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.3.12-SNAPSHOT</version>
<version>6.3.13-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.12-SNAPSHOT</version>
<version>6.3.13-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.12-SNAPSHOT</version>
<version>6.3.13-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.12-SNAPSHOT</version>
<version>6.3.13-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.3.12-SNAPSHOT</version>
<version>6.3.13-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.12-SNAPSHOT</version>
<version>6.3.13-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
@ -60,6 +60,11 @@
<artifactId>hapi-fhir-server-openapi</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-test-utilities</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) &ndash; This method returns the first element matched on `input` by the path expression, or _null_ if nothing matches.
* evaluate(input, pathExpression) &ndash; 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}}
```

View File

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

View File

@ -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
<a href="ips/overview.svg" target="_blank"><img src="ips/overview.svg" alt="IPS Overview" style="width: 100%; max-width: 600px;"/></a>
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.
<a name="generation-strategy"/>
# 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)
<a name="section-registry"/>
# 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)
<a name="narrative-templates"/>
# 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.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -11,7 +11,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.12-SNAPSHOT</version>
<version>6.3.13-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
@ -181,6 +181,11 @@
<artifactId>hapi-fhir-jpaserver-model</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-jpaserver-ips</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-jpaserver-mdm</artifactId>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.12-SNAPSHOT</version>
<version>6.3.13-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.12-SNAPSHOT</version>
<version>6.3.13-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.12-SNAPSHOT</version>
<version>6.3.13-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
@ -376,6 +376,12 @@
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-test-utilities</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>

View File

@ -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;

View File

@ -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;

View File

@ -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
*/

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.12-SNAPSHOT</version>
<version>6.3.13-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -0,0 +1,48 @@
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.13-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
<artifactId>hapi-fhir-jpaserver-ips</artifactId>
<packaging>jar</packaging>
<name>HAPI FHIR JPA Server - International Patient Summary (IPS)</name>
<dependencies>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-jpaserver-base</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Provided -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<!-- Test -->
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-jpaserver-test-utilities</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-test-utilities</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.sourceforge.htmlunit</groupId>
<artifactId>htmlunit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -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.
* <p>
* 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.
* <p>
* Entries should be of the format <code>classpath:path/to/file.properties</code>
* </p>
* <p>
* 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.
* </p>
*/
List<String> getNarrativePropertyFiles();
/**
* Create and return a new <code>Organization</code> 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 <code>Composition.confidentiality</code>
*
* @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 <code>null</code> 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.
* <p>
* For example, for a Vital Signs section, the implementation might add a parameter indicating
* the parameter <code>category=vital-signs</code>.
*
* @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<Include> 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);
}

View File

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

View File

@ -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
}

View File

@ -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.
* <p>
* By default, all standard sections in the
* <a href="http://hl7.org/fhir/uv/ips/">base IPS specification IG</a>
* are included. You can customize this to remove sections, or to add new ones
* as permitted by the IG.
* </p>
* <p>
* 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()}.
* </p>
*/
public class SectionRegistry {
private final ArrayList<Section> mySections = new ArrayList<>();
private List<Consumer<SectionBuilder>> 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<SectionBuilder> theGlobalCustomizer) {
Validate.notNull(theGlobalCustomizer, "theGlobalCustomizer must not be null");
myGlobalCustomizers.add(theGlobalCustomizer);
return this;
}
public List<Section> 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<String> 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<String> myResourceTypes;
private final String myProfile;
private final INoInfoGenerator myNoInfoGenerator;
public Section(IpsSectionEnum theSectionEnum, String theTitle, String theSectionCode, String theSectionDisplay, List<String> 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<String> 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;
}
}
}

View File

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

View File

@ -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<Include> 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<IBaseResource> 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<ResourceReferenceInfo> 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<String> narrativeLinkUri = (IPrimitiveType<String>) 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<String> 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<PatientSummary.IPSSection, List<Resource>> hashPrimaries(List<Resource> resourceList) {
HashMap<PatientSummary.IPSSection, List<Resource>> iPSResourceMap = new HashMap<PatientSummary.IPSSection, List<Resource>>();
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<Resource>());
}
iPSResourceMap.get(iPSSection).add(resource);
}
}
}
}
return iPSResourceMap;
}
private static HashMap<PatientSummary.IPSSection, List<Resource>> filterPrimaries(HashMap<PatientSummary.IPSSection, List<Resource>> sectionPrimaries) {
HashMap<PatientSummary.IPSSection, List<Resource>> filteredPrimaries = new HashMap<PatientSummary.IPSSection, List<Resource>>();
for ( PatientSummary.IPSSection section : sectionPrimaries.keySet() ) {
List<Resource> filteredList = new ArrayList<Resource>();
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<Resource> pruneResources(Patient patient, List<Resource> resources, HashMap<PatientSummary.IPSSection, List<Resource>> sectionPrimaries, FhirContext ctx) {
List<String> resourceIds = new ArrayList<String>();
List<String> followedIds = new ArrayList<String>();
HashMap<String, Resource> resourcesById = new HashMap<String, Resource>();
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<Resource> prunedResources = new ArrayList<Resource>();
for (Resource resource : resources) {
if (resourceIds.contains(resource.getIdElement().getIdPart())) {
prunedResources.add(resource);
}
}
return prunedResources;
}
private static Void recursivePrune(String resourceId, List<String> resourceIds, List<String> followedIds, HashMap<String, Resource> 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<Resource> addLinkToResources(List<Resource> resources, HashMap<PatientSummary.IPSSection, List<Resource>> sectionPrimaries, Composition composition) {
List<Resource> linkedResources = new ArrayList<Resource>();
HashMap<String, String> valueUrls = new HashMap<String, String>();
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<PatientSummary.IPSSection, String> createNarratives(HashMap<PatientSummary.IPSSection, List<Resource>> sectionPrimaries, List<Resource> resources, FhirContext ctx) {
HashMap<PatientSummary.IPSSection, String> hashedNarratives = new HashMap<PatientSummary.IPSSection, String>();
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<IBaseResource> myResources = new ArrayList<>();
private final Map<String, IBaseResource> myIdToResource = new HashMap<>();
private final Map<String, String> myOriginalIdToNewId = new HashMap<>();
public List<IBaseResource> 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();
}
}
}

View File

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

View File

@ -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<String> 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<Include> 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;
}
}

View File

@ -0,0 +1,37 @@
<!--/* AdvanceDirectives -->
<!--
Scope: Consent.scope.text || Consent.scope.coding[x].display
Status: Consent.status.code
Action Controlled: Consent.provision.action[x].coding[x].display (concatenate items separated by comma, e.g. x, y, z)
Date: Consent.dateTime
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<table class="hapiPropertyTable">
<caption>Advance Directives</caption>
<thead>
<tr>
<th>Scope</th>
<th>Status</th>
<th>Action Controlled</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<th:block th:each="entry : ${resource.entry}" th:object="${entry.getResource()}">
<th:block th:unless='*{getResourceType().name() == "Composition"}'>
<th:block th:with="extension=${entry.getResource().getExtensionByUrl('http://hl7.org/fhir/StructureDefinition/NarrativeLink').getValue().getValue()}">
<tr th:id="${#strings.arraySplit(extension, '#')[1]}">
<td th:insert="IpsUtilityFragments :: codeableConcept (cc=*{getScope()},attr='display')">Scope</td>
<td th:text="*{getStatus().getCode()}">Status</td>
<td th:text="*{getdateTimeType().getValue()}">Action Controlled</td>
<td th:insert="IpsUtilityFragments :: concatCodeableConcept (list=*{getProvision().getAction()})">Action Controlled</td>
<td th:text="*{getDateTimeElement().getValue()}">Date</td>
</tr>
</th:block>
</th:block>
</th:block>
</tbody>
</table>
</div>

View File

@ -0,0 +1,43 @@
<!--/* AllergiesAndIntolerances -->
<!--
Allergen: AllergyIntolerance.code.text || AllergyIntolerance.code.coding[x].display
Status: AllergyIntolerance.clinicalStatus.coding[x].display
Category: AllergyIntolerance.code[x]
Reaction: AllergyIntolerance.reaction.manifestation.text || AllergyIntolerance.reaction.manifestation.coding[x].display *** What about getReaction().getDescription() ***
Severity: AllergyIntolerance.reaction.severity.code
Comments: AllergyIntolerance.note[x].text (display all notes separated by <br /> )
Onset: AllergyIntolerance.onsetDateTime
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<table class="hapiPropertyTable">
<caption>Allergies And Intolerances</caption>
<thead>
<tr>
<th>Allergen</th>
<th>Status</th>
<th>Category</th>
<th>Reaction</th>
<th>Severity</th>
<th>Comments</th>
<th>Onset</th>
</tr>
</thead>
<tbody>
<th:block th:each="entry : ${resource.entry}" th:object="${entry.getResource()}">
<th:block th:with="extension=${entry.getResource().getExtensionByUrl('http://hl7.org/fhir/StructureDefinition/NarrativeLink').getValue().getValue()}">
<tr th:id="${#strings.arraySplit(extension, '#')[1]}">
<td th:insert="IpsUtilityFragments :: codeableConcept (cc=*{getCode()},attr='display')">Allergen</td>
<td th:insert="IpsUtilityFragments :: codeableConcept (cc=*{getClinicalStatus()},attr='code')">Status</td>
<td th:insert="IpsUtilityFragments :: concat (list=*{getCategory()},attr='value')">Category</td>
<td th:insert="IpsUtilityFragments :: concatReactionManifestation (list=*{getReaction()})">Reaction</td>
<td th:insert="IpsUtilityFragments :: concat (list=*{getReaction()},attr='severity')">Severity</td>
<td th:insert="IpsUtilityFragments :: concat (list=*{getNote()},attr='text')">Comments</td>
<td th:text="*{getOnsetDateTimeType().getValue()}">Onset</td>
</tr>
</th:block>
</th:block>
</tbody>
</table>
</div>

View File

@ -0,0 +1,11 @@
<!--/*
This template generates a composite narrative for the Composition, incorporating
all of the section narratives into a single narrative.
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<div th:each="section : ${resource.section}">
<h1 th:text="${section.title}"></h1>
<div th:utext="${section.text.getDivAsString()}">
</div>
</div>
</div>

View File

@ -0,0 +1,74 @@
<!--/* DiagnosticResults -->
<!--
TABLE 1: Observation
Code: Observation.code.text || Observation.code.coding[x].display
Result: Observation.valueQuantity.value || Observation.valueCodeableConcept.coding[x].display || Observation.valueString
Unit: Observation.valueQuantity.unit
Interpretation: Observation.interpretation.text || Observation. interpretation.coding[x].display
Reference Range: Observation.referenceRange.low.value && “-“ && Observation.referenceRange.high.value
Comments: Observation.note[x].text (display all notes separated by <br /> )
Date: Observation.effectiveDateTime
TABLE 2: DiagnosticReport
Code: DiagnosticReport.code.text || DiagnosticReport.code.coding[x].display
Date: DiagnosticReport.effectiveDateTime
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<table class="hapiPropertyTable">
<caption>Diagnostic Results: Observations</caption>
<thead>
<tr>
<th>Code</th>
<th>Result</th>
<th>Unit</th>
<th>Interpretation</th>
<th>Reference Range</th>
<th>Comments</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<th:block th:each="entry : ${resource.entry}" th:object="${entry.getResource()}">
<th:block th:if='*{getResourceType().name() == "Observation"}'>
<th:block th:unless='*{getResourceType().name() == "Composition"}'>
<th:block th:with="extension=${entry.getResource().getExtensionByUrl('http://hl7.org/fhir/StructureDefinition/NarrativeLink').getValue().getValue()}">
<tr th:id="${#strings.arraySplit(extension, '#')[1]}">
<td th:insert="IpsUtilityFragments :: codeableConcept (cc=*{getCode()},attr='display')">Code</td>
<td th:insert="IpsUtilityFragments :: renderValue (value=*{getValue()})">Result</td>
<td th:insert="IpsUtilityFragments :: renderValueUnit (value=*{getValue()})">Unit</td>
<td th:insert="IpsUtilityFragments :: firstFromCodeableConceptList (list=*{getInterpretation()})">Interpretation</td>
<td th:insert="IpsUtilityFragments :: concatReferenceRange (list=*{getReferenceRange()})">Reference Range</td>
<td th:insert="IpsUtilityFragments :: concat (list=*{getNote()},attr='text')">Comments</td>
<td th:insert="IpsUtilityFragments :: renderEffective (effective=*{getEffective()})">Date</td>
</tr>
</th:block>
</th:block>
</th:block>
</th:block>
</tbody>
</table>
<table class="hapiPropertyTable">
<caption>Diagnostic Results: Diagnostic Reports</caption>
<thead>
<tr>
<th>Code</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<th:block th:each="entry : ${resource.entry}" th:object="${entry.getResource()}">
<th:block th:if='*{getResourceType().name() == "DiagnosticReport"}'>
<th:block th:unless='*{getResourceType().name() == "Composition"}'>
<th:block th:with="extension=${entry.getResource().getExtensionByUrl('http://hl7.org/fhir/StructureDefinition/NarrativeLink').getValue().getValue()}">
<tr th:id="${#strings.arraySplit(extension, '#')[1]}">
<td th:insert="IpsUtilityFragments :: codeableConcept (cc=*{getCode()},attr='display')">Device</td>
<td th:insert="IpsUtilityFragments :: renderEffective (effective=*{getEffective()})">Date</td>
</tr>
</th:block>
</th:block>
</th:block>
</th:block>
</tbody>
</table>
</div>

View File

@ -0,0 +1,37 @@
<!--/* FunctionalStatus -->
<!--
Assessment: ClinicalImpression.code.text || ClinicalImpression.code[x].display
Status: ClinicalImpression.status.code
Finding: ClinicalImpression.summary
Comments: ClinicalImpression.note[x].text (display all notes separated by <br /> )
Date: ClinicalImpression.effectiveDateTime
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<table class="hapiPropertyTable">
<caption>Functional Status</caption>
<thead>
<tr>
<th>Assessment</th>
<th>Status</th>
<th>Finding</th>
<th>Comments</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<th:block th:each="entry : ${resource.entry}" th:object="${entry.getResource()}">
<th:block th:unless='*{getResourceType().name() == "Composition"}'>
<th:block th:with="extension=${entry.getResource().getExtensionByUrl('http://hl7.org/fhir/StructureDefinition/NarrativeLink').getValue().getValue()}">
<tr th:id="${#strings.arraySplit(extension, '#')[1]}">
<td th:insert="IpsUtilityFragments :: codeableConcept (cc=*{getCode()},attr='display')">Assessment</td>
<td th:text="*{getStatus().getCode()}">Status</td>
<td th:text="*{getSummary()}">Finding</td>
<td th:insert="IpsUtilityFragments :: concat (list=*{getNote()},attr='text')">Comments</td>
<td th:insert="IpsUtilityFragments :: renderEffective (effective=*{getEffective()})">Date</td>
</tr>
</th:block>
</th:block>
</th:block>
</tbody>
</table>
</div>

View File

@ -0,0 +1,31 @@
<!--/* HistoryOfProcedures -->
<!--
Procedure: Procedure.code.text || Procedure.code.coding[x].display
Comments: Procedure.note[x].text(display all notes separated by <br /> )
Date: Procedure.performedDateTime
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<table class="hapiPropertyTable">
<caption>History Of Procedures</caption>
<thead>
<tr>
<th>Procedure</th>
<th>Comments</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<th:block th:each="entry : ${resource.entry}" th:object="${entry.getResource()}">
<th:block th:unless='*{getResourceType().name() == "Composition"}'>
<th:block th:with="extension=${entry.getResource().getExtensionByUrl('http://hl7.org/fhir/StructureDefinition/NarrativeLink').getValue().getValue()}">
<tr th:id="${#strings.arraySplit(extension, '#')[1]}">
<td th:insert="IpsUtilityFragments :: codeableConcept (cc=*{getCode()},attr='display')">Procedure</td>
<td th:insert="IpsUtilityFragments :: concat (list=*{getNote()},attr='text')">Comments</td>
<td th:insert=":: renderPerformed (performed=*{getPerformed()})">Date</td>
</tr>
</th:block>
</th:block>
</th:block>
</tbody>
</table>
</div>

View File

@ -0,0 +1,43 @@
<!--/* Immunizations -->
<!--
Immunization: Immunization.vaccineCode.text || Immunization.vaccineCode.coding[x].display
Status: Immunization.status.code
Dose Number: Immunization.doseNumberPositiveInt || Immunization.doseNumberString
Manufacturer: Organization.name
Lot Number: Immunization.lotNumber
Comments: Immunization.note[x].text (display all notes separated by <br /> )
Date: Immunization.occurrenceDateTime
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<table class="hapiPropertyTable">
<caption>Immunizations</caption>
<thead>
<tr>
<th>Immunization</th>
<th>Status</th>
<th>Dose Number</th>
<th>Manufacturer</th>
<th>Lot Number</th>
<th>Comments</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<th:block th:each="entry : ${resource.entry}" th:object="${entry.getResource()}">
<th:block th:if='*{getResourceType().name() == "Immunization"}'>
<th:block th:with="extension=${entry.getResource().getExtensionByUrl('http://hl7.org/fhir/StructureDefinition/NarrativeLink').getValue().getValue()}">
<tr th:id="${#strings.arraySplit(extension, '#')[1]}">
<td th:insert="IpsUtilityFragments :: codeableConcept (cc=*{getVaccineCode()},attr='display')">Immunization</td>
<td th:text="*{getStatusElement().value}">Status</td>
<td th:insert="IpsUtilityFragments :: concatDoseNumber (list=*{getProtocolApplied()})">Comments</td>
<td th:insert="IpsUtilityFragments :: renderOrganization (orgRef=*{getManufacturer()})">Manufacturer</td>
<td th:text="*{getLotNumber()}">Lot Number</td>
<td th:insert="IpsUtilityFragments :: concat (list=*{getNote()},attr='text')">Comments</td>
<td th:insert="IpsUtilityFragments :: renderOccurrence (occurrence=*{getOccurrence()})">Date</td>
</tr>
</th:block>
</th:block>
</th:block>
</tbody>
</table>
</div>

View File

@ -0,0 +1,73 @@
################################################
# IPS Sections
################################################
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
ips-medicationsummary.resourceType=Bundle
ips-medicationsummary.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/MedicationSummary-uv-ips
ips-medicationsummary.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/medicationsummary.html
ips-problemlist.resourceType=Bundle
ips-problemlist.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/ProblemList-uv-ips
ips-problemlist.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/problemlist.html
ips-immunizations.resourceType=Bundle
ips-immunizations.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/Immunizations-uv-ips
ips-immunizations.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/immunizations.html
ips-historyofprocedures.resourceType=Bundle
ips-historyofprocedures.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/HistoryOfProcedures-uv-ips
ips-historyofprocedures.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/historyofprocedures.html
ips-medicaldevices.resourceType=Bundle
ips-medicaldevices.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/MedicalDevices-uv-ips
ips-medicaldevices.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/medicaldevices.html
ips-diagnosticresults.resourceType=Bundle
ips-diagnosticresults.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/DiagnosticResults-uv-ips
ips-diagnosticresults.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/diagnosticresults.html
ips-vitalsigns.resourceType=Bundle
ips-vitalsigns.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/VitalSigns-uv-ips
ips-vitalsigns.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/vitalsigns.html
ips-pregnancy.resourceType=Bundle
ips-pregnancy.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/Pregnancy-uv-ips
ips-pregnancy.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/pregnancy.html
ips-socialhistory.resourceType=Bundle
ips-socialhistory.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/SocialHistory-uv-ips
ips-socialhistory.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/socialhistory.html
ips-pasthistoryofillness.resourceType=Bundle
ips-pasthistoryofillness.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/PastHistoryOfIllness-uv-ips
ips-pasthistoryofillness.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/pasthistoryofillness.html
ips-functionalstatus.resourceType=Bundle
ips-functionalstatus.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/FunctionalStatus-uv-ips
ips-functionalstatus.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/functionalstatus.html
ips-planofcare.resourceType=Bundle
ips-planofcare.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/PlanOfCare-uv-ips
ips-planofcare.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/planofcare.html
ips-advancedirectives.resourceType=Bundle
ips-advancedirectives.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/AdvanceDirectives-uv-ips
ips-advancedirectives.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/advancedirectives.html
################################################
# Utility Fragments
################################################
ips-utility-fragments.fragmentName=IpsUtilityFragments
ips-utility-fragments.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/utility-fragments.html
################################################
# IPS Global Composition Narrative (applied at the end)
################################################
ips-global.resourceType=Composition
ips-global.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/composition.html

View File

@ -0,0 +1,34 @@
<!--/* MedicalDevices -->
<!--
Device: Device.type.coding.text || Device.type.coding[x].display
Status: DeviceUseStatement.status.code
Comments: DeviceUseStatement.note[x].text (display all notes separated by <br /> )
Date Recorded: DeviceUseStatement.recordedOn
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<table class="hapiPropertyTable">
<caption>Medical Devices</caption>
<thead>
<tr>
<th>Device</th>
<th>Status</th>
<th>Comments</th>
<th>Date Recorded</th>
</tr>
</thead>
<tbody>
<th:block th:each="entry : ${resource.entry}" th:object="${entry.getResource()}">
<th:block th:if='*{getResourceType().name() == "DeviceUseStatement"}'>
<th:block th:with="extension=${entry.getResource().getExtensionByUrl('http://hl7.org/fhir/StructureDefinition/NarrativeLink').getValue().getValue()}">
<tr th:id="${#strings.arraySplit(extension, '#')[1]}">
<td th:insert="IpsUtilityFragments :: renderDevice (deviceRef=*{getDevice()})">Device</td>
<td th:text="*{getStatusElement().value}">Status</td>
<td th:insert="IpsUtilityFragments :: concat (list=*{getNote()},attr='text')">Comments</td>
<td th:insert="IpsUtilityFragments :: renderRecorded (recorded=*{getRecordedOn()})">Date Recorded</td>
</tr>
</th:block>
</th:block>
</th:block>
</tbody>
</table>
</div>

View File

@ -0,0 +1,78 @@
<!--/* MedicationSummary -->
<!--
Table 1 MedicationRequest
Medication: MedicationRequest.medicationCodeableConcept.coding[x].display || Medication.code.coding.text || Medication.code.coding.code[x].display
Status: MedicationRequest.status.display
Route: MedicationRequest.dosageInstruction[x].route.coding[x].display
Sig: MedicationRequest.dosageInstruction[x].text (display all sigs separated by <br /> )
Comments: MedicationRequest.note[x].text (display all notes separated by <br /> )
Authored Date: MedicationRequest.DateTime
Table 2 MedicationStatement
Medication: MedicationStatement.medicationCodeableConcept.coding[x].display || Medication.code.coding.text || Medication.code.coding.code[x].display
Status: MedicationStatement.status.display
Route: MedicationStatement.dosage[x].route.coding[x].display
Sig: MedicationStatement.dosage[x].text (display all sigs separated by <br /> )
Date: MedicationStatement.effectiveDateTime
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<table class="hapiPropertyTable">
<caption>Medication Summary: Medication Requests</caption>
<thead>
<tr>
<th>Medication</th>
<th>Status</th>
<th>Route</th>
<th>Sig</th>
<th>Comments</th>
<th>Authored Date</th>
</tr>
</thead>
<tbody>
<th:block th:each="entry : ${resource.entry}" th:object="${entry.getResource()}">
<th:block th:if='*{getResourceType().name() == "MedicationRequest"}'>
<th:block th:unless='*{getResourceType().name() == "Composition"}'>
<th:block th:with="extension=${entry.getResource().getExtensionByUrl('http://hl7.org/fhir/StructureDefinition/NarrativeLink').getValue().getValue()}">
<tr th:id="${#strings.arraySplit(extension, '#')[1]}">
<td th:insert="IpsUtilityFragments :: renderMedication (medicationType=*{getMedication()})">Medication</td>
<td th:text="*{getStatus().getDisplay()}">Status</td>
<td th:insert="IpsUtilityFragments :: concatDosageRoute (list=*{getDosageInstruction()})">Route</td>
<td th:insert="IpsUtilityFragments :: concat (list=*{getDosageInstruction()},attr='text')">Sig</td>
<td th:insert="IpsUtilityFragments :: concat (list=*{getNote()},attr='text')">Comments</td>
<td th:text="*{getAuthoredOnElement().getValue()}">Authored Date</td>
</tr>
</th:block>
</th:block>
</th:block>
</th:block>
</tbody>
</table>
<table class="hapiPropertyTable">
<caption>Medication Summary: Medication Statements</caption>
<thead>
<tr>
<th>Medication</th>
<th>Status</th>
<th>Route</th>
<th>Sig</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<th:block th:each="entry : ${resource.entry}" th:object="${entry.getResource()}">
<th:block th:if='*{getResourceType().name() == "MedicationStatement"}'>
<th:block th:with="extension=${entry.getResource().getExtensionByUrl('http://hl7.org/fhir/StructureDefinition/NarrativeLink').getValue().getValue()}">
<tr th:id="${#strings.arraySplit(extension, '#')[1]}">
<td th:insert="IpsUtilityFragments :: renderMedication (medicationType=*{getMedication()})">Medication</td>
<td th:text="*{getStatus().getDisplay()}">Status</td>
<td th:insert="IpsUtilityFragments :: concatDosageRoute (list=*{getDosage()})">Route</td>
<td th:insert="IpsUtilityFragments :: concat (list=*{getDosage()},attr='text')">Sig</td>
<td th:insert="IpsUtilityFragments :: renderEffective (effective=*{getEffective()})">Date</td>
</tr>
</th:block>
</th:block>
</th:block>
</tbody>
</table>
</div>

View File

@ -0,0 +1,34 @@
<!--/* PastHistoryOfIllnesses -->
<!--
Medical Problem: Condition.code.text || Condition.code.coding[x].display
Status: Condition.clinicalStatus.coding[x].display
Comments: Condition.note[x].text (display all notes separated by <br /> )
Onset Date: Condition.onsetDateTime
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<table class="hapiPropertyTable">
<caption>Past History of Illnesses</caption>
<thead>
<tr>
<th>Medical Problems</th>
<th>Status</th>
<th>Comments</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<th:block th:each="entry : ${resource.entry}" th:object="${entry.getResource()}">
<th:block th:unless='*{getResourceType().name() == "Composition"}'>
<th:block th:with="extension=${entry.getResource().getExtensionByUrl('http://hl7.org/fhir/StructureDefinition/NarrativeLink').getValue().getValue()}">
<tr th:id="${#strings.arraySplit(extension, '#')[1]}">
<td th:insert="IpsUtilityFragments :: codeableConcept (cc=*{getCode()},attr='display')">Medical Problem</td>
<td th:insert="IpsUtilityFragments :: codeableConcept (cc=*{getClinicalStatus()},attr='code')">Status</td>
<td th:insert="IpsUtilityFragments :: concat (list=*{getNote()},attr='text')">Comments</td>
<td th:insert="IpsUtilityFragments :: renderOnset (onset=*{getOnset()})">Onset Date</td>
</tr>
</th:block>
</th:block>
</th:block>
</tbody>
</table>
</div>

View File

@ -0,0 +1,37 @@
<!--/* PlanOfCare -->
<!--
Activity: CarePlan.description
Intent: CarePlan.intent.code
Comments: CarePlan.dosage [x].text // Not dosaage but note... right?
Planned Start: CarePlan.period.start
Planned End: CarePlan.period.end
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<table class="hapiPropertyTable">
<caption>Plan of Care</caption>
<thead>
<tr>
<th>Activity</th>
<th>Intent</th>
<th>Comments</th>
<th>Planned Start</th>
<th>Planned End</th>
</tr>
</thead>
<tbody>
<th:block th:each="entry : ${resource.entry}" th:object="${entry.getResource()}">
<th:block th:unless='*{getResourceType().name() == "Composition"}'>
<th:block th:with="extension=${entry.getResource().getExtensionByUrl('http://hl7.org/fhir/StructureDefinition/NarrativeLink').getValue().getValue()}">
<tr th:id="${#strings.arraySplit(extension, '#')[1]}">
<td th:text="*{getDescription()}">Activity</td>
<td th:text="*{getIntent().toCode()}">Intent</td>
<td th:insert="IpsUtilityFragments :: concat (list=*{getNote()},attr='text')">Comments</td>
<td th:text="*{getPeriod().getStartElement().getValue()}">Planned Start</td>
<td th:text="*{getPeriod().getEndElement().getValue()}">Planned End</td>
</tr>
</th:block>
</th:block>
</th:block>
</tbody>
</table>
</div>

View File

@ -0,0 +1,34 @@
<!--/* Pregnancy -->
<!--
Code: Observation.code.text || Observation.code.coding[x].display
Result: Observation.valueQuantity.value || Observation.valueDateTime || Observation.valueCodeableConcept.coding[x].display || Observation.valueString
Comments: Observation.note[x].text (display all notes separated by <br /> )
Date: Observation.effectiveDateTime
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<table class="hapiPropertyTable">
<caption>Pregnancy</caption>
<thead>
<tr>
<th>Code</th>
<th>Result</th>
<th>Comments</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<th:block th:each="entry : ${resource.entry}" th:object="${entry.getResource()}">
<th:block th:unless='*{getResourceType().name() == "Composition"}'>
<th:block th:with="extension=${entry.getResource().getExtensionByUrl('http://hl7.org/fhir/StructureDefinition/NarrativeLink').getValue().getValue()}">
<tr th:id="${#strings.arraySplit(extension, '#')[1]}">
<td th:insert="IpsUtilityFragments :: codeableConcept (cc=*{getCode()},attr='display')">Code</td>
<td th:insert="IpsUtilityFragments :: renderValue (value=*{getValue()})">Result</td>
<td th:insert="IpsUtilityFragments :: concat (list=*{getNote()},attr='text')">Comments</td>
<td th:insert="IpsUtilityFragments :: renderEffective (effective=*{getEffective()})">Date</td>
</tr>
</th:block>
</th:block>
</th:block>
</tbody>
</table>
</div>

View File

@ -0,0 +1,34 @@
<!--/* ProblemList -->
<!--
Medical Problem: Condition.code.text || Condition.code.coding[x].display
Status: Condition.clinicalStatus.coding[x].display
Comments: Condition.note[x].text (display all notes separated by <br /> )
Onset Date: Condition.onsetDateTime
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<table class="hapiPropertyTable">
<caption>Problem List</caption>
<thead>
<tr>
<th>Medical Problems</th>
<th>Status</th>
<th>Comments</th>
<th>Onset Date</th>
</tr>
</thead>
<tbody>
<th:block th:each="entry : ${resource.entry}" th:object="${entry.getResource()}">
<th:block th:unless='*{getResourceType().name() == "Composition"}'>
<th:block th:with="extension=${entry.getResource().getExtensionByUrl('http://hl7.org/fhir/StructureDefinition/NarrativeLink').getValue().getValue()}">
<tr th:id="${#strings.arraySplit(extension, '#')[1]}">
<td th:insert="IpsUtilityFragments :: codeableConcept (cc=*{getCode()},attr='display')">Medical Problems</td>
<td th:insert="IpsUtilityFragments :: codeableConcept (cc=*{getClinicalStatus()},attr='code')">Status</td>
<td th:insert="IpsUtilityFragments :: concat (list=*{getNote()},attr='text')">Comments</td>
<td th:insert="IpsUtilityFragments :: renderOnset (onset=*{getOnset()})">Onset Date</td>
</tr>
</th:block>
</th:block>
</th:block>
</tbody>
</table>
</div>

View File

@ -0,0 +1,37 @@
<!--/* SocialHistory -->
<!--
Code: Observation.code.text || Observation.code.coding[x].display
Result: Observation.valueQuantity.value || Observation.valueCodeableConcept.coding[x].display || Observation.valueString
Unit: Observation.valueQuantity.unit
Comments: Observation.note[x].text (display all notes separated by <br /> )
Date: Observation.effectiveDateTime
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<table class="hapiPropertyTable">
<caption>Social History</caption>
<thead>
<tr>
<th>Code</th>
<th>Result</th>
<th>Unit</th>
<th>Comments</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<th:block th:each="entry : ${resource.entry}" th:object="${entry.getResource()}">
<th:block th:unless='*{getResourceType().name() == "Composition"}'>
<th:block th:with="extension=${entry.getResource().getExtensionByUrl('http://hl7.org/fhir/StructureDefinition/NarrativeLink').getValue().getValue()}">
<tr th:id="${#strings.arraySplit(extension, '#')[1]}">
<td th:insert="IpsUtilityFragments :: codeableConcept (cc=*{getCode()},attr='display')">Code</td>
<td th:insert="IpsUtilityFragments :: renderValue (value=*{getValue()})">Result</td>
<td th:insert="IpsUtilityFragments :: renderValueUnit (value=*{getValue()})">Unit</td>
<td th:insert="IpsUtilityFragments :: concat (list=*{getNote()},attr='text')">Comments</td>
<td th:insert="IpsUtilityFragments :: renderEffective (effective=*{getEffective()})">Date</td>
</tr>
</th:block>
</th:block>
</th:block>
</tbody>
</table>
</div>

View File

@ -0,0 +1,243 @@
<!--/* Utility Fragments */-->
<!--/* Referenced Models */-->
<th:block th:if="${deviceRef}" th:fragment="renderDevice (deviceRef)">
<th:block th:with="device = ${#fhirpath.evaluateFirst(deviceRef, 'resolve()')}">
<th:block th:if="${device != null}">
<th:block th:if='${device.getResourceType().name() == "Device"}'>
<th:block th:replace=":: codeableConcept (cc=${device.getType()},attr='display')">Device</th:block>
</th:block>
</th:block>
</th:block>
</th:block>
<th:block th:if="${orgRef}" th:fragment="renderOrganization (orgRef)">
<th:block th:with="organization = ${#fhirpath.evaluateFirst(orgRef, 'resolve()')}">
<th:block th:if="${organization != null}">
<th:block th:if='${organization.getResourceType().name() == "Organization"}'>
<th:block th:text="${organization.getName()}">Org Name</th:block>
</th:block>
</th:block>
</th:block>
</th:block>
<th:block th:fragment="renderMedication (medicationType)">
<th:block th:object="${medicationType}">
<th:block th:switch="*{getClass().getSimpleName()}">
<th:block th:case="'CodeableConcept'">
<th:block th:replace=":: codeableConcept (cc=${medicationType}, attr='display')">Medication</th:block>
</th:block>
<th:block th:case="'Reference'">
<th:block th:replace=":: renderMedicationRef (medicationRef=${medicationType})">Medication</th:block>
</th:block>
</th:block>
</th:block>
</th:block>
<th:block th:if="${medicationRef}" th:fragment="renderMedicationRef (medicationRef)">
<th:block th:with="medication = ${#fhirpath.evaluateFirst(medicationRef, 'resolve()')}">
<th:block th:if="${medication != null}">
<th:block th:replace=":: renderMedicationCode (medication=${medication})">Medication</th:block>
</th:block>
</th:block>
</th:block>
<th:block th:if="${medication}" th:fragment="renderMedicationCode (medication)">
<th:block th:replace=":: codeableConcept (cc=${medication.getCode()},attr='display')">Medication</th:block>
</th:block>
<!--/* Dose Number */-->
<th:block th:if="${doseNumber}" th:fragment="renderDoseNumber (doseNumber)">
<th:block th:object="${doseNumber}">
<th:block th:switch="*{getClass().getSimpleName()}">
<th:block th:case="'PositiveIntType'" th:text="*{getValue()}">Dose Number</th:block>
<th:block th:case="'StringType'" th:text="*{getValue()}">Dose Number</th:block>
</th:block>
</th:block>
</th:block>
<!--/* Value */-->
<th:block th:if="${value}" th:fragment="renderValue (value)">
<th:block th:object="${value}">
<th:block th:switch="*{getClass().getSimpleName()}">
<th:block th:case="'Quantity'" th:text="*{getValue()}">Result</th:block>
<th:block th:case="'DateTimeType'" th:text="*{getValue()}">Result</th:block>
<th:block th:case="'CodeableConcept'">
<th:block th:replace=":: codeableConcept (cc=${value}, attr='display')">Result</th:block>
</th:block>
<th:block th:case="'StringType'" th:text="*{getValue()}">Result</th:block>
</th:block>
</th:block>
</th:block>
<th:block th:if="${value}" th:fragment="renderValueUnit (value)">
<th:block th:object="${value}">
<th:block th:switch="*{getClass().getSimpleName()}">
<th:block th:case="'Quantity'" th:text="*{getUnit()}">Unit</th:block>
</th:block>
</th:block>
</th:block>
<!--/* Dates */-->
<th:block th:if="${effective}" th:fragment="renderEffective (effective)">
<th:block th:object="${effective}">
<th:block th:switch="*{getClass().getSimpleName()}">
<th:block th:case="'DateTimeType'" th:text="*{getValue()}">Date</th:block>
<th:block th:case="'Period'" th:text="*{getStartElement().getValue()}">Date</th:block>
</th:block>
</th:block>
</th:block>
<th:block th:if="${onset}" th:fragment="renderOnset (onset)">
<th:block th:object="${onset}">
<th:block th:switch="*{getClass().getSimpleName()}">
<th:block th:case="'DateTimeType'" th:text="*{getValue()}">Date</th:block>
<th:block th:case="'Period'"
th:text="*{#strings.concatReplaceNulls('', getStartElement().getValue(), '-', getEndElement().getValue() )}">
Date
</th:block>
<th:block th:case="'Age'" th:text="*{getValue()}">Date</th:block>
<th:block th:case="'Range'"
th:text="*{#strings.concatReplaceNulls('', getLow().getValue(), '-', getHigh().getValue() )}">Date
</th:block>
<th:block th:case="'StringType'" th:text="*{getValue()}">Date</th:block>
</th:block>
</th:block>
</th:block>
<th:block th:if="${performed}" th:fragment="renderPerformed (performed)">
<th:block th:object="${performed}">
<th:block th:switch="*{getClass().getSimpleName()}">
<th:block th:case="'DateTimeType'" th:text="*{getValue()}">Date</th:block>
<th:block th:case="'Period'"
th:text="*{#strings.concatReplaceNulls('', getStartElement().getValue(), '-', getEndElement().getValue() )}">
Date
</th:block>
<th:block th:case="'Age'" th:text="*{getValue()}">Date</th:block>
<th:block th:case="'Range'"
th:text="*{#strings.concatReplaceNulls('', getLow().getValue(), '-', getHigh().getValue() )}">Date
</th:block>
<th:block th:case="'StringType'" th:text="*{getValue()}">Date</th:block>
</th:block>
</th:block>
</th:block>
<th:block th:if="${occurrence}" th:fragment="renderOccurrence (occurrence)">
<th:block th:object="${occurrence}">
<th:block th:switch="*{getClass().getSimpleName()}">
<th:block th:case="'DateTimeType'" th:text="*{getValue()}">Date</th:block>
<th:block th:case="'StringType'" th:text="*{getValue()}">Date</th:block>
</th:block>
</th:block>
</th:block>
<th:block th:if="${recorded}" th:fragment="renderRecorded (recorded)">
<th:block th:object="${recorded}">
<th:block th:switch="*{getClass().getSimpleName()}">
<th:block th:case="'DateTimeType'" th:text="*{getValue()}">Date Recorded</th:block>
</th:block>
</th:block>
</th:block>
<!--/* CodeableConcept */-->
<th:block th:if="${cc}" th:fragment="codeableConcept (cc, attr)">
<th:block th:if="${!cc.getTextElement().empty}" th:text="${cc.getText()}"/>
<th:block th:if="${cc.getTextElement().empty}" th:switch="${attr} ?: 'display'">
<th:block th:case="'display'">
<th:block th:replace=":: concat (list=${cc.getCoding()},attr='display')"/>
</th:block>
<th:block th:case="'code'">
<th:block th:replace=":: concat (list=${cc.getCoding()},attr='code')"/>
</th:block>
</th:block>
</th:block>
<th:block th:if="${list}" th:fragment="firstFromCodeableConceptList (list)">
<th:block th:if="${!list.empty}" with="${attr} ?: 'display'">
<th:block th:replace=":: codeableConcept (cc=${list.get(0)},attr=${attr})">Interpretation</th:block>
</th:block>
</th:block>
<!--/* Lists */-->
<th:block th:if="${list}" th:fragment="concat (list, attr)">
<th:block th:each="item,iter : ${list}" th:if="${!list.empty}">
<th:block th:switch="${attr} ?: 'value'">
<th:block th:case="'display'">
<th:block th:replace=":: concatItem (listItem=${item.getDisplayElement()}, iter=${iter}, separator='')"/>
</th:block>
<th:block th:case="'code'">
<th:block th:replace=":: concatItem (listItem=${item.getCodeElement()}, iter=${iter}, separator='')"/>
</th:block>
<th:block th:case="'text'">
<th:block th:replace=":: concatItem (listItem=${item.getTextElement()}, iter=${iter}, separator='')"/>
</th:block>
<th:block th:case="'value'">
<th:block th:replace=":: concatItem (listItem=${item}, iter=${iter}, separator='')"/>
</th:block>
<th:block th:case="'severity'">
<th:block th:replace=":: concatItem (listItem=${item.getSeverity().toCode()}, iter=${iter}, separator='')"/>
</th:block>
</th:block>
</th:block>
</th:block>
<th:block th:if="${listItem}" th:fragment="concatItem (listItem, iter, separator)">
<th:block th:if="${!listItem.empty}">
<th:block th:text="${listItem.getClass().getSimpleName() == 'String'} ? ${listItem} : ${listItem.getValue()}">
</th:block>
<th:block th:if="${!iter.last}">
<th:block th:if="${!separator.empty}" th:text="${separator + ' '}"/>
<br th:if="${separator.empty}"/>
</th:block>
</th:block>
</th:block>
<!--/* Nested Lists */-->
<th:block th:if="${list}" th:fragment="concatReactionManifestation (list)">
<th:block th:each="item,iter : ${list}" th:if="${!list.empty}" with="attr=${attr} ?: 'display'">
<th:block th:if="${item.hasDescription()}" th:text="${item.getDescription()}">Reaction</th:block>
<th:block th:if="${!item.hasDescription()}">
<th:block th:replace=":: concatCodeableConcept (list=${item.getManifestation()})">Reaction</th:block>
</th:block>
<th:block th:if="${!iter.last}" th:text=", "/>
</th:block>
</th:block>
<th:block th:if="${list}" th:fragment="concatCodeableConcept (list)">
<th:block th:each="item,iter : ${list}" th:if="${!list.empty}" with="attr=${attr} ?: 'display'">
<th:block th:replace=":: codeableConcept (cc=${item},attr=${attr})"/>
<th:block th:if="${!iter.last}" th:text=", "/>
</th:block>
</th:block>
<th:block th:if="${list}" th:fragment="concatDosageRoute (list)">
<th:block th:each="item,iter : ${list}" th:if="${!list.empty}" with="attr=${attr} ?: 'display'">
<th:block th:replace=":: codeableConcept (cc=${item.getRoute()},attr=${attr})"/>
<th:block th:if="${!iter.last}" th:text=", "/>
</th:block>
</th:block>
<th:block th:if="${list}" th:fragment="concatDoseNumber (list)">
<th:block th:each="item,iter : ${list}" th:if="${!list.empty}">
<th:block th:replace=":: renderDoseNumber (doseNumber=${item.doseNumber})">Dose Number</th:block>
<th:block th:if="${!iter.last}" th:text="', '"/>
</th:block>
</th:block>
<th:block th:if="${list}" th:fragment="concatReferenceRange (list)">
<th:block th:each="item,iter : ${list}" th:if="${!list.empty}">
<th:block th:if="${item.hasText()}" th:text="${item.getText()}">Reference Range</th:block>
<th:block th:if="${!item.hasText()}"
th:text="${#strings.concatReplaceNulls('', item.getLow().getValue(), '-', item.getHigh().getValue() )}">
Reference Range
</th:block>
<th:block th:if="${!iter.last}" th:text=", "/>
</th:block>
</th:block>

View File

@ -0,0 +1,40 @@
<!--/* VitalSigns -->
<!--
Code: Observation.code.text || Observation.code.coding[x].display
Result: Observation.valueQuantity.value || Observation.valueCodeableConcept.coding[x].display || Observation.valueString
Unit: Observation.valueQuantity.unit
Interpretation: Observation.interpretation.text || Observation. interpretation.coding[x].display
Comments: Observation.note[x].text (display all notes separated by <br /> )
Date: Observation.effectiveDateTime
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<table class="hapiPropertyTable">
<caption>Vital Signs</caption>
<thead>
<tr>
<th>Code</th>
<th>Result</th>
<th>Unit</th>
<th>Interpretation</th>
<th>Comments</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<th:block th:each="entry : ${resource.entry}" th:object="${entry.getResource()}">
<th:block th:unless='*{getResourceType().name() == "Composition"}'>
<th:block th:with="extension=${entry.getResource().getExtensionByUrl('http://hl7.org/fhir/StructureDefinition/NarrativeLink').getValue().getValue()}">
<tr th:id="${#strings.arraySplit(extension, '#')[1]}">
<td th:insert="IpsUtilityFragments :: codeableConcept (cc=*{getCode()},attr='display')">Code</td>
<td th:insert="IpsUtilityFragments :: renderValue (value=*{getValue()})">Result</td>
<td th:insert="IpsUtilityFragments :: renderValueUnit (value=*{getValue()})">Unit</td>
<td th:replace="IpsUtilityFragments :: firstFromCodeableConceptList (list=*{getInterpretation()})">Interpretation</td>
<td th:insert="IpsUtilityFragments :: concat (list=*{getNote()},attr='text')">Comments</td>
<td th:insert="IpsUtilityFragments :: renderEffective (effective=*{getEffective()})">Date</td>
</tr>
</th:block>
</th:block>
</th:block>
</tbody>
</table>
</div>

View File

@ -0,0 +1,87 @@
package ca.uhn.fhir.jpa.ips.generator;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.ips.api.IIpsGenerationStrategy;
import ca.uhn.fhir.jpa.ips.provider.IpsOperationProvider;
import ca.uhn.fhir.jpa.ips.strategy.DefaultIpsGenerationStrategy;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test;
import ca.uhn.fhir.util.ClasspathUtil;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Parameters;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.ContextConfiguration;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ContextConfiguration(classes = {IpsGenerationTest.IpsConfig.class})
public class IpsGenerationTest extends BaseResourceProviderR4Test {
@Autowired
private IpsOperationProvider myIpsOperationProvider;
@BeforeEach
public void beforeEach() {
myServer.withServer(t->t.registerProvider(myIpsOperationProvider));
}
@AfterEach
public void afterEach() {
myServer.withServer(t->t.unregisterProvider(myIpsOperationProvider));
}
@Test
public void testGenerateLargePatientSummary() {
Bundle sourceData = ClasspathUtil.loadCompressedResource(myFhirContext, Bundle.class, "/large-patient-everything.json.gz");
sourceData.setType(Bundle.BundleType.TRANSACTION);
for (Bundle.BundleEntryComponent nextEntry : sourceData.getEntry()) {
nextEntry.getRequest().setMethod(Bundle.HTTPVerb.PUT);
nextEntry.getRequest().setUrl(nextEntry.getResource().getIdElement().toUnqualifiedVersionless().getValue());
}
Bundle outcome = mySystemDao.transaction(mySrd, sourceData);
ourLog.info("Created {} resources", outcome.getEntry().size());
Bundle output = myClient
.operation()
.onInstance("Patient/f15d2419-fbff-464a-826d-0afe8f095771")
.named(JpaConstants.OPERATION_SUMMARY)
.withNoParameters(Parameters.class)
.returnResourceType(Bundle.class)
.execute();
ourLog.info("Output: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(output));
assertEquals(37, output.getEntry().size());
}
@Configuration
public static class IpsConfig {
@Bean
public IIpsGenerationStrategy ipsGenerationStrategy() {
return new DefaultIpsGenerationStrategy();
}
@Bean
public IIpsGeneratorSvc ipsGeneratorSvc(FhirContext theFhirContext, IIpsGenerationStrategy theGenerationStrategy, DaoRegistry theDaoRegistry) {
return new IpsGeneratorSvcImpl(theFhirContext, theGenerationStrategy, theDaoRegistry);
}
@Bean
public IpsOperationProvider ipsOperationProvider(IIpsGeneratorSvc theIpsGeneratorSvc) {
return new IpsOperationProvider(theIpsGeneratorSvc);
}
}
}

View File

@ -0,0 +1,465 @@
package ca.uhn.fhir.jpa.ips.generator;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.ips.api.IpsSectionEnum;
import ca.uhn.fhir.jpa.ips.api.SectionRegistry;
import ca.uhn.fhir.jpa.ips.strategy.DefaultIpsGenerationStrategy;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.SimpleBundleProvider;
import ca.uhn.fhir.test.utilities.HtmlUtil;
import ca.uhn.fhir.util.ClasspathUtil;
import com.gargoylesoftware.htmlunit.html.DomElement;
import com.gargoylesoftware.htmlunit.html.DomNodeList;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.html.HtmlTable;
import com.gargoylesoftware.htmlunit.html.HtmlTableRow;
import com.google.common.collect.Lists;
import org.hamcrest.Matchers;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.AllergyIntolerance;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.CarePlan;
import org.hl7.fhir.r4.model.ClinicalImpression;
import org.hl7.fhir.r4.model.Composition;
import org.hl7.fhir.r4.model.Condition;
import org.hl7.fhir.r4.model.Consent;
import org.hl7.fhir.r4.model.DateTimeType;
import org.hl7.fhir.r4.model.Device;
import org.hl7.fhir.r4.model.DeviceUseStatement;
import org.hl7.fhir.r4.model.DiagnosticReport;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Immunization;
import org.hl7.fhir.r4.model.Medication;
import org.hl7.fhir.r4.model.MedicationAdministration;
import org.hl7.fhir.r4.model.MedicationDispense;
import org.hl7.fhir.r4.model.MedicationRequest;
import org.hl7.fhir.r4.model.MedicationStatement;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Organization;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.PositiveIntType;
import org.hl7.fhir.r4.model.Procedure;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.Resource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class IpsGeneratorSvcImplTest {
public static final String MEDICATION_ID = "Medication/tyl";
public static final String MEDICATION_STATEMENT_ID = "MedicationStatement/meds";
public static final String MEDICATION_STATEMENT_ID2 = "MedicationStatement/meds2";
private static final List<Class<? extends IBaseResource>> RESOURCE_TYPES = Lists.newArrayList(
AllergyIntolerance.class,
CarePlan.class,
Condition.class,
Consent.class,
ClinicalImpression.class,
DeviceUseStatement.class,
DiagnosticReport.class,
Immunization.class,
MedicationRequest.class,
MedicationStatement.class,
MedicationAdministration.class,
MedicationDispense.class,
Observation.class,
Patient.class,
Procedure.class
);
private static final Logger ourLog = LoggerFactory.getLogger(IpsGeneratorSvcImplTest.class);
private final FhirContext myFhirContext = FhirContext.forR4Cached();
private final DaoRegistry myDaoRegistry = new DaoRegistry(myFhirContext);
private IIpsGeneratorSvc mySvc;
private DefaultIpsGenerationStrategy myStrategy;
@BeforeEach
public void beforeEach() {
myDaoRegistry.setResourceDaos(Collections.emptyList());
myStrategy = new DefaultIpsGenerationStrategy();
mySvc = new IpsGeneratorSvcImpl(myFhirContext, myStrategy, myDaoRegistry);
}
@Test
public void testGenerateIps() {
// Setup
registerResourceDaosForSmallPatientSet();
// Test
Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new TokenParam("http://foo", "bar"));
// Verify
ourLog.info("Generated IPS:\n{}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome));
List<String> contentResourceTypes = toEntryResourceTypeStrings(outcome);
assertThat(contentResourceTypes.toString(), contentResourceTypes,
Matchers.contains("Composition", "Patient", "AllergyIntolerance", "MedicationStatement", "MedicationStatement", "MedicationStatement", "Condition", "Condition", "Condition", "Organization"));
Composition composition = (Composition) outcome.getEntry().get(0).getResource();
Composition.SectionComponent section;
// Allergy and Intolerances has no content
section = composition.getSection().get(0);
assertEquals("Allergies and Intolerances", section.getTitle());
assertThat(section.getText().getDivAsString(),
containsString("No information about allergies"));
// Medication Summary has a resource
section = composition.getSection().get(1);
assertEquals("Medication List", section.getTitle());
assertThat(section.getText().getDivAsString(),
containsString("Oral use"));
// Composition itself should also have a narrative
String compositionNarrative = composition.getText().getDivAsString();
ourLog.info("Composition narrative: {}", compositionNarrative);
assertThat(compositionNarrative, containsString("Allergies and Intolerances"));
assertThat(compositionNarrative, containsString("Pregnancy"));
}
@Test
public void testMedicationSummary_MedicationStatementWithMedicationReference() throws IOException {
// Setup Patient
registerPatientDaoWithRead();
// Setup Medication + MedicationStatement
Medication medication = createSecondaryMedication(MEDICATION_ID);
MedicationStatement medicationStatement = createPrimaryMedicationStatement(MEDICATION_ID, MEDICATION_STATEMENT_ID);
IFhirResourceDao<MedicationStatement> medicationStatementDao = registerResourceDaoWithNoData(MedicationStatement.class);
when(medicationStatementDao.search(any(), any())).thenReturn(new SimpleBundleProvider(Lists.newArrayList(medicationStatement, medication)));
registerRemainingResourceDaos();
// Test
Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType("Patient/123"));
// Verify Bundle Contents
List<String> contentResourceTypes = toEntryResourceTypeStrings(outcome);
assertThat(contentResourceTypes.toString(), contentResourceTypes,
Matchers.contains("Composition", "Patient", "AllergyIntolerance", "MedicationStatement", "Medication", "Condition", "Organization"));
MedicationStatement actualMedicationStatement = (MedicationStatement) outcome.getEntry().get(3).getResource();
Medication actualMedication = (Medication) outcome.getEntry().get(4).getResource();
assertThat(actualMedication.getId(), startsWith("urn:uuid:"));
assertEquals(actualMedication.getId(), actualMedicationStatement.getMedicationReference().getReference());
// Verify
Composition compositions = (Composition) outcome.getEntry().get(0).getResource();
Composition.SectionComponent section = compositions
.getSection()
.stream()
.filter(t -> t.getTitle().equals(myStrategy.getSectionRegistry().getSection(IpsSectionEnum.MEDICATION_SUMMARY).getTitle()))
.findFirst()
.orElseThrow();
HtmlPage narrativeHtml = HtmlUtil.parseAsHtml(section.getText().getDivAsString());
ourLog.info("Narrative:\n{}", narrativeHtml.asXml());
DomNodeList<DomElement> tables = narrativeHtml.getElementsByTagName("table");
assertEquals(2, tables.size());
HtmlTable table = (HtmlTable) tables.get(1);
HtmlTableRow row = table.getBodies().get(0).getRows().get(0);
assertEquals("Tylenol", row.getCell(0).asNormalizedText());
assertEquals("Active", row.getCell(1).asNormalizedText());
assertEquals("Oral", row.getCell(2).asNormalizedText());
assertEquals("DAW", row.getCell(3).asNormalizedText());
assertThat(row.getCell(4).asNormalizedText(), containsString("2023"));
}
@Test
public void testMedicationSummary_DuplicateSecondaryResources() {
myStrategy.setSectionRegistry(new SectionRegistry().addGlobalCustomizer(t -> t.withNoInfoGenerator(null)));
// Setup Patient
registerPatientDaoWithRead();
// Setup Medication + MedicationStatement
Medication medication = createSecondaryMedication(MEDICATION_ID);
MedicationStatement medicationStatement = createPrimaryMedicationStatement(MEDICATION_ID, MEDICATION_STATEMENT_ID);
Medication medication2 = createSecondaryMedication(MEDICATION_ID); // same ID again (could happen if we span multiple pages of results)
MedicationStatement medicationStatement2 = createPrimaryMedicationStatement(MEDICATION_ID, MEDICATION_STATEMENT_ID2);
IFhirResourceDao<MedicationStatement> medicationStatementDao = registerResourceDaoWithNoData(MedicationStatement.class);
when(medicationStatementDao.search(any(), any())).thenReturn(new SimpleBundleProvider(Lists.newArrayList(medicationStatement, medication, medicationStatement2, medication2)));
registerRemainingResourceDaos();
// Test
Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType("Patient/123"));
// Verify Bundle Contents
List<String> contentResourceTypes = toEntryResourceTypeStrings(outcome);
assertThat(contentResourceTypes.toString(), contentResourceTypes,
Matchers.contains(
"Composition",
"Patient",
"MedicationStatement",
"Medication",
"MedicationStatement",
"Organization"));
}
/**
* Make sure that if a resource is added as a secondary resource but then gets included as a
* primary resource, we include it.
*/
@Test
public void testMedicationSummary_ResourceAppearsAsSecondaryThenPrimary() throws IOException {
myStrategy.setSectionRegistry(new SectionRegistry().addGlobalCustomizer(t -> t.withNoInfoGenerator(null)));
// Setup Patient
registerPatientDaoWithRead();
// Setup Medication + MedicationStatement
Medication medication = createSecondaryMedication(MEDICATION_ID);
MedicationStatement medicationStatement = createPrimaryMedicationStatement(MEDICATION_ID, MEDICATION_STATEMENT_ID);
medicationStatement.addDerivedFrom().setReference(MEDICATION_STATEMENT_ID2);
MedicationStatement medicationStatement2 = createPrimaryMedicationStatement(MEDICATION_ID, MEDICATION_STATEMENT_ID2);
ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(medicationStatement2, BundleEntrySearchModeEnum.INCLUDE);
MedicationStatement medicationStatement3 = createPrimaryMedicationStatement(MEDICATION_ID, MEDICATION_STATEMENT_ID2);
IFhirResourceDao<MedicationStatement> medicationStatementDao = registerResourceDaoWithNoData(MedicationStatement.class);
when(medicationStatementDao.search(any(), any())).thenReturn(new SimpleBundleProvider(Lists.newArrayList(medicationStatement, medication, medicationStatement2, medicationStatement3)));
registerRemainingResourceDaos();
// Test
Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType("Patient/123"));
// Verify Bundle Contents
List<String> contentResourceTypes = toEntryResourceTypeStrings(outcome);
assertThat(contentResourceTypes.toString(), contentResourceTypes,
Matchers.contains(
"Composition",
"Patient",
"MedicationStatement",
"Medication",
"MedicationStatement",
"Organization"));
// Verify narrative - should have 2 rows (one for each primary MedicationStatement)
Composition compositions = (Composition) outcome.getEntry().get(0).getResource();
Composition.SectionComponent section = compositions
.getSection()
.stream()
.filter(t -> t.getTitle().equals(myStrategy.getSectionRegistry().getSection(IpsSectionEnum.MEDICATION_SUMMARY).getTitle()))
.findFirst()
.orElseThrow();
HtmlPage narrativeHtml = HtmlUtil.parseAsHtml(section.getText().getDivAsString());
ourLog.info("Narrative:\n{}", narrativeHtml.asXml());
DomNodeList<DomElement> tables = narrativeHtml.getElementsByTagName("table");
assertEquals(2, tables.size());
HtmlTable table = (HtmlTable) tables.get(1);
assertEquals(2, table.getBodies().get(0).getRows().size());
}
@Test
public void testMedicalDevices_DeviceUseStatementWithDevice() throws IOException {
// Setup Patient
registerPatientDaoWithRead();
// Setup Medication + MedicationStatement
Device device = new Device();
device.setId(new IdType("Device/pm"));
device.getType().addCoding().setDisplay("Pacemaker");
ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(device, BundleEntrySearchModeEnum.INCLUDE);
DeviceUseStatement deviceUseStatement = new DeviceUseStatement();
deviceUseStatement.setId("DeviceUseStatement/dus");
deviceUseStatement.setDevice(new Reference("Device/pm"));
deviceUseStatement.setStatus(DeviceUseStatement.DeviceUseStatementStatus.ACTIVE);
deviceUseStatement.addNote().setText("This is some note text");
deviceUseStatement.setRecordedOnElement(new DateTimeType("2023-01-01T12:22:33Z"));
ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(deviceUseStatement, BundleEntrySearchModeEnum.MATCH);
IFhirResourceDao<DeviceUseStatement> deviceUseStatementDao = registerResourceDaoWithNoData(DeviceUseStatement.class);
when(deviceUseStatementDao.search(any(), any())).thenReturn(new SimpleBundleProvider(Lists.newArrayList(deviceUseStatement, device)));
registerRemainingResourceDaos();
// Test
Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType("Patient/123"));
// Verify
Composition compositions = (Composition) outcome.getEntry().get(0).getResource();
Composition.SectionComponent section = compositions
.getSection()
.stream()
.filter(t -> t.getTitle().equals(myStrategy.getSectionRegistry().getSection(IpsSectionEnum.MEDICAL_DEVICES).getTitle()))
.findFirst()
.orElseThrow();
HtmlPage narrativeHtml = HtmlUtil.parseAsHtml(section.getText().getDivAsString());
ourLog.info("Narrative:\n{}", narrativeHtml.asXml());
DomNodeList<DomElement> tables = narrativeHtml.getElementsByTagName("table");
assertEquals(1, tables.size());
HtmlTable table = (HtmlTable) tables.get(0);
HtmlTableRow row = table.getBodies().get(0).getRows().get(0);
assertEquals("Pacemaker", row.getCell(0).asNormalizedText());
assertEquals("ACTIVE", row.getCell(1).asNormalizedText());
assertEquals("This is some note text", row.getCell(2).asNormalizedText());
}
@Test
public void testImmunizations() throws IOException {
// Setup Patient
registerPatientDaoWithRead();
// Setup Medication + MedicationStatement
Organization org = new Organization();
org.setId(new IdType("Organization/pfizer"));
org.setName("Pfizer");
ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(org, BundleEntrySearchModeEnum.INCLUDE);
Immunization immunization = new Immunization();
immunization.setId("Immunization/imm");
immunization.getVaccineCode().addCoding().setDisplay("SpikeVax");
immunization.setStatus(Immunization.ImmunizationStatus.COMPLETED);
immunization.addProtocolApplied().setDoseNumber(new PositiveIntType(2));
immunization.addProtocolApplied().setDoseNumber(new PositiveIntType(4));
immunization.setManufacturer(new Reference("Organization/pfizer"));
immunization.setLotNumber("35");
immunization.addNote().setText("Hello World");
immunization.setOccurrence(new DateTimeType("2023-01-01T11:22:33Z"));
ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(immunization, BundleEntrySearchModeEnum.MATCH);
IFhirResourceDao<Immunization> deviceUseStatementDao = registerResourceDaoWithNoData(Immunization.class);
when(deviceUseStatementDao.search(any(), any())).thenReturn(new SimpleBundleProvider(Lists.newArrayList(immunization, org)));
registerRemainingResourceDaos();
// Test
Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType("Patient/123"));
// Verify
Composition compositions = (Composition) outcome.getEntry().get(0).getResource();
Composition.SectionComponent section = compositions
.getSection()
.stream()
.filter(t -> t.getTitle().equals(myStrategy.getSectionRegistry().getSection(IpsSectionEnum.IMMUNIZATIONS).getTitle()))
.findFirst()
.orElseThrow();
HtmlPage narrativeHtml = HtmlUtil.parseAsHtml(section.getText().getDivAsString());
ourLog.info("Narrative:\n{}", narrativeHtml.asXml());
DomNodeList<DomElement> tables = narrativeHtml.getElementsByTagName("table");
assertEquals(1, tables.size());
HtmlTable table = (HtmlTable) tables.get(0);
HtmlTableRow row = table.getBodies().get(0).getRows().get(0);
assertEquals("SpikeVax", row.getCell(0).asNormalizedText());
assertEquals("COMPLETED", row.getCell(1).asNormalizedText());
assertEquals("2 , 4", row.getCell(2).asNormalizedText());
assertEquals("Pfizer", row.getCell(3).asNormalizedText());
assertEquals("35", row.getCell(4).asNormalizedText());
assertEquals("Hello World", row.getCell(5).asNormalizedText());
assertThat(row.getCell(6).asNormalizedText(), containsString("2023"));
}
private void registerPatientDaoWithRead() {
IFhirResourceDao<Patient> patientDao = registerResourceDaoWithNoData(Patient.class);
Patient patient = new Patient();
patient.setId("Patient/123");
when(patientDao.read(any(), any())).thenReturn(patient);
}
private void registerRemainingResourceDaos() {
for (var next : RESOURCE_TYPES) {
if (!myDaoRegistry.isResourceTypeSupported(myFhirContext.getResourceType(next))) {
IFhirResourceDao<? extends IBaseResource> dao = registerResourceDaoWithNoData(next);
when(dao.search(any(), any())).thenReturn(new SimpleBundleProvider());
}
}
}
private IBundleProvider bundleProviderWithAllOfType(Bundle theSourceData, Class<? extends IBaseResource> theType) {
List<Resource> resources = theSourceData
.getEntry()
.stream()
.filter(t -> t.getResource() != null && theType.isAssignableFrom(t.getResource().getClass()))
.map(Bundle.BundleEntryComponent::getResource)
.collect(Collectors.toList());
resources.forEach(t -> ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(t, BundleEntrySearchModeEnum.MATCH));
return new SimpleBundleProvider(resources);
}
@SuppressWarnings("unchecked")
@Nonnull
private <T extends IBaseResource> IFhirResourceDao<T> registerResourceDaoWithNoData(@Nonnull Class<T> theType) {
IFhirResourceDao<T> dao = mock(IFhirResourceDao.class);
when(dao.getResourceType()).thenReturn(theType);
myDaoRegistry.register(dao);
return dao;
}
@SuppressWarnings("rawtypes")
private void registerResourceDaosForSmallPatientSet() {
Bundle sourceData = ClasspathUtil.loadCompressedResource(myFhirContext, Bundle.class, "/small-patient-everything.json.gz");
for (var nextType : IpsGeneratorSvcImplTest.RESOURCE_TYPES) {
IFhirResourceDao dao = registerResourceDaoWithNoData(nextType);
when(dao.search(any(), any())).thenReturn(bundleProviderWithAllOfType(sourceData, nextType));
}
}
@Nonnull
private static List<String> toEntryResourceTypeStrings(Bundle outcome) {
List<String> contentResourceTypes = outcome
.getEntry()
.stream()
.map(t -> t.getResource().getResourceType().name())
.collect(Collectors.toList());
return contentResourceTypes;
}
@Nonnull
private static Medication createSecondaryMedication(String medicationId) {
Medication medication = new Medication();
medication.setId(new IdType(medicationId));
medication.getCode().addCoding().setDisplay("Tylenol");
ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(medication, BundleEntrySearchModeEnum.INCLUDE);
return medication;
}
@Nonnull
private static MedicationStatement createPrimaryMedicationStatement(String medicationId, String medicationStatementId) {
MedicationStatement medicationStatement = new MedicationStatement();
medicationStatement.setId(medicationStatementId);
medicationStatement.setMedication(new Reference(medicationId));
medicationStatement.setStatus(MedicationStatement.MedicationStatementStatus.ACTIVE);
medicationStatement.getDosageFirstRep().getRoute().addCoding().setDisplay("Oral");
medicationStatement.getDosageFirstRep().setText("DAW");
medicationStatement.setEffective(new DateTimeType("2023-01-01T11:22:33Z"));
ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(medicationStatement, BundleEntrySearchModeEnum.MATCH);
return medicationStatement;
}
}

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.12-SNAPSHOT</version>
<version>6.3.13-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.12-SNAPSHOT</version>
<version>6.3.13-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -277,6 +277,14 @@ public class JpaConstants {
public static final String HEADER_REWRITE_HISTORY = "X-Rewrite-History";
public static final String SKIP_REINDEX_ON_UPDATE = "SKIP-REINDEX-ON-UPDATE";
/**
* IPS Generation operation name
*/
public static final String OPERATION_SUMMARY = "$summary";
/**
* IPS Generation operation URL
*/
public static final String SUMMARY_OPERATION_URL = "http://hl7.org/fhir/uv/ips/OperationDefinition/summary";
/**
* Non-instantiable

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.12-SNAPSHOT</version>
<version>6.3.13-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.12-SNAPSHOT</version>
<version>6.3.13-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.12-SNAPSHOT</version>
<version>6.3.13-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.12-SNAPSHOT</version>
<version>6.3.13-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.12-SNAPSHOT</version>
<version>6.3.13-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -1402,6 +1402,10 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test
.setUrl("http://acme.org/bar")
.setValue(new StringType("HELLOHELLO"));
ourLog.info(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient));
ourLog.info(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(siblingSp));
IIdType p2id = myPatientDao.create(patient).getId().toUnqualifiedVersionless();
SearchParameterMap map;

View File

@ -20,8 +20,10 @@ public class TerminologyLoaderSvcLoincJpaTest extends BaseJpaR4Test {
private TermLoaderSvcImpl mySvc;
private ZipCollectionBuilder myFiles;
@Override
@BeforeEach
public void before() {
public void before() throws Exception {
super.before();
mySvc = new TermLoaderSvcImpl(myTerminologyDeferredStorageSvc, myTermCodeSystemStorageSvc);
myFiles = new ZipCollectionBuilder();
@ -29,7 +31,6 @@ public class TerminologyLoaderSvcLoincJpaTest extends BaseJpaR4Test {
@Test
public void testLoadLoincMultipleVersions() throws IOException {
// Load LOINC marked as version 2.67
TermTestUtil.addLoincMandatoryFilesWithPropertiesFileToZip(myFiles, "v267_loincupload.properties");
@ -61,7 +62,10 @@ public class TerminologyLoaderSvcLoincJpaTest extends BaseJpaR4Test {
mySvc.loadLoinc(myFiles.getFiles(), mySrd);
myTerminologyDeferredStorageSvc.saveAllDeferred();
await().atMost(10, SECONDS).until(() -> myTerminologyDeferredStorageSvc.isStorageQueueEmpty(true));
await().atMost(10, SECONDS).until(() -> {
myBatch2JobHelper.awaitNoJobsRunning();
return myTerminologyDeferredStorageSvc.isStorageQueueEmpty(true);
});
logAllCodeSystemsAndVersionsCodeSystemsAndVersions();
@ -89,8 +93,10 @@ public class TerminologyLoaderSvcLoincJpaTest extends BaseJpaR4Test {
TermTestUtil.addLoincMandatoryFilesWithPropertiesFileToZip(myFiles, "v268_loincupload.properties");
mySvc.loadLoinc(myFiles.getFiles(), mySrd);
myTerminologyDeferredStorageSvc.saveAllDeferred();
await().atMost(10, SECONDS).until(() -> myTerminologyDeferredStorageSvc.isStorageQueueEmpty(true));
await().atMost(10, SECONDS).until(() -> {
myBatch2JobHelper.awaitNoJobsRunning();
return myTerminologyDeferredStorageSvc.isStorageQueueEmpty(true);
});
runInTransaction(() -> {
assertEquals(1, myTermCodeSystemDao.count());

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.12-SNAPSHOT</version>
<version>6.3.13-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.12-SNAPSHOT</version>
<version>6.3.13-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.12-SNAPSHOT</version>
<version>6.3.13-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -268,8 +268,6 @@ public abstract class BaseJpaTest extends BaseTest {
DaoConfig defaultConfig = new DaoConfig();
myDaoConfig.setAdvancedHSearchIndexing(defaultConfig.isAdvancedHSearchIndexing());
myDaoConfig.setAllowContainsSearches(defaultConfig.isAllowContainsSearches());
}
@AfterEach

View File

@ -243,4 +243,10 @@ public class Batch2JobHelper {
myJobMaintenanceService.runMaintenancePass();
}
public void cancelAllJobsAndAwaitCancellation() {
List<JobInstance> instances = myJobPersistence.fetchInstances(1000, 0);
for (JobInstance next : instances) {
myJobPersistence.cancelInstance(next.getInstanceId());
}
}
}

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.3.12-SNAPSHOT</version>
<version>6.3.13-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.12-SNAPSHOT</version>
<version>6.3.13-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.12-SNAPSHOT</version>
<version>6.3.13-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
@ -56,11 +56,19 @@
<artifactId>flexmark</artifactId>
</dependency>
<!-- Servlet -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<!-- Unit Test Deps-->
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-test-utilities</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.12-SNAPSHOT</version>
<version>6.3.13-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -130,7 +130,7 @@ public class SimpleBundleProvider implements IBundleProvider {
@Nonnull
@Override
public List<IBaseResource> getResources(int theFromIndex, int theToIndex) {
return (List<IBaseResource>) myList.subList(theFromIndex, Math.min(theToIndex, myList.size()));
return (List<IBaseResource>) myList.subList(Math.min(theFromIndex, myList.size()), Math.min(theToIndex, myList.size()));
}
@Override

View File

@ -7,7 +7,7 @@
<parent>
<artifactId>hapi-fhir-serviceloaders</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>6.3.12-SNAPSHOT</version>
<version>6.3.13-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Some files were not shown because too many files have changed in this diff Show More