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:
parent
320a0d895f
commit
d584e14048
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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."
|
|
@ -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."
|
|
@ -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.
|
||||
"
|
|
@ -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."
|
|
@ -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
|
||||
|
|
|
@ -97,3 +97,38 @@ Finally, use the [CustomThymeleafNarrativeGenerator](/hapi-fhir/apidocs/hapi-fhi
|
|||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/NarrativeGenerator.java|gen}}
|
||||
```
|
||||
|
||||
# Fragments Expressions in Thyemleaf Templates
|
||||
|
||||
Thymeleaf has a concept called Fragments, which allow reusable template portions that can be imported anywhere you need them. It can be helpful to put these fragment definitions in their own file. For example, the following property file declares a template and a fragment:
|
||||
|
||||
```properties
|
||||
{{snippet:classpath:ca/uhn/fhir/narrative/narrative-with-fragment.properties}}
|
||||
```
|
||||
|
||||
The following template declares a fragment (this is `narrative-with-fragment-child.html` in the example above):
|
||||
|
||||
```html
|
||||
{{snippet:classpath:ca/uhn/fhir/narrative/narrative-with-fragment-child.html}}
|
||||
```
|
||||
|
||||
And the following template uses it (this is `narrative-with-fragment-child.html` in the example above):
|
||||
|
||||
```html
|
||||
{{snippet:classpath:ca/uhn/fhir/narrative/narrative-with-fragment-parent.html}}
|
||||
```
|
||||
|
||||
|
||||
# FHIRPath Expressions in Thyemleaf Templates
|
||||
|
||||
Thymeleaf templates can incorporate FHIRPath expressions using the `#fhirpath` expression object.
|
||||
|
||||
This object has the following methods:
|
||||
|
||||
* evaluateFirst(input, pathExpression) – This method returns the first element matched on `input` by the path expression, or _null_ if nothing matches.
|
||||
* evaluate(input, pathExpression) – This method returns a Java List of elements matched on `input` by the path expression, or an empty list if nothing matches.
|
||||
|
||||
For example:
|
||||
|
||||
```html
|
||||
{{snippet:classpath:ca/uhn/fhir/narrative/narratives-with-fhirpath-evaluate-single-primitive.html}}
|
||||
```
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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 |
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
||||
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
Binary file not shown.
Binary file not shown.
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -268,8 +268,6 @@ public abstract class BaseJpaTest extends BaseTest {
|
|||
DaoConfig defaultConfig = new DaoConfig();
|
||||
myDaoConfig.setAdvancedHSearchIndexing(defaultConfig.isAdvancedHSearchIndexing());
|
||||
myDaoConfig.setAllowContainsSearches(defaultConfig.isAllowContainsSearches());
|
||||
|
||||
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue