IPS API Refactor (#5682)

* IPS enhancements

* API design complete

* Work on section registry

* Work on external fetch

* IPS rewrite

* Cleanup

* Work

* IPS refactor

* Add changelog

* Changelog updates

* Spotless

* Compile fix

* Address review comments

* Address review comments

* License header

* Revert narrative builder change

* Address review comments

* Addres review comments

* Cleanup
This commit is contained in:
James Agnew 2024-02-11 10:43:56 -05:00 committed by GitHub
parent d71736bf9d
commit 12eb2d6f35
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
69 changed files with 3449 additions and 1478 deletions

View File

@ -8,6 +8,11 @@ tab_width = 4
indent_size = 4
charset = utf-8
[*.html]
indent_style = tab
tab_width = 3
indent_size = 3
[*.xml]
indent_style = tab
tab_width = 3

View File

@ -25,6 +25,7 @@ import ca.uhn.fhir.fhirpath.IFhirPathEvaluationContext;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.narrative2.BaseNarrativeGenerator;
import ca.uhn.fhir.narrative2.INarrativeTemplate;
import ca.uhn.fhir.narrative2.NarrativeGeneratorTemplateUtils;
import ca.uhn.fhir.narrative2.TemplateTypeEnum;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import com.google.common.collect.Sets;
@ -109,6 +110,7 @@ public abstract class BaseThymeleafNarrativeGenerator extends BaseNarrativeGener
Context context = new Context();
context.setVariable("resource", theTargetContext);
context.setVariable("context", theTargetContext);
context.setVariable("narrativeUtil", NarrativeGeneratorTemplateUtils.INSTANCE);
context.setVariable(
"fhirVersion", theFhirContext.getVersion().getVersion().name());

View File

@ -0,0 +1,53 @@
/*-
* #%L
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2024 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%
*/
package ca.uhn.fhir.narrative2;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.util.BundleUtil;
import org.apache.commons.lang3.tuple.Pair;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseResource;
import java.util.List;
import java.util.Objects;
/**
* An instance of this class is added to the Thymeleaf context as a variable with
* name <code>"narrativeUtil"</code> and can be accessed from narrative templates.
*
* @since 7.0.0
*/
public class NarrativeGeneratorTemplateUtils {
public static final NarrativeGeneratorTemplateUtils INSTANCE = new NarrativeGeneratorTemplateUtils();
/**
* Given a Bundle as input, are any entries present with a given resource type
*/
public boolean bundleHasEntriesWithResourceType(IBaseBundle theBaseBundle, String theResourceType) {
FhirContext ctx = theBaseBundle.getStructureFhirVersionEnum().newContextCached();
List<Pair<String, IBaseResource>> entryResources =
BundleUtil.getBundleEntryUrlsAndResources(ctx, theBaseBundle);
return entryResources.stream()
.map(Pair::getValue)
.filter(Objects::nonNull)
.anyMatch(t -> ctx.getResourceType(t).equals(theResourceType));
}
}

View File

@ -435,7 +435,7 @@ public class BundleBuilder {
*/
public void addCollectionEntry(IBaseResource theResource) {
setType("collection");
addEntryAndReturnRequest(theResource, theResource.getIdElement().getValue());
addEntryAndReturnRequest(theResource);
}
/**
@ -443,7 +443,7 @@ public class BundleBuilder {
*/
public void addDocumentEntry(IBaseResource theResource) {
setType("document");
addEntryAndReturnRequest(theResource, theResource.getIdElement().getValue());
addEntryAndReturnRequest(theResource);
}
/**
@ -475,6 +475,14 @@ public class BundleBuilder {
return (IBaseBackboneElement) searchInstance;
}
private IBase addEntryAndReturnRequest(IBaseResource theResource) {
IIdType id = theResource.getIdElement();
if (id.hasVersionIdPart()) {
id = id.toVersionless();
}
return addEntryAndReturnRequest(theResource, id.getValue());
}
private IBase addEntryAndReturnRequest(IBaseResource theResource, String theFullUrl) {
Validate.notNull(theResource, "theResource must not be null");

View File

@ -21,6 +21,7 @@ package ca.uhn.fhir.util;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import static org.apache.commons.lang3.StringUtils.isBlank;
@ -82,6 +83,12 @@ public class ValidateUtil {
}
}
public static void isTrueOrThrowResourceNotFound(boolean theSuccess, String theMessage, Object... theValues) {
if (!theSuccess) {
throw new ResourceNotFoundException(Msg.code(2494) + String.format(theMessage, theValues));
}
}
public static void exactlyOneNotNullOrThrowInvalidRequestException(Object[] theObjects, String theMessage) {
int count = 0;
for (Object next : theObjects) {

View File

@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import org.junit.jupiter.api.Test;
@ -12,7 +13,7 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
public class ValidateUtilTest {
@Test
public void testValidate() {
public void testIsTrueOrThrowInvalidRequest() {
ValidateUtil.isTrueOrThrowInvalidRequest(true, "");
try {
@ -23,6 +24,18 @@ public class ValidateUtilTest {
}
}
@Test
public void testIsTrueOrThrowResourceNotFound() {
ValidateUtil.isTrueOrThrowResourceNotFound(true, "");
try {
ValidateUtil.isTrueOrThrowResourceNotFound(false, "The message");
fail();
} catch (ResourceNotFoundException e) {
assertEquals(Msg.code(2494) + "The message", e.getMessage());
}
}
@Test
public void testIsGreaterThan() {
ValidateUtil.isGreaterThan(2L, 1L, "");

View File

@ -0,0 +1,6 @@
---
type: fix
issue: 5682
title: "The BundleBuilder utility class will no longer include the `/_version/xxx` portion of the
resource ID in the `Bundle.entry.fullUrl` it generates, as the FHIR specification states that this
should be omitted."

View File

@ -0,0 +1,9 @@
---
type: change
issue: 5682
title: "The IPS $summary generation API has been overhauled to make it more flexible for
future use cases. Specifically, the section registry has been removed and folded into
the generation strategy, and support has been added for non-JPA sources of data. This is
a breaking change to the API, and implementers will need to update their code. This updated
API incorporates community feedback, and should now be considered a stable API for IPS
generation."

View File

@ -0,0 +1,26 @@
---
type: add
issue: 5682
title: "Several enhancements have been made to the International Patient Summary generator based on
feedback from implementers:
<ul>
<li>
New methods have been added to the <code>IIpsGenerationStrategy</code> allowing resources
for any or all sections to be fetched from a source other than the FHIR repository.
</li>
<li>
The <code>IpsSectionEnum</code> class has been removed and replaced in any user-facing APIs
with references to <code>SectionRegistry.Section</code>. This makes it much easier to
extend or replace the section registry with custom sections not defined in the universal
IPS implementation guide.
</li>
<li>
Captions have been removed from narrative section tables, and replaced with H5 tags
directly above the table. This results in an easier to read display since the table
title will appear above the table instead of below it.
</li>
<li>
The IPS narrative generator built in templates will now omit tables when the template
specified multiple tables and the specific table would have no resources.
</li>
</ul>"

View File

@ -0,0 +1,5 @@
---
type: fix
issue: 5682
title: "The IPS Generator will no longer replace resource IDs with placeholder IDs in the resulting
bundle by default, although this can be overridden in the generation strategy object."

View File

@ -14,24 +14,17 @@ The IPS Generator uses FHIR resources stored in your repository as its input. Th
# 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.
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 `DefaultJpaIpsGenerationStrategy` 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.
The generation strategy also supplies the [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)
The default generation strategy 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.
<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)
* JavaDoc: [DefaultJpaIpsGenerationStrategy](/hapi-fhir/apidocs/hapi-fhir-jpaserver-ips/ca/uhn/fhir/jpa/ips/jpa/DefaultJpaIpsGenerationStrategy.html)
* Source Code: [DefaultJpaIpsGenerationStrategy.java](https://github.com/hapifhir/hapi-fhir/blob/master/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/DefaultJpaIpsGenerationStrategy.java)
<a name="narrative-templates"/>
@ -44,7 +37,7 @@ The IPS generator uses HAPI FHIR [Narrative Generation](/hapi-fhir/docs/model/na
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:
The narrative properties file should contain definitions using the profile URL of the individual section (as defined in the section definition within the generation strategy) as the `.profile` qualifier. For example:
```properties
ips-allergyintolerance.resourceType=Bundle

View File

@ -19,15 +19,17 @@
*/
package ca.uhn.fhir.jpa.ips.api;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.jpa.ips.strategy.BaseIpsGenerationStrategy;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import java.util.List;
import java.util.Set;
/**
* This interface is the primary configuration and strategy provider for the
@ -39,11 +41,34 @@ import java.util.Set;
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.
* This method returns the profile associated with the IPS document
* generated by this strategy.
*/
SectionRegistry getSectionRegistry();
String getBundleProfile();
/**
* This method will be called once by the framework. It can be
* used to perform any initialization.
*/
void initialize();
/**
* This method should return a list of the sections to include in the
* generated IPS. Note that each section must have a unique value for the
* {@link Section#getProfile()} value.
*/
@Nonnull
List<Section> getSections();
/**
* Returns the resource supplier for the given section. The resource supplier
* is used to supply the resources which will be used for a given
* section.
*
* @param theSection The section
*/
@Nonnull
ISectionResourceSupplier getSectionResourceSupplier(@Nonnull Section theSection);
/**
* Provides a list of configuration property files for the IPS narrative generator.
@ -53,7 +78,7 @@ public interface IIpsGenerationStrategy {
* <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}
* {@link BaseIpsGenerationStrategy#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>
@ -85,7 +110,13 @@ public interface IIpsGenerationStrategy {
/**
* 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.
* return <code>null</code> to leave the resource ID as-is, or generate a
* placeholder UUID to replace it with.
* <p>
* If you want to replace the native resource ID with a placeholder so as not
* to leak the server-generated IDs, the recommended way is to
* return <code>IdType.newRandomUuid()</code>
* </p>
*
* @param theIpsContext The associated context for the specific IPS document being
* generated. Note that this will be <code>null</code> when
@ -93,43 +124,33 @@ public interface IIpsGenerationStrategy {
* 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
* @return An ID to assign to the resource, or <code>null</code> to leave the existing ID intact,
* meaning that the server-assigned IDs will be used in the bundle.
*/
@Nullable
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>.
* Fetches and returns the patient to include in the generated IPS for the given patient ID.
*
* @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.
* @throws ResourceNotFoundException If the ID is not known.
*/
@Nonnull
Set<Include> provideResourceSearchIncludes(IpsContext.IpsSectionContext theIpsSectionContext);
IBaseResource fetchPatient(IIdType thePatientId, RequestDetails theRequestDetails) throws ResourceNotFoundException;
/**
* 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.
* Fetches and returns the patient to include in the generated IPS for the given patient identifier.
*
* @throws ResourceNotFoundException If the ID is not known.
*/
boolean shouldInclude(IpsContext.IpsSectionContext theIpsSectionContext, IBaseResource theCandidate);
@Nonnull
IBaseResource fetchPatient(TokenParam thePatientIdentifier, RequestDetails theRequestDetails);
/**
* This method is called once for each generated IPS document, after all other processing is complete. It can
* be used by the strategy to make direct manipulations prior to returning the document.
*/
default void postManipulateIpsBundle(IBaseBundle theBundle) {
// nothing
}
}

View File

@ -0,0 +1,39 @@
/*-
* #%L
* HAPI FHIR JPA Server - International Patient Summary (IPS)
* %%
* Copyright (C) 2014 - 2024 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%
*/
package ca.uhn.fhir.jpa.ips.api;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
/**
* This interface is invoked when a section has no resources found, and should generate
* a "stub" resource explaining why. Typically this would be content such as "no information
* is available for this section", and might indicate for example that the absence of
* AllergyIntolerance resources only indicates that the allergy status is not known, not that
* the patient has no allergies.
*/
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);
}

View File

@ -0,0 +1,125 @@
/*-
* #%L
* HAPI FHIR JPA Server - International Patient Summary (IPS)
* %%
* Copyright (C) 2014 - 2024 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%
*/
package ca.uhn.fhir.jpa.ips.api;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.thymeleaf.util.Validate;
import java.util.List;
/**
* This interface is invoked for each section of the IPS, and fetches/returns the
* resources which will be included in the IPS document for that section. This
* might be by performing a search in a local repository, but could also be
* done by calling a remote repository, performing a calculation, making
* JDBC database calls directly, etc.
* <p>
* Note that you only need to implement this interface directly if you want to
* provide manual logic for gathering and preparing resources to include in
* an IPS document. If your resources can be collected by querying a JPS
* repository, you can use {@link ca.uhn.fhir.jpa.ips.jpa.JpaSectionResourceSupplier}
* as the implementation of this interface, and
* {@link ca.uhn.fhir.jpa.ips.jpa.IJpaSectionSearchStrategy} becomes the class
* that is used to define your searches.
* </p>
*
* @since 7.2.0
* @see ca.uhn.fhir.jpa.ips.jpa.JpaSectionResourceSupplier
*/
public interface ISectionResourceSupplier {
/**
* This method will be called once for each section context (section and resource type combination),
* and will be used to supply the resources to include in the given IPS section. This method can
* be used if you wish to fetch resources for a given section from a source other than
* the repository. This could mean fetching resources using a FHIR REST client to an
* external server, or could even mean fetching data directly from a database using JDBC
* or similar.
*
* @param theIpsContext The IPS context, containing the identity of the patient whose IPS is being generated.
* @param theSectionContext The section context, containing the section name and resource type.
* @param theRequestDetails The RequestDetails object associated with the HTTP request associated with this generation.
* @return Returns a list of resources to add to the given section, or <code>null</code>.
*/
@Nullable
<T extends IBaseResource> List<ResourceEntry> fetchResourcesForSection(
IpsContext theIpsContext, IpsSectionContext<T> theSectionContext, RequestDetails theRequestDetails);
/**
* This enum specifies how an individual {@link ResourceEntry resource entry} that
* is returned by {@link #fetchResourcesForSection(IpsContext, IpsSectionContext, RequestDetails)}
* should be included in the resulting IPS document bundle.
*/
enum InclusionTypeEnum {
/**
* The resource should be included in the document bundle and linked to
* from the Composition via the <code>Composition.section.entry</code>
* reference.
*/
PRIMARY_RESOURCE,
/**
* The resource should be included in the document bundle, but not directly
* linked from the composition. This typically means that it is referenced
* by at least one primary resource.
*/
SECONDARY_RESOURCE,
/**
* Do not include this resource in the document
*/
EXCLUDE
}
/**
* This class is the return type for {@link #fetchResourcesForSection(IpsContext, IpsSectionContext, RequestDetails)}.
*/
class ResourceEntry {
private final IBaseResource myResource;
private final InclusionTypeEnum myInclusionType;
/**
* Constructor
*
* @param theResource The resource to include (must not be null)
* @param theInclusionType The inclusion type (must not be null)
*/
public ResourceEntry(@Nonnull IBaseResource theResource, @Nonnull InclusionTypeEnum theInclusionType) {
Validate.notNull(theResource, "theResource must not be null");
Validate.notNull(theInclusionType, "theInclusionType must not be null");
myResource = theResource;
myInclusionType = theInclusionType;
}
public IBaseResource getResource() {
return myResource;
}
public InclusionTypeEnum getInclusionType() {
return myInclusionType;
}
}
}

View File

@ -58,28 +58,8 @@ public class IpsContext {
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;
}
public <T extends IBaseResource> IpsSectionContext<T> newSectionContext(
Section theSection, Class<T> theResourceType) {
return new IpsSectionContext<>(mySubject, mySubjectId, theSection, theResourceType);
}
}

View File

@ -0,0 +1,43 @@
/*-
* #%L
* HAPI FHIR JPA Server - International Patient Summary (IPS)
* %%
* Copyright (C) 2014 - 2024 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%
*/
package ca.uhn.fhir.jpa.ips.api;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
public class IpsSectionContext<T extends IBaseResource> extends IpsContext {
private final Section mySection;
private final Class<T> myResourceType;
IpsSectionContext(IBaseResource theSubject, IIdType theSubjectId, Section theSection, Class<T> theResourceType) {
super(theSubject, theSubjectId);
mySection = theSection;
myResourceType = theResourceType;
}
public Class<T> getResourceType() {
return myResourceType;
}
public Section getSection() {
return mySection;
}
}

View File

@ -0,0 +1,223 @@
/*-
* #%L
* HAPI FHIR JPA Server - International Patient Summary (IPS)
* %%
* Copyright (C) 2014 - 2024 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%
*/
package ca.uhn.fhir.jpa.ips.api;
import jakarta.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseResource;
import java.util.ArrayList;
import java.util.List;
/**
* Call {@link #newBuilder()} to create a new instance of this class.
*/
public class Section {
private final String myTitle;
private final String mySectionCode;
private final String mySectionDisplay;
private final List<Class<? extends IBaseResource>> myResourceTypes;
private final String myProfile;
private final INoInfoGenerator myNoInfoGenerator;
private final String mySectionSystem;
private Section(
String theTitle,
String theSectionSystem,
String theSectionCode,
String theSectionDisplay,
List<Class<? extends IBaseResource>> theResourceTypes,
String theProfile,
INoInfoGenerator theNoInfoGenerator) {
myTitle = theTitle;
mySectionSystem = theSectionSystem;
mySectionCode = theSectionCode;
mySectionDisplay = theSectionDisplay;
myResourceTypes = List.copyOf(theResourceTypes);
myProfile = theProfile;
myNoInfoGenerator = theNoInfoGenerator;
}
@Nullable
public INoInfoGenerator getNoInfoGenerator() {
return myNoInfoGenerator;
}
public List<Class<? extends IBaseResource>> getResourceTypes() {
return myResourceTypes;
}
public String getProfile() {
return myProfile;
}
public String getTitle() {
return myTitle;
}
public String getSectionSystem() {
return mySectionSystem;
}
public String getSectionCode() {
return mySectionCode;
}
public String getSectionDisplay() {
return mySectionDisplay;
}
@Override
public boolean equals(Object theO) {
if (theO instanceof Section) {
Section o = (Section) theO;
return StringUtils.equals(myProfile, o.myProfile);
}
return false;
}
@Override
public int hashCode() {
return myProfile.hashCode();
}
/**
* Create a new empty section builder
*/
public static SectionBuilder newBuilder() {
return new SectionBuilder();
}
/**
* Create a new section builder which is a clone of an existing section
*/
public static SectionBuilder newBuilder(Section theSection) {
return new SectionBuilder(
theSection.myTitle,
theSection.mySectionSystem,
theSection.mySectionCode,
theSection.mySectionDisplay,
theSection.myProfile,
theSection.myNoInfoGenerator,
theSection.myResourceTypes);
}
public static class SectionBuilder {
private String myTitle;
private String mySectionSystem;
private String mySectionCode;
private String mySectionDisplay;
private List<Class<? extends IBaseResource>> myResourceTypes = new ArrayList<>();
private String myProfile;
private INoInfoGenerator myNoInfoGenerator;
private SectionBuilder() {
super();
}
public SectionBuilder(
String theTitle,
String theSectionSystem,
String theSectionCode,
String theSectionDisplay,
String theProfile,
INoInfoGenerator theNoInfoGenerator,
List<Class<? extends IBaseResource>> theResourceTypes) {
myTitle = theTitle;
mySectionSystem = theSectionSystem;
mySectionCode = theSectionCode;
mySectionDisplay = theSectionDisplay;
myNoInfoGenerator = theNoInfoGenerator;
myProfile = theProfile;
myResourceTypes = new ArrayList<>(theResourceTypes);
}
public SectionBuilder withTitle(String theTitle) {
Validate.notBlank(theTitle);
myTitle = theTitle;
return this;
}
public SectionBuilder withSectionSystem(String theSectionSystem) {
Validate.notBlank(theSectionSystem);
mySectionSystem = theSectionSystem;
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;
}
/**
* This method may be called multiple times if the section will contain multiple resource types
*/
public SectionBuilder withResourceType(Class<? extends IBaseResource> theResourceType) {
Validate.notNull(theResourceType, "theResourceType must not be null");
Validate.isTrue(!myResourceTypes.contains(theResourceType), "theResourceType has already been added");
myResourceTypes.add(theResourceType);
return this;
}
public SectionBuilder withProfile(String theProfile) {
Validate.notBlank(theProfile);
myProfile = theProfile;
return this;
}
/**
* Supplies a {@link INoInfoGenerator} which is used to create a stub resource
* to place in this section if no actual contents are found. This can be
* {@literal null} if you do not want any such stub to be included for this
* section.
*/
@SuppressWarnings("UnusedReturnValue")
public SectionBuilder withNoInfoGenerator(@Nullable INoInfoGenerator theNoInfoGenerator) {
myNoInfoGenerator = theNoInfoGenerator;
return this;
}
public Section build() {
Validate.notBlank(mySectionSystem, "No section system has been defined for this section");
Validate.notBlank(mySectionCode, "No section code has been defined for this section");
Validate.notBlank(mySectionDisplay, "No section display has been defined for this section");
return new Section(
myTitle,
mySectionSystem,
mySectionCode,
mySectionDisplay,
myResourceTypes,
myProfile,
myNoInfoGenerator);
}
}
}

View File

@ -1,470 +0,0 @@
/*-
* #%L
* HAPI FHIR JPA Server - International Patient Summary (IPS)
* %%
* Copyright (C) 2014 - 2024 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%
*/
package ca.uhn.fhir.jpa.ips.api;
import jakarta.annotation.Nullable;
import jakarta.annotation.PostConstruct;
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 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 Document")
.withResourceTypes(ResourceType.AllergyIntolerance.name())
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionAllergies")
.withNoInfoGenerator(new AllergyIntoleranceNoInfoR4Generator())
.build();
}
protected void addSectionMedicationSummary() {
addSection(IpsSectionEnum.MEDICATION_SUMMARY)
.withTitle("Medication List")
.withSectionCode("10160-0")
.withSectionDisplay("History of Medication use Narrative")
.withResourceTypes(
ResourceType.MedicationStatement.name(),
ResourceType.MedicationRequest.name(),
ResourceType.MedicationAdministration.name(),
ResourceType.MedicationDispense.name())
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionMedications")
.withNoInfoGenerator(new MedicationNoInfoR4Generator())
.build();
}
protected void addSectionProblemList() {
addSection(IpsSectionEnum.PROBLEM_LIST)
.withTitle("Problem List")
.withSectionCode("11450-4")
.withSectionDisplay("Problem list - Reported")
.withResourceTypes(ResourceType.Condition.name())
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionProblems")
.withNoInfoGenerator(new ProblemNoInfoR4Generator())
.build();
}
protected void addSectionImmunizations() {
addSection(IpsSectionEnum.IMMUNIZATIONS)
.withTitle("History of Immunizations")
.withSectionCode("11369-6")
.withSectionDisplay("History of Immunization Narrative")
.withResourceTypes(ResourceType.Immunization.name())
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionImmunizations")
.build();
}
protected void addSectionProcedures() {
addSection(IpsSectionEnum.PROCEDURES)
.withTitle("History of Procedures")
.withSectionCode("47519-4")
.withSectionDisplay("History of Procedures Document")
.withResourceTypes(ResourceType.Procedure.name())
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionProceduresHx")
.build();
}
protected void addSectionMedicalDevices() {
addSection(IpsSectionEnum.MEDICAL_DEVICES)
.withTitle("Medical Devices")
.withSectionCode("46264-8")
.withSectionDisplay("History of medical device use")
.withResourceTypes(ResourceType.DeviceUseStatement.name())
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionMedicalDevices")
.build();
}
protected void addSectionDiagnosticResults() {
addSection(IpsSectionEnum.DIAGNOSTIC_RESULTS)
.withTitle("Diagnostic Results")
.withSectionCode("30954-2")
.withSectionDisplay("Relevant diagnostic tests/laboratory data Narrative")
.withResourceTypes(ResourceType.DiagnosticReport.name(), ResourceType.Observation.name())
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionResults")
.build();
}
protected void addSectionVitalSigns() {
addSection(IpsSectionEnum.VITAL_SIGNS)
.withTitle("Vital Signs")
.withSectionCode("8716-3")
.withSectionDisplay("Vital signs")
.withResourceTypes(ResourceType.Observation.name())
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionVitalSigns")
.build();
}
protected void addSectionPregnancy() {
addSection(IpsSectionEnum.PREGNANCY)
.withTitle("Pregnancy Information")
.withSectionCode("10162-6")
.withSectionDisplay("History of pregnancies Narrative")
.withResourceTypes(ResourceType.Observation.name())
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionPregnancyHx")
.build();
}
protected void addSectionSocialHistory() {
addSection(IpsSectionEnum.SOCIAL_HISTORY)
.withTitle("Social History")
.withSectionCode("29762-2")
.withSectionDisplay("Social history Narrative")
.withResourceTypes(ResourceType.Observation.name())
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionSocialHistory")
.build();
}
protected void addSectionIllnessHistory() {
addSection(IpsSectionEnum.ILLNESS_HISTORY)
.withTitle("History of Past Illness")
.withSectionCode("11348-0")
.withSectionDisplay("History of Past illness Narrative")
.withResourceTypes(ResourceType.Condition.name())
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionPastIllnessHx")
.build();
}
protected void addSectionFunctionalStatus() {
addSection(IpsSectionEnum.FUNCTIONAL_STATUS)
.withTitle("Functional Status")
.withSectionCode("47420-5")
.withSectionDisplay("Functional status assessment note")
.withResourceTypes(ResourceType.ClinicalImpression.name())
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionFunctionalStatus")
.build();
}
protected void addSectionPlanOfCare() {
addSection(IpsSectionEnum.PLAN_OF_CARE)
.withTitle("Plan of Care")
.withSectionCode("18776-5")
.withSectionDisplay("Plan of care note")
.withResourceTypes(ResourceType.CarePlan.name())
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionPlanOfCare")
.build();
}
protected void addSectionAdvanceDirectives() {
addSection(IpsSectionEnum.ADVANCE_DIRECTIVES)
.withTitle("Advance Directives")
.withSectionCode("42348-3")
.withSectionDisplay("Advance directives")
.withResourceTypes(ResourceType.Consent.name())
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionAdvanceDirectives")
.build();
}
private SectionBuilder addSection(IpsSectionEnum theSectionEnum) {
return new SectionBuilder(theSectionEnum);
}
public SectionRegistry addGlobalCustomizer(Consumer<SectionBuilder> theGlobalCustomizer) {
Validate.notNull(theGlobalCustomizer, "theGlobalCustomizer must not be null");
myGlobalCustomizers.add(theGlobalCustomizer);
return this;
}
public List<Section> getSections() {
Validate.isTrue(isInitialized(), "Section registry has not been initialized");
return Collections.unmodifiableList(mySections);
}
public Section getSection(IpsSectionEnum theSectionEnum) {
return getSections().stream()
.filter(t -> t.getSectionEnum() == theSectionEnum)
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("No section for type: " + theSectionEnum));
}
public interface INoInfoGenerator {
/**
* Generate an appropriate no-info resource. The resource does not need to have an ID populated,
* although it can if it is a resource found in the repository.
*/
IBaseResource generate(IIdType theSubjectId);
}
public class SectionBuilder {
private final IpsSectionEnum mySectionEnum;
private String myTitle;
private String mySectionCode;
private String mySectionDisplay;
private List<String> myResourceTypes;
private String myProfile;
private INoInfoGenerator myNoInfoGenerator;
public SectionBuilder(IpsSectionEnum theSectionEnum) {
mySectionEnum = theSectionEnum;
}
public SectionBuilder withTitle(String theTitle) {
Validate.notBlank(theTitle);
myTitle = theTitle;
return this;
}
public SectionBuilder withSectionCode(String theSectionCode) {
Validate.notBlank(theSectionCode);
mySectionCode = theSectionCode;
return this;
}
public SectionBuilder withSectionDisplay(String theSectionDisplay) {
Validate.notBlank(theSectionDisplay);
mySectionDisplay = theSectionDisplay;
return this;
}
public SectionBuilder withResourceTypes(String... theResourceTypes) {
Validate.isTrue(theResourceTypes.length > 0);
myResourceTypes = Arrays.asList(theResourceTypes);
return this;
}
public SectionBuilder withProfile(String theProfile) {
Validate.notBlank(theProfile);
myProfile = theProfile;
return this;
}
public SectionBuilder withNoInfoGenerator(INoInfoGenerator theNoInfoGenerator) {
myNoInfoGenerator = theNoInfoGenerator;
return this;
}
public void build() {
myGlobalCustomizers.forEach(t -> t.accept(this));
mySections.add(new Section(
mySectionEnum,
myTitle,
mySectionCode,
mySectionDisplay,
myResourceTypes,
myProfile,
myNoInfoGenerator));
}
}
private static class AllergyIntoleranceNoInfoR4Generator implements INoInfoGenerator {
@Override
public IBaseResource generate(IIdType theSubjectId) {
AllergyIntolerance allergy = new AllergyIntolerance();
allergy.setCode(new CodeableConcept()
.addCoding(new Coding()
.setCode("no-allergy-info")
.setSystem("http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips")
.setDisplay("No information about allergies")))
.setPatient(new Reference(theSubjectId))
.setClinicalStatus(new CodeableConcept()
.addCoding(new Coding()
.setCode("active")
.setSystem("http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical")));
return allergy;
}
}
private static class MedicationNoInfoR4Generator implements INoInfoGenerator {
@Override
public IBaseResource generate(IIdType theSubjectId) {
MedicationStatement medication = new MedicationStatement();
// setMedicationCodeableConcept is not available
medication
.setMedication(new CodeableConcept()
.addCoding(new Coding()
.setCode("no-medication-info")
.setSystem("http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips")
.setDisplay("No information about medications")))
.setSubject(new Reference(theSubjectId))
.setStatus(MedicationStatement.MedicationStatementStatus.UNKNOWN);
// .setEffective(new
// Period().addExtension().setUrl("http://hl7.org/fhir/StructureDefinition/data-absent-reason").setValue((new Coding().setCode("not-applicable"))))
return medication;
}
}
private static class ProblemNoInfoR4Generator implements INoInfoGenerator {
@Override
public IBaseResource generate(IIdType theSubjectId) {
Condition condition = new Condition();
condition
.setCode(new CodeableConcept()
.addCoding(new Coding()
.setCode("no-problem-info")
.setSystem("http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips")
.setDisplay("No information about problems")))
.setSubject(new Reference(theSubjectId))
.setClinicalStatus(new CodeableConcept()
.addCoding(new Coding()
.setCode("active")
.setSystem("http://terminology.hl7.org/CodeSystem/condition-clinical")));
return condition;
}
}
public static class Section {
private final IpsSectionEnum mySectionEnum;
private final String myTitle;
private final String mySectionCode;
private final String mySectionDisplay;
private final List<String> myResourceTypes;
private final String myProfile;
private final INoInfoGenerator myNoInfoGenerator;
public Section(
IpsSectionEnum theSectionEnum,
String theTitle,
String theSectionCode,
String theSectionDisplay,
List<String> theResourceTypes,
String theProfile,
INoInfoGenerator theNoInfoGenerator) {
mySectionEnum = theSectionEnum;
myTitle = theTitle;
mySectionCode = theSectionCode;
mySectionDisplay = theSectionDisplay;
myResourceTypes = Collections.unmodifiableList(new ArrayList<>(theResourceTypes));
myProfile = theProfile;
myNoInfoGenerator = theNoInfoGenerator;
}
@Nullable
public INoInfoGenerator getNoInfoGenerator() {
return myNoInfoGenerator;
}
public List<String> getResourceTypes() {
return myResourceTypes;
}
public String getProfile() {
return myProfile;
}
public IpsSectionEnum getSectionEnum() {
return mySectionEnum;
}
public String getTitle() {
return myTitle;
}
public String getSectionCode() {
return mySectionCode;
}
public String getSectionDisplay() {
return mySectionDisplay;
}
}
}

View File

@ -30,11 +30,11 @@ 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);
IBaseBundle generateIps(RequestDetails theRequestDetails, IIdType thePatientId, String theProfile);
/**
* Generates an IPS document and returns the complete document bundle
* for the given patient by identifier
*/
IBaseBundle generateIps(RequestDetails theRequestDetails, TokenParam thePatientIdentifier);
IBaseBundle generateIps(RequestDetails theRequestDetails, TokenParam thePatientIdentifier, String theProfile);
}

View File

@ -20,33 +20,23 @@
package ca.uhn.fhir.jpa.ips.generator;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
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.ISectionResourceSupplier;
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.dstu2.resource.Observation;
import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum;
import ca.uhn.fhir.jpa.ips.api.IpsSectionContext;
import ca.uhn.fhir.jpa.ips.api.Section;
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 com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseExtension;
@ -58,94 +48,100 @@ 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 java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import static ca.uhn.fhir.jpa.term.api.ITermLoaderSvc.LOINC_URI;
import static java.util.Objects.requireNonNull;
import static org.apache.commons.lang3.StringUtils.isBlank;
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;
public static final String RESOURCE_ENTRY_INCLUSION_TYPE = "RESOURCE_ENTRY_INCLUSION_TYPE";
public static final String URL_NARRATIVE_LINK = "http://hl7.org/fhir/StructureDefinition/narrativeLink";
private final List<IIpsGenerationStrategy> myGenerationStrategies;
private final FhirContext myFhirContext;
/**
* Constructor
*/
public IpsGeneratorSvcImpl(
FhirContext theFhirContext, IIpsGenerationStrategy theGenerationStrategy, DaoRegistry theDaoRegistry) {
myGenerationStrategy = theGenerationStrategy;
myDaoRegistry = theDaoRegistry;
public IpsGeneratorSvcImpl(FhirContext theFhirContext, IIpsGenerationStrategy theGenerationStrategy) {
this(theFhirContext, List.of(theGenerationStrategy));
}
public IpsGeneratorSvcImpl(FhirContext theFhirContext, List<IIpsGenerationStrategy> theIpsGenerationStrategies) {
myGenerationStrategies = theIpsGenerationStrategies;
myFhirContext = theFhirContext;
myGenerationStrategies.forEach(IIpsGenerationStrategy::initialize);
}
/**
* Generate an IPS using a patient ID
*/
@Override
public IBaseBundle generateIps(RequestDetails theRequestDetails, IIdType thePatientId) {
IBaseResource patient = myDaoRegistry.getResourceDao("Patient").read(thePatientId, theRequestDetails);
return generateIpsForPatient(theRequestDetails, patient);
public IBaseBundle generateIps(RequestDetails theRequestDetails, IIdType thePatientId, String theProfile) {
IIpsGenerationStrategy strategy = selectGenerationStrategy(theProfile);
IBaseResource patient = strategy.fetchPatient(thePatientId, theRequestDetails);
return generateIpsForPatient(strategy, theRequestDetails, patient);
}
/**
* Generate an IPS using a patient identifier
*/
@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);
public IBaseBundle generateIps(
RequestDetails theRequestDetails, TokenParam thePatientIdentifier, String theProfile) {
IIpsGenerationStrategy strategy = selectGenerationStrategy(theProfile);
IBaseResource patient = strategy.fetchPatient(thePatientIdentifier, theRequestDetails);
return generateIpsForPatient(strategy, theRequestDetails, patient);
}
private IBaseBundle generateIpsForPatient(RequestDetails theRequestDetails, IBaseResource thePatient) {
IIpsGenerationStrategy selectGenerationStrategy(@Nullable String theRequestedProfile) {
return myGenerationStrategies.stream()
.filter(t -> isBlank(theRequestedProfile) || theRequestedProfile.equals(t.getBundleProfile()))
.findFirst()
.orElse(myGenerationStrategies.get(0));
}
private IBaseBundle generateIpsForPatient(
IIpsGenerationStrategy theStrategy, RequestDetails theRequestDetails, IBaseResource thePatient) {
IIdType originalSubjectId = myFhirContext
.getVersion()
.newIdType()
.setValue(thePatient.getIdElement().getValue())
.toUnqualifiedVersionless();
massageResourceId(null, thePatient);
massageResourceId(theStrategy, theRequestDetails, null, thePatient);
IpsContext context = new IpsContext(thePatient, originalSubjectId);
ResourceInclusionCollection globalResourcesToInclude = new ResourceInclusionCollection();
globalResourcesToInclude.addResourceIfNotAlreadyPresent(thePatient, originalSubjectId.getValue());
IBaseResource author = myGenerationStrategy.createAuthor();
massageResourceId(context, author);
IBaseResource author = theStrategy.createAuthor();
massageResourceId(theStrategy, theRequestDetails, context, author);
CompositionBuilder compositionBuilder = createComposition(thePatient, context, author);
determineInclusions(
theRequestDetails, originalSubjectId, context, compositionBuilder, globalResourcesToInclude);
CompositionBuilder compositionBuilder = createComposition(theStrategy, thePatient, context, author);
determineInclusions(theStrategy, theRequestDetails, context, compositionBuilder, globalResourcesToInclude);
IBaseResource composition = compositionBuilder.getComposition();
// Create the narrative for the Composition itself
CustomThymeleafNarrativeGenerator generator = newNarrativeGenerator(globalResourcesToInclude);
CustomThymeleafNarrativeGenerator generator = newNarrativeGenerator(theStrategy, globalResourcesToInclude);
generator.populateResourceNarrative(myFhirContext, composition);
return createCompositionDocument(author, composition, globalResourcesToInclude);
return createDocumentBundleForComposition(theStrategy, author, composition, globalResourcesToInclude);
}
private IBaseBundle createCompositionDocument(
IBaseResource author, IBaseResource composition, ResourceInclusionCollection theResourcesToInclude) {
private IBaseBundle createDocumentBundleForComposition(
IIpsGenerationStrategy theStrategy,
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());
@ -162,124 +158,51 @@ public class IpsGeneratorSvcImpl implements IIpsGeneratorSvc {
// Add author to document
bundleBuilder.addDocumentEntry(author);
return bundleBuilder.getBundle();
IBaseBundle retVal = bundleBuilder.getBundle();
theStrategy.postManipulateIpsBundle(retVal);
return retVal;
}
@Nonnull
private ResourceInclusionCollection determineInclusions(
private void determineInclusions(
IIpsGenerationStrategy theStrategy,
RequestDetails theRequestDetails,
IIdType originalSubjectId,
IpsContext context,
IpsContext theIpsContext,
CompositionBuilder theCompositionBuilder,
ResourceInclusionCollection theGlobalResourcesToInclude) {
SectionRegistry sectionRegistry = myGenerationStrategy.getSectionRegistry();
for (SectionRegistry.Section nextSection : sectionRegistry.getSections()) {
for (Section nextSection : theStrategy.getSections()) {
determineInclusionsForSection(
theStrategy,
theRequestDetails,
originalSubjectId,
context,
theIpsContext,
theCompositionBuilder,
theGlobalResourcesToInclude,
nextSection);
}
return theGlobalResourcesToInclude;
}
private void determineInclusionsForSection(
IIpsGenerationStrategy theStrategy,
RequestDetails theRequestDetails,
IIdType theOriginalSubjectId,
IpsContext theIpsContext,
CompositionBuilder theCompositionBuilder,
ResourceInclusionCollection theGlobalResourcesToInclude,
SectionRegistry.Section theSection) {
ResourceInclusionCollection sectionResourcesToInclude = new ResourceInclusionCollection();
for (String nextResourceType : theSection.getResourceTypes()) {
ResourceInclusionCollection theGlobalResourceCollectionToPopulate,
Section theSection) {
ResourceInclusionCollection sectionResourceCollectionToPopulate = new ResourceInclusionCollection();
ISectionResourceSupplier resourceSupplier = theStrategy.getSectionResourceSupplier(theSection);
SearchParameterMap searchParameterMap = new SearchParameterMap();
String subjectSp = determinePatientCompartmentSearchParameterName(nextResourceType);
searchParameterMap.add(subjectSp, new ReferenceParam(theOriginalSubjectId));
determineInclusionsForSectionResourceTypes(
theStrategy,
theRequestDetails,
theIpsContext,
theGlobalResourceCollectionToPopulate,
theSection,
resourceSupplier,
sectionResourceCollectionToPopulate);
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 candidateIsSearchInclude = ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(nextCandidate)
== BundleEntrySearchModeEnum.INCLUDE;
boolean addResourceToBundle;
if (candidateIsSearchInclude) {
addResourceToBundle = true;
} else {
addResourceToBundle = myGenerationStrategy.shouldInclude(ipsSectionContext, nextCandidate);
}
if (addResourceToBundle) {
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 if (theGlobalResourcesToInclude.hasResourceWithReplacementId(originalResourceId)) {
if (!candidateIsSearchInclude) {
sectionResourcesToInclude.addResourceIfNotAlreadyPresent(
nextCandidate, originalResourceId);
}
} else {
IIdType id = myGenerationStrategy.massageResourceId(theIpsContext, nextCandidate);
nextCandidate.setId(id);
theGlobalResourcesToInclude.addResourceIfNotAlreadyPresent(
nextCandidate, originalResourceId);
if (!candidateIsSearchInclude) {
sectionResourcesToInclude.addResourceIfNotAlreadyPresent(
nextCandidate, originalResourceId);
}
}
}
}
}
}
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);
}
generateSectionNoInfoResourceIfNoInclusionsFound(
theIpsContext, theGlobalResourceCollectionToPopulate, theSection, sectionResourceCollectionToPopulate);
/*
* Update any references within the added candidates - This is important
@ -287,7 +210,23 @@ public class IpsGeneratorSvcImpl implements IIpsGeneratorSvc {
* the summary, so we need to also update the references to those
* resources.
*/
for (IBaseResource nextResource : theGlobalResourcesToInclude.getResources()) {
updateReferencesInInclusionsForSection(theGlobalResourceCollectionToPopulate);
if (sectionResourceCollectionToPopulate.isEmpty()) {
return;
}
addSection(
theStrategy,
theSection,
theCompositionBuilder,
sectionResourceCollectionToPopulate,
theGlobalResourceCollectionToPopulate);
}
private void updateReferencesInInclusionsForSection(
ResourceInclusionCollection theGlobalResourceCollectionToPopulate) {
for (IBaseResource nextResource : theGlobalResourceCollectionToPopulate.getResources()) {
List<ResourceReferenceInfo> references = myFhirContext.newTerser().getAllResourceReferences(nextResource);
for (ResourceReferenceInfo nextReference : references) {
String existingReference = nextReference
@ -298,12 +237,12 @@ public class IpsGeneratorSvcImpl implements IIpsGeneratorSvc {
existingReference = new IdType(existingReference)
.toUnqualifiedVersionless()
.getValue();
String replacement = theGlobalResourcesToInclude.getIdSubstitution(existingReference);
String replacement = theGlobalResourceCollectionToPopulate.getIdSubstitution(existingReference);
if (isNotBlank(replacement)) {
if (!replacement.equals(existingReference)) {
nextReference.getResourceReference().setReference(replacement);
}
} else if (theGlobalResourcesToInclude.getResourceById(existingReference) == null) {
} else if (theGlobalResourceCollectionToPopulate.getResourceById(existingReference) == null) {
// If this reference doesn't point to something we have actually
// included in the bundle, clear the reference.
nextReference.getResourceReference().setReference(null);
@ -312,17 +251,184 @@ public class IpsGeneratorSvcImpl implements IIpsGeneratorSvc {
}
}
}
}
if (sectionResourcesToInclude.isEmpty()) {
return;
private static void generateSectionNoInfoResourceIfNoInclusionsFound(
IpsContext theIpsContext,
ResourceInclusionCollection theGlobalResourceCollectionToPopulate,
Section theSection,
ResourceInclusionCollection sectionResourceCollectionToPopulate) {
if (sectionResourceCollectionToPopulate.isEmpty() && theSection.getNoInfoGenerator() != null) {
IBaseResource noInfoResource = theSection.getNoInfoGenerator().generate(theIpsContext.getSubjectId());
String id = IdType.newRandomUuid().getValue();
if (noInfoResource.getIdElement().isEmpty()) {
noInfoResource.setId(id);
}
noInfoResource.setUserData(
RESOURCE_ENTRY_INCLUSION_TYPE, ISectionResourceSupplier.InclusionTypeEnum.PRIMARY_RESOURCE);
theGlobalResourceCollectionToPopulate.addResourceIfNotAlreadyPresent(
noInfoResource,
noInfoResource.getIdElement().toUnqualifiedVersionless().getValue());
sectionResourceCollectionToPopulate.addResourceIfNotAlreadyPresent(noInfoResource, id);
}
}
private void determineInclusionsForSectionResourceTypes(
IIpsGenerationStrategy theStrategy,
RequestDetails theRequestDetails,
IpsContext theIpsContext,
ResourceInclusionCollection theGlobalResourceCollectionToPopulate,
Section theSection,
ISectionResourceSupplier resourceSupplier,
ResourceInclusionCollection sectionResourceCollectionToPopulate) {
for (Class<? extends IBaseResource> nextResourceType : theSection.getResourceTypes()) {
determineInclusionsForSectionResourceType(
theStrategy,
theRequestDetails,
theIpsContext,
theGlobalResourceCollectionToPopulate,
theSection,
nextResourceType,
resourceSupplier,
sectionResourceCollectionToPopulate);
}
}
private <T extends IBaseResource> void determineInclusionsForSectionResourceType(
IIpsGenerationStrategy theStrategy,
RequestDetails theRequestDetails,
IpsContext theIpsContext,
ResourceInclusionCollection theGlobalResourceCollectionToPopulate,
Section theSection,
Class<T> nextResourceType,
ISectionResourceSupplier resourceSupplier,
ResourceInclusionCollection sectionResourceCollectionToPopulate) {
IpsSectionContext<T> ipsSectionContext = theIpsContext.newSectionContext(theSection, nextResourceType);
List<ISectionResourceSupplier.ResourceEntry> resources =
resourceSupplier.fetchResourcesForSection(theIpsContext, ipsSectionContext, theRequestDetails);
if (resources != null) {
for (ISectionResourceSupplier.ResourceEntry nextEntry : resources) {
IBaseResource resource = nextEntry.getResource();
Validate.isTrue(
resource.getIdElement().hasIdPart(),
"fetchResourcesForSection(..) returned resource(s) with no ID populated");
resource.setUserData(RESOURCE_ENTRY_INCLUSION_TYPE, nextEntry.getInclusionType());
}
addResourcesToIpsContents(
theStrategy,
theRequestDetails,
theIpsContext,
resources,
theGlobalResourceCollectionToPopulate,
sectionResourceCollectionToPopulate);
}
}
/**
* Given a collection of resources that have been fetched, analyze them and add them as appropriate
* to the collection that will be included in a given IPS section context.
*
* @param theStrategy The generation strategy
* @param theIpsContext The overall IPS generation context for this IPS.
* @param theCandidateResources The resources that have been fetched for inclusion in the IPS bundle
*/
private void addResourcesToIpsContents(
IIpsGenerationStrategy theStrategy,
RequestDetails theRequestDetails,
IpsContext theIpsContext,
List<ISectionResourceSupplier.ResourceEntry> theCandidateResources,
ResourceInclusionCollection theGlobalResourcesCollectionToPopulate,
ResourceInclusionCollection theSectionResourceCollectionToPopulate) {
for (ISectionResourceSupplier.ResourceEntry nextCandidateEntry : theCandidateResources) {
if (nextCandidateEntry.getInclusionType() == ISectionResourceSupplier.InclusionTypeEnum.EXCLUDE) {
continue;
}
IBaseResource nextCandidate = nextCandidateEntry.getResource();
boolean primaryResource = nextCandidateEntry.getInclusionType()
== ISectionResourceSupplier.InclusionTypeEnum.PRIMARY_RESOURCE;
String originalResourceId =
nextCandidate.getIdElement().toUnqualifiedVersionless().getValue();
// Check if we already have this resource included so that we don't
// include it twice
IBaseResource previouslyExistingResource =
theGlobalResourcesCollectionToPopulate.getResourceByOriginalId(originalResourceId);
if (previouslyExistingResource != null) {
reuseAlreadyIncludedGlobalResourceInSectionCollection(
theSectionResourceCollectionToPopulate,
previouslyExistingResource,
primaryResource,
originalResourceId);
} else if (theGlobalResourcesCollectionToPopulate.hasResourceWithReplacementId(originalResourceId)) {
addResourceToSectionCollectionOnlyIfPrimary(
theSectionResourceCollectionToPopulate, primaryResource, nextCandidate, originalResourceId);
} else {
addResourceToGlobalCollectionAndSectionCollection(
theStrategy,
theRequestDetails,
theIpsContext,
theGlobalResourcesCollectionToPopulate,
theSectionResourceCollectionToPopulate,
nextCandidate,
originalResourceId,
primaryResource);
}
}
}
private static void addResourceToSectionCollectionOnlyIfPrimary(
ResourceInclusionCollection theSectionResourceCollectionToPopulate,
boolean primaryResource,
IBaseResource nextCandidate,
String originalResourceId) {
if (primaryResource) {
theSectionResourceCollectionToPopulate.addResourceIfNotAlreadyPresent(nextCandidate, originalResourceId);
}
}
private void addResourceToGlobalCollectionAndSectionCollection(
IIpsGenerationStrategy theStrategy,
RequestDetails theRequestDetails,
IpsContext theIpsContext,
ResourceInclusionCollection theGlobalResourcesCollectionToPopulate,
ResourceInclusionCollection theSectionResourceCollectionToPopulate,
IBaseResource nextCandidate,
String originalResourceId,
boolean primaryResource) {
massageResourceId(theStrategy, theRequestDetails, theIpsContext, nextCandidate);
theGlobalResourcesCollectionToPopulate.addResourceIfNotAlreadyPresent(nextCandidate, originalResourceId);
addResourceToSectionCollectionOnlyIfPrimary(
theSectionResourceCollectionToPopulate, primaryResource, nextCandidate, originalResourceId);
}
private static void reuseAlreadyIncludedGlobalResourceInSectionCollection(
ResourceInclusionCollection theSectionResourceCollectionToPopulate,
IBaseResource previouslyExistingResource,
boolean primaryResource,
String originalResourceId) {
IBaseResource nextCandidate;
ISectionResourceSupplier.InclusionTypeEnum previouslyIncludedResourceInclusionType =
(ISectionResourceSupplier.InclusionTypeEnum)
previouslyExistingResource.getUserData(RESOURCE_ENTRY_INCLUSION_TYPE);
if (previouslyIncludedResourceInclusionType != ISectionResourceSupplier.InclusionTypeEnum.PRIMARY_RESOURCE) {
if (primaryResource) {
previouslyExistingResource.setUserData(
RESOURCE_ENTRY_INCLUSION_TYPE, ISectionResourceSupplier.InclusionTypeEnum.PRIMARY_RESOURCE);
}
}
addSection(theSection, theCompositionBuilder, sectionResourcesToInclude, theGlobalResourcesToInclude);
nextCandidate = previouslyExistingResource;
theSectionResourceCollectionToPopulate.addResourceIfNotAlreadyPresent(nextCandidate, originalResourceId);
}
@SuppressWarnings("unchecked")
private void addSection(
SectionRegistry.Section theSection,
IIpsGenerationStrategy theStrategy,
Section theSection,
CompositionBuilder theCompositionBuilder,
ResourceInclusionCollection theResourcesToInclude,
ResourceInclusionCollection theGlobalResourcesToInclude) {
@ -330,34 +436,44 @@ public class IpsGeneratorSvcImpl implements IIpsGeneratorSvc {
CompositionBuilder.SectionBuilder sectionBuilder = theCompositionBuilder.addSection();
sectionBuilder.setTitle(theSection.getTitle());
sectionBuilder.addCodeCoding(LOINC_URI, theSection.getSectionCode(), theSection.getSectionDisplay());
sectionBuilder.addCodeCoding(
theSection.getSectionSystem(), theSection.getSectionCode(), theSection.getSectionDisplay());
for (IBaseResource next : theResourcesToInclude.getResources()) {
if (ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(next) == BundleEntrySearchModeEnum.INCLUDE) {
ISectionResourceSupplier.InclusionTypeEnum inclusionType =
(ISectionResourceSupplier.InclusionTypeEnum) next.getUserData(RESOURCE_ENTRY_INCLUSION_TYPE);
if (inclusionType != ISectionResourceSupplier.InclusionTypeEnum.PRIMARY_RESOURCE) {
continue;
}
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("url").newInstance();
narrativeLinkUri.setValueAsString(narrativeLinkValue);
narrativeLink.setValue(narrativeLinkUri);
IBaseHasExtensions extensionHolder = (IBaseHasExtensions) next;
if (extensionHolder.getExtension().stream()
.noneMatch(t -> t.getUrl().equals(URL_NARRATIVE_LINK))) {
IBaseExtension<?, ?> narrativeLink = extensionHolder.addExtension();
narrativeLink.setUrl(URL_NARRATIVE_LINK);
String narrativeLinkValue =
theCompositionBuilder.getComposition().getIdElement().getValue()
+ "#"
+ myFhirContext.getResourceType(next)
+ "-"
+ next.getIdElement().getValue();
IPrimitiveType<String> narrativeLinkUri =
(IPrimitiveType<String>) requireNonNull(myFhirContext.getElementDefinition("url"))
.newInstance();
narrativeLinkUri.setValueAsString(narrativeLinkValue);
narrativeLink.setValue(narrativeLinkUri);
}
sectionBuilder.addEntry(next.getIdElement());
}
String narrative = createSectionNarrative(theSection, theResourcesToInclude, theGlobalResourcesToInclude);
String narrative =
createSectionNarrative(theStrategy, theSection, theResourcesToInclude, theGlobalResourcesToInclude);
sectionBuilder.setText("generated", narrative);
}
private CompositionBuilder createComposition(IBaseResource thePatient, IpsContext context, IBaseResource author) {
private CompositionBuilder createComposition(
IIpsGenerationStrategy theStrategy, IBaseResource thePatient, IpsContext context, IBaseResource author) {
CompositionBuilder compositionBuilder = new CompositionBuilder(myFhirContext);
compositionBuilder.setId(IdType.newRandomUuid());
@ -365,43 +481,44 @@ public class IpsGeneratorSvcImpl implements IIpsGeneratorSvc {
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.setTitle(theStrategy.createTitle(context));
compositionBuilder.setConfidentiality(theStrategy.createConfidentiality(context));
compositionBuilder.addAuthor(author.getIdElement());
return compositionBuilder;
}
private String determinePatientCompartmentSearchParameterName(String theResourceType) {
RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(theResourceType);
Set<String> searchParams = resourceDef.getSearchParamsForCompartmentName("Patient").stream()
.map(RuntimeSearchParam::getName)
.collect(Collectors.toSet());
// Prefer "patient", then "subject" then anything else
if (searchParams.contains(Observation.SP_PATIENT)) {
return Observation.SP_PATIENT;
}
if (searchParams.contains(Observation.SP_SUBJECT)) {
return Observation.SP_SUBJECT;
}
return searchParams.iterator().next();
}
private void massageResourceId(
IIpsGenerationStrategy theStrategy,
RequestDetails theRequestDetails,
IpsContext theIpsContext,
IBaseResource theResource) {
String base = theRequestDetails.getFhirServerBase();
private void massageResourceId(IpsContext theIpsContext, IBaseResource theResource) {
IIdType id = myGenerationStrategy.massageResourceId(theIpsContext, theResource);
theResource.setId(id);
IIdType id = theResource.getIdElement();
if (!id.hasBaseUrl() && id.hasResourceType() && id.hasIdPart()) {
id = id.withServerBase(base, id.getResourceType());
theResource.setId(id);
}
id = theStrategy.massageResourceId(theIpsContext, theResource);
if (id != null) {
theResource.setId(id);
}
}
private String createSectionNarrative(
SectionRegistry.Section theSection,
IIpsGenerationStrategy theStrategy,
Section theSection,
ResourceInclusionCollection theResources,
ResourceInclusionCollection theGlobalResourceCollection) {
CustomThymeleafNarrativeGenerator generator = newNarrativeGenerator(theGlobalResourceCollection);
CustomThymeleafNarrativeGenerator generator = newNarrativeGenerator(theStrategy, theGlobalResourceCollection);
Bundle bundle = new Bundle();
for (IBaseResource resource : theResources.getResources()) {
BundleEntrySearchModeEnum searchMode = ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(resource);
if (searchMode == BundleEntrySearchModeEnum.MATCH) {
ISectionResourceSupplier.InclusionTypeEnum inclusionType =
(ISectionResourceSupplier.InclusionTypeEnum) resource.getUserData(RESOURCE_ENTRY_INCLUSION_TYPE);
if (inclusionType == ISectionResourceSupplier.InclusionTypeEnum.PRIMARY_RESOURCE) {
bundle.addEntry().setResource((Resource) resource);
}
}
@ -414,14 +531,13 @@ public class IpsGeneratorSvcImpl implements IIpsGeneratorSvc {
@Nonnull
private CustomThymeleafNarrativeGenerator newNarrativeGenerator(
ResourceInclusionCollection theGlobalResourceCollection) {
List<String> narrativePropertyFiles = myGenerationStrategy.getNarrativePropertyFiles();
IIpsGenerationStrategy theStrategy, ResourceInclusionCollection theGlobalResourceCollection) {
List<String> narrativePropertyFiles = theStrategy.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 theGlobalResourceCollection.getResourceById(theReference);
}
});
return generator;

View File

@ -0,0 +1,477 @@
/*-
* #%L
* HAPI FHIR JPA Server - International Patient Summary (IPS)
* %%
* Copyright (C) 2014 - 2024 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%
*/
package ca.uhn.fhir.jpa.ips.jpa;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.ips.api.Section;
import ca.uhn.fhir.jpa.ips.jpa.section.AdvanceDirectivesJpaSectionSearchStrategy;
import ca.uhn.fhir.jpa.ips.jpa.section.AllergyIntoleranceJpaSectionSearchStrategy;
import ca.uhn.fhir.jpa.ips.jpa.section.DiagnosticResultsJpaSectionSearchStrategyDiagnosticReport;
import ca.uhn.fhir.jpa.ips.jpa.section.DiagnosticResultsJpaSectionSearchStrategyObservation;
import ca.uhn.fhir.jpa.ips.jpa.section.FunctionalStatusJpaSectionSearchStrategy;
import ca.uhn.fhir.jpa.ips.jpa.section.IllnessHistoryJpaSectionSearchStrategy;
import ca.uhn.fhir.jpa.ips.jpa.section.ImmunizationsJpaSectionSearchStrategy;
import ca.uhn.fhir.jpa.ips.jpa.section.MedicalDevicesJpaSectionSearchStrategy;
import ca.uhn.fhir.jpa.ips.jpa.section.MedicationSummaryJpaSectionSearchStrategyMedicationAdministration;
import ca.uhn.fhir.jpa.ips.jpa.section.MedicationSummaryJpaSectionSearchStrategyMedicationDispense;
import ca.uhn.fhir.jpa.ips.jpa.section.MedicationSummaryJpaSectionSearchStrategyMedicationRequest;
import ca.uhn.fhir.jpa.ips.jpa.section.MedicationSummaryJpaSectionSearchStrategyMedicationStatement;
import ca.uhn.fhir.jpa.ips.jpa.section.PlanOfCareJpaSectionSearchStrategy;
import ca.uhn.fhir.jpa.ips.jpa.section.PregnancyJpaSectionSearchStrategy;
import ca.uhn.fhir.jpa.ips.jpa.section.ProblemListJpaSectionSearchStrategy;
import ca.uhn.fhir.jpa.ips.jpa.section.ProceduresJpaSectionSearchStrategy;
import ca.uhn.fhir.jpa.ips.jpa.section.SocialHistoryJpaSectionSearchStrategy;
import ca.uhn.fhir.jpa.ips.jpa.section.VitalSignsJpaSectionSearchStrategy;
import ca.uhn.fhir.jpa.ips.strategy.AllergyIntoleranceNoInfoR4Generator;
import ca.uhn.fhir.jpa.ips.strategy.BaseIpsGenerationStrategy;
import ca.uhn.fhir.jpa.ips.strategy.MedicationNoInfoR4Generator;
import ca.uhn.fhir.jpa.ips.strategy.ProblemNoInfoR4Generator;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.term.api.ITermLoaderSvc;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.util.ValidateUtil;
import jakarta.annotation.Nonnull;
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.CarePlan;
import org.hl7.fhir.r4.model.ClinicalImpression;
import org.hl7.fhir.r4.model.Condition;
import org.hl7.fhir.r4.model.Consent;
import org.hl7.fhir.r4.model.DeviceUseStatement;
import org.hl7.fhir.r4.model.DiagnosticReport;
import org.hl7.fhir.r4.model.Immunization;
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.Patient;
import org.hl7.fhir.r4.model.Procedure;
import org.springframework.beans.factory.annotation.Autowired;
import org.thymeleaf.util.Validate;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.function.Function;
/**
* This {@link ca.uhn.fhir.jpa.ips.api.IIpsGenerationStrategy generation strategy} contains default rules for fetching
* IPS section contents for each of the base (universal realm) IPS definition sections. It fetches contents for each
* section from the JPA server repository.
* <p>
* This class can be used directly, but it can also be subclassed and extended if you want to
* create an IPS strategy that is based on the defaults but add or change the inclusion rules or
* sections. If you are subclassing this class, the typical approach is to override the
* {@link #addSections()} method and replace it with your own implementation. You can include
* any of the same sections that are defined in the parent class, but you can also omit any
* you don't want to include, and add your own as well.
* </p>
*/
public class DefaultJpaIpsGenerationStrategy extends BaseIpsGenerationStrategy {
public static final String SECTION_CODE_ALLERGY_INTOLERANCE = "48765-2";
public static final String SECTION_CODE_MEDICATION_SUMMARY = "10160-0";
public static final String SECTION_CODE_PROBLEM_LIST = "11450-4";
public static final String SECTION_CODE_IMMUNIZATIONS = "11369-6";
public static final String SECTION_CODE_PROCEDURES = "47519-4";
public static final String SECTION_CODE_MEDICAL_DEVICES = "46264-8";
public static final String SECTION_CODE_DIAGNOSTIC_RESULTS = "30954-2";
public static final String SECTION_CODE_VITAL_SIGNS = "8716-3";
public static final String SECTION_CODE_PREGNANCY = "10162-6";
public static final String SECTION_CODE_SOCIAL_HISTORY = "29762-2";
public static final String SECTION_CODE_ILLNESS_HISTORY = "11348-0";
public static final String SECTION_CODE_FUNCTIONAL_STATUS = "47420-5";
public static final String SECTION_CODE_PLAN_OF_CARE = "18776-5";
public static final String SECTION_CODE_ADVANCE_DIRECTIVES = "42348-3";
public static final String SECTION_SYSTEM_LOINC = ITermLoaderSvc.LOINC_URI;
private final List<Function<Section, Section>> myGlobalSectionCustomizers = new ArrayList<>();
@Autowired
private DaoRegistry myDaoRegistry;
@Autowired
private FhirContext myFhirContext;
private boolean myInitialized;
public void setDaoRegistry(DaoRegistry theDaoRegistry) {
myDaoRegistry = theDaoRegistry;
}
public void setFhirContext(FhirContext theFhirContext) {
myFhirContext = theFhirContext;
}
/**
* Subclasses may call this method to add customers that will customize every section
* added to the strategy.
*/
public void addGlobalSectionCustomizer(@Nonnull Function<Section, Section> theCustomizer) {
Validate.isTrue(!myInitialized, "This method must not be called after the strategy is initialized");
Validate.notNull(theCustomizer, "theCustomizer must not be null");
myGlobalSectionCustomizers.add(theCustomizer);
}
@Override
public final void initialize() {
Validate.isTrue(!myInitialized, "Strategy must not be initialized twice");
Validate.isTrue(myDaoRegistry != null, "No DaoRegistry has been supplied");
Validate.isTrue(myFhirContext != null, "No FhirContext has been supplied");
addSections();
myInitialized = true;
}
@Nonnull
@Override
public IBaseResource fetchPatient(IIdType thePatientId, RequestDetails theRequestDetails) {
return myDaoRegistry.getResourceDao("Patient").read(thePatientId, theRequestDetails);
}
@Nonnull
@Override
public IBaseResource fetchPatient(TokenParam thePatientIdentifier, RequestDetails theRequestDetails) {
SearchParameterMap searchParameterMap =
new SearchParameterMap().setLoadSynchronousUpTo(2).add(Patient.SP_IDENTIFIER, thePatientIdentifier);
IBundleProvider searchResults =
myDaoRegistry.getResourceDao("Patient").search(searchParameterMap, theRequestDetails);
ValidateUtil.isTrueOrThrowResourceNotFound(
searchResults.sizeOrThrowNpe() > 0, "No Patient could be found matching given identifier");
ValidateUtil.isTrueOrThrowInvalidRequest(
searchResults.sizeOrThrowNpe() == 1, "Multiple Patient resources were found matching given identifier");
return searchResults.getResources(0, 1).get(0);
}
/**
* Add the various sections to the registry in order. This method can be overridden for
* customization.
*/
protected void addSections() {
addJpaSectionAllergyIntolerance();
addJpaSectionMedicationSummary();
addJpaSectionProblemList();
addJpaSectionImmunizations();
addJpaSectionProcedures();
addJpaSectionMedicalDevices();
addJpaSectionDiagnosticResults();
addJpaSectionVitalSigns();
addJpaSectionPregnancy();
addJpaSectionSocialHistory();
addJpaSectionIllnessHistory();
addJpaSectionFunctionalStatus();
addJpaSectionPlanOfCare();
addJpaSectionAdvanceDirectives();
}
protected void addJpaSectionAllergyIntolerance() {
Section section = Section.newBuilder()
.withTitle("Allergies and Intolerances")
.withSectionSystem(SECTION_SYSTEM_LOINC)
.withSectionSystem(SECTION_SYSTEM_LOINC)
.withSectionCode(SECTION_CODE_ALLERGY_INTOLERANCE)
.withSectionDisplay("Allergies and adverse reactions Document")
.withResourceType(AllergyIntolerance.class)
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionAllergies")
.withNoInfoGenerator(new AllergyIntoleranceNoInfoR4Generator())
.build();
JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder()
.addStrategy(AllergyIntolerance.class, new AllergyIntoleranceJpaSectionSearchStrategy())
.build();
addJpaSection(section, searchStrategyCollection);
}
protected void addJpaSectionMedicationSummary() {
Section section = Section.newBuilder()
.withTitle("Medication List")
.withSectionSystem(SECTION_SYSTEM_LOINC)
.withSectionCode(SECTION_CODE_MEDICATION_SUMMARY)
.withSectionDisplay("History of Medication use Narrative")
.withResourceType(MedicationStatement.class)
.withResourceType(MedicationRequest.class)
.withResourceType(MedicationAdministration.class)
.withResourceType(MedicationDispense.class)
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionMedications")
.withNoInfoGenerator(new MedicationNoInfoR4Generator())
.build();
JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder()
.addStrategy(
MedicationAdministration.class,
new MedicationSummaryJpaSectionSearchStrategyMedicationAdministration())
.addStrategy(
MedicationDispense.class, new MedicationSummaryJpaSectionSearchStrategyMedicationDispense())
.addStrategy(MedicationRequest.class, new MedicationSummaryJpaSectionSearchStrategyMedicationRequest())
.addStrategy(
MedicationStatement.class, new MedicationSummaryJpaSectionSearchStrategyMedicationStatement())
.build();
addJpaSection(section, searchStrategyCollection);
}
protected void addJpaSectionProblemList() {
Section section = Section.newBuilder()
.withTitle("Problem List")
.withSectionSystem(SECTION_SYSTEM_LOINC)
.withSectionCode(SECTION_CODE_PROBLEM_LIST)
.withSectionDisplay("Problem list - Reported")
.withResourceType(Condition.class)
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionProblems")
.withNoInfoGenerator(new ProblemNoInfoR4Generator())
.build();
JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder()
.addStrategy(Condition.class, new ProblemListJpaSectionSearchStrategy())
.build();
addJpaSection(section, searchStrategyCollection);
}
protected void addJpaSectionImmunizations() {
Section section = Section.newBuilder()
.withTitle("History of Immunizations")
.withSectionSystem(SECTION_SYSTEM_LOINC)
.withSectionCode(SECTION_CODE_IMMUNIZATIONS)
.withSectionDisplay("History of Immunization Narrative")
.withResourceType(Immunization.class)
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionImmunizations")
.build();
JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder()
.addStrategy(Immunization.class, new ImmunizationsJpaSectionSearchStrategy())
.build();
addJpaSection(section, searchStrategyCollection);
}
protected void addJpaSectionProcedures() {
Section section = Section.newBuilder()
.withTitle("History of Procedures")
.withSectionSystem(SECTION_SYSTEM_LOINC)
.withSectionCode(SECTION_CODE_PROCEDURES)
.withSectionDisplay("History of Procedures Document")
.withResourceType(Procedure.class)
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionProceduresHx")
.build();
JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder()
.addStrategy(Procedure.class, new ProceduresJpaSectionSearchStrategy())
.build();
addJpaSection(section, searchStrategyCollection);
}
protected void addJpaSectionMedicalDevices() {
Section section = Section.newBuilder()
.withTitle("Medical Devices")
.withSectionSystem(SECTION_SYSTEM_LOINC)
.withSectionCode(SECTION_CODE_MEDICAL_DEVICES)
.withSectionDisplay("History of medical device use")
.withResourceType(DeviceUseStatement.class)
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionMedicalDevices")
.build();
JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder()
.addStrategy(DeviceUseStatement.class, new MedicalDevicesJpaSectionSearchStrategy())
.build();
addJpaSection(section, searchStrategyCollection);
}
protected void addJpaSectionDiagnosticResults() {
Section section = Section.newBuilder()
.withTitle("Diagnostic Results")
.withSectionSystem(SECTION_SYSTEM_LOINC)
.withSectionCode(SECTION_CODE_DIAGNOSTIC_RESULTS)
.withSectionDisplay("Relevant diagnostic tests/laboratory data Narrative")
.withResourceType(DiagnosticReport.class)
.withResourceType(Observation.class)
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionResults")
.build();
JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder()
.addStrategy(DiagnosticReport.class, new DiagnosticResultsJpaSectionSearchStrategyDiagnosticReport())
.addStrategy(Observation.class, new DiagnosticResultsJpaSectionSearchStrategyObservation())
.build();
addJpaSection(section, searchStrategyCollection);
}
protected void addJpaSectionVitalSigns() {
Section section = Section.newBuilder()
.withTitle("Vital Signs")
.withSectionSystem(SECTION_SYSTEM_LOINC)
.withSectionCode(SECTION_CODE_VITAL_SIGNS)
.withSectionDisplay("Vital signs")
.withResourceType(Observation.class)
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionVitalSigns")
.build();
JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder()
.addStrategy(Observation.class, new VitalSignsJpaSectionSearchStrategy())
.build();
addJpaSection(section, searchStrategyCollection);
}
protected void addJpaSectionPregnancy() {
Section section = Section.newBuilder()
.withTitle("Pregnancy Information")
.withSectionSystem(SECTION_SYSTEM_LOINC)
.withSectionCode(SECTION_CODE_PREGNANCY)
.withSectionDisplay("History of pregnancies Narrative")
.withResourceType(Observation.class)
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionPregnancyHx")
.build();
JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder()
.addStrategy(Observation.class, new PregnancyJpaSectionSearchStrategy())
.build();
addJpaSection(section, searchStrategyCollection);
}
protected void addJpaSectionSocialHistory() {
Section section = Section.newBuilder()
.withTitle("Social History")
.withSectionSystem(SECTION_SYSTEM_LOINC)
.withSectionCode(SECTION_CODE_SOCIAL_HISTORY)
.withSectionDisplay("Social history Narrative")
.withResourceType(Observation.class)
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionSocialHistory")
.build();
JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder()
.addStrategy(Observation.class, new SocialHistoryJpaSectionSearchStrategy())
.build();
addJpaSection(section, searchStrategyCollection);
}
protected void addJpaSectionIllnessHistory() {
Section section = Section.newBuilder()
.withTitle("History of Past Illness")
.withSectionSystem(SECTION_SYSTEM_LOINC)
.withSectionCode(SECTION_CODE_ILLNESS_HISTORY)
.withSectionDisplay("History of Past illness Narrative")
.withResourceType(Condition.class)
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionPastIllnessHx")
.build();
JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder()
.addStrategy(Condition.class, new IllnessHistoryJpaSectionSearchStrategy())
.build();
addJpaSection(section, searchStrategyCollection);
}
protected void addJpaSectionFunctionalStatus() {
Section section = Section.newBuilder()
.withTitle("Functional Status")
.withSectionSystem(SECTION_SYSTEM_LOINC)
.withSectionCode(SECTION_CODE_FUNCTIONAL_STATUS)
.withSectionDisplay("Functional status assessment note")
.withResourceType(ClinicalImpression.class)
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionFunctionalStatus")
.build();
JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder()
.addStrategy(ClinicalImpression.class, new FunctionalStatusJpaSectionSearchStrategy())
.build();
addJpaSection(section, searchStrategyCollection);
}
protected void addJpaSectionPlanOfCare() {
Section section = Section.newBuilder()
.withTitle("Plan of Care")
.withSectionSystem(SECTION_SYSTEM_LOINC)
.withSectionCode(SECTION_CODE_PLAN_OF_CARE)
.withSectionDisplay("Plan of care note")
.withResourceType(CarePlan.class)
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionPlanOfCare")
.build();
JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder()
.addStrategy(CarePlan.class, new PlanOfCareJpaSectionSearchStrategy())
.build();
addJpaSection(section, searchStrategyCollection);
}
protected void addJpaSectionAdvanceDirectives() {
Section section = Section.newBuilder()
.withTitle("Advance Directives")
.withSectionSystem(SECTION_SYSTEM_LOINC)
.withSectionCode(SECTION_CODE_ADVANCE_DIRECTIVES)
.withSectionDisplay("Advance directives")
.withResourceType(Consent.class)
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionAdvanceDirectives")
.build();
JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder()
.addStrategy(Consent.class, new AdvanceDirectivesJpaSectionSearchStrategy())
.build();
addJpaSection(section, searchStrategyCollection);
}
protected void addJpaSection(
Section theSection, JpaSectionSearchStrategyCollection theSectionSearchStrategyCollection) {
Section section = theSection;
for (var next : myGlobalSectionCustomizers) {
section = next.apply(section);
}
Validate.isTrue(
theSection.getResourceTypes().size()
== theSectionSearchStrategyCollection.getResourceTypes().size(),
"Search strategy types does not match section types");
Validate.isTrue(
new HashSet<>(theSection.getResourceTypes())
.containsAll(theSectionSearchStrategyCollection.getResourceTypes()),
"Search strategy types does not match section types");
addSection(
section,
new JpaSectionResourceSupplier(theSectionSearchStrategyCollection, myDaoRegistry, myFhirContext));
}
}

View File

@ -0,0 +1,68 @@
/*-
* #%L
* HAPI FHIR JPA Server - International Patient Summary (IPS)
* %%
* Copyright (C) 2014 - 2024 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%
*/
package ca.uhn.fhir.jpa.ips.jpa;
import ca.uhn.fhir.jpa.ips.api.IpsSectionContext;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import jakarta.annotation.Nonnull;
import org.hl7.fhir.instance.model.api.IBaseResource;
/**
* Implementations of this interface are used to fetch resources to include
* for a given IPS section by performing a search in a local JPA repository.
*
* @since 7.2.0
*/
public interface IJpaSectionSearchStrategy<T extends IBaseResource> {
/**
* 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 (e.g. <code>?patient=Patient/123</code>), but no
* other parameters. This method can add other parameters. The default implementation of this
* interface performs no action.
* <p>
* For example, for a Vital Signs section, the implementation might add a parameter indicating
* the parameter <code>category=vital-signs</code>.
* </p>
*
* @param theIpsSectionContext The context, which indicates the IPS section and the resource type
* being searched for.
* @param theSearchParameterMap The map to manipulate.
*/
default void massageResourceSearch(
@Nonnull IpsSectionContext<T> theIpsSectionContext, @Nonnull SearchParameterMap theSearchParameterMap) {
// no action taken by default
}
/**
* 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. Note that the
* default implementation will always return {@literal true}.
* <p>
* This method is called once for every resource that is being considered for inclusion
* in an IPS section.
* </p>
*/
default boolean shouldInclude(@Nonnull IpsSectionContext<T> theIpsSectionContext, @Nonnull T theCandidate) {
return true;
}
}

View File

@ -0,0 +1,25 @@
/*-
* #%L
* HAPI FHIR JPA Server - International Patient Summary (IPS)
* %%
* Copyright (C) 2014 - 2024 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%
*/
package ca.uhn.fhir.jpa.ips.jpa;
import org.springframework.context.annotation.Configuration;
@Configuration
public class IpsGenerationCtxConfig {}

View File

@ -0,0 +1,128 @@
/*-
* #%L
* HAPI FHIR JPA Server - International Patient Summary (IPS)
* %%
* Copyright (C) 2014 - 2024 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%
*/
package ca.uhn.fhir.jpa.ips.jpa;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.ips.api.ISectionResourceSupplier;
import ca.uhn.fhir.jpa.ips.api.IpsContext;
import ca.uhn.fhir.jpa.ips.api.IpsSectionContext;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.model.dstu2.resource.Observation;
import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.ReferenceParam;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Coverage;
import org.thymeleaf.util.Validate;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class JpaSectionResourceSupplier implements ISectionResourceSupplier {
public static final int CHUNK_SIZE = 10;
private final JpaSectionSearchStrategyCollection mySectionSearchStrategyCollection;
private final DaoRegistry myDaoRegistry;
private final FhirContext myFhirContext;
public JpaSectionResourceSupplier(
@Nonnull JpaSectionSearchStrategyCollection theSectionSearchStrategyCollection,
@Nonnull DaoRegistry theDaoRegistry,
@Nonnull FhirContext theFhirContext) {
Validate.notNull(theSectionSearchStrategyCollection, "theSectionSearchStrategyCollection must not be null");
Validate.notNull(theDaoRegistry, "theDaoRegistry must not be null");
Validate.notNull(theFhirContext, "theFhirContext must not be null");
mySectionSearchStrategyCollection = theSectionSearchStrategyCollection;
myDaoRegistry = theDaoRegistry;
myFhirContext = theFhirContext;
}
@Nullable
@Override
public <T extends IBaseResource> List<ResourceEntry> fetchResourcesForSection(
IpsContext theIpsContext, IpsSectionContext<T> theIpsSectionContext, RequestDetails theRequestDetails) {
IJpaSectionSearchStrategy<T> searchStrategy =
mySectionSearchStrategyCollection.getSearchStrategy(theIpsSectionContext.getResourceType());
SearchParameterMap searchParameterMap = new SearchParameterMap();
String subjectSp = determinePatientCompartmentSearchParameterName(theIpsSectionContext.getResourceType());
searchParameterMap.add(subjectSp, new ReferenceParam(theIpsContext.getSubjectId()));
searchStrategy.massageResourceSearch(theIpsSectionContext, searchParameterMap);
IFhirResourceDao<T> dao = myDaoRegistry.getResourceDao(theIpsSectionContext.getResourceType());
IBundleProvider searchResult = dao.search(searchParameterMap, theRequestDetails);
List<ResourceEntry> retVal = null;
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 next : resources) {
if (!next.getClass().isAssignableFrom(theIpsSectionContext.getResourceType())
|| searchStrategy.shouldInclude(theIpsSectionContext, (T) next)) {
if (retVal == null) {
retVal = new ArrayList<>();
}
InclusionTypeEnum inclusionType =
ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(next) == BundleEntrySearchModeEnum.INCLUDE
? InclusionTypeEnum.SECONDARY_RESOURCE
: InclusionTypeEnum.PRIMARY_RESOURCE;
retVal.add(new ResourceEntry(next, inclusionType));
}
}
}
return retVal;
}
private String determinePatientCompartmentSearchParameterName(Class<? extends IBaseResource> theResourceType) {
RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(theResourceType);
Set<String> searchParams = resourceDef.getSearchParamsForCompartmentName("Patient").stream()
.map(RuntimeSearchParam::getName)
.collect(Collectors.toSet());
// A few we prefer
if (searchParams.contains(Observation.SP_PATIENT)) {
return Observation.SP_PATIENT;
}
if (searchParams.contains(Observation.SP_SUBJECT)) {
return Observation.SP_SUBJECT;
}
if (searchParams.contains(Coverage.SP_BENEFICIARY)) {
return Observation.SP_SUBJECT;
}
return searchParams.iterator().next();
}
}

View File

@ -17,21 +17,12 @@
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.jpa.ips.api;
package ca.uhn.fhir.jpa.ips.jpa;
import org.hl7.fhir.instance.model.api.IBaseResource;
public class JpaSectionSearchStrategy<T extends IBaseResource> implements IJpaSectionSearchStrategy<T> {
// nothing for now, interface has default methods
public enum IpsSectionEnum {
ALLERGY_INTOLERANCE,
MEDICATION_SUMMARY,
PROBLEM_LIST,
IMMUNIZATIONS,
PROCEDURES,
MEDICAL_DEVICES,
DIAGNOSTIC_RESULTS,
VITAL_SIGNS,
ILLNESS_HISTORY,
PREGNANCY,
SOCIAL_HISTORY,
FUNCTIONAL_STATUS,
PLAN_OF_CARE,
ADVANCE_DIRECTIVES
}

View File

@ -0,0 +1,62 @@
/*-
* #%L
* HAPI FHIR JPA Server - International Patient Summary (IPS)
* %%
* Copyright (C) 2014 - 2024 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%
*/
package ca.uhn.fhir.jpa.ips.jpa;
import org.hl7.fhir.instance.model.api.IBaseResource;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
public class JpaSectionSearchStrategyCollection {
private Map<Class<? extends IBaseResource>, Object> mySearchStrategies;
private JpaSectionSearchStrategyCollection(Map<Class<? extends IBaseResource>, Object> theSearchStrategies) {
mySearchStrategies = theSearchStrategies;
}
@SuppressWarnings("unchecked")
public <T extends IBaseResource> IJpaSectionSearchStrategy<T> getSearchStrategy(Class<T> theClass) {
return (IJpaSectionSearchStrategy<T>) mySearchStrategies.get(theClass);
}
public Collection<Class<? extends IBaseResource>> getResourceTypes() {
return mySearchStrategies.keySet();
}
public static JpaSectionSearchStrategyCollectionBuilder newBuilder() {
return new JpaSectionSearchStrategyCollectionBuilder();
}
public static class JpaSectionSearchStrategyCollectionBuilder {
private Map<Class<? extends IBaseResource>, Object> mySearchStrategies = new HashMap<>();
public <T extends IBaseResource> JpaSectionSearchStrategyCollectionBuilder addStrategy(
Class<T> theType, IJpaSectionSearchStrategy<T> theSearchStrategy) {
mySearchStrategies.put(theType, theSearchStrategy);
return this;
}
public JpaSectionSearchStrategyCollection build() {
return new JpaSectionSearchStrategyCollection(mySearchStrategies);
}
}
}

View File

@ -0,0 +1,41 @@
/*-
* #%L
* HAPI FHIR JPA Server - International Patient Summary (IPS)
* %%
* Copyright (C) 2014 - 2024 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%
*/
package ca.uhn.fhir.jpa.ips.jpa.section;
import ca.uhn.fhir.jpa.ips.api.IpsSectionContext;
import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.param.TokenOrListParam;
import ca.uhn.fhir.rest.param.TokenParam;
import jakarta.annotation.Nonnull;
import org.hl7.fhir.r4.model.Consent;
public class AdvanceDirectivesJpaSectionSearchStrategy extends JpaSectionSearchStrategy<Consent> {
@Override
public void massageResourceSearch(
@Nonnull IpsSectionContext theIpsSectionContext, @Nonnull SearchParameterMap theSearchParameterMap) {
theSearchParameterMap.add(
Consent.SP_STATUS,
new TokenOrListParam()
.addOr(new TokenParam(
Consent.ConsentState.ACTIVE.getSystem(), Consent.ConsentState.ACTIVE.toCode())));
}
}

View File

@ -0,0 +1,44 @@
/*-
* #%L
* HAPI FHIR JPA Server - International Patient Summary (IPS)
* %%
* Copyright (C) 2014 - 2024 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%
*/
package ca.uhn.fhir.jpa.ips.jpa.section;
import ca.uhn.fhir.jpa.ips.api.IpsSectionContext;
import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy;
import jakarta.annotation.Nonnull;
import org.hl7.fhir.r4.model.AllergyIntolerance;
public class AllergyIntoleranceJpaSectionSearchStrategy extends JpaSectionSearchStrategy<AllergyIntolerance> {
@Override
public boolean shouldInclude(
@Nonnull IpsSectionContext theIpsSectionContext, @Nonnull AllergyIntolerance theCandidate) {
return !theCandidate
.getClinicalStatus()
.hasCoding("http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical", "inactive")
&& !theCandidate
.getClinicalStatus()
.hasCoding("http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical", "resolved")
&& !theCandidate
.getVerificationStatus()
.hasCoding(
"http://terminology.hl7.org/CodeSystem/allergyintolerance-verification",
"entered-in-error");
}
}

View File

@ -0,0 +1,42 @@
/*-
* #%L
* HAPI FHIR JPA Server - International Patient Summary (IPS)
* %%
* Copyright (C) 2014 - 2024 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%
*/
package ca.uhn.fhir.jpa.ips.jpa.section;
import ca.uhn.fhir.jpa.ips.api.IpsSectionContext;
import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy;
import jakarta.annotation.Nonnull;
import org.hl7.fhir.r4.model.DiagnosticReport;
public class DiagnosticResultsJpaSectionSearchStrategyDiagnosticReport
extends JpaSectionSearchStrategy<DiagnosticReport> {
@SuppressWarnings("RedundantIfStatement")
@Override
public boolean shouldInclude(
@Nonnull IpsSectionContext theIpsSectionContext, @Nonnull DiagnosticReport theCandidate) {
if (theCandidate.getStatus() == DiagnosticReport.DiagnosticReportStatus.CANCELLED
|| theCandidate.getStatus() == DiagnosticReport.DiagnosticReportStatus.ENTEREDINERROR
|| theCandidate.getStatus() == DiagnosticReport.DiagnosticReportStatus.PRELIMINARY) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,56 @@
/*-
* #%L
* HAPI FHIR JPA Server - International Patient Summary (IPS)
* %%
* Copyright (C) 2014 - 2024 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%
*/
package ca.uhn.fhir.jpa.ips.jpa.section;
import ca.uhn.fhir.jpa.ips.api.IpsSectionContext;
import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.param.TokenOrListParam;
import ca.uhn.fhir.rest.param.TokenParam;
import jakarta.annotation.Nonnull;
import org.hl7.fhir.r4.model.Observation;
public class DiagnosticResultsJpaSectionSearchStrategyObservation extends JpaSectionSearchStrategy<Observation> {
@Override
public void massageResourceSearch(
@Nonnull IpsSectionContext<Observation> theIpsSectionContext,
@Nonnull SearchParameterMap theSearchParameterMap) {
theSearchParameterMap.add(
Observation.SP_CATEGORY,
new TokenOrListParam()
.addOr(new TokenParam(
"http://terminology.hl7.org/CodeSystem/observation-category", "laboratory")));
}
@SuppressWarnings("RedundantIfStatement")
@Override
public boolean shouldInclude(
@Nonnull IpsSectionContext<Observation> theIpsSectionContext, @Nonnull Observation theCandidate) {
// code filtering not yet applied
if (theCandidate.getStatus() == Observation.ObservationStatus.CANCELLED
|| theCandidate.getStatus() == Observation.ObservationStatus.ENTEREDINERROR
|| theCandidate.getStatus() == Observation.ObservationStatus.PRELIMINARY) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,40 @@
/*-
* #%L
* HAPI FHIR JPA Server - International Patient Summary (IPS)
* %%
* Copyright (C) 2014 - 2024 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%
*/
package ca.uhn.fhir.jpa.ips.jpa.section;
import ca.uhn.fhir.jpa.ips.api.IpsSectionContext;
import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy;
import jakarta.annotation.Nonnull;
import org.hl7.fhir.r4.model.ClinicalImpression;
public class FunctionalStatusJpaSectionSearchStrategy extends JpaSectionSearchStrategy<ClinicalImpression> {
@SuppressWarnings("RedundantIfStatement")
@Override
public boolean shouldInclude(
@Nonnull IpsSectionContext<ClinicalImpression> theIpsSectionContext,
@Nonnull ClinicalImpression theCandidate) {
if (theCandidate.getStatus() == ClinicalImpression.ClinicalImpressionStatus.INPROGRESS
|| theCandidate.getStatus() == ClinicalImpression.ClinicalImpressionStatus.ENTEREDINERROR) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,53 @@
/*-
* #%L
* HAPI FHIR JPA Server - International Patient Summary (IPS)
* %%
* Copyright (C) 2014 - 2024 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%
*/
package ca.uhn.fhir.jpa.ips.jpa.section;
import ca.uhn.fhir.jpa.ips.api.IpsSectionContext;
import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy;
import jakarta.annotation.Nonnull;
import org.hl7.fhir.r4.model.Condition;
public class IllnessHistoryJpaSectionSearchStrategy extends JpaSectionSearchStrategy<Condition> {
@SuppressWarnings("RedundantIfStatement")
@Override
public boolean shouldInclude(
@Nonnull IpsSectionContext<Condition> theIpsSectionContext, @Nonnull Condition theCandidate) {
if (theCandidate
.getVerificationStatus()
.hasCoding("http://terminology.hl7.org/CodeSystem/condition-ver-status", "entered-in-error")) {
return false;
}
if (theCandidate
.getClinicalStatus()
.hasCoding("http://terminology.hl7.org/CodeSystem/condition-clinical", "inactive")
|| theCandidate
.getClinicalStatus()
.hasCoding("http://terminology.hl7.org/CodeSystem/condition-clinical", "resolved")
|| theCandidate
.getClinicalStatus()
.hasCoding("http://terminology.hl7.org/CodeSystem/condition-clinical", "remission")) {
return true;
} else {
return false;
}
}
}

View File

@ -0,0 +1,50 @@
/*-
* #%L
* HAPI FHIR JPA Server - International Patient Summary (IPS)
* %%
* Copyright (C) 2014 - 2024 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%
*/
package ca.uhn.fhir.jpa.ips.jpa.section;
import ca.uhn.fhir.jpa.ips.api.IpsSectionContext;
import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.SortOrderEnum;
import ca.uhn.fhir.rest.api.SortSpec;
import jakarta.annotation.Nonnull;
import org.hl7.fhir.r4.model.Immunization;
public class ImmunizationsJpaSectionSearchStrategy extends JpaSectionSearchStrategy<Immunization> {
@Override
public void massageResourceSearch(
@Nonnull IpsSectionContext<Immunization> theIpsSectionContext,
@Nonnull SearchParameterMap theSearchParameterMap) {
theSearchParameterMap.setSort(new SortSpec(Immunization.SP_DATE).setOrder(SortOrderEnum.DESC));
theSearchParameterMap.addInclude(Immunization.INCLUDE_MANUFACTURER);
}
@SuppressWarnings("RedundantIfStatement")
@Override
public boolean shouldInclude(
@Nonnull IpsSectionContext<Immunization> theIpsSectionContext, @Nonnull Immunization theCandidate) {
if (theCandidate.getStatus() == Immunization.ImmunizationStatus.ENTEREDINERROR) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,47 @@
/*-
* #%L
* HAPI FHIR JPA Server - International Patient Summary (IPS)
* %%
* Copyright (C) 2014 - 2024 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%
*/
package ca.uhn.fhir.jpa.ips.jpa.section;
import ca.uhn.fhir.jpa.ips.api.IpsSectionContext;
import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import jakarta.annotation.Nonnull;
import org.hl7.fhir.r4.model.DeviceUseStatement;
public class MedicalDevicesJpaSectionSearchStrategy extends JpaSectionSearchStrategy<DeviceUseStatement> {
@Override
public void massageResourceSearch(
@Nonnull IpsSectionContext<DeviceUseStatement> theIpsSectionContext,
@Nonnull SearchParameterMap theSearchParameterMap) {
theSearchParameterMap.addInclude(DeviceUseStatement.INCLUDE_DEVICE);
}
@SuppressWarnings("RedundantIfStatement")
@Override
public boolean shouldInclude(
@Nonnull IpsSectionContext<DeviceUseStatement> theIpsSectionContext,
@Nonnull DeviceUseStatement theCandidate) {
if (theCandidate.getStatus() == DeviceUseStatement.DeviceUseStatementStatus.ENTEREDINERROR) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,51 @@
/*-
* #%L
* HAPI FHIR JPA Server - International Patient Summary (IPS)
* %%
* Copyright (C) 2014 - 2024 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%
*/
package ca.uhn.fhir.jpa.ips.jpa.section;
import ca.uhn.fhir.jpa.ips.api.IpsSectionContext;
import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.param.TokenOrListParam;
import ca.uhn.fhir.rest.param.TokenParam;
import jakarta.annotation.Nonnull;
import org.hl7.fhir.r4.model.MedicationAdministration;
public class MedicationSummaryJpaSectionSearchStrategyMedicationAdministration
extends JpaSectionSearchStrategy<MedicationAdministration> {
@Override
public void massageResourceSearch(
@Nonnull IpsSectionContext<MedicationAdministration> theIpsSectionContext,
@Nonnull SearchParameterMap theSearchParameterMap) {
theSearchParameterMap.addInclude(MedicationAdministration.INCLUDE_MEDICATION);
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())));
}
}

View File

@ -0,0 +1,51 @@
/*-
* #%L
* HAPI FHIR JPA Server - International Patient Summary (IPS)
* %%
* Copyright (C) 2014 - 2024 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%
*/
package ca.uhn.fhir.jpa.ips.jpa.section;
import ca.uhn.fhir.jpa.ips.api.IpsSectionContext;
import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.param.TokenOrListParam;
import ca.uhn.fhir.rest.param.TokenParam;
import jakarta.annotation.Nonnull;
import org.hl7.fhir.r4.model.MedicationDispense;
public class MedicationSummaryJpaSectionSearchStrategyMedicationDispense
extends JpaSectionSearchStrategy<MedicationDispense> {
@Override
public void massageResourceSearch(
@Nonnull IpsSectionContext<MedicationDispense> theIpsSectionContext,
@Nonnull SearchParameterMap theSearchParameterMap) {
theSearchParameterMap.addInclude(MedicationDispense.INCLUDE_MEDICATION);
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())));
}
}

View File

@ -0,0 +1,51 @@
/*-
* #%L
* HAPI FHIR JPA Server - International Patient Summary (IPS)
* %%
* Copyright (C) 2014 - 2024 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%
*/
package ca.uhn.fhir.jpa.ips.jpa.section;
import ca.uhn.fhir.jpa.ips.api.IpsSectionContext;
import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.param.TokenOrListParam;
import ca.uhn.fhir.rest.param.TokenParam;
import jakarta.annotation.Nonnull;
import org.hl7.fhir.r4.model.MedicationRequest;
public class MedicationSummaryJpaSectionSearchStrategyMedicationRequest
extends JpaSectionSearchStrategy<MedicationRequest> {
@Override
public void massageResourceSearch(
@Nonnull IpsSectionContext<MedicationRequest> theIpsSectionContext,
@Nonnull SearchParameterMap theSearchParameterMap) {
theSearchParameterMap.addInclude(MedicationRequest.INCLUDE_MEDICATION);
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())));
}
}

View File

@ -0,0 +1,54 @@
/*-
* #%L
* HAPI FHIR JPA Server - International Patient Summary (IPS)
* %%
* Copyright (C) 2014 - 2024 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%
*/
package ca.uhn.fhir.jpa.ips.jpa.section;
import ca.uhn.fhir.jpa.ips.api.IpsSectionContext;
import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.param.TokenOrListParam;
import ca.uhn.fhir.rest.param.TokenParam;
import jakarta.annotation.Nonnull;
import org.hl7.fhir.r4.model.MedicationStatement;
public class MedicationSummaryJpaSectionSearchStrategyMedicationStatement
extends JpaSectionSearchStrategy<MedicationStatement> {
@Override
public void massageResourceSearch(
@Nonnull IpsSectionContext<MedicationStatement> theIpsSectionContext,
@Nonnull SearchParameterMap theSearchParameterMap) {
theSearchParameterMap.addInclude(MedicationStatement.INCLUDE_MEDICATION);
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())));
}
}

View File

@ -0,0 +1,47 @@
/*-
* #%L
* HAPI FHIR JPA Server - International Patient Summary (IPS)
* %%
* Copyright (C) 2014 - 2024 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%
*/
package ca.uhn.fhir.jpa.ips.jpa.section;
import ca.uhn.fhir.jpa.ips.api.IpsSectionContext;
import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.param.TokenOrListParam;
import ca.uhn.fhir.rest.param.TokenParam;
import jakarta.annotation.Nonnull;
import org.hl7.fhir.r4.model.CarePlan;
public class PlanOfCareJpaSectionSearchStrategy extends JpaSectionSearchStrategy<CarePlan> {
@Override
public void massageResourceSearch(
@Nonnull IpsSectionContext<CarePlan> theIpsSectionContext,
@Nonnull SearchParameterMap theSearchParameterMap) {
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())));
}
}

View File

@ -0,0 +1,75 @@
/*-
* #%L
* HAPI FHIR JPA Server - International Patient Summary (IPS)
* %%
* Copyright (C) 2014 - 2024 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%
*/
package ca.uhn.fhir.jpa.ips.jpa.section;
import ca.uhn.fhir.jpa.ips.api.IpsSectionContext;
import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.param.TokenOrListParam;
import ca.uhn.fhir.rest.param.TokenParam;
import jakarta.annotation.Nonnull;
import org.hl7.fhir.r4.model.Observation;
import static ca.uhn.fhir.jpa.term.api.ITermLoaderSvc.LOINC_URI;
public class PregnancyJpaSectionSearchStrategy extends JpaSectionSearchStrategy<Observation> {
public static final String LOINC_CODE_PREGNANCY_STATUS = "82810-3";
public static final String LOINC_CODE_NUMBER_BIRTHS_LIVE = "11636-8";
public static final String LOINC_CODE_NUMBER_BIRTHS_PRETERM = "11637-6";
public static final String LOINC_CODE_NUMBER_BIRTHS_STILL_LIVING = "11638-4";
public static final String LOINC_CODE_NUMBER_BIRTHS_TERM = "11639-2";
public static final String LOINC_CODE_NUMBER_BIRTHS_TOTAL = "11640-0";
public static final String LOINC_CODE_NUMBER_ABORTIONS = "11612-9";
public static final String LOINC_CODE_NUMBER_ABORTIONS_INDUCED = "11613-7";
public static final String LOINC_CODE_NUMBER_ABORTIONS_SPONTANEOUS = "11614-5";
public static final String LOINC_CODE_NUMBER_ECTOPIC_PREGNANCY = "33065-4";
@Override
public void massageResourceSearch(
@Nonnull IpsSectionContext<Observation> theIpsSectionContext,
@Nonnull SearchParameterMap theSearchParameterMap) {
theSearchParameterMap.add(
Observation.SP_CODE,
new TokenOrListParam()
.addOr(new TokenParam(LOINC_URI, LOINC_CODE_PREGNANCY_STATUS))
.addOr(new TokenParam(LOINC_URI, LOINC_CODE_NUMBER_BIRTHS_LIVE))
.addOr(new TokenParam(LOINC_URI, LOINC_CODE_NUMBER_BIRTHS_PRETERM))
.addOr(new TokenParam(LOINC_URI, LOINC_CODE_NUMBER_BIRTHS_STILL_LIVING))
.addOr(new TokenParam(LOINC_URI, LOINC_CODE_NUMBER_BIRTHS_TERM))
.addOr(new TokenParam(LOINC_URI, LOINC_CODE_NUMBER_BIRTHS_TOTAL))
.addOr(new TokenParam(LOINC_URI, LOINC_CODE_NUMBER_ABORTIONS))
.addOr(new TokenParam(LOINC_URI, LOINC_CODE_NUMBER_ABORTIONS_INDUCED))
.addOr(new TokenParam(LOINC_URI, LOINC_CODE_NUMBER_ABORTIONS_SPONTANEOUS))
.addOr(new TokenParam(LOINC_URI, LOINC_CODE_NUMBER_ECTOPIC_PREGNANCY)));
}
@SuppressWarnings("RedundantIfStatement")
@Override
public boolean shouldInclude(
@Nonnull IpsSectionContext<Observation> theIpsSectionContext, @Nonnull Observation theCandidate) {
// code filtering not yet applied
if (theCandidate.getStatus() == Observation.ObservationStatus.PRELIMINARY) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,47 @@
/*-
* #%L
* HAPI FHIR JPA Server - International Patient Summary (IPS)
* %%
* Copyright (C) 2014 - 2024 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%
*/
package ca.uhn.fhir.jpa.ips.jpa.section;
import ca.uhn.fhir.jpa.ips.api.IpsSectionContext;
import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy;
import jakarta.annotation.Nonnull;
import org.hl7.fhir.r4.model.Condition;
public class ProblemListJpaSectionSearchStrategy extends JpaSectionSearchStrategy<Condition> {
@SuppressWarnings("RedundantIfStatement")
@Override
public boolean shouldInclude(
@Nonnull IpsSectionContext<Condition> theIpsSectionContext, @Nonnull Condition theCandidate) {
if (theCandidate
.getClinicalStatus()
.hasCoding("http://terminology.hl7.org/CodeSystem/condition-clinical", "inactive")
|| theCandidate
.getClinicalStatus()
.hasCoding("http://terminology.hl7.org/CodeSystem/condition-clinical", "resolved")
|| theCandidate
.getVerificationStatus()
.hasCoding("http://terminology.hl7.org/CodeSystem/condition-ver-status", "entered-in-error")) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,40 @@
/*-
* #%L
* HAPI FHIR JPA Server - International Patient Summary (IPS)
* %%
* Copyright (C) 2014 - 2024 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%
*/
package ca.uhn.fhir.jpa.ips.jpa.section;
import ca.uhn.fhir.jpa.ips.api.IpsSectionContext;
import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy;
import jakarta.annotation.Nonnull;
import org.hl7.fhir.r4.model.Procedure;
public class ProceduresJpaSectionSearchStrategy extends JpaSectionSearchStrategy<Procedure> {
@SuppressWarnings("RedundantIfStatement")
@Override
public boolean shouldInclude(
@Nonnull IpsSectionContext<Procedure> theIpsSectionContext, @Nonnull Procedure theCandidate) {
if (theCandidate.getStatus() == Procedure.ProcedureStatus.ENTEREDINERROR
|| theCandidate.getStatus() == Procedure.ProcedureStatus.NOTDONE) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,54 @@
/*-
* #%L
* HAPI FHIR JPA Server - International Patient Summary (IPS)
* %%
* Copyright (C) 2014 - 2024 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%
*/
package ca.uhn.fhir.jpa.ips.jpa.section;
import ca.uhn.fhir.jpa.ips.api.IpsSectionContext;
import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.param.TokenOrListParam;
import ca.uhn.fhir.rest.param.TokenParam;
import jakarta.annotation.Nonnull;
import org.hl7.fhir.r4.model.Observation;
public class SocialHistoryJpaSectionSearchStrategy extends JpaSectionSearchStrategy<Observation> {
@Override
public void massageResourceSearch(
@Nonnull IpsSectionContext<Observation> theIpsSectionContext,
@Nonnull SearchParameterMap theSearchParameterMap) {
theSearchParameterMap.add(
Observation.SP_CATEGORY,
new TokenOrListParam()
.addOr(new TokenParam(
"http://terminology.hl7.org/CodeSystem/observation-category", "social-history")));
}
@SuppressWarnings("RedundantIfStatement")
@Override
public boolean shouldInclude(
@Nonnull IpsSectionContext<Observation> theIpsSectionContext, @Nonnull Observation theCandidate) {
// code filtering not yet applied
if (theCandidate.getStatus() == Observation.ObservationStatus.PRELIMINARY) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,51 @@
/*-
* #%L
* HAPI FHIR JPA Server - International Patient Summary (IPS)
* %%
* Copyright (C) 2014 - 2024 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%
*/
package ca.uhn.fhir.jpa.ips.jpa.section;
import ca.uhn.fhir.jpa.ips.api.IpsSectionContext;
import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.param.TokenOrListParam;
import ca.uhn.fhir.rest.param.TokenParam;
import jakarta.annotation.Nonnull;
import org.hl7.fhir.r4.model.Observation;
public class VitalSignsJpaSectionSearchStrategy extends JpaSectionSearchStrategy<Observation> {
@Override
public void massageResourceSearch(
@Nonnull IpsSectionContext<Observation> theIpsSectionContext,
@Nonnull SearchParameterMap theSearchParameterMap) {
theSearchParameterMap.add(
Observation.SP_CATEGORY,
new TokenOrListParam()
.addOr(new TokenParam(
"http://terminology.hl7.org/CodeSystem/observation-category", "vital-signs")));
}
@Override
public boolean shouldInclude(
@Nonnull IpsSectionContext<Observation> theIpsSectionContext, @Nonnull Observation theCandidate) {
// code filtering not yet applied
return theCandidate.getStatus() != Observation.ObservationStatus.CANCELLED
&& theCandidate.getStatus() != Observation.ObservationStatus.ENTEREDINERROR
&& theCandidate.getStatus() != Observation.ObservationStatus.PRELIMINARY;
}
}

View File

@ -28,8 +28,14 @@ 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 ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.ValidateUtil;
import jakarta.annotation.Nonnull;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.thymeleaf.util.Validate;
public class IpsOperationProvider {
@ -38,7 +44,8 @@ public class IpsOperationProvider {
/**
* Constructor
*/
public IpsOperationProvider(IIpsGeneratorSvc theIpsGeneratorSvc) {
public IpsOperationProvider(@Nonnull IIpsGeneratorSvc theIpsGeneratorSvc) {
Validate.notNull(theIpsGeneratorSvc, "theIpsGeneratorSvc must not be null");
myIpsGeneratorSvc = theIpsGeneratorSvc;
}
@ -54,9 +61,12 @@ public class IpsOperationProvider {
bundleType = BundleTypeEnum.DOCUMENT,
typeName = "Patient",
canonicalUrl = JpaConstants.SUMMARY_OPERATION_URL)
public IBaseBundle patientInstanceSummary(@IdParam IIdType thePatientId, RequestDetails theRequestDetails) {
return myIpsGeneratorSvc.generateIps(theRequestDetails, thePatientId);
public IBaseBundle patientInstanceSummary(
@IdParam IIdType thePatientId,
@OperationParam(name = "profile", min = 0, typeName = "uri") IPrimitiveType<String> theProfile,
RequestDetails theRequestDetails) {
String profile = theProfile != null ? theProfile.getValueAsString() : null;
return myIpsGeneratorSvc.generateIps(theRequestDetails, thePatientId, profile);
}
/**
@ -72,12 +82,20 @@ public class IpsOperationProvider {
typeName = "Patient",
canonicalUrl = JpaConstants.SUMMARY_OPERATION_URL)
public IBaseBundle patientTypeSummary(
@OperationParam(name = "profile", min = 0, typeName = "uri") IPrimitiveType<String> theProfile,
@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,
@OperationParam(name = "identifier", min = 1, max = 1, typeName = "Identifier")
IBase thePatientIdentifier,
RequestDetails theRequestDetails) {
return myIpsGeneratorSvc.generateIps(theRequestDetails, thePatientIdentifier);
String profile = theProfile != null ? theProfile.getValueAsString() : null;
ValidateUtil.isTrueOrThrowInvalidRequest(thePatientIdentifier != null, "No ID or identifier supplied");
FhirTerser terser = theRequestDetails.getFhirContext().newTerser();
String system = terser.getSinglePrimitiveValueOrNull(thePatientIdentifier, "system");
String value = terser.getSinglePrimitiveValueOrNull(thePatientIdentifier, "value");
return myIpsGeneratorSvc.generateIps(theRequestDetails, new TokenParam(system, value), profile);
}
}

View File

@ -0,0 +1,46 @@
/*-
* #%L
* HAPI FHIR JPA Server - International Patient Summary (IPS)
* %%
* Copyright (C) 2014 - 2024 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%
*/
package ca.uhn.fhir.jpa.ips.strategy;
import ca.uhn.fhir.jpa.ips.api.INoInfoGenerator;
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.Reference;
public 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;
}
}

View File

@ -0,0 +1,130 @@
/*-
* #%L
* HAPI FHIR JPA Server - International Patient Summary (IPS)
* %%
* Copyright (C) 2014 - 2024 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%
*/
package ca.uhn.fhir.jpa.ips.strategy;
import ca.uhn.fhir.jpa.ips.api.IIpsGenerationStrategy;
import ca.uhn.fhir.jpa.ips.api.ISectionResourceSupplier;
import ca.uhn.fhir.jpa.ips.api.IpsContext;
import ca.uhn.fhir.jpa.ips.api.Section;
import com.google.common.collect.Lists;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Address;
import org.hl7.fhir.r4.model.Composition;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Organization;
import org.thymeleaf.util.Validate;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@SuppressWarnings({"HttpUrlsUsage"})
public abstract class BaseIpsGenerationStrategy implements IIpsGenerationStrategy {
public static final String DEFAULT_IPS_NARRATIVES_PROPERTIES =
"classpath:ca/uhn/fhir/jpa/ips/narrative/ips-narratives.properties";
private final List<Section> mySections = new ArrayList<>();
private final Map<Section, ISectionResourceSupplier> mySectionToResourceSupplier = new HashMap<>();
/**
* Constructor
*/
public BaseIpsGenerationStrategy() {
super();
}
@Override
public String getBundleProfile() {
return "http://hl7.org/fhir/uv/ips/StructureDefinition/Bundle-uv-ips";
}
@Nonnull
@Override
public final List<Section> getSections() {
return Collections.unmodifiableList(mySections);
}
@Nonnull
@Override
public ISectionResourceSupplier getSectionResourceSupplier(@Nonnull Section theSection) {
return mySectionToResourceSupplier.get(theSection);
}
/**
* This should be called once per section to add a section for inclusion in generated IPS documents.
* It should include a {@link Section} which contains static details about the section, and a {@link ISectionResourceSupplier}
* which is used to fetch resources for inclusion at runtime.
*
* @param theSection Contains static details about the section, such as the resource types it can contain, and a title.
* @param theSectionResourceSupplier The strategy object which will be used to supply content for this section at runtime.
*/
public void addSection(Section theSection, ISectionResourceSupplier theSectionResourceSupplier) {
Validate.notNull(theSection, "theSection must not be null");
Validate.notNull(theSectionResourceSupplier, "theSectionResourceSupplier must not be null");
Validate.isTrue(
!mySectionToResourceSupplier.containsKey(theSection),
"A section with the given profile already exists");
mySections.add(theSection);
mySectionToResourceSupplier.put(theSection, theSectionResourceSupplier);
}
@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 null;
}
}

View File

@ -1,446 +0,0 @@
/*-
* #%L
* HAPI FHIR JPA Server - International Patient Summary (IPS)
* %%
* Copyright (C) 2014 - 2024 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%
*/
package ca.uhn.fhir.jpa.ips.strategy;
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.api.SortOrderEnum;
import ca.uhn.fhir.rest.api.SortSpec;
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 jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.*;
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 PROCEDURES:
case MEDICAL_DEVICES:
case ILLNESS_HISTORY:
case FUNCTIONAL_STATUS:
return;
case IMMUNIZATIONS:
theSearchParameterMap.setSort(new SortSpec(Immunization.SP_DATE).setOrder(SortOrderEnum.DESC));
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 IMMUNIZATIONS:
if (ResourceType.Immunization.name().equals(theIpsSectionContext.getResourceType())) {
return Sets.newHashSet(Immunization.INCLUDE_MANUFACTURER);
}
break;
case ALLERGY_INTOLERANCE:
case PROBLEM_LIST:
case PROCEDURES:
case DIAGNOSTIC_RESULTS:
case VITAL_SIGNS:
case ILLNESS_HISTORY:
case PREGNANCY:
case SOCIAL_HISTORY:
case FUNCTIONAL_STATUS:
case PLAN_OF_CARE:
case ADVANCE_DIRECTIVES:
break;
}
return Collections.emptySet();
}
@SuppressWarnings("EnhancedSwitchMigration")
@Override
public boolean shouldInclude(IpsContext.IpsSectionContext theIpsSectionContext, IBaseResource theCandidate) {
switch (theIpsSectionContext.getSection()) {
case MEDICATION_SUMMARY:
case PLAN_OF_CARE:
case ADVANCE_DIRECTIVES:
return true;
case ALLERGY_INTOLERANCE:
if (theIpsSectionContext.getResourceType().equals(ResourceType.AllergyIntolerance.name())) {
AllergyIntolerance allergyIntolerance = (AllergyIntolerance) theCandidate;
return !allergyIntolerance
.getClinicalStatus()
.hasCoding(
"http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical",
"inactive")
&& !allergyIntolerance
.getClinicalStatus()
.hasCoding(
"http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical",
"resolved")
&& !allergyIntolerance
.getVerificationStatus()
.hasCoding(
"http://terminology.hl7.org/CodeSystem/allergyintolerance-verification",
"entered-in-error");
}
break;
case PROBLEM_LIST:
if (theIpsSectionContext.getResourceType().equals(ResourceType.Condition.name())) {
Condition prob = (Condition) theCandidate;
return !prob.getClinicalStatus()
.hasCoding("http://terminology.hl7.org/CodeSystem/condition-clinical", "inactive")
&& !prob.getClinicalStatus()
.hasCoding("http://terminology.hl7.org/CodeSystem/condition-clinical", "resolved")
&& !prob.getVerificationStatus()
.hasCoding(
"http://terminology.hl7.org/CodeSystem/condition-ver-status",
"entered-in-error");
}
break;
case IMMUNIZATIONS:
if (theIpsSectionContext.getResourceType().equals(ResourceType.Immunization.name())) {
Immunization immunization = (Immunization) theCandidate;
return immunization.getStatus() != Immunization.ImmunizationStatus.ENTEREDINERROR;
}
break;
case PROCEDURES:
if (theIpsSectionContext.getResourceType().equals(ResourceType.Procedure.name())) {
Procedure proc = (Procedure) theCandidate;
return proc.getStatus() != Procedure.ProcedureStatus.ENTEREDINERROR
&& proc.getStatus() != Procedure.ProcedureStatus.NOTDONE;
}
break;
case MEDICAL_DEVICES:
if (theIpsSectionContext.getResourceType().equals(ResourceType.DeviceUseStatement.name())) {
DeviceUseStatement deviceUseStatement = (DeviceUseStatement) theCandidate;
return deviceUseStatement.getStatus() != DeviceUseStatement.DeviceUseStatementStatus.ENTEREDINERROR;
}
return true;
case DIAGNOSTIC_RESULTS:
if (theIpsSectionContext.getResourceType().equals(ResourceType.DiagnosticReport.name())) {
return true;
}
if (theIpsSectionContext.getResourceType().equals(ResourceType.Observation.name())) {
// code filtering not yet applied
Observation observation = (Observation) theCandidate;
return (observation.getStatus() != Observation.ObservationStatus.PRELIMINARY);
}
break;
case VITAL_SIGNS:
if (theIpsSectionContext.getResourceType().equals(ResourceType.Observation.name())) {
// code filtering not yet applied
Observation observation = (Observation) theCandidate;
return (observation.getStatus() != Observation.ObservationStatus.PRELIMINARY);
}
break;
case ILLNESS_HISTORY:
if (theIpsSectionContext.getResourceType().equals(ResourceType.Condition.name())) {
Condition prob = (Condition) theCandidate;
if (prob.getVerificationStatus()
.hasCoding(
"http://terminology.hl7.org/CodeSystem/condition-ver-status", "entered-in-error")) {
return false;
} else {
return prob.getClinicalStatus()
.hasCoding(
"http://terminology.hl7.org/CodeSystem/condition-clinical", "inactive")
|| prob.getClinicalStatus()
.hasCoding(
"http://terminology.hl7.org/CodeSystem/condition-clinical", "resolved")
|| prob.getClinicalStatus()
.hasCoding(
"http://terminology.hl7.org/CodeSystem/condition-clinical",
"remission");
}
}
break;
case PREGNANCY:
if (theIpsSectionContext.getResourceType().equals(ResourceType.Observation.name())) {
// code filtering not yet applied
Observation observation = (Observation) theCandidate;
return (observation.getStatus() != Observation.ObservationStatus.PRELIMINARY);
}
break;
case SOCIAL_HISTORY:
if (theIpsSectionContext.getResourceType().equals(ResourceType.Observation.name())) {
// code filtering not yet applied
Observation observation = (Observation) theCandidate;
return (observation.getStatus() != Observation.ObservationStatus.PRELIMINARY);
}
break;
case FUNCTIONAL_STATUS:
if (theIpsSectionContext.getResourceType().equals(ResourceType.ClinicalImpression.name())) {
ClinicalImpression clinicalImpression = (ClinicalImpression) theCandidate;
return clinicalImpression.getStatus() != ClinicalImpression.ClinicalImpressionStatus.INPROGRESS
&& clinicalImpression.getStatus()
!= ClinicalImpression.ClinicalImpressionStatus.ENTEREDINERROR;
}
break;
}
return true;
}
}

View File

@ -0,0 +1,45 @@
/*-
* #%L
* HAPI FHIR JPA Server - International Patient Summary (IPS)
* %%
* Copyright (C) 2014 - 2024 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%
*/
package ca.uhn.fhir.jpa.ips.strategy;
import ca.uhn.fhir.jpa.ips.api.INoInfoGenerator;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.CodeableConcept;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.MedicationStatement;
import org.hl7.fhir.r4.model.Reference;
public 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);
return medication;
}
}

View File

@ -0,0 +1,47 @@
/*-
* #%L
* HAPI FHIR JPA Server - International Patient Summary (IPS)
* %%
* Copyright (C) 2014 - 2024 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%
*/
package ca.uhn.fhir.jpa.ips.strategy;
import ca.uhn.fhir.jpa.ips.api.INoInfoGenerator;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
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.Reference;
public 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;
}
}

View File

@ -6,8 +6,8 @@ Action Controlled: Consent.provision.action[x].{ text || coding[x].display (sepa
Date: Consent.dateTime
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<h5>Advance Directives</h5>
<table class="hapiPropertyTable">
<caption>Advance Directives</caption>
<thead>
<tr>
<th>Scope</th>
@ -21,9 +21,9 @@ Date: Consent.dateTime
<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:insert="~{IpsUtilityFragments :: codeableConcept (cc=*{getScope()},attr='display')}">Scope</td>
<td th:text="*{getStatus().getDisplay()}">Status</td>
<td th:insert="IpsUtilityFragments :: concatCodeableConcept (list=*{getProvision().getAction()})">Action Controlled</td>
<td th:insert="~{IpsUtilityFragments :: concatCodeableConcept (list=*{getProvision().getAction()})}">Action Controlled</td>
<td th:text="*{getDateTimeElement().getValue()}">Date</td>
</tr>
</th:block>

View File

@ -8,8 +8,8 @@ Severity: AllergyIntolerance.reaction.severity[x].code (separated by <br />)
Comments: AllergyIntolerance.note[x].text (separated by <br />)
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<h5>Allergies And Intolerances</h5>
<table class="hapiPropertyTable">
<caption>Allergies And Intolerances</caption>
<thead>
<tr>
<th>Allergen</th>
@ -27,10 +27,10 @@ Comments: AllergyIntolerance.note[x].text (separated by <br />)
<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: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>
<th:block th:if="*{hasOnsetDateTimeType()}">
<td th:text="*{getOnsetDateTimeType().getValue()}">Onset</td>

View File

@ -14,61 +14,75 @@ Code: DiagnosticReport.code.text || DiagnosticReport.code.coding[x].display (sep
Date: DiagnosticReport.effectiveDateTime || DiagnosticReport.effectivePeriod.start
*/-->
<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>
<th:block th:if="${narrativeUtil.bundleHasEntriesWithResourceType(resource, 'Observation')}">
<h5>Diagnostic Results: Observations</h5>
<table class="hapiPropertyTable">
<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>
</th:block>
<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>
<th:block th:if="${narrativeUtil.bundleHasEntriesWithResourceType(resource, 'DiagnosticReport')}">
<h5>Diagnostic Results: Diagnostic Reports</h5>
<table class="hapiPropertyTable">
<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>
</th:block>
</div>

View File

@ -7,8 +7,8 @@ Comments: ClinicalImpression.note[x].text (separated by <br />)
Date: ClinicalImpression.effectiveDateTime || ClinicalImpression.effectivePeriod.start
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<h5>Functional Status</h5>
<table class="hapiPropertyTable">
<caption>Functional Status</caption>
<thead>
<tr>
<th>Assessment</th>
@ -23,11 +23,11 @@ Date: ClinicalImpression.effectiveDateTime || ClinicalImpression.effectivePeriod
<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: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>
<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>

View File

@ -5,8 +5,8 @@ Comments: Procedure.note[x].text(separated by <br />)
Date: Procedure.performedDateTime || Procedure.performedPeriod.start && “-“ && Procedure.performedPeriod.end || Procedure.performedAge || Procedure.performedRange.low && “-“ && Procedure.performedRange.high || Procedure.performedString
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<h5>History Of Procedures</h5>
<table class="hapiPropertyTable">
<caption>History Of Procedures</caption>
<thead>
<tr>
<th>Procedure</th>
@ -19,9 +19,9 @@ Date: Procedure.performedDateTime || Procedure.performedPeriod.start && “-“
<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="IpsUtilityFragments :: renderPerformed (performed=*{getPerformed()})">Date</td>
<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="~{IpsUtilityFragments :: renderPerformed (performed=*{getPerformed()})}">Date</td>
</tr>
</th:block>
</th:block>

View File

@ -9,8 +9,8 @@ Comments: Immunization.note[x].text (separated by <br />)
Date: Immunization.occurrenceDateTime || Immunization.occurrenceString
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<h5>Immunizations</h5>
<table class="hapiPropertyTable">
<caption>Immunizations</caption>
<thead>
<tr>
<th>Immunization</th>
@ -27,13 +27,13 @@ Date: Immunization.occurrenceDateTime || Immunization.occurrenceString
<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: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: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>
<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>

View File

@ -6,8 +6,8 @@ Comments: DeviceUseStatement.note[x].text (separated by <br />)
Date Recorded: DeviceUseStatement.recordedDateTime
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<h5>Medical Devices</h5>
<table class="hapiPropertyTable">
<caption>Medical Devices</caption>
<thead>
<tr>
<th>Device</th>
@ -21,10 +21,10 @@ Date Recorded: DeviceUseStatement.recordedDateTime
<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: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>
<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>

View File

@ -16,63 +16,78 @@ Sig: MedicationStatement.dosage[x].text (display all sigs separated by <br />)
Date: MedicationStatement.effectiveDateTime || MedicationStatement.effectivePeriod.start
*/-->
<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>
<th:block th:if="${narrativeUtil.bundleHasEntriesWithResourceType(resource, 'MedicationRequest')}">
<h5>Medication Summary: Medication Requests</h5>
<table class="hapiPropertyTable">
<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>
</th:block>
<th:block th:if="${narrativeUtil.bundleHasEntriesWithResourceType(resource, 'MedicationStatement')}">
<h5>Medication Summary: Medication Statements</h5>
<table class="hapiPropertyTable">
<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>
</th:block>
</div>

View File

@ -6,8 +6,8 @@ Comments: Condition.note[x].text (separated by <br />)
Onset Date: Condition.onsetDateTime || Condition.onsetPeriod.start && “-“ && Condition.onsetPeriod.end || Condition.onsetAge || Condition.onsetRange.low && “-“ && Condition.onsetRange.high || Condition.onsetString
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<h5>Past History of Illnesses</h5>
<table class="hapiPropertyTable">
<caption>Past History of Illnesses</caption>
<thead>
<tr>
<th>Medical Problems</th>
@ -21,10 +21,10 @@ Onset Date: Condition.onsetDateTime || Condition.onsetPeriod.start && “-“ &&
<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=*{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>
<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>

View File

@ -7,8 +7,8 @@ Planned Start: CarePlan.period.start
Planned End: CarePlan.period.end
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<h5>Plan of Care</h5>
<table class="hapiPropertyTable">
<caption>Plan of Care</caption>
<thead>
<tr>
<th>Activity</th>
@ -25,7 +25,7 @@ Planned End: CarePlan.period.end
<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: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>

View File

@ -6,8 +6,8 @@ Comments: Observation.note[x].text (separated by <br />)
Date: Observation.effectiveDateTime || Observation.effectivePeriod.start
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<h5>Pregnancy</h5>
<table class="hapiPropertyTable">
<caption>Pregnancy</caption>
<thead>
<tr>
<th>Code</th>
@ -21,10 +21,10 @@ Date: Observation.effectiveDateTime || Observation.effectivePeriod.start
<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>
<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>

View File

@ -6,8 +6,8 @@ Comments: Condition.note[x].text (separated by <br />)
Onset Date: Condition.onsetDateTime || Condition.onsetPeriod.start && “-“ && Condition.onsetPeriod.end || Condition.onsetAge || Condition.onsetRange.low && “-“ && Condition.onsetRange.high || Condition.onsetString
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<h5>Problem List</h5>
<table class="hapiPropertyTable">
<caption>Problem List</caption>
<thead>
<tr>
<th>Medical Problems</th>
@ -21,10 +21,10 @@ Onset Date: Condition.onsetDateTime || Condition.onsetPeriod.start && “-“ &&
<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=*{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>
<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>

View File

@ -7,8 +7,8 @@ Comments: Observation.note[x].text (separated by <br />)
Date: Observation.effectiveDateTime || Observation.effectivePeriod.start
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<h5>Social History</h5>
<table class="hapiPropertyTable">
<caption>Social History</caption>
<thead>
<tr>
<th>Code</th>
@ -23,11 +23,11 @@ Date: Observation.effectiveDateTime || Observation.effectivePeriod.start
<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>
<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>

View File

@ -8,8 +8,8 @@ Comments: Observation.note[x].text (separated by <br />)
Date: Observation.effectiveDateTime || Observation.effectivePeriod.start
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<h5>Vital Signs</h5>
<table class="hapiPropertyTable">
<caption>Vital Signs</caption>
<thead>
<tr>
<th>Code</th>
@ -25,12 +25,12 @@ Date: Observation.effectiveDateTime || Observation.effectivePeriod.start
<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>
<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>

View File

@ -7,14 +7,17 @@ import ca.uhn.fhir.context.support.ValidationSupportContext;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.ips.api.IIpsGenerationStrategy;
import ca.uhn.fhir.jpa.ips.jpa.DefaultJpaIpsGenerationStrategy;
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 ca.uhn.fhir.util.ResourceReferenceInfo;
import ca.uhn.fhir.validation.FhirValidator;
import ca.uhn.fhir.validation.ResultSeverityEnum;
import ca.uhn.fhir.validation.SingleValidationMessage;
import ca.uhn.fhir.validation.ValidationResult;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain;
import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator;
import org.hl7.fhir.instance.model.api.IBaseResource;
@ -38,19 +41,17 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.ContextConfiguration;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.matchesPattern;
import static org.hamcrest.Matchers.stringContainsInOrder;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
/**
* This test uses a complete R4 JPA server as a backend and wires the
@ -98,8 +99,8 @@ public class IpsGenerationR4Test extends BaseResourceProviderR4Test {
// Verify
validateDocument(output);
assertEquals(117, output.getEntry().size());
String patientId = findFirstEntryResource(output, Patient.class, 1).getId();
assertThat(patientId, matchesPattern("urn:uuid:.*"));
String patientId = findFirstEntryResource(output, Patient.class, 1).getIdElement().toUnqualifiedVersionless().getValue();
assertEquals("Patient/f15d2419-fbff-464a-826d-0afe8f095771", patientId);
MedicationStatement medicationStatement = findFirstEntryResource(output, MedicationStatement.class, 2);
assertEquals(patientId, medicationStatement.getSubject().getReference());
assertNull(medicationStatement.getInformationSource().getReference());
@ -185,8 +186,8 @@ public class IpsGenerationR4Test extends BaseResourceProviderR4Test {
// Verify
validateDocument(output);
assertEquals(7, output.getEntry().size());
String patientId = findFirstEntryResource(output, Patient.class, 1).getId();
assertThat(patientId, matchesPattern("urn:uuid:.*"));
String patientId = findFirstEntryResource(output, Patient.class, 1).getIdElement().toUnqualifiedVersionless().getValue();
assertEquals("Patient/5342998", patientId);
assertEquals(patientId, findEntryResource(output, Condition.class, 0, 2).getSubject().getReference());
assertEquals(patientId, findEntryResource(output, Condition.class, 1, 2).getSubject().getReference());
@ -279,18 +280,9 @@ public class IpsGenerationR4Test extends BaseResourceProviderR4Test {
instanceValidator.setValidationSupport(new ValidationSupportChain(new IpsTerminologySvc(), myFhirContext.getValidationSupport()));
validator.registerValidatorModule(instanceValidator);
ValidationResult validation = validator.validateWithResult(theOutcome);
assertTrue(validation.isSuccessful(), () -> myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(validation.toOperationOutcome()));
// Make sure that all refs have been replaced with UUIDs
List<ResourceReferenceInfo> references = myFhirContext.newTerser().getAllResourceReferences(theOutcome);
for (IBaseResource next : myFhirContext.newTerser().getAllEmbeddedResources(theOutcome, true)) {
references.addAll(myFhirContext.newTerser().getAllResourceReferences(next));
}
for (ResourceReferenceInfo next : references) {
if (!next.getResourceReference().getReferenceElement().getValue().startsWith("urn:uuid:")) {
fail(next.getName());
}
}
Optional<SingleValidationMessage> failure = validation.getMessages().stream().filter(t -> t.getSeverity().ordinal() >= ResultSeverityEnum.ERROR.ordinal()).findFirst();
assertFalse(failure.isPresent(), () -> failure.get().toString());
}
@Configuration
@ -298,12 +290,12 @@ public class IpsGenerationR4Test extends BaseResourceProviderR4Test {
@Bean
public IIpsGenerationStrategy ipsGenerationStrategy() {
return new DefaultIpsGenerationStrategy();
return new DefaultJpaIpsGenerationStrategy();
}
@Bean
public IIpsGeneratorSvc ipsGeneratorSvc(FhirContext theFhirContext, IIpsGenerationStrategy theGenerationStrategy, DaoRegistry theDaoRegistry) {
return new IpsGeneratorSvcImpl(theFhirContext, theGenerationStrategy, theDaoRegistry);
return new IpsGeneratorSvcImpl(theFhirContext, theGenerationStrategy);
}
@Bean
@ -314,7 +306,6 @@ public class IpsGenerationR4Test extends BaseResourceProviderR4Test {
}
@SuppressWarnings("unchecked")
private static <T extends IBaseResource> T findFirstEntryResource(Bundle theBundle, Class<T> theType, int theExpectedCount) {
return findEntryResource(theBundle, theType, 0, theExpectedCount);
}

View File

@ -3,9 +3,10 @@ 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.jpa.ips.api.IIpsGenerationStrategy;
import ca.uhn.fhir.jpa.ips.api.IpsContext;
import ca.uhn.fhir.jpa.ips.api.Section;
import ca.uhn.fhir.jpa.ips.jpa.DefaultJpaIpsGenerationStrategy;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
@ -15,13 +16,11 @@ 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 org.htmlunit.html.DomElement;
import org.htmlunit.html.DomNodeList;
import org.htmlunit.html.HtmlPage;
import org.htmlunit.html.HtmlTable;
import org.htmlunit.html.HtmlTableRow;
import com.google.common.collect.Lists;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
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.Bundle;
import org.hl7.fhir.r4.model.CarePlan;
@ -61,11 +60,11 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jakarta.annotation.Nonnull;
import java.io.IOException;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import static ca.uhn.fhir.jpa.ips.generator.IpsGenerationR4Test.findEntryResource;
@ -75,6 +74,7 @@ import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
@ -114,23 +114,44 @@ public class IpsGeneratorSvcImplTest {
private final FhirContext myFhirContext = FhirContext.forR4Cached();
private final DaoRegistry myDaoRegistry = new DaoRegistry(myFhirContext);
private IIpsGeneratorSvc mySvc;
private DefaultIpsGenerationStrategy myStrategy;
private DefaultJpaIpsGenerationStrategy myStrategy;
@BeforeEach
public void beforeEach() {
myDaoRegistry.setResourceDaos(Collections.emptyList());
}
myStrategy = new DefaultIpsGenerationStrategy();
mySvc = new IpsGeneratorSvcImpl(myFhirContext, myStrategy, myDaoRegistry);
private void initializeGenerationStrategy() {
initializeGenerationStrategy(List.of());
}
private void initializeGenerationStrategy(List<Function<Section, Section>> theGlobalSectionCustomizers) {
myStrategy = new DefaultJpaIpsGenerationStrategy() {
@Override
public IIdType massageResourceId(@Nullable IpsContext theIpsContext, @javax.annotation.Nonnull IBaseResource theResource) {
return IdType.newRandomUuid();
}
};
myStrategy.setFhirContext(myFhirContext);
myStrategy.setDaoRegistry(myDaoRegistry);
if (theGlobalSectionCustomizers != null) {
for (var next : theGlobalSectionCustomizers) {
myStrategy.addGlobalSectionCustomizer(next);
}
}
mySvc = new IpsGeneratorSvcImpl(myFhirContext, myStrategy);
}
@Test
public void testGenerateIps() {
// Setup
initializeGenerationStrategy();
registerResourceDaosForSmallPatientSet();
// Test
Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new TokenParam("http://foo", "bar"));
Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new TokenParam("http://foo", "bar"), null);
// Verify
ourLog.info("Generated IPS:\n{}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome));
@ -165,6 +186,7 @@ public class IpsGeneratorSvcImplTest {
@Test
public void testAllergyIntolerance_OnsetTypes() throws IOException {
// Setup Patient
initializeGenerationStrategy();
registerPatientDaoWithRead();
AllergyIntolerance allergy1 = new AllergyIntolerance();
@ -191,11 +213,11 @@ public class IpsGeneratorSvcImplTest {
registerRemainingResourceDaos();
// Test
Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID));
Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID), null);
// Verify
Composition compositions = (Composition) outcome.getEntry().get(0).getResource();
Composition.SectionComponent section = findSection(compositions, IpsSectionEnum.ALLERGY_INTOLERANCE);
Composition.SectionComponent section = findSection(compositions, DefaultJpaIpsGenerationStrategy.SECTION_CODE_ALLERGY_INTOLERANCE);
HtmlPage narrativeHtml = HtmlUtil.parseAsHtml(section.getText().getDivAsString());
ourLog.info("Narrative:\n{}", narrativeHtml.asXml());
@ -213,6 +235,7 @@ public class IpsGeneratorSvcImplTest {
@Test
public void testAllergyIntolerance_MissingElements() throws IOException {
// Setup Patient
initializeGenerationStrategy();
registerPatientDaoWithRead();
AllergyIntolerance allergy = new AllergyIntolerance();
@ -226,11 +249,11 @@ public class IpsGeneratorSvcImplTest {
registerRemainingResourceDaos();
// Test
Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID));
Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID), null);
// Verify
Composition compositions = (Composition) outcome.getEntry().get(0).getResource();
Composition.SectionComponent section = findSection(compositions, IpsSectionEnum.ALLERGY_INTOLERANCE);
Composition.SectionComponent section = findSection(compositions, DefaultJpaIpsGenerationStrategy.SECTION_CODE_ALLERGY_INTOLERANCE);
HtmlPage narrativeHtml = HtmlUtil.parseAsHtml(section.getText().getDivAsString());
ourLog.info("Narrative:\n{}", narrativeHtml.asXml());
@ -242,6 +265,7 @@ public class IpsGeneratorSvcImplTest {
@Test
public void testMedicationSummary_MedicationStatementWithMedicationReference() throws IOException {
// Setup Patient
initializeGenerationStrategy();
registerPatientDaoWithRead();
// Setup Medication + MedicationStatement
@ -253,7 +277,7 @@ public class IpsGeneratorSvcImplTest {
registerRemainingResourceDaos();
// Test
Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID));
Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID), null);
// Verify Bundle Contents
List<String> contentResourceTypes = toEntryResourceTypeStrings(outcome);
@ -266,14 +290,14 @@ public class IpsGeneratorSvcImplTest {
// Verify
Composition compositions = (Composition) outcome.getEntry().get(0).getResource();
Composition.SectionComponent section = findSection(compositions, IpsSectionEnum.MEDICATION_SUMMARY);
Composition.SectionComponent section = findSection(compositions, DefaultJpaIpsGenerationStrategy.SECTION_CODE_MEDICATION_SUMMARY);
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(1, tables.size());
HtmlTable table = (HtmlTable) tables.get(0);
HtmlTableRow row = table.getBodies().get(0).getRows().get(0);
assertEquals("Tylenol", row.getCell(0).asNormalizedText());
assertEquals("Active", row.getCell(1).asNormalizedText());
@ -285,6 +309,7 @@ public class IpsGeneratorSvcImplTest {
@Test
public void testMedicationSummary_MedicationRequestWithNoMedication() throws IOException {
// Setup Patient
initializeGenerationStrategy();
registerPatientDaoWithRead();
// Setup Medication + MedicationStatement
@ -298,17 +323,17 @@ public class IpsGeneratorSvcImplTest {
registerRemainingResourceDaos();
// Test
Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID));
Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID), null);
// Verify
Composition compositions = (Composition) outcome.getEntry().get(0).getResource();
Composition.SectionComponent section = findSection(compositions, IpsSectionEnum.MEDICATION_SUMMARY);
Composition.SectionComponent section = findSection(compositions, DefaultJpaIpsGenerationStrategy.SECTION_CODE_MEDICATION_SUMMARY);
HtmlPage narrativeHtml = HtmlUtil.parseAsHtml(section.getText().getDivAsString());
ourLog.info("Narrative:\n{}", narrativeHtml.asXml());
DomNodeList<DomElement> tables = narrativeHtml.getElementsByTagName("table");
assertEquals(2, tables.size());
assertEquals(1, tables.size());
HtmlTable table = (HtmlTable) tables.get(0);
HtmlTableRow row = table.getBodies().get(0).getRows().get(0);
assertEquals("", row.getCell(0).asNormalizedText());
@ -317,22 +342,12 @@ public class IpsGeneratorSvcImplTest {
assertEquals("", row.getCell(3).asNormalizedText());
}
@Nonnull
private Composition.SectionComponent findSection(Composition compositions, IpsSectionEnum sectionEnum) {
Composition.SectionComponent section = compositions
.getSection()
.stream()
.filter(t -> t.getTitle().equals(myStrategy.getSectionRegistry().getSection(sectionEnum).getTitle()))
.findFirst()
.orElseThrow();
return section;
}
@Test
public void testMedicationSummary_DuplicateSecondaryResources() {
myStrategy.setSectionRegistry(new SectionRegistry().addGlobalCustomizer(t -> t.withNoInfoGenerator(null)));
// Setup Patient
initializeGenerationStrategy(
List.of(t->Section.newBuilder(t).withNoInfoGenerator(null).build())
);
registerPatientDaoWithRead();
// Setup Medication + MedicationStatement
@ -346,7 +361,7 @@ public class IpsGeneratorSvcImplTest {
registerRemainingResourceDaos();
// Test
Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID));
Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID), null);
// Verify Bundle Contents
List<String> contentResourceTypes = toEntryResourceTypeStrings(outcome);
@ -367,9 +382,10 @@ public class IpsGeneratorSvcImplTest {
*/
@Test
public void testMedicationSummary_ResourceAppearsAsSecondaryThenPrimary() throws IOException {
myStrategy.setSectionRegistry(new SectionRegistry().addGlobalCustomizer(t -> t.withNoInfoGenerator(null)));
// Setup Patient
initializeGenerationStrategy(
List.of(t->Section.newBuilder(t).withNoInfoGenerator(null).build())
);
registerPatientDaoWithRead();
// Setup Medication + MedicationStatement
@ -385,7 +401,7 @@ public class IpsGeneratorSvcImplTest {
registerRemainingResourceDaos();
// Test
Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID));
Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID), null);
// Verify Bundle Contents
List<String> contentResourceTypes = toEntryResourceTypeStrings(outcome);
@ -400,20 +416,61 @@ public class IpsGeneratorSvcImplTest {
// Verify narrative - should have 2 rows (one for each primary MedicationStatement)
Composition compositions = (Composition) outcome.getEntry().get(0).getResource();
Composition.SectionComponent section = findSection(compositions, IpsSectionEnum.MEDICATION_SUMMARY);
Composition.SectionComponent section = findSection(compositions, DefaultJpaIpsGenerationStrategy.SECTION_CODE_MEDICATION_SUMMARY);
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(1, tables.size());
HtmlTable table = (HtmlTable) tables.get(0);
assertEquals(2, table.getBodies().get(0).getRows().size());
}
/**
* If there is no contents in one of the 2 medication summary tables it should be
* omitted
*/
@Test
public void testMedicationSummary_OmitMedicationRequestTable() throws IOException {
// Setup Patient
initializeGenerationStrategy(
List.of(t->Section.newBuilder(t).withNoInfoGenerator(null).build())
);
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_ID), null);
// Verify narrative - should have 2 rows (one for each primary MedicationStatement)
Composition compositions = (Composition) outcome.getEntry().get(0).getResource();
Composition.SectionComponent section = findSection(compositions, DefaultJpaIpsGenerationStrategy.SECTION_CODE_MEDICATION_SUMMARY);
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);
assertEquals(2, table.getBodies().get(0).getRows().size());
}
@Test
public void testMedicalDevices_DeviceUseStatementWithDevice() throws IOException {
// Setup Patient
initializeGenerationStrategy();
registerPatientDaoWithRead();
// Setup Medication + MedicationStatement
@ -436,11 +493,11 @@ public class IpsGeneratorSvcImplTest {
registerRemainingResourceDaos();
// Test
Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID));
Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID), null);
// Verify
Composition compositions = (Composition) outcome.getEntry().get(0).getResource();
Composition.SectionComponent section = findSection(compositions, IpsSectionEnum.MEDICAL_DEVICES);
Composition.SectionComponent section = findSection(compositions, DefaultJpaIpsGenerationStrategy.SECTION_CODE_MEDICAL_DEVICES);
HtmlPage narrativeHtml = HtmlUtil.parseAsHtml(section.getText().getDivAsString());
ourLog.info("Narrative:\n{}", narrativeHtml.asXml());
@ -457,6 +514,7 @@ public class IpsGeneratorSvcImplTest {
@Test
public void testImmunizations() throws IOException {
// Setup Patient
initializeGenerationStrategy();
registerPatientDaoWithRead();
// Setup Medication + MedicationStatement
@ -483,11 +541,11 @@ public class IpsGeneratorSvcImplTest {
registerRemainingResourceDaos();
// Test
Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID));
Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID), null);
// Verify
Composition compositions = (Composition) outcome.getEntry().get(0).getResource();
Composition.SectionComponent section = findSection(compositions, IpsSectionEnum.IMMUNIZATIONS);
Composition.SectionComponent section = findSection(compositions, DefaultJpaIpsGenerationStrategy.SECTION_CODE_IMMUNIZATIONS);
HtmlPage narrativeHtml = HtmlUtil.parseAsHtml(section.getText().getDivAsString());
ourLog.info("Narrative:\n{}", narrativeHtml.asXml());
@ -508,6 +566,7 @@ public class IpsGeneratorSvcImplTest {
@Test
public void testReferencesUpdatedInSecondaryInclusions() {
// Setup Patient
initializeGenerationStrategy();
registerPatientDaoWithRead();
// Setup Medication + MedicationStatement
@ -545,7 +604,7 @@ public class IpsGeneratorSvcImplTest {
registerRemainingResourceDaos();
// Test
Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID));
Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID), null);
ourLog.info(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome));
// Verify cross-references
@ -569,10 +628,10 @@ public class IpsGeneratorSvcImplTest {
ourLog.info("Resource: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome));
verify(conditionDao, times(2)).search(any(), any());
Composition composition = (Composition) outcome.getEntry().get(0).getResource();
Composition.SectionComponent problemListSection = findSection(composition, IpsSectionEnum.PROBLEM_LIST);
Composition.SectionComponent problemListSection = findSection(composition, DefaultJpaIpsGenerationStrategy.SECTION_CODE_PROBLEM_LIST);
assertEquals(addedCondition.getId(), problemListSection.getEntry().get(0).getReference());
assertEquals(1, problemListSection.getEntry().size());
Composition.SectionComponent illnessHistorySection = findSection(composition, IpsSectionEnum.ILLNESS_HISTORY);
Composition.SectionComponent illnessHistorySection = findSection(composition, DefaultJpaIpsGenerationStrategy.SECTION_CODE_ILLNESS_HISTORY);
assertEquals(addedCondition2.getId(), illnessHistorySection.getEntry().get(0).getReference());
assertEquals(1, illnessHistorySection.getEntry().size());
}
@ -580,6 +639,7 @@ public class IpsGeneratorSvcImplTest {
@Test
public void testPatientIsReturnedAsAnIncludeResource() {
// Setup Patient
initializeGenerationStrategy();
registerPatientDaoWithRead();
// Setup Condition
@ -605,7 +665,7 @@ public class IpsGeneratorSvcImplTest {
registerRemainingResourceDaos();
// Test
Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID));
Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID), null);
List<String> resources = outcome
.getEntry()
@ -617,6 +677,30 @@ public class IpsGeneratorSvcImplTest {
));
}
@Test
public void testSelectGenerator() {
IIpsGenerationStrategy strategy1 = mock(IIpsGenerationStrategy.class);
when(strategy1.getBundleProfile()).thenReturn("http://1");
IIpsGenerationStrategy strategy2 = mock(IIpsGenerationStrategy.class);
when(strategy2.getBundleProfile()).thenReturn("http://2");
IpsGeneratorSvcImpl svc = new IpsGeneratorSvcImpl(myFhirContext, List.of(strategy1, strategy2));
assertSame(strategy1, svc.selectGenerationStrategy("http://1"));
assertSame(strategy1, svc.selectGenerationStrategy(null));
assertSame(strategy1, svc.selectGenerationStrategy("http://foo"));
assertSame(strategy2, svc.selectGenerationStrategy("http://2"));
}
@Nonnull
private Composition.SectionComponent findSection(Composition compositions, String theSectionCode) {
return compositions
.getSection()
.stream()
.filter(t -> t.getCode().getCodingFirstRep().getCode().equals(theSectionCode))
.findFirst()
.orElseThrow();
}
private void registerPatientDaoWithRead() {
IFhirResourceDao<Patient> patientDao = registerResourceDaoWithNoData(Patient.class);
Patient patient = new Patient();
@ -674,19 +758,19 @@ public class IpsGeneratorSvcImplTest {
}
@Nonnull
private static Medication createSecondaryMedication(String medicationId) {
private static Medication createSecondaryMedication(String theMedicationId) {
Medication medication = new Medication();
medication.setId(new IdType(medicationId));
medication.setId(new IdType(theMedicationId));
medication.getCode().addCoding().setDisplay("Tylenol");
ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(medication, BundleEntrySearchModeEnum.INCLUDE);
return medication;
}
@Nonnull
private static MedicationStatement createPrimaryMedicationStatement(String medicationId, String medicationStatementId) {
private static MedicationStatement createPrimaryMedicationStatement(String theMedicationId, String medicationStatementId) {
MedicationStatement medicationStatement = new MedicationStatement();
medicationStatement.setId(medicationStatementId);
medicationStatement.setMedication(new Reference(medicationId));
medicationStatement.setMedication(new Reference(theMedicationId));
medicationStatement.setStatus(MedicationStatement.MedicationStatementStatus.ACTIVE);
medicationStatement.getDosageFirstRep().getRoute().addCoding().setDisplay("Oral");
medicationStatement.getDosageFirstRep().setText("DAW");

View File

@ -0,0 +1,152 @@
package ca.uhn.fhir.jpa.ips.provider;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.ips.generator.IIpsGeneratorSvc;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Identifier;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.UriType;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class IpsOperationProviderTest {
@Mock
private IIpsGeneratorSvc myIpsGeneratorSvc;
@RegisterExtension
private RestfulServerExtension myServer = new RestfulServerExtension(FhirContext.forR4Cached())
.withServer(t -> t.registerProviders(new IpsOperationProvider(myIpsGeneratorSvc)));
@Captor
private ArgumentCaptor<String> myProfileCaptor;
@Captor
private ArgumentCaptor<IIdType> myIdTypeCaptor;
@Captor
private ArgumentCaptor<TokenParam> myTokenCaptor;
@Test
public void testGenerateById() {
// setup
Bundle expected = new Bundle();
expected.setType(Bundle.BundleType.DOCUMENT);
when(myIpsGeneratorSvc.generateIps(any(), any(IIdType.class), any())).thenReturn(expected);
// test
Bundle actual = myServer
.getFhirClient()
.operation()
.onInstance(new IdType("Patient/123"))
.named("$summary")
.withNoParameters(Parameters.class)
.returnResourceType(Bundle.class)
.execute();
// verify
assertEquals(Bundle.BundleType.DOCUMENT, actual.getType());
verify(myIpsGeneratorSvc, times(1)).generateIps(any(), myIdTypeCaptor.capture(), myProfileCaptor.capture());
assertEquals("Patient/123", myIdTypeCaptor.getValue().getValue());
assertEquals(null, myProfileCaptor.getValue());
}
@Test
public void testGenerateById_WithProfile() {
// setup
Bundle expected = new Bundle();
expected.setType(Bundle.BundleType.DOCUMENT);
when(myIpsGeneratorSvc.generateIps(any(), any(IIdType.class), any())).thenReturn(expected);
// test
Bundle actual = myServer
.getFhirClient()
.operation()
.onInstance(new IdType("Patient/123"))
.named("$summary")
.withParameter(Parameters.class, "profile", new UriType("http://foo"))
.returnResourceType(Bundle.class)
.execute();
// verify
assertEquals(Bundle.BundleType.DOCUMENT, actual.getType());
verify(myIpsGeneratorSvc, times(1)).generateIps(any(), myIdTypeCaptor.capture(), myProfileCaptor.capture());
assertEquals("Patient/123", myIdTypeCaptor.getValue().getValue());
assertEquals("http://foo", myProfileCaptor.getValue());
}
@Test
public void testGenerateByIdentifier() {
// setup
Bundle expected = new Bundle();
expected.setType(Bundle.BundleType.DOCUMENT);
when(myIpsGeneratorSvc.generateIps(any(), any(TokenParam.class), any())).thenReturn(expected);
// test
Bundle actual = myServer
.getFhirClient()
.operation()
.onType("Patient")
.named("$summary")
.withParameter(Parameters.class, "identifier", new Identifier().setSystem("http://system").setValue("value"))
.returnResourceType(Bundle.class)
.execute();
// verify
assertEquals(Bundle.BundleType.DOCUMENT, actual.getType());
verify(myIpsGeneratorSvc, times(1)).generateIps(any(), myTokenCaptor.capture(), myProfileCaptor.capture());
assertEquals("http://system", myTokenCaptor.getValue().getSystem());
assertEquals("value", myTokenCaptor.getValue().getValue());
assertEquals(null, myProfileCaptor.getValue());
}
@Test
public void testGenerateByIdentifier_WithProfile() {
// setup
Bundle expected = new Bundle();
expected.setType(Bundle.BundleType.DOCUMENT);
when(myIpsGeneratorSvc.generateIps(any(), any(TokenParam.class), any())).thenReturn(expected);
// test
Bundle actual = myServer
.getFhirClient()
.operation()
.onType("Patient")
.named("$summary")
.withParameter(Parameters.class, "identifier", new Identifier().setSystem("http://system").setValue("value"))
.andParameter("profile", new UriType("http://foo"))
.returnResourceType(Bundle.class)
.execute();
// verify
assertEquals(Bundle.BundleType.DOCUMENT, actual.getType());
verify(myIpsGeneratorSvc, times(1)).generateIps(any(), myTokenCaptor.capture(), myProfileCaptor.capture());
assertEquals("http://system", myTokenCaptor.getValue().getSystem());
assertEquals("value", myTokenCaptor.getValue().getValue());
assertEquals("http://foo", myProfileCaptor.getValue());
}
}

View File

@ -2,15 +2,14 @@ package ca.uhn.fhirtest.config;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.config.HapiJpaConfig;
import ca.uhn.fhir.jpa.config.r4.JpaR4Config;
import ca.uhn.fhir.jpa.config.util.HapiEntityManagerFactoryUtil;
import ca.uhn.fhir.jpa.ips.api.IIpsGenerationStrategy;
import ca.uhn.fhir.jpa.ips.generator.IIpsGeneratorSvc;
import ca.uhn.fhir.jpa.ips.generator.IpsGeneratorSvcImpl;
import ca.uhn.fhir.jpa.ips.jpa.DefaultJpaIpsGenerationStrategy;
import ca.uhn.fhir.jpa.ips.provider.IpsOperationProvider;
import ca.uhn.fhir.jpa.ips.strategy.DefaultIpsGenerationStrategy;
import ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect;
import ca.uhn.fhir.jpa.model.dialect.HapiFhirPostgres94Dialect;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
@ -200,13 +199,12 @@ public class TestR4Config {
@Bean
public IIpsGenerationStrategy ipsGenerationStrategy() {
return new DefaultIpsGenerationStrategy();
return new DefaultJpaIpsGenerationStrategy();
}
@Bean
public IIpsGeneratorSvc ipsGeneratorSvc(
FhirContext theFhirContext, IIpsGenerationStrategy theGenerationStrategy, DaoRegistry theDaoRegistry) {
return new IpsGeneratorSvcImpl(theFhirContext, theGenerationStrategy, theDaoRegistry);
public IIpsGeneratorSvc ipsGeneratorSvc(FhirContext theFhirContext, IIpsGenerationStrategy theGenerationStrategy) {
return new IpsGeneratorSvcImpl(theFhirContext, theGenerationStrategy);
}
@Bean

View File

@ -0,0 +1,28 @@
package ca.uhn.fhir.narrative2;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.Medication;
import org.hl7.fhir.dstu3.model.Patient;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class NarrativeGeneratorTemplateUtilsTest {
@Test
public void testBundleHasEntriesWithResourceType_True() {
Bundle bundle = new Bundle();
bundle.addEntry().setResource(new Patient().setActive(true));
bundle.addEntry().setResource(new Medication().setIsBrand(true));
assertTrue(NarrativeGeneratorTemplateUtils.INSTANCE.bundleHasEntriesWithResourceType(bundle, "Patient"));
}
@Test
public void testBundleHasEntriesWithResourceType_False() {
Bundle bundle = new Bundle();
bundle.addEntry().setResource(new Medication().setIsBrand(true));
assertFalse(NarrativeGeneratorTemplateUtils.INSTANCE.bundleHasEntriesWithResourceType(bundle, "Patient"));
}
}