Merge branch 'master' into ja_20200206_multitenancy
This commit is contained in:
commit
d822867a4a
|
@ -0,0 +1,34 @@
|
|||
package ca.uhn.fhir.parser;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - Core Library
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2020 University Health Network
|
||||
* %%
|
||||
* 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%
|
||||
*/
|
||||
|
||||
class BaseErrorHandler {
|
||||
|
||||
String describeLocation(IParserErrorHandler.IParseLocation theLocation) {
|
||||
if (theLocation == null) {
|
||||
return "";
|
||||
} else {
|
||||
return theLocation.toString() + " ";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -58,6 +58,14 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
|||
@SuppressWarnings("WeakerAccess")
|
||||
public abstract class BaseParser implements IParser {
|
||||
|
||||
/**
|
||||
* Any resources that were created by the parser (i.e. by parsing a serialized resource) will have
|
||||
* a {@link IBaseResource#getUserData(String) user data} property with this key.
|
||||
*
|
||||
* @since 5.0.0
|
||||
*/
|
||||
public static final String RESOURCE_CREATED_BY_PARSER = BaseParser.class.getName() + "_" + "RESOURCE_CREATED_BY_PARSER";
|
||||
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseParser.class);
|
||||
|
||||
private static final Set<String> notEncodeForContainedResource = new HashSet<>(Arrays.asList("security", "versionId", "lastUpdated"));
|
||||
|
|
|
@ -174,7 +174,7 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
|
|||
theEventWriter.init();
|
||||
|
||||
RuntimeResourceDefinition resDef = myContext.getResourceDefinition(theResource);
|
||||
encodeResourceToJsonStreamWriter(resDef, theResource, theEventWriter, null, false, theEncodeContext);
|
||||
encodeResourceToJsonStreamWriter(resDef, theResource, theEventWriter, null, false, theEncodeContext);
|
||||
theEventWriter.flush();
|
||||
}
|
||||
|
||||
|
@ -258,9 +258,9 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
|
|||
break;
|
||||
} else if (valueObj instanceof Long) {
|
||||
if (theChildName != null) {
|
||||
theEventWriter.write(theChildName, (long)valueObj);
|
||||
theEventWriter.write(theChildName, (long) valueObj);
|
||||
} else {
|
||||
theEventWriter.write((long)valueObj);
|
||||
theEventWriter.write((long) valueObj);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -604,7 +604,7 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
|
|||
return maxCardinality > 1 || maxCardinality == Child.MAX_UNLIMITED;
|
||||
}
|
||||
|
||||
private void encodeCompositeElementToStreamWriter(RuntimeResourceDefinition theResDef, IBaseResource theResource, IBase theNextValue, JsonLikeWriter theEventWriter, boolean theContainedResource, CompositeChildElement theParent, EncodeContext theEncodeContext) throws IOException, DataFormatException {
|
||||
private void encodeCompositeElementToStreamWriter(RuntimeResourceDefinition theResDef, IBaseResource theResource, IBase theNextValue, JsonLikeWriter theEventWriter, boolean theContainedResource, CompositeChildElement theParent, EncodeContext theEncodeContext) throws IOException, DataFormatException {
|
||||
|
||||
writeCommentsPreAndPost(theNextValue, theEventWriter);
|
||||
encodeCompositeElementChildrenToStreamWriter(theResDef, theResource, theNextValue, theEventWriter, theContainedResource, theParent, theEncodeContext);
|
||||
|
@ -1387,10 +1387,6 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
|
|||
}
|
||||
}
|
||||
|
||||
private static void write(JsonLikeWriter theWriter, String theName, String theValue) throws IOException {
|
||||
theWriter.write(theName, theValue);
|
||||
}
|
||||
|
||||
private class HeldExtension implements Comparable<HeldExtension> {
|
||||
|
||||
private CompositeChildElement myChildElem;
|
||||
|
@ -1485,7 +1481,7 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
|
|||
extractAndWriteExtensionsAsDirectChild(myValue, theEventWriter, def, theResDef, theResource, myChildElem, null, theEncodeContext, theContainedResource);
|
||||
} else {
|
||||
String childName = myDef.getChildNameByDatatype(myValue.getClass());
|
||||
encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, myValue, def, childName, false, myParent, false, theEncodeContext);
|
||||
encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, myValue, def, childName, false, myParent, false, theEncodeContext);
|
||||
managePrimitiveExtension(myValue, theResDef, theResource, theEventWriter, def, childName, theEncodeContext, theContainedResource);
|
||||
}
|
||||
|
||||
|
@ -1562,7 +1558,7 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
|
|||
if (childDef == null) {
|
||||
throw new ConfigurationException("Unable to encode extension, unrecognized child element type: " + value.getClass().getCanonicalName());
|
||||
}
|
||||
encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, value, childDef, childName, false, myParent,false, theEncodeContext);
|
||||
encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, value, childDef, childName, false, myParent, false, theEncodeContext);
|
||||
managePrimitiveExtension(value, theResDef, theResource, theEventWriter, childDef, childName, theEncodeContext, theContainedResource);
|
||||
|
||||
theEncodeContext.popPath();
|
||||
|
@ -1574,4 +1570,8 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
|
|||
theEventWriter.endObject();
|
||||
}
|
||||
}
|
||||
|
||||
private static void write(JsonLikeWriter theWriter, String theName, String theValue) throws IOException {
|
||||
theWriter.write(theName, theValue);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ import ca.uhn.fhir.parser.json.JsonLikeValue.ValueType;
|
|||
* @see IParser#setParserErrorHandler(IParserErrorHandler)
|
||||
* @see FhirContext#setParserErrorHandler(IParserErrorHandler)
|
||||
*/
|
||||
public class LenientErrorHandler implements IParserErrorHandler {
|
||||
public class LenientErrorHandler extends BaseErrorHandler implements IParserErrorHandler {
|
||||
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(LenientErrorHandler.class);
|
||||
private static final StrictErrorHandler STRICT_ERROR_HANDLER = new StrictErrorHandler();
|
||||
|
@ -84,7 +84,7 @@ public class LenientErrorHandler implements IParserErrorHandler {
|
|||
public void invalidValue(IParseLocation theLocation, String theValue, String theError) {
|
||||
if (isBlank(theValue) || myErrorOnInvalidValue == false) {
|
||||
if (myLogErrors) {
|
||||
ourLog.warn("Invalid attribute value \"{}\": {}", theValue, theError);
|
||||
ourLog.warn("{}Invalid attribute value \"{}\": {}", describeLocation(theLocation), theValue, theError);
|
||||
}
|
||||
} else {
|
||||
STRICT_ERROR_HANDLER.invalidValue(theLocation, theValue, theError);
|
||||
|
@ -133,35 +133,35 @@ public class LenientErrorHandler implements IParserErrorHandler {
|
|||
@Override
|
||||
public void unexpectedRepeatingElement(IParseLocation theLocation, String theElementName) {
|
||||
if (myLogErrors) {
|
||||
ourLog.warn("Multiple repetitions of non-repeatable element '{}' found while parsing", theElementName);
|
||||
ourLog.warn("{}Multiple repetitions of non-repeatable element '{}' found while parsing", describeLocation(theLocation), theElementName);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unknownAttribute(IParseLocation theLocation, String theElementName) {
|
||||
if (myLogErrors) {
|
||||
ourLog.warn("Unknown attribute '{}' found while parsing", theElementName);
|
||||
ourLog.warn("{}Unknown attribute '{}' found while parsing",describeLocation(theLocation), theElementName);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unknownElement(IParseLocation theLocation, String theElementName) {
|
||||
if (myLogErrors) {
|
||||
ourLog.warn("Unknown element '{}' found while parsing", theElementName);
|
||||
ourLog.warn("{}Unknown element '{}' found while parsing", describeLocation(theLocation), theElementName);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unknownReference(IParseLocation theLocation, String theReference) {
|
||||
if (myLogErrors) {
|
||||
ourLog.warn("Resource has invalid reference: {}", theReference);
|
||||
ourLog.warn("{}Resource has invalid reference: {}", describeLocation(theLocation), theReference);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void extensionContainsValueAndNestedExtensions(IParseLocation theLocation) {
|
||||
if (myLogErrors) {
|
||||
ourLog.warn("Extension contains both a value and nested extensions: {}", theLocation);
|
||||
ourLog.warn("{}Extension contains both a value and nested extensions", describeLocation(theLocation));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -54,6 +54,13 @@ class ParseLocation implements IParseLocation {
|
|||
|
||||
@Override
|
||||
public String toString() {
|
||||
return defaultString(myParentElementName);
|
||||
return "[element=\"" + defaultString(myParentElementName) + "\"]";
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method
|
||||
*/
|
||||
static ParseLocation fromElementName(String theChildName) {
|
||||
return new ParseLocation(theChildName);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@ import ca.uhn.fhir.util.ReflectionUtil;
|
|||
import ca.uhn.fhir.util.XmlUtil;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.apache.commons.lang3.math.NumberUtils;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.hl7.fhir.instance.model.api.*;
|
||||
|
||||
|
@ -191,7 +192,7 @@ class ParserState<T> {
|
|||
return thePrimitiveTarget.newInstance(theDefinition.getInstanceConstructorArguments());
|
||||
}
|
||||
|
||||
public IPrimitiveType<?> getPrimitiveInstance(BaseRuntimeChildDefinition theChild, RuntimePrimitiveDatatypeDefinition thePrimitiveTarget) {
|
||||
public IPrimitiveType<?> getPrimitiveInstance(BaseRuntimeChildDefinition theChild, RuntimePrimitiveDatatypeDefinition thePrimitiveTarget, String theChildName) {
|
||||
return thePrimitiveTarget.newInstance(theChild.getInstanceConstructorArguments());
|
||||
}
|
||||
|
||||
|
@ -456,7 +457,7 @@ class ParserState<T> {
|
|||
RuntimePrimitiveDatatypeDefinition primitiveTarget = (RuntimePrimitiveDatatypeDefinition) target;
|
||||
IPrimitiveType<?> newChildInstance = newPrimitiveInstance(myDefinition, primitiveTarget);
|
||||
myDefinition.getMutator().addValue(myParentInstance, newChildInstance);
|
||||
PrimitiveState newState = new PrimitiveState(getPreResourceState(), newChildInstance);
|
||||
PrimitiveState newState = new PrimitiveState(getPreResourceState(), newChildInstance, theLocalPart, primitiveTarget.getName());
|
||||
push(newState);
|
||||
return;
|
||||
}
|
||||
|
@ -495,10 +496,10 @@ class ParserState<T> {
|
|||
|
||||
private class ElementCompositeState extends BaseState {
|
||||
|
||||
private BaseRuntimeElementCompositeDefinition<?> myDefinition;
|
||||
private IBase myInstance;
|
||||
private Set<String> myParsedNonRepeatableNames = new HashSet<>();
|
||||
private String myElementName;
|
||||
private final BaseRuntimeElementCompositeDefinition<?> myDefinition;
|
||||
private final IBase myInstance;
|
||||
private final Set<String> myParsedNonRepeatableNames = new HashSet<>();
|
||||
private final String myElementName;
|
||||
|
||||
ElementCompositeState(PreResourceState thePreResourceState, String theElementName, BaseRuntimeElementCompositeDefinition<?> theDef, IBase theInstance) {
|
||||
super(thePreResourceState);
|
||||
|
@ -583,9 +584,9 @@ class ParserState<T> {
|
|||
case PRIMITIVE_DATATYPE: {
|
||||
RuntimePrimitiveDatatypeDefinition primitiveTarget = (RuntimePrimitiveDatatypeDefinition) target;
|
||||
IPrimitiveType<?> newChildInstance;
|
||||
newChildInstance = getPrimitiveInstance(child, primitiveTarget);
|
||||
newChildInstance = getPrimitiveInstance(child, primitiveTarget, theChildName);
|
||||
child.getMutator().addValue(myInstance, newChildInstance);
|
||||
PrimitiveState newState = new PrimitiveState(getPreResourceState(), newChildInstance);
|
||||
PrimitiveState newState = new PrimitiveState(getPreResourceState(), newChildInstance, theChildName, primitiveTarget.getName());
|
||||
push(newState);
|
||||
return;
|
||||
}
|
||||
|
@ -668,7 +669,7 @@ class ParserState<T> {
|
|||
|
||||
public class ElementIdState extends BaseState {
|
||||
|
||||
private IBaseElement myElement;
|
||||
private final IBaseElement myElement;
|
||||
|
||||
ElementIdState(ParserState<T>.PreResourceState thePreResourceState, IBaseElement theElement) {
|
||||
super(thePreResourceState);
|
||||
|
@ -689,7 +690,7 @@ class ParserState<T> {
|
|||
|
||||
private class ExtensionState extends BaseState {
|
||||
|
||||
private IBaseExtension<?, ?> myExtension;
|
||||
private final IBaseExtension<?, ?> myExtension;
|
||||
|
||||
ExtensionState(PreResourceState thePreResourceState, IBaseExtension<?, ?> theExtension) {
|
||||
super(thePreResourceState);
|
||||
|
@ -752,7 +753,7 @@ class ParserState<T> {
|
|||
RuntimePrimitiveDatatypeDefinition primitiveTarget = (RuntimePrimitiveDatatypeDefinition) target;
|
||||
IPrimitiveType<?> newChildInstance = newInstance(primitiveTarget);
|
||||
myExtension.setValue(newChildInstance);
|
||||
PrimitiveState newState = new PrimitiveState(getPreResourceState(), newChildInstance);
|
||||
PrimitiveState newState = new PrimitiveState(getPreResourceState(), newChildInstance, theLocalPart, primitiveTarget.getName());
|
||||
push(newState);
|
||||
return;
|
||||
}
|
||||
|
@ -782,7 +783,7 @@ class ParserState<T> {
|
|||
|
||||
public class IdentifiableElementIdState extends BaseState {
|
||||
|
||||
private IIdentifiableElement myElement;
|
||||
private final IIdentifiableElement myElement;
|
||||
|
||||
public IdentifiableElementIdState(ParserState<T>.PreResourceState thePreResourceState, IIdentifiableElement theElement) {
|
||||
super(thePreResourceState);
|
||||
|
@ -802,7 +803,7 @@ class ParserState<T> {
|
|||
}
|
||||
|
||||
private class MetaElementState extends BaseState {
|
||||
private ResourceMetadataMap myMap;
|
||||
private final ResourceMetadataMap myMap;
|
||||
|
||||
public MetaElementState(ParserState<T>.PreResourceState thePreResourceState, ResourceMetadataMap theMap) {
|
||||
super(thePreResourceState);
|
||||
|
@ -824,7 +825,7 @@ class ParserState<T> {
|
|||
break;
|
||||
case "lastUpdated":
|
||||
InstantDt updated = new InstantDt();
|
||||
push(new PrimitiveState(getPreResourceState(), updated));
|
||||
push(new PrimitiveState(getPreResourceState(), updated, theLocalPart, "instant"));
|
||||
myMap.put(ResourceMetadataKeyEnum.UPDATED, updated);
|
||||
break;
|
||||
case "security":
|
||||
|
@ -850,7 +851,7 @@ class ParserState<T> {
|
|||
newProfiles = new ArrayList<>(1);
|
||||
}
|
||||
IdDt profile = new IdDt();
|
||||
push(new PrimitiveState(getPreResourceState(), profile));
|
||||
push(new PrimitiveState(getPreResourceState(), profile, theLocalPart, "id"));
|
||||
newProfiles.add(profile);
|
||||
myMap.put(ResourceMetadataKeyEnum.PROFILES, Collections.unmodifiableList(newProfiles));
|
||||
break;
|
||||
|
@ -891,7 +892,7 @@ class ParserState<T> {
|
|||
|
||||
private class MetaVersionElementState extends BaseState {
|
||||
|
||||
private ResourceMetadataMap myMap;
|
||||
private final ResourceMetadataMap myMap;
|
||||
|
||||
MetaVersionElementState(ParserState<T>.PreResourceState thePreResourceState, ResourceMetadataMap theMap) {
|
||||
super(thePreResourceState);
|
||||
|
@ -1048,6 +1049,8 @@ class ParserState<T> {
|
|||
}
|
||||
}
|
||||
|
||||
myInstance.setUserData(BaseParser.RESOURCE_CREATED_BY_PARSER, Boolean.TRUE);
|
||||
|
||||
populateTarget();
|
||||
}
|
||||
|
||||
|
@ -1268,37 +1271,51 @@ class ParserState<T> {
|
|||
}
|
||||
|
||||
private class PrimitiveState extends BaseState {
|
||||
private final String myChildName;
|
||||
private final String myTypeName;
|
||||
private IPrimitiveType<?> myInstance;
|
||||
|
||||
PrimitiveState(PreResourceState thePreResourceState, IPrimitiveType<?> theInstance) {
|
||||
PrimitiveState(PreResourceState thePreResourceState, IPrimitiveType<?> theInstance, String theChildName, String theTypeName) {
|
||||
super(thePreResourceState);
|
||||
myInstance = theInstance;
|
||||
myChildName = theChildName;
|
||||
myTypeName = theTypeName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void attributeValue(String theName, String theValue) throws DataFormatException {
|
||||
String value = theValue;
|
||||
if ("value".equals(theName)) {
|
||||
if ("".equals(theValue)) {
|
||||
myErrorHandler.invalidValue(null, theValue, "Attribute values must not be empty (\"\")");
|
||||
if ("".equals(value)) {
|
||||
ParseLocation location = ParseLocation.fromElementName(myChildName);
|
||||
myErrorHandler.invalidValue(location, value, "Attribute value must not be empty (\"\")");
|
||||
} else {
|
||||
if ("decimal".equals(myTypeName)) {
|
||||
if (value != null && value.startsWith(".") && NumberUtils.isDigits(value.substring(1))) {
|
||||
value = "0" + value;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
myInstance.setValueAsString(theValue);
|
||||
myInstance.setValueAsString(value);
|
||||
} catch (DataFormatException | IllegalArgumentException e) {
|
||||
myErrorHandler.invalidValue(null, theValue, e.getMessage());
|
||||
ParseLocation location = ParseLocation.fromElementName(myChildName);
|
||||
myErrorHandler.invalidValue(location, value, e.getMessage());
|
||||
}
|
||||
}
|
||||
} else if ("id".equals(theName)) {
|
||||
if (myInstance instanceof IIdentifiableElement) {
|
||||
((IIdentifiableElement) myInstance).setElementSpecificId(theValue);
|
||||
((IIdentifiableElement) myInstance).setElementSpecificId(value);
|
||||
} else if (myInstance instanceof IBaseElement) {
|
||||
((IBaseElement) myInstance).setId(theValue);
|
||||
((IBaseElement) myInstance).setId(value);
|
||||
} else if (myInstance instanceof IBaseResource) {
|
||||
new IdDt(theValue).applyTo((org.hl7.fhir.instance.model.api.IBaseResource) myInstance);
|
||||
new IdDt(value).applyTo((org.hl7.fhir.instance.model.api.IBaseResource) myInstance);
|
||||
} else {
|
||||
myErrorHandler.unknownAttribute(null, theName);
|
||||
ParseLocation location = ParseLocation.fromElementName(myChildName);
|
||||
myErrorHandler.unknownAttribute(location, theName);
|
||||
}
|
||||
} else {
|
||||
super.attributeValue(theName, theValue);
|
||||
super.attributeValue(theName, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1343,7 +1360,7 @@ class ParserState<T> {
|
|||
@Override
|
||||
public void enteringNewElement(String theNamespace, String theChildName) throws DataFormatException {
|
||||
if ("id".equals(theChildName)) {
|
||||
push(new PrimitiveState(getPreResourceState(), myInstance.getId()));
|
||||
push(new PrimitiveState(getPreResourceState(), myInstance.getId(), theChildName, "id"));
|
||||
} else if ("meta".equals(theChildName)) {
|
||||
push(new MetaElementState(getPreResourceState(), myInstance.getResourceMetadata()));
|
||||
} else {
|
||||
|
|
|
@ -3,6 +3,7 @@ package ca.uhn.fhir.parser;
|
|||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.parser.json.JsonLikeValue.ScalarType;
|
||||
import ca.uhn.fhir.parser.json.JsonLikeValue.ValueType;
|
||||
import ca.uhn.fhir.util.UrlUtil;
|
||||
|
||||
/*
|
||||
* #%L
|
||||
|
@ -31,7 +32,7 @@ import ca.uhn.fhir.parser.json.JsonLikeValue.ValueType;
|
|||
* @see IParser#setParserErrorHandler(IParserErrorHandler)
|
||||
* @see FhirContext#setParserErrorHandler(IParserErrorHandler)
|
||||
*/
|
||||
public class StrictErrorHandler implements IParserErrorHandler {
|
||||
public class StrictErrorHandler extends BaseErrorHandler implements IParserErrorHandler {
|
||||
|
||||
@Override
|
||||
public void containedResourceWithNoId(IParseLocation theLocation) {
|
||||
|
@ -46,7 +47,7 @@ public class StrictErrorHandler implements IParserErrorHandler {
|
|||
|
||||
@Override
|
||||
public void invalidValue(IParseLocation theLocation, String theValue, String theError) {
|
||||
throw new DataFormatException("Invalid attribute value \"" + theValue + "\": " + theError);
|
||||
throw new DataFormatException(describeLocation(theLocation) + "Invalid attribute value \"" + UrlUtil.sanitizeUrlPart(theValue) + "\": " + theError);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -65,27 +66,27 @@ public class StrictErrorHandler implements IParserErrorHandler {
|
|||
|
||||
@Override
|
||||
public void unexpectedRepeatingElement(IParseLocation theLocation, String theElementName) {
|
||||
throw new DataFormatException("Multiple repetitions of non-repeatable element '" + theElementName + "' found during parse");
|
||||
throw new DataFormatException(describeLocation(theLocation) + "Multiple repetitions of non-repeatable element '" + theElementName + "' found during parse");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unknownAttribute(IParseLocation theLocation, String theAttributeName) {
|
||||
throw new DataFormatException("Unknown attribute '" + theAttributeName + "' found during parse");
|
||||
throw new DataFormatException(describeLocation(theLocation) + "Unknown attribute '" + theAttributeName + "' found during parse");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unknownElement(IParseLocation theLocation, String theElementName) {
|
||||
throw new DataFormatException("Unknown element '" + theElementName + "' found during parse");
|
||||
throw new DataFormatException(describeLocation(theLocation) + "Unknown element '" + theElementName + "' found during parse");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unknownReference(IParseLocation theLocation, String theReference) {
|
||||
throw new DataFormatException("Resource has invalid reference: " + theReference);
|
||||
throw new DataFormatException(describeLocation(theLocation) + "Resource has invalid reference: " + theReference);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void extensionContainsValueAndNestedExtensions(IParseLocation theLocation) {
|
||||
throw new DataFormatException("Extension contains both a value and nested extensions: " + theLocation);
|
||||
throw new DataFormatException(describeLocation(theLocation) + "Extension contains both a value and nested extensions");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -35,7 +35,6 @@ import com.fasterxml.jackson.databind.node.ArrayNode;
|
|||
import com.fasterxml.jackson.databind.node.DecimalNode;
|
||||
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import org.apache.jena.tdb.setup.BuilderStdDB;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PushbackReader;
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
type: add
|
||||
issue: 1793
|
||||
title: "When parsing JSON resources, if an element contains an invalid value, the Parser Error Handler
|
||||
did not have access to the actual name of the element being parsed. This meant that errors lacked useful
|
||||
detail in order to diagnose the issue. This has been corrected. Thanks to GitHub user
|
||||
@jwalter for reporting!"
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
type: fix
|
||||
issue: 1801
|
||||
title: "When invoking JPA DAO methods programatically to store a resource (as opposed to using the FHIR REST API), if
|
||||
the resource being stored had any contained resources, these would sometimes not be visible to the search parameter
|
||||
indexer, leading to missing search params. This is a very fringe use case, but a workaround has been put in place to
|
||||
solve it."
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
type: fix
|
||||
issue: 1801
|
||||
title: "In previous versions of HAPI FHIR, the server incorrectly silently accepted decimal numbers in JSON with no
|
||||
leading numbers (e.g. `.123`). These will now be rejected by the JSON parser. Any values with this string that
|
||||
have previously been stored in the JPA server database will now automatically convert the value to `0.123`."
|
|
@ -17,6 +17,7 @@ import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperService;
|
|||
import ca.uhn.fhir.jpa.partition.PartitionConfigSvcImpl;
|
||||
import ca.uhn.fhir.jpa.partition.PartitionManagementProvider;
|
||||
import ca.uhn.fhir.jpa.partition.RequestPartitionHelperService;
|
||||
import ca.uhn.fhir.jpa.dao.index.DaoResourceLinkResolver;
|
||||
import ca.uhn.fhir.jpa.entity.Search;
|
||||
import ca.uhn.fhir.jpa.graphql.JpaStorageServices;
|
||||
import ca.uhn.fhir.jpa.interceptor.JpaConsentContextServices;
|
||||
|
@ -40,6 +41,9 @@ import ca.uhn.fhir.jpa.search.cache.ISearchResultCacheSvc;
|
|||
import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc;
|
||||
import ca.uhn.fhir.jpa.search.reindex.ResourceReindexingSvcImpl;
|
||||
import ca.uhn.fhir.jpa.searchparam.config.SearchParamConfig;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.IResourceLinkResolver;
|
||||
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
|
||||
import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryImpl;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.server.interceptor.consent.IConsentContextServices;
|
||||
import org.hibernate.jpa.HibernatePersistenceProvider;
|
||||
|
@ -51,6 +55,7 @@ import org.springframework.context.annotation.Configuration;
|
|||
import org.springframework.context.annotation.FilterType;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.task.AsyncTaskExecutor;
|
||||
|
@ -163,6 +168,12 @@ public abstract class BaseConfig {
|
|||
return new BinaryStorageInterceptor();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public IResourceLinkResolver daoResourceLinkResolver() {
|
||||
return new DaoResourceLinkResolver();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ISearchCacheSvc searchCacheSvc() {
|
||||
return new DatabaseSearchCacheSvcImpl();
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
package ca.uhn.fhir.jpa.dao;
|
||||
|
||||
import ca.uhn.fhir.context.*;
|
||||
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
|
||||
import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
|
||||
import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.FhirVersionEnum;
|
||||
import ca.uhn.fhir.context.RuntimeChildResourceDefinition;
|
||||
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||
import ca.uhn.fhir.interceptor.api.HookParams;
|
||||
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
|
||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||
|
@ -8,7 +15,12 @@ import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
|||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.fhir.jpa.api.dao.IDao;
|
||||
import ca.uhn.fhir.jpa.api.dao.IJpaDao;
|
||||
import ca.uhn.fhir.jpa.dao.data.*;
|
||||
import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc;
|
||||
import ca.uhn.fhir.jpa.dao.data.IForcedIdDao;
|
||||
import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao;
|
||||
import ca.uhn.fhir.jpa.dao.data.IResourceProvenanceDao;
|
||||
import ca.uhn.fhir.jpa.dao.data.IResourceTableDao;
|
||||
import ca.uhn.fhir.jpa.dao.data.IResourceTagDao;
|
||||
import ca.uhn.fhir.jpa.dao.expunge.ExpungeService;
|
||||
import ca.uhn.fhir.jpa.dao.index.DaoSearchParamSynchronizer;
|
||||
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
|
||||
|
@ -23,7 +35,6 @@ import ca.uhn.fhir.jpa.model.entity.*;
|
|||
import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
|
||||
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
|
||||
import ca.uhn.fhir.jpa.model.util.JpaConstants;
|
||||
import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc;
|
||||
import ca.uhn.fhir.jpa.search.PersistedJpaBundleProviderFactory;
|
||||
import ca.uhn.fhir.jpa.search.cache.ISearchCacheSvc;
|
||||
import ca.uhn.fhir.jpa.searchparam.ResourceMetaParams;
|
||||
|
@ -865,8 +876,9 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
|
|||
// 4. parse the text to FHIR
|
||||
R retVal;
|
||||
if (resourceEncoding != ResourceEncodingEnum.DEL) {
|
||||
IParser parser = resourceEncoding.newParser(getContext(theEntity.getFhirVersion()));
|
||||
parser.setParserErrorHandler(new LenientErrorHandler(false).setErrorOnInvalidValue(false));
|
||||
|
||||
LenientErrorHandler errorHandler = new LenientErrorHandler(false).setErrorOnInvalidValue(false);
|
||||
IParser parser = new TolerantJsonParser(getContext(theEntity.getFhirVersion()), errorHandler);
|
||||
|
||||
try {
|
||||
retVal = parser.parseResource(resourceType, resourceText);
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
package ca.uhn.fhir.jpa.dao;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.parser.DataFormatException;
|
||||
import ca.uhn.fhir.parser.IParserErrorHandler;
|
||||
import ca.uhn.fhir.parser.JsonParser;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.defaultString;
|
||||
|
||||
class TolerantJsonParser extends JsonParser {
|
||||
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(TolerantJsonParser.class);
|
||||
|
||||
TolerantJsonParser(FhirContext theContext, IParserErrorHandler theParserErrorHandler) {
|
||||
super(theContext, theParserErrorHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends IBaseResource> T parseResource(Class<T> theResourceType, String theMessageString) {
|
||||
try {
|
||||
return super.parseResource(theResourceType, theMessageString);
|
||||
} catch (DataFormatException e) {
|
||||
if (defaultString(e.getMessage()).contains("Unexpected character ('.' (code 46))")) {
|
||||
|
||||
/*
|
||||
* The following is a hacky and gross workaround until the following PR is hopefully merged:
|
||||
* https://github.com/FasterXML/jackson-core/pull/611
|
||||
*
|
||||
* The issue this solves is that under Gson it was possible to store JSON containing
|
||||
* decimal numbers with no leading integer, e.g. .123
|
||||
*
|
||||
* These don't parse in Jackson, meaning we can be stuck with data in the database
|
||||
* that can't be loaded back out.
|
||||
*
|
||||
* Note that if we fix this in the future to rely on Jackson natively handing this
|
||||
* nicely we may or may not be able to remove some code from
|
||||
* ParserState.Primitive state too.
|
||||
*/
|
||||
|
||||
Gson gson = new Gson();
|
||||
|
||||
JsonObject object = gson.fromJson(theMessageString, JsonObject.class);
|
||||
String corrected = gson.toJson(object);
|
||||
|
||||
return super.parseResource(theResourceType, corrected);
|
||||
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -50,7 +50,6 @@ import javax.persistence.PersistenceContext;
|
|||
import javax.persistence.PersistenceContextType;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
public class DaoResourceLinkResolver implements IResourceLinkResolver {
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(DaoResourceLinkResolver.class);
|
||||
@PersistenceContext(type = PersistenceContextType.TRANSACTION)
|
||||
|
|
|
@ -23,8 +23,8 @@ package ca.uhn.fhir.jpa.dao.index;
|
|||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
|
||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
|
||||
import ca.uhn.fhir.jpa.dao.MatchResourceUrlService;
|
||||
import ca.uhn.fhir.jpa.dao.data.IResourceIndexedCompositeStringUniqueDao;
|
||||
import ca.uhn.fhir.jpa.model.config.PartitionConfig;
|
||||
|
@ -35,7 +35,6 @@ import ca.uhn.fhir.jpa.model.entity.ResourceLink;
|
|||
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
||||
import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.ResourceLinkExtractor;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorService;
|
||||
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
|
||||
import ca.uhn.fhir.model.api.IQueryParameterType;
|
||||
|
@ -85,8 +84,6 @@ public class SearchParamWithInlineReferencesExtractor {
|
|||
@Autowired
|
||||
private SearchParamExtractorService mySearchParamExtractorService;
|
||||
@Autowired
|
||||
private ResourceLinkExtractor myResourceLinkExtractor;
|
||||
@Autowired
|
||||
private DaoResourceLinkResolver myDaoResourceLinkResolver;
|
||||
@Autowired
|
||||
private DaoSearchParamSynchronizer myDaoSearchParamSynchronizer;
|
||||
|
@ -97,19 +94,15 @@ public class SearchParamWithInlineReferencesExtractor {
|
|||
protected EntityManager myEntityManager;
|
||||
|
||||
public void populateFromResource(PartitionConfig thePartitionConfig, ResourceIndexedSearchParams theParams, Date theUpdateTime, ResourceTable theEntity, IBaseResource theResource, ResourceIndexedSearchParams theExistingParams, RequestDetails theRequest) {
|
||||
mySearchParamExtractorService.extractFromResource(theRequest, theParams, theEntity, theResource);
|
||||
extractInlineReferences(theResource, theRequest);
|
||||
|
||||
mySearchParamExtractorService.extractFromResource(theRequest, theParams, theEntity, theResource, theUpdateTime, true);
|
||||
|
||||
Set<Map.Entry<String, RuntimeSearchParam>> activeSearchParams = mySearchParamRegistry.getActiveSearchParams(theEntity.getResourceType()).entrySet();
|
||||
if (myDaoConfig.getIndexMissingFields() == DaoConfig.IndexEnabledEnum.ENABLED) {
|
||||
theParams.findMissingSearchParams(thePartitionConfig, myDaoConfig.getModelConfig(), theEntity, activeSearchParams);
|
||||
}
|
||||
|
||||
theParams.setUpdatedTime(theUpdateTime);
|
||||
|
||||
extractInlineReferences(theResource, theRequest);
|
||||
|
||||
myResourceLinkExtractor.extractResourceLinks(theParams, theEntity, theResource, theUpdateTime, myDaoResourceLinkResolver, true, theRequest);
|
||||
|
||||
/*
|
||||
* If the existing resource already has links and those match links we still want, use them instead of removing them and re adding them
|
||||
*/
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
package ca.uhn.fhir.jpa.util.xmlpatch;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.rest.api.Constants;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||
import com.github.dnault.xmlpatch.Patcher;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
||||
import com.github.dnault.xmlpatch.Patcher;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/*
|
||||
* #%L
|
||||
|
@ -28,9 +30,6 @@ import ca.uhn.fhir.context.FhirContext;
|
|||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.rest.api.Constants;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||
|
||||
public class XmlPatchUtils {
|
||||
|
||||
public static <T extends IBaseResource> T apply(FhirContext theCtx, T theResourceToUpdate, String thePatchBody) {
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
package ca.uhn.fhir.jpa.dao;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.parser.DataFormatException;
|
||||
import ca.uhn.fhir.parser.LenientErrorHandler;
|
||||
import org.hl7.fhir.r4.model.Observation;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class TolerantJsonParserR4Test {
|
||||
|
||||
private FhirContext myFhirContext = FhirContext.forR4();
|
||||
|
||||
@Test
|
||||
public void testParseInvalidNumeric() {
|
||||
String input = "{\n" +
|
||||
"\"resourceType\": \"Observation\",\n" +
|
||||
"\"valueQuantity\": {\n" +
|
||||
" \"value\": .5\n" +
|
||||
" }\n" +
|
||||
"}";
|
||||
|
||||
|
||||
TolerantJsonParser parser = new TolerantJsonParser(myFhirContext, new LenientErrorHandler());
|
||||
Observation obs = parser.parseResource(Observation.class, input);
|
||||
|
||||
assertEquals("0.5", obs.getValueQuantity().getValueElement().getValueAsString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseInvalidNumeric2() {
|
||||
String input = "{\n" +
|
||||
"\"resourceType\": \"Observation\",\n" +
|
||||
"\"valueQuantity\": {\n" +
|
||||
" \"value\": .\n" +
|
||||
" }\n" +
|
||||
"}";
|
||||
|
||||
|
||||
TolerantJsonParser parser = new TolerantJsonParser(myFhirContext, new LenientErrorHandler());
|
||||
try {
|
||||
parser.parseResource(Observation.class, input);
|
||||
} catch (DataFormatException e) {
|
||||
assertEquals("[element=\"value\"] Invalid attribute value \".\": No digits found.", e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -218,6 +218,8 @@ public abstract class BaseJpaDstu3Test extends BaseJpaTest {
|
|||
@Autowired
|
||||
protected IResourceIndexedSearchParamStringDao myResourceIndexedSearchParamStringDao;
|
||||
@Autowired
|
||||
protected IResourceIndexedSearchParamTokenDao myResourceIndexedSearchParamTokenDao;
|
||||
@Autowired
|
||||
protected IResourceTableDao myResourceTableDao;
|
||||
@Autowired
|
||||
protected IResourceTagDao myResourceTagDao;
|
||||
|
@ -368,8 +370,12 @@ public abstract class BaseJpaDstu3Test extends BaseJpaTest {
|
|||
|
||||
@AfterClass
|
||||
public static void afterClassClearContextBaseJpaDstu3Test() {
|
||||
ourValueSetDao.purgeCaches();
|
||||
ourJpaValidationSupportChainDstu3.invalidateCaches();
|
||||
if (ourValueSetDao != null) {
|
||||
ourValueSetDao.purgeCaches();
|
||||
}
|
||||
if (ourJpaValidationSupportChainDstu3 != null) {
|
||||
ourJpaValidationSupportChainDstu3.invalidateCaches();
|
||||
}
|
||||
TestUtil.clearAllStaticFieldsForUnitTest();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package ca.uhn.fhir.jpa.dao.dstu3;
|
||||
|
||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.model.api.Include;
|
||||
import ca.uhn.fhir.parser.IParser;
|
||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||
import ca.uhn.fhir.rest.param.*;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||
|
@ -20,6 +22,7 @@ import org.junit.Before;
|
|||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
@ -1054,6 +1057,48 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu
|
|||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProgramaticallyContainedByReferenceAreStillResolvable() {
|
||||
SearchParameter sp = new SearchParameter();
|
||||
sp.setUrl("http://hapifhir.io/fhir/StructureDefinition/sp-unique");
|
||||
sp.setName("MEDICATIONADMINISTRATION-INGREDIENT-MEDICATION");
|
||||
sp.setStatus(Enumerations.PublicationStatus.ACTIVE);
|
||||
sp.setCode("medicationadministration-ingredient-medication");
|
||||
sp.addBase("MedicationAdministration");
|
||||
sp.setType(Enumerations.SearchParamType.TOKEN);
|
||||
sp.setExpression("MedicationAdministration.medication.resolve().ingredient.item.as(Reference).resolve().code");
|
||||
mySearchParameterDao.create(sp);
|
||||
mySearchParamRegistry.forceRefresh();
|
||||
|
||||
Medication ingredient = new Medication();
|
||||
ingredient.getCode().addCoding().setSystem("system").setCode("code");
|
||||
|
||||
Medication medication = new Medication();
|
||||
medication.addIngredient().setItem(new Reference(ingredient));
|
||||
|
||||
MedicationAdministration medAdmin = new MedicationAdministration();
|
||||
medAdmin.setMedication(new Reference(medication));
|
||||
|
||||
myMedicationAdministrationDao.create(medAdmin);
|
||||
|
||||
runInTransaction(()->{
|
||||
List<ResourceIndexedSearchParamToken> tokens = myResourceIndexedSearchParamTokenDao
|
||||
.findAll()
|
||||
.stream()
|
||||
.filter(t -> t.getParamName().equals("medicationadministration-ingredient-medication"))
|
||||
.collect(Collectors.toList());
|
||||
ourLog.info("Tokens: {}", tokens);
|
||||
assertEquals(tokens.toString(), 1, tokens.size());
|
||||
|
||||
});
|
||||
|
||||
SearchParameterMap map = new SearchParameterMap();
|
||||
map.add("medicationadministration-ingredient-medication", new TokenParam("system","code"));
|
||||
assertEquals(1, myMedicationAdministrationDao.search(map).size().intValue());
|
||||
|
||||
}
|
||||
|
||||
|
||||
@AfterClass
|
||||
public static void afterClassClearContext() {
|
||||
TestUtil.clearAllStaticFieldsForUnitTest();
|
||||
|
|
|
@ -253,6 +253,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test
|
|||
|
||||
MessageHeader messageHeader = new MessageHeader();
|
||||
messageHeader.setId("123");
|
||||
messageHeader.setDefinition("Hello");
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.setType(Bundle.BundleType.MESSAGE);
|
||||
bundle.addEntry()
|
||||
|
|
|
@ -7,6 +7,7 @@ import ca.uhn.fhir.jpa.model.util.JpaConstants;
|
|||
import ca.uhn.fhir.jpa.model.util.ProviderConstants;
|
||||
import ca.uhn.fhir.jpa.provider.dstu3.BaseResourceProviderDstu3Test;
|
||||
import ca.uhn.fhir.jpa.subscription.SubscriptionTestUtil;
|
||||
import ca.uhn.fhir.jpa.subscription.triggering.ISubscriptionTriggeringSvc;
|
||||
import ca.uhn.fhir.jpa.subscription.triggering.SubscriptionTriggeringSvcImpl;
|
||||
import ca.uhn.fhir.rest.annotation.Create;
|
||||
import ca.uhn.fhir.rest.annotation.ResourceParam;
|
||||
|
@ -17,6 +18,7 @@ import ca.uhn.fhir.rest.server.IResourceProvider;
|
|||
import ca.uhn.fhir.rest.server.RestfulServer;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import ca.uhn.fhir.test.utilities.JettyUtil;
|
||||
import ca.uhn.fhir.test.utilities.ProxyUtil;
|
||||
import com.google.common.collect.Lists;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||
|
@ -71,7 +73,7 @@ public class SubscriptionTriggeringDstu3Test extends BaseResourceProviderDstu3Te
|
|||
@Autowired
|
||||
private SubscriptionTestUtil mySubscriptionTestUtil;
|
||||
@Autowired
|
||||
private SubscriptionTriggeringSvcImpl mySubscriptionTriggeringSvc;
|
||||
private ISubscriptionTriggeringSvc mySubscriptionTriggeringSvc;
|
||||
@Autowired
|
||||
private ISchedulerService mySchedulerService;
|
||||
|
||||
|
@ -93,8 +95,9 @@ public class SubscriptionTriggeringDstu3Test extends BaseResourceProviderDstu3Te
|
|||
|
||||
mySubscriptionTestUtil.unregisterSubscriptionInterceptor();
|
||||
|
||||
mySubscriptionTriggeringSvc.cancelAll();
|
||||
mySubscriptionTriggeringSvc.setMaxSubmitPerPass(null);
|
||||
SubscriptionTriggeringSvcImpl svc = ProxyUtil.getSingletonTarget(mySubscriptionTriggeringSvc, SubscriptionTriggeringSvcImpl.class);
|
||||
svc.cancelAll();
|
||||
svc.setMaxSubmitPerPass(null);
|
||||
|
||||
myDaoConfig.setSearchPreFetchThresholds(new DaoConfig().getSearchPreFetchThresholds());
|
||||
}
|
||||
|
@ -224,7 +227,8 @@ public class SubscriptionTriggeringDstu3Test extends BaseResourceProviderDstu3Te
|
|||
waitForSize(50, ourUpdatedPatients);
|
||||
beforeReset();
|
||||
|
||||
mySubscriptionTriggeringSvc.setMaxSubmitPerPass(33);
|
||||
SubscriptionTriggeringSvcImpl svc = ProxyUtil.getSingletonTarget(mySubscriptionTriggeringSvc, SubscriptionTriggeringSvcImpl.class);
|
||||
svc.setMaxSubmitPerPass(33);
|
||||
|
||||
Parameters response = ourClient
|
||||
.operation()
|
||||
|
@ -294,7 +298,9 @@ public class SubscriptionTriggeringDstu3Test extends BaseResourceProviderDstu3Te
|
|||
mySubscriptionTriggeringSvc.runDeliveryPass();
|
||||
mySubscriptionTriggeringSvc.runDeliveryPass();
|
||||
mySubscriptionTriggeringSvc.runDeliveryPass();
|
||||
assertEquals(0, mySubscriptionTriggeringSvc.getActiveJobCount());
|
||||
|
||||
SubscriptionTriggeringSvcImpl svc = ProxyUtil.getSingletonTarget(mySubscriptionTriggeringSvc, SubscriptionTriggeringSvcImpl.class);
|
||||
assertEquals(0, svc.getActiveJobCount());
|
||||
|
||||
assertEquals(0, ourCreatedPatients.size());
|
||||
await().until(() -> ourUpdatedPatients.size() == 3);
|
||||
|
@ -361,7 +367,8 @@ public class SubscriptionTriggeringDstu3Test extends BaseResourceProviderDstu3Te
|
|||
waitForSize(50, ourUpdatedPatients);
|
||||
beforeReset();
|
||||
|
||||
mySubscriptionTriggeringSvc.setMaxSubmitPerPass(33);
|
||||
SubscriptionTriggeringSvcImpl svc = ProxyUtil.getSingletonTarget(mySubscriptionTriggeringSvc, SubscriptionTriggeringSvcImpl.class);
|
||||
svc.setMaxSubmitPerPass(33);
|
||||
|
||||
Parameters response = ourClient
|
||||
.operation()
|
||||
|
@ -427,7 +434,8 @@ public class SubscriptionTriggeringDstu3Test extends BaseResourceProviderDstu3Te
|
|||
waitForSize(0, ourUpdatedPatients);
|
||||
beforeReset();
|
||||
|
||||
mySubscriptionTriggeringSvc.setMaxSubmitPerPass(50);
|
||||
SubscriptionTriggeringSvcImpl svc = ProxyUtil.getSingletonTarget(mySubscriptionTriggeringSvc, SubscriptionTriggeringSvcImpl.class);
|
||||
svc.setMaxSubmitPerPass(50);
|
||||
|
||||
Parameters response = ourClient
|
||||
.operation()
|
||||
|
|
|
@ -22,9 +22,7 @@ package ca.uhn.fhir.jpa.searchparam.config;
|
|||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.IResourceLinkResolver;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.ResourceLinkExtractor;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorDstu2;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorDstu3;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorR4;
|
||||
|
@ -32,13 +30,11 @@ import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorR5;
|
|||
import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorService;
|
||||
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryResourceMatcher;
|
||||
import ca.uhn.fhir.jpa.searchparam.matcher.IndexedSearchParamExtractor;
|
||||
import ca.uhn.fhir.jpa.searchparam.matcher.InlineResourceLinkResolver;
|
||||
import ca.uhn.fhir.jpa.searchparam.matcher.SearchParamMatcher;
|
||||
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
|
||||
import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryImpl;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
@ -79,11 +75,6 @@ public class SearchParamConfig {
|
|||
return new MatchUrlService();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ResourceLinkExtractor resourceLinkExtractor() {
|
||||
return new ResourceLinkExtractor();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Lazy
|
||||
public SearchParamExtractorService searchParamExtractorService(){
|
||||
|
@ -95,11 +86,6 @@ public class SearchParamConfig {
|
|||
return new IndexedSearchParamExtractor();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public InlineResourceLinkResolver inlineResourceLinkResolver() {
|
||||
return new InlineResourceLinkResolver();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public InMemoryResourceMatcher InMemoryResourceMatcher() {
|
||||
return new InMemoryResourceMatcher();
|
||||
|
|
|
@ -1054,6 +1054,10 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
systemAsString = myUseSystem;
|
||||
}
|
||||
|
||||
if (value instanceof IIdType) {
|
||||
valueAsString = ((IIdType) value).getIdPart();
|
||||
}
|
||||
|
||||
BaseSearchParamExtractor.this.createTokenIndexIfNotBlank(myResourceTypeName, params, searchParam, systemAsString, valueAsString);
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -114,7 +114,7 @@ public final class ResourceIndexedSearchParams {
|
|||
theEntity.setHasLinks(myLinks.isEmpty() == false);
|
||||
}
|
||||
|
||||
public void setUpdatedTime(Date theUpdateTime) {
|
||||
void setUpdatedTime(Date theUpdateTime) {
|
||||
setUpdatedTime(myStringParams, theUpdateTime);
|
||||
setUpdatedTime(myNumberParams, theUpdateTime);
|
||||
setUpdatedTime(myQuantityParams, theUpdateTime);
|
||||
|
|
|
@ -1,202 +0,0 @@
|
|||
package ca.uhn.fhir.jpa.searchparam.extractor;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR Search Parameters
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2020 University Health Network
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
|
||||
import ca.uhn.fhir.jpa.model.config.PartitionConfig;
|
||||
import ca.uhn.fhir.jpa.model.cross.IResourceLookup;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
import ca.uhn.fhir.jpa.model.entity.PartitionId;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceLink;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
||||
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
|
||||
import ca.uhn.fhir.parser.DataFormatException;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.hl7.fhir.instance.model.api.IBaseReference;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
|
||||
public class ResourceLinkExtractor {
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResourceLinkExtractor.class);
|
||||
|
||||
@Autowired
|
||||
private ModelConfig myModelConfig;
|
||||
@Autowired
|
||||
private PartitionConfig myPartitionConfig;
|
||||
@Autowired
|
||||
private FhirContext myContext;
|
||||
@Autowired
|
||||
private ISearchParamRegistry mySearchParamRegistry;
|
||||
@Autowired
|
||||
private ISearchParamExtractor mySearchParamExtractor;
|
||||
@Autowired
|
||||
private IInterceptorBroadcaster myInterceptorBroadcaster;
|
||||
|
||||
public void extractResourceLinks(ResourceIndexedSearchParams theParams, ResourceTable theEntity, IBaseResource theResource, Date theUpdateTime, IResourceLinkResolver theResourceLinkResolver, boolean theFailOnInvalidReference, RequestDetails theRequest) {
|
||||
String resourceName = myContext.getResourceDefinition(theResource).getName();
|
||||
|
||||
ISearchParamExtractor.SearchParamSet<PathAndRef> refs = mySearchParamExtractor.extractResourceLinks(theResource);
|
||||
SearchParamExtractorService.handleWarnings(theRequest, myInterceptorBroadcaster, refs);
|
||||
|
||||
Map<String, IResourceLookup> resourceIdToResolvedTarget = new HashMap<>();
|
||||
for (PathAndRef nextPathAndRef : refs) {
|
||||
RuntimeSearchParam searchParam = mySearchParamRegistry.getActiveSearchParam(resourceName, nextPathAndRef.getSearchParamName());
|
||||
extractResourceLinks(theParams, theEntity, theUpdateTime, theResourceLinkResolver, searchParam, nextPathAndRef, theFailOnInvalidReference, theRequest, resourceIdToResolvedTarget);
|
||||
}
|
||||
|
||||
theEntity.setHasLinks(theParams.myLinks.size() > 0);
|
||||
}
|
||||
|
||||
private void extractResourceLinks(ResourceIndexedSearchParams theParams, ResourceTable theEntity, Date theUpdateTime, IResourceLinkResolver theResourceLinkResolver, RuntimeSearchParam theRuntimeSearchParam, PathAndRef thePathAndRef, boolean theFailOnInvalidReference, RequestDetails theRequest, Map<String, IResourceLookup> theResourceIdToResolvedTarget) {
|
||||
IBaseReference nextReference = thePathAndRef.getRef();
|
||||
IIdType nextId = nextReference.getReferenceElement();
|
||||
String path = thePathAndRef.getPath();
|
||||
|
||||
/*
|
||||
* This can only really happen if the DAO is being called
|
||||
* programmatically with a Bundle (not through the FHIR REST API)
|
||||
* but Smile does this
|
||||
*/
|
||||
if (nextId.isEmpty() && nextReference.getResource() != null) {
|
||||
nextId = nextReference.getResource().getIdElement();
|
||||
}
|
||||
|
||||
theParams.myPopulatedResourceLinkParameters.add(thePathAndRef.getSearchParamName());
|
||||
|
||||
boolean canonical = thePathAndRef.isCanonical();
|
||||
if (LogicalReferenceHelper.isLogicalReference(myModelConfig, nextId) || canonical) {
|
||||
String value = nextId.getValue();
|
||||
ResourceLink resourceLink = ResourceLink.forLogicalReference(thePathAndRef.getPath(), theEntity, value, theUpdateTime);
|
||||
if (theParams.myLinks.add(resourceLink)) {
|
||||
ourLog.debug("Indexing remote resource reference URL: {}", nextId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
String baseUrl = nextId.getBaseUrl();
|
||||
String typeString = nextId.getResourceType();
|
||||
if (isBlank(typeString)) {
|
||||
String msg = "Invalid resource reference found at path[" + path + "] - Does not contain resource type - " + nextId.getValue();
|
||||
if (theFailOnInvalidReference) {
|
||||
throw new InvalidRequestException(msg);
|
||||
} else {
|
||||
ourLog.debug(msg);
|
||||
return;
|
||||
}
|
||||
}
|
||||
RuntimeResourceDefinition resourceDefinition;
|
||||
try {
|
||||
resourceDefinition = myContext.getResourceDefinition(typeString);
|
||||
} catch (DataFormatException e) {
|
||||
String msg = "Invalid resource reference found at path[" + path + "] - Resource type is unknown or not supported on this server - " + nextId.getValue();
|
||||
if (theFailOnInvalidReference) {
|
||||
throw new InvalidRequestException(msg);
|
||||
} else {
|
||||
ourLog.debug(msg);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (theRuntimeSearchParam.hasTargets()) {
|
||||
if (!theRuntimeSearchParam.getTargets().contains(typeString)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (isNotBlank(baseUrl)) {
|
||||
if (!myModelConfig.getTreatBaseUrlsAsLocal().contains(baseUrl) && !myModelConfig.isAllowExternalReferences()) {
|
||||
String msg = myContext.getLocalizer().getMessage(BaseSearchParamExtractor.class, "externalReferenceNotAllowed", nextId.getValue());
|
||||
throw new InvalidRequestException(msg);
|
||||
} else {
|
||||
ResourceLink resourceLink = ResourceLink.forAbsoluteReference(thePathAndRef.getPath(), theEntity, nextId, theUpdateTime);
|
||||
if (theParams.myLinks.add(resourceLink)) {
|
||||
ourLog.debug("Indexing remote resource reference URL: {}", nextId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Class<? extends IBaseResource> type = resourceDefinition.getImplementingClass();
|
||||
String id = nextId.getIdPart();
|
||||
if (StringUtils.isBlank(id)) {
|
||||
String msg = "Invalid resource reference found at path[" + path + "] - Does not contain resource ID - " + nextId.getValue();
|
||||
if (theFailOnInvalidReference) {
|
||||
throw new InvalidRequestException(msg);
|
||||
} else {
|
||||
ourLog.debug(msg);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
theResourceLinkResolver.validateTypeOrThrowException(type);
|
||||
ResourceLink resourceLink = createResourceLink(theEntity, theUpdateTime, theResourceLinkResolver, theRuntimeSearchParam, path, thePathAndRef, nextId, typeString, type, nextReference, theRequest, theResourceIdToResolvedTarget);
|
||||
if (resourceLink == null) {
|
||||
return;
|
||||
}
|
||||
theParams.myLinks.add(resourceLink);
|
||||
}
|
||||
|
||||
private ResourceLink createResourceLink(ResourceTable theEntity, Date theUpdateTime, IResourceLinkResolver theResourceLinkResolver, RuntimeSearchParam nextSpDef, String theNextPathsUnsplit, PathAndRef nextPathAndRef, IIdType theNextId, String theTypeString, Class<? extends IBaseResource> theType, IBaseReference theReference, RequestDetails theRequest, Map<String, IResourceLookup> theResourceIdToResolvedTarget) {
|
||||
/*
|
||||
* We keep a cache of resolved target resources. This is good since for some resource types, there
|
||||
* are multiple search parameters that map to the same element path within a resource (e.g.
|
||||
* Observation:patient and Observation.subject and we don't want to force a resolution of the
|
||||
* target any more times than we have to.
|
||||
*/
|
||||
|
||||
IResourceLookup targetResource = theResourceIdToResolvedTarget.get(theNextId.getValue());
|
||||
if (targetResource == null) {
|
||||
|
||||
// If we're allowing references across partitions, we just don't include the partition ID when resolving the target
|
||||
PartitionId targetPartitionId = theEntity.getPartitionId();
|
||||
if (myPartitionConfig.isAllowReferencesAcrossPartitions()) {
|
||||
targetPartitionId = null;
|
||||
}
|
||||
|
||||
targetResource = theResourceLinkResolver.findTargetResource(targetPartitionId, nextSpDef, theNextPathsUnsplit, theNextId, theTypeString, theType, theReference, theRequest);
|
||||
}
|
||||
|
||||
if (targetResource == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
theResourceIdToResolvedTarget.put(theNextId.getValue(), targetResource);
|
||||
|
||||
String targetResourceType = targetResource.getResourceType();
|
||||
Long targetResourcePid = targetResource.getResourceId();
|
||||
String targetResourceIdPart = theNextId.getIdPart();
|
||||
return ResourceLink.forLocalReference(nextPathAndRef.getPath(), theEntity, targetResourceType, targetResourcePid, targetResourceIdPart, theUpdateTime);
|
||||
}
|
||||
|
||||
}
|
|
@ -20,21 +20,36 @@ package ca.uhn.fhir.jpa.searchparam.extractor;
|
|||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||
import ca.uhn.fhir.interceptor.api.HookParams;
|
||||
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
|
||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||
import ca.uhn.fhir.jpa.model.cross.IResourceLookup;
|
||||
import ca.uhn.fhir.jpa.model.entity.*;
|
||||
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
|
||||
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
|
||||
import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster;
|
||||
import ca.uhn.fhir.parser.DataFormatException;
|
||||
import ca.uhn.fhir.parser.IParser;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.hl7.fhir.instance.model.api.IBaseReference;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
|
||||
public class SearchParamExtractorService {
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchParamExtractorService.class);
|
||||
|
@ -43,30 +58,60 @@ public class SearchParamExtractorService {
|
|||
private ISearchParamExtractor mySearchParamExtractor;
|
||||
@Autowired
|
||||
private IInterceptorBroadcaster myInterceptorBroadcaster;
|
||||
@Autowired
|
||||
private ModelConfig myModelConfig;
|
||||
@Autowired
|
||||
private FhirContext myContext;
|
||||
@Autowired
|
||||
private ISearchParamRegistry mySearchParamRegistry;
|
||||
@Autowired(required = false)
|
||||
private IResourceLinkResolver myResourceLinkResolver;
|
||||
|
||||
public void extractFromResource(RequestDetails theRequestDetails, ResourceIndexedSearchParams theParams, ResourceTable theEntity, IBaseResource theResource) {
|
||||
/**
|
||||
* This method is responsible for scanning a resource for all of the search parameter instances. I.e. for all search parameters defined for
|
||||
* a given resource type, it extracts the associated indexes and populates {@literal theParams}.
|
||||
*/
|
||||
public void extractFromResource(RequestDetails theRequestDetails, ResourceIndexedSearchParams theParams, ResourceTable theEntity, IBaseResource theResource, Date theUpdateTime, boolean theFailOnInvalidReference) {
|
||||
IBaseResource resource = normalizeResource(theResource);
|
||||
|
||||
// All search parameter types except Reference
|
||||
extractSearchIndexParameters(theRequestDetails, theParams, resource, theEntity);
|
||||
|
||||
// Reference search parameters
|
||||
extractResourceLinks(theParams, theEntity, resource, theUpdateTime, theFailOnInvalidReference, theRequestDetails);
|
||||
|
||||
theParams.setUpdatedTime(theUpdateTime);
|
||||
}
|
||||
|
||||
private void extractSearchIndexParameters(RequestDetails theRequestDetails, ResourceIndexedSearchParams theParams, IBaseResource theResource, ResourceTable theEntity) {
|
||||
|
||||
// Strings
|
||||
ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamString> strings = extractSearchParamStrings(theResource);
|
||||
handleWarnings(theRequestDetails, myInterceptorBroadcaster, strings);
|
||||
theParams.myStringParams.addAll(strings);
|
||||
|
||||
// Numbers
|
||||
ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamNumber> numbers = extractSearchParamNumber(theResource);
|
||||
handleWarnings(theRequestDetails, myInterceptorBroadcaster, numbers);
|
||||
theParams.myNumberParams.addAll(numbers);
|
||||
|
||||
// Quantities
|
||||
ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamQuantity> quantities = extractSearchParamQuantity(theResource);
|
||||
handleWarnings(theRequestDetails, myInterceptorBroadcaster, quantities);
|
||||
theParams.myQuantityParams.addAll(quantities);
|
||||
|
||||
// Dates
|
||||
ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamDate> dates = extractSearchParamDates(theResource);
|
||||
handleWarnings(theRequestDetails, myInterceptorBroadcaster, dates);
|
||||
theParams.myDateParams.addAll(dates);
|
||||
|
||||
// URIs
|
||||
ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamUri> uris = extractSearchParamUri(theResource);
|
||||
handleWarnings(theRequestDetails, myInterceptorBroadcaster, uris);
|
||||
theParams.myUriParams.addAll(uris);
|
||||
|
||||
ourLog.trace("Storing date indexes: {}", theParams.myDateParams);
|
||||
|
||||
// Tokens (can result in both Token and String, as we index the display name for
|
||||
// the types: Coding, CodeableConcept)
|
||||
for (BaseResourceIndexedSearchParam next : extractSearchParamTokens(theResource)) {
|
||||
if (next instanceof ResourceIndexedSearchParamToken) {
|
||||
theParams.myTokenParams.add((ResourceIndexedSearchParamToken) next);
|
||||
|
@ -77,21 +122,182 @@ public class SearchParamExtractorService {
|
|||
}
|
||||
}
|
||||
|
||||
// Specials
|
||||
for (BaseResourceIndexedSearchParam next : extractSearchParamSpecial(theResource)) {
|
||||
if (next instanceof ResourceIndexedSearchParamCoords) {
|
||||
theParams.myCoordsParams.add((ResourceIndexedSearchParamCoords) next);
|
||||
}
|
||||
}
|
||||
|
||||
populateResourceTable(theParams.myStringParams, theEntity);
|
||||
// Do this after, because we add to strings during both string and token processing
|
||||
populateResourceTable(theParams.myNumberParams, theEntity);
|
||||
populateResourceTable(theParams.myQuantityParams, theEntity);
|
||||
populateResourceTable(theParams.myDateParams, theEntity);
|
||||
populateResourceTable(theParams.myUriParams, theEntity);
|
||||
populateResourceTable(theParams.myCoordsParams, theEntity);
|
||||
populateResourceTable(theParams.myTokenParams, theEntity);
|
||||
populateResourceTable(theParams.myStringParams, theEntity);
|
||||
populateResourceTable(theParams.myCoordsParams, theEntity);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a bit hacky, but if someone has manually populated a resource (ie. my working directly with the model
|
||||
* as opposed to by parsing a serialized instance) it's possible that they have put in contained resources
|
||||
* using {@link IBaseReference#setResource(IBaseResource)}, and those contained resources have not yet
|
||||
* ended up in the Resource.contained array, meaning that FHIRPath expressions won't be able to find them.
|
||||
*
|
||||
* As a result, we to a serialize-and-parse to normalize the object. This really only affects people who
|
||||
* are calling the JPA DAOs directly, but there are a few of those...
|
||||
*/
|
||||
private IBaseResource normalizeResource(IBaseResource theResource) {
|
||||
IParser parser = myContext.newJsonParser().setPrettyPrint(false);
|
||||
theResource = parser.parseResource(parser.encodeResourceToString(theResource));
|
||||
return theResource;
|
||||
}
|
||||
|
||||
private void extractResourceLinks(ResourceIndexedSearchParams theParams, ResourceTable theEntity, IBaseResource theResource, Date theUpdateTime, boolean theFailOnInvalidReference, RequestDetails theRequest) {
|
||||
String resourceName = myContext.getResourceDefinition(theResource).getName();
|
||||
|
||||
ISearchParamExtractor.SearchParamSet<PathAndRef> refs = mySearchParamExtractor.extractResourceLinks(theResource);
|
||||
SearchParamExtractorService.handleWarnings(theRequest, myInterceptorBroadcaster, refs);
|
||||
|
||||
Map<String, IResourceLookup> resourceIdToResolvedTarget = new HashMap<>();
|
||||
for (PathAndRef nextPathAndRef : refs) {
|
||||
RuntimeSearchParam searchParam = mySearchParamRegistry.getActiveSearchParam(resourceName, nextPathAndRef.getSearchParamName());
|
||||
extractResourceLinks(theParams, theEntity, theUpdateTime, searchParam, nextPathAndRef, theFailOnInvalidReference, theRequest, resourceIdToResolvedTarget);
|
||||
}
|
||||
|
||||
theEntity.setHasLinks(theParams.myLinks.size() > 0);
|
||||
}
|
||||
|
||||
private void extractResourceLinks(ResourceIndexedSearchParams theParams, ResourceTable theEntity, Date theUpdateTime, RuntimeSearchParam theRuntimeSearchParam, PathAndRef thePathAndRef, boolean theFailOnInvalidReference, RequestDetails theRequest, Map<String, IResourceLookup> theResourceIdToResolvedTarget) {
|
||||
IBaseReference nextReference = thePathAndRef.getRef();
|
||||
IIdType nextId = nextReference.getReferenceElement();
|
||||
String path = thePathAndRef.getPath();
|
||||
|
||||
/*
|
||||
* This can only really happen if the DAO is being called
|
||||
* programmatically with a Bundle (not through the FHIR REST API)
|
||||
* but Smile does this
|
||||
*/
|
||||
if (nextId.isEmpty() && nextReference.getResource() != null) {
|
||||
nextId = nextReference.getResource().getIdElement();
|
||||
}
|
||||
|
||||
theParams.myPopulatedResourceLinkParameters.add(thePathAndRef.getSearchParamName());
|
||||
|
||||
boolean canonical = thePathAndRef.isCanonical();
|
||||
if (LogicalReferenceHelper.isLogicalReference(myModelConfig, nextId) || canonical) {
|
||||
String value = nextId.getValue();
|
||||
ResourceLink resourceLink = ResourceLink.forLogicalReference(thePathAndRef.getPath(), theEntity, value, theUpdateTime);
|
||||
if (theParams.myLinks.add(resourceLink)) {
|
||||
ourLog.debug("Indexing remote resource reference URL: {}", nextId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
String baseUrl = nextId.getBaseUrl();
|
||||
String typeString = nextId.getResourceType();
|
||||
if (isBlank(typeString)) {
|
||||
String msg = "Invalid resource reference found at path[" + path + "] - Does not contain resource type - " + nextId.getValue();
|
||||
if (theFailOnInvalidReference) {
|
||||
throw new InvalidRequestException(msg);
|
||||
} else {
|
||||
ourLog.debug(msg);
|
||||
return;
|
||||
}
|
||||
}
|
||||
RuntimeResourceDefinition resourceDefinition;
|
||||
try {
|
||||
resourceDefinition = myContext.getResourceDefinition(typeString);
|
||||
} catch (DataFormatException e) {
|
||||
String msg = "Invalid resource reference found at path[" + path + "] - Resource type is unknown or not supported on this server - " + nextId.getValue();
|
||||
if (theFailOnInvalidReference) {
|
||||
throw new InvalidRequestException(msg);
|
||||
} else {
|
||||
ourLog.debug(msg);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (theRuntimeSearchParam.hasTargets()) {
|
||||
if (!theRuntimeSearchParam.getTargets().contains(typeString)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (isNotBlank(baseUrl)) {
|
||||
if (!myModelConfig.getTreatBaseUrlsAsLocal().contains(baseUrl) && !myModelConfig.isAllowExternalReferences()) {
|
||||
String msg = myContext.getLocalizer().getMessage(BaseSearchParamExtractor.class, "externalReferenceNotAllowed", nextId.getValue());
|
||||
throw new InvalidRequestException(msg);
|
||||
} else {
|
||||
ResourceLink resourceLink = ResourceLink.forAbsoluteReference(thePathAndRef.getPath(), theEntity, nextId, theUpdateTime);
|
||||
if (theParams.myLinks.add(resourceLink)) {
|
||||
ourLog.debug("Indexing remote resource reference URL: {}", nextId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Class<? extends IBaseResource> type = resourceDefinition.getImplementingClass();
|
||||
String id = nextId.getIdPart();
|
||||
if (StringUtils.isBlank(id)) {
|
||||
String msg = "Invalid resource reference found at path[" + path + "] - Does not contain resource ID - " + nextId.getValue();
|
||||
if (theFailOnInvalidReference) {
|
||||
throw new InvalidRequestException(msg);
|
||||
} else {
|
||||
ourLog.debug(msg);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ResourceLink resourceLink;
|
||||
if (theFailOnInvalidReference) {
|
||||
|
||||
myResourceLinkResolver.validateTypeOrThrowException(type);
|
||||
resourceLink = resolveTargetAndCreateResourceLinkOrReturnNull(theEntity, theUpdateTime, theRuntimeSearchParam, path, thePathAndRef, nextId, typeString, type, nextReference, theRequest, theResourceIdToResolvedTarget);
|
||||
if (resourceLink == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
ResourceTable target;
|
||||
target = new ResourceTable();
|
||||
target.setResourceType(typeString);
|
||||
resourceLink = ResourceLink.forLocalReference(thePathAndRef.getPath(), theEntity, typeString, null, nextId.getIdPart(), theUpdateTime);
|
||||
|
||||
}
|
||||
|
||||
theParams.myLinks.add(resourceLink);
|
||||
}
|
||||
|
||||
private ResourceLink resolveTargetAndCreateResourceLinkOrReturnNull(ResourceTable theEntity, Date theUpdateTime, RuntimeSearchParam nextSpDef, String theNextPathsUnsplit, PathAndRef nextPathAndRef, IIdType theNextId, String theTypeString, Class<? extends IBaseResource> theType, IBaseReference theReference, RequestDetails theRequest, Map<String, IResourceLookup> theResourceIdToResolvedTarget) {
|
||||
/*
|
||||
* We keep a cache of resolved target resources. This is good since for some resource types, there
|
||||
* are multiple search parameters that map to the same element path within a resource (e.g.
|
||||
* Observation:patient and Observation.subject and we don't want to force a resolution of the
|
||||
* target any more times than we have to.
|
||||
*/
|
||||
|
||||
IResourceLookup targetResource = theResourceIdToResolvedTarget.get(theNextId.getValue());
|
||||
if (targetResource == null) {
|
||||
targetResource = myResourceLinkResolver.findTargetResource(nextSpDef, theNextPathsUnsplit, theNextId, theTypeString, theType, theReference, theRequest);
|
||||
}
|
||||
|
||||
if (targetResource == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
theResourceIdToResolvedTarget.put(theNextId.getValue(), targetResource);
|
||||
|
||||
String targetResourceType = targetResource.getResourceType();
|
||||
Long targetResourcePid = targetResource.getResourceId();
|
||||
String targetResourceIdPart = theNextId.getIdPart();
|
||||
return ResourceLink.forLocalReference(nextPathAndRef.getPath(), theEntity, targetResourceType, targetResourcePid, targetResourceIdPart, theUpdateTime);
|
||||
}
|
||||
|
||||
|
||||
static void handleWarnings(RequestDetails theRequestDetails, IInterceptorBroadcaster theInterceptorBroadcaster, ISearchParamExtractor.SearchParamSet<?> theSearchParamSet) {
|
||||
if (theSearchParamSet.getWarnings().isEmpty()) {
|
||||
return;
|
||||
|
|
|
@ -23,30 +23,23 @@ package ca.uhn.fhir.jpa.searchparam.matcher;
|
|||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.ResourceLinkExtractor;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorService;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
public class IndexedSearchParamExtractor {
|
||||
@Autowired
|
||||
private FhirContext myContext;
|
||||
@Autowired
|
||||
private SearchParamExtractorService mySearchParamExtractorService;
|
||||
@Autowired
|
||||
private ResourceLinkExtractor myResourceLinkExtractor;
|
||||
@Autowired
|
||||
private InlineResourceLinkResolver myInlineResourceLinkResolver;
|
||||
|
||||
public ResourceIndexedSearchParams extractIndexedSearchParams(IBaseResource theResource, RequestDetails theRequest) {
|
||||
ResourceTable entity = new ResourceTable();
|
||||
String resourceType = myContext.getResourceDefinition(theResource).getName();
|
||||
entity.setResourceType(resourceType);
|
||||
ResourceIndexedSearchParams resourceIndexedSearchParams = new ResourceIndexedSearchParams();
|
||||
mySearchParamExtractorService.extractFromResource(theRequest, resourceIndexedSearchParams, entity, theResource);
|
||||
myResourceLinkExtractor.extractResourceLinks(resourceIndexedSearchParams, entity, theResource, theResource.getMeta().getLastUpdated(), myInlineResourceLinkResolver, false, theRequest);
|
||||
mySearchParamExtractorService.extractFromResource(theRequest, resourceIndexedSearchParams, entity, theResource, theResource.getMeta().getLastUpdated(), false);
|
||||
return resourceIndexedSearchParams;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
package ca.uhn.fhir.jpa.searchparam.matcher;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR Search Parameters
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2020 University Health Network
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||
import ca.uhn.fhir.jpa.model.cross.IResourceLookup;
|
||||
import ca.uhn.fhir.jpa.model.cross.ResourceLookup;
|
||||
import ca.uhn.fhir.jpa.model.entity.PartitionId;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.IResourceLinkResolver;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import org.hl7.fhir.instance.model.api.IBaseReference;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
public class InlineResourceLinkResolver implements IResourceLinkResolver {
|
||||
|
||||
@Override
|
||||
public IResourceLookup findTargetResource(PartitionId thePartitionId, RuntimeSearchParam theSearchParam, String theSourcePath, IIdType theSourceResourceId, String theTypeString, Class<? extends IBaseResource> theType, IBaseReference theReference, RequestDetails theRequest) {
|
||||
|
||||
/*
|
||||
* TODO: JA - This gets used during runtime in-memory matching for subscription. It's not
|
||||
* really clear if it's useful or not.
|
||||
*/
|
||||
|
||||
ResourceTable target;
|
||||
target = new ResourceTable();
|
||||
target.setResourceType(theTypeString);
|
||||
return new ResourceLookup(theTypeString, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateTypeOrThrowException(Class<? extends IBaseResource> theType) {
|
||||
// When resolving reference in-memory for a single resource, there's nothing to validate
|
||||
}
|
||||
}
|
|
@ -33,7 +33,9 @@ public class SearchParamMatcher {
|
|||
private InMemoryResourceMatcher myInMemoryResourceMatcher;
|
||||
|
||||
public InMemoryMatchResult match(String theCriteria, IBaseResource theResource, RequestDetails theRequest) {
|
||||
|
||||
ResourceIndexedSearchParams resourceIndexedSearchParams = myIndexedSearchParamExtractor.extractIndexedSearchParams(theResource, theRequest);
|
||||
|
||||
return myInMemoryResourceMatcher.match(theCriteria, theResource, resourceIndexedSearchParams);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import static org.junit.Assert.assertSame;
|
|||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Matchers.isNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
@ -1098,8 +1099,8 @@ public class JsonParserDstu2_1Test {
|
|||
assertEquals(null, parsed.getGenderElement().getValueAsString());
|
||||
|
||||
ArgumentCaptor<String> msgCaptor = ArgumentCaptor.forClass(String.class);
|
||||
verify(errorHandler, times(1)).invalidValue(isNull(IParseLocation.class), eq(""), msgCaptor.capture());
|
||||
assertEquals("Attribute values must not be empty (\"\")", msgCaptor.getValue());
|
||||
verify(errorHandler, times(1)).invalidValue(any(IParseLocation.class), eq(""), msgCaptor.capture());
|
||||
assertEquals("Attribute value must not be empty (\"\")", msgCaptor.getValue());
|
||||
|
||||
String encoded = ourCtx.newJsonParser().encodeResourceToString(parsed);
|
||||
assertEquals("{\"resourceType\":\"Patient\"}", encoded);
|
||||
|
@ -1118,7 +1119,7 @@ public class JsonParserDstu2_1Test {
|
|||
assertEquals("foo", parsed.getGenderElement().getValueAsString());
|
||||
|
||||
ArgumentCaptor<String> msgCaptor = ArgumentCaptor.forClass(String.class);
|
||||
verify(errorHandler, times(1)).invalidValue(isNull(IParseLocation.class), eq("foo"), msgCaptor.capture());
|
||||
verify(errorHandler, times(1)).invalidValue(any(IParseLocation.class), eq("foo"), msgCaptor.capture());
|
||||
assertEquals("Unknown AdministrativeGender code 'foo'", msgCaptor.getValue());
|
||||
|
||||
String encoded = ourCtx.newJsonParser().encodeResourceToString(parsed);
|
||||
|
@ -1138,7 +1139,7 @@ public class JsonParserDstu2_1Test {
|
|||
assertEquals("foo", parsed.getValueDateTimeType().getValueAsString());
|
||||
|
||||
ArgumentCaptor<String> msgCaptor = ArgumentCaptor.forClass(String.class);
|
||||
verify(errorHandler, times(1)).invalidValue(isNull(IParseLocation.class), eq("foo"), msgCaptor.capture());
|
||||
verify(errorHandler, times(1)).invalidValue(any(IParseLocation.class), eq("foo"), msgCaptor.capture());
|
||||
assertEquals("Invalid date/time format: \"foo\"", msgCaptor.getValue());
|
||||
|
||||
String encoded = ourCtx.newJsonParser().encodeResourceToString(parsed);
|
||||
|
|
|
@ -2379,7 +2379,7 @@ public class XmlParserDstu2_1Test {
|
|||
p.parseResource(resource);
|
||||
fail();
|
||||
} catch (DataFormatException e) {
|
||||
assertEquals("DataFormatException at [[row,col {unknown-source}]: [2,4]]: Invalid attribute value \"1\": Invalid boolean string: '1'", e.getMessage());
|
||||
assertEquals("DataFormatException at [[row,col {unknown-source}]: [2,4]]: [element=\"active\"] Invalid attribute value \"1\": Invalid boolean string: '1'", e.getMessage());
|
||||
}
|
||||
|
||||
LenientErrorHandler errorHandler = new LenientErrorHandler();
|
||||
|
|
|
@ -82,7 +82,7 @@ public class ResourceValidatorDstu2Test {
|
|||
parser.parseResource(encoded);
|
||||
fail();
|
||||
} catch (DataFormatException e) {
|
||||
assertEquals("DataFormatException at [[row,col {unknown-source}]: [2,4]]: Invalid attribute value \"2000-15-31\": Invalid date/time format: \"2000-15-31\"", e.getMessage());
|
||||
assertEquals("DataFormatException at [[row,col {unknown-source}]: [2,4]]: [element=\"birthDate\"] Invalid attribute value \"2000-15-31\": Invalid date/time format: \"2000-15-31\"", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -58,6 +58,24 @@ public class JsonParserDstu3Test {
|
|||
ourCtx.setNarrativeGenerator(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncodedResourceWithIncorrectRepresentationOfDecimalTypeToJson() {
|
||||
DecimalType decimalType = new DecimalType();
|
||||
decimalType.setValueAsString(".5");
|
||||
MedicationRequest mr = new MedicationRequest();
|
||||
Dosage dosage = new Dosage();
|
||||
dosage.setDose(new SimpleQuantity()
|
||||
.setValue(decimalType.getValue())
|
||||
.setUnit("{tablet}")
|
||||
.setSystem("http://unitsofmeasure.org")
|
||||
.setCode("{tablet}"));
|
||||
mr.addDosageInstruction(dosage);
|
||||
String encoded = ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(mr);
|
||||
ourLog.info(encoded);
|
||||
mr = ourCtx.newJsonParser().parseResource(MedicationRequest.class, encoded);
|
||||
assertEquals(BigDecimal.valueOf(0.5), mr.getDosageInstructionFirstRep().getDoseSimpleQuantity().getValue());
|
||||
assertTrue(encoded.contains("0.5"));
|
||||
}
|
||||
|
||||
/**
|
||||
* See #563
|
||||
|
@ -1504,7 +1522,7 @@ public class JsonParserDstu3Test {
|
|||
assertEquals("foo", parsed.getValueDateTimeType().getValueAsString());
|
||||
|
||||
ArgumentCaptor<String> msgCaptor = ArgumentCaptor.forClass(String.class);
|
||||
verify(errorHandler, times(1)).invalidValue(isNull(), eq("foo"), msgCaptor.capture());
|
||||
verify(errorHandler, times(1)).invalidValue(any(), eq("foo"), msgCaptor.capture());
|
||||
assertEquals("Invalid date/time format: \"foo\"", msgCaptor.getValue());
|
||||
|
||||
String encoded = ourCtx.newJsonParser().encodeResourceToString(parsed);
|
||||
|
@ -1536,8 +1554,8 @@ public class JsonParserDstu3Test {
|
|||
assertEquals(null, parsed.getGenderElement().getValueAsString());
|
||||
|
||||
ArgumentCaptor<String> msgCaptor = ArgumentCaptor.forClass(String.class);
|
||||
verify(errorHandler, times(1)).invalidValue(isNull(), eq(""), msgCaptor.capture());
|
||||
assertEquals("Attribute values must not be empty (\"\")", msgCaptor.getValue());
|
||||
verify(errorHandler, times(1)).invalidValue(any(), eq(""), msgCaptor.capture());
|
||||
assertEquals("Attribute value must not be empty (\"\")", msgCaptor.getValue());
|
||||
|
||||
String encoded = ourCtx.newJsonParser().encodeResourceToString(parsed);
|
||||
assertEquals("{\"resourceType\":\"Patient\"}", encoded);
|
||||
|
@ -1556,7 +1574,7 @@ public class JsonParserDstu3Test {
|
|||
assertEquals("foo", parsed.getGenderElement().getValueAsString());
|
||||
|
||||
ArgumentCaptor<String> msgCaptor = ArgumentCaptor.forClass(String.class);
|
||||
verify(errorHandler, times(1)).invalidValue(isNull(), eq("foo"), msgCaptor.capture());
|
||||
verify(errorHandler, times(1)).invalidValue(any(), eq("foo"), msgCaptor.capture());
|
||||
assertEquals("Unknown AdministrativeGender code 'foo'", msgCaptor.getValue());
|
||||
|
||||
String encoded = ourCtx.newJsonParser().encodeResourceToString(parsed);
|
||||
|
|
|
@ -45,6 +45,7 @@ import org.xmlunit.diff.ElementSelectors;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
|
@ -70,6 +71,25 @@ public class XmlParserDstu3Test {
|
|||
ourCtx.setNarrativeGenerator(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncodedResourceWithIncorrectRepresentationOfDecimalTypeToXml() {
|
||||
DecimalType decimalType = new DecimalType();
|
||||
decimalType.setValueAsString(".5");
|
||||
MedicationRequest mr = new MedicationRequest();
|
||||
Dosage dosage = new Dosage();
|
||||
dosage.setDose(new SimpleQuantity()
|
||||
.setValue(decimalType.getValue())
|
||||
.setUnit("{tablet}")
|
||||
.setSystem("http://unitsofmeasure.org")
|
||||
.setCode("{tablet}"));
|
||||
mr.addDosageInstruction(dosage);
|
||||
String encoded = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(mr);
|
||||
ourLog.info(encoded);
|
||||
mr = ourCtx.newXmlParser().parseResource(MedicationRequest.class, encoded);
|
||||
assertEquals(BigDecimal.valueOf(0.5), mr.getDosageInstructionFirstRep().getDoseSimpleQuantity().getValue());
|
||||
assertTrue(encoded.contains("0.5"));
|
||||
}
|
||||
|
||||
/**
|
||||
* We specifically include extensions on CapabilityStatment even in
|
||||
* summary mode, since this is behaviour that people depend on
|
||||
|
@ -1542,7 +1562,7 @@ public class XmlParserDstu3Test {
|
|||
parser.encodeResourceToString(p);
|
||||
fail();
|
||||
} catch (DataFormatException e) {
|
||||
assertEquals("Extension contains both a value and nested extensions: Patient(res).extension", e.getMessage());
|
||||
assertEquals("[element=\"Patient(res).extension\"] Extension contains both a value and nested extensions", e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3119,7 +3139,7 @@ public class XmlParserDstu3Test {
|
|||
p.parseResource(resource);
|
||||
fail();
|
||||
} catch (DataFormatException e) {
|
||||
assertEquals("DataFormatException at [[row,col {unknown-source}]: [2,4]]: Invalid attribute value \"1\": Invalid boolean string: '1'", e.getMessage());
|
||||
assertEquals("DataFormatException at [[row,col {unknown-source}]: [2,4]]: [element=\"active\"] Invalid attribute value \"1\": Invalid boolean string: '1'", e.getMessage());
|
||||
}
|
||||
|
||||
LenientErrorHandler errorHandler = new LenientErrorHandler();
|
||||
|
|
|
@ -100,7 +100,7 @@ public class ClientServerValidationTestHl7OrgDstu2 {
|
|||
|
||||
@Test
|
||||
public void testServerReturnsWrongVersionForDstu2() throws Exception {
|
||||
String wrongFhirVersion = "3.0.1";
|
||||
String wrongFhirVersion = "3.0.2";
|
||||
assertThat(wrongFhirVersion, is(FhirVersionEnum.DSTU3.getFhirVersionString())); // asserting that what we assume to be the DSTU3 FHIR version is still correct
|
||||
Conformance conf = new Conformance();
|
||||
conf.setFhirVersion(wrongFhirVersion);
|
||||
|
@ -119,7 +119,7 @@ public class ClientServerValidationTestHl7OrgDstu2 {
|
|||
myCtx.newRestfulGenericClient("http://foo").read(new UriDt("http://foo/Patient/1"));
|
||||
fail();
|
||||
} catch (FhirClientInappropriateForServerException e) {
|
||||
assertThat(e.toString(), containsString("The server at base URL \"http://foo/metadata\" returned a conformance statement indicating that it supports FHIR version \"3.0.1\" which corresponds to DSTU3, but this client is configured to use DSTU2_HL7ORG (via the FhirContext)"));
|
||||
assertThat(e.toString(), containsString("The server at base URL \"http://foo/metadata\" returned a conformance statement indicating that it supports FHIR version \"3.0.2\" which corresponds to DSTU3, but this client is configured to use DSTU2_HL7ORG (via the FhirContext)"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,10 +5,7 @@ import ca.uhn.fhir.rest.api.Constants;
|
|||
import ca.uhn.fhir.test.BaseTest;
|
||||
import ca.uhn.fhir.util.StopWatch;
|
||||
import ca.uhn.fhir.util.TestUtil;
|
||||
import com.google.common.base.Charsets;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.io.Resources;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.io.output.NullWriter;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
|
@ -21,7 +18,6 @@ import org.slf4j.Logger;
|
|||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
@ -30,8 +26,10 @@ import java.util.concurrent.TimeUnit;
|
|||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.stringContainsInOrder;
|
||||
import static org.hamcrest.core.IsNot.not;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public class JsonParserR4Test extends BaseTest {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(JsonParserR4Test.class);
|
||||
|
@ -156,10 +154,12 @@ public class JsonParserR4Test extends BaseTest {
|
|||
|
||||
@Test
|
||||
public void testParseBundleWithMultipleNestedContainedResources() throws Exception {
|
||||
URL url = Resources.getResource("bundle-with-two-patient-resources.json");
|
||||
String text = Resources.toString(url, Charsets.UTF_8);
|
||||
String text = loadResource("/bundle-with-two-patient-resources.json");
|
||||
|
||||
Bundle bundle = ourCtx.newJsonParser().parseResource(Bundle.class, text);
|
||||
assertEquals(Boolean.TRUE, bundle.getUserData(BaseParser.RESOURCE_CREATED_BY_PARSER));
|
||||
assertEquals(Boolean.TRUE, bundle.getEntry().get(0).getResource().getUserData(BaseParser.RESOURCE_CREATED_BY_PARSER));
|
||||
assertEquals(Boolean.TRUE, bundle.getEntry().get(1).getResource().getUserData(BaseParser.RESOURCE_CREATED_BY_PARSER));
|
||||
|
||||
assertEquals("12346", getPatientIdValue(bundle, 0));
|
||||
assertEquals("12345", getPatientIdValue(bundle, 1));
|
||||
|
@ -350,7 +350,7 @@ public class JsonParserR4Test extends BaseTest {
|
|||
parser.encodeResourceToString(p);
|
||||
fail();
|
||||
} catch (DataFormatException e) {
|
||||
assertEquals("Extension contains both a value and nested extensions: Patient(res).extension", e.getMessage());
|
||||
assertEquals("[element=\"Patient(res).extension\"] Extension contains both a value and nested extensions", e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -569,6 +569,32 @@ public class JsonParserR4Test extends BaseTest {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* See #1793
|
||||
*/
|
||||
@Test
|
||||
public void testParseEmptyAttribute() {
|
||||
String input = "{\n" +
|
||||
" \"resourceType\": \"Patient\",\n" +
|
||||
" \"identifier\": [\n" +
|
||||
" {\n" +
|
||||
" \"system\": \"https://example.com\",\n" +
|
||||
" \"value\": \"\"\n" +
|
||||
" }\n" +
|
||||
" ]\n" +
|
||||
"}";
|
||||
|
||||
IParser jsonParser = ourCtx.newJsonParser();
|
||||
jsonParser.setParserErrorHandler(new StrictErrorHandler());
|
||||
try {
|
||||
jsonParser.parseResource(Patient.class, input);
|
||||
fail();
|
||||
} catch (DataFormatException e) {
|
||||
assertEquals("[element=\"value\"] Invalid attribute value \"\": Attribute value must not be empty (\"\")", e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseExtensionOnPrimitive() throws IOException {
|
||||
String input = IOUtils.toString(JsonParserR4Test.class.getResourceAsStream("/extension-on-line.txt"), Constants.CHARSET_UTF8);
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package ca.uhn.fhir.rest.server.interceptor;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||
import ca.uhn.fhir.test.utilities.server.HashMapResourceProviderRule;
|
||||
import ca.uhn.fhir.test.utilities.server.RestfulServerRule;
|
||||
import ca.uhn.test.concurrency.PointcutLatch;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
|
@ -28,13 +30,17 @@ import java.util.regex.Pattern;
|
|||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.awaitility.Awaitility.waitAtMost;
|
||||
import static org.hamcrest.CoreMatchers.not;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.matchesPattern;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.atMost;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.timeout;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
|
@ -63,11 +69,18 @@ public class ResponseSizeCapturingInterceptorTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testReadResource() {
|
||||
public void testReadResource() throws InterruptedException {
|
||||
PointcutLatch createLatch = new PointcutLatch(Pointcut.SERVER_PROCESSING_COMPLETED);
|
||||
createLatch.setExpectedCount(1);
|
||||
ourServerRule.getRestfulServer().getInterceptorService().registerAnonymousInterceptor(Pointcut.SERVER_PROCESSING_COMPLETED, createLatch);
|
||||
|
||||
Patient resource = new Patient();
|
||||
resource.setActive(true);
|
||||
IIdType id = ourServerRule.getFhirClient().create().resource(resource).execute().getId().toUnqualifiedVersionless();
|
||||
|
||||
createLatch.awaitExpected();
|
||||
ourServerRule.getRestfulServer().getInterceptorService().unregisterInterceptor(createLatch);
|
||||
|
||||
myInterceptor.registerConsumer(myConsumer);
|
||||
|
||||
List<String> stacks = Collections.synchronizedList(new ArrayList<>());
|
||||
|
@ -84,8 +97,8 @@ public class ResponseSizeCapturingInterceptorTest {
|
|||
resource = ourServerRule.getFhirClient().read().resource(Patient.class).withId(id).execute();
|
||||
assertEquals(true, resource.getActive());
|
||||
|
||||
await().until(()->stacks.size() > 0);
|
||||
await().until(()->stacks.stream().collect(Collectors.joining("\n")), not(matchesPattern(Pattern.compile(".*INVOCATION.*INVOCATION.*", Pattern.MULTILINE | Pattern.DOTALL))));
|
||||
verify(myConsumer, timeout(Duration.ofSeconds(10)).times(1)).accept(myResultCaptor.capture());
|
||||
assertEquals(100, myResultCaptor.getValue().getWrittenChars());
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
package ca.uhn.fhir.test.utilities;
|
||||
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.springframework.aop.framework.AopProxyUtils;
|
||||
|
||||
public class ProxyUtil {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T getSingletonTarget(Object theSource, Class<T> theSubscriptionTriggeringSvcClass) {
|
||||
Validate.notNull(theSource);
|
||||
if (theSubscriptionTriggeringSvcClass.isAssignableFrom(theSource.getClass())) {
|
||||
return (T) theSource;
|
||||
}
|
||||
return (T) AopProxyUtils.getSingletonTarget(theSource);
|
||||
}
|
||||
|
||||
}
|
|
@ -72,7 +72,7 @@ public class ResourceValidatorDstu2_1Test {
|
|||
parser.parseResource(encoded);
|
||||
fail();
|
||||
} catch (DataFormatException e) {
|
||||
assertEquals("DataFormatException at [[row,col {unknown-source}]: [2,4]]: Invalid attribute value \"2000-15-31\": Invalid date/time format: \"2000-15-31\"", e.getMessage());
|
||||
assertEquals("DataFormatException at [[row,col {unknown-source}]: [2,4]]: [element=\"birthDate\"] Invalid attribute value \"2000-15-31\": Invalid date/time format: \"2000-15-31\"", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -98,7 +98,7 @@ public class ResourceValidatorDstu3Test {
|
|||
parser.parseResource(encoded);
|
||||
fail();
|
||||
} catch (DataFormatException e) {
|
||||
assertEquals("DataFormatException at [[row,col {unknown-source}]: [2,4]]: Invalid attribute value \"2000-15-31\": Invalid date/time format: \"2000-15-31\"", e.getMessage());
|
||||
assertEquals("DataFormatException at [[row,col {unknown-source}]: [2,4]]: [element=\"birthDate\"] Invalid attribute value \"2000-15-31\": Invalid date/time format: \"2000-15-31\"", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue