Merge branch 'master' into issue-3144-multi-threaded-bundle-validation
This commit is contained in:
commit
72515c2094
|
@ -22,7 +22,12 @@ package ca.uhn.fhir.context;
|
|||
|
||||
import ca.uhn.fhir.parser.IParser;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* This object supplies default configuration to all {@link IParser parser} instances
|
||||
|
@ -37,31 +42,40 @@ public class ParserOptions {
|
|||
private boolean myStripVersionsFromReferences = true;
|
||||
private Set<String> myDontStripVersionsFromReferencesAtPaths = Collections.emptySet();
|
||||
private boolean myOverrideResourceIdWithBundleEntryFullUrl = true;
|
||||
private boolean myAutoContainReferenceTargetsWithNoId = true;
|
||||
|
||||
/**
|
||||
* If supplied value(s), any resource references at the specified paths will have their
|
||||
* resource versions encoded instead of being automatically stripped during the encoding
|
||||
* process. This setting has no effect on the parsing process.
|
||||
* If set to {@literal true} (which is the default), contained resources may be specified by
|
||||
* populating the target (contained) resource directly in {@link org.hl7.fhir.instance.model.api.IBaseReference#setReference(String)}
|
||||
* and the parser will automatically locate it and insert it into <code>Resource.contained</code> when
|
||||
* serializing. This is convenient, but also imposes a performance cost when serializing large numbers
|
||||
* of resources, so this can be disabled if it is not needed.
|
||||
* <p>
|
||||
* This method provides a finer-grained level of control than {@link #setStripVersionsFromReferences(boolean)}
|
||||
* and any paths specified by this method will be encoded even if {@link #setStripVersionsFromReferences(boolean)}
|
||||
* has been set to <code>true</code> (which is the default)
|
||||
* If disabled, only resources that are directly placed in <code>Resource.contained</code> will be
|
||||
* serialized.
|
||||
* </p>
|
||||
*
|
||||
* @param thePaths A collection of paths for which the resource versions will not be removed automatically
|
||||
* when serializing, e.g. "Patient.managingOrganization" or "AuditEvent.object.reference". Note that
|
||||
* only resource name and field names with dots separating is allowed here (no repetition
|
||||
* indicators, FluentPath expressions, etc.)
|
||||
* @return Returns a reference to <code>this</code> parser so that method calls can be chained together
|
||||
* @see #setStripVersionsFromReferences(boolean)
|
||||
* @since 6.0.0
|
||||
*/
|
||||
public ParserOptions setDontStripVersionsFromReferencesAtPaths(String... thePaths) {
|
||||
if (thePaths == null) {
|
||||
setDontStripVersionsFromReferencesAtPaths((List<String>) null);
|
||||
} else {
|
||||
setDontStripVersionsFromReferencesAtPaths(Arrays.asList(thePaths));
|
||||
}
|
||||
return this;
|
||||
public boolean isAutoContainReferenceTargetsWithNoId() {
|
||||
return myAutoContainReferenceTargetsWithNoId;
|
||||
}
|
||||
|
||||
/**
|
||||
* If set to {@literal true} (which is the default), contained resources may be specified by
|
||||
* populating the target (contained) resource directly in {@link org.hl7.fhir.instance.model.api.IBaseReference#setReference(String)}
|
||||
* and the parser will automatically locate it and insert it into <code>Resource.contained</code> when
|
||||
* serializing. This is convenient, but also imposes a performance cost when serializing large numbers
|
||||
* of resources, so this can be disabled if it is not needed.
|
||||
* <p>
|
||||
* If disabled, only resources that are directly placed in <code>Resource.contained</code> will be
|
||||
* serialized.
|
||||
* </p>
|
||||
*
|
||||
* @since 6.0.0
|
||||
*/
|
||||
public void setAutoContainReferenceTargetsWithNoId(boolean theAllowAutoContainedReferences) {
|
||||
myAutoContainReferenceTargetsWithNoId = theAllowAutoContainedReferences;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -109,6 +123,32 @@ public class ParserOptions {
|
|||
return myDontStripVersionsFromReferencesAtPaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* If supplied value(s), any resource references at the specified paths will have their
|
||||
* resource versions encoded instead of being automatically stripped during the encoding
|
||||
* process. This setting has no effect on the parsing process.
|
||||
* <p>
|
||||
* This method provides a finer-grained level of control than {@link #setStripVersionsFromReferences(boolean)}
|
||||
* and any paths specified by this method will be encoded even if {@link #setStripVersionsFromReferences(boolean)}
|
||||
* has been set to <code>true</code> (which is the default)
|
||||
* </p>
|
||||
*
|
||||
* @param thePaths A collection of paths for which the resource versions will not be removed automatically
|
||||
* when serializing, e.g. "Patient.managingOrganization" or "AuditEvent.object.reference". Note that
|
||||
* only resource name and field names with dots separating is allowed here (no repetition
|
||||
* indicators, FluentPath expressions, etc.)
|
||||
* @return Returns a reference to <code>this</code> parser so that method calls can be chained together
|
||||
* @see #setStripVersionsFromReferences(boolean)
|
||||
*/
|
||||
public ParserOptions setDontStripVersionsFromReferencesAtPaths(String... thePaths) {
|
||||
if (thePaths == null) {
|
||||
setDontStripVersionsFromReferencesAtPaths((List<String>) null);
|
||||
} else {
|
||||
setDontStripVersionsFromReferencesAtPaths(Arrays.asList(thePaths));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* If supplied value(s), any resource references at the specified paths will have their
|
||||
* resource versions encoded instead of being automatically stripped during the encoding
|
||||
|
|
|
@ -214,6 +214,15 @@ public interface IValidationSupport {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns <code>true</code> if a Remote Terminology Service is currently configured
|
||||
*
|
||||
* @return Returns <code>true</code> if a Remote Terminology Service is currently configured
|
||||
*/
|
||||
default boolean isRemoteTerminologyServiceConfigured() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the given ValueSet by URL
|
||||
*/
|
||||
|
|
|
@ -45,6 +45,7 @@ import ca.uhn.fhir.util.BundleUtil;
|
|||
import ca.uhn.fhir.util.FhirTerser;
|
||||
import ca.uhn.fhir.util.UrlUtil;
|
||||
import com.google.common.base.Charsets;
|
||||
import org.apache.commons.io.output.StringBuilderWriter;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.hl7.fhir.instance.model.api.IBase;
|
||||
|
@ -268,7 +269,7 @@ public abstract class BaseParser implements IParser {
|
|||
|
||||
@Override
|
||||
public String encodeResourceToString(IBaseResource theResource) throws DataFormatException {
|
||||
Writer stringWriter = new StringWriter();
|
||||
Writer stringWriter = new StringBuilderWriter();
|
||||
try {
|
||||
encodeResourceToWriter(theResource, stringWriter);
|
||||
} catch (IOException e) {
|
||||
|
@ -615,7 +616,7 @@ public abstract class BaseParser implements IParser {
|
|||
if (isBlank(resourceId.getValue())) {
|
||||
resourceId.setValue(fullUrl);
|
||||
} else {
|
||||
if (fullUrl.startsWith("urn:") && fullUrl.endsWith(":" + resourceId.getIdPart())) {
|
||||
if (fullUrl.startsWith("urn:") && fullUrl.length() > resourceId.getIdPart().length() && fullUrl.charAt(fullUrl.length() - resourceId.getIdPart().length() - 1) == ':' && fullUrl.endsWith(resourceId.getIdPart())) {
|
||||
resourceId.setValue(fullUrl);
|
||||
} else {
|
||||
IIdType fullUrlId = myContext.getVersion().newIdType();
|
||||
|
|
|
@ -57,6 +57,7 @@ import java.io.Writer;
|
|||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
@ -939,7 +940,9 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
|
|||
}
|
||||
|
||||
JsonLikeObject alternate = alternateVal.getAsObject();
|
||||
for (String nextKey : alternate.keySet()) {
|
||||
|
||||
for (Iterator<String> keyIter = alternate.keyIterator(); keyIter.hasNext(); ) {
|
||||
String nextKey = keyIter.next();
|
||||
JsonLikeValue nextVal = alternate.get(nextKey);
|
||||
if ("extension".equals(nextKey)) {
|
||||
boolean isModifier = false;
|
||||
|
@ -962,12 +965,11 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
|
|||
}
|
||||
|
||||
private void parseChildren(JsonLikeObject theObject, ParserState<?> theState) {
|
||||
Set<String> keySet = theObject.keySet();
|
||||
|
||||
int allUnderscoreNames = 0;
|
||||
int handledUnderscoreNames = 0;
|
||||
|
||||
for (String nextName : keySet) {
|
||||
for (Iterator<String> keyIter = theObject.keyIterator(); keyIter.hasNext(); ) {
|
||||
String nextName = keyIter.next();
|
||||
if ("resourceType".equals(nextName)) {
|
||||
continue;
|
||||
} else if ("extension".equals(nextName)) {
|
||||
|
@ -1013,7 +1015,8 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
|
|||
* for example.
|
||||
*/
|
||||
if (allUnderscoreNames > handledUnderscoreNames) {
|
||||
for (String alternateName : keySet) {
|
||||
for (Iterator<String> keyIter = theObject.keyIterator(); keyIter.hasNext(); ) {
|
||||
String alternateName = keyIter.next();
|
||||
if (alternateName.startsWith("_") && alternateName.length() > 1) {
|
||||
JsonLikeValue nextValue = theObject.get(alternateName);
|
||||
if (nextValue != null) {
|
||||
|
@ -1116,7 +1119,8 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
|
|||
url = getExtensionUrl(jsonElement.getAsString());
|
||||
}
|
||||
theState.enteringNewElementExtension(null, url, theIsModifier, getServerBaseUrl());
|
||||
for (String next : nextExtObj.keySet()) {
|
||||
for (Iterator<String> keyIter = nextExtObj.keyIterator(); keyIter.hasNext(); ) {
|
||||
String next = keyIter.next();
|
||||
if ("url".equals(next)) {
|
||||
continue;
|
||||
} else if ("extension".equals(next)) {
|
||||
|
@ -1146,7 +1150,8 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
|
|||
* for example.
|
||||
*/
|
||||
if (allUnderscoreNames > handledUnderscoreNames) {
|
||||
for (String alternateName : nextExtObj.keySet()) {
|
||||
for (Iterator<String> keyIter = nextExtObj.keyIterator(); keyIter.hasNext(); ) {
|
||||
String alternateName = keyIter.next();
|
||||
if (alternateName.startsWith("_") && alternateName.length() > 1) {
|
||||
JsonLikeValue nextValue = nextExtObj.get(alternateName);
|
||||
if (nextValue != null) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package ca.uhn.fhir.parser.json;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.Iterator;
|
||||
|
||||
/*
|
||||
* #%L
|
||||
|
@ -28,7 +28,7 @@ public abstract class JsonLikeObject extends JsonLikeValue {
|
|||
public ValueType getJsonType() {
|
||||
return ValueType.OBJECT;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ScalarType getDataType() {
|
||||
return null;
|
||||
|
@ -49,8 +49,8 @@ public abstract class JsonLikeObject extends JsonLikeValue {
|
|||
return null;
|
||||
}
|
||||
|
||||
public abstract Set<String> keySet ();
|
||||
|
||||
public abstract JsonLikeValue get (String key);
|
||||
|
||||
public abstract Iterator<String> keyIterator();
|
||||
|
||||
public abstract JsonLikeValue get(String key);
|
||||
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ import com.fasterxml.jackson.core.JsonParser;
|
|||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.DecimalNode;
|
||||
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
|
||||
|
@ -46,9 +47,6 @@ import java.util.ArrayList;
|
|||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
public class JacksonStructure implements JsonLikeStructure {
|
||||
|
||||
|
@ -153,8 +151,6 @@ public class JacksonStructure implements JsonLikeStructure {
|
|||
|
||||
private static class JacksonJsonObject extends JsonLikeObject {
|
||||
private final ObjectNode nativeObject;
|
||||
private final Map<String, JsonLikeValue> jsonLikeMap = new LinkedHashMap<>();
|
||||
private Set<String> keySet = null;
|
||||
|
||||
public JacksonJsonObject(ObjectNode json) {
|
||||
this.nativeObject = json;
|
||||
|
@ -166,30 +162,17 @@ public class JacksonStructure implements JsonLikeStructure {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Set<String> keySet() {
|
||||
if (null == keySet) {
|
||||
final Iterable<Map.Entry<String, JsonNode>> iterable = nativeObject::fields;
|
||||
keySet = StreamSupport.stream(iterable.spliterator(), false)
|
||||
.map(Map.Entry::getKey)
|
||||
.collect(Collectors.toCollection(EntryOrderedSet::new));
|
||||
}
|
||||
|
||||
return keySet;
|
||||
public Iterator<String> keyIterator() {
|
||||
return nativeObject.fieldNames();
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonLikeValue get(String key) {
|
||||
JsonLikeValue result = null;
|
||||
if (jsonLikeMap.containsKey(key)) {
|
||||
result = jsonLikeMap.get(key);
|
||||
} else {
|
||||
JsonNode child = nativeObject.get(key);
|
||||
if (child != null) {
|
||||
result = new JacksonJsonValue(child);
|
||||
}
|
||||
jsonLikeMap.put(key, result);
|
||||
JsonNode child = nativeObject.get(key);
|
||||
if (child != null) {
|
||||
return new JacksonJsonValue(child);
|
||||
}
|
||||
return result;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -300,19 +283,28 @@ public class JacksonStructure implements JsonLikeStructure {
|
|||
|
||||
@Override
|
||||
public ValueType getJsonType() {
|
||||
if (null == nativeValue || nativeValue.isNull()) {
|
||||
if (null == nativeValue) {
|
||||
return ValueType.NULL;
|
||||
}
|
||||
if (nativeValue.isObject()) {
|
||||
return ValueType.OBJECT;
|
||||
|
||||
switch (nativeValue.getNodeType()) {
|
||||
case NULL:
|
||||
case MISSING:
|
||||
return ValueType.NULL;
|
||||
case OBJECT:
|
||||
return ValueType.OBJECT;
|
||||
case ARRAY:
|
||||
return ValueType.ARRAY;
|
||||
case POJO:
|
||||
case BINARY:
|
||||
case STRING:
|
||||
case NUMBER:
|
||||
case BOOLEAN:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (nativeValue.isArray()) {
|
||||
return ValueType.ARRAY;
|
||||
}
|
||||
if (nativeValue.isValueNode()) {
|
||||
return ValueType.SCALAR;
|
||||
}
|
||||
return null;
|
||||
|
||||
return ValueType.SCALAR;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -378,7 +370,10 @@ public class JacksonStructure implements JsonLikeStructure {
|
|||
}
|
||||
|
||||
private static ObjectMapper createObjectMapper() {
|
||||
ObjectMapper retVal = new ObjectMapper();
|
||||
ObjectMapper retVal =
|
||||
JsonMapper
|
||||
.builder()
|
||||
.build();
|
||||
retVal = retVal.setNodeFactory(new JsonNodeFactory(true));
|
||||
retVal = retVal.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS);
|
||||
retVal = retVal.enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS);
|
||||
|
|
|
@ -73,6 +73,7 @@ public class Constants {
|
|||
public static final String CT_OCTET_STREAM = "application/octet-stream";
|
||||
public static final String CT_TEXT = "text/plain";
|
||||
public static final String CT_TEXT_WITH_UTF8 = CT_TEXT + CHARSET_UTF8_CTSUFFIX;
|
||||
public static final String CT_TEXT_CDA = "text/xml+cda";
|
||||
public static final String CT_X_FORM_URLENCODED = "application/x-www-form-urlencoded";
|
||||
public static final String CT_XML = "application/xml";
|
||||
public static final String CT_XML_PATCH = "application/xml-patch+xml";
|
||||
|
@ -201,6 +202,14 @@ public class Constants {
|
|||
public static final String PARAM_TAGS = "_tags";
|
||||
public static final String PARAM_TEXT = "_text";
|
||||
public static final String PARAM_VALIDATE = "_validate";
|
||||
|
||||
/**
|
||||
* $member-match operation
|
||||
*/
|
||||
public static final String PARAM_MEMBER_PATIENT = "MemberPatient";
|
||||
public static final String PARAM_OLD_COVERAGE = "OldCoverage";
|
||||
public static final String PARAM_NEW_COVERAGE = "NewCoverage";
|
||||
|
||||
public static final String PARAMQUALIFIER_MISSING = ":missing";
|
||||
public static final String PARAMQUALIFIER_MISSING_FALSE = "false";
|
||||
public static final String PARAMQUALIFIER_MISSING_TRUE = "true";
|
||||
|
|
|
@ -83,7 +83,6 @@ import static org.apache.commons.lang3.StringUtils.substring;
|
|||
public class FhirTerser {
|
||||
|
||||
private static final Pattern COMPARTMENT_MATCHER_PATH = Pattern.compile("([a-zA-Z.]+)\\.where\\(resolve\\(\\) is ([a-zA-Z]+)\\)");
|
||||
private static final EnumSet<OptionsEnum> EMPTY_OPTION_SET = EnumSet.noneOf(OptionsEnum.class);
|
||||
private static final String USER_DATA_KEY_CONTAIN_RESOURCES_COMPLETED = FhirTerser.class.getName() + "_CONTAIN_RESOURCES_COMPLETED";
|
||||
private final FhirContext myContext;
|
||||
|
||||
|
@ -323,11 +322,11 @@ public class FhirTerser {
|
|||
}
|
||||
|
||||
public Optional<String> getSinglePrimitiveValue(IBase theTarget, String thePath) {
|
||||
return getSingleValue(theTarget, thePath, IPrimitiveType.class).map(t->t.getValueAsString());
|
||||
return getSingleValue(theTarget, thePath, IPrimitiveType.class).map(t -> t.getValueAsString());
|
||||
}
|
||||
|
||||
public String getSinglePrimitiveValueOrNull(IBase theTarget, String thePath) {
|
||||
return getSingleValue(theTarget, thePath, IPrimitiveType.class).map(t->t.getValueAsString()).orElse(null);
|
||||
return getSingleValue(theTarget, thePath, IPrimitiveType.class).map(t -> t.getValueAsString()).orElse(null);
|
||||
}
|
||||
|
||||
public <T extends IBase> Optional<T> getSingleValue(IBase theTarget, String thePath, Class<T> theWantedType) {
|
||||
|
@ -712,9 +711,9 @@ public class FhirTerser {
|
|||
* Returns <code>true</code> if <code>theSource</code> is in the compartment named <code>theCompartmentName</code>
|
||||
* belonging to resource <code>theTarget</code>
|
||||
*
|
||||
* @param theCompartmentName The name of the compartment
|
||||
* @param theSource The potential member of the compartment
|
||||
* @param theTarget The owner of the compartment. Note that both the resource type and ID must be filled in on this IIdType or the method will throw an {@link IllegalArgumentException}
|
||||
* @param theCompartmentName The name of the compartment
|
||||
* @param theSource The potential member of the compartment
|
||||
* @param theTarget The owner of the compartment. Note that both the resource type and ID must be filled in on this IIdType or the method will throw an {@link IllegalArgumentException}
|
||||
* @param theAdditionalCompartmentParamNames If provided, search param names provided here will be considered as included in the given compartment for this comparison.
|
||||
* @return <code>true</code> if <code>theSource</code> is in the compartment or one of the additional parameters matched.
|
||||
* @throws IllegalArgumentException If theTarget does not contain both a resource type and ID
|
||||
|
@ -1112,7 +1111,7 @@ public class FhirTerser {
|
|||
});
|
||||
}
|
||||
|
||||
private void containResourcesForEncoding(ContainedResources theContained, IBaseResource theResource, EnumSet<OptionsEnum> theOptions) {
|
||||
private void containResourcesForEncoding(ContainedResources theContained, IBaseResource theResource, boolean theModifyResource) {
|
||||
List<IBaseReference> allReferences = getAllPopulatedChildElementsOfType(theResource, IBaseReference.class);
|
||||
for (IBaseReference next : allReferences) {
|
||||
IBaseResource resource = next.getResource();
|
||||
|
@ -1121,7 +1120,7 @@ public class FhirTerser {
|
|||
IBaseResource potentialTarget = theContained.getExistingIdToContainedResource().remove(next.getReferenceElement().getValue());
|
||||
if (potentialTarget != null) {
|
||||
theContained.addContained(next.getReferenceElement(), potentialTarget);
|
||||
containResourcesForEncoding(theContained, potentialTarget, theOptions);
|
||||
containResourcesForEncoding(theContained, potentialTarget, theModifyResource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1136,15 +1135,13 @@ public class FhirTerser {
|
|||
continue;
|
||||
}
|
||||
IIdType id = theContained.addContained(resource);
|
||||
if (theOptions.contains(OptionsEnum.MODIFY_RESOURCE)) {
|
||||
if (theModifyResource) {
|
||||
getContainedResourceList(theResource).add(resource);
|
||||
next.setReference(id.getValue());
|
||||
}
|
||||
if (resource.getIdElement().isLocal() && theContained.hasExistingIdToContainedResource()) {
|
||||
theContained.getExistingIdToContainedResource().remove(resource.getIdElement().getValue());
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1161,9 +1158,20 @@ public class FhirTerser {
|
|||
* @since 5.4.0
|
||||
*/
|
||||
public ContainedResources containResources(IBaseResource theResource, OptionsEnum... theOptions) {
|
||||
EnumSet<OptionsEnum> options = toOptionSet(theOptions);
|
||||
boolean storeAndReuse = false;
|
||||
boolean modifyResource = false;
|
||||
for (OptionsEnum next : theOptions) {
|
||||
switch (next) {
|
||||
case MODIFY_RESOURCE:
|
||||
modifyResource = true;
|
||||
break;
|
||||
case STORE_AND_REUSE_RESULTS:
|
||||
storeAndReuse = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.contains(OptionsEnum.STORE_AND_REUSE_RESULTS)) {
|
||||
if (storeAndReuse) {
|
||||
Object cachedValue = theResource.getUserData(USER_DATA_KEY_CONTAIN_RESOURCES_COMPLETED);
|
||||
if (cachedValue != null) {
|
||||
return (ContainedResources) cachedValue;
|
||||
|
@ -1184,25 +1192,17 @@ public class FhirTerser {
|
|||
contained.addContained(next);
|
||||
}
|
||||
|
||||
containResourcesForEncoding(contained, theResource, options);
|
||||
if (myContext.getParserOptions().isAutoContainReferenceTargetsWithNoId()) {
|
||||
containResourcesForEncoding(contained, theResource, modifyResource);
|
||||
}
|
||||
|
||||
if (options.contains(OptionsEnum.STORE_AND_REUSE_RESULTS)) {
|
||||
if (storeAndReuse) {
|
||||
theResource.setUserData(USER_DATA_KEY_CONTAIN_RESOURCES_COMPLETED, contained);
|
||||
}
|
||||
|
||||
return contained;
|
||||
}
|
||||
|
||||
private EnumSet<OptionsEnum> toOptionSet(OptionsEnum[] theOptions) {
|
||||
EnumSet<OptionsEnum> options;
|
||||
if (theOptions == null || theOptions.length == 0) {
|
||||
options = EMPTY_OPTION_SET;
|
||||
} else {
|
||||
options = EnumSet.of(theOptions[0], theOptions);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T extends IBaseResource> List<T> getContainedResourceList(T theResource) {
|
||||
List<T> containedResources = Collections.emptyList();
|
||||
|
@ -1404,14 +1404,13 @@ public class FhirTerser {
|
|||
|
||||
/**
|
||||
* Clones a resource object, copying all data elements from theSource into a new copy of the same type.
|
||||
*
|
||||
* <p>
|
||||
* Note that:
|
||||
* <ul>
|
||||
* <li>Only FHIR data elements are copied (i.e. user data maps are not copied)</li>
|
||||
* <li>If a class extending a HAPI FHIR type (e.g. an instance of a class extending the Patient class) is supplied, an instance of the base type will be returned.</li>
|
||||
* </ul>
|
||||
*
|
||||
*
|
||||
* @param theSource The source resource
|
||||
* @return A copy of the source resource
|
||||
* @since 5.6.0
|
||||
|
|
|
@ -80,7 +80,6 @@ public enum VersionEnum {
|
|||
V5_5_3,
|
||||
V5_6_0,
|
||||
V5_7_0
|
||||
|
||||
;
|
||||
|
||||
public static VersionEnum latestVersion() {
|
||||
|
|
|
@ -183,3 +183,9 @@ ca.uhn.fhir.jpa.provider.DiffProvider.cantDiffDifferentTypes=Unable to diff two
|
|||
|
||||
ca.uhn.fhir.jpa.interceptor.validation.RuleRequireProfileDeclaration.noMatchingProfile=Resource of type "{0}" does not declare conformance to profile from: {1}
|
||||
ca.uhn.fhir.jpa.interceptor.validation.RuleRequireProfileDeclaration.illegalProfile=Resource of type "{0}" must not declare conformance to profile: {1}
|
||||
|
||||
operation.member.match.error.coverage.not.found=Could not find coverage for member based on coverage id or coverage identifier.
|
||||
operation.member.match.error.beneficiary.not.found=Could not find beneficiary for coverage.
|
||||
operation.member.match.error.missing.parameter=Parameter "{0}" is required.
|
||||
operation.member.match.error.beneficiary.without.identifier=Coverage beneficiary does not have an identifier.
|
||||
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
type: add
|
||||
issue: 3136
|
||||
title: "Provided a Remote Terminology Service implementation for the $validate-code Operation."
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
type: add
|
||||
issue: 3172
|
||||
title: "Implement support for $member-match operation by coverage-id or coverage-identifier.
|
||||
(Beneficiary demographic matching not supported)"
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
type: add
|
||||
issue: 3158
|
||||
title: "Add the concept of `messageKey` to the BaseResourceMessage class. This is to support brokers which permit key-based
|
||||
partitioning."
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
type: change
|
||||
issue: 3158
|
||||
title: "Previously in configuring MDM, you were only allowed to set a single `eidSystem` which was global regardless of how many resource types you were performing
|
||||
MDM on. This has been changed. That field is now deprecated, and a new field called `eidSystems` (a json object) should be used instead. This will allow you to have granular
|
||||
control over which resource types use which EIDs. See the [documentation](/hapi-fhir/docs/server_jpa_mdm/mdm_rules.html) of `eidSystems` for more information."
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
type: perf
|
||||
issue: 3197
|
||||
title: "A new configuration option has been added to ParserOptions called `setAutoContainReferenceTargetsWithNoId`. This
|
||||
setting disables the automatic creation of contained resources in cases where the target/contained resource is set
|
||||
to a Reference instance but not actually added to the `Resource.contained` list. Disabling this automatic containing
|
||||
yields a minor performance improvement in systems that do not need this functionality."
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
type: perf
|
||||
issue: 3197
|
||||
title: "A number of minor optimizations have been added to the JsonParser serializer module as well as to the
|
||||
transaction processor. These optimizations lead to a significant performance improvement when processing
|
||||
large transaction bundles (i.e. transaction bundles containing a larger number of entries)."
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
type: change
|
||||
issue: 3198
|
||||
jira: SMILE-3452
|
||||
title: "Fixed a regression where packages would fail to load if HAPI-FHIR was operating in DATABASE binary storage mode."
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
type: fix
|
||||
jira: SMILE-3472
|
||||
title: "Fixed a serious performance issue with the `$reindex` operation."
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
- item:
|
||||
type: "add"
|
||||
title: "The version of a few dependencies have been bumped to the latest versions
|
||||
(dependent HAPI modules listed in brackets):
|
||||
<ul>
|
||||
<li>Spring (JPA): 5.3.7 -> 5.3.13</li>
|
||||
<li>Thymeleaf (Testpage Overlay): 3.0.12.RELEASE -> 3.0.13.RELEASE (Addresses CVE-2021-43466)</li>
|
||||
</ul>
|
||||
"
|
||||
|
|
@ -99,7 +99,11 @@ Here is an example of a full HAPI MDM rules json document:
|
|||
"lastname-jaro,phone,birthday": "POSSIBLE_MATCH",
|
||||
"firstname-jaro,phone,birthday": "POSSIBLE_MATCH",
|
||||
"org-name": "MATCH"
|
||||
}
|
||||
},
|
||||
"eidSystems": {
|
||||
"Organization": "https://hapifhir.org/identifier/naming/business-number",
|
||||
"Practitioner": "https://hapifhir.org/identifier/naming/license-number"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -483,6 +487,13 @@ These entries convert combinations of successful matchFields into an MDM Match R
|
|||
}
|
||||
```
|
||||
|
||||
### eidSystem
|
||||
### eidSystems
|
||||
|
||||
The external EID systems that the HAPI MDM system can expect to see on incoming resources. These are defined on a per-resource basis. Alternatively, you may use `*` to indicate
|
||||
that an EID is valid for all managed resource types. The values must be valid URIs, and the keys must be valid resource types, or `*`.
|
||||
See [MDM EID](/hapi-fhir/docs/server_jpa_mdm/mdm_eid.html) for details on how EIDs are managed by HAPI MDM.
|
||||
|
||||
<p class="helpInfoCalloutBox">
|
||||
Note that this field used to be called `eidSystem`. While that field is deprecated, it will continue to work. In the background, it effectively sets the eid for resource type `*`.
|
||||
</p>
|
||||
|
||||
The external EID system that the HAPI MDM system should expect to see on incoming Patient resources. Must be a valid URI. See [MDM EID](/hapi-fhir/docs/server_jpa_mdm/mdm_eid.html) for details on how EIDs are managed by HAPI MDM.
|
||||
|
|
|
@ -197,8 +197,8 @@
|
|||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.swagger</groupId>
|
||||
<artifactId>swagger-annotations</artifactId>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-ui</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
|
|
@ -86,6 +86,7 @@ import ca.uhn.fhir.jpa.provider.DiffProvider;
|
|||
import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider;
|
||||
import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider;
|
||||
import ca.uhn.fhir.jpa.provider.ValueSetOperationProvider;
|
||||
import ca.uhn.fhir.jpa.provider.r4.MemberMatcherR4Helper;
|
||||
import ca.uhn.fhir.jpa.reindex.ReindexJobSubmitterImpl;
|
||||
import ca.uhn.fhir.jpa.sched.AutowiringSpringBeanJobFactory;
|
||||
import ca.uhn.fhir.jpa.sched.HapiSchedulerServiceImpl;
|
||||
|
@ -942,6 +943,14 @@ public abstract class BaseConfig {
|
|||
return new UnknownCodeSystemWarningValidationSupport(fhirContext());
|
||||
}
|
||||
|
||||
@Lazy
|
||||
@Bean
|
||||
public MemberMatcherR4Helper memberMatcherR4Helper(FhirContext theFhirContext) {
|
||||
return new MemberMatcherR4Helper(theFhirContext);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static void configureEntityManagerFactory(LocalContainerEntityManagerFactoryBean theFactory, FhirContext theCtx) {
|
||||
theFactory.setJpaDialect(hibernateJpaDialect(theCtx.getLocalizer()));
|
||||
theFactory.setPackagesToScan("ca.uhn.fhir.jpa.model.entity", "ca.uhn.fhir.jpa.entity");
|
||||
|
|
|
@ -117,8 +117,8 @@ public class TransactionProcessor extends BaseTransactionProcessor {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected Map<IBase, IIdType> doTransactionWriteOperations(final RequestDetails theRequest, String theActionName, TransactionDetails theTransactionDetails, Set<IIdType> theAllIds,
|
||||
Map<IIdType, IIdType> theIdSubstitutions, Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome, IBaseBundle theResponse, IdentityHashMap<IBase, Integer> theOriginalRequestOrder, List<IBase> theEntries, StopWatch theTransactionStopWatch) {
|
||||
protected EntriesToProcessMap doTransactionWriteOperations(final RequestDetails theRequest, String theActionName, TransactionDetails theTransactionDetails, Set<IIdType> theAllIds,
|
||||
IdSubstitutionMap theIdSubstitutions, Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome, IBaseBundle theResponse, IdentityHashMap<IBase, Integer> theOriginalRequestOrder, List<IBase> theEntries, StopWatch theTransactionStopWatch) {
|
||||
|
||||
ITransactionProcessorVersionAdapter versionAdapter = getVersionAdapter();
|
||||
RequestPartitionId requestPartitionId = null;
|
||||
|
|
|
@ -28,6 +28,7 @@ import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
|||
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
||||
import ca.uhn.fhir.jpa.api.model.ExpungeOptions;
|
||||
import ca.uhn.fhir.jpa.binstore.IBinaryStorageSvc;
|
||||
import ca.uhn.fhir.jpa.binstore.NullBinaryStorageSvcImpl;
|
||||
import ca.uhn.fhir.jpa.dao.data.INpmPackageDao;
|
||||
import ca.uhn.fhir.jpa.dao.data.INpmPackageVersionDao;
|
||||
import ca.uhn.fhir.jpa.dao.data.INpmPackageVersionResourceDao;
|
||||
|
@ -198,7 +199,7 @@ public class JpaPackageCache extends BasePackageCacheManager implements IHapiPac
|
|||
* @throws IOException
|
||||
*/
|
||||
private byte[] fetchBlobFromBinary(IBaseBinary theBinary) throws IOException {
|
||||
if (myBinaryStorageSvc != null) {
|
||||
if (myBinaryStorageSvc != null && !(myBinaryStorageSvc instanceof NullBinaryStorageSvcImpl)) {
|
||||
return myBinaryStorageSvc.fetchDataBlobFromBinary(theBinary);
|
||||
} else {
|
||||
byte[] value = BinaryUtil.getOrCreateData(myCtx, theBinary).getValue();
|
||||
|
|
|
@ -27,15 +27,14 @@ import com.fasterxml.jackson.annotation.JsonInclude;
|
|||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@ApiModel("Represents an NPM package metadata response")
|
||||
@Schema(description = "Represents an NPM package metadata response")
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@JsonAutoDetect(creatorVisibility = JsonAutoDetect.Visibility.NONE, fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE)
|
||||
public class NpmPackageMetadataJson {
|
||||
|
@ -108,7 +107,7 @@ public class NpmPackageMetadataJson {
|
|||
private String myDescription;
|
||||
@JsonProperty("fhirVersion")
|
||||
private String myFhirVersion;
|
||||
@ApiModelProperty(value = "The size of this package in bytes", example = "1000")
|
||||
@Schema(description = "The size of this package in bytes", example = "1000")
|
||||
@JsonProperty("_bytes")
|
||||
private long myBytes;
|
||||
|
||||
|
|
|
@ -23,13 +23,12 @@ package ca.uhn.fhir.jpa.packages;
|
|||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@ApiModel("Represents an NPM package search response")
|
||||
@Schema(description = "Represents an NPM package search response")
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@JsonAutoDetect(creatorVisibility = JsonAutoDetect.Visibility.NONE, fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE)
|
||||
public class NpmPackageSearchResultJson {
|
||||
|
@ -95,7 +94,7 @@ public class NpmPackageSearchResultJson {
|
|||
private String myDescription;
|
||||
@JsonProperty("fhirVersion")
|
||||
private List<String> myFhirVersion;
|
||||
@ApiModelProperty(value = "The size of this package in bytes", example = "1000")
|
||||
@Schema(description = "The size of this package in bytes", example = "1000")
|
||||
@JsonProperty("_bytes")
|
||||
private long myBytes;
|
||||
|
||||
|
|
|
@ -23,12 +23,12 @@ package ca.uhn.fhir.jpa.packages;
|
|||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@ApiModel("Represents an NPM package deletion response")
|
||||
@Schema(description = "Represents an NPM package deletion response")
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@JsonAutoDetect(creatorVisibility = JsonAutoDetect.Visibility.NONE, fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE)
|
||||
public class PackageDeleteOutcomeJson {
|
||||
|
|
|
@ -23,14 +23,14 @@ package ca.uhn.fhir.jpa.packages;
|
|||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@ApiModel("Represents an NPM package installation response")
|
||||
@Schema(description = "Represents an NPM package installation response")
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@JsonAutoDetect(creatorVisibility = JsonAutoDetect.Visibility.NONE, fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE)
|
||||
public class PackageInstallOutcomeJson {
|
||||
|
|
|
@ -27,15 +27,14 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
|
|||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@ApiModel(
|
||||
value = "PackageInstallationSpec",
|
||||
@Schema(
|
||||
name = "PackageInstallationSpec",
|
||||
description =
|
||||
"Defines a set of instructions for package installation"
|
||||
)
|
||||
|
@ -47,25 +46,25 @@ import java.util.function.Supplier;
|
|||
@JsonAutoDetect(creatorVisibility = JsonAutoDetect.Visibility.NONE, fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE)
|
||||
public class PackageInstallationSpec {
|
||||
|
||||
@ApiModelProperty("The direct package URL")
|
||||
@Schema(description = "The direct package URL")
|
||||
@JsonProperty("packageUrl")
|
||||
private String myPackageUrl;
|
||||
@ApiModelProperty("The NPM package Name")
|
||||
@Schema(description = "The NPM package Name")
|
||||
@JsonProperty("name")
|
||||
private String myName;
|
||||
@ApiModelProperty("The direct package version")
|
||||
@Schema(description = "The direct package version")
|
||||
@JsonProperty("version")
|
||||
private String myVersion;
|
||||
@ApiModelProperty("Should resources from this package be extracted from the package and installed into the repository individually")
|
||||
@Schema(description = "Should resources from this package be extracted from the package and installed into the repository individually")
|
||||
@JsonProperty("installMode")
|
||||
private InstallModeEnum myInstallMode;
|
||||
@ApiModelProperty("If resources are being installed individually, this is list provides the resource types to install. By default, all conformance resources will be installed.")
|
||||
@Schema(description = "If resources are being installed individually, this is list provides the resource types to install. By default, all conformance resources will be installed.")
|
||||
@JsonProperty("installResourceTypes")
|
||||
private List<String> myInstallResourceTypes;
|
||||
@ApiModelProperty("Should dependencies be automatically resolved, fetched and installed with the same settings")
|
||||
@Schema(description = "Should dependencies be automatically resolved, fetched and installed with the same settings")
|
||||
@JsonProperty("fetchDependencies")
|
||||
private boolean myFetchDependencies;
|
||||
@ApiModelProperty("Any values provided here will be interpreted as a regex. Dependencies with an ID matching any regex will be skipped.")
|
||||
@Schema(description = "Any values provided here will be interpreted as a regex. Dependencies with an ID matching any regex will be skipped.")
|
||||
private List<String> myDependencyExcludes;
|
||||
@JsonIgnore
|
||||
private byte[] myPackageContents;
|
||||
|
|
|
@ -20,11 +20,14 @@ package ca.uhn.fhir.jpa.provider;
|
|||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.context.support.ConceptValidationOptions;
|
||||
import ca.uhn.fhir.context.support.IValidationSupport;
|
||||
import ca.uhn.fhir.context.support.ValidationSupportContext;
|
||||
import ca.uhn.fhir.context.support.ValueSetExpansionOptions;
|
||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoValueSet;
|
||||
import ca.uhn.fhir.jpa.config.BaseConfig;
|
||||
import ca.uhn.fhir.jpa.model.util.JpaConstants;
|
||||
import ca.uhn.fhir.jpa.term.api.ITermReadSvc;
|
||||
import ca.uhn.fhir.rest.annotation.IdParam;
|
||||
|
@ -34,6 +37,7 @@ import ca.uhn.fhir.rest.api.server.RequestDetails;
|
|||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
|
||||
import ca.uhn.fhir.util.ParametersUtil;
|
||||
import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain;
|
||||
import org.hl7.fhir.instance.model.api.IBaseParameters;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.ICompositeType;
|
||||
|
@ -42,6 +46,7 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
|
@ -56,6 +61,9 @@ public class ValueSetOperationProvider extends BaseJpaProvider {
|
|||
private DaoRegistry myDaoRegistry;
|
||||
@Autowired
|
||||
private ITermReadSvc myTermReadSvc;
|
||||
@Autowired
|
||||
@Qualifier(BaseConfig.JPA_VALIDATION_SUPPORT_CHAIN)
|
||||
private ValidationSupportChain myValidationSupportChain;
|
||||
|
||||
public void setDaoConfig(DaoConfig theDaoConfig) {
|
||||
myDaoConfig = theDaoConfig;
|
||||
|
@ -69,6 +77,10 @@ public class ValueSetOperationProvider extends BaseJpaProvider {
|
|||
myTermReadSvc = theTermReadSvc;
|
||||
}
|
||||
|
||||
public void setValidationSupportChain(ValidationSupportChain theValidationSupportChain) {
|
||||
myValidationSupportChain = theValidationSupportChain;
|
||||
}
|
||||
|
||||
@Operation(name = JpaConstants.OPERATION_EXPAND, idempotent = true, typeName = "ValueSet")
|
||||
public IBaseResource expand(
|
||||
HttpServletRequest theServletRequest,
|
||||
|
@ -142,24 +154,37 @@ public class ValueSetOperationProvider extends BaseJpaProvider {
|
|||
RequestDetails theRequestDetails
|
||||
) {
|
||||
|
||||
IValidationSupport.CodeValidationResult result;
|
||||
startRequest(theServletRequest);
|
||||
try {
|
||||
IFhirResourceDaoValueSet<IBaseResource, ICompositeType, ICompositeType> dao = getDao();
|
||||
IPrimitiveType<String> valueSetIdentifier;
|
||||
if (theValueSetVersion != null) {
|
||||
valueSetIdentifier = (IPrimitiveType<String>) getContext().getElementDefinition("uri").newInstance();
|
||||
valueSetIdentifier.setValue(theValueSetUrl.getValue() + "|" + theValueSetVersion);
|
||||
// If a Remote Terminology Server has been configured, use it
|
||||
if (myValidationSupportChain != null && myValidationSupportChain.isRemoteTerminologyServiceConfigured()) {
|
||||
String theSystemString = (theSystem != null && theSystem.hasValue()) ? theSystem.getValueAsString() : null;
|
||||
String theCodeString = (theCode != null && theCode.hasValue()) ? theCode.getValueAsString() : null;
|
||||
String theDisplayString = (theDisplay != null && theDisplay.hasValue()) ? theDisplay.getValueAsString() : null;
|
||||
String theValueSetUrlString = (theValueSetUrl != null && theValueSetUrl.hasValue()) ?
|
||||
theValueSetUrl.getValueAsString() : null;
|
||||
result = myValidationSupportChain.validateCode(new ValidationSupportContext(myValidationSupportChain),
|
||||
new ConceptValidationOptions(), theSystemString, theCodeString, theDisplayString, theValueSetUrlString);
|
||||
} else {
|
||||
valueSetIdentifier = theValueSetUrl;
|
||||
// Otherwise, use the local DAO layer to validate the code
|
||||
IFhirResourceDaoValueSet<IBaseResource, ICompositeType, ICompositeType> dao = getDao();
|
||||
IPrimitiveType<String> valueSetIdentifier;
|
||||
if (theValueSetUrl != null && theValueSetVersion != null) {
|
||||
valueSetIdentifier = (IPrimitiveType<String>) getContext().getElementDefinition("uri").newInstance();
|
||||
valueSetIdentifier.setValue(theValueSetUrl.getValue() + "|" + theValueSetVersion);
|
||||
} else {
|
||||
valueSetIdentifier = theValueSetUrl;
|
||||
}
|
||||
IPrimitiveType<String> codeSystemIdentifier;
|
||||
if (theSystem != null && theSystemVersion != null) {
|
||||
codeSystemIdentifier = (IPrimitiveType<String>) getContext().getElementDefinition("uri").newInstance();
|
||||
codeSystemIdentifier.setValue(theSystem.getValue() + "|" + theSystemVersion);
|
||||
} else {
|
||||
codeSystemIdentifier = theSystem;
|
||||
}
|
||||
result = dao.validateCode(valueSetIdentifier, theId, theCode, codeSystemIdentifier, theDisplay, theCoding, theCodeableConcept, theRequestDetails);
|
||||
}
|
||||
IPrimitiveType<String> codeSystemIdentifier;
|
||||
if (theSystemVersion != null) {
|
||||
codeSystemIdentifier = (IPrimitiveType<String>) getContext().getElementDefinition("uri").newInstance();
|
||||
codeSystemIdentifier.setValue(theSystem.getValue() + "|" + theSystemVersion);
|
||||
} else {
|
||||
codeSystemIdentifier = theSystem;
|
||||
}
|
||||
IValidationSupport.CodeValidationResult result = dao.validateCode(valueSetIdentifier, theId, theCode, codeSystemIdentifier, theDisplay, theCoding, theCodeableConcept, theRequestDetails);
|
||||
return BaseJpaResourceProviderValueSetDstu2.toValidateCodeResult(getContext(), result);
|
||||
} finally {
|
||||
endRequest(theServletRequest);
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
package ca.uhn.fhir.jpa.provider.r4;
|
||||
|
||||
import ca.uhn.fhir.context.support.ConceptValidationOptions;
|
||||
import ca.uhn.fhir.context.support.IValidationSupport;
|
||||
import ca.uhn.fhir.context.support.ValidationSupportContext;
|
||||
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoCodeSystem;
|
||||
import ca.uhn.fhir.jpa.config.BaseConfig;
|
||||
import ca.uhn.fhir.jpa.model.util.JpaConstants;
|
||||
import ca.uhn.fhir.jpa.provider.BaseJpaResourceProviderValueSetDstu2;
|
||||
import ca.uhn.fhir.jpa.term.api.ITermReadSvcR4;
|
||||
import ca.uhn.fhir.rest.annotation.IdParam;
|
||||
import ca.uhn.fhir.rest.annotation.Operation;
|
||||
import ca.uhn.fhir.rest.annotation.OperationParam;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain;
|
||||
import org.hl7.fhir.r4.model.BooleanType;
|
||||
import org.hl7.fhir.r4.model.CodeSystem;
|
||||
import org.hl7.fhir.r4.model.CodeType;
|
||||
|
@ -17,6 +23,8 @@ import org.hl7.fhir.r4.model.IdType;
|
|||
import org.hl7.fhir.r4.model.Parameters;
|
||||
import org.hl7.fhir.r4.model.StringType;
|
||||
import org.hl7.fhir.r4.model.UriType;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.List;
|
||||
|
@ -43,6 +51,13 @@ import java.util.List;
|
|||
|
||||
public class BaseJpaResourceProviderCodeSystemR4 extends JpaResourceProviderR4<CodeSystem> {
|
||||
|
||||
@Autowired
|
||||
@Qualifier(BaseConfig.JPA_VALIDATION_SUPPORT_CHAIN)
|
||||
private ValidationSupportChain myValidationSupportChain;
|
||||
|
||||
@Autowired
|
||||
protected ITermReadSvcR4 myTermSvc;
|
||||
|
||||
/**
|
||||
* $lookup operation
|
||||
*/
|
||||
|
@ -133,11 +148,35 @@ public class BaseJpaResourceProviderCodeSystemR4 extends JpaResourceProviderR4<C
|
|||
RequestDetails theRequestDetails
|
||||
) {
|
||||
|
||||
IValidationSupport.CodeValidationResult result = null;
|
||||
startRequest(theServletRequest);
|
||||
try {
|
||||
IFhirResourceDaoCodeSystem<CodeSystem, Coding, CodeableConcept> dao = (IFhirResourceDaoCodeSystem<CodeSystem, Coding, CodeableConcept>) getDao();
|
||||
|
||||
IValidationSupport.CodeValidationResult result = dao.validateCode(theId, theCodeSystemUrl, theVersion, theCode, theDisplay, theCoding, theCodeableConcept, theRequestDetails);
|
||||
// If a Remote Terminology Server has been configured, use it
|
||||
if (myValidationSupportChain.isRemoteTerminologyServiceConfigured()) {
|
||||
String codeSystemUrl = (theCodeSystemUrl != null && theCodeSystemUrl.hasValue()) ?
|
||||
theCodeSystemUrl.asStringValue() : null;
|
||||
String code = (theCode != null && theCode.hasValue()) ? theCode.asStringValue() : null;
|
||||
String display = (theDisplay != null && theDisplay.hasValue()) ? theDisplay.asStringValue() : null;
|
||||
|
||||
if (theCoding != null) {
|
||||
if (theCoding.hasSystem()) {
|
||||
if (codeSystemUrl != null && !codeSystemUrl.equalsIgnoreCase(theCoding.getSystem())) {
|
||||
throw new InvalidRequestException("Coding.system '" + theCoding.getSystem() + "' does not equal param url '" + theCodeSystemUrl + "'. Unable to validate-code.");
|
||||
}
|
||||
codeSystemUrl = theCoding.getSystem();
|
||||
code = theCoding.getCode();
|
||||
display = theCoding.getDisplay();
|
||||
|
||||
result = myValidationSupportChain.validateCode(
|
||||
new ValidationSupportContext(myValidationSupportChain), new ConceptValidationOptions(),
|
||||
codeSystemUrl, code, display, null);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Otherwise, use the local DAO layer to validate the code
|
||||
IFhirResourceDaoCodeSystem<CodeSystem, Coding, CodeableConcept> dao = (IFhirResourceDaoCodeSystem<CodeSystem, Coding, CodeableConcept>) getDao();
|
||||
result = dao.validateCode(theId, theCodeSystemUrl, theVersion, theCode, theDisplay, theCoding, theCodeableConcept, theRequestDetails);
|
||||
}
|
||||
return (Parameters) BaseJpaResourceProviderValueSetDstu2.toValidateCodeResult(getContext(), result);
|
||||
} finally {
|
||||
endRequest(theServletRequest);
|
||||
|
|
|
@ -3,6 +3,7 @@ package ca.uhn.fhir.jpa.provider.r4;
|
|||
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoPatient;
|
||||
import ca.uhn.fhir.jpa.model.util.JpaConstants;
|
||||
import ca.uhn.fhir.model.api.annotation.Description;
|
||||
import ca.uhn.fhir.model.primitive.StringDt;
|
||||
import ca.uhn.fhir.model.valueset.BundleTypeEnum;
|
||||
import ca.uhn.fhir.rest.annotation.IdParam;
|
||||
import ca.uhn.fhir.rest.annotation.Operation;
|
||||
|
@ -18,13 +19,20 @@ import ca.uhn.fhir.rest.param.StringOrListParam;
|
|||
import ca.uhn.fhir.rest.param.StringParam;
|
||||
import ca.uhn.fhir.rest.param.TokenOrListParam;
|
||||
import ca.uhn.fhir.rest.param.TokenParam;
|
||||
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
||||
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
|
||||
import org.hl7.fhir.r4.model.Coverage;
|
||||
import org.hl7.fhir.r4.model.IdType;
|
||||
import org.hl7.fhir.r4.model.Parameters;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.hl7.fhir.r4.model.StringType;
|
||||
import org.hl7.fhir.r4.model.UnsignedIntType;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
|
||||
|
@ -50,6 +58,10 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
|||
|
||||
public class BaseJpaResourceProviderPatientR4 extends JpaResourceProviderR4<Patient> {
|
||||
|
||||
@Autowired
|
||||
private MemberMatcherR4Helper myMemberMatcherR4Helper;
|
||||
|
||||
|
||||
/**
|
||||
* Patient/123/$everything
|
||||
*/
|
||||
|
@ -150,6 +162,85 @@ public class BaseJpaResourceProviderPatientR4 extends JpaResourceProviderR4<Pati
|
|||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* /Patient/$member-match operation
|
||||
* Basic implementation matching by coverage id or by coverage identifier. Not matching by
|
||||
* Beneficiary (Patient) demographics in this version
|
||||
*/
|
||||
@Operation(name = ProviderConstants.OPERATION_MEMBER_MATCH, idempotent = true, returnParameters = {
|
||||
@OperationParam(name = "MemberIdentifier", type = StringDt.class)
|
||||
})
|
||||
public Parameters patientMemberMatch(
|
||||
javax.servlet.http.HttpServletRequest theServletRequest,
|
||||
|
||||
@Description(shortDefinition = "The target of the operation. Will be returned with Identifier for matched coverage added.")
|
||||
@OperationParam(name = Constants.PARAM_MEMBER_PATIENT, min = 1, max = 1)
|
||||
Patient theMemberPatient,
|
||||
|
||||
@Description(shortDefinition = "Old coverage information as extracted from beneficiary's card.")
|
||||
@OperationParam(name = Constants.PARAM_OLD_COVERAGE, min = 1, max = 1)
|
||||
Coverage oldCoverage,
|
||||
|
||||
@Description(shortDefinition = "New Coverage information. Provided as a reference. Optionally returned unmodified.")
|
||||
@OperationParam(name = Constants.PARAM_NEW_COVERAGE, min = 1, max = 1)
|
||||
Coverage newCoverage,
|
||||
|
||||
RequestDetails theRequestDetails
|
||||
) {
|
||||
return doMemberMatchOperation(theServletRequest, theMemberPatient, oldCoverage, newCoverage, theRequestDetails);
|
||||
}
|
||||
|
||||
|
||||
private Parameters doMemberMatchOperation(HttpServletRequest theServletRequest, Patient theMemberPatient,
|
||||
Coverage theCoverageToMatch, Coverage theCoverageToLink, RequestDetails theRequestDetails) {
|
||||
|
||||
validateParams(theMemberPatient, theCoverageToMatch, theCoverageToLink);
|
||||
|
||||
Optional<Coverage> coverageOpt = myMemberMatcherR4Helper.findMatchingCoverage(theCoverageToMatch);
|
||||
if ( ! coverageOpt.isPresent()) {
|
||||
String i18nMessage = getContext().getLocalizer().getMessage(
|
||||
"operation.member.match.error.coverage.not.found");
|
||||
throw new UnprocessableEntityException(i18nMessage);
|
||||
}
|
||||
Coverage coverage = coverageOpt.get();
|
||||
|
||||
Optional<Patient> patientOpt = myMemberMatcherR4Helper.getBeneficiaryPatient(coverage);
|
||||
if (! patientOpt.isPresent()) {
|
||||
String i18nMessage = getContext().getLocalizer().getMessage(
|
||||
"operation.member.match.error.beneficiary.not.found");
|
||||
throw new UnprocessableEntityException(i18nMessage);
|
||||
}
|
||||
Patient patient = patientOpt.get();
|
||||
|
||||
if (patient.getIdentifier().isEmpty()) {
|
||||
String i18nMessage = getContext().getLocalizer().getMessage(
|
||||
"operation.member.match.error.beneficiary.without.identifier");
|
||||
throw new UnprocessableEntityException(i18nMessage);
|
||||
}
|
||||
|
||||
myMemberMatcherR4Helper.addMemberIdentifierToMemberPatient(theMemberPatient, patient.getIdentifierFirstRep());
|
||||
|
||||
return myMemberMatcherR4Helper.buildSuccessReturnParameters(theMemberPatient, theCoverageToLink);
|
||||
}
|
||||
|
||||
|
||||
private void validateParams(Patient theMemberPatient, Coverage theOldCoverage, Coverage theNewCoverage) {
|
||||
validateParam(theMemberPatient, Constants.PARAM_MEMBER_PATIENT);
|
||||
validateParam(theOldCoverage, Constants.PARAM_OLD_COVERAGE);
|
||||
validateParam(theNewCoverage, Constants.PARAM_NEW_COVERAGE);
|
||||
}
|
||||
|
||||
|
||||
private void validateParam(Object theParam, String theParamName) {
|
||||
if (theParam == null) {
|
||||
String i18nMessage = getContext().getLocalizer().getMessage(
|
||||
"operation.member.match.error.missing.parameter", theParamName);
|
||||
throw new UnprocessableEntityException(i18nMessage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Given a list of string types, return only the ID portions of any parameters passed in.
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,172 @@
|
|||
package ca.uhn.fhir.jpa.provider.r4;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.model.primitive.IdDt;
|
||||
import ca.uhn.fhir.rest.param.StringParam;
|
||||
import ca.uhn.fhir.rest.param.TokenOrListParam;
|
||||
import ca.uhn.fhir.util.ParametersUtil;
|
||||
import com.google.common.collect.Lists;
|
||||
import org.hl7.fhir.instance.model.api.IBaseParameters;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.r4.model.CodeableConcept;
|
||||
import org.hl7.fhir.r4.model.Coding;
|
||||
import org.hl7.fhir.r4.model.Coverage;
|
||||
import org.hl7.fhir.r4.model.Identifier;
|
||||
import org.hl7.fhir.r4.model.Parameters;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.hl7.fhir.r4.model.Reference;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static ca.uhn.fhir.rest.api.Constants.PARAM_MEMBER_PATIENT;
|
||||
import static ca.uhn.fhir.rest.api.Constants.PARAM_NEW_COVERAGE;
|
||||
|
||||
/*
|
||||
* #%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%
|
||||
*/
|
||||
|
||||
public class MemberMatcherR4Helper {
|
||||
|
||||
private static final String OUT_COVERAGE_IDENTIFIER_CODE_SYSTEM = "http://terminology.hl7.org/CodeSystem/v2-0203";
|
||||
private static final String OUT_COVERAGE_IDENTIFIER_CODE = "UMB";
|
||||
private static final String OUT_COVERAGE_IDENTIFIER_TEXT = "Member Number";
|
||||
private static final String COVERAGE_TYPE = "Coverage";
|
||||
|
||||
private final FhirContext myFhirContext;
|
||||
|
||||
@Autowired
|
||||
private IFhirResourceDao<Coverage> myCoverageDao;
|
||||
|
||||
@Autowired
|
||||
private IFhirResourceDao<Patient> myPatientDao;
|
||||
|
||||
|
||||
public MemberMatcherR4Helper(FhirContext theContext) {
|
||||
myFhirContext = theContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find Coverage matching the received member (Patient) by coverage id or by coverage identifier
|
||||
* Matching by member patient demographics is not supported.
|
||||
*/
|
||||
public Optional<Coverage> findMatchingCoverage(Coverage theCoverageToMatch) {
|
||||
// search by received old coverage id
|
||||
List<IBaseResource> foundCoverages = findCoverageByCoverageId(theCoverageToMatch);
|
||||
if (foundCoverages.size() == 1 && isCoverage(foundCoverages.get(0))) {
|
||||
return Optional.of( (Coverage) foundCoverages.get(0) );
|
||||
}
|
||||
|
||||
// search by received old coverage identifier
|
||||
foundCoverages = findCoverageByCoverageIdentifier(theCoverageToMatch);
|
||||
if (foundCoverages.size() == 1 && isCoverage(foundCoverages.get(0))) {
|
||||
return Optional.of( (Coverage) foundCoverages.get(0) );
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
|
||||
private List<IBaseResource> findCoverageByCoverageIdentifier(Coverage theCoverageToMatch) {
|
||||
TokenOrListParam identifierParam = new TokenOrListParam();
|
||||
for (Identifier identifier : theCoverageToMatch.getIdentifier()) {
|
||||
identifierParam.add(identifier.getSystem(), identifier.getValue());
|
||||
}
|
||||
|
||||
SearchParameterMap paramMap = new SearchParameterMap()
|
||||
.add("identifier", identifierParam);
|
||||
ca.uhn.fhir.rest.api.server.IBundleProvider retVal = myCoverageDao.search(paramMap);
|
||||
|
||||
return retVal.getAllResources();
|
||||
}
|
||||
|
||||
|
||||
private boolean isCoverage(IBaseResource theIBaseResource) {
|
||||
return theIBaseResource.fhirType().equals(COVERAGE_TYPE);
|
||||
}
|
||||
|
||||
|
||||
private List<IBaseResource> findCoverageByCoverageId(Coverage theCoverageToMatch) {
|
||||
SearchParameterMap paramMap = new SearchParameterMap()
|
||||
.add("_id", new StringParam(theCoverageToMatch.getId()));
|
||||
ca.uhn.fhir.rest.api.server.IBundleProvider retVal = myCoverageDao.search(paramMap);
|
||||
|
||||
return retVal.getAllResources();
|
||||
}
|
||||
|
||||
|
||||
public Parameters buildSuccessReturnParameters(Patient theMemberPatient, Coverage theCoverage) {
|
||||
IBaseParameters parameters = ParametersUtil.newInstance(myFhirContext);
|
||||
ParametersUtil.addParameterToParameters(myFhirContext, parameters, PARAM_MEMBER_PATIENT, theMemberPatient);
|
||||
ParametersUtil.addParameterToParameters(myFhirContext, parameters, PARAM_NEW_COVERAGE, theCoverage);
|
||||
return (Parameters) parameters;
|
||||
}
|
||||
|
||||
|
||||
public void addMemberIdentifierToMemberPatient(Patient theMemberPatient, Identifier theNewIdentifier) {
|
||||
Coding coding = new Coding()
|
||||
.setSystem(OUT_COVERAGE_IDENTIFIER_CODE_SYSTEM)
|
||||
.setCode(OUT_COVERAGE_IDENTIFIER_CODE)
|
||||
.setDisplay(OUT_COVERAGE_IDENTIFIER_TEXT)
|
||||
.setUserSelected(false);
|
||||
|
||||
CodeableConcept concept = new CodeableConcept()
|
||||
.setCoding(Lists.newArrayList(coding))
|
||||
.setText(OUT_COVERAGE_IDENTIFIER_TEXT);
|
||||
|
||||
Identifier newIdentifier = new Identifier()
|
||||
.setUse(Identifier.IdentifierUse.USUAL)
|
||||
.setType(concept)
|
||||
.setSystem(theNewIdentifier.getSystem())
|
||||
.setValue(theNewIdentifier.getValue());
|
||||
|
||||
theMemberPatient.addIdentifier(newIdentifier);
|
||||
}
|
||||
|
||||
|
||||
public Optional<Patient> getBeneficiaryPatient(Coverage theCoverage) {
|
||||
if (theCoverage.getBeneficiaryTarget() == null && theCoverage.getBeneficiary() == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
if (theCoverage.getBeneficiaryTarget() != null
|
||||
&& ! theCoverage.getBeneficiaryTarget().getIdentifier().isEmpty()) {
|
||||
return Optional.of(theCoverage.getBeneficiaryTarget());
|
||||
}
|
||||
|
||||
Reference beneficiaryRef = theCoverage.getBeneficiary();
|
||||
if (beneficiaryRef == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
if (beneficiaryRef.getResource() != null) {
|
||||
return Optional.of((Patient) beneficiaryRef.getResource());
|
||||
}
|
||||
|
||||
if (beneficiaryRef.getReference() == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
Patient beneficiary = myPatientDao.read(new IdDt(beneficiaryRef.getReference()));
|
||||
return Optional.ofNullable(beneficiary);
|
||||
}
|
||||
}
|
|
@ -56,7 +56,7 @@ public class ReindexWriter implements ItemWriter<List<Long>> {
|
|||
|
||||
// Note that since our chunk size is 1, there will always be exactly one list
|
||||
for (List<Long> pidList : thePidLists) {
|
||||
partitionRunner.runInPartitionedThreads(new SliceImpl<>(pidList), pids -> reindexPids(pidList));
|
||||
partitionRunner.runInPartitionedThreads(new SliceImpl<>(pidList), pids -> reindexPids(pids));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -125,13 +125,13 @@ public class ElasticsearchSvcImpl implements IElasticsearchSvc {
|
|||
private PartitionSettings myPartitionSettings;
|
||||
|
||||
//This constructor used to inject a dummy partitionsettings in test.
|
||||
public ElasticsearchSvcImpl(PartitionSettings thePartitionSetings, String theHostname, @Nullable String theUsername, @Nullable String thePassword) {
|
||||
this(theHostname, theUsername, thePassword);
|
||||
public ElasticsearchSvcImpl(PartitionSettings thePartitionSetings, String theProtocol, String theHostname, @Nullable String theUsername, @Nullable String thePassword) {
|
||||
this(theProtocol, theHostname, theUsername, thePassword);
|
||||
this.myPartitionSettings = thePartitionSetings;
|
||||
}
|
||||
|
||||
public ElasticsearchSvcImpl(String theHostname, @Nullable String theUsername, @Nullable String thePassword) {
|
||||
myRestHighLevelClient = ElasticsearchRestClientFactory.createElasticsearchHighLevelRestClient("http", theHostname, theUsername, thePassword);
|
||||
public ElasticsearchSvcImpl(String theProtocol, String theHostname, @Nullable String theUsername, @Nullable String thePassword) {
|
||||
myRestHighLevelClient = ElasticsearchRestClientFactory.createElasticsearchHighLevelRestClient(theProtocol, theHostname, theUsername, thePassword);
|
||||
|
||||
try {
|
||||
createObservationIndexIfMissing();
|
||||
|
|
|
@ -30,7 +30,13 @@ import javax.persistence.EntityManagerFactory;
|
|||
public class TestJPAConfig {
|
||||
@Bean
|
||||
public DaoConfig daoConfig() {
|
||||
return new DaoConfig();
|
||||
DaoConfig retVal = new DaoConfig();
|
||||
|
||||
if ("true".equals(System.getProperty("mass_ingestion_mode"))) {
|
||||
retVal.setMassIngestionMode(true);
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
|
|
@ -53,6 +53,9 @@ public class TestR4Config extends BaseJavaConfigR4 {
|
|||
if ("true".equals(System.getProperty("single_db_connection"))) {
|
||||
ourMaxThreads = 1;
|
||||
}
|
||||
if ("true".equals(System.getProperty("unlimited_db_connection"))) {
|
||||
ourMaxThreads = 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ public class TestR4ConfigWithElasticsearchClient extends TestR4ConfigWithElastic
|
|||
public ElasticsearchSvcImpl myElasticsearchSvc() {
|
||||
int elasticsearchPort = elasticContainer().getMappedPort(9200);
|
||||
String host = elasticContainer().getHost();
|
||||
return new ElasticsearchSvcImpl(host + ":" + elasticsearchPort, null, null);
|
||||
return new ElasticsearchSvcImpl("http", host + ":" + elasticsearchPort, null, null);
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
|
|
|
@ -39,29 +39,27 @@ import org.hl7.fhir.r4.model.ServiceRequest;
|
|||
import org.hl7.fhir.r4.model.StringType;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.StringTokenizer;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.empty;
|
||||
import static org.hamcrest.Matchers.in;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@TestMethodOrder(MethodOrderer.MethodName.class)
|
||||
public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test {
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4QueryCountTest.class);
|
||||
|
||||
|
@ -70,7 +68,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
|
|||
myDaoConfig.setResourceMetaCountHardLimit(new DaoConfig().getResourceMetaCountHardLimit());
|
||||
myDaoConfig.setIndexMissingFields(new DaoConfig().getIndexMissingFields());
|
||||
myDaoConfig.setDeleteEnabled(new DaoConfig().isDeleteEnabled());
|
||||
myDaoConfig.setMatchUrlCache(new DaoConfig().getMatchUrlCache());
|
||||
myDaoConfig.setMatchUrlCacheEnabled(new DaoConfig().isMatchUrlCacheEnabled());
|
||||
myDaoConfig.setHistoryCountMode(DaoConfig.DEFAULT_HISTORY_COUNT_MODE);
|
||||
myDaoConfig.setMassIngestionMode(new DaoConfig().isMassIngestionMode());
|
||||
myModelConfig.setAutoVersionReferenceAtPaths(new ModelConfig().getAutoVersionReferenceAtPaths());
|
||||
|
@ -811,7 +809,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
|
|||
assertEquals(5, myCaptureQueriesListener.countInsertQueries());
|
||||
assertEquals(1, myCaptureQueriesListener.countUpdateQueries());
|
||||
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
|
||||
runInTransaction(()->assertEquals(4, myResourceTableDao.count()));
|
||||
runInTransaction(() -> assertEquals(4, myResourceTableDao.count()));
|
||||
|
||||
// Run it again - This time even the match URL should be cached
|
||||
|
||||
|
@ -822,7 +820,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
|
|||
assertEquals(4, myCaptureQueriesListener.countInsertQueries());
|
||||
assertEquals(1, myCaptureQueriesListener.countUpdateQueries());
|
||||
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
|
||||
runInTransaction(()->assertEquals(7, myResourceTableDao.count()));
|
||||
runInTransaction(() -> assertEquals(7, myResourceTableDao.count()));
|
||||
|
||||
// Once more for good measure
|
||||
|
||||
|
@ -833,28 +831,28 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
|
|||
assertEquals(4, myCaptureQueriesListener.countInsertQueries());
|
||||
assertEquals(1, myCaptureQueriesListener.countUpdateQueries());
|
||||
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
|
||||
runInTransaction(()->assertEquals(10, myResourceTableDao.count()));
|
||||
runInTransaction(() -> assertEquals(10, myResourceTableDao.count()));
|
||||
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private Bundle createTransactionWithCreatesAndOneMatchUrl() {
|
||||
BundleBuilder bb = new BundleBuilder(myFhirCtx);
|
||||
BundleBuilder bb = new BundleBuilder(myFhirCtx);
|
||||
|
||||
Patient p = new Patient();
|
||||
p.setId(IdType.newRandomUuid());
|
||||
p.setActive(true);
|
||||
bb.addTransactionCreateEntry(p);
|
||||
Patient p = new Patient();
|
||||
p.setId(IdType.newRandomUuid());
|
||||
p.setActive(true);
|
||||
bb.addTransactionCreateEntry(p);
|
||||
|
||||
Encounter enc = new Encounter();
|
||||
enc.setSubject(new Reference(p.getId()));
|
||||
enc.addParticipant().setIndividual(new Reference("Practitioner?identifier=foo|bar"));
|
||||
bb.addTransactionCreateEntry(enc);
|
||||
Encounter enc = new Encounter();
|
||||
enc.setSubject(new Reference(p.getId()));
|
||||
enc.addParticipant().setIndividual(new Reference("Practitioner?identifier=foo|bar"));
|
||||
bb.addTransactionCreateEntry(enc);
|
||||
|
||||
enc = new Encounter();
|
||||
enc.setSubject(new Reference(p.getId()));
|
||||
enc.addParticipant().setIndividual(new Reference("Practitioner?identifier=foo|bar"));
|
||||
bb.addTransactionCreateEntry(enc);
|
||||
enc = new Encounter();
|
||||
enc.setSubject(new Reference(p.getId()));
|
||||
enc.addParticipant().setIndividual(new Reference("Practitioner?identifier=foo|bar"));
|
||||
bb.addTransactionCreateEntry(enc);
|
||||
|
||||
return (Bundle) bb.getBundle();
|
||||
}
|
||||
|
@ -870,6 +868,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
|
|||
Practitioner pract = new Practitioner();
|
||||
pract.addIdentifier().setSystem("foo").setValue("bar");
|
||||
myPractitionerDao.create(pract);
|
||||
runInTransaction(() -> assertEquals(1, myResourceTableDao.count(), () -> myResourceTableDao.findAll().stream().map(t -> t.getIdDt().toUnqualifiedVersionless().getValue()).collect(Collectors.joining(","))));
|
||||
|
||||
// First pass
|
||||
|
||||
|
@ -881,7 +880,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
|
|||
assertEquals(5, myCaptureQueriesListener.countInsertQueries());
|
||||
assertEquals(1, myCaptureQueriesListener.countUpdateQueries());
|
||||
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
|
||||
runInTransaction(()->assertEquals(4, myResourceTableDao.count()));
|
||||
runInTransaction(() -> assertEquals(4, myResourceTableDao.count(), () -> myResourceTableDao.findAll().stream().map(t -> t.getIdDt().toUnqualifiedVersionless().getValue()).collect(Collectors.joining(","))));
|
||||
|
||||
// Run it again - This time even the match URL should be cached
|
||||
|
||||
|
@ -892,7 +891,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
|
|||
assertEquals(4, myCaptureQueriesListener.countInsertQueries());
|
||||
assertEquals(1, myCaptureQueriesListener.countUpdateQueries());
|
||||
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
|
||||
runInTransaction(()->assertEquals(7, myResourceTableDao.count()));
|
||||
runInTransaction(() -> assertEquals(7, myResourceTableDao.count()));
|
||||
|
||||
// Once more for good measure
|
||||
|
||||
|
@ -903,7 +902,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
|
|||
assertEquals(4, myCaptureQueriesListener.countInsertQueries());
|
||||
assertEquals(1, myCaptureQueriesListener.countUpdateQueries());
|
||||
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
|
||||
runInTransaction(()->assertEquals(10, myResourceTableDao.count()));
|
||||
runInTransaction(() -> assertEquals(10, myResourceTableDao.count()));
|
||||
|
||||
}
|
||||
|
||||
|
@ -923,9 +922,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
|
|||
pt2.addIdentifier().setSystem("http://foo").setValue("456");
|
||||
bb.addTransactionCreateEntry(pt2);
|
||||
|
||||
runInTransaction(() -> {
|
||||
assertEquals(0, myResourceTableDao.count());
|
||||
});
|
||||
runInTransaction(() -> assertEquals(0, myResourceTableDao.count()));
|
||||
|
||||
ourLog.info("About to start transaction");
|
||||
|
||||
|
@ -940,9 +937,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
|
|||
assertEquals(0, myCaptureQueriesListener.countUpdateQueries());
|
||||
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
|
||||
|
||||
runInTransaction(() -> {
|
||||
assertEquals(2, myResourceTableDao.count());
|
||||
});
|
||||
runInTransaction(() -> assertEquals(2, myResourceTableDao.count()));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -1299,7 +1294,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
|
|||
* Third time with mass ingestion mode enabled
|
||||
*/
|
||||
myDaoConfig.setMassIngestionMode(true);
|
||||
myDaoConfig.setMatchUrlCache(true);
|
||||
myDaoConfig.setMatchUrlCacheEnabled(true);
|
||||
|
||||
myCaptureQueriesListener.clear();
|
||||
outcome = mySystemDao.transaction(mySrd, input.get());
|
||||
|
@ -1331,7 +1326,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
|
|||
|
||||
@Test
|
||||
public void testTransactionWithConditionalCreate_MatchUrlCacheEnabled() {
|
||||
myDaoConfig.setMatchUrlCache(true);
|
||||
myDaoConfig.setMatchUrlCacheEnabled(true);
|
||||
|
||||
Supplier<Bundle> bundleCreator = () -> {
|
||||
BundleBuilder bb = new BundleBuilder(myFhirCtx);
|
||||
|
@ -2155,7 +2150,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
|
|||
@Test
|
||||
public void testMassIngestionMode_TransactionWithChanges() {
|
||||
myDaoConfig.setDeleteEnabled(false);
|
||||
myDaoConfig.setMatchUrlCache(true);
|
||||
myDaoConfig.setMatchUrlCacheEnabled(true);
|
||||
myDaoConfig.setMassIngestionMode(true);
|
||||
myFhirCtx.getParserOptions().setStripVersionsFromReferences(false);
|
||||
myModelConfig.setRespectVersionsForSearchIncludes(true);
|
||||
|
@ -2219,7 +2214,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
|
|||
@Test
|
||||
public void testMassIngestionMode_TransactionWithChanges_2() throws IOException {
|
||||
myDaoConfig.setDeleteEnabled(false);
|
||||
myDaoConfig.setMatchUrlCache(true);
|
||||
myDaoConfig.setMatchUrlCacheEnabled(true);
|
||||
myDaoConfig.setMassIngestionMode(true);
|
||||
myFhirCtx.getParserOptions().setStripVersionsFromReferences(false);
|
||||
myModelConfig.setRespectVersionsForSearchIncludes(true);
|
||||
|
|
|
@ -55,9 +55,7 @@ import ca.uhn.fhir.rest.param.UriParam;
|
|||
import ca.uhn.fhir.rest.param.UriParamQualifierEnum;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
|
||||
import ca.uhn.fhir.util.CollectionUtil;
|
||||
import ca.uhn.fhir.util.HapiExtensions;
|
||||
import ca.uhn.fhir.util.ResourceUtil;
|
||||
import com.google.common.collect.Lists;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
@ -133,11 +131,6 @@ import org.junit.jupiter.api.BeforeEach;
|
|||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.CsvFileSource;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
@ -149,12 +142,9 @@ import org.springframework.transaction.support.TransactionCallbackWithoutResult;
|
|||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
|
@ -162,7 +152,6 @@ import java.util.List;
|
|||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static ca.uhn.fhir.rest.api.Constants.PARAM_TYPE;
|
||||
import static org.apache.commons.lang3.StringUtils.countMatches;
|
||||
|
@ -190,17 +179,12 @@ import static org.mockito.Mockito.verify;
|
|||
@SuppressWarnings({"unchecked", "Duplicates"})
|
||||
@TestPropertySource(properties = {
|
||||
BaseJpaTest.CONFIG_ENABLE_LUCENE_FALSE
|
||||
})
|
||||
})
|
||||
public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4SearchNoFtTest.class);
|
||||
@Autowired
|
||||
MatchUrlService myMatchUrlService;
|
||||
|
||||
@BeforeAll
|
||||
public static void beforeAllTest(){
|
||||
System.setProperty("user.timezone", "EST");
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void afterResetSearchSize() {
|
||||
myDaoConfig.setReuseCachedSearchResultsForMillis(new DaoConfig().getReuseCachedSearchResultsForMillis());
|
||||
|
@ -209,6 +193,9 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
myDaoConfig.setSearchPreFetchThresholds(new DaoConfig().getSearchPreFetchThresholds());
|
||||
myDaoConfig.setIndexMissingFields(new DaoConfig().getIndexMissingFields());
|
||||
myModelConfig.setNormalizedQuantitySearchLevel(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_NOT_SUPPORTED);
|
||||
|
||||
myModelConfig.setAutoSupportDefaultSearchParams(true);
|
||||
mySearchParamRegistry.resetForUnitTest();
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
|
@ -217,6 +204,35 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
myDaoConfig.setReuseCachedSearchResultsForMillis(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDisableAutoSupportDefaultSearchParams() {
|
||||
myModelConfig.setAutoSupportDefaultSearchParams(false);
|
||||
mySearchParamRegistry.resetForUnitTest();
|
||||
|
||||
Patient patient = new Patient();
|
||||
patient.setActive(true);
|
||||
patient.addName().setFamily("FAMILY");
|
||||
myPatientDao.create(patient);
|
||||
|
||||
runInTransaction(() -> {
|
||||
assertEquals(0, myResourceIndexedSearchParamStringDao.count());
|
||||
assertEquals(0, myResourceIndexedSearchParamTokenDao.count());
|
||||
});
|
||||
|
||||
SearchParameterMap map = SearchParameterMap.newSynchronous("name", new StringParam("FAMILY"));
|
||||
try {
|
||||
myPatientDao.search(map, mySrd);
|
||||
fail();
|
||||
} catch (InvalidRequestException e) {
|
||||
// good
|
||||
}
|
||||
|
||||
// Make sure we can support mandatory SPs
|
||||
map = SearchParameterMap.newSynchronous("url", new UriParam("http://foo"));
|
||||
myCodeSystemDao.search(map, mySrd); // should not fail
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSearchInExistingTransaction() {
|
||||
createPatient(withBirthdate("2021-01-01"));
|
||||
|
@ -237,8 +253,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
public void testCanonicalReference() {
|
||||
StructureDefinition sd = new StructureDefinition();
|
||||
|
@ -665,7 +679,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* See #1053
|
||||
*/
|
||||
|
@ -1061,7 +1074,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testHasParameterChained() {
|
||||
IIdType pid0;
|
||||
|
@ -1415,15 +1427,15 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
|
||||
Bundle bundleResponse = mySystemDao.transaction(new SystemRequestDetails(), firstBundle);
|
||||
bundleResponse.getEntry()
|
||||
.forEach( entry -> assertThat(entry.getResponse().getStatus(), is(equalTo("201 Created"))));
|
||||
.forEach(entry -> assertThat(entry.getResponse().getStatus(), is(equalTo("201 Created"))));
|
||||
|
||||
IBundleProvider search = myOrganizationDao.search(new SearchParameterMap().setLoadSynchronous(true));
|
||||
assertEquals(1, search.getAllResources().size());
|
||||
|
||||
//Running the bundle again should just result in 0 new resources created, as the org should already exist, and there is no update to the SR.
|
||||
bundleResponse= mySystemDao.transaction(new SystemRequestDetails(), duplicateBundle);
|
||||
bundleResponse = mySystemDao.transaction(new SystemRequestDetails(), duplicateBundle);
|
||||
bundleResponse.getEntry()
|
||||
.forEach( entry -> {
|
||||
.forEach(entry -> {
|
||||
assertThat(entry.getResponse().getStatus(), is(equalTo("200 OK")));
|
||||
});
|
||||
|
||||
|
@ -1574,7 +1586,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testPeriodWithNoStart() {
|
||||
ServiceRequest serviceRequest = new ServiceRequest();
|
||||
|
@ -1611,7 +1622,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSearchByIdParamInverse() {
|
||||
String id1;
|
||||
|
@ -1641,7 +1651,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSearchByIdParam_QueryIsMinimal() {
|
||||
// With only an _id parameter
|
||||
|
@ -1919,7 +1928,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSearchWithIncludeStarQualified() {
|
||||
|
||||
|
@ -1984,7 +1992,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
assertThat(ids, containsInAnyOrder(obsId, encId));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testComponentQuantity() {
|
||||
Observation o1 = new Observation();
|
||||
|
@ -2061,7 +2068,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSearchDate_TimingValueUsingPeriod() {
|
||||
ServiceRequest p1 = new ServiceRequest();
|
||||
|
@ -2081,7 +2087,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSearchDateWrongParam() {
|
||||
Patient p1 = new Patient();
|
||||
|
@ -2105,7 +2110,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testDateRangeOnPeriod_SearchByDateTime_NoUpperBound() {
|
||||
Encounter enc = new Encounter();
|
||||
|
@ -2153,7 +2157,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
assertThat(ids, empty());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testDateRangeOnPeriod_SearchByDate_NoUpperBound() {
|
||||
Encounter enc = new Encounter();
|
||||
|
@ -2201,7 +2204,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
assertThat(ids, empty());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testDateRangeOnPeriod_SearchByDateTime_NoLowerBound() {
|
||||
Encounter enc = new Encounter();
|
||||
|
@ -2249,7 +2251,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
assertThat(ids, contains(id1));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testDateRangeOnPeriod_SearchByDate_NoLowerBound() {
|
||||
Encounter enc = new Encounter();
|
||||
|
@ -2297,7 +2298,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
assertThat(ids, contains(id1));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testDatePeriodParamEndOnly() {
|
||||
{
|
||||
|
@ -2436,7 +2436,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* See #1174
|
||||
*/
|
||||
|
@ -2546,7 +2545,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSearchLastUpdatedParam() {
|
||||
String methodName = "testSearchLastUpdatedParam";
|
||||
|
@ -2861,7 +2859,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSearchNumberParam() {
|
||||
RiskAssessment e1 = new RiskAssessment();
|
||||
|
@ -2985,7 +2982,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSearchParamChangesType() {
|
||||
String name = "testSearchParamChangesType";
|
||||
|
@ -3133,7 +3129,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSearchResourceLinkWithChain() {
|
||||
Patient patient = new Patient();
|
||||
|
@ -3417,7 +3412,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSearchTokenListWithMixedCombinations() {
|
||||
|
||||
|
@ -3665,7 +3659,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
assertEquals(5, countMatches(searchQuery.toUpperCase(), "LEFT OUTER JOIN"), searchQuery);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSearchWithDateRange() {
|
||||
|
||||
|
@ -3792,7 +3785,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSearchDoubleToken() {
|
||||
Patient patient = new Patient();
|
||||
|
@ -3829,7 +3821,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSearchDoubleString() {
|
||||
Patient patient = new Patient();
|
||||
|
@ -4297,7 +4288,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSearchReferenceUntyped() {
|
||||
Patient p = new Patient();
|
||||
|
@ -4334,7 +4324,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
assertEquals("This search uses an unqualified resource(a parameter in a chain without a resource type). This is less efficient than using a qualified type. If you know what you're looking for, try qualifying it using the form: 'entity:[resourceType]'", message.getMessage());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSearchWithDateAndReusesExistingJoin() {
|
||||
// Add a search parameter to Observation.issued, so that between that one
|
||||
|
@ -4434,7 +4423,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSearchWithFetchSizeDefaultMaximum() {
|
||||
myDaoConfig.setFetchSizeDefaultMaximum(5);
|
||||
|
@ -5119,7 +5107,7 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
p.addName().setFamily("A1");
|
||||
myPatientDao.create(p);
|
||||
|
||||
runInTransaction(()->assertEquals(0, mySearchEntityDao.count()));
|
||||
runInTransaction(() -> assertEquals(0, mySearchEntityDao.count()));
|
||||
|
||||
SearchParameterMap map = new SearchParameterMap();
|
||||
StringOrListParam or = new StringOrListParam();
|
||||
|
@ -5130,7 +5118,7 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
map.add(Patient.SP_NAME, or);
|
||||
IBundleProvider results = myPatientDao.search(map);
|
||||
assertEquals(1, results.getResources(0, 10).size());
|
||||
runInTransaction(()->assertEquals(1, mySearchEntityDao.count()));
|
||||
runInTransaction(() -> assertEquals(1, mySearchEntityDao.count()));
|
||||
|
||||
map = new SearchParameterMap();
|
||||
or = new StringOrListParam();
|
||||
|
@ -5143,7 +5131,7 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
results = myPatientDao.search(map);
|
||||
assertEquals(1, results.getResources(0, 10).size());
|
||||
// We expect a new one because we don't cache the search URL for very long search URLs
|
||||
runInTransaction(()->assertEquals(2, mySearchEntityDao.count()));
|
||||
runInTransaction(() -> assertEquals(2, mySearchEntityDao.count()));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -5189,7 +5177,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testDateSearchParametersShouldBeTimezoneIndependent() {
|
||||
|
||||
|
@ -5316,7 +5303,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
));
|
||||
}
|
||||
|
||||
|
||||
private void createObservationWithEffective(String theId, String theEffective) {
|
||||
Observation obs = new Observation();
|
||||
obs.setId(theId);
|
||||
|
@ -5337,7 +5323,7 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
p.addName().setFamily("A1");
|
||||
myPatientDao.create(p);
|
||||
|
||||
runInTransaction(()->assertEquals(0, mySearchEntityDao.count()));
|
||||
runInTransaction(() -> assertEquals(0, mySearchEntityDao.count()));
|
||||
|
||||
SearchParameterMap map = new SearchParameterMap();
|
||||
StringOrListParam or = new StringOrListParam();
|
||||
|
@ -5348,7 +5334,7 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
map.add(Patient.SP_NAME, or);
|
||||
IBundleProvider results = myPatientDao.search(map);
|
||||
assertEquals(1, results.getResources(0, 10).size());
|
||||
assertEquals(1, runInTransaction(()->mySearchEntityDao.count()));
|
||||
assertEquals(1, runInTransaction(() -> mySearchEntityDao.count()));
|
||||
|
||||
map = new SearchParameterMap();
|
||||
or = new StringOrListParam();
|
||||
|
@ -5359,7 +5345,7 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
map.add(Patient.SP_NAME, or);
|
||||
results = myPatientDao.search(map);
|
||||
assertEquals(1, results.getResources(0, 10).size());
|
||||
assertEquals(1, runInTransaction(()->mySearchEntityDao.count()));
|
||||
assertEquals(1, runInTransaction(() -> mySearchEntityDao.count()));
|
||||
|
||||
}
|
||||
|
||||
|
@ -5404,7 +5390,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
assertThat(toUnqualifiedVersionlessIdValues(outcome), contains(crId));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSearchWithTwoChainedDates() {
|
||||
// Matches
|
||||
|
@ -5457,7 +5442,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testCircularReferencesDontBreakRevIncludes() {
|
||||
|
||||
|
@ -5495,7 +5479,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
|
||||
}
|
||||
|
||||
|
||||
private String toStringMultiline(List<?> theResults) {
|
||||
StringBuilder b = new StringBuilder();
|
||||
for (Object next : theResults) {
|
||||
|
@ -5505,5 +5488,10 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
return b.toString();
|
||||
}
|
||||
|
||||
@BeforeAll
|
||||
public static void beforeAllTest() {
|
||||
System.setProperty("user.timezone", "EST");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -136,6 +136,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
|
|||
myDaoConfig.setBundleBatchMaxPoolSize(new DaoConfig().getBundleBatchMaxPoolSize());
|
||||
myDaoConfig.setAutoCreatePlaceholderReferenceTargets(new DaoConfig().isAutoCreatePlaceholderReferenceTargets());
|
||||
myModelConfig.setAutoVersionReferenceAtPaths(new ModelConfig().getAutoVersionReferenceAtPaths());
|
||||
myFhirCtx.getParserOptions().setAutoContainReferenceTargetsWithNoId(true);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
|
@ -1043,6 +1044,25 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
|
|||
assertThat(task.getBasedOn().get(0).getReference(), matchesPattern("Patient/[0-9]+"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTransactionNoContainedRedux_ContainedProcessingDisabled() throws IOException {
|
||||
myFhirCtx.getParserOptions().setAutoContainReferenceTargetsWithNoId(false);
|
||||
|
||||
//Pre-create the patient, which will cause the ifNoneExist to prevent a new creation during bundle transaction
|
||||
Patient patient = loadResourceFromClasspath(Patient.class, "/r4/preexisting-patient.json");
|
||||
myPatientDao.create(patient);
|
||||
|
||||
//Post the Bundle containing a conditional POST with an identical patient from the above resource.
|
||||
Bundle request = loadResourceFromClasspath(Bundle.class, "/r4/transaction-no-contained-2.json");
|
||||
|
||||
Bundle outcome = mySystemDao.transaction(mySrd, request);
|
||||
|
||||
IdType taskId = new IdType(outcome.getEntry().get(0).getResponse().getLocation());
|
||||
Task task = myTaskDao.read(taskId, mySrd);
|
||||
|
||||
assertThat(task.getBasedOn().get(0).getReference(), matchesPattern("Patient/[0-9]+"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTransactionNoContained() throws IOException {
|
||||
|
||||
|
|
|
@ -0,0 +1,203 @@
|
|||
package ca.uhn.fhir.jpa.dao.r4;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao;
|
||||
import ca.uhn.fhir.jpa.config.TestR4Config;
|
||||
import ca.uhn.fhir.jpa.config.TestR4WithLuceneDisabledConfig;
|
||||
import ca.uhn.fhir.jpa.dao.BaseJpaTest;
|
||||
import ca.uhn.fhir.jpa.dao.BaseTransactionProcessor;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceEncodingEnum;
|
||||
import ca.uhn.fhir.jpa.partition.SystemRequestDetails;
|
||||
import ca.uhn.fhir.jpa.search.reindex.BlockPolicy;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||
import ca.uhn.fhir.util.StopWatch;
|
||||
import org.apache.commons.compress.utils.Lists;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.hl7.fhir.r4.model.Bundle;
|
||||
import org.hl7.fhir.r4.model.Meta;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration(classes = {TestR4WithLuceneDisabledConfig.class})
|
||||
@DirtiesContext
|
||||
public class SyntheaPerfTest extends BaseJpaTest {
|
||||
|
||||
@BeforeAll
|
||||
public static void beforeAll() {
|
||||
System.setProperty("unlimited_db_connection", "true");
|
||||
System.setProperty("mass_ingestion_mode", "true");
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void afterAll() {
|
||||
System.clearProperty("unlimited_db_connection");
|
||||
System.clearProperty("mass_ingestion_mode");
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void afterEach() {
|
||||
myCtx.getParserOptions().setAutoContainReferenceTargetsWithNoId(true);
|
||||
}
|
||||
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(SyntheaPerfTest.class);
|
||||
private static final FhirContext ourCtx = FhirContext.forR4Cached();
|
||||
public static final String PATH_TO_SYNTHEA_OUTPUT = "../../synthea/output/fhir/";
|
||||
public static final int CONCURRENCY = 4;
|
||||
@Autowired
|
||||
private FhirContext myCtx;
|
||||
@Autowired
|
||||
private PlatformTransactionManager myTxManager;
|
||||
@Autowired
|
||||
private IFhirSystemDao<Bundle, Meta> mySystemDao;
|
||||
|
||||
@Disabled
|
||||
@Test
|
||||
public void testLoadSynthea() throws Exception {
|
||||
assertEquals(100, TestR4Config.getMaxThreads());
|
||||
|
||||
myDaoConfig.setResourceEncoding(ResourceEncodingEnum.JSON);
|
||||
myDaoConfig.setTagStorageMode(DaoConfig.TagStorageModeEnum.INLINE);
|
||||
myDaoConfig.setMatchUrlCacheEnabled(true);
|
||||
myDaoConfig.setDeleteEnabled(false);
|
||||
myCtx.getParserOptions().setAutoContainReferenceTargetsWithNoId(false);
|
||||
|
||||
assertTrue(myDaoConfig.isMassIngestionMode());
|
||||
|
||||
List<Path> files = Files
|
||||
.list(FileSystems.getDefault().getPath(PATH_TO_SYNTHEA_OUTPUT))
|
||||
.filter(t->t.toString().endsWith(".json"))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<Path> meta = files.stream().filter(t -> t.toString().contains("hospital") || t.toString().contains("practitioner")).collect(Collectors.toList());
|
||||
new Uploader(meta);
|
||||
|
||||
List<Path> nonMeta = files.stream().filter(t -> !t.toString().contains("hospital") && !t.toString().contains("practitioner")).collect(Collectors.toList());
|
||||
|
||||
// new Uploader(Collections.singletonList(nonMeta.remove(0)));
|
||||
// new Uploader(Collections.singletonList(nonMeta.remove(0)));
|
||||
// new Uploader(Collections.singletonList(nonMeta.remove(0)));
|
||||
new Uploader(Collections.singletonList(nonMeta.remove(0)));
|
||||
|
||||
new Uploader(nonMeta);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected FhirContext getContext() {
|
||||
return myCtx;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PlatformTransactionManager getTxManager() {
|
||||
return myTxManager;
|
||||
}
|
||||
|
||||
|
||||
private class Uploader {
|
||||
|
||||
private final ThreadPoolTaskExecutor myExecutor;
|
||||
private final int myTotal;
|
||||
private final StopWatch mySw;
|
||||
private final AtomicInteger myFilesCounter = new AtomicInteger(0);
|
||||
private final AtomicInteger myResourcesCounter = new AtomicInteger(0);
|
||||
|
||||
public Uploader(List<Path> thePaths) throws ExecutionException, InterruptedException {
|
||||
Validate.isTrue(thePaths.size() > 0);
|
||||
myTotal = thePaths.size();
|
||||
|
||||
myExecutor = new ThreadPoolTaskExecutor();
|
||||
myExecutor.setCorePoolSize(0);
|
||||
myExecutor.setMaxPoolSize(CONCURRENCY);
|
||||
myExecutor.setQueueCapacity(100);
|
||||
myExecutor.setAllowCoreThreadTimeOut(true);
|
||||
myExecutor.setThreadNamePrefix("Uploader-");
|
||||
myExecutor.setRejectedExecutionHandler(new BlockPolicy());
|
||||
myExecutor.initialize();
|
||||
|
||||
mySw = new StopWatch();
|
||||
List<Future<?>> futures = new ArrayList<>();
|
||||
for (Path next : thePaths) {
|
||||
futures.add(myExecutor.submit(new MyTask(next)));
|
||||
}
|
||||
|
||||
for (Future<?> next : futures) {
|
||||
next.get();
|
||||
}
|
||||
|
||||
ourLog.info("Finished uploading {} files with {} resources in {} - {} files/sec - {} res/sec",
|
||||
myFilesCounter.get(),
|
||||
myResourcesCounter.get(),
|
||||
mySw,
|
||||
mySw.formatThroughput(myFilesCounter.get(), TimeUnit.SECONDS),
|
||||
mySw.formatThroughput(myResourcesCounter.get(), TimeUnit.SECONDS));
|
||||
}
|
||||
|
||||
private class MyTask implements Runnable {
|
||||
|
||||
private final Path myPath;
|
||||
|
||||
public MyTask(Path thePath) {
|
||||
myPath = thePath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Bundle bundle;
|
||||
try (FileReader reader = new FileReader(myPath.toFile())) {
|
||||
bundle = ourCtx.newJsonParser().parseResource(Bundle.class, reader);
|
||||
} catch (IOException e) {
|
||||
throw new InternalErrorException(e);
|
||||
}
|
||||
|
||||
mySystemDao.transaction(new SystemRequestDetails(myInterceptorRegistry), bundle);
|
||||
|
||||
int fileCount = myFilesCounter.incrementAndGet();
|
||||
myResourcesCounter.addAndGet(bundle.getEntry().size());
|
||||
|
||||
if (fileCount % 10 == 0) {
|
||||
ourLog.info("Have uploaded {} files with {} resources in {} - {} files/sec - {} res/sec",
|
||||
myFilesCounter.get(),
|
||||
myResourcesCounter.get(),
|
||||
mySw,
|
||||
mySw.formatThroughput(myFilesCounter.get(), TimeUnit.SECONDS),
|
||||
mySw.formatThroughput(myResourcesCounter.get(), TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,241 @@
|
|||
package ca.uhn.fhir.jpa.provider.r4;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.model.api.IQueryParameterType;
|
||||
import ca.uhn.fhir.model.primitive.IdDt;
|
||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||
import com.google.common.collect.Lists;
|
||||
import org.hl7.fhir.r4.model.Coverage;
|
||||
import org.hl7.fhir.r4.model.Identifier;
|
||||
import org.hl7.fhir.r4.model.Parameters;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.hl7.fhir.r4.model.Reference;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Answers;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static ca.uhn.fhir.rest.api.Constants.PARAM_MEMBER_PATIENT;
|
||||
import static ca.uhn.fhir.rest.api.Constants.PARAM_NEW_COVERAGE;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.isA;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class MemberMatcherR4HelperTest {
|
||||
|
||||
private final FhirContext myFhirContext = FhirContext.forR4();
|
||||
@Mock private IFhirResourceDao<Coverage> myCoverageDao;
|
||||
@Mock private IFhirResourceDao<Patient> myPatientDao;
|
||||
|
||||
private MemberMatcherR4Helper myTestedHelper;
|
||||
|
||||
@Mock private Coverage myCoverageToMatch;
|
||||
@Mock private Patient myPatient;
|
||||
@Mock private IBundleProvider myBundleProvider;
|
||||
|
||||
private final Coverage myMatchedCoverage = new Coverage();
|
||||
private final Identifier myMatchingIdentifier = new Identifier()
|
||||
.setSystem("identifier-system").setValue("identifier-value");
|
||||
|
||||
@Captor ArgumentCaptor<SearchParameterMap> mySearchParameterMapCaptor;
|
||||
|
||||
@BeforeEach
|
||||
public void beforeEach() {
|
||||
myTestedHelper = new MemberMatcherR4Helper(myFhirContext);
|
||||
|
||||
// @InjectMocks didn't work
|
||||
ReflectionTestUtils.setField(myTestedHelper, "myCoverageDao", myCoverageDao);
|
||||
ReflectionTestUtils.setField(myTestedHelper, "myPatientDao", myPatientDao);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void findMatchingCoverageMatchByIdReturnsMatched() {
|
||||
when(myCoverageToMatch.getId()).thenReturn("cvg-to-match-id");
|
||||
when(myCoverageDao.search(isA(SearchParameterMap.class))).thenReturn(myBundleProvider);
|
||||
when(myBundleProvider.getAllResources()).thenReturn(Collections.singletonList(myMatchedCoverage));
|
||||
|
||||
Optional<Coverage> result = myTestedHelper.findMatchingCoverage(myCoverageToMatch);
|
||||
|
||||
assertEquals(Optional.of(myMatchedCoverage), result);
|
||||
verify(myCoverageDao).search(mySearchParameterMapCaptor.capture());
|
||||
SearchParameterMap spMap = mySearchParameterMapCaptor.getValue();
|
||||
assertTrue(spMap.containsKey("_id"));
|
||||
List<List<IQueryParameterType>> listListParams = spMap.get("_id");
|
||||
assertEquals(1, listListParams.size());
|
||||
assertEquals(1, listListParams.get(0).size());
|
||||
IQueryParameterType param = listListParams.get(0).get(0);
|
||||
assertEquals("cvg-to-match-id", param.getValueAsQueryToken(myFhirContext));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void findMatchingCoverageMatchByIdentifierReturnsMatched() {
|
||||
when(myCoverageToMatch.getId()).thenReturn("non-matching-id");
|
||||
when(myCoverageToMatch.getIdentifier()).thenReturn(Collections.singletonList(myMatchingIdentifier));
|
||||
when(myCoverageDao.search(isA(SearchParameterMap.class))).thenReturn(myBundleProvider);
|
||||
when(myBundleProvider.getAllResources()).thenReturn(
|
||||
Collections.emptyList(), Collections.singletonList(myMatchedCoverage));
|
||||
|
||||
Optional<Coverage> result = myTestedHelper.findMatchingCoverage(myCoverageToMatch);
|
||||
|
||||
assertEquals(Optional.of(myMatchedCoverage), result);
|
||||
verify(myCoverageDao, times(2)).search(mySearchParameterMapCaptor.capture());
|
||||
List<SearchParameterMap> spMap = mySearchParameterMapCaptor.getAllValues();
|
||||
assertTrue(spMap.get(0).containsKey("_id"));
|
||||
assertTrue(spMap.get(1).containsKey("identifier"));
|
||||
List<List<IQueryParameterType>> listListParams = spMap.get(1).get("identifier");
|
||||
assertEquals(1, listListParams.size());
|
||||
assertEquals(1, listListParams.get(0).size());
|
||||
IQueryParameterType param = listListParams.get(0).get(0);
|
||||
assertEquals(myMatchingIdentifier.getSystem() + "|" + myMatchingIdentifier.getValue(),
|
||||
param.getValueAsQueryToken(myFhirContext));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void findMatchingCoverageNoMatchReturnsEmpty() {
|
||||
when(myCoverageToMatch.getId()).thenReturn("non-matching-id");
|
||||
when(myCoverageToMatch.getIdentifier()).thenReturn(Collections.singletonList(myMatchingIdentifier));
|
||||
when(myCoverageDao.search(isA(SearchParameterMap.class))).thenReturn(myBundleProvider);
|
||||
when(myBundleProvider.getAllResources()).thenReturn(Collections.emptyList(), Collections.emptyList());
|
||||
|
||||
Optional<Coverage> result = myTestedHelper.findMatchingCoverage(myCoverageToMatch);
|
||||
|
||||
assertFalse(result.isPresent());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void buildSuccessReturnParameters() {
|
||||
Patient patient = new Patient();
|
||||
Coverage coverage = new Coverage();
|
||||
|
||||
Parameters result = myTestedHelper.buildSuccessReturnParameters(patient, coverage);
|
||||
|
||||
assertEquals(PARAM_MEMBER_PATIENT, result.getParameter().get(0).getName());
|
||||
assertEquals(patient, result.getParameter().get(0).getResource());
|
||||
|
||||
assertEquals(PARAM_NEW_COVERAGE, result.getParameter().get(1).getName());
|
||||
assertEquals(coverage, result.getParameter().get(1).getResource());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void addMemberIdentifierToMemberPatient() {
|
||||
Identifier originalIdentifier = new Identifier()
|
||||
.setSystem("original-identifier-system").setValue("original-identifier-value");
|
||||
|
||||
Identifier newIdentifier = new Identifier()
|
||||
.setSystem("new-identifier-system").setValue("new-identifier-value");
|
||||
|
||||
Patient patient = new Patient().setIdentifier(Lists.newArrayList(originalIdentifier));
|
||||
|
||||
myTestedHelper.addMemberIdentifierToMemberPatient(patient, newIdentifier);
|
||||
|
||||
assertEquals(2, patient.getIdentifier().size());
|
||||
|
||||
assertEquals("original-identifier-system", patient.getIdentifier().get(0).getSystem());
|
||||
assertEquals("original-identifier-value", patient.getIdentifier().get(0).getValue());
|
||||
|
||||
assertEquals("new-identifier-system", patient.getIdentifier().get(1).getSystem());
|
||||
assertEquals("new-identifier-value", patient.getIdentifier().get(1).getValue());
|
||||
}
|
||||
|
||||
@Nested
|
||||
public class TestGetBeneficiaryPatient {
|
||||
|
||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||
private Coverage coverage;
|
||||
|
||||
|
||||
@Test
|
||||
void noBeneficiaryOrBeneficiaryTargetReturnsEmpty() {
|
||||
when(coverage.getBeneficiaryTarget()).thenReturn(null);
|
||||
when(coverage.getBeneficiary()).thenReturn(null);
|
||||
|
||||
Optional<Patient> result = myTestedHelper.getBeneficiaryPatient(coverage);
|
||||
|
||||
assertFalse(result.isPresent());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void beneficiaryTargetWithNoIdentifierReturnsEmpty() {
|
||||
when(coverage.getBeneficiary()).thenReturn(null);
|
||||
when(coverage.getBeneficiaryTarget()).thenReturn(new Patient());
|
||||
|
||||
Optional<Patient> result = myTestedHelper.getBeneficiaryPatient(coverage);
|
||||
|
||||
assertFalse(result.isPresent());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void beneficiaryTargetWithIdentifierReturnsBeneficiary() {
|
||||
Patient patient = new Patient().setIdentifier(Collections.singletonList(new Identifier()));
|
||||
when(coverage.getBeneficiaryTarget()).thenReturn(patient);
|
||||
|
||||
Optional<Patient> result = myTestedHelper.getBeneficiaryPatient(coverage);
|
||||
|
||||
assertTrue(result.isPresent());
|
||||
assertEquals(patient, result.get());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void beneficiaryReferenceResourceReturnsBeneficiary() {
|
||||
Patient patient = new Patient().setIdentifier(Collections.singletonList(new Identifier()));
|
||||
when(coverage.getBeneficiaryTarget()).thenReturn(null);
|
||||
when(coverage.getBeneficiary().getResource()).thenReturn(patient);
|
||||
|
||||
Optional<Patient> result = myTestedHelper.getBeneficiaryPatient(coverage);
|
||||
|
||||
assertTrue(result.isPresent());
|
||||
assertEquals(patient, result.get());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void beneficiaryReferenceNoResourceOrReferenceReturnsEmpty() {
|
||||
when(coverage.getBeneficiaryTarget()).thenReturn(null);
|
||||
when(coverage.getBeneficiary()).thenReturn(new Reference());
|
||||
|
||||
Optional<Patient> result = myTestedHelper.getBeneficiaryPatient(coverage);
|
||||
|
||||
assertFalse(result.isPresent());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void beneficiaryReferenceReferenceReturnsReadPatient() {
|
||||
when(coverage.getBeneficiaryTarget()).thenReturn(null);
|
||||
when(coverage.getBeneficiary().getResource()).thenReturn(null);
|
||||
when(coverage.getBeneficiary().getReference()).thenReturn("patient-id");
|
||||
|
||||
myTestedHelper.getBeneficiaryPatient(coverage);
|
||||
|
||||
verify(myPatientDao).read(new IdDt("patient-id"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,388 @@
|
|||
package ca.uhn.fhir.jpa.provider.r4;
|
||||
|
||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.parser.StrictErrorHandler;
|
||||
import ca.uhn.fhir.rest.api.Constants;
|
||||
import ca.uhn.fhir.rest.api.EncodingEnum;
|
||||
import ca.uhn.fhir.rest.client.apache.ResourceEntity;
|
||||
import ca.uhn.fhir.util.ParametersUtil;
|
||||
import com.google.common.collect.Lists;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.hl7.fhir.instance.model.api.IBase;
|
||||
import org.hl7.fhir.r4.model.CodeableConcept;
|
||||
import org.hl7.fhir.r4.model.Coding;
|
||||
import org.hl7.fhir.r4.model.Coverage;
|
||||
import org.hl7.fhir.r4.model.Enumerations;
|
||||
import org.hl7.fhir.r4.model.HumanName;
|
||||
import org.hl7.fhir.r4.model.Identifier;
|
||||
import org.hl7.fhir.r4.model.Organization;
|
||||
import org.hl7.fhir.r4.model.Parameters;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.hl7.fhir.r4.model.Reference;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static ca.uhn.fhir.rest.api.Constants.PARAM_MEMBER_PATIENT;
|
||||
import static ca.uhn.fhir.rest.api.Constants.PARAM_NEW_COVERAGE;
|
||||
import static ca.uhn.fhir.rest.api.Constants.PARAM_OLD_COVERAGE;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
@SuppressWarnings("Duplicates")
|
||||
public class PatientMemberMatchOperationR4Test extends BaseResourceProviderR4Test {
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(PatientMemberMatchOperationR4Test.class);
|
||||
|
||||
private static final String ourQuery = "/Patient/$member-match?_format=json";
|
||||
private static final String EXISTING_COVERAGE_ID = "cov-id-123";
|
||||
private static final String EXISTING_COVERAGE_IDENT_SYSTEM = "http://centene.com/insurancePlanIds";
|
||||
private static final String EXISTING_COVERAGE_IDENT_VALUE = "U1234567890";
|
||||
private static final String EXISTING_COVERAGE_PATIENT_IDENT_SYSTEM = "http://oldhealthplan.example.com";
|
||||
private static final String EXISTING_COVERAGE_PATIENT_IDENT_VALUE = "DHU-55678";
|
||||
|
||||
private Identifier ourExistingCoverageIdentifier;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
public void beforeDisableResultReuse() {
|
||||
myDaoConfig.setReuseCachedSearchResultsForMillis(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@AfterEach
|
||||
public void after() throws Exception {
|
||||
super.after();
|
||||
myDaoConfig.setReuseCachedSearchResultsForMillis(new DaoConfig().getReuseCachedSearchResultsForMillis());
|
||||
myDaoConfig.setEverythingIncludesFetchPageSize(new DaoConfig().getEverythingIncludesFetchPageSize());
|
||||
myDaoConfig.setSearchPreFetchThresholds(new DaoConfig().getSearchPreFetchThresholds());
|
||||
myDaoConfig.setAllowExternalReferences(new DaoConfig().isAllowExternalReferences());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@BeforeEach
|
||||
public void before() throws Exception {
|
||||
super.before();
|
||||
myFhirCtx.setParserErrorHandler(new StrictErrorHandler());
|
||||
}
|
||||
|
||||
|
||||
private void createCoverageWithBeneficiary(
|
||||
boolean theAssociateBeneficiaryPatient, boolean includeBeneficiaryIdentifier) {
|
||||
|
||||
Patient member = null;
|
||||
if (theAssociateBeneficiaryPatient) {
|
||||
// Patient
|
||||
member = new Patient().setName(Lists.newArrayList(new HumanName()
|
||||
.setUse(HumanName.NameUse.OFFICIAL).setFamily("Person").addGiven("Patricia").addGiven("Ann")));
|
||||
if (includeBeneficiaryIdentifier) {
|
||||
member.setIdentifier(Collections.singletonList(new Identifier()
|
||||
.setSystem(EXISTING_COVERAGE_PATIENT_IDENT_SYSTEM).setValue(EXISTING_COVERAGE_PATIENT_IDENT_VALUE)));
|
||||
}
|
||||
|
||||
myClient.create().resource(member).execute().getId().toUnqualifiedVersionless().getValue();
|
||||
}
|
||||
|
||||
// Coverage
|
||||
ourExistingCoverageIdentifier = new Identifier()
|
||||
.setSystem(EXISTING_COVERAGE_IDENT_SYSTEM).setValue(EXISTING_COVERAGE_IDENT_VALUE);
|
||||
Coverage ourExistingCoverage = new Coverage()
|
||||
.setStatus(Coverage.CoverageStatus.ACTIVE)
|
||||
.setIdentifier(Collections.singletonList(ourExistingCoverageIdentifier));
|
||||
|
||||
if (theAssociateBeneficiaryPatient) {
|
||||
// this doesn't work
|
||||
// myOldCoverage.setBeneficiaryTarget(patient)
|
||||
ourExistingCoverage.setBeneficiary(new Reference(member))
|
||||
.setId(EXISTING_COVERAGE_ID);
|
||||
}
|
||||
|
||||
myClient.create().resource(ourExistingCoverage).execute().getId().toUnqualifiedVersionless().getValue();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testMemberMatchByCoverageId() throws Exception {
|
||||
createCoverageWithBeneficiary(true, true);
|
||||
|
||||
// patient doesn't participate in match
|
||||
Patient patient = new Patient().setGender(Enumerations.AdministrativeGender.FEMALE);
|
||||
|
||||
// Old Coverage
|
||||
Coverage oldCoverage = new Coverage();
|
||||
oldCoverage.setId(EXISTING_COVERAGE_ID); // must match field
|
||||
|
||||
// New Coverage (must return unchanged)
|
||||
Coverage newCoverage = new Coverage();
|
||||
newCoverage.setId("AA87654");
|
||||
newCoverage.setIdentifier(Lists.newArrayList(
|
||||
new Identifier().setSystem("http://newealthplan.example.com").setValue("234567")));
|
||||
|
||||
Parameters inputParameters = buildInputParameters(patient, oldCoverage, newCoverage);
|
||||
Parameters parametersResponse = performOperation(ourServerBase + ourQuery,
|
||||
EncodingEnum.JSON, inputParameters);
|
||||
|
||||
validateMemberPatient(parametersResponse);
|
||||
validateNewCoverage(parametersResponse, newCoverage);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testCoverageNoBeneficiaryReturns422() throws Exception {
|
||||
createCoverageWithBeneficiary(false, false);
|
||||
|
||||
// patient doesn't participate in match
|
||||
Patient patient = new Patient().setGender(Enumerations.AdministrativeGender.FEMALE);
|
||||
|
||||
// Old Coverage
|
||||
Coverage oldCoverage = new Coverage();
|
||||
oldCoverage.setId(EXISTING_COVERAGE_ID); // must match field
|
||||
|
||||
// New Coverage (must return unchanged)
|
||||
Coverage newCoverage = new Coverage();
|
||||
newCoverage.setId("AA87654");
|
||||
newCoverage.setIdentifier(Lists.newArrayList(
|
||||
new Identifier().setSystem("http://newealthplan.example.com").setValue("234567")));
|
||||
|
||||
Parameters inputParameters = buildInputParameters(patient, oldCoverage, newCoverage);
|
||||
performOperationExpecting422(ourServerBase + ourQuery, EncodingEnum.JSON, inputParameters,
|
||||
"Could not find beneficiary for coverage.");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testCoverageBeneficiaryNoIdentifierReturns422() throws Exception {
|
||||
createCoverageWithBeneficiary(true, false);
|
||||
|
||||
// patient doesn't participate in match
|
||||
Patient patient = new Patient().setGender(Enumerations.AdministrativeGender.FEMALE);
|
||||
|
||||
// Old Coverage
|
||||
Coverage oldCoverage = new Coverage();
|
||||
oldCoverage.setId(EXISTING_COVERAGE_ID); // must match field
|
||||
|
||||
// New Coverage (must return unchanged)
|
||||
Coverage newCoverage = new Coverage();
|
||||
newCoverage.setId("AA87654");
|
||||
newCoverage.setIdentifier(Lists.newArrayList(
|
||||
new Identifier().setSystem("http://newealthplan.example.com").setValue("234567")));
|
||||
|
||||
Parameters inputParameters = buildInputParameters(patient, oldCoverage, newCoverage);
|
||||
performOperationExpecting422(ourServerBase + ourQuery, EncodingEnum.JSON, inputParameters,
|
||||
"Coverage beneficiary does not have an identifier.");
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
public class ValidateParameterErrors {
|
||||
private Patient ourPatient;
|
||||
private Coverage ourOldCoverage;
|
||||
private Coverage ourNewCoverage;
|
||||
|
||||
@BeforeEach
|
||||
public void beforeValidateParameterErrors() {
|
||||
ourPatient = new Patient().setGender(Enumerations.AdministrativeGender.FEMALE);
|
||||
|
||||
ourOldCoverage = new Coverage();
|
||||
ourOldCoverage.setId(EXISTING_COVERAGE_ID);
|
||||
|
||||
ourNewCoverage = new Coverage();
|
||||
ourNewCoverage.setId("AA87654");
|
||||
ourNewCoverage.setIdentifier(Lists.newArrayList(
|
||||
new Identifier().setSystem("http://newealthplan.example.com").setValue("234567")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidPatient() throws Exception {
|
||||
Parameters inputParameters = buildInputParameters(new Patient(), ourOldCoverage, ourNewCoverage);
|
||||
performOperationExpecting422(ourServerBase + ourQuery, EncodingEnum.JSON, inputParameters,
|
||||
"Parameter \\\"" + PARAM_MEMBER_PATIENT + "\\\" is required.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidOldCoverage() throws Exception {
|
||||
Parameters inputParameters = buildInputParameters(ourPatient, new Coverage(), ourNewCoverage);
|
||||
performOperationExpecting422(ourServerBase + ourQuery, EncodingEnum.JSON, inputParameters,
|
||||
"Parameter \\\"" + PARAM_OLD_COVERAGE + "\\\" is required.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidNewCoverage() throws Exception {
|
||||
Parameters inputParameters = buildInputParameters(ourPatient, ourOldCoverage, new Coverage());
|
||||
performOperationExpecting422(ourServerBase + ourQuery, EncodingEnum.JSON, inputParameters,
|
||||
"Parameter \\\"" + PARAM_NEW_COVERAGE + "\\\" is required.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testMemberMatchByCoverageIdentifier() throws Exception {
|
||||
createCoverageWithBeneficiary(true, true);
|
||||
|
||||
// patient doesn't participate in match
|
||||
Patient patient = new Patient().setGender(Enumerations.AdministrativeGender.FEMALE);
|
||||
|
||||
// Old Coverage
|
||||
Coverage oldCoverage = new Coverage();
|
||||
oldCoverage.setId("9876B1");
|
||||
oldCoverage.setIdentifier(Lists.newArrayList(ourExistingCoverageIdentifier)); // must match field
|
||||
|
||||
// New Coverage (must return unchanged)
|
||||
Coverage newCoverage = new Coverage();
|
||||
newCoverage.setId("AA87654");
|
||||
newCoverage.setIdentifier(Lists.newArrayList(
|
||||
new Identifier().setSystem("http://newealthplan.example.com").setValue("234567")));
|
||||
|
||||
Parameters inputParameters = buildInputParameters(patient, oldCoverage, newCoverage);
|
||||
Parameters parametersResponse = performOperation(ourServerBase + ourQuery, EncodingEnum.JSON, inputParameters);
|
||||
|
||||
validateMemberPatient(parametersResponse);
|
||||
validateNewCoverage(parametersResponse, newCoverage);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Validates that second resource from the response is same as the received coverage
|
||||
*/
|
||||
private void validateNewCoverage(Parameters theResponse, Coverage theOriginalCoverage) {
|
||||
List<IBase> patientList = ParametersUtil.getNamedParameters(getContext(), theResponse, PARAM_NEW_COVERAGE);
|
||||
assertEquals(1, patientList.size());
|
||||
Coverage respCoverage = (Coverage) theResponse.getParameter().get(1).getResource();
|
||||
|
||||
assertEquals("Coverage/" + theOriginalCoverage.getId(), respCoverage.getId());
|
||||
assertEquals(theOriginalCoverage.getIdentifierFirstRep().getSystem(), respCoverage.getIdentifierFirstRep().getSystem());
|
||||
assertEquals(theOriginalCoverage.getIdentifierFirstRep().getValue(), respCoverage.getIdentifierFirstRep().getValue());
|
||||
}
|
||||
|
||||
|
||||
private void validateMemberPatient(Parameters response) {
|
||||
// parameter MemberPatient must have a new identifier with:
|
||||
// {
|
||||
// "use": "usual",
|
||||
// "type": {
|
||||
// "coding": [
|
||||
// {
|
||||
// "system": "http://terminology.hl7.org/CodeSystem/v2-0203",
|
||||
// "code": "UMB",
|
||||
// "display": "Member Number",
|
||||
// "userSelected": false
|
||||
// }
|
||||
// ],
|
||||
// "text": "Member Number"
|
||||
// },
|
||||
// "system": COVERAGE_PATIENT_IDENT_SYSTEM,
|
||||
// "value": COVERAGE_PATIENT_IDENT_VALUE
|
||||
// }
|
||||
List<IBase> patientList = ParametersUtil.getNamedParameters(getContext(), response, PARAM_MEMBER_PATIENT);
|
||||
assertEquals(1, patientList.size());
|
||||
Patient resultPatient = (Patient) response.getParameter().get(0).getResource();
|
||||
|
||||
assertNotNull(resultPatient.getIdentifier());
|
||||
assertEquals(1, resultPatient.getIdentifier().size());
|
||||
Identifier addedIdentifier = resultPatient.getIdentifier().get(0);
|
||||
assertEquals(Identifier.IdentifierUse.USUAL, addedIdentifier.getUse());
|
||||
checkCoding(addedIdentifier.getType());
|
||||
assertEquals(EXISTING_COVERAGE_PATIENT_IDENT_SYSTEM, addedIdentifier.getSystem());
|
||||
assertEquals(EXISTING_COVERAGE_PATIENT_IDENT_VALUE, addedIdentifier.getValue());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testNoCoverageMatchFound() throws Exception {
|
||||
// Patient doesn't participate in match
|
||||
Patient patient = new Patient().setGender(Enumerations.AdministrativeGender.FEMALE);
|
||||
|
||||
// Old Coverage
|
||||
Coverage oldCoverage = new Coverage();
|
||||
oldCoverage.setId("9876B1");
|
||||
oldCoverage.setIdentifier(Lists.newArrayList(
|
||||
new Identifier().setSystem("http://oldhealthplan.example.com").setValue("DH10001235")));
|
||||
|
||||
// New Coverage
|
||||
Organization newOrg = new Organization();
|
||||
newOrg.setId("Organization/ProviderOrg1");
|
||||
newOrg.setName("New Health Plan");
|
||||
|
||||
Coverage newCoverage = new Coverage();
|
||||
newCoverage.setId("AA87654");
|
||||
newCoverage.getContained().add(newOrg);
|
||||
newCoverage.setIdentifier(Lists.newArrayList(
|
||||
new Identifier().setSystem("http://newealthplan.example.com").setValue("234567")));
|
||||
|
||||
Parameters inputParameters = buildInputParameters(patient, oldCoverage, newCoverage);
|
||||
performOperationExpecting422(ourServerBase + ourQuery, EncodingEnum.JSON, inputParameters,
|
||||
"Could not find coverage for member");
|
||||
}
|
||||
|
||||
|
||||
private Parameters buildInputParameters(Patient thePatient, Coverage theOldCoverage, Coverage theNewCoverage) {
|
||||
Parameters p = new Parameters();
|
||||
ParametersUtil.addParameterToParameters(getContext(), p, PARAM_MEMBER_PATIENT, thePatient);
|
||||
ParametersUtil.addParameterToParameters(getContext(), p, PARAM_OLD_COVERAGE, theOldCoverage);
|
||||
ParametersUtil.addParameterToParameters(getContext(), p, PARAM_NEW_COVERAGE, theNewCoverage);
|
||||
return p;
|
||||
}
|
||||
|
||||
|
||||
private Parameters performOperation(String theUrl,
|
||||
EncodingEnum theEncoding, Parameters theInputParameters) throws Exception {
|
||||
|
||||
HttpPost post = new HttpPost(theUrl);
|
||||
post.addHeader(Constants.HEADER_ACCEPT_ENCODING, theEncoding.toString());
|
||||
post.setEntity(new ResourceEntity(getContext(), theInputParameters));
|
||||
ourLog.info("Request: {}", post);
|
||||
try (CloseableHttpResponse response = ourHttpClient.execute(post)) {
|
||||
assertEquals(200, response.getStatusLine().getStatusCode());
|
||||
|
||||
return theEncoding.newParser(myFhirCtx).parseResource(Parameters.class,
|
||||
IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void performOperationExpecting422(String theUrl, EncodingEnum theEncoding,
|
||||
Parameters theInputParameters, String theExpectedErrorMsg) throws Exception {
|
||||
|
||||
HttpPost post = new HttpPost(theUrl);
|
||||
post.addHeader(Constants.HEADER_ACCEPT_ENCODING, theEncoding.toString());
|
||||
post.setEntity(new ResourceEntity(getContext(), theInputParameters));
|
||||
ourLog.info("Request: {}", post);
|
||||
try (CloseableHttpResponse response = ourHttpClient.execute(post)) {
|
||||
String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
ourLog.info("Response: {}", responseString);
|
||||
assertEquals(422, response.getStatusLine().getStatusCode());
|
||||
assertThat(responseString, containsString(theExpectedErrorMsg));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void checkCoding(CodeableConcept theType) {
|
||||
// must match:
|
||||
// "coding": [
|
||||
// {
|
||||
// "system": "http://terminology.hl7.org/CodeSystem/v2-0203",
|
||||
// "code": "UMB",
|
||||
// "display": "Member Number",
|
||||
// "userSelected": false
|
||||
// }
|
||||
// * ]
|
||||
Coding coding = theType.getCoding().get(0);
|
||||
assertEquals("http://terminology.hl7.org/CodeSystem/v2-0203", coding.getSystem());
|
||||
assertEquals("UMB", coding.getCode());
|
||||
assertEquals("Member Number", coding.getDisplay());
|
||||
assertFalse(coding.getUserSelected());
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,257 @@
|
|||
package ca.uhn.fhir.jpa.provider.r4;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.support.IValidationSupport;
|
||||
import ca.uhn.fhir.rest.annotation.IdParam;
|
||||
import ca.uhn.fhir.rest.annotation.Operation;
|
||||
import ca.uhn.fhir.rest.annotation.OperationParam;
|
||||
import ca.uhn.fhir.rest.annotation.RequiredParam;
|
||||
import ca.uhn.fhir.rest.annotation.Search;
|
||||
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
|
||||
import ca.uhn.fhir.rest.param.UriParam;
|
||||
import ca.uhn.fhir.rest.server.IResourceProvider;
|
||||
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
|
||||
import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.r4.model.BooleanType;
|
||||
import org.hl7.fhir.r4.model.CodeSystem;
|
||||
import org.hl7.fhir.r4.model.CodeType;
|
||||
import org.hl7.fhir.r4.model.IdType;
|
||||
import org.hl7.fhir.r4.model.Parameters;
|
||||
import org.hl7.fhir.r4.model.StringType;
|
||||
import org.hl7.fhir.r4.model.UriType;
|
||||
import org.hl7.fhir.r4.model.ValueSet;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
/*
|
||||
* This set of Unit Tests simulates the call to a remote server and therefore, only tests the code in the
|
||||
* {@link org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport#invokeRemoteValidateCode}
|
||||
* method, before and after it makes that remote client call.
|
||||
*/
|
||||
public class RemoteTerminologyServiceResourceProviderR4Test {
|
||||
private static final String DISPLAY = "DISPLAY";
|
||||
private static final String CODE_SYSTEM = "CODE_SYS";
|
||||
private static final String CODE = "CODE";
|
||||
private static final String VALUE_SET_URL = "http://value.set/url";
|
||||
private static final String SAMPLE_MESSAGE = "This is a sample message";
|
||||
private static FhirContext ourCtx = FhirContext.forR4();
|
||||
private MyCodeSystemProvider myCodeSystemProvider = new MyCodeSystemProvider();
|
||||
private MyValueSetProvider myValueSetProvider = new MyValueSetProvider();
|
||||
|
||||
@RegisterExtension
|
||||
public RestfulServerExtension myRestfulServerExtension = new RestfulServerExtension(ourCtx, myCodeSystemProvider,
|
||||
myValueSetProvider);
|
||||
|
||||
private RemoteTerminologyServiceValidationSupport mySvc;
|
||||
|
||||
@BeforeEach
|
||||
public void before_ConfigureService() {
|
||||
String myBaseUrl = "http://localhost:" + myRestfulServerExtension.getPort();
|
||||
mySvc = new RemoteTerminologyServiceValidationSupport(ourCtx, myBaseUrl);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void after_UnregisterProviders() {
|
||||
ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.ONCE);
|
||||
myRestfulServerExtension.getRestfulServer().getInterceptorService().unregisterAllInterceptors();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateCodeInCodeSystem_BlankCode_ReturnsNull() {
|
||||
IValidationSupport.CodeValidationResult outcome = mySvc
|
||||
.validateCode(null, null, CODE_SYSTEM, null, DISPLAY, null);
|
||||
assertNull(outcome);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateCodeInCodeSystem_ProvidingMinimalInputs_ReturnsSuccess() {
|
||||
createNextCodeSystemReturnParameters(true, null, null);
|
||||
|
||||
IValidationSupport.CodeValidationResult outcome = mySvc
|
||||
.validateCode(null, null, CODE_SYSTEM, CODE, null, null);
|
||||
assertEquals(CODE, outcome.getCode());
|
||||
assertEquals(null, outcome.getSeverity());
|
||||
assertEquals(null, outcome.getMessage());
|
||||
|
||||
assertEquals(CODE, myCodeSystemProvider.myLastCode.getCode());
|
||||
assertEquals(CODE_SYSTEM, myCodeSystemProvider.myLastUrl.getValueAsString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateCodeInCodeSystem_WithMessageValue_ReturnsMessage() {
|
||||
createNextCodeSystemReturnParameters(true, DISPLAY, SAMPLE_MESSAGE);
|
||||
|
||||
IValidationSupport.CodeValidationResult outcome = mySvc
|
||||
.validateCode(null, null, CODE_SYSTEM, CODE, DISPLAY, null);
|
||||
assertEquals(CODE, outcome.getCode());
|
||||
assertEquals(DISPLAY, outcome.getDisplay());
|
||||
assertEquals(null, outcome.getSeverity());
|
||||
assertEquals(null, outcome.getMessage());
|
||||
|
||||
assertEquals(CODE, myCodeSystemProvider.myLastCode.getCode());
|
||||
assertEquals(DISPLAY, myCodeSystemProvider.myLastDisplay.getValue());
|
||||
assertEquals(CODE_SYSTEM, myCodeSystemProvider.myLastUrl.getValueAsString());
|
||||
assertEquals(SAMPLE_MESSAGE, myCodeSystemProvider.myNextReturnParams.getParameter("message").toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateCodeInCodeSystem_AssumeFailure_ReturnsFailureCodeAndFailureMessage() {
|
||||
createNextCodeSystemReturnParameters(false, null, SAMPLE_MESSAGE);
|
||||
|
||||
IValidationSupport.CodeValidationResult outcome = mySvc
|
||||
.validateCode(null, null, CODE_SYSTEM, CODE, null, null);
|
||||
assertEquals(IValidationSupport.IssueSeverity.ERROR, outcome.getSeverity());
|
||||
assertEquals(SAMPLE_MESSAGE, outcome.getMessage());
|
||||
|
||||
assertEquals(false, ((BooleanType)myCodeSystemProvider.myNextReturnParams.getParameter("result")).booleanValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateCodeInValueSet_ProvidingMinimalInputs_ReturnsSuccess() {
|
||||
createNextValueSetReturnParameters(true, null, null);
|
||||
|
||||
IValidationSupport.CodeValidationResult outcome = mySvc
|
||||
.validateCode(null, null, CODE_SYSTEM, CODE, null, VALUE_SET_URL);
|
||||
assertEquals(CODE, outcome.getCode());
|
||||
assertEquals(null, outcome.getSeverity());
|
||||
assertEquals(null, outcome.getMessage());
|
||||
|
||||
assertEquals(CODE, myValueSetProvider.myLastCode.getCode());
|
||||
assertEquals(VALUE_SET_URL, myValueSetProvider.myLastUrl.getValueAsString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateCodeInValueSet_WithMessageValue_ReturnsMessage() {
|
||||
createNextValueSetReturnParameters(true, DISPLAY, SAMPLE_MESSAGE);
|
||||
|
||||
IValidationSupport.CodeValidationResult outcome = mySvc
|
||||
.validateCode(null, null, CODE_SYSTEM, CODE, DISPLAY, VALUE_SET_URL);
|
||||
assertEquals(CODE, outcome.getCode());
|
||||
assertEquals(DISPLAY, outcome.getDisplay());
|
||||
assertEquals(null, outcome.getSeverity());
|
||||
assertEquals(null, outcome.getMessage());
|
||||
|
||||
assertEquals(CODE, myValueSetProvider.myLastCode.getCode());
|
||||
assertEquals(DISPLAY, myValueSetProvider.myLastDisplay.getValue());
|
||||
assertEquals(VALUE_SET_URL, myValueSetProvider.myLastUrl.getValueAsString());
|
||||
assertEquals(SAMPLE_MESSAGE, myValueSetProvider.myNextReturnParams.getParameter("message").toString());
|
||||
}
|
||||
|
||||
private void createNextCodeSystemReturnParameters(boolean theResult, String theDisplay, String theMessage) {
|
||||
myCodeSystemProvider.myNextReturnParams = new Parameters();
|
||||
myCodeSystemProvider.myNextReturnParams.addParameter("result", theResult);
|
||||
myCodeSystemProvider.myNextReturnParams.addParameter("display", theDisplay);
|
||||
if (theMessage != null) {
|
||||
myCodeSystemProvider.myNextReturnParams.addParameter("message", theMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private void createNextValueSetReturnParameters(boolean theResult, String theDisplay, String theMessage) {
|
||||
myValueSetProvider.myNextReturnParams = new Parameters();
|
||||
myValueSetProvider.myNextReturnParams.addParameter("result", theResult);
|
||||
myValueSetProvider.myNextReturnParams.addParameter("display", theDisplay);
|
||||
if (theMessage != null) {
|
||||
myValueSetProvider.myNextReturnParams.addParameter("message", theMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private static class MyCodeSystemProvider implements IResourceProvider {
|
||||
private UriParam myLastUrlParam;
|
||||
private List<CodeSystem> myNextReturnCodeSystems;
|
||||
private int myInvocationCount;
|
||||
private UriType myLastUrl;
|
||||
private CodeType myLastCode;
|
||||
private StringType myLastDisplay;
|
||||
private Parameters myNextReturnParams;
|
||||
|
||||
@Operation(name = "validate-code", idempotent = true, returnParameters = {
|
||||
@OperationParam(name = "result", type = BooleanType.class, min = 1),
|
||||
@OperationParam(name = "message", type = StringType.class),
|
||||
@OperationParam(name = "display", type = StringType.class)
|
||||
})
|
||||
public Parameters validateCode(
|
||||
HttpServletRequest theServletRequest,
|
||||
@IdParam(optional = true) IdType theId,
|
||||
@OperationParam(name = "url", min = 0, max = 1) UriType theCodeSystemUrl,
|
||||
@OperationParam(name = "code", min = 0, max = 1) CodeType theCode,
|
||||
@OperationParam(name = "display", min = 0, max = 1) StringType theDisplay
|
||||
) {
|
||||
myInvocationCount++;
|
||||
myLastUrl = theCodeSystemUrl;
|
||||
myLastCode = theCode;
|
||||
myLastDisplay = theDisplay;
|
||||
return myNextReturnParams;
|
||||
|
||||
}
|
||||
|
||||
@Search
|
||||
public List<CodeSystem> find(@RequiredParam(name = "url") UriParam theUrlParam) {
|
||||
myLastUrlParam = theUrlParam;
|
||||
assert myNextReturnCodeSystems != null;
|
||||
return myNextReturnCodeSystems;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends IBaseResource> getResourceType() {
|
||||
return CodeSystem.class;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class MyValueSetProvider implements IResourceProvider {
|
||||
private Parameters myNextReturnParams;
|
||||
private List<ValueSet> myNextReturnValueSets;
|
||||
private UriType myLastUrl;
|
||||
private CodeType myLastCode;
|
||||
private int myInvocationCount;
|
||||
private UriType myLastSystem;
|
||||
private StringType myLastDisplay;
|
||||
private ValueSet myLastValueSet;
|
||||
private UriParam myLastUrlParam;
|
||||
|
||||
@Operation(name = "validate-code", idempotent = true, returnParameters = {
|
||||
@OperationParam(name = "result", type = BooleanType.class, min = 1),
|
||||
@OperationParam(name = "message", type = StringType.class),
|
||||
@OperationParam(name = "display", type = StringType.class)
|
||||
})
|
||||
public Parameters validateCode(
|
||||
HttpServletRequest theServletRequest,
|
||||
@IdParam(optional = true) IdType theId,
|
||||
@OperationParam(name = "url", min = 0, max = 1) UriType theValueSetUrl,
|
||||
@OperationParam(name = "code", min = 0, max = 1) CodeType theCode,
|
||||
@OperationParam(name = "system", min = 0, max = 1) UriType theSystem,
|
||||
@OperationParam(name = "display", min = 0, max = 1) StringType theDisplay,
|
||||
@OperationParam(name = "valueSet") ValueSet theValueSet
|
||||
) {
|
||||
myInvocationCount++;
|
||||
myLastUrl = theValueSetUrl;
|
||||
myLastCode = theCode;
|
||||
myLastSystem = theSystem;
|
||||
myLastDisplay = theDisplay;
|
||||
myLastValueSet = theValueSet;
|
||||
return myNextReturnParams;
|
||||
}
|
||||
|
||||
@Search
|
||||
public List<ValueSet> find(@RequiredParam(name = "url") UriParam theUrlParam) {
|
||||
myLastUrlParam = theUrlParam;
|
||||
assert myNextReturnValueSets != null;
|
||||
return myNextReturnValueSets;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends IBaseResource> getResourceType() {
|
||||
return ValueSet.class;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,270 @@
|
|||
package ca.uhn.fhir.jpa.provider.r4;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.config.BaseConfig;
|
||||
import ca.uhn.fhir.jpa.model.util.JpaConstants;
|
||||
import ca.uhn.fhir.rest.annotation.IdParam;
|
||||
import ca.uhn.fhir.rest.annotation.Operation;
|
||||
import ca.uhn.fhir.rest.annotation.OperationParam;
|
||||
import ca.uhn.fhir.rest.annotation.RequiredParam;
|
||||
import ca.uhn.fhir.rest.annotation.Search;
|
||||
import ca.uhn.fhir.rest.param.UriParam;
|
||||
import ca.uhn.fhir.rest.server.IResourceProvider;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
|
||||
import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport;
|
||||
import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.r4.model.BooleanType;
|
||||
import org.hl7.fhir.r4.model.CodeSystem;
|
||||
import org.hl7.fhir.r4.model.CodeType;
|
||||
import org.hl7.fhir.r4.model.Coding;
|
||||
import org.hl7.fhir.r4.model.IdType;
|
||||
import org.hl7.fhir.r4.model.Parameters;
|
||||
import org.hl7.fhir.r4.model.StringType;
|
||||
import org.hl7.fhir.r4.model.UriType;
|
||||
import org.hl7.fhir.r4.model.ValueSet;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
/*
|
||||
* This set of Unit Tests instantiates and injects an instance of
|
||||
* {@link org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport}
|
||||
* into the ValidationSupportChain, which tests the logic of dynamically selecting the correct Remote Terminology
|
||||
* implementation. It also exercises the code found in
|
||||
* {@link org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport#invokeRemoteValidateCode}
|
||||
*/
|
||||
public class ResourceProviderR4RemoteTerminologyTest extends BaseResourceProviderR4Test {
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResourceProviderR4RemoteTerminologyTest.class);
|
||||
private static final String DISPLAY = "DISPLAY";
|
||||
private static final String DISPLAY_BODY_MASS_INDEX = "Body mass index (BMI) [Ratio]";
|
||||
private static final String CODE_BODY_MASS_INDEX = "39156-5";
|
||||
private static FhirContext ourCtx = FhirContext.forR4();
|
||||
private MyCodeSystemProvider myCodeSystemProvider = new MyCodeSystemProvider();
|
||||
private MyValueSetProvider myValueSetProvider = new MyValueSetProvider();
|
||||
|
||||
@RegisterExtension
|
||||
public RestfulServerExtension myRestfulServerExtension = new RestfulServerExtension(ourCtx, myCodeSystemProvider,
|
||||
myValueSetProvider);
|
||||
|
||||
private RemoteTerminologyServiceValidationSupport mySvc;
|
||||
|
||||
@Autowired
|
||||
@Qualifier(BaseConfig.JPA_VALIDATION_SUPPORT_CHAIN)
|
||||
private ValidationSupportChain myValidationSupportChain;
|
||||
|
||||
@BeforeEach
|
||||
public void before_addRemoteTerminologySupport() throws Exception {
|
||||
String baseUrl = "http://localhost:" + myRestfulServerExtension.getPort();
|
||||
mySvc = new RemoteTerminologyServiceValidationSupport(ourCtx, baseUrl);
|
||||
myValidationSupportChain.addValidationSupport(0, mySvc);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void after_removeRemoteTerminologySupport() {
|
||||
myValidationSupportChain.removeValidationSupport(mySvc);
|
||||
myRestfulServerExtension.getRestfulServer().getInterceptorService().unregisterAllInterceptors();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateCodeOperationOnCodeSystem_ByCodingAndUrlWhereSystemIsDifferent_ThrowsException() {
|
||||
assertThrows(InvalidRequestException.class, () -> {
|
||||
Parameters respParam = myClient
|
||||
.operation()
|
||||
.onType(CodeSystem.class)
|
||||
.named(JpaConstants.OPERATION_VALIDATE_CODE)
|
||||
.withParameter(Parameters.class, "coding", new Coding().setSystem("http://terminology.hl7.org/CodeSystem/v2-0247").setCode("P"))
|
||||
.andParameter("url", new UriType("http://terminology.hl7.org/CodeSystem/INVALID-CODESYSTEM"))
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateCodeOperationOnCodeSystem_ByCodingAndUrl_UsingBuiltInCodeSystems() {
|
||||
myCodeSystemProvider.myNextReturnCodeSystems = new ArrayList<>();
|
||||
myCodeSystemProvider.myNextReturnCodeSystems.add((CodeSystem) new CodeSystem().setId("CodeSystem/v2-0247"));
|
||||
createNextCodeSystemReturnParameters(true, DISPLAY, null);
|
||||
|
||||
Parameters respParam = myClient
|
||||
.operation()
|
||||
.onType(CodeSystem.class)
|
||||
.named(JpaConstants.OPERATION_VALIDATE_CODE)
|
||||
.withParameter(Parameters.class, "coding", new Coding().setSystem("http://terminology.hl7.org/CodeSystem/v2-0247").setCode("P"))
|
||||
.andParameter("url", new UriType("http://terminology.hl7.org/CodeSystem/v2-0247"))
|
||||
.execute();
|
||||
|
||||
String resp = myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(respParam);
|
||||
ourLog.info(resp);
|
||||
|
||||
assertEquals(true, ((BooleanType)respParam.getParameter("result")).booleanValue());
|
||||
assertEquals(DISPLAY, respParam.getParameter("display").toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateCodeOperationOnValueSet_ByUrlAndSystem_UsingBuiltInCodeSystems() {
|
||||
myCodeSystemProvider.myNextReturnCodeSystems = new ArrayList<>();
|
||||
myCodeSystemProvider.myNextReturnCodeSystems.add((CodeSystem) new CodeSystem().setId("CodeSystem/list-example-use-codes"));
|
||||
myValueSetProvider.myNextReturnValueSets = new ArrayList<>();
|
||||
myValueSetProvider.myNextReturnValueSets.add((ValueSet) new ValueSet().setId("ValueSet/list-example-codes"));
|
||||
createNextValueSetReturnParameters(true, DISPLAY, null);
|
||||
|
||||
Parameters respParam = myClient
|
||||
.operation()
|
||||
.onType(ValueSet.class)
|
||||
.named(JpaConstants.OPERATION_VALIDATE_CODE)
|
||||
.withParameter(Parameters.class, "code", new CodeType("alerts"))
|
||||
.andParameter("system", new UriType("http://terminology.hl7.org/CodeSystem/list-example-use-codes"))
|
||||
.andParameter("url", new UriType("http://hl7.org/fhir/ValueSet/list-example-codes"))
|
||||
.useHttpGet()
|
||||
.execute();
|
||||
|
||||
String resp = myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(respParam);
|
||||
ourLog.info(resp);
|
||||
|
||||
assertEquals(true, ((BooleanType)respParam.getParameter("result")).booleanValue());
|
||||
assertEquals(DISPLAY, respParam.getParameter("display").toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateCodeOperationOnValueSet_ByUrlSystemAndCode() {
|
||||
myCodeSystemProvider.myNextReturnCodeSystems = new ArrayList<>();
|
||||
myCodeSystemProvider.myNextReturnCodeSystems.add((CodeSystem) new CodeSystem().setId("CodeSystem/list-example-use-codes"));
|
||||
myValueSetProvider.myNextReturnValueSets = new ArrayList<>();
|
||||
myValueSetProvider.myNextReturnValueSets.add((ValueSet) new ValueSet().setId("ValueSet/list-example-codes"));
|
||||
createNextValueSetReturnParameters(true, DISPLAY_BODY_MASS_INDEX, null);
|
||||
|
||||
Parameters respParam = myClient
|
||||
.operation()
|
||||
.onType(ValueSet.class)
|
||||
.named(JpaConstants.OPERATION_VALIDATE_CODE)
|
||||
.withParameter(Parameters.class, "code", new CodeType(CODE_BODY_MASS_INDEX))
|
||||
.andParameter("url", new UriType("https://loinc.org"))
|
||||
.andParameter("system", new UriType("http://loinc.org"))
|
||||
.execute();
|
||||
|
||||
String resp = myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(respParam);
|
||||
ourLog.info(resp);
|
||||
|
||||
assertEquals(true, ((BooleanType)respParam.getParameter("result")).booleanValue());
|
||||
assertEquals(DISPLAY_BODY_MASS_INDEX, respParam.getParameter("display").toString());
|
||||
}
|
||||
|
||||
private void createNextCodeSystemReturnParameters(boolean theResult, String theDisplay, String theMessage) {
|
||||
myCodeSystemProvider.myNextReturnParams = new Parameters();
|
||||
myCodeSystemProvider.myNextReturnParams.addParameter("result", theResult);
|
||||
myCodeSystemProvider.myNextReturnParams.addParameter("display", theDisplay);
|
||||
if (theMessage != null) {
|
||||
myCodeSystemProvider.myNextReturnParams.addParameter("message", theMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private void createNextValueSetReturnParameters(boolean theResult, String theDisplay, String theMessage) {
|
||||
myValueSetProvider.myNextReturnParams = new Parameters();
|
||||
myValueSetProvider.myNextReturnParams.addParameter("result", theResult);
|
||||
myValueSetProvider.myNextReturnParams.addParameter("display", theDisplay);
|
||||
if (theMessage != null) {
|
||||
myValueSetProvider.myNextReturnParams.addParameter("message", theMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private static class MyCodeSystemProvider implements IResourceProvider {
|
||||
|
||||
private UriParam myLastUrlParam;
|
||||
private List<CodeSystem> myNextReturnCodeSystems;
|
||||
private int myInvocationCount;
|
||||
private UriType myLastUrl;
|
||||
private CodeType myLastCode;
|
||||
private StringType myLastDisplay;
|
||||
private Parameters myNextReturnParams;
|
||||
|
||||
@Operation(name = "validate-code", idempotent = true, returnParameters = {
|
||||
@OperationParam(name = "result", type = BooleanType.class, min = 1),
|
||||
@OperationParam(name = "message", type = StringType.class),
|
||||
@OperationParam(name = "display", type = StringType.class)
|
||||
})
|
||||
public Parameters validateCode(
|
||||
HttpServletRequest theServletRequest,
|
||||
@IdParam(optional = true) IdType theId,
|
||||
@OperationParam(name = "url", min = 0, max = 1) UriType theCodeSystemUrl,
|
||||
@OperationParam(name = "code", min = 0, max = 1) CodeType theCode,
|
||||
@OperationParam(name = "display", min = 0, max = 1) StringType theDisplay
|
||||
) {
|
||||
myInvocationCount++;
|
||||
myLastUrl = theCodeSystemUrl;
|
||||
myLastCode = theCode;
|
||||
myLastDisplay = theDisplay;
|
||||
return myNextReturnParams;
|
||||
}
|
||||
|
||||
@Search
|
||||
public List<CodeSystem> find(@RequiredParam(name = "url") UriParam theUrlParam) {
|
||||
myLastUrlParam = theUrlParam;
|
||||
assert myNextReturnCodeSystems != null;
|
||||
return myNextReturnCodeSystems;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends IBaseResource> getResourceType() {
|
||||
return CodeSystem.class;
|
||||
}
|
||||
}
|
||||
|
||||
private static class MyValueSetProvider implements IResourceProvider {
|
||||
private Parameters myNextReturnParams;
|
||||
private List<ValueSet> myNextReturnValueSets;
|
||||
private UriType myLastUrl;
|
||||
private CodeType myLastCode;
|
||||
private int myInvocationCount;
|
||||
private UriType myLastSystem;
|
||||
private StringType myLastDisplay;
|
||||
private ValueSet myLastValueSet;
|
||||
private UriParam myLastUrlParam;
|
||||
|
||||
@Operation(name = "validate-code", idempotent = true, returnParameters = {
|
||||
@OperationParam(name = "result", type = BooleanType.class, min = 1),
|
||||
@OperationParam(name = "message", type = StringType.class),
|
||||
@OperationParam(name = "display", type = StringType.class)
|
||||
})
|
||||
public Parameters validateCode(
|
||||
HttpServletRequest theServletRequest,
|
||||
@IdParam(optional = true) IdType theId,
|
||||
@OperationParam(name = "url", min = 0, max = 1) UriType theValueSetUrl,
|
||||
@OperationParam(name = "code", min = 0, max = 1) CodeType theCode,
|
||||
@OperationParam(name = "system", min = 0, max = 1) UriType theSystem,
|
||||
@OperationParam(name = "display", min = 0, max = 1) StringType theDisplay,
|
||||
@OperationParam(name = "valueSet") ValueSet theValueSet
|
||||
) {
|
||||
myInvocationCount++;
|
||||
myLastUrl = theValueSetUrl;
|
||||
myLastCode = theCode;
|
||||
myLastSystem = theSystem;
|
||||
myLastDisplay = theDisplay;
|
||||
myLastValueSet = theValueSet;
|
||||
return myNextReturnParams;
|
||||
}
|
||||
|
||||
@Search
|
||||
public List<ValueSet> find(@RequiredParam(name = "url") UriParam theUrlParam) {
|
||||
myLastUrlParam = theUrlParam;
|
||||
assert myNextReturnValueSets != null;
|
||||
return myNextReturnValueSets;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends IBaseResource> getResourceType() {
|
||||
return ValueSet.class;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package ca.uhn.fhir.jpa.reindex.job;
|
||||
|
||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.search.reindex.ResourceReindexer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class ReindexWriterTest {
|
||||
|
||||
@Mock
|
||||
private DaoConfig myDaoConfig;
|
||||
@Mock
|
||||
private PlatformTransactionManager myPlatformTransactionManager;
|
||||
@Mock
|
||||
ResourceReindexer myResourceReindexer;
|
||||
|
||||
@InjectMocks
|
||||
private ReindexWriter myReindexWriter;
|
||||
|
||||
@Test
|
||||
public void testReindexSplitsPidList() throws Exception {
|
||||
when(myDaoConfig.getReindexBatchSize()).thenReturn(5);
|
||||
when(myDaoConfig.getReindexThreadCount()).thenReturn(4);
|
||||
|
||||
List<Long> pidList = new ArrayList<>();
|
||||
int count = 20;
|
||||
for (long i = 0; i < count; ++i) {
|
||||
pidList.add(i);
|
||||
}
|
||||
List<List<Long>> pidListList = new ArrayList<>();
|
||||
pidListList.add(pidList);
|
||||
myReindexWriter.write(pidListList);
|
||||
|
||||
verify(myResourceReindexer, times(count)).readAndReindexResourceByPid(anyLong());
|
||||
verifyNoMoreInteractions(myResourceReindexer);
|
||||
}
|
||||
}
|
|
@ -67,7 +67,7 @@ public class LastNElasticsearchSvcMultipleObservationsIT {
|
|||
public void before() throws IOException {
|
||||
PartitionSettings partitionSettings = new PartitionSettings();
|
||||
partitionSettings.setPartitioningEnabled(false);
|
||||
elasticsearchSvc = new ElasticsearchSvcImpl(partitionSettings, elasticsearchContainer.getHost() + ":" + elasticsearchContainer.getMappedPort(9200), null, null);
|
||||
elasticsearchSvc = new ElasticsearchSvcImpl(partitionSettings, "http", elasticsearchContainer.getHost() + ":" + elasticsearchContainer.getMappedPort(9200), null, null);
|
||||
|
||||
if (!indexLoaded) {
|
||||
createMultiplePatientsAndObservations();
|
||||
|
|
|
@ -90,7 +90,7 @@ public class LastNElasticsearchSvcSingleObservationIT {
|
|||
public void before() {
|
||||
PartitionSettings partitionSettings = new PartitionSettings();
|
||||
partitionSettings.setPartitioningEnabled(false);
|
||||
elasticsearchSvc = new ElasticsearchSvcImpl(partitionSettings, elasticsearchContainer.getHost() + ":" + elasticsearchContainer.getMappedPort(9200), "", "");
|
||||
elasticsearchSvc = new ElasticsearchSvcImpl(partitionSettings, "http", elasticsearchContainer.getHost() + ":" + elasticsearchContainer.getMappedPort(9200), "", "");
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
package ca.uhn.fhir.jpa.mdm.broker;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server - Master Data Management
|
||||
* %%
|
||||
* 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.subscription.api.ISubscriptionMessageKeySvc;
|
||||
import ca.uhn.fhir.mdm.model.CanonicalEID;
|
||||
import ca.uhn.fhir.mdm.util.EIDHelper;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class MdmMessageKeySvc implements ISubscriptionMessageKeySvc {
|
||||
@Autowired
|
||||
private EIDHelper myEIDHelper;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getMessageKeyOrNull(IBaseResource theTargetResource) {
|
||||
List<CanonicalEID> eidList = myEIDHelper.getExternalEid(theTargetResource);
|
||||
if (eidList.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return eidList.get(0).getValue();
|
||||
}
|
||||
}
|
|
@ -26,6 +26,7 @@ import ca.uhn.fhir.jpa.batch.mdm.MdmBatchJobSubmitterFactoryImpl;
|
|||
import ca.uhn.fhir.jpa.dao.mdm.MdmLinkDeleteSvc;
|
||||
import ca.uhn.fhir.jpa.interceptor.MdmSearchExpandingInterceptor;
|
||||
import ca.uhn.fhir.jpa.mdm.broker.MdmMessageHandler;
|
||||
import ca.uhn.fhir.jpa.mdm.broker.MdmMessageKeySvc;
|
||||
import ca.uhn.fhir.jpa.mdm.broker.MdmQueueConsumerLoader;
|
||||
import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc;
|
||||
import ca.uhn.fhir.jpa.mdm.dao.MdmLinkFactory;
|
||||
|
@ -104,6 +105,10 @@ public class MdmConsumerConfig {
|
|||
return new MdmMessageHandler();
|
||||
}
|
||||
|
||||
@Bean
|
||||
MdmMessageKeySvc mdmMessageKeySvc() {
|
||||
return new MdmMessageKeySvc();
|
||||
}
|
||||
@Bean
|
||||
MdmMatchLinkSvc mdmMatchLinkSvc() {
|
||||
return new MdmMatchLinkSvc();
|
||||
|
|
|
@ -77,7 +77,7 @@ public class MdmResourceDaoSvc {
|
|||
|
||||
//TODO GGG MDM address this
|
||||
public Optional<IAnyResource> searchGoldenResourceByEID(String theEid, String theResourceType) {
|
||||
SearchParameterMap map = buildEidSearchParameterMap(theEid);
|
||||
SearchParameterMap map = buildEidSearchParameterMap(theEid, theResourceType);
|
||||
|
||||
IFhirResourceDao resourceDao = myDaoRegistry.getResourceDao(theResourceType);
|
||||
IBundleProvider search = resourceDao.search(map);
|
||||
|
@ -101,10 +101,10 @@ public class MdmResourceDaoSvc {
|
|||
}
|
||||
|
||||
@Nonnull
|
||||
private SearchParameterMap buildEidSearchParameterMap(String theTheEid) {
|
||||
private SearchParameterMap buildEidSearchParameterMap(String theEid, String theResourceType) {
|
||||
SearchParameterMap map = new SearchParameterMap();
|
||||
map.setLoadSynchronous(true);
|
||||
map.add("identifier", new TokenParam(myMdmSettings.getMdmRules().getEnterpriseEIDSystem(), theTheEid));
|
||||
map.add("identifier", new TokenParam(myMdmSettings.getMdmRules().getEnterpriseEIDSystemForResourceType(theResourceType), theEid));
|
||||
map.add("_tag", new TokenParam(MdmConstants.SYSTEM_GOLDEN_RECORD_STATUS, MdmConstants.CODE_GOLDEN_RECORD));
|
||||
return map;
|
||||
}
|
||||
|
|
|
@ -327,12 +327,12 @@ abstract public class BaseMdmR4Test extends BaseJpaR4Test {
|
|||
}
|
||||
|
||||
protected Patient addExternalEID(Patient thePatient, String theEID) {
|
||||
thePatient.addIdentifier().setSystem(myMdmSettings.getMdmRules().getEnterpriseEIDSystem()).setValue(theEID);
|
||||
thePatient.addIdentifier().setSystem(myMdmSettings.getMdmRules().getEnterpriseEIDSystemForResourceType("Patient")).setValue(theEID);
|
||||
return thePatient;
|
||||
}
|
||||
|
||||
protected Patient clearExternalEIDs(Patient thePatient) {
|
||||
thePatient.getIdentifier().removeIf(theIdentifier -> theIdentifier.getSystem().equalsIgnoreCase(myMdmSettings.getMdmRules().getEnterpriseEIDSystem()));
|
||||
thePatient.getIdentifier().removeIf(theIdentifier -> theIdentifier.getSystem().equalsIgnoreCase(myMdmSettings.getMdmRules().getEnterpriseEIDSystemForResourceType("Patient")));
|
||||
return thePatient;
|
||||
}
|
||||
|
||||
|
|
|
@ -71,11 +71,11 @@ public class MdmMatchLinkSvcMultipleEidModeTest extends BaseMdmR4Test {
|
|||
|
||||
//The collision should have added a new identifier with the external system.
|
||||
Identifier secondIdentifier = identifier.get(1);
|
||||
assertThat(secondIdentifier.getSystem(), is(equalTo(myMdmSettings.getMdmRules().getEnterpriseEIDSystem())));
|
||||
assertThat(secondIdentifier.getSystem(), is(equalTo(myMdmSettings.getMdmRules().getEnterpriseEIDSystemForResourceType("Patient"))));
|
||||
assertThat(secondIdentifier.getValue(), is(equalTo("12345")));
|
||||
|
||||
Identifier thirdIdentifier = identifier.get(2);
|
||||
assertThat(thirdIdentifier.getSystem(), is(equalTo(myMdmSettings.getMdmRules().getEnterpriseEIDSystem())));
|
||||
assertThat(thirdIdentifier.getSystem(), is(equalTo(myMdmSettings.getMdmRules().getEnterpriseEIDSystemForResourceType("Patient"))));
|
||||
assertThat(thirdIdentifier.getValue(), is(equalTo("67890")));
|
||||
}
|
||||
|
||||
|
|
|
@ -157,7 +157,7 @@ public class MdmMatchLinkSvcTest extends BaseMdmR4Test {
|
|||
Patient patient = getTargetResourceFromMdmLink(mdmLink.get(), "Patient");
|
||||
List<CanonicalEID> externalEid = myEidHelper.getExternalEid(patient);
|
||||
|
||||
assertThat(externalEid.get(0).getSystem(), is(equalTo(myMdmSettings.getMdmRules().getEnterpriseEIDSystem())));
|
||||
assertThat(externalEid.get(0).getSystem(), is(equalTo(myMdmSettings.getMdmRules().getEnterpriseEIDSystemForResourceType("Patient"))));
|
||||
assertThat(externalEid.get(0).getValue(), is(equalTo(sampleEID)));
|
||||
}
|
||||
|
||||
|
@ -222,7 +222,7 @@ public class MdmMatchLinkSvcTest extends BaseMdmR4Test {
|
|||
|
||||
//The collision should have added a new identifier with the external system.
|
||||
Identifier secondIdentifier = identifier.get(1);
|
||||
assertThat(secondIdentifier.getSystem(), is(equalTo(myMdmSettings.getMdmRules().getEnterpriseEIDSystem())));
|
||||
assertThat(secondIdentifier.getSystem(), is(equalTo(myMdmSettings.getMdmRules().getEnterpriseEIDSystemForResourceType("Patient"))));
|
||||
assertThat(secondIdentifier.getValue(), is(equalTo("12345")));
|
||||
}
|
||||
|
||||
|
|
|
@ -34,7 +34,6 @@ import javax.persistence.TemporalType;
|
|||
import javax.persistence.Transient;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.defaultString;
|
||||
|
||||
|
@ -113,12 +112,16 @@ public abstract class BaseHasResource extends BasePartitionable implements IBase
|
|||
@Override
|
||||
public InstantDt getPublished() {
|
||||
if (myPublished != null) {
|
||||
return new InstantDt(cloneDate(myPublished));
|
||||
return new InstantDt(getPublishedDate());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Date getPublishedDate() {
|
||||
return cloneDate(myPublished);
|
||||
}
|
||||
|
||||
public void setPublished(Date thePublished) {
|
||||
myPublished = thePublished;
|
||||
}
|
||||
|
@ -137,7 +140,12 @@ public abstract class BaseHasResource extends BasePartitionable implements IBase
|
|||
|
||||
@Override
|
||||
public InstantDt getUpdated() {
|
||||
return new InstantDt(cloneDate(myUpdated));
|
||||
return new InstantDt(getUpdatedDate());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date getUpdatedDate() {
|
||||
return cloneDate(myUpdated);
|
||||
}
|
||||
|
||||
public void setUpdated(Date theUpdated) {
|
||||
|
@ -148,11 +156,6 @@ public abstract class BaseHasResource extends BasePartitionable implements IBase
|
|||
myUpdated = theUpdated.getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date getUpdatedDate() {
|
||||
return cloneDate(myUpdated);
|
||||
}
|
||||
|
||||
@Override
|
||||
public abstract long getVersion();
|
||||
|
||||
|
|
|
@ -28,8 +28,6 @@ import org.hl7.fhir.dstu2.model.Subscription;
|
|||
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||
import org.hl7.fhir.r4.model.DateTimeType;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
|
@ -102,6 +100,7 @@ public class ModelConfig {
|
|||
private boolean myIndexOnContainedResources = false;
|
||||
private boolean myIndexOnContainedResourcesRecursively = false;
|
||||
private boolean myAllowMdmExpansion = false;
|
||||
private boolean myAutoSupportDefaultSearchParams = true;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
|
@ -162,36 +161,6 @@ 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.
|
||||
|
@ -210,6 +179,38 @@ public class ModelConfig {
|
|||
this.myAllowContainsSearches = 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 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 set to <code>true</code> (default is <code>false</code>) the server will allow
|
||||
* resources to have references to external servers. For example if this server is
|
||||
|
@ -385,7 +386,6 @@ public class ModelConfig {
|
|||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* This setting indicates which subscription channel types are supported by the server. Any subscriptions submitted
|
||||
* to the server matching these types will be activated.
|
||||
|
@ -769,13 +769,13 @@ public class ModelConfig {
|
|||
/**
|
||||
* Should indexing and searching on contained resources be enabled on this server.
|
||||
* This may have performance impacts, and should be enabled only if it is needed. Default is <code>false</code>.
|
||||
*
|
||||
*
|
||||
* @since 5.4.0
|
||||
*/
|
||||
public boolean isIndexOnContainedResources() {
|
||||
return myIndexOnContainedResources;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Should indexing and searching on contained resources be enabled on this server.
|
||||
* This may have performance impacts, and should be enabled only if it is needed. Default is <code>false</code>.
|
||||
|
@ -785,7 +785,7 @@ public class ModelConfig {
|
|||
public void setIndexOnContainedResources(boolean theIndexOnContainedResources) {
|
||||
myIndexOnContainedResources = theIndexOnContainedResources;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Should recursive indexing and searching on contained resources be enabled on this server.
|
||||
* This may have performance impacts, and should be enabled only if it is needed. Default is <code>false</code>.
|
||||
|
@ -806,6 +806,38 @@ public class ModelConfig {
|
|||
myIndexOnContainedResourcesRecursively = theIndexOnContainedResourcesRecursively;
|
||||
}
|
||||
|
||||
/**
|
||||
* If this is disabled by setting this to {@literal false} (default is {@literal true}),
|
||||
* the server will not automatically implement and support search parameters that
|
||||
* are not explcitly created in the repository.
|
||||
* <p>
|
||||
* Disabling this can have a dramatic improvement on performance (especially write performance)
|
||||
* in servers that only need to support a small number of search parameters, or no search parameters at all.
|
||||
* Disabling this obviously reduces the options for searching however.
|
||||
* </p>
|
||||
*
|
||||
* @since 6.0.0
|
||||
*/
|
||||
public boolean isAutoSupportDefaultSearchParams() {
|
||||
return myAutoSupportDefaultSearchParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* If this is disabled by setting this to {@literal false} (default is {@literal true}),
|
||||
* the server will not automatically implement and support search parameters that
|
||||
* are not explcitly created in the repository.
|
||||
* <p>
|
||||
* Disabling this can have a dramatic improvement on performance (especially write performance)
|
||||
* in servers that only need to support a small number of search parameters, or no search parameters at all.
|
||||
* Disabling this obviously reduces the options for searching however.
|
||||
* </p>
|
||||
*
|
||||
* @since 6.0.0
|
||||
*/
|
||||
public void setAutoSupportDefaultSearchParams(boolean theAutoSupportDefaultSearchParams) {
|
||||
myAutoSupportDefaultSearchParams = theAutoSupportDefaultSearchParams;
|
||||
}
|
||||
|
||||
private static void validateTreatBaseUrlsAsLocal(String theUrl) {
|
||||
Validate.notBlank(theUrl, "Base URL must not be null or empty");
|
||||
|
||||
|
|
|
@ -638,8 +638,8 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas
|
|||
retVal.setVersion(myVersion);
|
||||
retVal.setTransientForcedId(getTransientForcedId());
|
||||
|
||||
retVal.setPublished(getPublished());
|
||||
retVal.setUpdated(getUpdated());
|
||||
retVal.setPublished(getPublishedDate());
|
||||
retVal.setUpdated(getUpdatedDate());
|
||||
retVal.setFhirVersion(getFhirVersion());
|
||||
retVal.setDeleted(getDeleted());
|
||||
retVal.setResourceTable(this);
|
||||
|
|
|
@ -991,7 +991,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
myModelConfig.isIndexOnContainedResources()
|
||||
|| anySearchParameterUsesResolve(searchParams, theSearchParamType);
|
||||
|
||||
if (havePathWithResolveExpression) {
|
||||
if (havePathWithResolveExpression && myContext.getParserOptions().isAutoContainReferenceTargetsWithNoId()) {
|
||||
//TODO GGG/JA: At this point, if the Task.basedOn.reference.resource does _not_ have an ID, we will attempt to contain it internally. Wild
|
||||
myContext.newTerser().containResources(theResource, FhirTerser.OptionsEnum.MODIFY_RESOURCE, FhirTerser.OptionsEnum.STORE_AND_REUSE_RESULTS);
|
||||
}
|
||||
|
|
|
@ -24,6 +24,8 @@ import ca.uhn.fhir.context.FhirContext;
|
|||
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.hl7.fhir.exceptions.FHIRException;
|
||||
import org.hl7.fhir.exceptions.PathEngineException;
|
||||
|
@ -32,6 +34,7 @@ import org.hl7.fhir.instance.model.api.IBaseResource;
|
|||
import org.hl7.fhir.r4.context.IWorkerContext;
|
||||
import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext;
|
||||
import org.hl7.fhir.r4.model.Base;
|
||||
import org.hl7.fhir.r4.model.ExpressionNode;
|
||||
import org.hl7.fhir.r4.model.IdType;
|
||||
import org.hl7.fhir.r4.model.Resource;
|
||||
import org.hl7.fhir.r4.model.ResourceType;
|
||||
|
@ -45,11 +48,13 @@ import java.util.Collections;
|
|||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
|
||||
public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements ISearchParamExtractor {
|
||||
|
||||
private Cache<String, ExpressionNode> myParsedFhirPathCache;
|
||||
private FHIRPathEngine myFhirPathEngine;
|
||||
|
||||
/**
|
||||
|
@ -70,8 +75,8 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
|
|||
@Override
|
||||
public IValueExtractor getPathValueExtractor(IBaseResource theResource, String theSinglePath) {
|
||||
return () -> {
|
||||
List<Base> allValues = myFhirPathEngine.evaluate((Base) theResource, theSinglePath);
|
||||
return (List<IBase>) new ArrayList<IBase>(allValues);
|
||||
ExpressionNode parsed = myParsedFhirPathCache.get(theSinglePath, path -> myFhirPathEngine.parse(path));
|
||||
return myFhirPathEngine.evaluate((Base) theResource, parsed);
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -89,6 +94,11 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
|
|||
IWorkerContext worker = new HapiWorkerContext(getContext(), getContext().getValidationSupport());
|
||||
myFhirPathEngine = new FHIRPathEngine(worker);
|
||||
myFhirPathEngine.setHostServices(new SearchParamExtractorR4HostServices());
|
||||
|
||||
myParsedFhirPathCache = Caffeine
|
||||
.newBuilder()
|
||||
.expireAfterWrite(10, TimeUnit.MINUTES)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -24,12 +24,16 @@ import ca.uhn.fhir.context.FhirContext;
|
|||
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import org.hl7.fhir.exceptions.FHIRException;
|
||||
import org.hl7.fhir.exceptions.PathEngineException;
|
||||
import org.hl7.fhir.instance.model.api.IBase;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.r5.context.IWorkerContext;
|
||||
import org.hl7.fhir.r5.hapi.ctx.HapiWorkerContext;
|
||||
import org.hl7.fhir.r5.model.Base;
|
||||
import org.hl7.fhir.r5.model.ExpressionNode;
|
||||
import org.hl7.fhir.r5.model.IdType;
|
||||
import org.hl7.fhir.r5.model.Resource;
|
||||
import org.hl7.fhir.r5.model.ResourceType;
|
||||
|
@ -38,16 +42,19 @@ import org.hl7.fhir.r5.model.ValueSet;
|
|||
import org.hl7.fhir.r5.utils.FHIRPathEngine;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
|
||||
public class SearchParamExtractorR5 extends BaseSearchParamExtractor implements ISearchParamExtractor {
|
||||
|
||||
private FHIRPathEngine myFhirPathEngine;
|
||||
private Cache<String, ExpressionNode> myParsedFhirPathCache;
|
||||
|
||||
public SearchParamExtractorR5() {
|
||||
super();
|
||||
|
@ -75,11 +82,19 @@ public class SearchParamExtractorR5 extends BaseSearchParamExtractor implements
|
|||
IWorkerContext worker = new HapiWorkerContext(getContext(), getContext().getValidationSupport());
|
||||
myFhirPathEngine = new FHIRPathEngine(worker);
|
||||
myFhirPathEngine.setHostServices(new SearchParamExtractorR5HostServices());
|
||||
|
||||
myParsedFhirPathCache = Caffeine
|
||||
.newBuilder()
|
||||
.expireAfterWrite(10, TimeUnit.MINUTES)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IValueExtractor getPathValueExtractor(IBaseResource theResource, String nextPath) {
|
||||
return () -> myFhirPathEngine.evaluate((Base) theResource, nextPath);
|
||||
public IValueExtractor getPathValueExtractor(IBaseResource theResource, String theSinglePath) {
|
||||
return () -> {
|
||||
ExpressionNode parsed = myParsedFhirPathCache.get(theSinglePath, path -> myFhirPathEngine.parse(path));
|
||||
return myFhirPathEngine.evaluate((Base) theResource, parsed);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -26,11 +26,12 @@ import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
|||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||
import ca.uhn.fhir.util.BundleUtil;
|
||||
import ca.uhn.fhir.util.ClasspathUtil;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.hl7.fhir.instance.model.api.IBaseBundle;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
|
@ -80,7 +81,11 @@ public class ReadOnlySearchParamCache {
|
|||
return myUrlToParam.get(theUrl);
|
||||
}
|
||||
|
||||
public static ReadOnlySearchParamCache fromFhirContext(FhirContext theFhirContext, SearchParameterCanonicalizer theCanonicalizer) {
|
||||
public static ReadOnlySearchParamCache fromFhirContext(@Nonnull FhirContext theFhirContext, @Nonnull SearchParameterCanonicalizer theCanonicalizer) {
|
||||
return fromFhirContext(theFhirContext, theCanonicalizer, null);
|
||||
}
|
||||
|
||||
public static ReadOnlySearchParamCache fromFhirContext(@Nonnull FhirContext theFhirContext, @Nonnull SearchParameterCanonicalizer theCanonicalizer, @Nullable Set<String> theSearchParamPatternsToInclude) {
|
||||
assert theCanonicalizer != null;
|
||||
|
||||
ReadOnlySearchParamCache retVal = new ReadOnlySearchParamCache();
|
||||
|
@ -97,10 +102,12 @@ public class ReadOnlySearchParamCache {
|
|||
base = resourceNames;
|
||||
}
|
||||
|
||||
for (String nextBase : base) {
|
||||
Map<String, RuntimeSearchParam> nameToParam = retVal.myResourceNameToSpNameToSp.computeIfAbsent(nextBase, t -> new HashMap<>());
|
||||
String nextName = nextCanonical.getName();
|
||||
nameToParam.putIfAbsent(nextName, nextCanonical);
|
||||
for (String nextResourceName : base) {
|
||||
Map<String, RuntimeSearchParam> nameToParam = retVal.myResourceNameToSpNameToSp.computeIfAbsent(nextResourceName, t -> new HashMap<>());
|
||||
String nextParamName = nextCanonical.getName();
|
||||
if (theSearchParamPatternsToInclude == null || searchParamMatchesAtLeastOnePattern(theSearchParamPatternsToInclude, nextResourceName, nextParamName)) {
|
||||
nameToParam.putIfAbsent(nextParamName, nextCanonical);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -110,15 +117,42 @@ public class ReadOnlySearchParamCache {
|
|||
RuntimeResourceDefinition nextResDef = theFhirContext.getResourceDefinition(resourceName);
|
||||
String nextResourceName = nextResDef.getName();
|
||||
|
||||
Map<String, RuntimeSearchParam> nameToParam = retVal.myResourceNameToSpNameToSp.computeIfAbsent(nextResourceName, t-> new HashMap<>());
|
||||
Map<String, RuntimeSearchParam> nameToParam = retVal.myResourceNameToSpNameToSp.computeIfAbsent(nextResourceName, t -> new HashMap<>());
|
||||
for (RuntimeSearchParam nextSp : nextResDef.getSearchParams()) {
|
||||
nameToParam.putIfAbsent(nextSp.getName(), nextSp);
|
||||
String nextParamName = nextSp.getName();
|
||||
if (theSearchParamPatternsToInclude == null || searchParamMatchesAtLeastOnePattern(theSearchParamPatternsToInclude, nextResourceName, nextParamName)) {
|
||||
nameToParam.putIfAbsent(nextParamName, nextSp);
|
||||
}
|
||||
}
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
public static boolean searchParamMatchesAtLeastOnePattern(Set<String> theSearchParamPatterns, String theResourceType, String theSearchParamName) {
|
||||
for (String nextPattern : theSearchParamPatterns) {
|
||||
if ("*".equals(nextPattern)) {
|
||||
return true;
|
||||
}
|
||||
int colonIdx = nextPattern.indexOf(':');
|
||||
Validate.isTrue(colonIdx > 0, "Invalid search param pattern: %s", nextPattern);
|
||||
String resourceType = nextPattern.substring(0, colonIdx);
|
||||
String searchParamName = nextPattern.substring(colonIdx + 1);
|
||||
Validate.notBlank(resourceType, "No resource type specified in pattern: %s", nextPattern);
|
||||
Validate.notBlank(searchParamName, "No param name specified in pattern: %s", nextPattern);
|
||||
if (!resourceType.equals("*") && !resourceType.equals(theResourceType)) {
|
||||
continue;
|
||||
}
|
||||
if (!searchParamName.equals("*") && !searchParamName.equals(theSearchParamName)) {
|
||||
continue;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static ReadOnlySearchParamCache fromRuntimeSearchParamCache(RuntimeSearchParamCache theRuntimeSearchParamCache) {
|
||||
return new ReadOnlySearchParamCache(theRuntimeSearchParamCache);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
|
|||
import ca.uhn.fhir.util.SearchParameterUtil;
|
||||
import ca.uhn.fhir.util.StopWatch;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.Sets;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.time.DateUtils;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
@ -59,7 +60,12 @@ import java.util.Set;
|
|||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||
|
||||
public class SearchParamRegistryImpl implements ISearchParamRegistry, IResourceChangeListener, ISearchParamRegistryController {
|
||||
// TODO: JA remove unused?
|
||||
|
||||
public static final Set<String> NON_DISABLEABLE_SEARCH_PARAMS = Collections.unmodifiableSet(Sets.newHashSet(
|
||||
"*:url",
|
||||
"Subscription:*",
|
||||
"SearchParameter:*"
|
||||
));
|
||||
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(SearchParamRegistryImpl.class);
|
||||
private static final int MAX_MANAGED_PARAM_COUNT = 10000;
|
||||
|
@ -78,7 +84,6 @@ public class SearchParamRegistryImpl implements ISearchParamRegistry, IResourceC
|
|||
private volatile ReadOnlySearchParamCache myBuiltInSearchParams;
|
||||
private volatile IPhoneticEncoder myPhoneticEncoder;
|
||||
private volatile RuntimeSearchParamCache myActiveSearchParams;
|
||||
|
||||
@Autowired
|
||||
private IInterceptorService myInterceptorBroadcaster;
|
||||
private IResourceChangeListenerCache myResourceChangeListenerCache;
|
||||
|
@ -134,6 +139,8 @@ public class SearchParamRegistryImpl implements ISearchParamRegistry, IResourceC
|
|||
params.setLoadSynchronousUpTo(MAX_MANAGED_PARAM_COUNT);
|
||||
|
||||
IBundleProvider allSearchParamsBp = mySearchParamProvider.search(params);
|
||||
|
||||
List<IBaseResource> allSearchParams = allSearchParamsBp.getResources(0, MAX_MANAGED_PARAM_COUNT);
|
||||
int size = allSearchParamsBp.sizeOrThrowNpe();
|
||||
|
||||
ourLog.trace("Loaded {} search params from the DB", size);
|
||||
|
@ -141,9 +148,8 @@ public class SearchParamRegistryImpl implements ISearchParamRegistry, IResourceC
|
|||
// Just in case..
|
||||
if (size >= MAX_MANAGED_PARAM_COUNT) {
|
||||
ourLog.warn("Unable to support >" + MAX_MANAGED_PARAM_COUNT + " search params!");
|
||||
size = MAX_MANAGED_PARAM_COUNT;
|
||||
}
|
||||
List<IBaseResource> allSearchParams = allSearchParamsBp.getResources(0, size);
|
||||
|
||||
initializeActiveSearchParams(allSearchParams);
|
||||
}
|
||||
|
||||
|
@ -167,7 +173,12 @@ public class SearchParamRegistryImpl implements ISearchParamRegistry, IResourceC
|
|||
|
||||
private ReadOnlySearchParamCache getBuiltInSearchParams() {
|
||||
if (myBuiltInSearchParams == null) {
|
||||
myBuiltInSearchParams = ReadOnlySearchParamCache.fromFhirContext(myFhirContext, mySearchParameterCanonicalizer);
|
||||
if (myModelConfig.isAutoSupportDefaultSearchParams()) {
|
||||
myBuiltInSearchParams = ReadOnlySearchParamCache.fromFhirContext(myFhirContext, mySearchParameterCanonicalizer);
|
||||
} else {
|
||||
// Only the built-in search params that can not be disabled will be supported automatically
|
||||
myBuiltInSearchParams = ReadOnlySearchParamCache.fromFhirContext(myFhirContext, mySearchParameterCanonicalizer, NON_DISABLEABLE_SEARCH_PARAMS);
|
||||
}
|
||||
}
|
||||
return myBuiltInSearchParams;
|
||||
}
|
||||
|
@ -311,6 +322,7 @@ public class SearchParamRegistryImpl implements ISearchParamRegistry, IResourceC
|
|||
|
||||
@VisibleForTesting
|
||||
public void resetForUnitTest() {
|
||||
myBuiltInSearchParams = null;
|
||||
handleInit(Collections.emptyList());
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
package ca.uhn.fhir.jpa.searchparam.registry;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static ca.uhn.fhir.jpa.searchparam.registry.ReadOnlySearchParamCache.searchParamMatchesAtLeastOnePattern;
|
||||
import static com.google.common.collect.Sets.newHashSet;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class ReadOnlySearchParamCacheTest {
|
||||
|
||||
@Test
|
||||
void testSearchParamMatchesAtLeastOnePattern() {
|
||||
assertTrue(searchParamMatchesAtLeastOnePattern(newHashSet("*"), "Patient", "name"));
|
||||
assertTrue(searchParamMatchesAtLeastOnePattern(newHashSet("Patient:name"), "Patient", "name"));
|
||||
assertTrue(searchParamMatchesAtLeastOnePattern(newHashSet("Patient:*"), "Patient", "name"));
|
||||
assertTrue(searchParamMatchesAtLeastOnePattern(newHashSet("*:name"), "Patient", "name"));
|
||||
assertFalse(searchParamMatchesAtLeastOnePattern(newHashSet("Patient:foo"), "Patient", "name"));
|
||||
assertFalse(searchParamMatchesAtLeastOnePattern(newHashSet("Foo:name"), "Patient", "name"));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testSearchParamMatchesAtLeastOnePattern_InvalidPattern() {
|
||||
assertThrows(IllegalArgumentException.class, () -> searchParamMatchesAtLeastOnePattern(newHashSet("aaa"), "Patient", "name"));
|
||||
assertThrows(IllegalArgumentException.class, () -> searchParamMatchesAtLeastOnePattern(newHashSet(":name"), "Patient", "name"));
|
||||
assertThrows(IllegalArgumentException.class, () -> searchParamMatchesAtLeastOnePattern(newHashSet("Patient:"), "Patient", "name"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package ca.uhn.fhir.jpa.subscription.api;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR Subscription 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 org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* This is used by "message" type subscriptions to provide a key to the message wrapper before submitting it to the channel
|
||||
*/
|
||||
public interface ISubscriptionMessageKeySvc {
|
||||
/**
|
||||
* Given an {@link IBaseResource}, return a key that can be used to identify the message. This key will be used to
|
||||
* partition the message into a queue.
|
||||
*
|
||||
* @param thePayloadResource the payload resource.
|
||||
* @return the key or null.
|
||||
*/
|
||||
@Nullable
|
||||
String getMessageKeyOrNull(IBaseResource thePayloadResource);
|
||||
}
|
|
@ -1,5 +1,25 @@
|
|||
package ca.uhn.fhir.jpa.subscription.channel.models;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR Subscription 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.subscription.model.ChannelRetryConfiguration;
|
||||
|
||||
public class BaseChannelParameters {
|
||||
|
|
|
@ -1,5 +1,25 @@
|
|||
package ca.uhn.fhir.jpa.subscription.channel.models;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR Subscription 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%
|
||||
*/
|
||||
|
||||
public class ProducingChannelParameters extends BaseChannelParameters {
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,5 +1,25 @@
|
|||
package ca.uhn.fhir.jpa.subscription.channel.models;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR Subscription 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%
|
||||
*/
|
||||
|
||||
public class ReceivingChannelParameters extends BaseChannelParameters {
|
||||
|
||||
/**
|
||||
|
|
|
@ -64,6 +64,7 @@ public class SubscriptionDeliveringMessageSubscriber extends BaseSubscriptionDel
|
|||
|
||||
protected void doDelivery(ResourceDeliveryMessage theMsg, CanonicalSubscription theSubscription, IChannelProducer theChannelProducer, IBaseResource thePayloadResource) {
|
||||
ResourceModifiedMessage payload = new ResourceModifiedMessage(myFhirContext, thePayloadResource, theMsg.getOperationType());
|
||||
payload.setMessageKey(theMsg.getMessageKeyOrNull());
|
||||
payload.setTransactionId(theMsg.getTransactionId());
|
||||
ResourceModifiedJsonMessage message = new ResourceModifiedJsonMessage(payload);
|
||||
theChannelProducer.send(message);
|
||||
|
|
|
@ -44,7 +44,6 @@ import ca.uhn.fhir.rest.gclient.IClientExecutable;
|
|||
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
||||
import ca.uhn.fhir.util.BundleBuilder;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.text.StringSubstitutor;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
|
@ -57,10 +56,10 @@ import org.springframework.messaging.MessagingException;
|
|||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
|
||||
|
@ -251,21 +250,10 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe
|
|||
*/
|
||||
protected void sendNotification(ResourceDeliveryMessage theMsg) {
|
||||
Map<String, List<String>> params = new HashMap<>();
|
||||
List<Header> headers = new ArrayList<>();
|
||||
if (theMsg.getSubscription().getHeaders() != null) {
|
||||
theMsg.getSubscription().getHeaders().stream().filter(Objects::nonNull).forEach(h -> {
|
||||
final int sep = h.indexOf(':');
|
||||
if (sep > 0) {
|
||||
final String name = h.substring(0, sep);
|
||||
final String value = h.substring(sep + 1);
|
||||
if (StringUtils.isNotBlank(name)) {
|
||||
headers.add(new Header(name.trim(), value.trim()));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
CanonicalSubscription subscription = theMsg.getSubscription();
|
||||
List<Header> headers = parseHeadersFromSubscription(subscription);
|
||||
|
||||
StringBuilder url = new StringBuilder(theMsg.getSubscription().getEndpointUrl());
|
||||
StringBuilder url = new StringBuilder(subscription.getEndpointUrl());
|
||||
IHttpClient client = myFhirContext.getRestfulClientFactory().getHttpClient(url, params, "", RequestTypeEnum.POST, headers);
|
||||
IHttpRequest request = client.createParamRequest(myFhirContext, params, null);
|
||||
try {
|
||||
|
@ -273,8 +261,36 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe
|
|||
// close connection in order to return a possible cached connection to the connection pool
|
||||
response.close();
|
||||
} catch (IOException e) {
|
||||
ourLog.error("Error trying to reach {}: {}", theMsg.getSubscription().getEndpointUrl(), e.toString());
|
||||
ourLog.error("Error trying to reach {}: {}", subscription.getEndpointUrl(), e.toString());
|
||||
throw new ResourceNotFoundException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static List<Header> parseHeadersFromSubscription(CanonicalSubscription subscription) {
|
||||
List<Header> headers = null;
|
||||
if (subscription != null) {
|
||||
for (String h : subscription.getHeaders()) {
|
||||
if (h != null) {
|
||||
final int sep = h.indexOf(':');
|
||||
if (sep > 0) {
|
||||
final String name = h.substring(0, sep);
|
||||
final String value = h.substring(sep + 1);
|
||||
if (isNotBlank(name)) {
|
||||
if (headers == null) {
|
||||
headers = new ArrayList<>();
|
||||
}
|
||||
headers.add(new Header(name.trim(), value.trim()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (headers == null) {
|
||||
headers = Collections.emptyList();
|
||||
} else {
|
||||
headers = Collections.unmodifiableList(headers);
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -16,10 +16,13 @@ import java.util.HashMap;
|
|||
import java.util.List;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class CanonicalSubscriptionTest {
|
||||
|
||||
private static final String TAG_SYSTEM = "https://hapifhir.org/NamingSystem/managing-mdm-system";
|
||||
private static final String TAG_VALUE = "HAPI-MDM";
|
||||
@Test
|
||||
public void testGetChannelExtension() throws IOException {
|
||||
|
||||
|
@ -54,6 +57,14 @@ public class CanonicalSubscriptionTest {
|
|||
assertThat(s.getChannelExtensions("key3"), Matchers.empty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCanonicalSubscriptionRetainsMetaTags() throws IOException {
|
||||
SubscriptionCanonicalizer canonicalizer = new SubscriptionCanonicalizer(FhirContext.forR4());
|
||||
CanonicalSubscription sub1 = canonicalizer.canonicalize(makeMdmSubscription());
|
||||
assertTrue(sub1.getTags().keySet().contains(TAG_SYSTEM));
|
||||
assertEquals(sub1.getTags().get(TAG_SYSTEM), TAG_VALUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emailDetailsEquals() {
|
||||
SubscriptionCanonicalizer canonicalizer = new SubscriptionCanonicalizer(FhirContext.forR4());
|
||||
|
@ -69,6 +80,14 @@ public class CanonicalSubscriptionTest {
|
|||
retVal.setChannel(channel);
|
||||
return retVal;
|
||||
}
|
||||
private Subscription makeMdmSubscription() {
|
||||
Subscription retVal = new Subscription();
|
||||
Subscription.SubscriptionChannelComponent channel = new Subscription.SubscriptionChannelComponent();
|
||||
channel.setType(Subscription.SubscriptionChannelType.MESSAGE);
|
||||
retVal.setChannel(channel);
|
||||
retVal.getMeta().addTag("https://hapifhir.org/NamingSystem/managing-mdm-system", "HAPI-MDM", "managed by hapi mdm");
|
||||
return retVal;
|
||||
}
|
||||
|
||||
private CanonicalSubscription serializeAndDeserialize(CanonicalSubscription theSubscription) throws IOException {
|
||||
|
||||
|
|
|
@ -35,17 +35,21 @@ import ca.uhn.fhir.mdm.rules.json.MdmSimilarityJson;
|
|||
import ca.uhn.fhir.parser.DataFormatException;
|
||||
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
|
||||
import ca.uhn.fhir.util.FhirTerser;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.xml.crypto.Data;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class MdmRuleValidator implements IMdmRuleValidator {
|
||||
|
@ -73,7 +77,40 @@ public class MdmRuleValidator implements IMdmRuleValidator {
|
|||
validateMdmTypes(theMdmRules);
|
||||
validateSearchParams(theMdmRules);
|
||||
validateMatchFields(theMdmRules);
|
||||
validateSystemIsUri(theMdmRules);
|
||||
validateSystemsAreUris(theMdmRules);
|
||||
validateEidSystemsMatchMdmTypes(theMdmRules);
|
||||
}
|
||||
|
||||
private void validateEidSystemsMatchMdmTypes(MdmRulesJson theMdmRules) {
|
||||
theMdmRules.getEnterpriseEIDSystems().keySet()
|
||||
.forEach(key -> {
|
||||
//Ensure each key is either * or a valid resource type.
|
||||
if (!key.equalsIgnoreCase("*") && !theMdmRules.getMdmTypes().contains(key)) {
|
||||
throw new ConfigurationException(String.format("There is an eidSystem set for [%s] but that is not one of the mdmTypes. Valid options are [%s].", key, buildValidEidKeysMessage(theMdmRules)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String buildValidEidKeysMessage(MdmRulesJson theMdmRulesJson) {
|
||||
List<String> validTypes = new ArrayList<>(theMdmRulesJson.getMdmTypes());
|
||||
validTypes.add("*");
|
||||
return String.join(", ", validTypes);
|
||||
}
|
||||
|
||||
private void validateSystemsAreUris(MdmRulesJson theMdmRules) {
|
||||
theMdmRules.getEnterpriseEIDSystems().entrySet()
|
||||
.forEach(entry -> {
|
||||
String resourceType = entry.getKey();
|
||||
String uri = entry.getValue();
|
||||
if (!resourceType.equals("*")) {
|
||||
try {
|
||||
myFhirContext.getResourceType(resourceType);
|
||||
}catch (DataFormatException e) {
|
||||
throw new ConfigurationException(String.format("%s is not a valid resource type, but is set in the eidSystems field.", resourceType));
|
||||
}
|
||||
}
|
||||
validateIsUri(uri);
|
||||
});
|
||||
}
|
||||
|
||||
public void validateMdmTypes(MdmRulesJson theMdmRulesJson) {
|
||||
|
@ -212,15 +249,10 @@ public class MdmRuleValidator implements IMdmRuleValidator {
|
|||
validateFieldPathForType(theFieldMatch.getResourceType(), theFieldMatch);
|
||||
}
|
||||
|
||||
private void validateSystemIsUri(MdmRulesJson theMdmRulesJson) {
|
||||
if (theMdmRulesJson.getEnterpriseEIDSystem() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ourLog.info("Validating system URI {}", theMdmRulesJson.getEnterpriseEIDSystem());
|
||||
|
||||
private void validateIsUri(String theUri) {
|
||||
ourLog.info("Validating system URI {}", theUri);
|
||||
try {
|
||||
new URI(theMdmRulesJson.getEnterpriseEIDSystem());
|
||||
new URI(theUri);
|
||||
} catch (URISyntaxException e) {
|
||||
throw new ConfigurationException("Enterprise Identifier System (eidSystem) must be a valid URI");
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
|||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.util.StdConverter;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
@ -34,6 +35,8 @@ import java.util.HashMap;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static ca.uhn.fhir.mdm.api.MdmConstants.ALL_RESOURCE_SEARCH_PARAM_TYPE;
|
||||
|
||||
@JsonDeserialize(converter = MdmRulesJson.MdmRulesJsonConverter.class)
|
||||
public class MdmRulesJson implements IModelJson {
|
||||
|
||||
|
@ -47,10 +50,16 @@ public class MdmRulesJson implements IModelJson {
|
|||
List<MdmFieldMatchJson> myMatchFieldJsonList = new ArrayList<>();
|
||||
@JsonProperty(value = "matchResultMap", required = true)
|
||||
Map<String, MdmMatchResultEnum> myMatchResultMap = new HashMap<>();
|
||||
|
||||
/**
|
||||
* This field is deprecated, use eidSystems instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@JsonProperty(value = "eidSystem")
|
||||
String myEnterpriseEIDSystem;
|
||||
|
||||
|
||||
@JsonProperty(value = "eidSystems")
|
||||
Map<String, String> myEnterpriseEidSystems = new HashMap<>();
|
||||
@JsonProperty(value = "mdmTypes")
|
||||
List<String> myMdmTypes;
|
||||
|
||||
|
@ -112,14 +121,57 @@ public class MdmRulesJson implements IModelJson {
|
|||
return Collections.unmodifiableList(myCandidateFilterSearchParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use {@link this#getEnterpriseEIDSystemForResourceType(String)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public String getEnterpriseEIDSystem() {
|
||||
return myEnterpriseEIDSystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use {@link this#setEnterpriseEIDSystems(Map)} (String)} or {@link this#addEnterpriseEIDSystem(String, String)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public void setEnterpriseEIDSystem(String theEnterpriseEIDSystem) {
|
||||
myEnterpriseEIDSystem = theEnterpriseEIDSystem;
|
||||
}
|
||||
|
||||
public void setEnterpriseEIDSystems(Map<String, String> theEnterpriseEIDSystems) {
|
||||
myEnterpriseEidSystems = theEnterpriseEIDSystems;
|
||||
}
|
||||
|
||||
public void addEnterpriseEIDSystem(String theResourceType, String theEidSystem) {
|
||||
if (myEnterpriseEidSystems == null) {
|
||||
myEnterpriseEidSystems = new HashMap<>();
|
||||
}
|
||||
myEnterpriseEidSystems.put(theResourceType, theEidSystem);
|
||||
}
|
||||
|
||||
public Map<String, String> getEnterpriseEIDSystems() {
|
||||
//First try the new property.
|
||||
if (myEnterpriseEidSystems != null && !myEnterpriseEidSystems.isEmpty()) {
|
||||
return myEnterpriseEidSystems;
|
||||
//If that fails, fall back to our deprecated property.
|
||||
} else if (!StringUtils.isBlank(myEnterpriseEIDSystem)) {
|
||||
HashMap<String , String> retVal = new HashMap<>();
|
||||
retVal.put(ALL_RESOURCE_SEARCH_PARAM_TYPE, myEnterpriseEIDSystem);
|
||||
return retVal;
|
||||
//Otherwise, return an empty map.
|
||||
} else {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
}
|
||||
|
||||
public String getEnterpriseEIDSystemForResourceType(String theResourceType) {
|
||||
Map<String, String> enterpriseEIDSystems = getEnterpriseEIDSystems();
|
||||
if (enterpriseEIDSystems.containsKey(ALL_RESOURCE_SEARCH_PARAM_TYPE)) {
|
||||
return enterpriseEIDSystems.get(ALL_RESOURCE_SEARCH_PARAM_TYPE);
|
||||
} else {
|
||||
return enterpriseEIDSystems.get(theResourceType);
|
||||
}
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return myVersion;
|
||||
}
|
||||
|
@ -131,6 +183,14 @@ public class MdmRulesJson implements IModelJson {
|
|||
|
||||
private void validate() {
|
||||
Validate.notBlank(myVersion, "version may not be blank");
|
||||
|
||||
Map<String, String> enterpriseEIDSystems = getEnterpriseEIDSystems();
|
||||
|
||||
//If we have a * eid system, there should only be one.
|
||||
if (enterpriseEIDSystems.containsKey(ALL_RESOURCE_SEARCH_PARAM_TYPE)) {
|
||||
Validate.isTrue(enterpriseEIDSystems.size() == 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public String getSummary() {
|
||||
|
|
|
@ -63,7 +63,8 @@ public final class EIDHelper {
|
|||
* @return An optional {@link CanonicalEID} representing the external EID. Absent if the EID is not present.
|
||||
*/
|
||||
public List<CanonicalEID> getExternalEid(IBaseResource theResource) {
|
||||
return CanonicalEID.extractFromResource(myFhirContext, myMdmSettings.getMdmRules().getEnterpriseEIDSystem(), theResource);
|
||||
String resourceType = myFhirContext.getResourceType(theResource);
|
||||
return CanonicalEID.extractFromResource(myFhirContext, myMdmSettings.getMdmRules().getEnterpriseEIDSystemForResourceType(resourceType), theResource);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -120,7 +120,7 @@ public class GoldenResourceHelper {
|
|||
}
|
||||
|
||||
private void cloneAllExternalEidsIntoNewGoldenResource(BaseRuntimeChildDefinition theGoldenResourceIdentifier,
|
||||
IBase theGoldenResource, IBase theNewGoldenResource) {
|
||||
IAnyResource theGoldenResource, IBase theNewGoldenResource) {
|
||||
// FHIR choice types - fields within fhir where we have a choice of ids
|
||||
IFhirPath fhirPath = myFhirContext.newFhirPath();
|
||||
List<IBase> goldenResourceIdentifiers = theGoldenResourceIdentifier.getAccessor().getValues(theGoldenResource);
|
||||
|
@ -128,7 +128,8 @@ public class GoldenResourceHelper {
|
|||
for (IBase base : goldenResourceIdentifiers) {
|
||||
Optional<IPrimitiveType> system = fhirPath.evaluateFirst(base, "system", IPrimitiveType.class);
|
||||
if (system.isPresent()) {
|
||||
String mdmSystem = myMdmSettings.getMdmRules().getEnterpriseEIDSystem();
|
||||
String resourceType = myFhirContext.getResourceType(theGoldenResource);
|
||||
String mdmSystem = myMdmSettings.getMdmRules().getEnterpriseEIDSystemForResourceType(resourceType);
|
||||
String baseSystem = system.get().getValueAsString();
|
||||
if (Objects.equals(baseSystem, mdmSystem)) {
|
||||
ca.uhn.fhir.util.TerserUtil.cloneEidIntoResource(myFhirContext, theGoldenResourceIdentifier, base, theNewGoldenResource);
|
||||
|
@ -188,7 +189,7 @@ public class GoldenResourceHelper {
|
|||
return theGoldenResource;
|
||||
}
|
||||
|
||||
private void clearExternalEidsFromTheGoldenResource(BaseRuntimeChildDefinition theGoldenResourceIdentifier, IBase theGoldenResource) {
|
||||
private void clearExternalEidsFromTheGoldenResource(BaseRuntimeChildDefinition theGoldenResourceIdentifier, IBaseResource theGoldenResource) {
|
||||
IFhirPath fhirPath = myFhirContext.newFhirPath();
|
||||
List<IBase> goldenResourceIdentifiers = theGoldenResourceIdentifier.getAccessor().getValues(theGoldenResource);
|
||||
List<IBase> clonedIdentifiers = new ArrayList<>();
|
||||
|
@ -197,7 +198,8 @@ public class GoldenResourceHelper {
|
|||
for (IBase base : goldenResourceIdentifiers) {
|
||||
Optional<IPrimitiveType> system = fhirPath.evaluateFirst(base, "system", IPrimitiveType.class);
|
||||
if (system.isPresent()) {
|
||||
String mdmSystem = myMdmSettings.getMdmRules().getEnterpriseEIDSystem();
|
||||
String resourceType = myFhirContext.getResourceType(theGoldenResource);
|
||||
String mdmSystem = myMdmSettings.getMdmRules().getEnterpriseEIDSystemForResourceType(resourceType);
|
||||
String baseSystem = system.get().getValueAsString();
|
||||
if (Objects.equals(baseSystem, mdmSystem)) {
|
||||
ourLog.debug("Found EID confirming to MDM rules {}. It should not be copied, skipping", baseSystem);
|
||||
|
|
|
@ -12,6 +12,7 @@ import org.springframework.core.io.Resource;
|
|||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.startsWith;
|
||||
|
@ -161,6 +162,25 @@ public class MdmRuleValidatorTest extends BaseR4Test {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadEidResourceType() throws IOException {
|
||||
try {
|
||||
setMdmRuleJson("bad-rules-illegal-resource-type-eid.json");
|
||||
}
|
||||
catch (ConfigurationException e){
|
||||
assertThat(e.getMessage(), is(equalTo("not-a-resource is not a valid resource type, but is set in the eidSystems field.")));
|
||||
}
|
||||
}
|
||||
@Test
|
||||
public void testBadEidDoesntMatchKnownMdmTypes() throws IOException {
|
||||
try {
|
||||
setMdmRuleJson("bad-rules-illegal-missing-resource-type.json");
|
||||
}
|
||||
catch (ConfigurationException e){
|
||||
assertThat(e.getMessage(), is(equalTo("There is an eidSystem set for [Patient] but that is not one of the mdmTypes. Valid options are [Organization, *].")));
|
||||
}
|
||||
}
|
||||
|
||||
private void setMdmRuleJson(String theTheS) throws IOException {
|
||||
MdmRuleValidator mdmRuleValidator = new MdmRuleValidator(ourFhirContext, mySearchParamRetriever);
|
||||
MdmSettings mdmSettings = new MdmSettings(mdmRuleValidator);
|
||||
|
|
|
@ -11,9 +11,13 @@ import org.slf4j.Logger;
|
|||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
|
@ -72,4 +76,32 @@ public class MdmRulesJsonR4Test extends BaseMdmRulesR4Test {
|
|||
assertEquals("There is no matchField with name bad", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidResourceTypeDoesntDeserialize() throws IOException {
|
||||
myRules = buildOldStyleEidRules();
|
||||
|
||||
String eidSystem = myRules.getEnterpriseEIDSystemForResourceType("Patient");
|
||||
assertThat(eidSystem, is(equalTo(PATIENT_EID_FOR_TEST)));
|
||||
|
||||
eidSystem = myRules.getEnterpriseEIDSystemForResourceType("Practitioner");
|
||||
assertThat(eidSystem, is(equalTo(PATIENT_EID_FOR_TEST)));
|
||||
|
||||
eidSystem = myRules.getEnterpriseEIDSystemForResourceType("Medication");
|
||||
assertThat(eidSystem, is(equalTo(PATIENT_EID_FOR_TEST)));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MdmRulesJson buildActiveBirthdateIdRules() {
|
||||
return super.buildActiveBirthdateIdRules();
|
||||
}
|
||||
|
||||
private MdmRulesJson buildOldStyleEidRules() {
|
||||
MdmRulesJson mdmRulesJson = super.buildActiveBirthdateIdRules();
|
||||
mdmRulesJson.setEnterpriseEIDSystems(Collections.emptyMap());
|
||||
//This sets the new-style eid resource type to `*`
|
||||
mdmRulesJson.setEnterpriseEIDSystem(PATIENT_EID_FOR_TEST);
|
||||
return mdmRulesJson;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -18,6 +18,9 @@ public abstract class BaseMdmRulesR4Test extends BaseR4Test {
|
|||
public static final String PATIENT_GIVEN = "patient-given";
|
||||
public static final String PATIENT_GIVEN_FIRST = "patient-given-first";
|
||||
public static final String PATIENT_FAMILY = "patient-last";
|
||||
public static final String PATIENT_EID_FOR_TEST = "http://hello.com/naming/patient-eid";
|
||||
public static final String MEDICATION_EID_FOR_TEST= "http://hello.com/naming/medication-eid";
|
||||
public static final String PRACTITIONER_EID_FOR_TEST = "http://hello.com/naming/practitioner-eid";
|
||||
|
||||
public static final double NAME_THRESHOLD = 0.8;
|
||||
protected MdmFieldMatchJson myGivenNameMatchField;
|
||||
|
@ -71,6 +74,10 @@ public abstract class BaseMdmRulesR4Test extends BaseR4Test {
|
|||
retval.setMdmTypes(Arrays.asList("Patient", "Practitioner", "Medication"));
|
||||
retval.putMatchResult(myBothNameFields, MdmMatchResultEnum.MATCH);
|
||||
retval.putMatchResult(PATIENT_GIVEN, MdmMatchResultEnum.POSSIBLE_MATCH);
|
||||
|
||||
retval.addEnterpriseEIDSystem("Patient", PATIENT_EID_FOR_TEST);
|
||||
retval.addEnterpriseEIDSystem("Medication", MEDICATION_EID_FOR_TEST);
|
||||
retval.addEnterpriseEIDSystem("Practitioner", PRACTITIONER_EID_FOR_TEST);
|
||||
return retval;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ public class EIDHelperR4Test extends BaseR4Test {
|
|||
|
||||
private static final MdmRulesJson ourRules = new MdmRulesJson() {
|
||||
{
|
||||
setEnterpriseEIDSystem(EXTERNAL_ID_SYSTEM_FOR_TEST);
|
||||
addEnterpriseEIDSystem("Patient", EXTERNAL_ID_SYSTEM_FOR_TEST);
|
||||
setMdmTypes(Arrays.asList(new String[] {"Patient"}));
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"version": "1",
|
||||
"mdmTypes": ["Organization"],
|
||||
"candidateSearchParams": [],
|
||||
"candidateFilterSearchParams": [],
|
||||
"matchFields": [
|
||||
{
|
||||
"name": "name-prefix",
|
||||
"resourceType": "Organization",
|
||||
"resourcePath": "name",
|
||||
"matcher": {
|
||||
"algorithm": "STRING"
|
||||
}
|
||||
}
|
||||
],
|
||||
"matchResultMap": {},
|
||||
"eidSystems": {
|
||||
"Patient": "http://hello.com/uri"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"version": "1",
|
||||
"mdmTypes": ["Organization"],
|
||||
"candidateSearchParams": [],
|
||||
"candidateFilterSearchParams": [],
|
||||
"matchFields": [
|
||||
{
|
||||
"name": "name-prefix",
|
||||
"resourceType": "Organization",
|
||||
"resourcePath": "name",
|
||||
"matcher": {
|
||||
"algorithm": "STRING"
|
||||
}
|
||||
}
|
||||
],
|
||||
"matchResultMap": {},
|
||||
"eidSystems": {
|
||||
"not-a-resource": "http://hello.com/uri"
|
||||
}
|
||||
}
|
|
@ -46,6 +46,12 @@ public abstract class BaseResourceMessage implements IResourceMessage, IModelJso
|
|||
@JsonProperty("mediaType")
|
||||
private String myMediaType;
|
||||
|
||||
/**
|
||||
* This is used by any message going to kafka for topic partition selection purposes.
|
||||
*/
|
||||
@JsonProperty("messageKey")
|
||||
private String myMessageKey;
|
||||
|
||||
/**
|
||||
* Returns an attribute stored in this message.
|
||||
* <p>
|
||||
|
@ -156,6 +162,15 @@ public abstract class BaseResourceMessage implements IResourceMessage, IModelJso
|
|||
myMediaType = theMediaType;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getMessageKeyOrNull() {
|
||||
return myMessageKey;
|
||||
}
|
||||
|
||||
public void setMessageKey(String theMessageKey) {
|
||||
myMessageKey = theMessageKey;
|
||||
}
|
||||
|
||||
public enum OperationTypeEnum {
|
||||
CREATE,
|
||||
UPDATE,
|
||||
|
|
|
@ -26,7 +26,6 @@ import org.hl7.fhir.instance.model.api.IBaseResource;
|
|||
|
||||
public class ResourceOperationMessage extends BaseResourceModifiedMessage {
|
||||
|
||||
|
||||
public ResourceOperationMessage() {
|
||||
}
|
||||
|
||||
|
|
|
@ -26,6 +26,8 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
|||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public abstract class BaseJsonMessage<T> implements Message<T>, IModelJson {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
@ -58,4 +60,9 @@ public abstract class BaseJsonMessage<T> implements Message<T>, IModelJson {
|
|||
public void setHeaders(HapiMessageHeaders theHeaders) {
|
||||
myHeaders = theHeaders;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getMessageKeyOrNull() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -186,6 +186,11 @@ public class ProviderConstants {
|
|||
*/
|
||||
public static final String OPERATION_REINDEX_RESPONSE_JOB_ID = "jobId";
|
||||
|
||||
/**
|
||||
* Operation name for the $member-match operation
|
||||
*/
|
||||
public static final String OPERATION_MEMBER_MATCH = "$member-match";
|
||||
|
||||
@Deprecated
|
||||
public static final String MARK_ALL_RESOURCES_FOR_REINDEXING = "$mark-all-resources-for-reindexing";
|
||||
/**
|
||||
|
|
|
@ -108,6 +108,7 @@ import org.springframework.transaction.TransactionDefinition;
|
|||
import org.springframework.transaction.support.TransactionCallback;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
@ -137,8 +138,10 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
|||
public abstract class BaseTransactionProcessor {
|
||||
|
||||
public static final String URN_PREFIX = "urn:";
|
||||
public static final String URN_PREFIX_ESCAPED = UrlUtil.escapeUrlParam(URN_PREFIX);
|
||||
public static final Pattern UNQUALIFIED_MATCH_URL_START = Pattern.compile("^[a-zA-Z0-9_]+=");
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(BaseTransactionProcessor.class);
|
||||
public static final Pattern INVALID_PLACEHOLDER_PATTERN = Pattern.compile("[a-zA-Z]+:.*");
|
||||
private BaseStorageDao myDao;
|
||||
@Autowired
|
||||
private PlatformTransactionManager myTxManager;
|
||||
|
@ -168,17 +171,6 @@ public abstract class BaseTransactionProcessor {
|
|||
@Autowired
|
||||
private IResourceVersionSvc myResourceVersionSvc;
|
||||
|
||||
public static boolean isPlaceholder(IIdType theId) {
|
||||
if (theId != null && theId.getValue() != null) {
|
||||
return theId.getValue().startsWith("urn:oid:") || theId.getValue().startsWith("urn:uuid:");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String toStatusString(int theStatusCode) {
|
||||
return theStatusCode + " " + defaultString(Constants.HTTP_STATUS_NAMES.get(theStatusCode));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setDaoConfig(DaoConfig theDaoConfig) {
|
||||
myDaoConfig = theDaoConfig;
|
||||
|
@ -267,14 +259,16 @@ public abstract class BaseTransactionProcessor {
|
|||
myVersionAdapter.populateEntryWithOperationOutcome(caughtEx, nextEntry);
|
||||
}
|
||||
|
||||
private void handleTransactionCreateOrUpdateOutcome(Map<IIdType, IIdType> idSubstitutions, Map<IIdType, DaoMethodOutcome> idToPersistedOutcome,
|
||||
private void handleTransactionCreateOrUpdateOutcome(IdSubstitutionMap idSubstitutions, Map<IIdType, DaoMethodOutcome> idToPersistedOutcome,
|
||||
IIdType nextResourceId, DaoMethodOutcome outcome,
|
||||
IBase newEntry, String theResourceType,
|
||||
IBaseResource theRes, RequestDetails theRequestDetails) {
|
||||
IIdType newId = outcome.getId().toUnqualified();
|
||||
IIdType resourceId = isPlaceholder(nextResourceId) ? nextResourceId : nextResourceId.toUnqualifiedVersionless();
|
||||
if (newId.equals(resourceId) == false) {
|
||||
idSubstitutions.put(resourceId, newId);
|
||||
if (!nextResourceId.isEmpty()) {
|
||||
idSubstitutions.put(resourceId, newId);
|
||||
}
|
||||
if (isPlaceholder(resourceId)) {
|
||||
/*
|
||||
* The correct way for substitution IDs to be is to be with no resource type, but we'll accept the qualified kind too just to be lenient.
|
||||
|
@ -330,30 +324,6 @@ public abstract class BaseTransactionProcessor {
|
|||
return theRes.getMeta().getLastUpdated();
|
||||
}
|
||||
|
||||
private String performIdSubstitutionsInMatchUrl(Map<IIdType, IIdType> theIdSubstitutions, String theMatchUrl) {
|
||||
String matchUrl = theMatchUrl;
|
||||
if (isNotBlank(matchUrl)) {
|
||||
for (Map.Entry<IIdType, IIdType> nextSubstitutionEntry : theIdSubstitutions.entrySet()) {
|
||||
IIdType nextTemporaryId = nextSubstitutionEntry.getKey();
|
||||
IIdType nextReplacementId = nextSubstitutionEntry.getValue();
|
||||
String nextTemporaryIdPart = nextTemporaryId.getIdPart();
|
||||
String nextReplacementIdPart = nextReplacementId.getValueAsString();
|
||||
if (isUrn(nextTemporaryId) && nextTemporaryIdPart.length() > URN_PREFIX.length()) {
|
||||
matchUrl = matchUrl.replace(nextTemporaryIdPart, nextReplacementIdPart);
|
||||
String escapedUrlParam = UrlUtil.escapeUrlParam(nextTemporaryIdPart);
|
||||
if (isNotBlank(escapedUrlParam)) {
|
||||
matchUrl = matchUrl.replace(escapedUrlParam, nextReplacementIdPart);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return matchUrl;
|
||||
}
|
||||
|
||||
private boolean isUrn(IIdType theId) {
|
||||
return defaultString(theId.getValue()).startsWith(URN_PREFIX);
|
||||
}
|
||||
|
||||
public void setDao(BaseStorageDao theDao) {
|
||||
myDao = theDao;
|
||||
}
|
||||
|
@ -392,10 +362,10 @@ public abstract class BaseTransactionProcessor {
|
|||
List<RetriableBundleTask> nonGetCalls = new ArrayList<>();
|
||||
|
||||
CountDownLatch completionLatch = new CountDownLatch(requestEntriesSize);
|
||||
for (int i=0; i< requestEntriesSize ; i++ ) {
|
||||
for (int i = 0; i < requestEntriesSize; i++) {
|
||||
IBase nextRequestEntry = requestEntries.get(i);
|
||||
RetriableBundleTask retriableBundleTask = new RetriableBundleTask(completionLatch, theRequestDetails, responseMap, i, nextRequestEntry, theNestedMode);
|
||||
if (myVersionAdapter.getEntryRequestVerb(myContext, nextRequestEntry).equalsIgnoreCase("GET")) {
|
||||
if (myVersionAdapter.getEntryRequestVerb(myContext, nextRequestEntry).equalsIgnoreCase("GET")) {
|
||||
getCalls.add(retriableBundleTask);
|
||||
} else {
|
||||
nonGetCalls.add(retriableBundleTask);
|
||||
|
@ -408,24 +378,24 @@ public abstract class BaseTransactionProcessor {
|
|||
|
||||
// waiting for all async tasks to be completed
|
||||
AsyncUtil.awaitLatchAndIgnoreInterrupt(completionLatch, 300L, TimeUnit.SECONDS);
|
||||
|
||||
|
||||
// Now, create the bundle response in original order
|
||||
Object nextResponseEntry;
|
||||
for (int i=0; i<requestEntriesSize; i++ ) {
|
||||
|
||||
for (int i = 0; i < requestEntriesSize; i++) {
|
||||
|
||||
nextResponseEntry = responseMap.get(i);
|
||||
if (nextResponseEntry instanceof BaseServerResponseExceptionHolder) {
|
||||
BaseServerResponseExceptionHolder caughtEx = (BaseServerResponseExceptionHolder)nextResponseEntry;
|
||||
BaseServerResponseExceptionHolder caughtEx = (BaseServerResponseExceptionHolder) nextResponseEntry;
|
||||
if (caughtEx.getException() != null) {
|
||||
IBase nextEntry = myVersionAdapter.addEntry(response);
|
||||
populateEntryWithOperationOutcome(caughtEx.getException(), nextEntry);
|
||||
myVersionAdapter.setResponseStatus(nextEntry, toStatusString(caughtEx.getException().getStatusCode()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
myVersionAdapter.addEntry(response, (IBase)nextResponseEntry);
|
||||
myVersionAdapter.addEntry(response, (IBase) nextResponseEntry);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
long delay = System.currentTimeMillis() - start;
|
||||
ourLog.info("Batch completed in {}ms", delay);
|
||||
|
||||
|
@ -631,12 +601,12 @@ public abstract class BaseTransactionProcessor {
|
|||
callWriteOperationsHook(Pointcut.STORAGE_TRANSACTION_WRITE_OPERATIONS_PRE, theRequestDetails, theTransactionDetails, writeOperationsDetails);
|
||||
}
|
||||
|
||||
TransactionCallback<Map<IBase, IIdType>> txCallback = status -> {
|
||||
TransactionCallback<EntriesToProcessMap> txCallback = status -> {
|
||||
final Set<IIdType> allIds = new LinkedHashSet<>();
|
||||
final Map<IIdType, IIdType> idSubstitutions = new HashMap<>();
|
||||
final IdSubstitutionMap idSubstitutions = new IdSubstitutionMap();
|
||||
final Map<IIdType, DaoMethodOutcome> idToPersistedOutcome = new HashMap<>();
|
||||
|
||||
Map<IBase, IIdType> retVal = doTransactionWriteOperations(theRequestDetails, theActionName,
|
||||
EntriesToProcessMap retVal = doTransactionWriteOperations(theRequestDetails, theActionName,
|
||||
theTransactionDetails, allIds,
|
||||
idSubstitutions, idToPersistedOutcome,
|
||||
theResponse, theOriginalRequestOrder,
|
||||
|
@ -645,7 +615,7 @@ public abstract class BaseTransactionProcessor {
|
|||
theTransactionStopWatch.startTask("Commit writes to database");
|
||||
return retVal;
|
||||
};
|
||||
Map<IBase, IIdType> entriesToProcess;
|
||||
EntriesToProcessMap entriesToProcess;
|
||||
|
||||
try {
|
||||
entriesToProcess = myHapiTransactionService.execute(theRequestDetails, theTransactionDetails, txCallback);
|
||||
|
@ -838,8 +808,13 @@ public abstract class BaseTransactionProcessor {
|
|||
}
|
||||
}
|
||||
|
||||
if (nextResourceId.hasIdPart() && nextResourceId.getIdPart().matches("[a-zA-Z]+:.*") && !isPlaceholder(nextResourceId)) {
|
||||
throw new InvalidRequestException("Invalid placeholder ID found: " + nextResourceId.getIdPart() + " - Must be of the form 'urn:uuid:[uuid]' or 'urn:oid:[oid]'");
|
||||
if (nextResourceId.hasIdPart() && !isPlaceholder(nextResourceId)) {
|
||||
int colonIndex = nextResourceId.getIdPart().indexOf(':');
|
||||
if (colonIndex != -1) {
|
||||
if (INVALID_PLACEHOLDER_PATTERN.matcher(nextResourceId.getIdPart()).matches()) {
|
||||
throw new InvalidRequestException("Invalid placeholder ID found: " + nextResourceId.getIdPart() + " - Must be of the form 'urn:uuid:[uuid]' or 'urn:oid:[oid]'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nextResourceId.hasIdPart() && !nextResourceId.hasResourceType() && !isPlaceholder(nextResourceId)) {
|
||||
|
@ -869,9 +844,9 @@ public abstract class BaseTransactionProcessor {
|
|||
/**
|
||||
* After pre-hooks have been called
|
||||
*/
|
||||
protected Map<IBase, IIdType> doTransactionWriteOperations(final RequestDetails theRequest, String theActionName,
|
||||
protected EntriesToProcessMap doTransactionWriteOperations(final RequestDetails theRequest, String theActionName,
|
||||
TransactionDetails theTransactionDetails, Set<IIdType> theAllIds,
|
||||
Map<IIdType, IIdType> theIdSubstitutions, Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome,
|
||||
IdSubstitutionMap theIdSubstitutions, Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome,
|
||||
IBaseBundle theResponse, IdentityHashMap<IBase, Integer> theOriginalRequestOrder,
|
||||
List<IBase> theEntries, StopWatch theTransactionStopWatch) {
|
||||
|
||||
|
@ -884,7 +859,7 @@ public abstract class BaseTransactionProcessor {
|
|||
try {
|
||||
Set<String> deletedResources = new HashSet<>();
|
||||
DeleteConflictList deleteConflicts = new DeleteConflictList();
|
||||
Map<IBase, IIdType> entriesToProcess = new IdentityHashMap<>();
|
||||
EntriesToProcessMap entriesToProcess = new EntriesToProcessMap();
|
||||
Set<IIdType> nonUpdatedEntities = new HashSet<>();
|
||||
Set<IBasePersistedResource> updatedEntities = new HashSet<>();
|
||||
Map<String, IIdType> conditionalUrlToIdMap = new HashMap<>();
|
||||
|
@ -1133,7 +1108,7 @@ public abstract class BaseTransactionProcessor {
|
|||
theTransactionStopWatch.endCurrentTask();
|
||||
|
||||
for (IIdType next : theAllIds) {
|
||||
IIdType replacement = theIdSubstitutions.get(next);
|
||||
IIdType replacement = theIdSubstitutions.getForSource(next);
|
||||
if (replacement != null && !replacement.equals(next)) {
|
||||
ourLog.debug("Placeholder resource ID \"{}\" was replaced with permanent ID \"{}\"", next, replacement);
|
||||
}
|
||||
|
@ -1177,9 +1152,6 @@ public abstract class BaseTransactionProcessor {
|
|||
/**
|
||||
* After transaction processing and resolution of indexes and references, we want to validate that the resources that were stored _actually_
|
||||
* match the conditional URLs that they were brought in on.
|
||||
*
|
||||
* @param theIdToPersistedOutcome
|
||||
* @param conditionalUrlToIdMap
|
||||
*/
|
||||
private void validateAllInsertsMatchTheirConditionalUrls(Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome, Map<String, IIdType> conditionalUrlToIdMap, RequestDetails theRequest) {
|
||||
conditionalUrlToIdMap.entrySet().stream()
|
||||
|
@ -1274,8 +1246,8 @@ public abstract class BaseTransactionProcessor {
|
|||
* account for NOPs, so we block NOPs in that pass.
|
||||
*/
|
||||
private void resolveReferencesThenSaveAndIndexResources(RequestDetails theRequest, TransactionDetails theTransactionDetails,
|
||||
Map<IIdType, IIdType> theIdSubstitutions, Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome,
|
||||
StopWatch theTransactionStopWatch, Map<IBase, IIdType> entriesToProcess,
|
||||
IdSubstitutionMap theIdSubstitutions, Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome,
|
||||
StopWatch theTransactionStopWatch, EntriesToProcessMap entriesToProcess,
|
||||
Set<IIdType> nonUpdatedEntities, Set<IBasePersistedResource> updatedEntities) {
|
||||
FhirTerser terser = myContext.newTerser();
|
||||
theTransactionStopWatch.startTask("Index " + theIdToPersistedOutcome.size() + " resources");
|
||||
|
@ -1333,8 +1305,8 @@ public abstract class BaseTransactionProcessor {
|
|||
}
|
||||
|
||||
private void resolveReferencesThenSaveAndIndexResource(RequestDetails theRequest, TransactionDetails theTransactionDetails,
|
||||
Map<IIdType, IIdType> theIdSubstitutions, Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome,
|
||||
Map<IBase, IIdType> entriesToProcess, Set<IIdType> nonUpdatedEntities,
|
||||
IdSubstitutionMap theIdSubstitutions, Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome,
|
||||
EntriesToProcessMap entriesToProcess, Set<IIdType> nonUpdatedEntities,
|
||||
Set<IBasePersistedResource> updatedEntities, FhirTerser terser,
|
||||
DaoMethodOutcome nextOutcome, IBaseResource nextResource,
|
||||
Set<IBaseReference> theReferencesToAutoVersion) {
|
||||
|
@ -1350,7 +1322,7 @@ public abstract class BaseTransactionProcessor {
|
|||
if (targetId.getValue() == null || targetId.getValue().startsWith("#")) {
|
||||
// This means it's a contained resource
|
||||
continue;
|
||||
} else if (theIdSubstitutions.containsValue(targetId)) {
|
||||
} else if (theIdSubstitutions.containsTarget(targetId)) {
|
||||
newId = targetId;
|
||||
} else {
|
||||
throw new InternalErrorException("References by resource with no reference ID are not supported in DAO layer");
|
||||
|
@ -1359,9 +1331,9 @@ public abstract class BaseTransactionProcessor {
|
|||
continue;
|
||||
}
|
||||
}
|
||||
if (newId != null || theIdSubstitutions.containsKey(nextId)) {
|
||||
if (newId != null || theIdSubstitutions.containsSource(nextId)) {
|
||||
if (newId == null) {
|
||||
newId = theIdSubstitutions.get(nextId);
|
||||
newId = theIdSubstitutions.getForSource(nextId);
|
||||
}
|
||||
if (newId != null) {
|
||||
ourLog.debug(" * Replacing resource ref {} with {}", nextId, newId);
|
||||
|
@ -1421,9 +1393,9 @@ public abstract class BaseTransactionProcessor {
|
|||
if (nextRef instanceof IIdType) {
|
||||
continue; // No substitution on the resource ID itself!
|
||||
}
|
||||
IIdType nextUriString = newIdType(nextRef.getValueAsString());
|
||||
if (theIdSubstitutions.containsKey(nextUriString)) {
|
||||
IIdType newId = theIdSubstitutions.get(nextUriString);
|
||||
String nextUriString = nextRef.getValueAsString();
|
||||
if (theIdSubstitutions.containsSource(nextUriString)) {
|
||||
IIdType newId = theIdSubstitutions.getForSource(nextUriString);
|
||||
ourLog.debug(" * Replacing resource ref {} with {}", nextUriString, newId);
|
||||
|
||||
String existingValue = nextRef.getValueAsString();
|
||||
|
@ -1458,26 +1430,17 @@ public abstract class BaseTransactionProcessor {
|
|||
// Make sure we reflect the actual final version for the resource.
|
||||
if (updateOutcome != null) {
|
||||
IIdType newId = updateOutcome.getIdDt();
|
||||
for (IIdType nextEntry : entriesToProcess.values()) {
|
||||
if (nextEntry.getResourceType().equals(newId.getResourceType())) {
|
||||
if (nextEntry.getIdPart().equals(newId.getIdPart())) {
|
||||
if (!nextEntry.hasVersionIdPart() || !nextEntry.getVersionIdPart().equals(newId.getVersionIdPart())) {
|
||||
nextEntry.setParts(nextEntry.getBaseUrl(), nextEntry.getResourceType(), nextEntry.getIdPart(), newId.getVersionIdPart());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IIdType entryId = entriesToProcess.getIdWithVersionlessComparison(newId);
|
||||
if (entryId != null && !StringUtils.equals(entryId.getValue(), newId.getValue())) {
|
||||
entryId.setValue(newId.getValue());
|
||||
}
|
||||
|
||||
nextOutcome.setId(newId);
|
||||
|
||||
for (IIdType next : theIdSubstitutions.values()) {
|
||||
if (next.getResourceType().equals(newId.getResourceType())) {
|
||||
if (next.getIdPart().equals(newId.getIdPart())) {
|
||||
if (!next.getValue().equals(newId.getValue())) {
|
||||
next.setValue(newId.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
IIdType target = theIdSubstitutions.getForSource(newId);
|
||||
if (target != null) {
|
||||
target.setValue(newId.getValue());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1626,18 +1589,6 @@ public abstract class BaseTransactionProcessor {
|
|||
return null;
|
||||
}
|
||||
|
||||
private static class BaseServerResponseExceptionHolder {
|
||||
private BaseServerResponseException myException;
|
||||
|
||||
public BaseServerResponseException getException() {
|
||||
return myException;
|
||||
}
|
||||
|
||||
public void setException(BaseServerResponseException myException) {
|
||||
this.myException = myException;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transaction Order, per the spec:
|
||||
* <p>
|
||||
|
@ -1808,4 +1759,99 @@ public abstract class BaseTransactionProcessor {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
private static class BaseServerResponseExceptionHolder {
|
||||
private BaseServerResponseException myException;
|
||||
|
||||
public BaseServerResponseException getException() {
|
||||
return myException;
|
||||
}
|
||||
|
||||
public void setException(BaseServerResponseException myException) {
|
||||
this.myException = myException;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isPlaceholder(IIdType theId) {
|
||||
if (theId != null && theId.getValue() != null) {
|
||||
return theId.getValue().startsWith("urn:oid:") || theId.getValue().startsWith("urn:uuid:");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String toStatusString(int theStatusCode) {
|
||||
return theStatusCode + " " + defaultString(Constants.HTTP_STATUS_NAMES.get(theStatusCode));
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a match URL containing
|
||||
*
|
||||
* @param theIdSubstitutions
|
||||
* @param theMatchUrl
|
||||
* @return
|
||||
*/
|
||||
static String performIdSubstitutionsInMatchUrl(IdSubstitutionMap theIdSubstitutions, String theMatchUrl) {
|
||||
String matchUrl = theMatchUrl;
|
||||
if (isNotBlank(matchUrl) && !theIdSubstitutions.isEmpty()) {
|
||||
|
||||
int startIdx = matchUrl.indexOf('?');
|
||||
while (startIdx != -1) {
|
||||
|
||||
int endIdx = matchUrl.indexOf('&', startIdx + 1);
|
||||
if (endIdx == -1) {
|
||||
endIdx = matchUrl.length();
|
||||
}
|
||||
|
||||
int equalsIdx = matchUrl.indexOf('=', startIdx + 1);
|
||||
|
||||
int searchFrom;
|
||||
if (equalsIdx == -1) {
|
||||
searchFrom = matchUrl.length();
|
||||
} else if (equalsIdx >= endIdx) {
|
||||
// First equals we found is from a subsequent parameter
|
||||
searchFrom = matchUrl.length();
|
||||
} else {
|
||||
String paramValue = matchUrl.substring(equalsIdx + 1, endIdx);
|
||||
boolean isUrn = isUrn(paramValue);
|
||||
boolean isUrnEscaped = !isUrn && isUrnEscaped(paramValue);
|
||||
if (isUrn || isUrnEscaped) {
|
||||
if (isUrnEscaped) {
|
||||
paramValue = UrlUtil.unescape(paramValue);
|
||||
}
|
||||
IIdType replacement = theIdSubstitutions.getForSource(paramValue);
|
||||
if (replacement != null) {
|
||||
String replacementValue;
|
||||
if (replacement.hasVersionIdPart()) {
|
||||
replacementValue = replacement.toVersionless().getValue();
|
||||
} else {
|
||||
replacementValue = replacement.getValue();
|
||||
}
|
||||
matchUrl = matchUrl.substring(0, equalsIdx + 1) + replacementValue + matchUrl.substring(endIdx);
|
||||
searchFrom = equalsIdx + 1 + replacementValue.length();
|
||||
} else {
|
||||
searchFrom = endIdx;
|
||||
}
|
||||
} else {
|
||||
searchFrom = endIdx;
|
||||
}
|
||||
}
|
||||
|
||||
if (searchFrom >= matchUrl.length()) {
|
||||
break;
|
||||
}
|
||||
|
||||
startIdx = matchUrl.indexOf('&', searchFrom);
|
||||
}
|
||||
|
||||
}
|
||||
return matchUrl;
|
||||
}
|
||||
|
||||
private static boolean isUrn(@Nonnull String theId) {
|
||||
return theId.startsWith(URN_PREFIX);
|
||||
}
|
||||
|
||||
private static boolean isUrnEscaped(@Nonnull String theId) {
|
||||
return theId.startsWith(URN_PREFIX_ESCAPED);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
package ca.uhn.fhir.jpa.dao;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR Storage api
|
||||
* %%
|
||||
* 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 org.hl7.fhir.instance.model.api.IBase;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static ca.uhn.fhir.jpa.dao.IdSubstitutionMap.toVersionlessValue;
|
||||
|
||||
public class EntriesToProcessMap {
|
||||
|
||||
private final IdentityHashMap<IBase, IIdType> myEntriesToProcess = new IdentityHashMap<>();
|
||||
private final Map<String, IIdType> myVersionlessIdToVersionedId = new HashMap<>();
|
||||
|
||||
public void put(IBase theBundleEntry, IIdType theId) {
|
||||
myEntriesToProcess.put(theBundleEntry, theId);
|
||||
myVersionlessIdToVersionedId.put(toVersionlessValue(theId), theId);
|
||||
}
|
||||
|
||||
public IIdType getIdWithVersionlessComparison(IIdType theId) {
|
||||
return myVersionlessIdToVersionedId.get(toVersionlessValue(theId));
|
||||
}
|
||||
|
||||
public Set<Map.Entry<IBase, IIdType>> entrySet() {
|
||||
return myEntriesToProcess.entrySet();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
package ca.uhn.fhir.jpa.dao;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR Storage api
|
||||
* %%
|
||||
* 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 com.google.common.collect.Multimap;
|
||||
import com.google.common.collect.MultimapBuilder;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class IdSubstitutionMap {
|
||||
|
||||
private final Map<Entry, Entry> myMap = new HashMap<>();
|
||||
private final Multimap<Entry, Entry> myReverseMap = MultimapBuilder.hashKeys().arrayListValues().build();
|
||||
|
||||
|
||||
public boolean containsSource(IIdType theId) {
|
||||
if (theId.isLocal()) {
|
||||
return false;
|
||||
}
|
||||
return myMap.containsKey(new Entry(theId));
|
||||
}
|
||||
|
||||
public boolean containsSource(String theId) {
|
||||
return myMap.containsKey(new Entry(theId));
|
||||
}
|
||||
|
||||
public boolean containsTarget(IIdType theId) {
|
||||
return myReverseMap.containsKey(new Entry(theId));
|
||||
}
|
||||
|
||||
public boolean containsTarget(String theId) {
|
||||
return myReverseMap.containsKey(new Entry(theId));
|
||||
}
|
||||
|
||||
public IIdType getForSource(IIdType theId) {
|
||||
Entry target = myMap.get(new Entry(theId));
|
||||
if (target != null) {
|
||||
assert target.myId != null;
|
||||
return target.myId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public IIdType getForSource(String theId) {
|
||||
Entry target = myMap.get(new Entry(theId));
|
||||
if (target != null) {
|
||||
assert target.myId != null;
|
||||
return target.myId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<Pair<IIdType, IIdType>> entrySet() {
|
||||
return myMap
|
||||
.entrySet()
|
||||
.stream()
|
||||
.map(t->Pair.of(t.getKey().myId, t.getValue().myId))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public void put(IIdType theSource, IIdType theTarget) {
|
||||
myMap.put(new Entry(theSource), new Entry(theTarget));
|
||||
myReverseMap.put(new Entry(theTarget), new Entry(theSource));
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return myMap.isEmpty();
|
||||
}
|
||||
|
||||
|
||||
private static class Entry {
|
||||
|
||||
private final String myUnversionedId;
|
||||
private final IIdType myId;
|
||||
|
||||
private Entry(String theId) {
|
||||
myId = null;
|
||||
myUnversionedId = theId;
|
||||
}
|
||||
|
||||
private Entry(IIdType theId) {
|
||||
String unversionedId = toVersionlessValue(theId);
|
||||
myUnversionedId = unversionedId;
|
||||
myId = theId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object theOther) {
|
||||
if (theOther instanceof Entry) {
|
||||
String otherUnversionedId = ((Entry) theOther).myUnversionedId;
|
||||
if (myUnversionedId.equals(otherUnversionedId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return myUnversionedId.hashCode();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static String toVersionlessValue(IIdType theId) {
|
||||
boolean isPlaceholder = theId.getValue().startsWith("urn:");
|
||||
String unversionedId;
|
||||
if (isPlaceholder || (!theId.hasBaseUrl() && !theId.hasVersionIdPart()) || !theId.hasResourceType()) {
|
||||
unversionedId = theId.getValue();
|
||||
} else {
|
||||
unversionedId = theId.toUnqualifiedVersionless().getValue();
|
||||
}
|
||||
return unversionedId;
|
||||
}
|
||||
}
|
|
@ -185,7 +185,7 @@ public class MatchResourceUrlService {
|
|||
@Nullable
|
||||
public ResourcePersistentId processMatchUrlUsingCacheOnly(String theResourceType, String theMatchUrl) {
|
||||
ResourcePersistentId existing = null;
|
||||
if (myDaoConfig.getMatchUrlCache()) {
|
||||
if (myDaoConfig.isMatchUrlCacheEnabled()) {
|
||||
String matchUrl = massageForStorage(theResourceType, theMatchUrl);
|
||||
existing = myMemoryCacheService.getIfPresent(MemoryCacheService.CacheEnum.MATCH_URL, matchUrl);
|
||||
}
|
||||
|
|
|
@ -25,11 +25,14 @@ import ca.uhn.fhir.context.FhirContext;
|
|||
import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionMatchingStrategy;
|
||||
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription;
|
||||
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType;
|
||||
import ca.uhn.fhir.model.dstu2.composite.CodingDt;
|
||||
import ca.uhn.fhir.model.dstu2.resource.Subscription;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
|
||||
import ca.uhn.fhir.util.HapiExtensions;
|
||||
import org.hl7.fhir.dstu3.model.Coding;
|
||||
import org.hl7.fhir.exceptions.FHIRException;
|
||||
import org.hl7.fhir.instance.model.api.IBaseCoding;
|
||||
import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
|
||||
import org.hl7.fhir.instance.model.api.IBaseMetaType;
|
||||
import org.hl7.fhir.instance.model.api.IBaseReference;
|
||||
|
@ -43,6 +46,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
@ -79,7 +83,6 @@ public class SubscriptionCanonicalizer {
|
|||
|
||||
private CanonicalSubscription canonicalizeDstu2(IBaseResource theSubscription) {
|
||||
ca.uhn.fhir.model.dstu2.resource.Subscription subscription = (ca.uhn.fhir.model.dstu2.resource.Subscription) theSubscription;
|
||||
|
||||
CanonicalSubscription retVal = new CanonicalSubscription();
|
||||
try {
|
||||
retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.fromCode(subscription.getStatus()));
|
||||
|
@ -90,12 +93,28 @@ public class SubscriptionCanonicalizer {
|
|||
retVal.setChannelExtensions(extractExtension(subscription));
|
||||
retVal.setIdElement(subscription.getIdElement());
|
||||
retVal.setPayloadString(subscription.getChannel().getPayload());
|
||||
retVal.setTags(extractTags(subscription));
|
||||
} catch (FHIRException theE) {
|
||||
throw new InternalErrorException(theE);
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the meta tags from the subscription and convert them to a simple string map.
|
||||
* @param theSubscription The subscription to extract the tags from
|
||||
* @return A map of tags System:Code
|
||||
*/
|
||||
private Map<String, String> extractTags(IBaseResource theSubscription) {
|
||||
return theSubscription.getMeta().getTag()
|
||||
.stream()
|
||||
.filter(t -> t.getSystem() != null && t.getCode() != null)
|
||||
.collect(Collectors.toMap(
|
||||
IBaseCoding::getSystem,
|
||||
IBaseCoding::getCode
|
||||
));
|
||||
}
|
||||
|
||||
private CanonicalSubscription canonicalizeDstu3(IBaseResource theSubscription) {
|
||||
org.hl7.fhir.dstu3.model.Subscription subscription = (org.hl7.fhir.dstu3.model.Subscription) theSubscription;
|
||||
|
||||
|
@ -113,6 +132,7 @@ public class SubscriptionCanonicalizer {
|
|||
retVal.setIdElement(subscription.getIdElement());
|
||||
retVal.setPayloadString(subscription.getChannel().getPayload());
|
||||
retVal.setPayloadSearchCriteria(getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_PAYLOAD_SEARCH_CRITERIA));
|
||||
retVal.setTags(extractTags(subscription));
|
||||
|
||||
if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) {
|
||||
String from;
|
||||
|
@ -201,7 +221,6 @@ public class SubscriptionCanonicalizer {
|
|||
|
||||
private CanonicalSubscription canonicalizeR4(IBaseResource theSubscription) {
|
||||
org.hl7.fhir.r4.model.Subscription subscription = (org.hl7.fhir.r4.model.Subscription) theSubscription;
|
||||
|
||||
CanonicalSubscription retVal = new CanonicalSubscription();
|
||||
retVal.setStatus(subscription.getStatus());
|
||||
retVal.setChannelType(getChannelType(theSubscription));
|
||||
|
@ -212,6 +231,7 @@ public class SubscriptionCanonicalizer {
|
|||
retVal.setIdElement(subscription.getIdElement());
|
||||
retVal.setPayloadString(subscription.getChannel().getPayload());
|
||||
retVal.setPayloadSearchCriteria(getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_PAYLOAD_SEARCH_CRITERIA));
|
||||
retVal.setTags(extractTags(subscription));
|
||||
|
||||
if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) {
|
||||
String from;
|
||||
|
@ -266,6 +286,7 @@ public class SubscriptionCanonicalizer {
|
|||
retVal.setIdElement(subscription.getIdElement());
|
||||
retVal.setPayloadString(subscription.getContentType());
|
||||
retVal.setPayloadSearchCriteria(getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_PAYLOAD_SEARCH_CRITERIA));
|
||||
retVal.setTags(extractTags(subscription));
|
||||
|
||||
if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) {
|
||||
String from;
|
||||
|
|
|
@ -68,6 +68,8 @@ public class CanonicalSubscription implements Serializable, Cloneable, IModelJso
|
|||
private RestHookDetails myRestHookDetails;
|
||||
@JsonProperty("extensions")
|
||||
private Map<String, List<String>> myChannelExtensions;
|
||||
@JsonProperty("tags")
|
||||
private Map<String, String> myTags;
|
||||
@JsonProperty("payloadSearchCriteria")
|
||||
private String myPayloadSearchCriteria;
|
||||
|
||||
|
@ -139,6 +141,15 @@ public class CanonicalSubscription implements Serializable, Cloneable, IModelJso
|
|||
}
|
||||
}
|
||||
|
||||
public Map<String, String> getTags() {
|
||||
return myTags;
|
||||
}
|
||||
|
||||
public void setTags(Map<String, String> theTags) {
|
||||
this.myTags = theTags;
|
||||
}
|
||||
|
||||
|
||||
public void setHeaders(String theHeaders) {
|
||||
myHeaders = new ArrayList<>();
|
||||
if (isNotBlank(theHeaders)) {
|
||||
|
|
|
@ -1,5 +1,25 @@
|
|||
package ca.uhn.fhir.jpa.subscription.model;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR Storage api
|
||||
* %%
|
||||
* 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%
|
||||
*/
|
||||
|
||||
public class ChannelRetryConfiguration {
|
||||
/**
|
||||
* Number of times to retry a failed message.
|
||||
|
|
|
@ -24,6 +24,8 @@ import ca.uhn.fhir.rest.server.messaging.json.BaseJsonMessage;
|
|||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class ResourceModifiedJsonMessage extends BaseJsonMessage<ResourceModifiedMessage> {
|
||||
|
||||
@JsonProperty("payload")
|
||||
|
@ -52,6 +54,15 @@ public class ResourceModifiedJsonMessage extends BaseJsonMessage<ResourceModifie
|
|||
myPayload = thePayload;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public String getMessageKeyOrNull() {
|
||||
if (myPayload == null) {
|
||||
return null;
|
||||
}
|
||||
return myPayload.getMessageKeyOrNull();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringBuilder(this)
|
||||
|
|
|
@ -27,6 +27,8 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
|||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Most of this class has been moved to ResourceModifiedMessage in the hapi-fhir-server project, for a reusable channel ResourceModifiedMessage
|
||||
* that doesn't require knowledge of subscriptions.
|
||||
|
@ -40,6 +42,8 @@ public class ResourceModifiedMessage extends BaseResourceModifiedMessage {
|
|||
@JsonProperty(value = "subscriptionId", required = false)
|
||||
private String mySubscriptionId;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
|
@ -64,6 +68,7 @@ public class ResourceModifiedMessage extends BaseResourceModifiedMessage {
|
|||
mySubscriptionId = theSubscriptionId;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringBuilder(this)
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
package ca.uhn.fhir.jpa.dao;
|
||||
|
||||
import ca.uhn.fhir.util.UrlUtil;
|
||||
import org.hl7.fhir.r4.model.IdType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class BaseTransactionProcessorTest {
|
||||
|
||||
@Test
|
||||
void testPerformIdSubstitutionsInMatchUrl_MatchAtStart() {
|
||||
IdSubstitutionMap idSubstitutions = new IdSubstitutionMap();
|
||||
idSubstitutions.put(new IdType("urn:uuid:1234"), new IdType("Patient/123"));
|
||||
String outcome = BaseTransactionProcessor.performIdSubstitutionsInMatchUrl(idSubstitutions, "Patient?foo=urn:uuid:1234&bar=baz");
|
||||
assertEquals("Patient?foo=Patient/123&bar=baz", outcome);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure versioned targets get the version stripped
|
||||
*/
|
||||
@Test
|
||||
void testPerformIdSubstitutionsInMatchUrl_MatchAtEnd() {
|
||||
IdSubstitutionMap idSubstitutions = new IdSubstitutionMap();
|
||||
idSubstitutions.put(new IdType("urn:uuid:7ea4f3a6-d2a3-4105-9f31-374d525085d4"), new IdType("Patient/123/_history/1"));
|
||||
String outcome = BaseTransactionProcessor.performIdSubstitutionsInMatchUrl(idSubstitutions, "Patient?name=FAMILY1&organization=urn%3Auuid%3A7ea4f3a6-d2a3-4105-9f31-374d525085d4");
|
||||
assertEquals("Patient?name=FAMILY1&organization=Patient/123", outcome);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPerformIdSubstitutionsInMatchUrl_MatchEscapedParam() {
|
||||
IdSubstitutionMap idSubstitutions = new IdSubstitutionMap();
|
||||
idSubstitutions.put(new IdType("urn:uuid:1234"), new IdType("Patient/123"));
|
||||
String outcome = BaseTransactionProcessor.performIdSubstitutionsInMatchUrl(idSubstitutions, "Patient?foo=" + UrlUtil.escapeUrlParam("urn:uuid:1234") + "&bar=baz");
|
||||
assertEquals("Patient?foo=Patient/123&bar=baz", outcome);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPerformIdSubstitutionsInMatchUrl_MatchInParamNameShouldntBeReplaced() {
|
||||
IdSubstitutionMap idSubstitutions = new IdSubstitutionMap();
|
||||
idSubstitutions.put(new IdType("urn:uuid:1234"), new IdType("Patient/123"));
|
||||
String outcome = BaseTransactionProcessor.performIdSubstitutionsInMatchUrl(idSubstitutions, "Patient?urn:uuid:1234=foo&bar=baz");
|
||||
assertEquals("Patient?urn:uuid:1234=foo&bar=baz", outcome);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPerformIdSubstitutionsInMatchUrl_NoParams() {
|
||||
IdSubstitutionMap idSubstitutions = new IdSubstitutionMap();
|
||||
idSubstitutions.put(new IdType("urn:uuid:1234"), new IdType("Patient/123"));
|
||||
String input = "Patient";
|
||||
String outcome = BaseTransactionProcessor.performIdSubstitutionsInMatchUrl(idSubstitutions, input);
|
||||
assertEquals(input, outcome);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPerformIdSubstitutionsInMatchUrl_UnterminatedParams() {
|
||||
IdSubstitutionMap idSubstitutions = new IdSubstitutionMap();
|
||||
idSubstitutions.put(new IdType("urn:uuid:1234"), new IdType("Patient/123"));
|
||||
String input = "Patient?foo&bar=&baz";
|
||||
String outcome = BaseTransactionProcessor.performIdSubstitutionsInMatchUrl(idSubstitutions, input);
|
||||
assertEquals(input, outcome);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPerformIdSubstitutionsInMatchUrl_ReplaceMultiple() {
|
||||
IdSubstitutionMap idSubstitutions = new IdSubstitutionMap();
|
||||
idSubstitutions.put(new IdType("urn:uuid:1234"), new IdType("Patient/abcdefghijklmnopqrstuvwxyz0123456789"));
|
||||
String input = "Patient?foo=urn:uuid:1234&bar=urn:uuid:1234&baz=urn:uuid:1234";
|
||||
String outcome = BaseTransactionProcessor.performIdSubstitutionsInMatchUrl(idSubstitutions, input);
|
||||
String expected = "Patient?foo=Patient/abcdefghijklmnopqrstuvwxyz0123456789&bar=Patient/abcdefghijklmnopqrstuvwxyz0123456789&baz=Patient/abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
assertEquals(expected, outcome);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPerformIdSubstitutionsInMatchUrl_NonUrnSubstitution() {
|
||||
IdSubstitutionMap idSubstitutions = new IdSubstitutionMap();
|
||||
idSubstitutions.put(new IdType("Patient/123"), new IdType("Patient/456"));
|
||||
String input = "Patient?foo=Patient/123";
|
||||
String outcome = BaseTransactionProcessor.performIdSubstitutionsInMatchUrl(idSubstitutions, input);
|
||||
assertEquals(input, outcome);
|
||||
}
|
||||
|
||||
}
|
|
@ -2338,7 +2338,7 @@ public class JsonParserDstu3Test {
|
|||
ourCtx.newJsonParser().parseResource(Bundle.class, bundle);
|
||||
fail();
|
||||
} catch (DataFormatException e) {
|
||||
assertEquals("Failed to parse JSON encoded FHIR content: Unexpected close marker '}': expected ']' (for root starting at [Source: UNKNOWN; line: 1, column: 0])\n" +
|
||||
assertEquals("Failed to parse JSON encoded FHIR content: Unexpected close marker '}': expected ']' (for root starting at [Source: UNKNOWN; line: 1])\n" +
|
||||
" at [Source: UNKNOWN; line: 4, column: 3]", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ import org.hl7.fhir.r4.model.StringType;
|
|||
import org.hl7.fhir.r4.model.Type;
|
||||
import org.hl7.fhir.utilities.xhtml.XhtmlNode;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -80,6 +81,11 @@ public class JsonParserR4Test extends BaseTest {
|
|||
return b;
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void afterEach() {
|
||||
ourCtx.getParserOptions().setAutoContainReferenceTargetsWithNoId(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEntitiesNotConverted() throws IOException {
|
||||
Device input = loadResource(ourCtx, Device.class, "/entities-from-cerner.json");
|
||||
|
@ -198,6 +204,25 @@ public class JsonParserR4Test extends BaseTest {
|
|||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testContainedResourcesNotAutoContainedWhenConfiguredNotToDoSo() {
|
||||
MedicationDispense md = new MedicationDispense();
|
||||
md.addIdentifier().setValue("DISPENSE");
|
||||
|
||||
Medication med = new Medication();
|
||||
med.getCode().setText("MED");
|
||||
md.setMedication(new Reference(med));
|
||||
|
||||
ourCtx.getParserOptions().setAutoContainReferenceTargetsWithNoId(false);
|
||||
String encoded = ourCtx.newJsonParser().setPrettyPrint(false).encodeResourceToString(md);
|
||||
assertEquals("{\"resourceType\":\"MedicationDispense\",\"identifier\":[{\"value\":\"DISPENSE\"}],\"medicationReference\":{}}", encoded);
|
||||
|
||||
ourCtx.getParserOptions().setAutoContainReferenceTargetsWithNoId(true);
|
||||
encoded = ourCtx.newJsonParser().setPrettyPrint(false).encodeResourceToString(md);
|
||||
assertEquals("{\"resourceType\":\"MedicationDispense\",\"contained\":[{\"resourceType\":\"Medication\",\"id\":\"1\",\"code\":{\"text\":\"MED\"}}],\"identifier\":[{\"value\":\"DISPENSE\"}],\"medicationReference\":{\"reference\":\"#1\"}}", encoded);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseBundleWithMultipleNestedContainedResources() throws Exception {
|
||||
String text = loadResource("/bundle-with-two-patient-resources.json");
|
||||
|
|
|
@ -28,6 +28,7 @@ import java.math.BigDecimal;
|
|||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -537,8 +538,8 @@ public class JsonLikeParserTest {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Set<String> keySet() {
|
||||
return nativeObject.keySet();
|
||||
public Iterator<String> keyIterator() {
|
||||
return nativeObject.keySet().iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -62,10 +62,11 @@ public class BatchJobHelper {
|
|||
public List<JobExecution> awaitAllBulkJobCompletions(boolean theFailIfNotJobsFound, String... theJobNames) {
|
||||
assert theJobNames.length > 0;
|
||||
|
||||
List<JobInstance> matchingJobInstances = new ArrayList<>();
|
||||
for (String nextName : theJobNames) {
|
||||
matchingJobInstances.addAll(myJobExplorer.findJobInstancesByJobName(nextName, 0, 100));
|
||||
if (theFailIfNotJobsFound) {
|
||||
await().until(() -> !getJobInstances(theJobNames).isEmpty());
|
||||
}
|
||||
List<JobInstance> matchingJobInstances = getJobInstances(theJobNames);
|
||||
|
||||
if (theFailIfNotJobsFound) {
|
||||
if (matchingJobInstances.isEmpty()) {
|
||||
List<String> wantNames = Arrays.asList(theJobNames);
|
||||
|
@ -81,6 +82,14 @@ public class BatchJobHelper {
|
|||
return matchingExecutions;
|
||||
}
|
||||
|
||||
private List<JobInstance> getJobInstances(String[] theJobNames) {
|
||||
List<JobInstance> matchingJobInstances = new ArrayList<>();
|
||||
for (String nextName : theJobNames) {
|
||||
matchingJobInstances.addAll(myJobExplorer.findJobInstancesByJobName(nextName, 0, 100));
|
||||
}
|
||||
return matchingJobInstances;
|
||||
}
|
||||
|
||||
public JobExecution awaitJobExecution(Long theJobExecutionId) {
|
||||
JobExecution jobExecution = myJobExplorer.getJobExecution(theJobExecutionId);
|
||||
awaitJobCompletion(jobExecution);
|
||||
|
|
|
@ -49,6 +49,11 @@ public class RemoteTerminologyServiceValidationSupport extends BaseValidationSup
|
|||
super(theFhirContext);
|
||||
}
|
||||
|
||||
public RemoteTerminologyServiceValidationSupport(FhirContext theFhirContext, String theBaseUrl) {
|
||||
super(theFhirContext);
|
||||
myBaseUrl = theBaseUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CodeValidationResult validateCode(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl) {
|
||||
return invokeRemoteValidateCode(theCodeSystem, theCode, theDisplay, theValueSetUrl, null);
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue