Merge remote-tracking branch 'origin/master' into issue-2534-new-tx-pointcut
This commit is contained in:
commit
fcbfb11506
|
@ -38,6 +38,7 @@ import java.util.Set;
|
|||
import java.util.TreeMap;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
import org.hl7.fhir.instance.model.api.IAnyResource;
|
||||
import org.hl7.fhir.instance.model.api.IBase;
|
||||
import org.hl7.fhir.instance.model.api.IBaseBackboneElement;
|
||||
|
@ -601,6 +602,11 @@ public abstract class BaseRuntimeElementCompositeDefinition<T extends IBase> ext
|
|||
public boolean isFirstFieldInNewClass() {
|
||||
return myFirstFieldInNewClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return myField.getName();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -902,21 +902,21 @@ public class FhirContext {
|
|||
}
|
||||
|
||||
private BaseRuntimeElementDefinition<?> scanDatatype(final Class<? extends IElement> theResourceType) {
|
||||
ArrayList<Class<? extends IElement>> resourceTypes = new ArrayList<Class<? extends IElement>>();
|
||||
ArrayList<Class<? extends IElement>> resourceTypes = new ArrayList<>();
|
||||
resourceTypes.add(theResourceType);
|
||||
Map<Class<? extends IBase>, BaseRuntimeElementDefinition<?>> defs = scanResourceTypes(resourceTypes);
|
||||
return defs.get(theResourceType);
|
||||
}
|
||||
|
||||
private RuntimeResourceDefinition scanResourceType(final Class<? extends IBaseResource> theResourceType) {
|
||||
ArrayList<Class<? extends IElement>> resourceTypes = new ArrayList<Class<? extends IElement>>();
|
||||
ArrayList<Class<? extends IElement>> resourceTypes = new ArrayList<>();
|
||||
resourceTypes.add(theResourceType);
|
||||
Map<Class<? extends IBase>, BaseRuntimeElementDefinition<?>> defs = scanResourceTypes(resourceTypes);
|
||||
return (RuntimeResourceDefinition) defs.get(theResourceType);
|
||||
}
|
||||
|
||||
private synchronized Map<Class<? extends IBase>, BaseRuntimeElementDefinition<?>> scanResourceTypes(final Collection<Class<? extends IElement>> theResourceTypes) {
|
||||
List<Class<? extends IBase>> typesToScan = new ArrayList<Class<? extends IBase>>();
|
||||
List<Class<? extends IBase>> typesToScan = new ArrayList<>();
|
||||
if (theResourceTypes != null) {
|
||||
typesToScan.addAll(theResourceTypes);
|
||||
}
|
||||
|
|
|
@ -1202,6 +1202,9 @@ public enum Pointcut implements IPointcut {
|
|||
* pulled out of the servlet request. This parameter is identical to the RequestDetails parameter above but will
|
||||
* only be populated when operating in a RestfulServer implementation. It is provided as a convenience.
|
||||
* </li>
|
||||
* <li>
|
||||
* ca.uhn.fhir.jpa.searchparam.SearchParameterMap - Contains the details of the search being checked. This can be modified.
|
||||
* </li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Hooks should return <code>void</code>.
|
||||
|
@ -1210,7 +1213,8 @@ public enum Pointcut implements IPointcut {
|
|||
STORAGE_PRESEARCH_REGISTERED(void.class,
|
||||
"ca.uhn.fhir.rest.server.util.ICachedSearchDetails",
|
||||
"ca.uhn.fhir.rest.api.server.RequestDetails",
|
||||
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails"
|
||||
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails",
|
||||
"ca.uhn.fhir.jpa.searchparam.SearchParameterMap"
|
||||
),
|
||||
|
||||
/**
|
||||
|
|
|
@ -49,13 +49,13 @@ public @interface Extension {
|
|||
* by regional authorities or jurisdictional governments)
|
||||
* </p>
|
||||
*/
|
||||
boolean definedLocally();
|
||||
boolean definedLocally() default true;
|
||||
|
||||
/**
|
||||
* Returns <code>true</code> if this extension is a <a
|
||||
* href="http://www.hl7.org/implement/standards/fhir/extensibility.html#modifierExtension">modifier extension</a>
|
||||
*/
|
||||
boolean isModifier();
|
||||
boolean isModifier() default false;
|
||||
|
||||
/**
|
||||
* The URL associated with this extension
|
||||
|
|
|
@ -32,7 +32,7 @@ public interface INarrativeTemplate {
|
|||
|
||||
Set<String> getAppliesToResourceTypes();
|
||||
|
||||
Set<Class<? extends IBase>> getAppliesToResourceClasses();
|
||||
Set<Class<? extends IBase>> getAppliesToClasses();
|
||||
|
||||
TemplateTypeEnum getTemplateType();
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ public class NarrativeTemplate implements INarrativeTemplate {
|
|||
private Set<String> myAppliesToProfiles = new HashSet<>();
|
||||
private Set<String> myAppliesToResourceTypes = new HashSet<>();
|
||||
private Set<String> myAppliesToDataTypes = new HashSet<>();
|
||||
private Set<Class<? extends IBase>> myAppliesToResourceClasses = new HashSet<>();
|
||||
private Set<Class<? extends IBase>> myAppliesToClasses = new HashSet<>();
|
||||
private TemplateTypeEnum myTemplateType = TemplateTypeEnum.THYMELEAF;
|
||||
private String myContextPath;
|
||||
private String myTemplateName;
|
||||
|
@ -79,12 +79,12 @@ public class NarrativeTemplate implements INarrativeTemplate {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Set<Class<? extends IBase>> getAppliesToResourceClasses() {
|
||||
return Collections.unmodifiableSet(myAppliesToResourceClasses);
|
||||
public Set<Class<? extends IBase>> getAppliesToClasses() {
|
||||
return Collections.unmodifiableSet(myAppliesToClasses);
|
||||
}
|
||||
|
||||
void addAppliesToResourceClass(Class<? extends IBase> theAppliesToResourceClass) {
|
||||
myAppliesToResourceClasses.add(theAppliesToResourceClass);
|
||||
void addAppliesToClass(Class<? extends IBase> theAppliesToClass) {
|
||||
myAppliesToClasses.add(theAppliesToClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -118,4 +118,5 @@ public class NarrativeTemplate implements INarrativeTemplate {
|
|||
void addAppliesToDatatype(String theDataType) {
|
||||
myAppliesToDataTypes.add(theDataType);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ package ca.uhn.fhir.narrative2;
|
|||
import ca.uhn.fhir.context.ConfigurationException;
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||
import com.google.common.base.Charsets;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
@ -32,8 +33,20 @@ import org.hl7.fhir.instance.model.api.IBaseResource;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.StringReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
|
@ -41,15 +54,17 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
|||
public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(NarrativeTemplateManifest.class);
|
||||
|
||||
private final Map<String, List<NarrativeTemplate>> myStyleToResourceTypeToTemplate;
|
||||
private final Map<String, List<NarrativeTemplate>> myStyleToDatatypeToTemplate;
|
||||
private final Map<String, List<NarrativeTemplate>> myStyleToNameToTemplate;
|
||||
private final Map<String, List<NarrativeTemplate>> myResourceTypeToTemplate;
|
||||
private final Map<String, List<NarrativeTemplate>> myDatatypeToTemplate;
|
||||
private final Map<String, List<NarrativeTemplate>> myNameToTemplate;
|
||||
private final Map<String, List<NarrativeTemplate>> myClassToTemplate;
|
||||
private final int myTemplateCount;
|
||||
|
||||
private NarrativeTemplateManifest(Collection<NarrativeTemplate> theTemplates) {
|
||||
Map<String, List<NarrativeTemplate>> resourceTypeToTemplate = new HashMap<>();
|
||||
Map<String, List<NarrativeTemplate>> datatypeToTemplate = new HashMap<>();
|
||||
Map<String, List<NarrativeTemplate>> nameToTemplate = new HashMap<>();
|
||||
Map<String, List<NarrativeTemplate>> classToTemplate = new HashMap<>();
|
||||
|
||||
for (NarrativeTemplate nextTemplate : theTemplates) {
|
||||
nameToTemplate.computeIfAbsent(nextTemplate.getTemplateName(), t -> new ArrayList<>()).add(nextTemplate);
|
||||
|
@ -59,12 +74,16 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
|
|||
for (String nextDataType : nextTemplate.getAppliesToDataTypes()) {
|
||||
datatypeToTemplate.computeIfAbsent(nextDataType.toUpperCase(), t -> new ArrayList<>()).add(nextTemplate);
|
||||
}
|
||||
for (Class<? extends IBase> nextAppliesToClass : nextTemplate.getAppliesToClasses()) {
|
||||
classToTemplate.computeIfAbsent(nextAppliesToClass.getName(), t -> new ArrayList<>()).add(nextTemplate);
|
||||
}
|
||||
}
|
||||
|
||||
myTemplateCount = theTemplates.size();
|
||||
myStyleToNameToTemplate = makeImmutable(nameToTemplate);
|
||||
myStyleToResourceTypeToTemplate = makeImmutable(resourceTypeToTemplate);
|
||||
myStyleToDatatypeToTemplate = makeImmutable(datatypeToTemplate);
|
||||
myClassToTemplate = makeImmutable(classToTemplate);
|
||||
myNameToTemplate = makeImmutable(nameToTemplate);
|
||||
myResourceTypeToTemplate = makeImmutable(resourceTypeToTemplate);
|
||||
myDatatypeToTemplate = makeImmutable(datatypeToTemplate);
|
||||
}
|
||||
|
||||
public int getNamedTemplateCount() {
|
||||
|
@ -73,23 +92,27 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
|
|||
|
||||
@Override
|
||||
public List<INarrativeTemplate> getTemplateByResourceName(FhirContext theFhirContext, EnumSet<TemplateTypeEnum> theStyles, String theResourceName) {
|
||||
return getFromMap(theStyles, theResourceName.toUpperCase(), myStyleToResourceTypeToTemplate);
|
||||
return getFromMap(theStyles, theResourceName.toUpperCase(), myResourceTypeToTemplate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<INarrativeTemplate> getTemplateByName(FhirContext theFhirContext, EnumSet<TemplateTypeEnum> theStyles, String theName) {
|
||||
return getFromMap(theStyles, theName, myStyleToNameToTemplate);
|
||||
return getFromMap(theStyles, theName, myNameToTemplate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<INarrativeTemplate> getTemplateByElement(FhirContext theFhirContext, EnumSet<TemplateTypeEnum> theStyles, IBase theElement) {
|
||||
if (theElement instanceof IBaseResource) {
|
||||
String resourceName = theFhirContext.getResourceDefinition((IBaseResource) theElement).getName();
|
||||
return getTemplateByResourceName(theFhirContext, theStyles, resourceName);
|
||||
} else {
|
||||
String datatypeName = theFhirContext.getElementDefinition(theElement.getClass()).getName();
|
||||
return getFromMap(theStyles, datatypeName.toUpperCase(), myStyleToDatatypeToTemplate);
|
||||
List<INarrativeTemplate> retVal = getFromMap(theStyles, theElement.getClass().getName(), myClassToTemplate);
|
||||
if (retVal.isEmpty()) {
|
||||
if (theElement instanceof IBaseResource) {
|
||||
String resourceName = theFhirContext.getResourceDefinition((IBaseResource) theElement).getName();
|
||||
retVal = getTemplateByResourceName(theFhirContext, theStyles, resourceName);
|
||||
} else {
|
||||
String datatypeName = theFhirContext.getElementDefinition(theElement.getClass()).getName();
|
||||
retVal = getFromMap(theStyles, datatypeName.toUpperCase(), myDatatypeToTemplate);
|
||||
}
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
public static NarrativeTemplateManifest forManifestFileLocation(String... thePropertyFilePaths) throws IOException {
|
||||
|
@ -134,9 +157,16 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
|
|||
|
||||
NarrativeTemplate nextTemplate = nameToTemplate.computeIfAbsent(name, t -> new NarrativeTemplate().setTemplateName(name));
|
||||
|
||||
Validate.isTrue(!nextKey.endsWith(".class"), "Narrative manifest does not support specifying templates by class name - Use \"[name].resourceType=[resourceType]\" instead");
|
||||
|
||||
if (nextKey.endsWith(".profile")) {
|
||||
if (nextKey.endsWith(".class")) {
|
||||
String className = file.getProperty(nextKey);
|
||||
if (isNotBlank(className)) {
|
||||
try {
|
||||
nextTemplate.addAppliesToClass((Class<? extends IBase>) Class.forName(className));
|
||||
} catch (ClassNotFoundException theE) {
|
||||
throw new InternalErrorException("Could not find class " + className + " declared in narative manifest");
|
||||
}
|
||||
}
|
||||
} else if (nextKey.endsWith(".profile")) {
|
||||
String profile = file.getProperty(nextKey);
|
||||
if (isNotBlank(profile)) {
|
||||
nextTemplate.addAppliesToProfile(profile);
|
||||
|
@ -144,17 +174,17 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
|
|||
} else if (nextKey.endsWith(".resourceType")) {
|
||||
String resourceType = file.getProperty(nextKey);
|
||||
Arrays
|
||||
.stream(resourceType.split(","))
|
||||
.map(t -> t.trim())
|
||||
.filter(t -> isNotBlank(t))
|
||||
.forEach(t -> nextTemplate.addAppliesToResourceType(t));
|
||||
.stream(resourceType.split(","))
|
||||
.map(t -> t.trim())
|
||||
.filter(t -> isNotBlank(t))
|
||||
.forEach(t -> nextTemplate.addAppliesToResourceType(t));
|
||||
} else if (nextKey.endsWith(".dataType")) {
|
||||
String dataType = file.getProperty(nextKey);
|
||||
Arrays
|
||||
.stream(dataType.split(","))
|
||||
.map(t -> t.trim())
|
||||
.filter(t -> isNotBlank(t))
|
||||
.forEach(t -> nextTemplate.addAppliesToDatatype(t));
|
||||
.stream(dataType.split(","))
|
||||
.map(t -> t.trim())
|
||||
.filter(t -> isNotBlank(t))
|
||||
.forEach(t -> nextTemplate.addAppliesToDatatype(t));
|
||||
} else if (nextKey.endsWith(".style")) {
|
||||
String templateTypeName = file.getProperty(nextKey).toUpperCase();
|
||||
TemplateTypeEnum templateType = TemplateTypeEnum.valueOf(templateTypeName);
|
||||
|
@ -171,9 +201,9 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
|
|||
} else if (nextKey.endsWith(".title")) {
|
||||
ourLog.debug("Ignoring title property as narrative generator no longer generates titles: {}", nextKey);
|
||||
} else {
|
||||
throw new ConfigurationException("Invalid property name: " + nextKey
|
||||
+ " - the key must end in one of the expected extensions "
|
||||
+ "'.profile', '.resourceType', '.dataType', '.style', '.contextPath', '.narrative', '.title'");
|
||||
throw new ConfigurationException("Invalid property name: " + nextKey
|
||||
+ " - the key must end in one of the expected extensions "
|
||||
+ "'.profile', '.resourceType', '.dataType', '.style', '.contextPath', '.narrative', '.title'");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -210,10 +240,10 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
|
|||
|
||||
private static <T> List<INarrativeTemplate> getFromMap(EnumSet<TemplateTypeEnum> theStyles, T theKey, Map<T, List<NarrativeTemplate>> theMap) {
|
||||
return theMap
|
||||
.getOrDefault(theKey, Collections.emptyList())
|
||||
.stream()
|
||||
.filter(t->theStyles.contains(t.getTemplateType()))
|
||||
.collect(Collectors.toList());
|
||||
.getOrDefault(theKey, Collections.emptyList())
|
||||
.stream()
|
||||
.filter(t -> theStyles.contains(t.getTemplateType()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static <T> Map<T, List<NarrativeTemplate>> makeImmutable(Map<T, List<NarrativeTemplate>> theStyleToResourceTypeToTemplate) {
|
||||
|
|
|
@ -980,7 +980,12 @@ public abstract class BaseParser implements IParser {
|
|||
myEncodeContext = theEncodeContext;
|
||||
}
|
||||
|
||||
private void addParent(CompositeChildElement theParent, StringBuilder theB) {
|
||||
@Override
|
||||
public String toString() {
|
||||
return myDef.getElementName();
|
||||
}
|
||||
|
||||
private void addParent(CompositeChildElement theParent, StringBuilder theB) {
|
||||
if (theParent != null) {
|
||||
if (theParent.myResDef != null) {
|
||||
theB.append(theParent.myResDef.getName());
|
||||
|
|
|
@ -379,7 +379,8 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
|
|||
}
|
||||
|
||||
boolean haveWrittenExtensions = false;
|
||||
for (CompositeChildElement nextChildElem : super.compositeChildIterator(theElement, theContainedResource, theParent, theEncodeContext)) {
|
||||
Iterable<CompositeChildElement> compositeChildElements = super.compositeChildIterator(theElement, theContainedResource, theParent, theEncodeContext);
|
||||
for (CompositeChildElement nextChildElem : compositeChildElements) {
|
||||
|
||||
BaseRuntimeChildDefinition nextChild = nextChildElem.getDef();
|
||||
|
||||
|
|
|
@ -205,6 +205,7 @@ public class Constants {
|
|||
public static final String PARAMQUALIFIER_STRING_CONTAINS = ":contains";
|
||||
public static final String PARAMQUALIFIER_STRING_EXACT = ":exact";
|
||||
public static final String PARAMQUALIFIER_TOKEN_TEXT = ":text";
|
||||
public static final String PARAMQUALIFIER_MDM = ":mdm";
|
||||
public static final int STATUS_HTTP_200_OK = 200;
|
||||
public static final int STATUS_HTTP_201_CREATED = 201;
|
||||
public static final int STATUS_HTTP_204_NO_CONTENT = 204;
|
||||
|
|
|
@ -22,6 +22,7 @@ package ca.uhn.fhir.rest.param;
|
|||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.model.primitive.IdDt;
|
||||
import ca.uhn.fhir.rest.api.Constants;
|
||||
import ca.uhn.fhir.util.CoverageIgnore;
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
import org.apache.commons.lang3.builder.ToStringStyle;
|
||||
|
@ -41,6 +42,7 @@ public class ReferenceParam extends BaseParam /*implements IQueryParameterType*/
|
|||
private String myBaseUrl;
|
||||
private String myValue;
|
||||
private String myIdPart;
|
||||
private Boolean myMdmExpand;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
|
@ -90,8 +92,8 @@ public class ReferenceParam extends BaseParam /*implements IQueryParameterType*/
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
String doGetQueryParameterQualifier() {
|
||||
|
||||
private String defaultGetQueryParameterQualifier() {
|
||||
StringBuilder b = new StringBuilder();
|
||||
if (isNotBlank(myChain)) {
|
||||
if (isNotBlank(getResourceType())) {
|
||||
|
@ -106,6 +108,10 @@ public class ReferenceParam extends BaseParam /*implements IQueryParameterType*/
|
|||
}
|
||||
return null;
|
||||
}
|
||||
@Override
|
||||
String doGetQueryParameterQualifier() {
|
||||
return this.myMdmExpand != null ? ":mdm" : defaultGetQueryParameterQualifier();
|
||||
}
|
||||
|
||||
@Override
|
||||
String doGetValueAsQueryToken(FhirContext theContext) {
|
||||
|
@ -121,6 +127,11 @@ public class ReferenceParam extends BaseParam /*implements IQueryParameterType*/
|
|||
|
||||
@Override
|
||||
void doSetValueAsQueryToken(FhirContext theContext, String theParamName, String theQualifier, String theValue) {
|
||||
if (Constants.PARAMQUALIFIER_MDM.equals(theQualifier)) {
|
||||
myMdmExpand = true;
|
||||
theQualifier = "";
|
||||
}
|
||||
|
||||
String q = theQualifier;
|
||||
if (isNotBlank(q)) {
|
||||
if (q.startsWith(":")) {
|
||||
|
@ -166,6 +177,14 @@ public class ReferenceParam extends BaseParam /*implements IQueryParameterType*/
|
|||
return myBaseUrl;
|
||||
}
|
||||
|
||||
public boolean isMdmExpand() {
|
||||
return myMdmExpand != null && myMdmExpand;
|
||||
}
|
||||
|
||||
public ReferenceParam setMdmExpand(boolean theMdmExpand) {
|
||||
myMdmExpand = theMdmExpand;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getChain() {
|
||||
return myChain;
|
||||
|
|
|
@ -11,14 +11,7 @@
|
|||
<tr th:each="issue : ${resource.issue}">
|
||||
<td th:text="${issue.severityElement.value}" style="font-weight: bold;"></td>
|
||||
<td th:text="${issue.location}"></td>
|
||||
<th:block th:switch="${fhirVersion}">
|
||||
<th:block th:case="'DSTU1'">
|
||||
<td><pre th:text="${issue.details}"/></td>
|
||||
</th:block>
|
||||
<th:block th:case="*">
|
||||
<td><pre th:text="${issue.diagnostics}"/></td>
|
||||
</th:block>
|
||||
</th:block>
|
||||
<td><pre th:text="${issue.diagnostics}"/></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
type: add
|
||||
issue: 2520
|
||||
title: "Add support for `:mdm` search parameter qualifier on reference search parameters. Details about enabling this feature
|
||||
can be found [in the documentation](/hapi-fhir/docs/server_jpa_mdm/mdm_expansion.html)."
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
type: add
|
||||
issue: 2537
|
||||
title: "It is now possible t create narrative generator templates that apply to any
|
||||
custom strucures including custom extension structures."
|
|
@ -67,6 +67,7 @@ page.server_jpa_mdm.mdm_rules=MDM Rules
|
|||
page.server_jpa_mdm.mdm_eid=MDM Enterprise Identifiers
|
||||
page.server_jpa_mdm.mdm_operations=MDM Operations
|
||||
page.server_jpa_mdm.mdm_details=MDM Technical Details
|
||||
page.server_jpa_mdm.mdm_expansion=MDM Search Expansion
|
||||
|
||||
section.server_jpa_partitioning.title=JPA Server: Partitioning and Multitenancy
|
||||
page.server_jpa_partitioning.partitioning=Partitioning and Multitenancy
|
||||
|
|
|
@ -117,7 +117,7 @@ If you wish to override this behaviour and supply a static CapabilityStatement,
|
|||
* [StaticCapabilityStatementInterceptor JavaDoc](/apidocs/hapi-fhir-server/ca/uhn/fhir/rest/server/interceptor/StaticCapabilityStatementInterceptor.html)
|
||||
* [StaticCapabilityStatementInterceptor Source](https://github.com/hapifhir/hapi-fhir/blob/master/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/StaticCapabilityStatementInterceptor.java)
|
||||
|
||||
The following example shows how to register the ExceptionHandlingInterceptor.
|
||||
The following example shows how to register the StaticCapabilityStatementInterceptor.
|
||||
|
||||
```java
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/ServletExamples.java|staticCapabilityStatementInterceptor}}
|
||||
|
|
|
@ -7,12 +7,115 @@ This process is described on the [Profiles & Extensions](./profiles_and_exte
|
|||
There are situations however when you might want to create an entirely custom resource type. This feature should be used only if there is no other option, since it means you are creating a resource type that will not be interoperable with other FHIR implementations.
|
||||
|
||||
<p class="doc_info_bubble">
|
||||
This is an advanced features and isn't needed for most uses of HAPI-FHIR. Feel free to skip this page.
|
||||
This is an advanced features and isn't needed for most uses of HAPI FHIR. Feel free to skip this page. For a simpler way of interacting with resource extensions, see <a href="./profiles_and_extensions.html">Profiles & Extensions</a>.
|
||||
</p>
|
||||
|
||||
|
||||
# Extending FHIR Resource Classes
|
||||
|
||||
The most elegant way of adding extensions to a resource is through the use of custom fields. The following example shows a custom type which extends the FHIR Patient resource definition through two extensions.
|
||||
|
||||
```java
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/MyPatient.java|patientDef}}
|
||||
```
|
||||
|
||||
Using this custom type is as simple as instantiating the type and working with the new fields.
|
||||
|
||||
```java
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/MyPatientUse.java|patientUse}}
|
||||
```
|
||||
|
||||
This example produces the following output:
|
||||
|
||||
```xml
|
||||
<Patient xmlns="http://hl7.org/fhir">
|
||||
<modifierExtension url="http://example.com/dontuse#importantDates">
|
||||
<valueDateTime value="2010-01-02"/>
|
||||
</modifierExtension>
|
||||
<modifierExtension url="http://example.com/dontuse#importantDates">
|
||||
<valueDateTime value="2014-01-26T11:11:11"/>
|
||||
</modifierExtension>
|
||||
<extension url="http://example.com/dontuse#petname">
|
||||
<valueString value="Fido"/>
|
||||
</extension>
|
||||
<name>
|
||||
<family value="Smith"/>
|
||||
<given value="John"/>
|
||||
<given value="Quincy"/>
|
||||
<suffix value="Jr"/>
|
||||
</name>
|
||||
</Patient>
|
||||
```
|
||||
|
||||
Parsing messages using your new custom type is equally simple. These types can also be used as method return types in clients and servers.
|
||||
|
||||
```java
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/MyPatientUse.java|patientParse}}
|
||||
```
|
||||
|
||||
# Using Custom Types in a Client
|
||||
|
||||
If you are using a client and wish to use a specific custom structure, you may simply use the custom structure as you would a build in HAPI type.
|
||||
|
||||
```java
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/ExtensionsDstu3.java|customTypeClientSimple}}
|
||||
```
|
||||
|
||||
You may also explicitly use custom types in searches and other operations which return resources.
|
||||
|
||||
```java
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/ExtensionsDstu3.java|customTypeClientSearch}}
|
||||
```
|
||||
|
||||
You can also explicitly declare a preferred response resource custom type. This is useful for some operations that do not otherwise declare their resource types in the method signature.
|
||||
|
||||
```java
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/ExtensionsDstu3.java|customTypeClientSearch2}}
|
||||
```
|
||||
|
||||
## Using Multiple Custom Types in a Client
|
||||
|
||||
Sometimes you may not know in advance exactly which type you will be receiving. For example, there are Patient resources which conform to several different profiles on a server and you aren't sure which profile you will get back for a specific read, you can declare the "primary" type for a given profile.
|
||||
|
||||
This is declared at the FhirContext level, and will apply to any clients created from this context (including clients created before the default was set).
|
||||
|
||||
```java
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/ExtensionsDstu3.java|customTypeClientDeclared}}
|
||||
```
|
||||
# Using Custom Types in a Server
|
||||
|
||||
If you are using a client and wish to use a specific custom structure, you may simply use the custom structure as you would a build in HAPI type.
|
||||
|
||||
```java
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/ExtensionsDstu3.java|customTypeClientSimple}}
|
||||
```
|
||||
|
||||
# Custom Composite Extension Classes
|
||||
|
||||
The following example shows a resource containing a composite extension.
|
||||
|
||||
```java
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/customtype/CustomCompositeExtension.java|resource}}
|
||||
```
|
||||
|
||||
This could be used to create a resource such as the following:
|
||||
|
||||
```xml
|
||||
<Patient xmlns="http://hl7.org/fhir">
|
||||
<id value="123"/>
|
||||
<extension url="http://acme.org/fooParent">
|
||||
<extension url="http://acme.org/fooChildA">
|
||||
<valueString value="ValueA"/>
|
||||
</extension>
|
||||
<extension url="http://acme.org/fooChildB">
|
||||
<valueString value="ValueB"/>
|
||||
</extension>
|
||||
</extension>
|
||||
</Patient>
|
||||
```
|
||||
|
||||
# Custom Resource Structure
|
||||
|
||||
The following example shows a custom resource structure class:
|
||||
The following example shows a custom resource structure class creating an entirely new resource type as opposed to simply extending an existing one. Note that this is allowable in FHIR, but is **highly discouraged** as they are by definition not good for interoperability.
|
||||
|
||||
```java
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/customtype/CustomResource.java|resource}}
|
||||
|
|
|
@ -63,22 +63,32 @@ Then create a properties file which describes your templates. In this properties
|
|||
The first (name.class) defines the class name of the resource to define a template for. The second (name.narrative) defines the path/classpath to the template file. The format of this path is `file:/path/foo.html` or `classpath:/com/classpath/foo.html`.
|
||||
|
||||
```properties
|
||||
# Two property lines in the file per template
|
||||
# Two property lines in the file per template. There are several forms you
|
||||
# can use. This first form assigns a template type to a resource by
|
||||
# resource name
|
||||
practitioner.resourceType=Practitioner
|
||||
practitioner.narrative=file:src/test/resources/narrative/Practitioner.html
|
||||
practitioner.narrative=classpath:com/example/narrative/Practitioner.html
|
||||
|
||||
observation.class=ca.uhn.fhir.model.dstu.resource.Observation
|
||||
observation.narrative=file:src/test/resources/narrative/Observation.html
|
||||
# This second form assigns a template by class name. This can be used for
|
||||
# HAPI FHIR built-in structures, or for custom structures as well.
|
||||
observation.class=org.hl7.fhir.r4.model.Observation
|
||||
observation.narrative=classpath:com/example/narrative/Observation.html
|
||||
|
||||
# etc...
|
||||
# You can also assign a template based on profile ID (Resource.meta.profile)
|
||||
vitalsigns.profile=http://hl7.org/fhir/StructureDefinition/vitalsigns
|
||||
vitalsigns.narrative=classpath:com/example/narrative/Observation_Vitals.html
|
||||
```
|
||||
|
||||
You may also override/define behaviour for datatypes. These datatype narrative definitions will be used as content within <code>th:narrative</code> blocks in resource templates. See the example resource template above for an example.
|
||||
You may also override/define behaviour for datatypes and other structures. These datatype narrative definitions will be used as content within <code>th:narrative</code> blocks in resource templates. See the example resource template above for an example.
|
||||
|
||||
```properties
|
||||
# datatypes use the same format as resources
|
||||
humanname.resourceType=HumanNameDt
|
||||
humanname.narrative=classpath:ca/uhn/fhir/narrative/HumanNameDt.html]]></source>
|
||||
# You can create a template based on a type name
|
||||
quantity.dataType=Quantity
|
||||
quantity.narrative=classpath:com/example/narrative/Quantity.html
|
||||
|
||||
# Or by class name, which can be useful for custom datatypes and structures
|
||||
custom_extension.class=com.example.model.MyCustomExtension
|
||||
custom_extension.narrative=classpath:com/example/narrative/CustomExtension.html
|
||||
```
|
||||
|
||||
Finally, use the [CustomThymeleafNarrativeGenerator](/hapi-fhir/apidocs/hapi-fhir-base/ca/uhn/fhir/narrative/CustomThymeleafNarrativeGenerator.html) and provide it to the FhirContext.
|
||||
|
|
|
@ -70,105 +70,8 @@ HAPI provides a few ways of accessing extension values in resources which are re
|
|||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/ExtensionsDstu3.java|parseExtension}}
|
||||
```
|
||||
|
||||
# Custom Resource Types
|
||||
# Custom Resource Structures
|
||||
|
||||
The most elegant way of adding extensions to a resource is through the use of custom fields. The following example shows a custom type which extends the FHIR Patient resource definition through two extensions.
|
||||
All of the examples on this page show how to work with the existing data model classes.
|
||||
|
||||
```java
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/MyPatient.java|patientDef}}
|
||||
```
|
||||
|
||||
Using this custom type is as simple as instantiating the type and working with the new fields.
|
||||
|
||||
```java
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/MyPatientUse.java|patientUse}}
|
||||
```
|
||||
|
||||
This example produces the following output:
|
||||
|
||||
```xml
|
||||
<Patient xmlns="http://hl7.org/fhir">
|
||||
<modifierExtension url="http://example.com/dontuse#importantDates">
|
||||
<valueDateTime value="2010-01-02"/>
|
||||
</modifierExtension>
|
||||
<modifierExtension url="http://example.com/dontuse#importantDates">
|
||||
<valueDateTime value="2014-01-26T11:11:11"/>
|
||||
</modifierExtension>
|
||||
<extension url="http://example.com/dontuse#petname">
|
||||
<valueString value="Fido"/>
|
||||
</extension>
|
||||
<name>
|
||||
<family value="Smith"/>
|
||||
<given value="John"/>
|
||||
<given value="Quincy"/>
|
||||
<suffix value="Jr"/>
|
||||
</name>
|
||||
</Patient>
|
||||
```
|
||||
|
||||
Parsing messages using your new custom type is equally simple. These types can also be used as method return types in clients and servers.
|
||||
|
||||
```java
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/MyPatientUse.java|patientParse}}
|
||||
```
|
||||
|
||||
## Using Custom Types in a Client
|
||||
|
||||
If you are using a client and wish to use a specific custom structure, you may simply use the custom structure as you would a build in HAPI type.
|
||||
|
||||
```java
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/ExtensionsDstu3.java|customTypeClientSimple}}
|
||||
```
|
||||
|
||||
You may also explicitly use custom types in searches and other operations which return resources.
|
||||
|
||||
```java
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/ExtensionsDstu3.java|customTypeClientSearch}}
|
||||
```
|
||||
|
||||
You can also explicitly declare a preferred response resource custom type. This is useful for some operations that do not otherwise declare their resource types in the method signature.
|
||||
|
||||
```java
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/ExtensionsDstu3.java|customTypeClientSearch2}}
|
||||
```
|
||||
|
||||
## Using Multiple Custom Types in a Client
|
||||
|
||||
Sometimes you may not know in advance exactly which type you will be receiving. For example, there are Patient resources which conform to several different profiles on a server and you aren't sure which profile you will get back for a specific read, you can declare the "primary" type for a given profile.
|
||||
|
||||
This is declared at the FhirContext level, and will apply to any clients created from this context (including clients created before the default was set).
|
||||
|
||||
```java
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/ExtensionsDstu3.java|customTypeClientDeclared}}
|
||||
```
|
||||
## Using Custom Types in a Server
|
||||
|
||||
If you are using a client and wish to use a specific custom structure, you may simply use the custom structure as you would a build in HAPI type.
|
||||
|
||||
```java
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/ExtensionsDstu3.java|customTypeClientSimple}}
|
||||
```
|
||||
|
||||
## Custom Type Examples: Composite Extensions
|
||||
|
||||
The following example shows a resource containing a composite extension.
|
||||
|
||||
```java
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/customtype/CustomCompositeExtension.java|resource}}
|
||||
```
|
||||
|
||||
This could be used to create a resource such as the following:
|
||||
|
||||
```xml
|
||||
<Patient xmlns="http://hl7.org/fhir">
|
||||
<id value="123"/>
|
||||
<extension url="http://acme.org/fooParent">
|
||||
<extension url="http://acme.org/fooChildA">
|
||||
<valueString value="ValueA"/>
|
||||
</extension>
|
||||
<extension url="http://acme.org/fooChildB">
|
||||
<valueString value="ValueB"/>
|
||||
</extension>
|
||||
</extension>
|
||||
</Patient>
|
||||
```
|
||||
This is a great way to work with extensions, and most HAPI FHIR applications use the techniques described on this page. However, there is a more advanced technique available as well, involving the creation of custom Java classes that extend the built-in classes to add statically bound extensions (as oppoed to the dynamically bound ones shown on this page). See [Custom Structures](./custom_structures.html) for more information.
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
# MDM Expansion
|
||||
|
||||
Once you have MDM enabled, and you have many linked resources, it can be useful to search across all linked resources. Let's say you have the following MDM links in your database:
|
||||
```bash
|
||||
Patient/1 --> Patient/3
|
||||
Patient/2 --> Patient/3
|
||||
```
|
||||
This indicates that both Patient/1 and Patient/2 are MDM-matched to the same golden resource (Patient/3).
|
||||
What if you want to get all observations from Patient/1, but also include any observations from all of their linked resources. You could do this by first querying the [$mdm-query-links](/docs/server_jpa_mdm/mdm_operations.html) endpoint, and then making a subsequent call like the following
|
||||
```http request
|
||||
GET http://example.com:8000/Observation?subject=Patient/1,Patient/2,Patient/3
|
||||
```
|
||||
|
||||
But HAPI-FHIR allows a shorthand for this, by means of a Search Parameter qualifier, as follows:
|
||||
```http request
|
||||
GET http://example.com:8000/Observation?subject:mdm=Patient/1
|
||||
```
|
||||
|
||||
This `:mdm` parameter qualifier instructs an interceptor in HAPI fhir to expand the set of resources included in the search by their MDM-matched resources. The two above HTTP requests will return the same result.
|
||||
|
||||
|
||||
<div class="helpWarningCalloutBox">
|
||||
One important caveat is that chaining is currently not supported when using this prefix.
|
||||
</div>
|
||||
|
||||
## Enabling MDM Expansion
|
||||
|
||||
On top of needing to instantiate an MDM module, you must enable this feature in the [DaoConfig](/hapi-fhir/apidocs/hapi-fhir-jpaserver-api/ca/uhn/fhir/jpa/api/config/DaoConfig.html) bean, using the [Allow MDM Expansion](/hapi-fhir/apidocs/hapi-fhir-jpaserver-api/ca/uhn/fhir/jpa/api/config/DaoConfig.html#setAllowMdmExpansion(boolean)) property.
|
||||
|
||||
<div class="helpWarningCalloutBox">
|
||||
It is important to note that enabling this functionality can lead to incorrect data being returned by a request, if your MDM links are incorrect. Use with caution.
|
||||
</div>
|
||||
|
|
@ -1716,6 +1716,36 @@ public class DaoConfig {
|
|||
this.myModelConfig.setAllowContainsSearches(theAllowContainsSearches);
|
||||
}
|
||||
|
||||
/**
|
||||
* If enabled, the server will support the use of :mdm search parameter qualifier on Reference Search Parameters.
|
||||
* This Parameter Qualifier is HAPI-specific, and not defined anywhere in the FHIR specification. Using this qualifier
|
||||
* will result in an MDM expansion being done on the reference, which will expand the search scope. For example, if Patient/1
|
||||
* is MDM-matched to Patient/2 and you execute the search:
|
||||
* Observation?subject:mdm=Patient/1 , you will receive observations for both Patient/1 and Patient/2.
|
||||
* <p>
|
||||
* Default is <code>false</code>
|
||||
* </p>
|
||||
* @since 5.4.0
|
||||
*/
|
||||
public boolean isAllowMdmExpansion() {
|
||||
return myModelConfig.isAllowMdmExpansion();
|
||||
}
|
||||
|
||||
/**
|
||||
* If enabled, the server will support the use of :mdm search parameter qualifier on Reference Search Parameters.
|
||||
* This Parameter Qualifier is HAPI-specific, and not defined anywhere in the FHIR specification. Using this qualifier
|
||||
* will result in an MDM expansion being done on the reference, which will expand the search scope. For example, if Patient/1
|
||||
* is MDM-matched to Patient/2 and you execute the search:
|
||||
* Observation?subject:mdm=Patient/1 , you will receive observations for both Patient/1 and Patient/2.
|
||||
* <p>
|
||||
* Default is <code>false</code>
|
||||
* </p>
|
||||
* @since 5.4.0
|
||||
*/
|
||||
public void setAllowMdmExpansion(boolean theAllowMdmExpansion) {
|
||||
myModelConfig.setAllowMdmExpansion(theAllowMdmExpansion);
|
||||
}
|
||||
|
||||
/**
|
||||
* This setting may be used to advise the server that any references found in
|
||||
* resources that have any of the base URLs given here will be replaced with
|
||||
|
|
|
@ -41,6 +41,7 @@ import ca.uhn.fhir.jpa.dao.index.DaoResourceLinkResolver;
|
|||
import ca.uhn.fhir.jpa.dao.index.DaoSearchParamSynchronizer;
|
||||
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
|
||||
import ca.uhn.fhir.jpa.dao.index.SearchParamWithInlineReferencesExtractor;
|
||||
import ca.uhn.fhir.jpa.dao.mdm.MdmLinkExpandSvc;
|
||||
import ca.uhn.fhir.jpa.dao.predicate.PredicateBuilder;
|
||||
import ca.uhn.fhir.jpa.dao.predicate.PredicateBuilderCoords;
|
||||
import ca.uhn.fhir.jpa.dao.predicate.PredicateBuilderDate;
|
||||
|
@ -60,6 +61,7 @@ import ca.uhn.fhir.jpa.entity.Search;
|
|||
import ca.uhn.fhir.jpa.graphql.JpaStorageServices;
|
||||
import ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor;
|
||||
import ca.uhn.fhir.jpa.interceptor.JpaConsentContextServices;
|
||||
import ca.uhn.fhir.jpa.interceptor.MdmSearchExpandingInterceptor;
|
||||
import ca.uhn.fhir.jpa.interceptor.OverridePathBasedReferentialIntegrityForDeletesInterceptor;
|
||||
import ca.uhn.fhir.rest.server.interceptor.ResponseTerminologyTranslationInterceptor;
|
||||
import ca.uhn.fhir.jpa.interceptor.validation.RepositoryValidatingRuleBuilder;
|
||||
|
@ -473,6 +475,17 @@ public abstract class BaseConfig {
|
|||
return new RequestTenantPartitionInterceptor();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Lazy
|
||||
public MdmSearchExpandingInterceptor mdmSearchExpandingInterceptor() {
|
||||
return new MdmSearchExpandingInterceptor();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MdmLinkExpandSvc myMdmLinkExpandSvc() {
|
||||
return new MdmLinkExpandSvc();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Lazy
|
||||
public TerminologyUploaderProvider terminologyUploaderProvider() {
|
||||
|
|
|
@ -58,4 +58,13 @@ public interface IMdmLinkDao extends JpaRepository<MdmLink, Long> {
|
|||
Long getSourcePid();
|
||||
}
|
||||
|
||||
@Query("SELECT ml.myGoldenResourcePid as goldenPid, ml.mySourcePid as sourcePid " +
|
||||
"FROM MdmLink ml " +
|
||||
"INNER JOIN MdmLink ml2 " +
|
||||
"on ml.myGoldenResourcePid=ml2.myGoldenResourcePid " +
|
||||
"WHERE ml2.mySourcePid=:sourcePid " +
|
||||
"AND ml2.myMatchResult=:matchResult " +
|
||||
"AND ml.myMatchResult=:matchResult")
|
||||
List<MdmPidTuple> expandPidsBySourcePidAndMatchResult(@Param("sourcePid") Long theSourcePid, @Param("matchResult") MdmMatchResultEnum theMdmMatchResultEnum);
|
||||
|
||||
}
|
||||
|
|
|
@ -380,6 +380,29 @@ public class IdHelperService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Given a set of PIDs, return a set of public FHIR Resource IDs.
|
||||
* This function will resolve a forced ID if it resolves, and if it fails to resolve to a forced it, will just return the pid
|
||||
* Example:
|
||||
* Let's say we have Patient/1(pid == 1), Patient/pat1 (pid == 2), Patient/3 (pid == 3), their pids would resolve as follows:
|
||||
*
|
||||
* [1,2,3] -> ["1","pat1","3"]
|
||||
*
|
||||
* @param thePids The Set of pids you would like to resolve to external FHIR Resource IDs.
|
||||
* @return A Set of strings representing the FHIR IDs of the pids.
|
||||
*/
|
||||
public Set<String> translatePidsToFhirResourceIds(Set<Long> thePids) {
|
||||
Map<Long, Optional<String>> pidToForcedIdMap = translatePidsToForcedIds(thePids);
|
||||
|
||||
//If the result of the translation is an empty optional, it means there is no forced id, and we can use the PID as the resource ID.
|
||||
Set<String> resolvedResourceIds = pidToForcedIdMap.entrySet().stream()
|
||||
.map(entry -> entry.getValue().isPresent() ? entry.getValue().get() : entry.getKey().toString())
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
return resolvedResourceIds;
|
||||
|
||||
}
|
||||
public Map<Long, Optional<String>> translatePidsToForcedIds(Set<Long> thePids) {
|
||||
Map<Long, Optional<String>> retVal = new HashMap<>(myMemoryCacheService.getAllPresent(MemoryCacheService.CacheEnum.FORCED_ID, thePids));
|
||||
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
package ca.uhn.fhir.jpa.dao.mdm;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.jpa.dao.data.IMdmLinkDao;
|
||||
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
|
||||
import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
|
||||
import ca.uhn.fhir.mdm.log.Logs;
|
||||
import ca.uhn.fhir.model.primitive.IdDt;
|
||||
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
public class MdmLinkExpandSvc {
|
||||
private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
|
||||
|
||||
@Autowired
|
||||
private IMdmLinkDao myMdmLinkDao;
|
||||
@Autowired
|
||||
private IdHelperService myIdHelperService;
|
||||
|
||||
/**
|
||||
* Given a source resource, perform MDM expansion and return all the resource IDs of all resources that are
|
||||
* MDM-Matched to this resource.
|
||||
*
|
||||
* @param theResource The resource to MDM-Expand
|
||||
* @return A set of strings representing the FHIR IDs of the expanded resources.
|
||||
*/
|
||||
public Set<String> expandMdmBySourceResource(IBaseResource theResource) {
|
||||
ourLog.debug("About to MDM-expand source resource {}", theResource);
|
||||
return expandMdmBySourceResourceId(theResource.getIdElement());
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a resource ID of a source resource, perform MDM expansion and return all the resource IDs of all resources that are
|
||||
* MDM-Matched to this resource.
|
||||
*
|
||||
* @param theId The Resource ID of the resource to MDM-Expand
|
||||
* @return A set of strings representing the FHIR ids of the expanded resources.
|
||||
*/
|
||||
public Set<String> expandMdmBySourceResourceId(IIdType theId) {
|
||||
ourLog.debug("About to expand source resource with resource id {}", theId);
|
||||
Long pidOrThrowException = myIdHelperService.getPidOrThrowException(theId);
|
||||
return expandMdmBySourceResourcePid(pidOrThrowException);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a PID of a source resource, perform MDM expansion and return all the resource IDs of all resources that are
|
||||
* MDM-Matched to this resource.
|
||||
*
|
||||
* @param theSourceResourcePid The PID of the resource to MDM-Expand
|
||||
* @return A set of strings representing the FHIR ids of the expanded resources.
|
||||
*/
|
||||
public Set<String> expandMdmBySourceResourcePid(Long theSourceResourcePid) {
|
||||
ourLog.debug("About to expand source resource with PID {}", theSourceResourcePid);
|
||||
List<IMdmLinkDao.MdmPidTuple> goldenPidSourcePidTuples = myMdmLinkDao.expandPidsBySourcePidAndMatchResult(theSourceResourcePid, MdmMatchResultEnum.MATCH);
|
||||
Set<Long> flattenedPids = new HashSet<>();
|
||||
goldenPidSourcePidTuples.forEach(tuple -> {
|
||||
flattenedPids.add(tuple.getSourcePid());
|
||||
flattenedPids.add(tuple.getGoldenPid());
|
||||
});
|
||||
Set<String> resourceIds = myIdHelperService.translatePidsToFhirResourceIds(flattenedPids);
|
||||
ourLog.debug("Pid {} has been expanded to [{}]", theSourceResourcePid, String.join(",", resourceIds));
|
||||
return resourceIds;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
package ca.uhn.fhir.jpa.interceptor;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.interceptor.api.Hook;
|
||||
import ca.uhn.fhir.interceptor.api.Interceptor;
|
||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.dao.mdm.MdmLinkExpandSvc;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.mdm.log.Logs;
|
||||
import ca.uhn.fhir.model.api.IQueryParameterType;
|
||||
import ca.uhn.fhir.model.primitive.IdDt;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.param.ReferenceParam;
|
||||
import joptsimple.internal.Strings;
|
||||
import org.slf4j.Logger;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.slf4j.LoggerFactory.getLogger;
|
||||
|
||||
/**
|
||||
* This interceptor replaces the auto-generated CapabilityStatement that is generated
|
||||
* by the HAPI FHIR Server with a static hard-coded resource.
|
||||
*/
|
||||
@Interceptor
|
||||
public class MdmSearchExpandingInterceptor {
|
||||
private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
|
||||
|
||||
@Autowired
|
||||
private MdmLinkExpandSvc myMdmLinkExpandSvc;
|
||||
|
||||
@Autowired
|
||||
private DaoConfig myDaoConfig;
|
||||
|
||||
@Hook(Pointcut.STORAGE_PRESEARCH_REGISTERED)
|
||||
public void hook(SearchParameterMap theSearchParameterMap) {
|
||||
if (myDaoConfig.isAllowMdmExpansion()) {
|
||||
for (List<List<IQueryParameterType>> andList : theSearchParameterMap.values()) {
|
||||
for (List<IQueryParameterType> orList : andList) {
|
||||
expandAnyReferenceParameters(orList);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If a Parameter is a reference parameter, and it has been set to expand MDM, perform the expansion.
|
||||
*/
|
||||
private void expandAnyReferenceParameters(List<IQueryParameterType> orList) {
|
||||
List<IQueryParameterType> toRemove = new ArrayList<>();
|
||||
List<IQueryParameterType> toAdd = new ArrayList<>();
|
||||
for (IQueryParameterType iQueryParameterType : orList) {
|
||||
if (iQueryParameterType instanceof ReferenceParam) {
|
||||
ReferenceParam refParam = (ReferenceParam) iQueryParameterType;
|
||||
if (refParam.isMdmExpand()) {
|
||||
ourLog.debug("Found a reference parameter to expand: {}", refParam.toString());
|
||||
Set<String> expandedResourceIds = myMdmLinkExpandSvc.expandMdmBySourceResourceId(new IdDt(refParam.getValue()));
|
||||
if (!expandedResourceIds.isEmpty()) {
|
||||
ourLog.debug("Parameter has been expanded to: {}", String.join(", ", expandedResourceIds));
|
||||
toRemove.add(refParam);
|
||||
expandedResourceIds.stream().map(resourceId -> new ReferenceParam(refParam.getResourceType() + "/" + resourceId)).forEach(toAdd::add);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
orList.removeAll(toRemove);
|
||||
orList.addAll(toAdd);
|
||||
}
|
||||
}
|
|
@ -305,8 +305,18 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
|
|||
public IBundleProvider registerSearch(final IFhirResourceDao<?> theCallingDao, final SearchParameterMap theParams, String theResourceType, CacheControlDirective theCacheControlDirective, RequestDetails theRequestDetails, RequestPartitionId theRequestPartitionId) {
|
||||
final String searchUuid = UUID.randomUUID().toString();
|
||||
|
||||
final String queryString = theParams.toNormalizedQueryString(myContext);
|
||||
ourLog.debug("Registering new search {}", searchUuid);
|
||||
Search search = new Search();
|
||||
populateSearchEntity(theParams, theResourceType, searchUuid, queryString, search, theRequestPartitionId);
|
||||
|
||||
// Interceptor call: STORAGE_PRESEARCH_REGISTERED
|
||||
HookParams params = new HookParams()
|
||||
.add(ICachedSearchDetails.class, search)
|
||||
.add(RequestDetails.class, theRequestDetails)
|
||||
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
|
||||
.add(SearchParameterMap.class, theParams);
|
||||
JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_PRESEARCH_REGISTERED, params);
|
||||
Class<? extends IBaseResource> resourceTypeClass = myContext.getResourceDefinition(theResourceType).getImplementingClass();
|
||||
final ISearchBuilder sb = mySearchBuilderFactory.newSearchBuilder(theCallingDao, theResourceType, resourceTypeClass);
|
||||
sb.setFetchSize(mySyncSize);
|
||||
|
@ -327,7 +337,6 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
|
|||
cacheStatus = SearchCacheStatusEnum.NOT_TRIED;
|
||||
}
|
||||
|
||||
final String queryString = theParams.toNormalizedQueryString(myContext);
|
||||
if (cacheStatus != SearchCacheStatusEnum.NOT_TRIED) {
|
||||
if (theParams.getEverythingMode() == null) {
|
||||
if (myDaoConfig.getReuseCachedSearchResultsForMillis() != null) {
|
||||
|
@ -340,7 +349,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
|
|||
}
|
||||
}
|
||||
|
||||
PersistedJpaSearchFirstPageBundleProvider retVal = submitSearch(theCallingDao, theParams, theResourceType, theRequestDetails, searchUuid, sb, queryString, theRequestPartitionId);
|
||||
PersistedJpaSearchFirstPageBundleProvider retVal = submitSearch(theCallingDao, theParams, theResourceType, theRequestDetails, searchUuid, sb, queryString, theRequestPartitionId, search);
|
||||
retVal.setCacheStatus(cacheStatus);
|
||||
return retVal;
|
||||
|
||||
|
@ -379,23 +388,25 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
|
|||
}
|
||||
|
||||
@NotNull
|
||||
private PersistedJpaSearchFirstPageBundleProvider submitSearch(IDao theCallingDao, SearchParameterMap theParams, String theResourceType, RequestDetails theRequestDetails, String theSearchUuid, ISearchBuilder theSb, String theQueryString, RequestPartitionId theRequestPartitionId) {
|
||||
private PersistedJpaSearchFirstPageBundleProvider submitSearch(IDao theCallingDao, SearchParameterMap theParams, String theResourceType, RequestDetails theRequestDetails, String theSearchUuid, ISearchBuilder theSb, String theQueryString, RequestPartitionId theRequestPartitionId, Search theSearch) {
|
||||
StopWatch w = new StopWatch();
|
||||
Search search = new Search();
|
||||
populateSearchEntity(theParams, theResourceType, theSearchUuid, theQueryString, search, theRequestPartitionId);
|
||||
// Search search = new Search();
|
||||
//TODO GGG MOVE THIS POPULATE AND ALSO THE HOOK CALL HIGHER UP IN THE STACK.
|
||||
// populateSearchEntity(theParams, theResourceType, theSearchUuid, theQueryString, search, theRequestPartitionId);
|
||||
|
||||
// Interceptor call: STORAGE_PRESEARCH_REGISTERED
|
||||
HookParams params = new HookParams()
|
||||
.add(ICachedSearchDetails.class, search)
|
||||
.add(RequestDetails.class, theRequestDetails)
|
||||
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails);
|
||||
JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_PRESEARCH_REGISTERED, params);
|
||||
// Interceptor call: STORAGE_PRESEARCH_REGISTERED
|
||||
// HookParams params = new HookParams()
|
||||
// .add(ICachedSearchDetails.class, search)
|
||||
// .add(RequestDetails.class, theRequestDetails)
|
||||
// .addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
|
||||
// .add(SearchParameterMap.class, theParams);
|
||||
// JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_PRESEARCH_REGISTERED, params);
|
||||
|
||||
SearchTask task = new SearchTask(search, theCallingDao, theParams, theResourceType, theRequestDetails, theRequestPartitionId);
|
||||
myIdToSearchTask.put(search.getUuid(), task);
|
||||
SearchTask task = new SearchTask(theSearch, theCallingDao, theParams, theResourceType, theRequestDetails, theRequestPartitionId);
|
||||
myIdToSearchTask.put(theSearch.getUuid(), task);
|
||||
myExecutor.submit(task);
|
||||
|
||||
PersistedJpaSearchFirstPageBundleProvider retVal = myPersistedJpaBundleProviderFactory.newInstanceFirstPage(theRequestDetails, search, task, theSb);
|
||||
PersistedJpaSearchFirstPageBundleProvider retVal = myPersistedJpaBundleProviderFactory.newInstanceFirstPage(theRequestDetails, theSearch, task, theSb);
|
||||
|
||||
ourLog.debug("Search initial phase completed in {}ms", w.getMillis());
|
||||
return retVal;
|
||||
|
|
|
@ -643,8 +643,8 @@ public class SearchBuilder implements ISearchBuilder {
|
|||
*/
|
||||
if (resourcePidToVersion != null) {
|
||||
Long version = resourcePidToVersion.get(next.getResourceId());
|
||||
resourceId.setVersion(version);
|
||||
if (version != null && !version.equals(next.getVersion())) {
|
||||
resourceId.setVersion(version);
|
||||
IFhirResourceDao<? extends IBaseResource> dao = myDaoRegistry.getResourceDao(resourceType);
|
||||
next = dao.readEntity(next.getIdDt().withVersion(Long.toString(version)), null);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
package ca.uhn.fhir.jpa.search.helper;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class SearchParamHelper {
|
||||
|
||||
@Autowired
|
||||
private FhirContext myFhirContext;
|
||||
|
||||
|
||||
public Collection<RuntimeSearchParam> getPatientSearchParamsForResourceType(String theResourceType) {
|
||||
RuntimeResourceDefinition runtimeResourceDefinition = myFhirContext.getResourceDefinition(theResourceType);
|
||||
Map<String, RuntimeSearchParam> searchParams = new HashMap<>();
|
||||
|
||||
RuntimeSearchParam patientSearchParam = runtimeResourceDefinition.getSearchParam("patient");
|
||||
if (patientSearchParam != null) {
|
||||
searchParams.put(patientSearchParam.getName(), patientSearchParam);
|
||||
|
||||
}
|
||||
RuntimeSearchParam subjectSearchParam = runtimeResourceDefinition.getSearchParam("subject");
|
||||
if (subjectSearchParam != null) {
|
||||
searchParams.put(subjectSearchParam.getName(), subjectSearchParam);
|
||||
}
|
||||
|
||||
List<RuntimeSearchParam> compartmentSearchParams = getPatientCompartmentRuntimeSearchParams(runtimeResourceDefinition);
|
||||
compartmentSearchParams.forEach(param -> searchParams.put(param.getName(), param));
|
||||
|
||||
return searchParams.values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Search the resource definition for a compartment named 'patient' and return its related Search Parameter.
|
||||
*/
|
||||
public List<RuntimeSearchParam> getPatientCompartmentRuntimeSearchParams(RuntimeResourceDefinition runtimeResourceDefinition) {
|
||||
List<RuntimeSearchParam> patientSearchParam = new ArrayList<>();
|
||||
List<RuntimeSearchParam> searchParams = runtimeResourceDefinition.getSearchParamsForCompartmentName("Patient");
|
||||
return searchParams;
|
||||
// if (searchParams == null || searchParams.size() == 0) {
|
||||
// String errorMessage = String.format("Resource type [%s] is not eligible for this type of export, as it contains no Patient compartment, and no `patient` or `subject` search parameter", myResourceType);
|
||||
// throw new IllegalArgumentException(errorMessage);
|
||||
// } else if (searchParams.size() == 1) {
|
||||
// patientSearchParam = searchParams.get(0);
|
||||
// } else {
|
||||
// String errorMessage = String.format("Resource type [%s] is not eligible for Group Bulk export, as we are unable to disambiguate which patient search parameter we should be searching by.", myResourceType);
|
||||
// throw new IllegalArgumentException(errorMessage);
|
||||
// }
|
||||
// return patientSearchParam;
|
||||
}
|
||||
}
|
|
@ -2054,6 +2054,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
|
|||
@CoverageIgnore
|
||||
@Override
|
||||
public IValidationSupport.CodeValidationResult validateCode(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl) {
|
||||
//TODO GGG TRY TO JUST AUTO_PASS HERE AND SEE WHAT HAPPENS.
|
||||
invokeRunnableForUnitTest();
|
||||
|
||||
if (isNotBlank(theValueSetUrl)) {
|
||||
|
|
|
@ -74,6 +74,7 @@ public class JpaValidationSupportChain extends ValidationSupportChain {
|
|||
public void postConstruct() {
|
||||
addValidationSupport(myDefaultProfileValidationSupport);
|
||||
addValidationSupport(myJpaValidationSupport);
|
||||
//TODO MAKE SURE THAT THIS IS BEING CAL
|
||||
addValidationSupport(myTerminologyService);
|
||||
addValidationSupport(new SnapshotGeneratingValidationSupport(myFhirContext));
|
||||
addValidationSupport(new InMemoryTerminologyServerValidationSupport(myFhirContext));
|
||||
|
|
|
@ -134,6 +134,7 @@ import org.hl7.fhir.r4.model.DocumentReference;
|
|||
import org.hl7.fhir.r4.model.Encounter;
|
||||
import org.hl7.fhir.r4.model.Enumerations.ConceptMapEquivalence;
|
||||
import org.hl7.fhir.r4.model.EpisodeOfCare;
|
||||
import org.hl7.fhir.r4.model.ExplanationOfBenefit;
|
||||
import org.hl7.fhir.r4.model.Group;
|
||||
import org.hl7.fhir.r4.model.Immunization;
|
||||
import org.hl7.fhir.r4.model.ImmunizationRecommendation;
|
||||
|
@ -387,6 +388,9 @@ public abstract class BaseJpaR4Test extends BaseJpaTest implements ITestDataBuil
|
|||
@Qualifier("myPatientDaoR4")
|
||||
protected IFhirResourceDaoPatient<Patient> myPatientDao;
|
||||
@Autowired
|
||||
@Qualifier("myExplanationOfBenefitDaoR4")
|
||||
protected IFhirResourceDao<ExplanationOfBenefit> myExplanationOfBenefitDao;
|
||||
@Autowired
|
||||
protected IResourceTableDao myResourceTableDao;
|
||||
@Autowired
|
||||
protected IResourceHistoryTableDao myResourceHistoryTableDao;
|
||||
|
|
|
@ -38,6 +38,7 @@ import org.hl7.fhir.r4.model.DiagnosticReport;
|
|||
import org.hl7.fhir.r4.model.Encounter;
|
||||
import org.hl7.fhir.r4.model.Enumerations;
|
||||
import org.hl7.fhir.r4.model.Enumerations.AdministrativeGender;
|
||||
import org.hl7.fhir.r4.model.ExplanationOfBenefit;
|
||||
import org.hl7.fhir.r4.model.Extension;
|
||||
import org.hl7.fhir.r4.model.Group;
|
||||
import org.hl7.fhir.r4.model.IntegerType;
|
||||
|
@ -193,10 +194,8 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test
|
|||
|
||||
search = myPatientDao.search(SearchParameterMap.newSynchronous("future-appointment-count", new NumberParam("lt0")));
|
||||
assertEquals(0, search.size());
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Draft search parameters should be ok even if they aren't completely valid
|
||||
*/
|
||||
|
|
|
@ -8,14 +8,21 @@ import ca.uhn.fhir.util.BundleBuilder;
|
|||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r4.model.Bundle;
|
||||
import org.hl7.fhir.r4.model.Condition;
|
||||
import org.hl7.fhir.r4.model.Encounter;
|
||||
import org.hl7.fhir.r4.model.IdType;
|
||||
import org.hl7.fhir.r4.model.Observation;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.hl7.fhir.r4.model.Reference;
|
||||
import org.hl7.fhir.r4.model.Task;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
@ -397,6 +404,46 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test {
|
|||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSearchAndIncludeVersionedReference_WhenOnlyOneVersionExists() {
|
||||
HashSet<String> refPaths = new HashSet<String>();
|
||||
refPaths.add("Task.basedOn");
|
||||
myFhirCtx.getParserOptions().setDontStripVersionsFromReferencesAtPaths(refPaths);
|
||||
myModelConfig.setRespectVersionsForSearchIncludes(true);
|
||||
myFhirCtx.getParserOptions().setStripVersionsFromReferences(false);
|
||||
|
||||
// Create a Condition
|
||||
Condition condition = new Condition();
|
||||
IIdType conditionId = myConditionDao.create(condition).getId().toUnqualified();
|
||||
|
||||
// Create a Task which is basedOn that Condition
|
||||
Task task = new Task();
|
||||
task.setBasedOn(Arrays.asList(new Reference(conditionId)));
|
||||
IIdType taskId = myTaskDao.create(task).getId().toUnqualified();
|
||||
|
||||
// Search for the Task using an _include=Task.basedOn and make sure we get the Condition resource in the Response
|
||||
IBundleProvider outcome = myTaskDao.search(SearchParameterMap.newSynchronous().addInclude(Task.INCLUDE_BASED_ON));
|
||||
assertEquals(2, outcome.size());
|
||||
List<IBaseResource> resources = outcome.getResources(0, 2);
|
||||
assertEquals(2, resources.size(), resources.stream().map(t->t.getIdElement().toUnqualified().getValue()).collect(Collectors.joining(", ")));
|
||||
assertEquals(taskId.getValue(), resources.get(0).getIdElement().getValue());
|
||||
assertEquals(conditionId.getValue(), ((Task)resources.get(0)).getBasedOn().get(0).getReference());
|
||||
assertEquals(conditionId.withVersion("1").getValue(), resources.get(1).getIdElement().getValue());
|
||||
|
||||
// Now, update the Condition to generate another version of it
|
||||
condition.setRecordedDate(new Date(System.currentTimeMillis()));
|
||||
String conditionIdString = myConditionDao.update(condition).getId().getValue();
|
||||
|
||||
// Search for the Task again and make sure that we get the original version of the Condition resource in the Response
|
||||
outcome = myTaskDao.search(SearchParameterMap.newSynchronous().addInclude(Task.INCLUDE_BASED_ON));
|
||||
assertEquals(2, outcome.size());
|
||||
resources = outcome.getResources(0, 2);
|
||||
assertEquals(2, resources.size());
|
||||
assertEquals(taskId.getValue(), resources.get(0).getIdElement().getValue());
|
||||
assertEquals(conditionId.getValue(), ((Task)resources.get(0)).getBasedOn().get(0).getReference());
|
||||
assertEquals(conditionId.withVersion("1").getValue(), resources.get(1).getIdElement().getValue());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSearchAndIncludeUnersionedReference_Asynchronous() {
|
||||
|
|
|
@ -13,9 +13,11 @@ import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
|
|||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.rest.api.Constants;
|
||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||
import ca.uhn.fhir.rest.gclient.DateClientParam;
|
||||
import ca.uhn.fhir.rest.gclient.ReferenceClientParam;
|
||||
import ca.uhn.fhir.rest.gclient.StringClientParam;
|
||||
import ca.uhn.fhir.rest.gclient.TokenClientParam;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
||||
import ca.uhn.fhir.util.BundleUtil;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
@ -332,9 +334,9 @@ public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProvide
|
|||
|
||||
List<String> foundResources = toUnqualifiedVersionlessIdValues(bundle);
|
||||
assertThat(foundResources, contains(p1id.getValue()));
|
||||
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testSearchQualifiedWithCustomReferenceParam() {
|
||||
|
|
|
@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.mdm.config;
|
|||
*/
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.interceptor.MdmSearchExpandingInterceptor;
|
||||
import ca.uhn.fhir.jpa.mdm.svc.MdmSurvivorshipSvcImpl;
|
||||
import ca.uhn.fhir.mdm.api.IMdmControllerSvc;
|
||||
import ca.uhn.fhir.mdm.api.IMdmExpungeSvc;
|
||||
|
@ -79,6 +80,11 @@ public class MdmConsumerConfig {
|
|||
return new MdmStorageInterceptor();
|
||||
}
|
||||
|
||||
@Bean
|
||||
MdmSearchExpandingInterceptor myMdmSearchExpandingInterceptorInterceptor() {
|
||||
return new MdmSearchExpandingInterceptor();
|
||||
}
|
||||
|
||||
@Bean
|
||||
IMdmSurvivorshipService mdmSurvivorshipService() { return new MdmSurvivorshipSvcImpl(); }
|
||||
|
||||
|
|
|
@ -36,6 +36,8 @@ import ca.uhn.fhir.rest.api.server.RequestDetails;
|
|||
import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
|
||||
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.r4.model.Extension;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
@ -57,13 +59,10 @@ public class MdmStorageInterceptor implements IMdmStorageInterceptor {
|
|||
private EIDHelper myEIDHelper;
|
||||
@Autowired
|
||||
private IMdmSettings myMdmSettings;
|
||||
@Autowired
|
||||
private GoldenResourceHelper myGoldenResourceHelper;
|
||||
|
||||
|
||||
@Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED)
|
||||
public void blockManualResourceManipulationOnCreate(IBaseResource theBaseResource, RequestDetails theRequestDetails, ServletRequestDetails theServletRequestDetails) {
|
||||
|
||||
//If running in single EID mode, forbid multiple eids.
|
||||
if (myMdmSettings.isPreventMultipleEids()) {
|
||||
forbidIfHasMultipleEids(theBaseResource);
|
||||
|
|
|
@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.mdm.interceptor;
|
|||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.jpa.interceptor.MdmSearchExpandingInterceptor;
|
||||
import ca.uhn.fhir.mdm.api.IMdmSettings;
|
||||
import ca.uhn.fhir.mdm.log.Logs;
|
||||
import ca.uhn.fhir.interceptor.api.IInterceptorService;
|
||||
|
@ -41,6 +42,8 @@ public class MdmSubmitterInterceptorLoader {
|
|||
@Autowired
|
||||
private IMdmStorageInterceptor myIMdmStorageInterceptor;
|
||||
@Autowired
|
||||
private MdmSearchExpandingInterceptor myMdmSearchExpandingInterceptorInterceptor;
|
||||
@Autowired
|
||||
private IInterceptorService myInterceptorService;
|
||||
@Autowired
|
||||
private SubscriptionSubmitInterceptorLoader mySubscriptionSubmitInterceptorLoader;
|
||||
|
@ -53,6 +56,7 @@ public class MdmSubmitterInterceptorLoader {
|
|||
|
||||
myDaoConfig.addSupportedSubscriptionType(Subscription.SubscriptionChannelType.MESSAGE);
|
||||
myInterceptorService.registerInterceptor(myIMdmStorageInterceptor);
|
||||
myInterceptorService.registerInterceptor(myMdmSearchExpandingInterceptorInterceptor);
|
||||
ourLog.info("MDM interceptor registered");
|
||||
// We need to call SubscriptionSubmitInterceptorLoader.start() again in case there were no subscription types the first time it was called.
|
||||
mySubscriptionSubmitInterceptorLoader.start();
|
||||
|
|
|
@ -44,6 +44,7 @@ import org.hl7.fhir.r4.model.CodeableConcept;
|
|||
import org.hl7.fhir.r4.model.ContactPoint;
|
||||
import org.hl7.fhir.r4.model.DateType;
|
||||
import org.hl7.fhir.r4.model.Medication;
|
||||
import org.hl7.fhir.r4.model.Observation;
|
||||
import org.hl7.fhir.r4.model.Organization;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.hl7.fhir.r4.model.Practitioner;
|
||||
|
@ -96,6 +97,8 @@ abstract public class BaseMdmR4Test extends BaseJpaR4Test {
|
|||
@Autowired
|
||||
protected IFhirResourceDao<Practitioner> myPractitionerDao;
|
||||
@Autowired
|
||||
protected IFhirResourceDao<Observation> myObservationDao;
|
||||
@Autowired
|
||||
protected MdmResourceMatcherSvc myMdmResourceMatcherSvc;
|
||||
@Autowired
|
||||
protected IMdmLinkDao myMdmLinkDao;
|
||||
|
@ -182,7 +185,6 @@ abstract public class BaseMdmR4Test extends BaseJpaR4Test {
|
|||
Patient patient = (Patient) outcome.getResource();
|
||||
patient.setId(outcome.getId());
|
||||
return patient;
|
||||
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
|
|
|
@ -1,16 +1,28 @@
|
|||
package ca.uhn.fhir.jpa.mdm.dao;
|
||||
|
||||
import ca.uhn.fhir.jpa.dao.data.IMdmLinkDao;
|
||||
import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum;
|
||||
import ca.uhn.fhir.mdm.api.IMdmSettings;
|
||||
import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
|
||||
import ca.uhn.fhir.mdm.rules.json.MdmRulesJson;
|
||||
import ca.uhn.fhir.jpa.mdm.BaseMdmR4Test;
|
||||
import ca.uhn.fhir.jpa.entity.MdmLink;
|
||||
import ca.uhn.fhir.jpa.util.TestUtil;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.in;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.isIn;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
@ -48,4 +60,47 @@ public class MdmLinkDaoSvcTest extends BaseMdmR4Test {
|
|||
assertEquals(rules.getVersion(), newLink.getVersion());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExpandPidsWorks() {
|
||||
|
||||
Patient golden = createGoldenPatient();
|
||||
|
||||
//Create 10 linked patients.
|
||||
List<MdmLink> mdmLinks = new ArrayList<>();
|
||||
for (int i = 0; i < 10; i++) {
|
||||
mdmLinks.add(createPatientAndLinkTo(golden.getIdElement().getIdPartAsLong(), MdmMatchResultEnum.MATCH));
|
||||
}
|
||||
|
||||
//Now lets connect a few as just POSSIBLE_MATCHes and ensure they aren't returned.
|
||||
for (int i = 0 ; i < 5; i++) {
|
||||
createPatientAndLinkTo(golden.getIdElement().getIdPartAsLong(), MdmMatchResultEnum.POSSIBLE_MATCH);
|
||||
}
|
||||
|
||||
List<Long> expectedExpandedPids = mdmLinks.stream().map(MdmLink::getSourcePid).collect(Collectors.toList());
|
||||
|
||||
//SUT
|
||||
List<IMdmLinkDao.MdmPidTuple> lists = myMdmLinkDao.expandPidsBySourcePidAndMatchResult(mdmLinks.get(0).getSourcePid(), MdmMatchResultEnum.MATCH);
|
||||
|
||||
assertThat(lists, hasSize(10));
|
||||
|
||||
lists.stream()
|
||||
.forEach(tuple -> {
|
||||
assertThat(tuple.getGoldenPid(), is(equalTo(golden.getIdElement().getIdPartAsLong())));
|
||||
assertThat(tuple.getSourcePid(), is(in(expectedExpandedPids)));
|
||||
});
|
||||
}
|
||||
|
||||
private MdmLink createPatientAndLinkTo(Long thePatientPid, MdmMatchResultEnum theMdmMatchResultEnum) {
|
||||
Patient patient = createPatient();
|
||||
|
||||
MdmLink mdmLink = myMdmLinkDaoSvc.newMdmLink();
|
||||
mdmLink.setLinkSource(MdmLinkSourceEnum.MANUAL);
|
||||
mdmLink.setMatchResult(theMdmMatchResultEnum);
|
||||
mdmLink.setCreated(new Date());
|
||||
mdmLink.setUpdated(new Date());
|
||||
mdmLink.setGoldenResourcePid(thePatientPid);
|
||||
mdmLink.setSourcePid(myIdHelperService.getPidOrNull(patient));
|
||||
MdmLink saved= myMdmLinkDao.save(mdmLink);
|
||||
return saved;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,8 @@ import org.junit.jupiter.api.BeforeEach;
|
|||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.core.StringContains.containsString;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
@ -49,6 +51,8 @@ public class MdmExpungeTest extends BaseMdmR4Test {
|
|||
saveLink(mdmLink);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
public void testUninterceptedDeleteRemovesMdmReference() {
|
||||
assertEquals(1, myMdmLinkDao.count());
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
package ca.uhn.fhir.jpa.mdm.interceptor;
|
||||
|
||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
|
||||
import ca.uhn.fhir.jpa.mdm.BaseMdmR4Test;
|
||||
import ca.uhn.fhir.jpa.mdm.helper.MdmHelperConfig;
|
||||
import ca.uhn.fhir.jpa.mdm.helper.MdmHelperR4;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.mdm.api.MdmConstants;
|
||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||
import ca.uhn.fhir.rest.param.ReferenceOrListParam;
|
||||
import ca.uhn.fhir.rest.param.ReferenceParam;
|
||||
import org.hl7.fhir.r4.model.CodeableConcept;
|
||||
import org.hl7.fhir.r4.model.Observation;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.hl7.fhir.r4.model.Reference;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
import org.slf4j.Logger;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.slf4j.LoggerFactory.getLogger;
|
||||
|
||||
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
|
||||
@ContextConfiguration(classes = {MdmHelperConfig.class})
|
||||
public class MdmSearchExpandingInterceptorIT extends BaseMdmR4Test {
|
||||
|
||||
private static final Logger ourLog = getLogger(MdmSearchExpandingInterceptorIT.class);
|
||||
|
||||
@RegisterExtension
|
||||
@Autowired
|
||||
public MdmHelperR4 myMdmHelper;
|
||||
@Autowired
|
||||
private DaoConfig myDaoConfig;
|
||||
|
||||
@Test
|
||||
public void testReferenceExpansionWorks() throws InterruptedException {
|
||||
myDaoConfig.setAllowMdmExpansion(false);
|
||||
MdmHelperR4.OutcomeAndLogMessageWrapper withLatch = myMdmHelper.createWithLatch(addExternalEID(buildJanePatient(), "123"));
|
||||
MdmHelperR4.OutcomeAndLogMessageWrapper withLatch1 = myMdmHelper.createWithLatch(addExternalEID(buildJanePatient(), "123"));
|
||||
MdmHelperR4.OutcomeAndLogMessageWrapper withLatch2 = myMdmHelper.createWithLatch(addExternalEID(buildJanePatient(), "123"));
|
||||
MdmHelperR4.OutcomeAndLogMessageWrapper withLatch3 = myMdmHelper.createWithLatch(addExternalEID(buildJanePatient(), "123"));
|
||||
|
||||
assertLinkCount(4);
|
||||
|
||||
String id = withLatch.getDaoMethodOutcome().getId().getIdPart();
|
||||
String id1 = withLatch1.getDaoMethodOutcome().getId().getIdPart();
|
||||
String id2 = withLatch2.getDaoMethodOutcome().getId().getIdPart();
|
||||
String id3 = withLatch3.getDaoMethodOutcome().getId().getIdPart();
|
||||
|
||||
//Create an Observation for each Patient
|
||||
createObservationWithSubject(id);
|
||||
createObservationWithSubject(id1);
|
||||
createObservationWithSubject(id2);
|
||||
createObservationWithSubject(id3);
|
||||
|
||||
SearchParameterMap searchParameterMap = new SearchParameterMap();
|
||||
searchParameterMap.setLoadSynchronous(true);
|
||||
ReferenceOrListParam referenceOrListParam = new ReferenceOrListParam();
|
||||
referenceOrListParam.addOr(new ReferenceParam("Patient/" + id).setMdmExpand(true));
|
||||
searchParameterMap.add(Observation.SP_SUBJECT, referenceOrListParam);
|
||||
|
||||
//With MDM Expansion disabled, this should return 1 result.
|
||||
IBundleProvider search = myObservationDao.search(searchParameterMap);
|
||||
assertThat(search.size(), is(equalTo(1)));
|
||||
|
||||
//Once MDM Expansion is allowed, this should now return 4 resourecs.
|
||||
myDaoConfig.setAllowMdmExpansion(true);
|
||||
search = myObservationDao.search(searchParameterMap);
|
||||
assertThat(search.size(), is(equalTo(4)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReferenceExpansionQuietlyFailsOnMissingMdmMatches() {
|
||||
myDaoConfig.setAllowMdmExpansion(true);
|
||||
Patient patient = buildJanePatient();
|
||||
patient.getMeta().addTag(MdmConstants.SYSTEM_MDM_MANAGED, MdmConstants.CODE_NO_MDM_MANAGED, "Don't MDM on me!");
|
||||
DaoMethodOutcome daoMethodOutcome = myMdmHelper.doCreateResource(patient, true);
|
||||
String id = daoMethodOutcome.getId().getIdPart();
|
||||
createObservationWithSubject(id);
|
||||
|
||||
//Even though the user has NO mdm links, that should not cause a request failure.
|
||||
SearchParameterMap map = new SearchParameterMap();
|
||||
map.add(Observation.SP_SUBJECT, new ReferenceParam("Patient/" + id).setMdmExpand(true));
|
||||
IBundleProvider search = myObservationDao.search(map);
|
||||
assertThat(search.size(), is(equalTo(1)));
|
||||
}
|
||||
|
||||
private Observation createObservationWithSubject(String thePatientId) {
|
||||
Observation observation = new Observation();
|
||||
observation.setSubject(new Reference("Patient/" + thePatientId));
|
||||
observation.setCode(new CodeableConcept().setText("Made for Patient/" + thePatientId));
|
||||
DaoMethodOutcome daoMethodOutcome = myObservationDao.create(observation);
|
||||
return (Observation) daoMethodOutcome.getResource();
|
||||
|
||||
}
|
||||
}
|
|
@ -6,10 +6,12 @@ import ca.uhn.fhir.jpa.entity.MdmLink;
|
|||
import ca.uhn.fhir.jpa.mdm.BaseMdmR4Test;
|
||||
import ca.uhn.fhir.jpa.mdm.helper.MdmHelperConfig;
|
||||
import ca.uhn.fhir.jpa.mdm.helper.MdmHelperR4;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.mdm.model.CanonicalEID;
|
||||
import ca.uhn.fhir.mdm.rules.config.MdmSettings;
|
||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
|
||||
import ca.uhn.fhir.rest.param.ReferenceParam;
|
||||
import ca.uhn.fhir.rest.server.TransactionLogMessages;
|
||||
import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
|
||||
import org.hl7.fhir.instance.model.api.IAnyResource;
|
||||
|
@ -65,6 +67,12 @@ public class MdmStorageInterceptorIT extends BaseMdmR4Test {
|
|||
assertLinkCount(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSearchExpandingInterceptorWorks() {
|
||||
SearchParameterMap subject = new SearchParameterMap("subject", new ReferenceParam("Patient/123").setMdmExpand(true)).setLoadSynchronous(true);
|
||||
myObservationDao.search(subject);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteGoldenResourceDeletesLinks() throws InterruptedException {
|
||||
myMdmHelper.createWithLatch(buildPaulPatient());
|
||||
|
|
|
@ -99,6 +99,7 @@ public class ModelConfig {
|
|||
private Map<String, Set<String>> myTypeToAutoVersionReferenceAtPaths = Collections.emptyMap();
|
||||
private boolean myRespectVersionsForSearchIncludes;
|
||||
private boolean myIndexOnContainedResources = false;
|
||||
private boolean myAllowMdmExpansion = false;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
|
@ -159,6 +160,36 @@ public class ModelConfig {
|
|||
return myAllowContainsSearches;
|
||||
}
|
||||
|
||||
/**
|
||||
* If enabled, the server will support the use of :mdm search parameter qualifier on Reference Search Parameters.
|
||||
* This Parameter Qualifier is HAPI-specific, and not defined anywhere in the FHIR specification. Using this qualifier
|
||||
* will result in an MDM expansion being done on the reference, which will expand the search scope. For example, if Patient/1
|
||||
* is MDM-matched to Patient/2 and you execute the search:
|
||||
* Observation?subject:mdm=Patient/1 , you will receive observations for both Patient/1 and Patient/2.
|
||||
* <p>
|
||||
* Default is <code>false</code>
|
||||
* </p>
|
||||
* @since 5.4.0
|
||||
*/
|
||||
public boolean isAllowMdmExpansion() {
|
||||
return myAllowMdmExpansion;
|
||||
}
|
||||
|
||||
/**
|
||||
* If enabled, the server will support the use of :mdm search parameter qualifier on Reference Search Parameters.
|
||||
* This Parameter Qualifier is HAPI-specific, and not defined anywhere in the FHIR specification. Using this qualifier
|
||||
* will result in an MDM expansion being done on the reference, which will expand the search scope. For example, if Patient/1
|
||||
* is MDM-matched to Patient/2 and you execute the search:
|
||||
* Observation?subject:mdm=Patient/1 , you will receive observations for both Patient/1 and Patient/2.
|
||||
* <p>
|
||||
* Default is <code>false</code>
|
||||
* </p>
|
||||
* @since 5.4.0
|
||||
*/
|
||||
public void setAllowMdmExpansion(boolean theAllowMdmExpansion) {
|
||||
myAllowMdmExpansion = theAllowMdmExpansion;
|
||||
}
|
||||
|
||||
/**
|
||||
* If enabled, the server will support the use of :contains searches,
|
||||
* which are helpful but can have adverse effects on performance.
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
package ca.uhn.fhir.narrative;
|
||||
|
||||
import ca.uhn.fhir.model.api.annotation.Child;
|
||||
import ca.uhn.fhir.model.api.annotation.Extension;
|
||||
import ca.uhn.fhir.model.api.annotation.ResourceDef;
|
||||
import ca.uhn.fhir.util.ElementUtil;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
|
||||
@ResourceDef(profile = "http://custom_patient")
|
||||
public class CustomPatient extends Patient {
|
||||
|
||||
@Child(name = "favouritePizzaExtension")
|
||||
@Extension(url = "http://example.com/favourite_pizza")
|
||||
private FavouritePizzaExtension myFavouritePizza;
|
||||
|
||||
public FavouritePizzaExtension getFavouritePizza() {
|
||||
return myFavouritePizza;
|
||||
}
|
||||
|
||||
public void setFavouritePizza(FavouritePizzaExtension theFavouritePizza) {
|
||||
myFavouritePizza = theFavouritePizza;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return super.isEmpty() && ElementUtil.isEmpty(myFavouritePizza);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
package ca.uhn.fhir.narrative;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.util.TestUtil;
|
||||
import org.hl7.fhir.r4.model.Practitioner;
|
||||
import org.hl7.fhir.r4.model.Quantity;
|
||||
import org.hl7.fhir.r4.model.StringType;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
public class CustomThymeleafNarrativeGeneratorR4Test {
|
||||
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(CustomThymeleafNarrativeGeneratorR4Test.class);
|
||||
|
||||
/** Don't use cached here since we modify the context */
|
||||
private FhirContext myCtx = FhirContext.forR4();
|
||||
|
||||
/**
|
||||
* Implement narrative for standard type
|
||||
*/
|
||||
@Test
|
||||
public void testStandardType() {
|
||||
|
||||
CustomThymeleafNarrativeGenerator gen = new CustomThymeleafNarrativeGenerator("classpath:narrative/standardtypes_r4.properties");
|
||||
myCtx.setNarrativeGenerator(gen);
|
||||
|
||||
Practitioner p = new Practitioner();
|
||||
p.addIdentifier().setSystem("sys").setValue("val1");
|
||||
p.addIdentifier().setSystem("sys").setValue("val2");
|
||||
p.addAddress().addLine("line1").addLine("line2");
|
||||
p.addName().setFamily("fam1").addGiven("given");
|
||||
|
||||
gen.populateResourceNarrative(myCtx, p);
|
||||
|
||||
String actual = p.getText().getDiv().getValueAsString();
|
||||
ourLog.info(actual);
|
||||
|
||||
assertThat(actual, containsString("<h1>Name</h1><div class=\"nameElement\">given <b>FAM1 </b></div><h1>Address</h1><div><span>line1 </span><br/><span>line2 </span><br/></div></div>"));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCustomType() {
|
||||
|
||||
CustomPatient patient = new CustomPatient();
|
||||
patient.setActive(true);
|
||||
FavouritePizzaExtension parentExtension = new FavouritePizzaExtension();
|
||||
parentExtension.setToppings(new StringType("Mushrooms, Onions"));
|
||||
parentExtension.setSize(new Quantity(null, 14, "http://unitsofmeasure", "[in_i]", "Inches"));
|
||||
patient.setFavouritePizza(parentExtension);
|
||||
|
||||
String output = myCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient);
|
||||
ourLog.info("Encoded: {}", output);
|
||||
|
||||
String expectedEncoding = "{\n" +
|
||||
" \"resourceType\": \"Patient\",\n" +
|
||||
" \"meta\": {\n" +
|
||||
" \"profile\": [ \"http://custom_patient\" ]\n" +
|
||||
" },\n" +
|
||||
" \"extension\": [ {\n" +
|
||||
" \"url\": \"http://example.com/favourite_pizza\",\n" +
|
||||
" \"extension\": [ {\n" +
|
||||
" \"url\": \"toppings\",\n" +
|
||||
" \"valueString\": \"Mushrooms, Onions\"\n" +
|
||||
" }, {\n" +
|
||||
" \"url\": \"size\",\n" +
|
||||
" \"valueQuantity\": {\n" +
|
||||
" \"value\": 14,\n" +
|
||||
" \"unit\": \"Inches\",\n" +
|
||||
" \"system\": \"http://unitsofmeasure\",\n" +
|
||||
" \"code\": \"[in_i]\"\n" +
|
||||
" }\n" +
|
||||
" } ]\n" +
|
||||
" } ],\n" +
|
||||
" \"active\": true\n" +
|
||||
"}";
|
||||
assertEquals(expectedEncoding, output);
|
||||
|
||||
CustomThymeleafNarrativeGenerator gen = new CustomThymeleafNarrativeGenerator("classpath:narrative/customtypes_r4.properties");
|
||||
myCtx.setNarrativeGenerator(gen);
|
||||
gen.populateResourceNarrative(myCtx, patient);
|
||||
|
||||
String actual = patient.getText().getDiv().getValueAsString();
|
||||
ourLog.info(actual);
|
||||
|
||||
String expected = "<div xmlns=\"http://www.w3.org/1999/xhtml\"><h1>CustomPatient</h1><div><div><h1>Favourite Pizza</h1> Toppings: <span>Mushrooms, Onions</span> Size: <span>14</span></div></div></div>";
|
||||
assertEquals(expected, actual);
|
||||
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void afterClassClearContext() {
|
||||
TestUtil.clearAllStaticFieldsForUnitTest();
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package ca.uhn.fhir.narrative;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.FhirVersionEnum;
|
||||
import ca.uhn.fhir.parser.DataFormatException;
|
||||
import ca.uhn.fhir.util.TestUtil;
|
||||
import org.hamcrest.core.StringContains;
|
||||
|
@ -21,7 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
|||
|
||||
public class DefaultThymeleafNarrativeGeneratorR4Test {
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(DefaultThymeleafNarrativeGeneratorR4Test.class);
|
||||
private static FhirContext ourCtx = FhirContext.forR4();
|
||||
private FhirContext myCtx = FhirContext.forCached(FhirVersionEnum.R4);
|
||||
private DefaultThymeleafNarrativeGenerator myGen;
|
||||
|
||||
@BeforeEach
|
||||
|
@ -29,7 +30,7 @@ public class DefaultThymeleafNarrativeGeneratorR4Test {
|
|||
myGen = new DefaultThymeleafNarrativeGenerator();
|
||||
myGen.setUseHapiServerConformanceNarrative(true);
|
||||
|
||||
ourCtx.setNarrativeGenerator(myGen);
|
||||
myCtx.setNarrativeGenerator(myGen);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -44,7 +45,7 @@ public class DefaultThymeleafNarrativeGeneratorR4Test {
|
|||
|
||||
value.setBirthDate(new Date());
|
||||
|
||||
myGen.populateResourceNarrative(ourCtx, value);
|
||||
myGen.populateResourceNarrative(myCtx, value);
|
||||
String output = value.getText().getDiv().getValueAsString();
|
||||
ourLog.info(output);
|
||||
assertThat(output, StringContains.containsString("<div class=\"hapiHeaderText\">joe john <b>BLOW </b></div>"));
|
||||
|
@ -60,7 +61,7 @@ public class DefaultThymeleafNarrativeGeneratorR4Test {
|
|||
value.addResult().setReference("Observation/2");
|
||||
value.addResult().setReference("Observation/3");
|
||||
|
||||
myGen.populateResourceNarrative(ourCtx, value);
|
||||
myGen.populateResourceNarrative(myCtx, value);
|
||||
String output = value.getText().getDiv().getValueAsString();
|
||||
|
||||
ourLog.info(output);
|
||||
|
@ -82,13 +83,13 @@ public class DefaultThymeleafNarrativeGeneratorR4Test {
|
|||
"</OperationOutcome>";
|
||||
//@formatter:on
|
||||
|
||||
OperationOutcome oo = ourCtx.newXmlParser().parseResource(OperationOutcome.class, parse);
|
||||
OperationOutcome oo = myCtx.newXmlParser().parseResource(OperationOutcome.class, parse);
|
||||
|
||||
// String output = gen.generateTitle(oo);
|
||||
// ourLog.info(output);
|
||||
// assertEquals("Operation Outcome (2 issues)", output);
|
||||
|
||||
myGen.populateResourceNarrative(ourCtx, oo);
|
||||
myGen.populateResourceNarrative(myCtx, oo);
|
||||
String output = oo.getText().getDiv().getValueAsString();
|
||||
|
||||
ourLog.info(output);
|
||||
|
@ -126,7 +127,7 @@ public class DefaultThymeleafNarrativeGeneratorR4Test {
|
|||
value.addResult().setResource(obs);
|
||||
}
|
||||
|
||||
myGen.populateResourceNarrative(ourCtx, value);
|
||||
myGen.populateResourceNarrative(myCtx, value);
|
||||
String output = value.getText().getDiv().getValueAsString();
|
||||
|
||||
ourLog.info(output);
|
||||
|
@ -189,8 +190,8 @@ public class DefaultThymeleafNarrativeGeneratorR4Test {
|
|||
" }";
|
||||
|
||||
|
||||
DiagnosticReport value = ourCtx.newJsonParser().parseResource(DiagnosticReport.class, input);
|
||||
myGen.populateResourceNarrative(ourCtx, value);
|
||||
DiagnosticReport value = myCtx.newJsonParser().parseResource(DiagnosticReport.class, input);
|
||||
myGen.populateResourceNarrative(myCtx, value);
|
||||
String output = value.getText().getDiv().getValueAsString();
|
||||
|
||||
ourLog.info(output);
|
||||
|
@ -210,7 +211,7 @@ public class DefaultThymeleafNarrativeGeneratorR4Test {
|
|||
mp.setStatus(MedicationRequestStatus.ACTIVE);
|
||||
mp.setAuthoredOnElement(new DateTimeType("2014-09-01"));
|
||||
|
||||
myGen.populateResourceNarrative(ourCtx, mp);
|
||||
myGen.populateResourceNarrative(myCtx, mp);
|
||||
String output = mp.getText().getDiv().getValueAsString();
|
||||
|
||||
assertTrue(output.contains("ciprofloaxin"), "Expected medication name of ciprofloaxin within narrative: " + output);
|
||||
|
@ -223,7 +224,7 @@ public class DefaultThymeleafNarrativeGeneratorR4Test {
|
|||
Medication med = new Medication();
|
||||
med.getCode().setText("ciproflaxin");
|
||||
|
||||
myGen.populateResourceNarrative(ourCtx, med);
|
||||
myGen.populateResourceNarrative(myCtx, med);
|
||||
|
||||
String output = med.getText().getDiv().getValueAsString();
|
||||
assertThat(output, containsString("ciproflaxin"));
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
package ca.uhn.fhir.narrative;
|
||||
|
||||
import ca.uhn.fhir.model.api.annotation.Block;
|
||||
import ca.uhn.fhir.model.api.annotation.Child;
|
||||
import ca.uhn.fhir.util.ElementUtil;
|
||||
import org.hl7.fhir.r4.model.BackboneElement;
|
||||
import org.hl7.fhir.r4.model.Quantity;
|
||||
import org.hl7.fhir.r4.model.StringType;
|
||||
|
||||
@Block
|
||||
public class FavouritePizzaExtension extends BackboneElement {
|
||||
|
||||
@Child(name = "childBazExtension")
|
||||
@ca.uhn.fhir.model.api.annotation.Extension(url = "toppings")
|
||||
private StringType myToppings;
|
||||
@Child(name = "childBarExtension")
|
||||
@ca.uhn.fhir.model.api.annotation.Extension(url = "size")
|
||||
private Quantity mySize;
|
||||
|
||||
@Override
|
||||
public BackboneElement copy() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return super.isEmpty() && ElementUtil.isEmpty(myToppings, mySize);
|
||||
}
|
||||
|
||||
public StringType getToppings() {
|
||||
return myToppings;
|
||||
}
|
||||
|
||||
public void setToppings(StringType theToppings) {
|
||||
myToppings = theToppings;
|
||||
}
|
||||
|
||||
public Quantity getSize() {
|
||||
return mySize;
|
||||
}
|
||||
|
||||
public void setSize(Quantity theSize) {
|
||||
mySize = theSize;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
<div>
|
||||
<h1>CustomPatient</h1>
|
||||
|
||||
<div th:narrative="${resource.favouritePizza}"></div>
|
||||
|
||||
</div>
|
|
@ -0,0 +1,10 @@
|
|||
<div>
|
||||
<h1>Favourite Pizza</h1>
|
||||
|
||||
Toppings:
|
||||
<span th:text="${resource.toppings.valueAsString}"/>
|
||||
|
||||
Size:
|
||||
<span th:text="${resource.size.value}"/>
|
||||
|
||||
</div>
|
|
@ -0,0 +1,15 @@
|
|||
|
||||
# Each resource to be defined has a pair or properties.
|
||||
#
|
||||
# The first (name.class) defines the class name of the
|
||||
# resource to define a template for
|
||||
#
|
||||
# The second (name.narrative) defines the path/classpath to the
|
||||
# template file.
|
||||
# Format is file:/path/foo.html or classpath:/com/classpath/foo.html
|
||||
#
|
||||
custompatient.class=ca.uhn.fhir.narrative.CustomPatient
|
||||
custompatient.narrative=classpath:narrative/customtypes_CustomPatientR4.html
|
||||
|
||||
favourite_pizza.class=ca.uhn.fhir.narrative.FavouritePizzaExtension
|
||||
favourite_pizza.narrative=classpath:narrative/customtypes_FavouritePizzaExtensionR4.html
|
|
@ -0,0 +1,24 @@
|
|||
<div>
|
||||
<!--
|
||||
Normal Thymeleaf tags apply. Here, we loop through each of the
|
||||
identifiers that the practitioner has, and create a DIV tag for
|
||||
each one, with the text "Identifier: [value]"
|
||||
-->
|
||||
<div th:each="identifier : ${resource.identifier}" th:text="'Identifier: ' + ${identifier.value}"></div>
|
||||
|
||||
<!--
|
||||
HAPI also defines a custom tag attribute, th:narrative="value",
|
||||
which is used to render a datatype into more HTML. In the example
|
||||
below, the value of ${resource.name} (which is the name property
|
||||
of the Practitioner resource, a HumanName datatype instance)
|
||||
is rendered, and its contents placed in a DIV tag. That DIV tag is
|
||||
then given a .nameElement CSS style.
|
||||
-->
|
||||
<h1>Name</h1>
|
||||
<div th:narrative="${resource.nameFirstRep}" class="nameElement"></div>
|
||||
|
||||
<h1>Address</h1>
|
||||
<div th:narrative="${resource.addressFirstRep}"></div>
|
||||
|
||||
|
||||
</div>
|
|
@ -0,0 +1,19 @@
|
|||
|
||||
# Each resource to be defined has a pair or properties.
|
||||
#
|
||||
# The first (name.class) defines the class name of the
|
||||
# resource to define a template for
|
||||
#
|
||||
# The second (name.narrative) defines the path/classpath to the
|
||||
# template file.
|
||||
# Format is file:/path/foo.html or classpath:/com/classpath/foo.html
|
||||
#
|
||||
practitioner.resourceType=Practitioner
|
||||
practitioner.narrative=classpath:narrative/standardtypes_PractitionerR4.html
|
||||
|
||||
# You may also override/define behaviour for datatypes
|
||||
humanname.dataType=HumanName
|
||||
humanname.narrative=classpath:ca/uhn/fhir/narrative/datatype/HumanNameDt.html
|
||||
|
||||
address.dataType=Address
|
||||
address.narrative=classpath:ca/uhn/fhir/narrative/datatype/AddressDt.html
|
Loading…
Reference in New Issue