Implement FHIR Patch (#1850)

* Start working on FHIRPatch

* More work on fhirpatch

* Work on FHIR Patch

* Add patch

* Test fixes

* Test fixes

* Get tests fixed

* Chnage to trigger a build

* Compile fix

* Dependency version fixes

* Test fix

* COmpile fix

* Try to fix build

* Test fix attempt

* Another build attempt

* Another build tweak

* Cleanup
This commit is contained in:
James Agnew 2020-05-20 19:43:55 -04:00 committed by GitHub
parent b044d05332
commit 5b2181a563
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
89 changed files with 3555 additions and 950 deletions

View File

@ -33,7 +33,7 @@ jobs:
# These are Maven CLI options (and show up in the build logs) - "-nsu"=Don't update snapshots. We can remove this when Maven OSS is more healthy
options: '-P ALLMODULES,JACOCO,CI,ERRORPRONE -nsu -e -B -Dmaven.repo.local=$(MAVEN_CACHE_FOLDER)'
# These are JVM options (and don't show up in the build logs)
mavenOptions: '-Xmx2048m $(MAVEN_OPTS) -Dorg.slf4j.simpleLogger.showDateTime=true -Dorg.slf4j.simpleLogger.dateTimeFormat=HH:mm:ss,SSS -Duser.timezone=America/Toronto'
mavenOptions: '-Xmx1024m $(MAVEN_OPTS) -Dorg.slf4j.simpleLogger.showDateTime=true -Dorg.slf4j.simpleLogger.dateTimeFormat=HH:mm:ss,SSS -Duser.timezone=America/Toronto'
jdkVersionOption: 1.11
- script: bash <(curl https://codecov.io/bash) -t $(CODECOV_TOKEN)
displayName: 'codecov'

View File

@ -164,6 +164,9 @@ public abstract class BaseRuntimeDeclaredChildDefinition extends BaseRuntimeChil
}
if (theClear) {
existingList.clear();
if (theValue == null) {
return;
}
}
existingList.add(theValue);
}

View File

@ -141,10 +141,10 @@ public abstract class BaseRuntimeElementCompositeDefinition<T extends IBase> ext
myChildren.add(theNext);
}
@Override
public BaseRuntimeChildDefinition getChildByName(String theName){
validateSealed();
BaseRuntimeChildDefinition retVal = myNameToChild.get(theName);
return retVal;
return myNameToChild.get(theName);
}
public BaseRuntimeChildDefinition getChildByNameOrThrowDataFormatException(String theName) throws DataFormatException {
@ -156,6 +156,7 @@ public abstract class BaseRuntimeElementCompositeDefinition<T extends IBase> ext
return retVal;
}
@Override
public List<BaseRuntimeChildDefinition> getChildren() {
validateSealed();
return myChildren;

View File

@ -66,6 +66,10 @@ public abstract class BaseRuntimeElementDefinition<T extends IBase> {
public abstract ChildTypeEnum getChildType();
public List<BaseRuntimeChildDefinition> getChildren() {
return Collections.emptyList();
}
@SuppressWarnings("unchecked")
private Constructor<T> getConstructor(@Nullable Object theArgument) {
@ -225,6 +229,10 @@ public abstract class BaseRuntimeElementDefinition<T extends IBase> {
}
public BaseRuntimeChildDefinition getChildByName(String theChildName) {
return null;
}
public enum ChildTypeEnum {
COMPOSITE_DATATYPE,
/**

View File

@ -0,0 +1,111 @@
package ca.uhn.fhir.context;
import com.google.common.collect.Sets;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseExtension;
import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class RuntimeChildExt extends BaseRuntimeChildDefinition {
private Map<String, BaseRuntimeElementDefinition<?>> myNameToChild;
private Map<Class<? extends IBase>, BaseRuntimeElementDefinition<?>> myDatatypeToChild;
private Map<Class<? extends IBase>, String> myDatatypeToChildName;
@Override
public IAccessor getAccessor() {
return new IAccessor() {
@SuppressWarnings({"unchecked", "rawtypes"})
@Override
public List<IBase> getValues(IBase theTarget) {
List extension = ((IBaseHasExtensions) theTarget).getExtension();
return Collections.unmodifiableList(extension);
}
};
}
@Override
public BaseRuntimeElementDefinition<?> getChildByName(String theName) {
return myNameToChild.get(theName);
}
@Override
public BaseRuntimeElementDefinition<?> getChildElementDefinitionByDatatype(Class<? extends IBase> theType) {
return myDatatypeToChild.get(theType);
}
@Override
public String getChildNameByDatatype(Class<? extends IBase> theDatatype) {
return myDatatypeToChildName.get(theDatatype);
}
@Override
public String getElementName() {
return "extension";
}
@Override
public int getMax() {
return -1;
}
@Override
public int getMin() {
return 0;
}
@Override
public IMutator getMutator() {
return new IMutator() {
@Override
public void addValue(IBase theTarget, IBase theValue) {
List extensions = ((IBaseHasExtensions) theTarget).getExtension();
IBaseExtension<?, ?> value = (IBaseExtension<?, ?>) theValue;
extensions.add(value);
}
@Override
public void setValue(IBase theTarget, IBase theValue) {
List extensions = ((IBaseHasExtensions) theTarget).getExtension();
extensions.clear();
if (theValue != null) {
IBaseExtension<?, ?> value = (IBaseExtension<?, ?>) theValue;
extensions.add(value);
}
}
};
}
@Override
public Set<String> getValidChildNames() {
return Sets.newHashSet("extension");
}
@Override
public boolean isSummary() {
return false;
}
@Override
void sealAndInitialize(FhirContext theContext, Map<Class<? extends IBase>, BaseRuntimeElementDefinition<?>> theClassToElementDefinitions) {
myNameToChild = new HashMap<>();
myDatatypeToChild = new HashMap<>();
myDatatypeToChildName = new HashMap<>();
for (BaseRuntimeElementDefinition<?> next : theClassToElementDefinitions.values()) {
if (next.getName().equals("Extension")) {
myNameToChild.put("extension", next);
myDatatypeToChild.put(next.getImplementingClass(), next);
myDatatypeToChildName.put(next.getImplementingClass(), "extension");
}
}
Validate.isTrue(!myNameToChild.isEmpty());
}
}

View File

@ -23,10 +23,14 @@ import static org.apache.commons.lang3.StringUtils.isBlank;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseDatatype;
import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import ca.uhn.fhir.model.api.annotation.DatatypeDef;
@ -38,6 +42,8 @@ public class RuntimePrimitiveDatatypeDefinition extends BaseRuntimeElementDefini
private BaseRuntimeElementDefinition<?> myProfileOf;
private Class<? extends IBaseDatatype> myProfileOfType;
private boolean mySpecialization;
private List<BaseRuntimeChildDefinition> myChildren;
private RuntimeChildExt myRuntimeChildExt;
public RuntimePrimitiveDatatypeDefinition(DatatypeDef theDef, Class<? extends IPrimitiveType<?>> theImplementingClass, boolean theStandardType) {
super(theDef.name(), theImplementingClass, theStandardType);
@ -56,6 +62,19 @@ public class RuntimePrimitiveDatatypeDefinition extends BaseRuntimeElementDefini
determineNativeType(theImplementingClass);
}
@Override
public List<BaseRuntimeChildDefinition> getChildren() {
return myChildren;
}
@Override
public BaseRuntimeChildDefinition getChildByName(String theChildName) {
if ("extension".equals(theChildName)) {
return myRuntimeChildExt;
}
return null;
}
private void determineNativeType(Class<? extends IPrimitiveType<?>> theImplementingClass) {
Class<?> clazz = theImplementingClass;
while (clazz.equals(Object.class) == false) {
@ -126,6 +145,14 @@ public class RuntimePrimitiveDatatypeDefinition extends BaseRuntimeElementDefini
throw new ConfigurationException(b.toString());
}
}
myRuntimeChildExt = new RuntimeChildExt();
myRuntimeChildExt.sealAndInitialize(theContext, theClassToElementDefinitions);
myChildren = new ArrayList<>();
myChildren.addAll(super.getChildren());
myChildren.add(myRuntimeChildExt);
myChildren = Collections.unmodifiableList(myChildren);
}
}

View File

@ -20,8 +20,19 @@ package ca.uhn.fhir.parser;
* #L%
*/
import ca.uhn.fhir.context.*;
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
import ca.uhn.fhir.context.BaseRuntimeDeclaredChildDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementDefinition.ChildTypeEnum;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.RuntimeChildChoiceDefinition;
import ca.uhn.fhir.context.RuntimeChildContainedResources;
import ca.uhn.fhir.context.RuntimeChildDirectResource;
import ca.uhn.fhir.context.RuntimeChildNarrativeDefinition;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.model.api.IIdentifiableElement;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.ISupportsUndeclaredExtensions;
@ -29,6 +40,7 @@ import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.model.api.Tag;
import ca.uhn.fhir.model.api.TagList;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.parser.path.EncodeContextPath;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.util.BundleUtil;
@ -36,9 +48,19 @@ import ca.uhn.fhir.util.UrlUtil;
import com.google.common.base.Charsets;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.hl7.fhir.instance.model.api.*;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseCoding;
import org.hl7.fhir.instance.model.api.IBaseElement;
import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
import org.hl7.fhir.instance.model.api.IBaseHasModifierExtensions;
import org.hl7.fhir.instance.model.api.IBaseMetaType;
import org.hl7.fhir.instance.model.api.IBaseReference;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IDomainResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import javax.annotation.Nullable;
import java.io.IOException;
@ -49,7 +71,17 @@ import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.isBlank;
@ -73,8 +105,8 @@ public abstract class BaseParser implements IParser {
private ContainedResources myContainedResources;
private boolean myEncodeElementsAppliesToChildResourcesOnly;
private FhirContext myContext;
private List<ElementsPath> myDontEncodeElements;
private List<ElementsPath> myEncodeElements;
private List<EncodeContextPath> myDontEncodeElements;
private List<EncodeContextPath> myEncodeElements;
private Set<String> myEncodeElementsAppliesToResourceTypes;
private IIdType myEncodeForceResourceId;
private IParserErrorHandler myErrorHandler;
@ -95,7 +127,7 @@ public abstract class BaseParser implements IParser {
myErrorHandler = theParserErrorHandler;
}
List<ElementsPath> getDontEncodeElements() {
List<EncodeContextPath> getDontEncodeElements() {
return myDontEncodeElements;
}
@ -106,13 +138,13 @@ public abstract class BaseParser implements IParser {
} else {
myDontEncodeElements = theDontEncodeElements
.stream()
.map(ElementsPath::new)
.map(EncodeContextPath::new)
.collect(Collectors.toList());
}
return this;
}
List<ElementsPath> getEncodeElements() {
List<EncodeContextPath> getEncodeElements() {
return myEncodeElements;
}
@ -125,7 +157,7 @@ public abstract class BaseParser implements IParser {
} else {
myEncodeElements = theEncodeElements
.stream()
.map(ElementsPath::new)
.map(EncodeContextPath::new)
.collect(Collectors.toList());
myEncodeElementsAppliesToResourceTypes = new HashSet<>();
@ -1018,7 +1050,7 @@ public abstract class BaseParser implements IParser {
protected boolean shouldEncodeResource(String theName) {
if (myDontEncodeElements != null) {
for (ElementsPath next : myDontEncodeElements) {
for (EncodeContextPath next : myDontEncodeElements) {
if (next.equalsPath(theName)) {
return false;
}
@ -1047,6 +1079,20 @@ public abstract class BaseParser implements IParser {
}
/**
* EncodeContext is a shared state object that is passed around the
* encode process
*/
public class EncodeContext extends EncodeContextPath {
private final Map<Key, List<BaseParser.CompositeChildElement>> myCompositeChildrenCache = new HashMap<>();
public Map<Key, List<BaseParser.CompositeChildElement>> getCompositeChildrenCache() {
return myCompositeChildrenCache;
}
}
protected class CompositeChildElement {
private final BaseRuntimeChildDefinition myDef;
private final CompositeChildElement myParent;
@ -1127,7 +1173,7 @@ public abstract class BaseParser implements IParser {
}
private boolean checkIfParentShouldBeEncodedAndBuildPath() {
List<ElementsPath> encodeElements = myEncodeElements;
List<EncodeContextPath> encodeElements = myEncodeElements;
String currentResourceName = myEncodeContext.getResourcePath().get(myEncodeContext.getResourcePath().size() - 1).getName();
if (myEncodeElementsAppliesToResourceTypes != null && !myEncodeElementsAppliesToResourceTypes.contains(currentResourceName)) {
@ -1158,7 +1204,7 @@ public abstract class BaseParser implements IParser {
return checkIfPathMatchesForEncoding(myDontEncodeElements, false);
}
private boolean checkIfPathMatchesForEncoding(List<ElementsPath> theElements, boolean theCheckingForEncodeElements) {
private boolean checkIfPathMatchesForEncoding(List<EncodeContextPath> theElements, boolean theCheckingForEncodeElements) {
boolean retVal = false;
if (myDef != null) {
@ -1172,9 +1218,9 @@ public abstract class BaseParser implements IParser {
} else {
EncodeContextPath currentResourcePath = myEncodeContext.getCurrentResourcePath();
ourLog.trace("Current resource path: {}", currentResourcePath);
for (ElementsPath next : theElements) {
for (EncodeContextPath next : theElements) {
if (next.startsWith(currentResourcePath)) {
if (next.startsWith(currentResourcePath, true)) {
if (theCheckingForEncodeElements || next.getPath().size() == currentResourcePath.getPath().size()) {
retVal = true;
break;
@ -1272,225 +1318,13 @@ public abstract class BaseParser implements IParser {
}
}
protected class EncodeContextPath {
private final List<EncodeContextPathElement> myPath;
public EncodeContextPath() {
myPath = new ArrayList<>(10);
}
public EncodeContextPath(List<EncodeContextPathElement> thePath) {
myPath = thePath;
}
@Override
public String toString() {
return myPath.stream().map(t -> t.toString()).collect(Collectors.joining("."));
}
protected List<EncodeContextPathElement> getPath() {
return myPath;
}
public EncodeContextPath getCurrentResourcePath() {
EncodeContextPath retVal = null;
for (int i = myPath.size() - 1; i >= 0; i--) {
if (myPath.get(i).isResource()) {
retVal = new EncodeContextPath(myPath.subList(i, myPath.size()));
break;
}
}
Validate.isTrue(retVal != null);
return retVal;
}
}
protected class ElementsPath extends EncodeContextPath {
protected ElementsPath(String thePath) {
StringTokenizer tok = new StringTokenizer(thePath, ".");
boolean first = true;
while (tok.hasMoreTokens()) {
String next = tok.nextToken();
if (first && next.equals("*")) {
getPath().add(new EncodeContextPathElement("*", true));
} else if (isNotBlank(next)) {
getPath().add(new EncodeContextPathElement(next, Character.isUpperCase(next.charAt(0))));
}
first = false;
}
}
public boolean startsWith(EncodeContextPath theCurrentResourcePath) {
for (int i = 0; i < getPath().size(); i++) {
if (theCurrentResourcePath.getPath().size() == i) {
return true;
}
EncodeContextPathElement expected = getPath().get(i);
EncodeContextPathElement actual = theCurrentResourcePath.getPath().get(i);
if (!expected.matches(actual)) {
return false;
}
}
return true;
}
public boolean equalsPath(String thePath) {
ElementsPath parsedPath = new ElementsPath(thePath);
return getPath().equals(parsedPath.getPath());
}
}
/**
* EncodeContext is a shared state object that is passed around the
* encode process
*/
protected class EncodeContext extends EncodeContextPath {
private final ArrayList<EncodeContextPathElement> myResourcePath = new ArrayList<>(10);
private final Map<Key, List<CompositeChildElement>> myCompositeChildrenCache = new HashMap<>();
public Map<Key, List<CompositeChildElement>> getCompositeChildrenCache() {
return myCompositeChildrenCache;
}
protected ArrayList<EncodeContextPathElement> getResourcePath() {
return myResourcePath;
}
public String getLeafElementName() {
return getPath().get(getPath().size() - 1).getName();
}
public String getLeafResourceName() {
return myResourcePath.get(myResourcePath.size() - 1).getName();
}
public String getLeafResourcePathFirstField() {
String retVal = null;
for (int i = getPath().size() - 1; i >= 0; i--) {
if (getPath().get(i).isResource()) {
break;
} else {
retVal = getPath().get(i).getName();
}
}
return retVal;
}
/**
* Add an element at the end of the path
*/
protected void pushPath(String thePathElement, boolean theResource) {
assert isNotBlank(thePathElement);
assert !thePathElement.contains(".");
assert theResource ^ Character.isLowerCase(thePathElement.charAt(0));
EncodeContextPathElement element = new EncodeContextPathElement(thePathElement, theResource);
getPath().add(element);
if (theResource) {
myResourcePath.add(element);
}
}
/**
* Remove the element at the end of the path
*/
public void popPath() {
EncodeContextPathElement removed = getPath().remove(getPath().size() - 1);
if (removed.isResource()) {
myResourcePath.remove(myResourcePath.size() - 1);
}
}
}
protected class EncodeContextPathElement {
private final String myName;
private final boolean myResource;
public EncodeContextPathElement(String theName, boolean theResource) {
Validate.notBlank(theName);
myName = theName;
myResource = theResource;
}
public boolean matches(EncodeContextPathElement theOther) {
if (myResource != theOther.isResource()) {
return false;
}
String otherName = theOther.getName();
if (myName.equals(otherName)) {
return true;
}
/*
* This is here to handle situations where a path like
* Observation.valueQuantity has been specified as an include/exclude path,
* since we only know that path as
* Observation.value
* until we get to actually looking at the values there.
*/
if (myName.length() > otherName.length() && myName.startsWith(otherName)) {
char ch = myName.charAt(otherName.length());
if (Character.isUpperCase(ch)) {
return true;
}
}
return myName.equals("*");
}
@Override
public boolean equals(Object theO) {
if (this == theO) {
return true;
}
if (theO == null || getClass() != theO.getClass()) {
return false;
}
EncodeContextPathElement that = (EncodeContextPathElement) theO;
return new EqualsBuilder()
.append(myResource, that.myResource)
.append(myName, that.myName)
.isEquals();
}
@Override
public int hashCode() {
return new HashCodeBuilder(17, 37)
.append(myName)
.append(myResource)
.toHashCode();
}
@Override
public String toString() {
if (myResource) {
return myName + "(res)";
}
return myName;
}
public String getName() {
return myName;
}
public boolean isResource() {
return myResource;
}
}
private static class Key {
private final BaseRuntimeElementCompositeDefinition<?> resDef;
private final boolean theContainedResource;
private final CompositeChildElement theParent;
private final EncodeContext theEncodeContext;
private final BaseParser.CompositeChildElement theParent;
private final BaseParser.EncodeContext theEncodeContext;
public Key(BaseRuntimeElementCompositeDefinition<?> resDef, final boolean theContainedResource, final CompositeChildElement theParent, EncodeContext theEncodeContext) {
public Key(BaseRuntimeElementCompositeDefinition<?> resDef, final boolean theContainedResource, final BaseParser.CompositeChildElement theParent, BaseParser.EncodeContext theEncodeContext) {
this.resDef = resDef;
this.theContainedResource = theContainedResource;
this.theParent = theParent;
@ -1524,6 +1358,7 @@ public abstract class BaseParser implements IParser {
}
}
static class ContainedResources {
private long myNextContainedId = 1;

View File

@ -104,12 +104,16 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
}
private boolean addToHeldExtensions(int valueIdx, List<? extends IBaseExtension<?, ?>> ext, ArrayList<ArrayList<HeldExtension>> list, boolean theIsModifier, CompositeChildElement theChildElem,
CompositeChildElement theParent, EncodeContext theEncodeContext, boolean theContainedResource, IBase theContainingElement) {
CompositeChildElement theParent, EncodeContext theEncodeContext, boolean theContainedResource, IBase theContainingElement) {
boolean retVal = false;
if (ext.size() > 0) {
Boolean encodeExtension = null;
for (IBaseExtension<?, ?> next : ext) {
if (next.isEmpty()) {
continue;
}
// Make sure we respect _summary and _elements
if (encodeExtension == null) {
encodeExtension = isEncodeExtension(theParent, theEncodeContext, theContainedResource, theContainingElement);

View File

@ -0,0 +1,141 @@
package ca.uhn.fhir.parser.path;
import org.apache.commons.lang3.Validate;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class EncodeContextPath {
private final List<EncodeContextPathElement> myPath;
private final ArrayList<EncodeContextPathElement> myResourcePath = new ArrayList<>(10);
public EncodeContextPath() {
this(new ArrayList<>(10));
}
public EncodeContextPath(String thePath) {
this();
StringTokenizer tok = new StringTokenizer(thePath, ".");
boolean first = true;
while (tok.hasMoreTokens()) {
String next = tok.nextToken();
if (first && next.equals("*")) {
getPath().add(new EncodeContextPathElement("*", true));
} else if (isNotBlank(next)) {
getPath().add(new EncodeContextPathElement(next, Character.isUpperCase(next.charAt(0))));
}
first = false;
}
}
public EncodeContextPath(List<EncodeContextPathElement> thePath) {
myPath = thePath;
}
@Override
public String toString() {
return myPath.stream().map(t -> t.toString()).collect(Collectors.joining("."));
}
public List<EncodeContextPathElement> getPath() {
return myPath;
}
public EncodeContextPath getCurrentResourcePath() {
EncodeContextPath retVal = null;
for (int i = myPath.size() - 1; i >= 0; i--) {
if (myPath.get(i).isResource()) {
retVal = new EncodeContextPath(myPath.subList(i, myPath.size()));
break;
}
}
Validate.isTrue(retVal != null);
return retVal;
}
/**
* Add an element at the end of the path
*/
public void pushPath(String thePathElement, boolean theResource) {
assert isNotBlank(thePathElement);
assert !thePathElement.contains(".");
assert theResource ^ Character.isLowerCase(thePathElement.charAt(0));
EncodeContextPathElement element = new EncodeContextPathElement(thePathElement, theResource);
getPath().add(element);
if (theResource) {
myResourcePath.add(element);
}
}
/**
* Remove the element at the end of the path
*/
public void popPath() {
EncodeContextPathElement removed = getPath().remove(getPath().size() - 1);
if (removed.isResource()) {
myResourcePath.remove(myResourcePath.size() - 1);
}
}
public ArrayList<EncodeContextPathElement> getResourcePath() {
return myResourcePath;
}
public String getLeafElementName() {
return getPath().get(getPath().size() - 1).getName();
}
public String getLeafResourceName() {
return myResourcePath.get(myResourcePath.size() - 1).getName();
}
public String getLeafResourcePathFirstField() {
String retVal = null;
for (int i = getPath().size() - 1; i >= 0; i--) {
if (getPath().get(i).isResource()) {
break;
} else {
retVal = getPath().get(i).getName();
}
}
return retVal;
}
/**
* Tests and returns whether this path starts with {@literal theCurrentResourcePath}
*
* @param theCurrentResourcePath The path to test
* @param theAllowSymmmetrical If <code>true</code>, this method will return true if {@literal theCurrentResourcePath} starts with this path as well as testing whether this path starts with {@literal theCurrentResourcePath}
*/
public boolean startsWith(EncodeContextPath theCurrentResourcePath, boolean theAllowSymmmetrical) {
for (int i = 0; i < getPath().size(); i++) {
if (theCurrentResourcePath.getPath().size() == i) {
return true;
}
EncodeContextPathElement expected = getPath().get(i);
EncodeContextPathElement actual = theCurrentResourcePath.getPath().get(i);
if (!expected.matches(actual)) {
return false;
}
}
if (theAllowSymmmetrical) {
return true;
}
return getPath().size() == theCurrentResourcePath.getPath().size();
}
public boolean equalsPath(String thePath) {
EncodeContextPath parsedPath = new EncodeContextPath(thePath);
return getPath().equals(parsedPath.getPath());
}
}

View File

@ -0,0 +1,83 @@
package ca.uhn.fhir.parser.path;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
public class EncodeContextPathElement {
private final String myName;
private final boolean myResource;
public EncodeContextPathElement(String theName, boolean theResource) {
Validate.notBlank(theName);
myName = theName;
myResource = theResource;
}
public boolean matches(EncodeContextPathElement theOther) {
if (myResource != theOther.isResource()) {
return false;
}
String otherName = theOther.getName();
if (myName.equals(otherName)) {
return true;
}
/*
* This is here to handle situations where a path like
* Observation.valueQuantity has been specified as an include/exclude path,
* since we only know that path as
* Observation.value
* until we get to actually looking at the values there.
*/
if (myName.length() > otherName.length() && myName.startsWith(otherName)) {
char ch = myName.charAt(otherName.length());
if (Character.isUpperCase(ch)) {
return true;
}
}
return myName.equals("*") || otherName.equals("*");
}
@Override
public boolean equals(Object theO) {
if (this == theO) {
return true;
}
if (theO == null || getClass() != theO.getClass()) {
return false;
}
EncodeContextPathElement that = (EncodeContextPathElement) theO;
return new EqualsBuilder()
.append(myResource, that.myResource)
.append(myName, that.myName)
.isEquals();
}
@Override
public int hashCode() {
return new HashCodeBuilder(17, 37)
.append(myName)
.append(myResource)
.toHashCode();
}
@Override
public String toString() {
if (myResource) {
return myName + "(res)";
}
return myName;
}
public String getName() {
return myName;
}
public boolean isResource() {
return myResource;
}
}

View File

@ -20,18 +20,29 @@ package ca.uhn.fhir.rest.api;
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.annotation.Patch;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.util.UrlUtil;
import javax.annotation.Nonnull;
import java.util.HashMap;
import java.util.Map;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isBlank;
/**
* Parameter type for methods annotated with {@link Patch}
*/
public enum PatchTypeEnum {
JSON_PATCH(Constants.CT_JSON_PATCH),
XML_PATCH(Constants.CT_XML_PATCH);
XML_PATCH(Constants.CT_XML_PATCH),
FHIR_PATCH_JSON(Constants.CT_FHIR_JSON_NEW),
FHIR_PATCH_XML(Constants.CT_FHIR_XML_NEW);
private static volatile Map<String, PatchTypeEnum> ourContentTypeToPatchType;
private final String myContentType;
PatchTypeEnum(String theContentType) {
@ -42,19 +53,36 @@ public enum PatchTypeEnum {
return myContentType;
}
public static PatchTypeEnum forContentTypeOrThrowInvalidRequestException(String theContentType) {
String contentType = theContentType;
@Nonnull
public static PatchTypeEnum forContentTypeOrThrowInvalidRequestException(FhirContext theContext, String theContentType) {
String contentType = defaultString(theContentType);
int semiColonIdx = contentType.indexOf(';');
if (semiColonIdx != -1) {
contentType = theContentType.substring(0, semiColonIdx);
}
contentType = contentType.trim();
if (Constants.CT_JSON_PATCH.equals(contentType)) {
return JSON_PATCH;
} else if (Constants.CT_XML_PATCH.equals(contentType)) {
return XML_PATCH;
} else {
throw new InvalidRequestException("Invalid Content-Type for PATCH operation: " + UrlUtil.sanitizeUrlPart(theContentType));
Map<String, PatchTypeEnum> map = ourContentTypeToPatchType;
if (map == null) {
map = new HashMap<>();
for (PatchTypeEnum next : values()) {
map.put(next.getContentType(), next);
}
ourContentTypeToPatchType = map;
}
PatchTypeEnum retVal = map.get(contentType);
if (retVal == null) {
if (isBlank(contentType)) {
String msg = theContext.getLocalizer().getMessage(PatchTypeEnum.class, "missingPatchContentType");
throw new InvalidRequestException(msg);
}
String msg = theContext.getLocalizer().getMessageSanitized(PatchTypeEnum.class, "invalidPatchContentType", contentType);
throw new InvalidRequestException(msg);
}
return retVal;
}
}

View File

@ -20,15 +20,23 @@ package ca.uhn.fhir.rest.gclient;
* #L%
*/
import org.hl7.fhir.instance.model.api.IBaseParameters;
public interface IPatch {
/**
* The body of the patch document serialized in either XML or JSON which conforms to
* http://jsonpatch.com/ or http://tools.ietf.org/html/rfc5261
*
* @param thePatchBody
* The body of the patch
*
* @param thePatchBody The body of the patch
*/
IPatchWithBody withBody(String thePatchBody);
/**
* The body of the patch document using FHIR Patch syntax as described at
* http://hl7.org/fhir/fhirpatch.html
*
* @since 5.1.0
*/
IPatchWithBody withFhirPatch(IBaseParameters thePatchBody);
}

View File

@ -28,11 +28,11 @@ public interface IPatchWithBody extends IPatchExecutable {
/**
* Build a conditional URL using fluent constants on resource types
*
*
* @param theResourceType
* The resource type to patch (e.g. "Patient.class")
*/
IPatchWithQuery conditional(Class<? extends IBaseResource> theClass);
IPatchWithQuery conditional(Class<? extends IBaseResource> theResourceType);
/**
* Build a conditional URL using fluent constants on resource types
@ -53,12 +53,12 @@ public interface IPatchWithBody extends IPatchExecutable {
IPatchExecutable conditionalByUrl(String theSearchUrl);
/**
* The resource ID to patch
* The resource ID to patch (must include both a resource type and an ID, e.g. <code>Patient/123</code>)
*/
IPatchExecutable withId(IIdType theId);
/**
* The resource ID to patch
* The resource ID to patch (must include both a resource type and an ID, e.g. <code>Patient/123</code>)
*/
IPatchExecutable withId(String theId);

View File

@ -267,12 +267,12 @@ public class BundleUtil {
* <code>Bundle.entry.resource</code> is a Binary resource with a patch
* payload type.
*/
public static boolean isDstu3TransactionPatch(IBaseResource thePayloadResource) {
public static boolean isDstu3TransactionPatch(FhirContext theContext, IBaseResource thePayloadResource) {
boolean isPatch = false;
if (thePayloadResource instanceof IBaseBinary) {
String contentType = ((IBaseBinary) thePayloadResource).getContentType();
try {
PatchTypeEnum.forContentTypeOrThrowInvalidRequestException(contentType);
PatchTypeEnum.forContentTypeOrThrowInvalidRequestException(theContext, contentType);
isPatch = true;
} catch (InvalidRequestException e) {
// ignore

View File

@ -1,7 +1,17 @@
package ca.uhn.fhir.util;
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.BaseRuntimeElementDefinition.ChildTypeEnum;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.RuntimeChildChoiceDefinition;
import ca.uhn.fhir.context.RuntimeChildDirectResource;
import ca.uhn.fhir.context.RuntimeExtensionDtDefinition;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.model.api.ExtensionDt;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.ISupportsUndeclaredExtensions;
@ -10,14 +20,30 @@ import ca.uhn.fhir.model.base.composite.BaseResourceReferenceDt;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.parser.DataFormatException;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.*;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseExtension;
import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
import org.hl7.fhir.instance.model.api.IBaseHasModifierExtensions;
import org.hl7.fhir.instance.model.api.IBaseReference;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.*;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
/*
* #%L
@ -241,6 +267,11 @@ public class FhirTerser {
return retVal.get(0);
}
public <T extends IBase> Optional<T> getSingleValue(IBase theTarget, String thePath, Class<T> theWantedType) {
return Optional.ofNullable(getSingleValueOrNull(theTarget, thePath, theWantedType));
}
private <T extends IBase> List<T> getValues(BaseRuntimeElementCompositeDefinition<?> theCurrentDef, IBase theCurrentObj, List<String> theSubList, Class<T> theWantedClass) {
return getValues(theCurrentDef, theCurrentObj, theSubList, theWantedClass, false, false);
}
@ -471,85 +502,85 @@ public class FhirTerser {
* Returns values stored in an element identified by its path. The list of values is of
* type {@link Object}.
*
* @param theResource The resource instance to be accessed. Must not be null.
* @param thePath The path for the element to be accessed.
* @param theElement The element to be accessed. Must not be null.
* @param thePath The path for the element to be accessed.@param theElement The resource instance to be accessed. Must not be null.
* @return A list of values of type {@link Object}.
*/
public List<IBase> getValues(IBaseResource theResource, String thePath) {
public List<IBase> getValues(IBase theElement, String thePath) {
Class<IBase> wantedClass = IBase.class;
return getValues(theResource, thePath, wantedClass);
return getValues(theElement, thePath, wantedClass);
}
/**
* Returns values stored in an element identified by its path. The list of values is of
* type {@link Object}.
*
* @param theResource The resource instance to be accessed. Must not be null.
* @param thePath The path for the element to be accessed.
* @param theCreate When set to <code>true</code>, the terser will create a null-valued element where none exists.
* @param theElement The element to be accessed. Must not be null.
* @param thePath The path for the element to be accessed.
* @param theCreate When set to <code>true</code>, the terser will create a null-valued element where none exists.
* @return A list of values of type {@link Object}.
*/
public List<IBase> getValues(IBaseResource theResource, String thePath, boolean theCreate) {
public List<IBase> getValues(IBase theElement, String thePath, boolean theCreate) {
Class<IBase> wantedClass = IBase.class;
return getValues(theResource, thePath, wantedClass, theCreate);
return getValues(theElement, thePath, wantedClass, theCreate);
}
/**
* Returns values stored in an element identified by its path. The list of values is of
* type {@link Object}.
*
* @param theResource The resource instance to be accessed. Must not be null.
* @param theElement The element to be accessed. Must not be null.
* @param thePath The path for the element to be accessed.
* @param theCreate When set to <code>true</code>, the terser will create a null-valued element where none exists.
* @param theAddExtension When set to <code>true</code>, the terser will add a null-valued extension where one or more such extensions already exist.
* @return A list of values of type {@link Object}.
*/
public List<IBase> getValues(IBaseResource theResource, String thePath, boolean theCreate, boolean theAddExtension) {
public List<IBase> getValues(IBase theElement, String thePath, boolean theCreate, boolean theAddExtension) {
Class<IBase> wantedClass = IBase.class;
return getValues(theResource, thePath, wantedClass, theCreate, theAddExtension);
return getValues(theElement, thePath, wantedClass, theCreate, theAddExtension);
}
/**
* Returns values stored in an element identified by its path. The list of values is of
* type <code>theWantedClass</code>.
*
* @param theResource The resource instance to be accessed. Must not be null.
* @param theElement The element to be accessed. Must not be null.
* @param thePath The path for the element to be accessed.
* @param theWantedClass The desired class to be returned in a list.
* @param <T> Type declared by <code>theWantedClass</code>
* @return A list of values of type <code>theWantedClass</code>.
*/
public <T extends IBase> List<T> getValues(IBaseResource theResource, String thePath, Class<T> theWantedClass) {
RuntimeResourceDefinition def = myContext.getResourceDefinition(theResource);
public <T extends IBase> List<T> getValues(IBase theElement, String thePath, Class<T> theWantedClass) {
BaseRuntimeElementCompositeDefinition<?> def = (BaseRuntimeElementCompositeDefinition<?>) myContext.getElementDefinition(theElement.getClass());
List<String> parts = parsePath(def, thePath);
return getValues(def, theResource, parts, theWantedClass);
return getValues(def, theElement, parts, theWantedClass);
}
/**
* Returns values stored in an element identified by its path. The list of values is of
* type <code>theWantedClass</code>.
*
* @param theResource The resource instance to be accessed. Must not be null.
* @param theElement The element to be accessed. Must not be null.
* @param thePath The path for the element to be accessed.
* @param theWantedClass The desired class to be returned in a list.
* @param theCreate When set to <code>true</code>, the terser will create a null-valued element where none exists.
* @param <T> Type declared by <code>theWantedClass</code>
* @return A list of values of type <code>theWantedClass</code>.
*/
public <T extends IBase> List<T> getValues(IBaseResource theResource, String thePath, Class<T> theWantedClass, boolean theCreate) {
RuntimeResourceDefinition def = myContext.getResourceDefinition(theResource);
public <T extends IBase> List<T> getValues(IBase theElement, String thePath, Class<T> theWantedClass, boolean theCreate) {
BaseRuntimeElementCompositeDefinition<?> def = (BaseRuntimeElementCompositeDefinition<?>) myContext.getElementDefinition(theElement.getClass());
List<String> parts = parsePath(def, thePath);
return getValues(def, theResource, parts, theWantedClass, theCreate, false);
return getValues(def, theElement, parts, theWantedClass, theCreate, false);
}
/**
* Returns values stored in an element identified by its path. The list of values is of
* type <code>theWantedClass</code>.
*
* @param theResource The resource instance to be accessed. Must not be null.
* @param theElement The element to be accessed. Must not be null.
* @param thePath The path for the element to be accessed.
* @param theWantedClass The desired class to be returned in a list.
* @param theCreate When set to <code>true</code>, the terser will create a null-valued element where none exists.
@ -557,10 +588,10 @@ public class FhirTerser {
* @param <T> Type declared by <code>theWantedClass</code>
* @return A list of values of type <code>theWantedClass</code>.
*/
public <T extends IBase> List<T> getValues(IBaseResource theResource, String thePath, Class<T> theWantedClass, boolean theCreate, boolean theAddExtension) {
RuntimeResourceDefinition def = myContext.getResourceDefinition(theResource);
public <T extends IBase> List<T> getValues(IBase theElement, String thePath, Class<T> theWantedClass, boolean theCreate, boolean theAddExtension) {
BaseRuntimeElementCompositeDefinition<?> def = (BaseRuntimeElementCompositeDefinition<?>) myContext.getElementDefinition(theElement.getClass());
List<String> parts = parsePath(def, thePath);
return getValues(def, theResource, parts, theWantedClass, theCreate, theAddExtension);
return getValues(def, theElement, parts, theWantedClass, theCreate, theAddExtension);
}
private List<String> parsePath(BaseRuntimeElementCompositeDefinition<?> theElementDef, String thePath) {
@ -801,7 +832,7 @@ public class FhirTerser {
}
/**
* Visit all elements in a given resource
* Visit all elements in a given resource or element
* <p>
* <b>THIS ALTERNATE METHOD IS STILL EXPERIMENTAL! USE WITH CAUTION</b>
* </p>
@ -810,12 +841,19 @@ public class FhirTerser {
* {@link BaseResourceReferenceDt#getResource()}) or embedded resources (e.g. Bundle.entry.resource)
* </p>
*
* @param theResource The resource to visit
* @param theVisitor The visitor
* @param theElement The element to visit
* @param theVisitor The visitor
*/
public void visit(IBaseResource theResource, IModelVisitor2 theVisitor) {
BaseRuntimeElementCompositeDefinition<?> def = myContext.getResourceDefinition(theResource);
visit(theResource, null, def, theVisitor, new ArrayList<>(), new ArrayList<>(), new ArrayList<>());
public void visit(IBase theElement, IModelVisitor2 theVisitor) {
BaseRuntimeElementDefinition<?> def = myContext.getElementDefinition(theElement.getClass());
if (def instanceof BaseRuntimeElementCompositeDefinition) {
BaseRuntimeElementCompositeDefinition<?> defComposite = (BaseRuntimeElementCompositeDefinition<?>) def;
visit(theElement, null, def, theVisitor, new ArrayList<>(), new ArrayList<>(), new ArrayList<>());
} else if (theElement instanceof IBaseExtension) {
theVisitor.acceptUndeclaredExtension((IBaseExtension<?, ?>) theElement, Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
} else {
theVisitor.acceptElement(theElement, Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
}
}
private void visit(Map<Object, Object> theStack, IBaseResource theResource, IBase theElement, List<String> thePathToElement, BaseRuntimeChildDefinition theChildDefinition,
@ -971,4 +1009,5 @@ public class FhirTerser {
});
}
}

View File

@ -20,17 +20,26 @@ package ca.uhn.fhir.util;
* #L%
*/
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.RuntimeResourceDefinition;
import ca.uhn.fhir.model.primitive.StringDt;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.*;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseDatatype;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseReference;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
@ -45,7 +54,7 @@ public class ParametersUtil {
}
public static List<Integer> getNamedParameterValuesAsInteger(FhirContext theCtx, IBaseParameters theParameters, String theParameterName) {
Function<IPrimitiveType<?>, Integer> mapper = t -> (Integer)t.getValue();
Function<IPrimitiveType<?>, Integer> mapper = t -> (Integer) t.getValue();
return extractNamedParameters(theCtx, theParameters, theParameterName, mapper);
}
@ -53,33 +62,72 @@ public class ParametersUtil {
return getNamedParameterValuesAsInteger(theCtx, theParameters, theParameterName).stream().findFirst();
}
private static <T> List<T> extractNamedParameters(FhirContext theCtx, IBaseParameters theParameters, String theParameterName, Function<IPrimitiveType<?>, T> theMapper) {
public static List<IBase> getNamedParameters(FhirContext theCtx, IBaseResource theParameters, String theParameterName) {
Validate.notNull(theParameters, "theParameters must not be null");
RuntimeResourceDefinition resDef = theCtx.getResourceDefinition(theParameters.getClass());
BaseRuntimeChildDefinition parameterChild = resDef.getChildByName("parameter");
List<IBase> parameterReps = parameterChild.getAccessor().getValues(theParameters);
return parameterReps
.stream()
.filter(param -> {
BaseRuntimeElementCompositeDefinition<?> nextParameterDef = (BaseRuntimeElementCompositeDefinition<?>) theCtx.getElementDefinition(param.getClass());
BaseRuntimeChildDefinition nameChild = nextParameterDef.getChildByName("name");
List<IBase> nameValues = nameChild.getAccessor().getValues(param);
Optional<? extends IPrimitiveType<?>> nameValue = nameValues
.stream()
.filter(t -> t instanceof IPrimitiveType<?>)
.map(t -> ((IPrimitiveType<?>) t))
.findFirst();
if (!nameValue.isPresent() || !theParameterName.equals(nameValue.get().getValueAsString())) {
return false;
}
return true;
})
.collect(Collectors.toList());
}
public static Optional<IBase> getParameterPart(FhirContext theCtx, IBase theParameter, String theParameterName) {
BaseRuntimeElementCompositeDefinition<?> nextParameterDef = (BaseRuntimeElementCompositeDefinition<?>) theCtx.getElementDefinition(theParameter.getClass());
BaseRuntimeChildDefinition valueChild = nextParameterDef.getChildByName("part");
List<IBase> parts = valueChild.getAccessor().getValues(theParameter);
for (IBase nextPart : parts) {
Optional<IPrimitiveType> name = theCtx.newTerser().getSingleValue(nextPart, "name", IPrimitiveType.class);
if (name.isPresent() && theParameterName.equals(name.get().getValueAsString())) {
return Optional.of(nextPart);
}
}
return Optional.empty();
}
public static Optional<IBase> getParameterPartValue(FhirContext theCtx, IBase theParameter, String theParameterName) {
Optional<IBase> part = getParameterPart(theCtx, theParameter, theParameterName);
if (part.isPresent()) {
return theCtx.newTerser().getSingleValue(part.get(), "value[x]", IBase.class);
} else {
return Optional.empty();
}
}
public static String getParameterPartValueAsString(FhirContext theCtx, IBase theParameter, String theParameterName) {
return getParameterPartValue(theCtx, theParameter, theParameterName).map(t -> (IPrimitiveType<?>) t).map(t -> t.getValueAsString()).orElse(null);
}
private static <T> List<T> extractNamedParameters(FhirContext theCtx, IBaseParameters theParameters, String theParameterName, Function<IPrimitiveType<?>, T> theMapper) {
List<T> retVal = new ArrayList<>();
for (IBase nextParameter : parameterReps) {
List<IBase> namedParameters = getNamedParameters(theCtx, theParameters, theParameterName);
for (IBase nextParameter : namedParameters) {
BaseRuntimeElementCompositeDefinition<?> nextParameterDef = (BaseRuntimeElementCompositeDefinition<?>) theCtx.getElementDefinition(nextParameter.getClass());
BaseRuntimeChildDefinition nameChild = nextParameterDef.getChildByName("name");
List<IBase> nameValues = nameChild.getAccessor().getValues(nextParameter);
Optional<? extends IPrimitiveType<?>> nameValue = nameValues
.stream()
.filter(t -> t instanceof IPrimitiveType<?>)
.map(t -> ((IPrimitiveType<?>) t))
.findFirst();
if (!nameValue.isPresent() || !theParameterName.equals(nameValue.get().getValueAsString())) {
continue;
}
BaseRuntimeChildDefinition valueChild = nextParameterDef.getChildByName("value[x]");
List<IBase> valueValues = valueChild.getAccessor().getValues(nextParameter);
valueValues
.stream()
.filter(t -> t instanceof IPrimitiveType<?>)
.map(t->((IPrimitiveType<?>) t))
.map(t -> ((IPrimitiveType<?>) t))
.map(theMapper)
.filter(t -> t != null)
.forEach(retVal::add);
@ -237,6 +285,13 @@ public class ParametersUtil {
addPart(theContext, theParameter, theName, value);
}
public static void addPartInteger(FhirContext theContext, IBase theParameter, String theName, Integer theInteger) {
IPrimitiveType<Integer> value = (IPrimitiveType<Integer>) theContext.getElementDefinition("integer").newInstance();
value.setValue(theInteger);
addPart(theContext, theParameter, theName, value);
}
public static void addPartString(FhirContext theContext, IBase theParameter, String theName, String theValue) {
IPrimitiveType<String> value = (IPrimitiveType<String>) theContext.getElementDefinition("string").newInstance();
value.setValue(theValue);
@ -267,7 +322,12 @@ public class ParametersUtil {
name.setValue(theName);
partChildElem.getChildByName("name").getMutator().addValue(part, name);
partChildElem.getChildByName("value[x]").getMutator().addValue(part, theValue);
if (theValue instanceof IBaseResource) {
partChildElem.getChildByName("resource").getMutator().addValue(part, theValue);
} else {
partChildElem.getChildByName("value[x]").getMutator().addValue(part, theValue);
}
}
public static void addPartResource(FhirContext theContext, IBase theParameter, String theName, IBaseResource theValue) {
@ -284,4 +344,5 @@ public class ParametersUtil {
partChildElem.getChildByName("resource").getMutator().addValue(part, theValue);
}
}

View File

@ -31,16 +31,42 @@ import org.apache.commons.text.StringEscapeUtils;
import org.codehaus.stax2.XMLOutputFactory2;
import org.codehaus.stax2.io.EscapingWriterFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.*;
import javax.xml.stream.FactoryConfigurationError;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLResolver;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import javax.xml.stream.events.XMLEvent;
import java.io.*;
import java.util.*;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.apache.commons.lang3.StringUtils.isBlank;
@ -1513,8 +1539,11 @@ public class XmlUtil {
VALID_ENTITY_NAMES = Collections.unmodifiableMap(validEntityNames);
}
/** Non-instantiable */
private XmlUtil() {}
/**
* Non-instantiable
*/
private XmlUtil() {
}
private static final class ExtendedEntityReplacingXmlResolver implements XMLResolver {
@Override
@ -1835,7 +1864,7 @@ public class XmlUtil {
}
public static Document parseDocument(String theInput) throws IOException, SAXException {
DocumentBuilder builder = null;
DocumentBuilder builder;
try {
DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
docBuilderFactory.setNamespaceAware(true);
@ -1860,4 +1889,13 @@ public class XmlUtil {
InputSource src = new InputSource(new StringReader(theInput));
return builder.parse(src);
}
public static String encodeDocument(Element theElement) throws TransformerException {
TransformerFactory transFactory = TransformerFactory.newInstance();
Transformer transformer = transFactory.newTransformer();
StringWriter buffer = new StringWriter();
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
transformer.transform(new DOMSource(theElement), new StreamResult(buffer));
return buffer.toString();
}
}

View File

@ -100,11 +100,17 @@ ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.unableToDeleteNotFound=Unable to fin
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.successfulCreate=Successfully created resource "{0}" in {1}ms
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.successfulUpdate=Successfully updated resource "{0}" in {1}ms
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.successfulDeletes=Successfully deleted {0} resource(s) in {1}ms
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.invalidSearchParameter=Unknown search parameter "{0}". Value search parameters for this search are: {1}
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.invalidSearchParameter=Unknown search parameter "{0}" for resource type "{1}". Valid search parameters for this search are: {2}
ca.uhn.fhir.rest.api.PatchTypeEnum.missingPatchContentType=Missing or invalid content type for PATCH operation
ca.uhn.fhir.rest.api.PatchTypeEnum.invalidPatchContentType=Invalid Content-Type for PATCH operation: {0}
ca.uhn.fhir.jpa.dao.TransactionProcessor.missingMandatoryResource=Missing required resource in Bundle.entry[{1}].resource for operation {0}
ca.uhn.fhir.jpa.dao.TransactionProcessor.missingPatchContentType=Missing or invalid content type for PATCH operation
ca.uhn.fhir.jpa.dao.TransactionProcessor.missingPatchBody=Unable to determine PATCH body from request
ca.uhn.fhir.jpa.dao.TransactionProcessor.fhirPatchShouldNotUseBinaryResource=Binary PATCH detected with FHIR content type. FHIR Patch should use Parameters resource.
ca.uhn.fhir.jpa.patch.FhirPatch.invalidInsertIndex=Invalid insert index {0} for path {1} - Only have {2} existing entries
ca.uhn.fhir.jpa.patch.FhirPatch.invalidMoveSourceIndex=Invalid move source index {0} for path {1} - Only have {2} existing entries
ca.uhn.fhir.jpa.patch.FhirPatch.invalidMoveDestinationIndex=Invalid move destination index {0} for path {1} - Only have {2} existing entries
ca.uhn.fhir.jpa.searchparam.extractor.BaseSearchParamExtractor.externalReferenceNotAllowed=Resource contains external reference to URL "{0}" but this server is not configured to allow external references
ca.uhn.fhir.jpa.searchparam.extractor.BaseSearchParamExtractor.failedToExtractPaths=Failed to extract values from resource using FHIRPath "{0}": {1}
@ -139,7 +145,7 @@ ca.uhn.fhir.jpa.term.BaseTermReadSvcImpl.cannotCreateDuplicateConceptMapUrl=Can
ca.uhn.fhir.jpa.term.BaseTermReadSvcImpl.cannotCreateDuplicateValueSetUrl=Can not create multiple ValueSet resources with ValueSet.url "{0}", already have one with resource ID: {1}
ca.uhn.fhir.jpa.term.BaseTermReadSvcImpl.expansionTooLarge=Expansion of ValueSet produced too many codes (maximum {0}) - Operation aborted!
ca.uhn.fhir.jpa.util.jsonpatch.JsonPatchUtils.failedToApplyPatch=Failed to apply JSON patch to {0}: {1}
ca.uhn.fhir.jpa.patch.JsonPatchUtils.failedToApplyPatch=Failed to apply JSON patch to {0}: {1}
ca.uhn.fhir.jpa.graphql.JpaStorageServices.invalidGraphqlArgument=Unknown GraphQL argument "{0}". Value GraphQL argument for this type are: {1}
@ -165,3 +171,5 @@ ca.uhn.fhir.jpa.partition.PartitionLookupSvcImpl.cantRenameDefaultPartition=Can
ca.uhn.fhir.rest.server.interceptor.partition.RequestTenantPartitionInterceptor.unknownTenantName=Unknown tenant: {0}
ca.uhn.fhir.jpa.dao.HistoryBuilder.noSystemOrTypeHistoryForPartitionAwareServer=Type- and Server- level history operation not supported across partitions on partitioned server
ca.uhn.fhir.jpa.provider.DiffProvider.cantDiffDifferentTypes=Unable to diff two resources of different types

View File

@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.demo;
*/
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory;
import org.apache.commons.dbcp2.BasicDataSource;
@ -59,7 +60,7 @@ public class CommonConfig {
/**
* The following bean configures the database connection. The 'url' property value of "jdbc:h2:file:target./jpaserver_h2_files" indicates that the server should save resources in a
* directory called "jpaserver_h2_files".
*
* <p>
* A URL to a remote database could also be placed here, along with login credentials and other properties supported by BasicDataSource.
*/
@Bean(destroyMethod = "close")
@ -94,12 +95,17 @@ public class CommonConfig {
extraProperties.put("hibernate.search.default.indexBase", "target/lucenefiles");
extraProperties.put("hibernate.search.lucene_version", "LUCENE_CURRENT");
extraProperties.put("hibernate.search.default.worker.execution", "async");
if (System.getProperty("lowmem") != null) {
extraProperties.put("hibernate.search.autoregister_listeners", "false");
}
return extraProperties;
}
@Bean
public PartitionSettings partitionSettings() {
return new PartitionSettings();
}
}

View File

@ -45,6 +45,7 @@ import ca.uhn.fhir.rest.server.ETagSupportEnum;
import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.interceptor.CorsInterceptor;
import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.WebApplicationContext;
@ -176,6 +177,9 @@ public class JpaServerDemo extends RestfulServer {
IInterceptorBroadcaster interceptorBroadcaster = myAppCtx.getBean(IInterceptorBroadcaster.class);
CascadingDeleteInterceptor cascadingDeleteInterceptor = new CascadingDeleteInterceptor(ctx, daoRegistry, interceptorBroadcaster);
getInterceptorService().registerInterceptor(cascadingDeleteInterceptor);
getInterceptorService().registerInterceptor(new ResponseHighlighterInterceptor());
}
}

View File

@ -1617,14 +1617,23 @@ public class GenericClient extends BaseClient implements IGenericClient {
return this;
}
@Override
public IPatchWithBody withFhirPatch(IBaseParameters thePatchBody) {
Validate.notNull(thePatchBody, "thePatchBody must not be null");
myPatchType = PatchTypeEnum.FHIR_PATCH_JSON;
myPatchBody = myContext.newJsonParser().encodeResourceToString(thePatchBody);
return this;
}
@Override
public IPatchExecutable withId(IIdType theId) {
if (theId == null) {
throw new NullPointerException("theId can not be null");
}
if (theId.hasIdPart() == false) {
throw new NullPointerException("theId must not be blank and must contain an ID, found: " + theId.getValue());
}
Validate.notBlank(theId.getIdPart(), "theId must not be blank and must contain a resource type and ID (e.g. \"Patient/123\"), found: %s", UrlUtil.sanitizeUrlPart(theId.getValue()));
Validate.notBlank(theId.getResourceType(), "theId must not be blank and must contain a resource type and ID (e.g. \"Patient/123\"), found: %s", UrlUtil.sanitizeUrlPart(theId.getValue()));
myId = theId;
return this;
}
@ -1634,11 +1643,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
if (theId == null) {
throw new NullPointerException("theId can not be null");
}
if (isBlank(theId)) {
throw new NullPointerException("theId must not be blank and must contain an ID, found: " + theId);
}
myId = new IdDt(theId);
return this;
return withId(new IdDt(theId));
}
}

View File

@ -0,0 +1,6 @@
---
type: fix
issue: 1850
title: "A new operation has been added to the JPA server called `$diff`. This operation can generate a FHIR Patch diff showing
the changes between two versions of a resource, or even two separate resources. See [Diff](/hapi-fhir/docs/server_jpa/diff.html)
for more information."

View File

@ -0,0 +1,5 @@
---
type: fix
issue: 1850
title: "The [FHIR Patch](https://www.hl7.org/fhir/fhirpatch.html) format is now supported for patching resources, in addition
to the previously supported JSON Patch and XML Patch."

View File

@ -0,0 +1,5 @@
---
type: fix
issue: 1850
title: When serializing a resource, the JSON Parser will now ignore any extensions that are present on an
element if they do not have a URL or any children populated.

View File

@ -209,6 +209,22 @@ FHIR also specifies a type of update called "conditional updates", where instead
**See Also:** See the description of [Update ETags](#update_etags) below for information on specifying a matching version in the client request.
# Patch - Instance
The PATCH operation can be used to modify a resource in place by supplying a delta
The following example shows how to perform a patch using a [FHIR Patch](http://hl7.org/fhir/fhirpatch.html)
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/GenericClientExample.java|patchFhir}}
```
The following example shows how to perform a patch using a [JSON Patch](https://tools.ietf.org/html/rfc6902.)
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/GenericClientExample.java|patchJson}}
```
# History - Server/Type/Instance
To retrieve the version history of all resources, or all resources of a given type, or of a specific instance of a resource, you call the [`history()`](/hapi-fhir/apidocs/hapi-fhir-base/ca/uhn/fhir/rest/client/api/IGenericClient.html#history()) method.

View File

@ -46,6 +46,7 @@ page.server_jpa.configuration=Configuration
page.server_jpa.search=Search
page.server_jpa.performance=Performance
page.server_jpa.upgrading=Upgrade Guide
page.server_jpa.diff=Diff Operation
section.server_jpa_partitioning.title=JPA Server: Partitioning and Multitenancy
page.server_jpa_partitioning.partitioning=Partitioning and Multitenancy

View File

@ -0,0 +1,88 @@
# Diff Operation
The `$diff` operation can be used to generate a differential between two versions of a resource, or even two different resources of the same type.
Differentials generated by this operation are in [FHIR Patch](https://www.hl7.org/fhir/fhirpatch.html) format.
In generated differentials, where a value has changed (i.e. a **replace** operation), an additional part value will be present on the given operation called `previousValue`. This part shows the value as it was in the *from* version of the resource.
# Diff Instance
When the $diff operation is invoked at the instance level (meaning it is invoked on a specific resource ID), it will compare two versions of the given resource.
## Parameters
* `[[#{T(ca.uhn.fhir.jpa.model.util.ProviderConstants).DIFF_FROM_VERSION_PARAMETER}]]=[versionId]`: (*optional*) If specified, compare using this version as the source. If not specified, the immediately previous version will be compared.
* `[[#{T(ca.uhn.fhir.jpa.model.util.ProviderConstants).DIFF_INCLUDE_META_PARAMETER}]]=true`: (*optional*) If specified, changes to Resource.meta will be included in the diff. This element is omitted by default.
To invoke:
```http
GET http://fhir.example.com/baseR4/Patient/123/$diff
```
The server will produce a response resembling the following:
```json
{
"resourceType": "Parameters",
"parameter": [ {
"name": "operation",
"part": [ {
"name": "type",
"valueCode": "replace"
}, {
"name": "path",
"valueString": "Patient.name.family"
}, {
"name": "previousValue",
"valueId": "Smyth"
}, {
"name": "value",
"valueId": "SmithB"
} ]
} ]
}
```
# Diff Instance
When the $diff operation is invoked at the instance level (meaning it is invoked on a specific resource ID), it will compare two versions of the given resource.
## Parameters
* `[[#{T(ca.uhn.fhir.jpa.model.util.ProviderConstants).DIFF_FROM_PARAMETER}]]=[reference]`: Specifies the source of the comparison. The value must include a resource type and a resource ID, and can optionally include a version, e.g. `Patient/123` or `Patient/123/_history/2`.
* `[[#{T(ca.uhn.fhir.jpa.model.util.ProviderConstants).DIFF_TO_PARAMETER}]]=[reference]`: Specifies the target of the comparison. The value must include a resource type and a resource ID, and can optionally include a version, e.g. `Patient/123` or `Patient/123/_history/2`.
* `[[#{T(ca.uhn.fhir.jpa.model.util.ProviderConstants).DIFF_INCLUDE_META_PARAMETER}]]=true`: (*optional*) If specified, changes to Resource.meta will be included in the diff. This element is omitted by default.
To invoke:
```http
GET http://fhir.example.com/baseR4/$diff?[[#{T(ca.uhn.fhir.jpa.model.util.ProviderConstants).DIFF_FROM_PARAMETER}]]=Patient/1&[[#{T(ca.uhn.fhir.jpa.model.util.ProviderConstants).DIFF_TO_PARAMETER}]]=Patient/2
```
The server will produce a response resembling the following:
```json
{
"resourceType": "Parameters",
"parameter": [ {
"name": "operation",
"part": [ {
"name": "type",
"valueCode": "replace"
}, {
"name": "path",
"valueString": "Patient.id"
}, {
"name": "previousValue",
"valueId": "1"
}, {
"name": "value",
"valueId": "2"
} ]
} ]
}
```

View File

@ -42,6 +42,7 @@ import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import org.hl7.fhir.instance.model.api.IBaseMetaType;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
@ -153,7 +154,7 @@ public interface IFhirResourceDao<T extends IBaseResource> extends IDao {
*/
<MT extends IBaseMetaType> MT metaGetOperation(Class<MT> theType, RequestDetails theRequestDetails);
DaoMethodOutcome patch(IIdType theId, String theConditionalUrl, PatchTypeEnum thePatchType, String thePatchBody, RequestDetails theRequestDetails);
DaoMethodOutcome patch(IIdType theId, String theConditionalUrl, PatchTypeEnum thePatchType, String thePatchBody, IBaseParameters theFhirPatchBody, RequestDetails theRequestDetails);
/**
* Read a resource - Note that this variant of the method does not take in a {@link RequestDetails} and

View File

@ -256,7 +256,6 @@
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.199</version>
<scope>test</scope>
</dependency>
<dependency>
@ -562,6 +561,11 @@
<artifactId>embedded-elasticsearch</artifactId>
<version>2.10.0</version>
</dependency>
<dependency>
<groupId>org.hl7.fhir.testcases</groupId>
<artifactId>fhir-test-cases</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
@ -576,8 +580,6 @@
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>16.0.3</version>
<scope>compile</scope>
</dependency>
</dependencies>
@ -669,7 +671,7 @@
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<runOrder>alphabetical</runOrder>
<argLine>@{argLine} ${surefire_jvm_args} -XX:+HeapDumpOnOutOfMemoryError -XX:+CrashOnOutOfMemoryError</argLine>
<argLine>@{argLine} ${surefire_jvm_args}</argLine>
<forkCount>0.6C</forkCount>
<excludes>*StressTest*</excludes>
</configuration>
@ -820,6 +822,21 @@
</plugins>
</build>
</profile>
<profile>
<id>CI</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<forkCount>1</forkCount>
<runOrder>alphabetical</runOrder>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<profile>
<!--
This profile is used on the Travis CI server because the full test suite

View File

@ -26,6 +26,7 @@ import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
import ca.uhn.fhir.jpa.partition.PartitionLookupSvcImpl;
import ca.uhn.fhir.jpa.partition.PartitionManagementProvider;
import ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc;
import ca.uhn.fhir.jpa.provider.DiffProvider;
import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider;
import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider;
import ca.uhn.fhir.jpa.sched.AutowiringSpringBeanJobFactory;
@ -251,6 +252,12 @@ public abstract class BaseConfig {
return new JpaConsentContextServices();
}
@Bean
@Lazy
public DiffProvider diffProvider() {
return new DiffProvider();
}
@Bean
@Lazy
public IPartitionLookupSvc partitionConfigSvc() {

View File

@ -35,6 +35,7 @@ import ca.uhn.fhir.jpa.api.model.ExpungeOptions;
import ca.uhn.fhir.jpa.api.model.ExpungeOutcome;
import ca.uhn.fhir.jpa.delete.DeleteConflictService;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.patch.FhirPatch;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import ca.uhn.fhir.jpa.model.entity.BaseHasResource;
import ca.uhn.fhir.jpa.model.entity.BaseTag;
@ -52,8 +53,8 @@ import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider;
import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster;
import ca.uhn.fhir.jpa.util.jsonpatch.JsonPatchUtils;
import ca.uhn.fhir.jpa.util.xmlpatch.XmlPatchUtils;
import ca.uhn.fhir.jpa.patch.JsonPatchUtils;
import ca.uhn.fhir.jpa.patch.XmlPatchUtils;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.CacheControlDirective;
@ -91,6 +92,7 @@ import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseCoding;
import org.hl7.fhir.instance.model.api.IBaseMetaType;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
@ -856,7 +858,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
}
@Override
public DaoMethodOutcome patch(IIdType theId, String theConditionalUrl, PatchTypeEnum thePatchType, String thePatchBody, RequestDetails theRequest) {
public DaoMethodOutcome patch(IIdType theId, String theConditionalUrl, PatchTypeEnum thePatchType, String thePatchBody, IBaseParameters theFhirPatchBody, RequestDetails theRequest) {
ResourceTable entityToUpdate;
if (isNotBlank(theConditionalUrl)) {
@ -886,10 +888,20 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
IBaseResource resourceToUpdate = toResource(entityToUpdate, false);
IBaseResource destination;
if (thePatchType == PatchTypeEnum.JSON_PATCH) {
switch (thePatchType) {
case JSON_PATCH:
destination = JsonPatchUtils.apply(getContext(), resourceToUpdate, thePatchBody);
} else {
break;
case XML_PATCH:
destination = XmlPatchUtils.apply(getContext(), resourceToUpdate, thePatchBody);
break;
case FHIR_PATCH_XML:
case FHIR_PATCH_JSON:
default:
IBaseParameters fhirPatchJson = theFhirPatchBody;
new FhirPatch(getContext()).apply(resourceToUpdate, fhirPatchJson);
destination = resourceToUpdate;
break;
}
@SuppressWarnings("unchecked")

View File

@ -34,13 +34,16 @@ import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster;
import ca.uhn.fhir.model.api.IQueryParameterAnd;
import ca.uhn.fhir.rest.api.QualifiedParamList;
import ca.uhn.fhir.rest.api.server.*;
import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails;
import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.SimplePreResourceAccessDetails;
import ca.uhn.fhir.rest.api.server.SimplePreResourceShowDetails;
import ca.uhn.fhir.rest.param.ParameterUtil;
import ca.uhn.fhir.rest.param.QualifierDetails;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.rest.server.method.SearchMethodBinding;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.BundleUtil;
import ca.uhn.fhir.util.FhirTerser;
@ -51,6 +54,8 @@ import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.InstantType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@ -58,7 +63,11 @@ import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.validation.constraints.NotNull;
import java.util.*;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import static ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.OO_SEVERITY_ERROR;
import static ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.OO_SEVERITY_INFO;
@ -66,6 +75,7 @@ import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public abstract class BaseStorageDao {
private static final Logger ourLog = LoggerFactory.getLogger(BaseStorageDao.class);
@Autowired
protected ISearchParamRegistry mySearchParamRegistry;
@ -213,7 +223,6 @@ public abstract class BaseStorageDao {
*/
protected abstract FhirContext getContext();
@Transactional(propagation = Propagation.SUPPORTS)
public void translateRawParameters(Map<String, List<String>> theSource, SearchParameterMap theTarget) {
if (theSource == null || theSource.isEmpty()) {

View File

@ -32,10 +32,8 @@ import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
import ca.uhn.fhir.jpa.api.model.DeleteConflict;
import ca.uhn.fhir.jpa.api.model.DeleteConflictList;
import ca.uhn.fhir.jpa.api.model.DeleteMethodOutcome;
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
import ca.uhn.fhir.jpa.delete.DeleteConflictService;
import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster;
@ -47,6 +45,8 @@ import ca.uhn.fhir.rest.api.PatchTypeEnum;
import ca.uhn.fhir.rest.api.PreferReturnEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
import ca.uhn.fhir.rest.param.ParameterUtil;
import ca.uhn.fhir.rest.server.RestfulServerUtils;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
@ -74,6 +74,7 @@ import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseBinary;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
@ -350,7 +351,7 @@ public abstract class BaseTransactionProcessor {
// Do all entries have a verb?
for (int i = 0; i < myVersionAdapter.getEntries(theRequest).size(); i++) {
IBase nextReqEntry = requestEntries.get(i);
String verb = myVersionAdapter.getEntryRequestVerb(nextReqEntry);
String verb = myVersionAdapter.getEntryRequestVerb(myContext, nextReqEntry);
if (verb == null || !isValidVerb(verb)) {
throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionEntryHasInvalidVerb", verb, i));
}
@ -372,7 +373,7 @@ public abstract class BaseTransactionProcessor {
for (int i = 0; i < requestEntries.size(); i++) {
originalRequestOrder.put(requestEntries.get(i), i);
myVersionAdapter.addEntry(response);
if (myVersionAdapter.getEntryRequestVerb(requestEntries.get(i)).equals("GET")) {
if (myVersionAdapter.getEntryRequestVerb(myContext, requestEntries.get(i)).equals("GET")) {
getEntries.add(requestEntries.get(i));
}
}
@ -544,7 +545,7 @@ public abstract class BaseTransactionProcessor {
IBase nextReqEntry = theEntries.get(index);
IBaseResource resource = myVersionAdapter.getResource(nextReqEntry);
if (resource != null) {
String verb = myVersionAdapter.getEntryRequestVerb(nextReqEntry);
String verb = myVersionAdapter.getEntryRequestVerb(myContext, nextReqEntry);
String entryUrl = myVersionAdapter.getFullUrl(nextReqEntry);
String requestUrl = myVersionAdapter.getEntryRequestUrl(nextReqEntry);
String ifNoneExist = myVersionAdapter.getEntryRequestIfNoneExist(nextReqEntry);
@ -647,7 +648,7 @@ public abstract class BaseTransactionProcessor {
}
String verb = myVersionAdapter.getEntryRequestVerb(nextReqEntry);
String verb = myVersionAdapter.getEntryRequestVerb(myContext, nextReqEntry);
String resourceType = res != null ? myContext.getResourceDefinition(res).getName() : null;
Integer order = theOriginalRequestOrder.get(nextReqEntry);
IBase nextRespEntry = (IBase) myVersionAdapter.getEntries(theResponse).get(order);
@ -768,6 +769,8 @@ public abstract class BaseTransactionProcessor {
matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl);
String patchBody = null;
String contentType = null;
IBaseParameters patchBodyParameters = null;
PatchTypeEnum patchType = null;
if (res instanceof IBaseBinary) {
IBaseBinary binary = (IBaseBinary) res;
@ -775,21 +778,26 @@ public abstract class BaseTransactionProcessor {
patchBody = new String(binary.getContent(), Charsets.UTF_8);
}
contentType = binary.getContentType();
patchType = PatchTypeEnum.forContentTypeOrThrowInvalidRequestException(myContext, contentType);
if (patchType == PatchTypeEnum.FHIR_PATCH_JSON || patchType == PatchTypeEnum.FHIR_PATCH_XML) {
String msg = myContext.getLocalizer().getMessage(TransactionProcessor.class, "fhirPatchShouldNotUseBinaryResource");
throw new InvalidRequestException(msg);
}
} else if (res instanceof IBaseParameters) {
patchBodyParameters = (IBaseParameters) res;
patchType = PatchTypeEnum.FHIR_PATCH_JSON;
}
if (isBlank(patchBody)) {
String msg = myContext.getLocalizer().getMessage(TransactionProcessor.class, "missingPatchBody");
throw new InvalidRequestException(msg);
}
if (isBlank(contentType)) {
String msg = myContext.getLocalizer().getMessage(TransactionProcessor.class, "missingPatchContentType");
throw new InvalidRequestException(msg);
if (patchBodyParameters == null) {
if (isBlank(patchBody)) {
String msg = myContext.getLocalizer().getMessage(TransactionProcessor.class, "missingPatchBody");
throw new InvalidRequestException(msg);
}
}
IFhirResourceDao<? extends IBaseResource> dao = toDao(parts, verb, url);
PatchTypeEnum patchType = PatchTypeEnum.forContentTypeOrThrowInvalidRequestException(contentType);
IIdType patchId = myContext.getVersion().newIdType().setValue(parts.getResourceId());
DaoMethodOutcome outcome = dao.patch(patchId, matchUrl, patchType, patchBody, theRequest);
DaoMethodOutcome outcome = dao.patch(patchId, matchUrl, patchType, patchBody, patchBodyParameters, theRequest);
updatedEntities.add(outcome.getEntity());
if (outcome.getResource() != null) {
updatedResources.add(outcome.getResource());
@ -1030,7 +1038,7 @@ public abstract class BaseTransactionProcessor {
}
private String toMatchUrl(IBase theEntry) {
String verb = myVersionAdapter.getEntryRequestVerb(theEntry);
String verb = myVersionAdapter.getEntryRequestVerb(myContext, theEntry);
if (verb.equals("POST")) {
return myVersionAdapter.getEntryIfNoneExist(theEntry);
}
@ -1069,7 +1077,7 @@ public abstract class BaseTransactionProcessor {
BUNDLEENTRY addEntry(BUNDLE theBundle);
String getEntryRequestVerb(BUNDLEENTRY theEntry);
String getEntryRequestVerb(FhirContext theContext, BUNDLEENTRY theEntry);
String getFullUrl(BUNDLEENTRY theEntry);
@ -1106,7 +1114,7 @@ public abstract class BaseTransactionProcessor {
//@formatter:off
public class TransactionSorter implements Comparator<IBase> {
private Set<String> myPlaceholderIds;
private final Set<String> myPlaceholderIds;
public TransactionSorter(Set<String> thePlaceholderIds) {
myPlaceholderIds = thePlaceholderIds;
@ -1159,8 +1167,8 @@ public abstract class BaseTransactionProcessor {
private int toOrder(IBase theO1) {
int o1 = 0;
if (myVersionAdapter.getEntryRequestVerb(theO1) != null) {
switch (myVersionAdapter.getEntryRequestVerb(theO1)) {
if (myVersionAdapter.getEntryRequestVerb(myContext, theO1) != null) {
switch (myVersionAdapter.getEntryRequestVerb(myContext, theO1)) {
case "DELETE":
o1 = 1;
break;
@ -1206,7 +1214,7 @@ public abstract class BaseTransactionProcessor {
}
private static String toStatusString(int theStatusCode) {
return Integer.toString(theStatusCode) + " " + defaultString(Constants.HTTP_STATUS_NAMES.get(theStatusCode));
return theStatusCode + " " + defaultString(Constants.HTTP_STATUS_NAMES.get(theStatusCode));
}

View File

@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.dao.dstu3;
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.TransactionProcessor;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
@ -102,7 +103,7 @@ public class TransactionProcessorVersionAdapterDstu3 implements TransactionProce
}
@Override
public String getEntryRequestVerb(Bundle.BundleEntryComponent theEntry) {
public String getEntryRequestVerb(FhirContext theContext, Bundle.BundleEntryComponent theEntry) {
String retVal = null;
Bundle.HTTPVerb value = theEntry.getRequest().getMethodElement().getValue();
if (value != null) {
@ -115,7 +116,7 @@ public class TransactionProcessorVersionAdapterDstu3 implements TransactionProce
*/
if (isBlank(retVal)) {
Resource resource = theEntry.getResource();
boolean isPatch = BundleUtil.isDstu3TransactionPatch(resource);
boolean isPatch = BundleUtil.isDstu3TransactionPatch(theContext, resource);
if (isPatch) {
retVal = "PATCH";

View File

@ -100,6 +100,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.isBlank;
@ -685,9 +686,10 @@ class PredicateBuilderReference extends BasePredicateBuilder {
}
}
} else {
throw new InvalidRequestException("Unknown search parameter " + theParamName + " for resource type " + theResourceName);
String validNames = new TreeSet<>(mySearchParamRegistry.getActiveSearchParams(theResourceName).keySet()).toString();
String msg = myContext.getLocalizer().getMessageSanitized(BaseHapiFhirResourceDao.class, "invalidSearchParameter", theParamName, theResourceName, validNames);
throw new InvalidRequestException(msg);
}
}
break;

View File

@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.dao.r4;
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.TransactionProcessor;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
@ -99,7 +100,7 @@ public class TransactionProcessorVersionAdapterR4 implements TransactionProcesso
}
@Override
public String getEntryRequestVerb(Bundle.BundleEntryComponent theEntry) {
public String getEntryRequestVerb(FhirContext theContext, Bundle.BundleEntryComponent theEntry) {
String retVal = null;
Bundle.HTTPVerb value = theEntry.getRequest().getMethodElement().getValue();
if (value != null) {

View File

@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.dao.r5;
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.TransactionProcessor;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
@ -99,7 +100,7 @@ public class TransactionProcessorVersionAdapterR5 implements TransactionProcesso
}
@Override
public String getEntryRequestVerb(Bundle.BundleEntryComponent theEntry) {
public String getEntryRequestVerb(FhirContext theContext, Bundle.BundleEntryComponent theEntry) {
String retVal = null;
Bundle.HTTPVerb value = theEntry.getRequest().getMethodElement().getValue();
if (value != null) {

View File

@ -0,0 +1,420 @@
package ca.uhn.fhir.jpa.patch;
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.parser.path.EncodeContextPath;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.util.IModelVisitor2;
import ca.uhn.fhir.util.ParametersUtil;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseEnumeration;
import org.hl7.fhir.instance.model.api.IBaseExtension;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.utilities.xhtml.XhtmlNode;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class FhirPatch {
private final FhirContext myContext;
private boolean myIncludePreviousValueInDiff;
private Set<EncodeContextPath> myIgnorePaths = Collections.emptySet();
public FhirPatch(FhirContext theContext) {
myContext = theContext;
}
/**
* Adds a path element that will not be included in generated diffs. Values can take the form
* <code>ResourceName.fieldName.fieldName</code> and wildcards are supported, such
* as <code>*.meta</code>.
*/
public void addIgnorePath(String theIgnorePath) {
Validate.notBlank(theIgnorePath, "theIgnorePath must not be null or empty");
if (myIgnorePaths.isEmpty()) {
myIgnorePaths = new HashSet<>();
}
myIgnorePaths.add(new EncodeContextPath(theIgnorePath));
}
public void setIncludePreviousValueInDiff(boolean theIncludePreviousValueInDiff) {
myIncludePreviousValueInDiff = theIncludePreviousValueInDiff;
}
public void apply(IBaseResource theResource, IBaseResource thePatch) {
List<IBase> opParameters = ParametersUtil.getNamedParameters(myContext, thePatch, "operation");
for (IBase nextOp : opParameters) {
String type = ParametersUtil.getParameterPartValueAsString(myContext, nextOp, "type");
String path = ParametersUtil.getParameterPartValueAsString(myContext, nextOp, "path");
Optional<IBase> valuePart = ParametersUtil.getParameterPart(myContext, nextOp, "value");
Optional<IBase> valuePartValue = ParametersUtil.getParameterPartValue(myContext, nextOp, "value");
type = defaultString(type);
path = defaultString(path);
String containingPath;
String elementName;
Integer removeIndex = null;
Integer insertIndex = null;
if ("delete".equals(type)) {
doDelete(theResource, path);
return;
} else if ("add".equals(type)) {
containingPath = path;
elementName = ParametersUtil.getParameterPartValueAsString(myContext, nextOp, "name");
} else if ("replace".equals(type)) {
int lastDot = path.lastIndexOf(".");
containingPath = path.substring(0, lastDot);
elementName = path.substring(lastDot + 1);
} else if ("insert".equals(type)) {
int lastDot = path.lastIndexOf(".");
containingPath = path.substring(0, lastDot);
elementName = path.substring(lastDot + 1);
insertIndex = ParametersUtil
.getParameterPartValue(myContext, nextOp, "index")
.map(t -> (IPrimitiveType<Integer>) t)
.map(t -> t.getValue())
.orElseThrow(() -> new InvalidRequestException("No index supplied for insert operation"));
} else if ("move".equals(type)) {
int lastDot = path.lastIndexOf(".");
containingPath = path.substring(0, lastDot);
elementName = path.substring(lastDot + 1);
insertIndex = ParametersUtil
.getParameterPartValue(myContext, nextOp, "destination")
.map(t -> (IPrimitiveType<Integer>) t)
.map(t -> t.getValue())
.orElseThrow(() -> new InvalidRequestException("No index supplied for insert operation"));
removeIndex = ParametersUtil
.getParameterPartValue(myContext, nextOp, "source")
.map(t -> (IPrimitiveType<Integer>) t)
.map(t -> t.getValue())
.orElseThrow(() -> new InvalidRequestException("No index supplied for insert operation"));
} else {
throw new InvalidRequestException("Unknown patch operation type: " + type);
}
List<IBase> paths = myContext.newFhirPath().evaluate(theResource, containingPath, IBase.class);
for (IBase next : paths) {
BaseRuntimeElementDefinition<?> elementDef = myContext.getElementDefinition(next.getClass());
String childName = elementName;
BaseRuntimeChildDefinition childDef = elementDef.getChildByName(childName);
BaseRuntimeElementDefinition<?> childElement;
if (childDef == null) {
childName = elementName + "[x]";
childDef = elementDef.getChildByName(childName);
childElement = childDef.getChildByName(childDef.getValidChildNames().iterator().next());
} else {
childElement = childDef.getChildByName(childName);
}
if ("move".equals(type)) {
List<IBase> existingValues = new ArrayList<>(childDef.getAccessor().getValues(next));
if (removeIndex == null || removeIndex >= existingValues.size()) {
String msg = myContext.getLocalizer().getMessage(FhirPatch.class, "invalidMoveSourceIndex", removeIndex, path, existingValues.size());
throw new InvalidRequestException(msg);
}
IBase newValue = existingValues.remove(removeIndex.intValue());
if (insertIndex == null || insertIndex > existingValues.size()) {
String msg = myContext.getLocalizer().getMessage(FhirPatch.class, "invalidMoveDestinationIndex", insertIndex, path, existingValues.size());
throw new InvalidRequestException(msg);
}
existingValues.add(insertIndex, newValue);
childDef.getMutator().setValue(next, null);
for (IBase nextNewValue : existingValues) {
childDef.getMutator().addValue(next, nextNewValue);
}
continue;
}
IBase newValue;
if (valuePartValue.isPresent()) {
newValue = valuePartValue.get();
} else {
newValue = childElement.newInstance();
if (valuePart.isPresent()) {
List<IBase> valuePartParts = myContext.newTerser().getValues(valuePart.get(), "part");
for (IBase nextValuePartPart : valuePartParts) {
String name = myContext.newTerser().getSingleValue(nextValuePartPart, "name", IPrimitiveType.class).map(t -> t.getValueAsString()).orElse(null);
if (isNotBlank(name)) {
Optional<IBase> value = myContext.newTerser().getSingleValue(nextValuePartPart, "value[x]", IBase.class);
if (value.isPresent()) {
BaseRuntimeChildDefinition partChildDef = childElement.getChildByName(name);
partChildDef.getMutator().addValue(newValue, value.get());
}
}
}
}
}
if (IBaseEnumeration.class.isAssignableFrom(childElement.getImplementingClass()) || XhtmlNode.class.isAssignableFrom(childElement.getImplementingClass())) {
// If the compositeElementDef is an IBaseEnumeration, we will use the actual compositeElementDef definition to build one, since
// it needs the right factory object passed to its constructor
IPrimitiveType<?> newValueInstance = (IPrimitiveType<?>) childElement.newInstance();
newValueInstance.setValueAsString(((IPrimitiveType<?>) newValue).getValueAsString());
childDef.getMutator().setValue(next, newValueInstance);
newValue = newValueInstance;
}
if ("insert".equals(type)) {
List<IBase> existingValues = new ArrayList<>(childDef.getAccessor().getValues(next));
if (insertIndex == null || insertIndex > existingValues.size()) {
String msg = myContext.getLocalizer().getMessage(FhirPatch.class, "invalidInsertIndex", insertIndex, path, existingValues.size());
throw new InvalidRequestException(msg);
}
existingValues.add(insertIndex, newValue);
childDef.getMutator().setValue(next, null);
for (IBase nextNewValue : existingValues) {
childDef.getMutator().addValue(next, nextNewValue);
}
} else {
childDef.getMutator().setValue(next, newValue);
}
}
}
}
private void doDelete(IBaseResource theResource, String thePath) {
List<IBase> paths = myContext.newFhirPath().evaluate(theResource, thePath, IBase.class);
for (IBase next : paths) {
myContext.newTerser().visit(next, new IModelVisitor2() {
@Override
public boolean acceptElement(IBase theElement, List<IBase> theContainingElementPath, List<BaseRuntimeChildDefinition> theChildDefinitionPath, List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) {
if (theElement instanceof IPrimitiveType) {
((IPrimitiveType<?>) theElement).setValueAsString(null);
}
return true;
}
@Override
public boolean acceptUndeclaredExtension(IBaseExtension<?, ?> theNextExt, List<IBase> theContainingElementPath, List<BaseRuntimeChildDefinition> theChildDefinitionPath, List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) {
theNextExt.setUrl(null);
theNextExt.setValue(null);
return true;
}
});
}
}
public IBaseParameters diff(@Nullable IBaseResource theOldValue, @Nonnull IBaseResource theNewValue) {
IBaseParameters retVal = ParametersUtil.newInstance(myContext);
String newValueTypeName = myContext.getResourceDefinition(theNewValue).getName();
if (theOldValue == null) {
IBase operation = ParametersUtil.addParameterToParameters(myContext, retVal, "operation");
ParametersUtil.addPartCode(myContext, operation, "type", "insert");
ParametersUtil.addPartString(myContext, operation, "path", newValueTypeName);
ParametersUtil.addPart(myContext, operation, "value", theNewValue);
} else {
String oldValueTypeName = myContext.getResourceDefinition(theOldValue).getName();
Validate.isTrue(oldValueTypeName.equalsIgnoreCase(newValueTypeName), "Resources must be of same type");
BaseRuntimeElementCompositeDefinition<?> def = myContext.getResourceDefinition(theOldValue).getBaseDefinition();
String path = def.getName();
EncodeContextPath contextPath = new EncodeContextPath();
contextPath.pushPath(path, true);
compare(retVal, contextPath, def, path, path, theOldValue, theNewValue);
contextPath.popPath();
assert contextPath.getPath().isEmpty();
}
return retVal;
}
private void compare(IBaseParameters theDiff, EncodeContextPath theSourceEncodeContext, BaseRuntimeElementDefinition<?> theDef, String theSourcePath, String theTargetPath, IBase theOldField, IBase theNewField) {
boolean pathIsIgnored = pathIsIgnored(theSourceEncodeContext);
if (pathIsIgnored) {
return;
}
BaseRuntimeElementDefinition<?> sourceDef = myContext.getElementDefinition(theOldField.getClass());
BaseRuntimeElementDefinition<?> targetDef = myContext.getElementDefinition(theNewField.getClass());
if (!sourceDef.getName().equals(targetDef.getName())) {
IBase operation = ParametersUtil.addParameterToParameters(myContext, theDiff, "operation");
ParametersUtil.addPartCode(myContext, operation, "type", "replace");
ParametersUtil.addPartString(myContext, operation, "path", theTargetPath);
addValueToDiff(operation, theOldField, theNewField);
} else {
if (theOldField instanceof IPrimitiveType) {
IPrimitiveType<?> oldPrimitive = (IPrimitiveType<?>) theOldField;
IPrimitiveType<?> newPrimitive = (IPrimitiveType<?>) theNewField;
String oldValueAsString = toValue(oldPrimitive);
String newValueAsString = toValue(newPrimitive);
if (!Objects.equals(oldValueAsString, newValueAsString)) {
IBase operation = ParametersUtil.addParameterToParameters(myContext, theDiff, "operation");
ParametersUtil.addPartCode(myContext, operation, "type", "replace");
ParametersUtil.addPartString(myContext, operation, "path", theTargetPath);
addValueToDiff(operation, oldPrimitive, newPrimitive);
}
}
List<BaseRuntimeChildDefinition> children = theDef.getChildren();
for (BaseRuntimeChildDefinition nextChild : children) {
compareField(theDiff, theSourceEncodeContext, theSourcePath, theTargetPath, theOldField, theNewField, nextChild);
}
}
}
private void compareField(IBaseParameters theDiff, EncodeContextPath theSourceEncodePath, String theSourcePath, String theTargetPath, IBase theOldField, IBase theNewField, BaseRuntimeChildDefinition theChildDef) {
String elementName = theChildDef.getElementName();
boolean repeatable = theChildDef.getMax() != 1;
theSourceEncodePath.pushPath(elementName, false);
if (pathIsIgnored(theSourceEncodePath)) {
theSourceEncodePath.popPath();
return;
}
List<? extends IBase> sourceValues = theChildDef.getAccessor().getValues(theOldField);
List<? extends IBase> targetValues = theChildDef.getAccessor().getValues(theNewField);
int sourceIndex = 0;
int targetIndex = 0;
while (sourceIndex < sourceValues.size() && targetIndex < targetValues.size()) {
IBase sourceChildField = sourceValues.get(sourceIndex);
Validate.notNull(sourceChildField); // not expected to happen, but just in case
BaseRuntimeElementDefinition<?> def = myContext.getElementDefinition(sourceChildField.getClass());
IBase targetChildField = targetValues.get(targetIndex);
Validate.notNull(targetChildField); // not expected to happen, but just in case
String sourcePath = theSourcePath + "." + elementName + (repeatable ? "[" + sourceIndex + "]" : "");
String targetPath = theSourcePath + "." + elementName + (repeatable ? "[" + targetIndex + "]" : "");
compare(theDiff, theSourceEncodePath, def, sourcePath, targetPath, sourceChildField, targetChildField);
sourceIndex++;
targetIndex++;
}
// Find newly inserted items
while (targetIndex < targetValues.size()) {
String path = theTargetPath + "." + elementName;
IBase operation = ParametersUtil.addParameterToParameters(myContext, theDiff, "operation");
ParametersUtil.addPartCode(myContext, operation, "type", "insert");
ParametersUtil.addPartString(myContext, operation, "path", path);
ParametersUtil.addPartInteger(myContext, operation, "index", targetIndex);
ParametersUtil.addPart(myContext, operation, "value", targetValues.get(targetIndex));
targetIndex++;
}
// Find deleted items
while (sourceIndex < sourceValues.size()) {
IBase operation = ParametersUtil.addParameterToParameters(myContext, theDiff, "operation");
ParametersUtil.addPartCode(myContext, operation, "type", "delete");
ParametersUtil.addPartString(myContext, operation, "path", theTargetPath + "." + elementName + (repeatable ? "[" + targetIndex + "]" : ""));
sourceIndex++;
targetIndex++;
}
theSourceEncodePath.popPath();
}
private void addValueToDiff(IBase theOperationPart, IBase theOldValue, IBase theNewValue) {
if (myIncludePreviousValueInDiff) {
IBase oldValue = massageValueForDiff(theOldValue);
ParametersUtil.addPart(myContext, theOperationPart, "previousValue", oldValue);
}
IBase newValue = massageValueForDiff(theNewValue);
ParametersUtil.addPart(myContext, theOperationPart, "value", newValue);
}
private boolean pathIsIgnored(EncodeContextPath theSourceEncodeContext) {
boolean pathIsIgnored = false;
for (EncodeContextPath next : myIgnorePaths) {
if (theSourceEncodeContext.startsWith(next, false)) {
pathIsIgnored = true;
break;
}
}
return pathIsIgnored;
}
private IBase massageValueForDiff(IBase theNewValue) {
// XHTML content is dealt with by putting it in a string
if (theNewValue instanceof XhtmlNode) {
String xhtmlString = ((XhtmlNode) theNewValue).getValueAsString();
theNewValue = myContext.getElementDefinition("string").newInstance(xhtmlString);
}
// IIdType can hold a fully qualified ID, but we just want the ID part to show up in diffs
if (theNewValue instanceof IIdType) {
String idPart = ((IIdType) theNewValue).getIdPart();
theNewValue = myContext.getElementDefinition("id").newInstance(idPart);
}
return theNewValue;
}
private String toValue(IPrimitiveType<?> theOldPrimitive) {
if (theOldPrimitive instanceof IIdType) {
return ((IIdType) theOldPrimitive).getIdPart();
}
return theOldPrimitive.getValueAsString();
}
}

View File

@ -0,0 +1,85 @@
package ca.uhn.fhir.jpa.patch;
/*
* #%L
* HAPI FHIR JPA Server
* %%
* 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.parser.DataFormatException;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.parser.StrictErrorHandler;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.fge.jsonpatch.JsonPatch;
import com.github.fge.jsonpatch.JsonPatchException;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.intellij.lang.annotations.Language;
import java.io.IOException;
import static org.apache.commons.lang3.StringUtils.defaultString;
public class JsonPatchUtils {
public static <T extends IBaseResource> T apply(FhirContext theCtx, T theResourceToUpdate, @Language("JSON") String thePatchBody) {
// Parse the patch
ObjectMapper mapper = new ObjectMapper();
mapper.configure(JsonParser.Feature.INCLUDE_SOURCE_IN_LOCATION, false);
JsonFactory factory = mapper.getFactory();
final JsonPatch patch;
try {
com.fasterxml.jackson.core.JsonParser parser = factory.createParser(thePatchBody);
JsonNode jsonPatchNode = mapper.readTree(parser);
patch = JsonPatch.fromJson(jsonPatchNode);
JsonNode originalJsonDocument = mapper.readTree(theCtx.newJsonParser().encodeResourceToString(theResourceToUpdate));
JsonNode after = patch.apply(originalJsonDocument);
@SuppressWarnings("unchecked")
Class<T> clazz = (Class<T>) theResourceToUpdate.getClass();
String postPatchedContent = mapper.writeValueAsString(after);
IParser fhirJsonParser = theCtx.newJsonParser();
fhirJsonParser.setParserErrorHandler(new StrictErrorHandler());
T retVal;
try {
retVal = fhirJsonParser.parseResource(clazz, postPatchedContent);
} catch (DataFormatException e) {
String resourceId = theResourceToUpdate.getIdElement().toUnqualifiedVersionless().getValue();
String resourceType = theCtx.getResourceDefinition(theResourceToUpdate).getName();
resourceId = defaultString(resourceId, resourceType);
String msg = theCtx.getLocalizer().getMessage(JsonPatchUtils.class, "failedToApplyPatch", resourceId, e.getMessage());
throw new InvalidRequestException(msg);
}
return retVal;
} catch (IOException | JsonPatchException theE) {
throw new InvalidRequestException(theE.getMessage());
}
}
}

View File

@ -0,0 +1,56 @@
package ca.uhn.fhir.jpa.patch;
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 java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
/*
* #%L
* HAPI FHIR JPA Server
* %%
* 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%
*/
public class XmlPatchUtils {
public static <T extends IBaseResource> T apply(FhirContext theCtx, T theResourceToUpdate, String thePatchBody) {
@SuppressWarnings("unchecked")
Class<T> clazz = (Class<T>) theResourceToUpdate.getClass();
String inputResource = theCtx.newXmlParser().encodeResourceToString(theResourceToUpdate);
ByteArrayOutputStream result = new ByteArrayOutputStream();
try {
Patcher.patch(new ByteArrayInputStream(inputResource.getBytes(Constants.CHARSET_UTF8)), new ByteArrayInputStream(thePatchBody.getBytes(Constants.CHARSET_UTF8)), result);
} catch (IOException e) {
throw new InternalErrorException(e);
}
String resultString = new String(result.toByteArray(), Constants.CHARSET_UTF8);
T retVal = theCtx.newXmlParser().parseResource(clazz, resultString);
return retVal;
}
}

View File

@ -38,11 +38,13 @@ import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.util.CoverageIgnore;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.Parameters;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.web.bind.annotation.RequestBody;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
@ -122,10 +124,10 @@ public abstract class BaseJpaResourceProvider<T extends IBaseResource> extends B
}
@Patch
public DaoMethodOutcome patch(HttpServletRequest theRequest, @IdParam IIdType theId, @ConditionalUrlParam String theConditionalUrl, RequestDetails theRequestDetails, @ResourceParam String theBody, PatchTypeEnum thePatchType) {
public DaoMethodOutcome patch(HttpServletRequest theRequest, @IdParam IIdType theId, @ConditionalUrlParam String theConditionalUrl, RequestDetails theRequestDetails, @ResourceParam String theBody, PatchTypeEnum thePatchType, @ResourceParam IBaseParameters theRequestBody) {
startRequest(theRequest);
try {
return myDao.patch(theId, theConditionalUrl, thePatchType, theBody, theRequestDetails);
return myDao.patch(theId, theConditionalUrl, thePatchType, theBody, theRequestBody, theRequestDetails);
} finally {
endRequest(theRequest);
}

View File

@ -0,0 +1,105 @@
package ca.uhn.fhir.jpa.provider;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.model.util.ProviderConstants;
import ca.uhn.fhir.jpa.patch.FhirPatch;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import com.google.common.base.Objects;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.Nonnull;
public class DiffProvider {
private static final Logger ourLog = LoggerFactory.getLogger(DiffProvider.class);
@Autowired
private FhirContext myContext;
@Autowired
private DaoRegistry myDaoRegistry;
@Operation(name = ProviderConstants.DIFF_OPERATION_NAME, global = true, idempotent = true)
public IBaseParameters diff(
@IdParam IIdType theResourceId,
@OperationParam(name = ProviderConstants.DIFF_FROM_VERSION_PARAMETER, typeName = "string", min = 0, max = 1) IPrimitiveType<?> theFromVersion,
@OperationParam(name = ProviderConstants.DIFF_INCLUDE_META_PARAMETER, typeName = "boolean", min = 0, max = 1) IPrimitiveType<Boolean> theIncludeMeta,
RequestDetails theRequestDetails) {
IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResourceId.getResourceType());
IBaseResource targetResource = dao.read(theResourceId, theRequestDetails);
IBaseResource sourceResource = null;
Long versionId = targetResource.getIdElement().getVersionIdPartAsLong();
if (theFromVersion == null || theFromVersion.getValueAsString() == null) {
// If no explicit from version is specified, find the next previous existing version
while (--versionId > 0L && sourceResource == null) {
IIdType nextVersionedId = theResourceId.withVersion(Long.toString(versionId));
try {
sourceResource = dao.read(nextVersionedId, theRequestDetails);
} catch (ResourceNotFoundException e) {
ourLog.trace("Resource version {} can not be found, most likely it was expunged", nextVersionedId);
}
}
} else {
long fromVersion = Long.parseLong(theFromVersion.getValueAsString());
sourceResource = dao.read(theResourceId.withVersion(Long.toString(fromVersion)), theRequestDetails);
}
FhirPatch fhirPatch = newPatch(theIncludeMeta);
IBaseParameters diff = fhirPatch.diff(sourceResource, targetResource);
return diff;
}
@Operation(name = ProviderConstants.DIFF_OPERATION_NAME, idempotent = true)
public IBaseParameters diff(
@OperationParam(name = ProviderConstants.DIFF_FROM_PARAMETER, typeName = "id", min = 1, max = 1) IIdType theFromVersion,
@OperationParam(name = ProviderConstants.DIFF_TO_PARAMETER, typeName = "id", min = 1, max = 1) IIdType theToVersion,
@OperationParam(name = ProviderConstants.DIFF_INCLUDE_META_PARAMETER, typeName = "boolean", min = 0, max = 1) IPrimitiveType<Boolean> theIncludeMeta,
RequestDetails theRequestDetails) {
if (!Objects.equal(theFromVersion.getResourceType(), theToVersion.getResourceType())) {
String msg = myContext.getLocalizer().getMessage(DiffProvider.class, "cantDiffDifferentTypes");
throw new InvalidRequestException(msg);
}
IFhirResourceDao dao = myDaoRegistry.getResourceDao(theFromVersion.getResourceType());
IBaseResource sourceResource = dao.read(theFromVersion, theRequestDetails);
IBaseResource targetResource = dao.read(theToVersion, theRequestDetails);
FhirPatch fhirPatch = newPatch(theIncludeMeta);
IBaseParameters diff = fhirPatch.diff(sourceResource, targetResource);
return diff;
}
@Nonnull
public FhirPatch newPatch(IPrimitiveType<Boolean> theIncludeMeta) {
FhirPatch fhirPatch = new FhirPatch(myContext);
fhirPatch.setIncludePreviousValueInDiff(true);
if (theIncludeMeta != null && theIncludeMeta.getValue()) {
ourLog.trace("Including resource metadata in patch");
} else {
fhirPatch.addIgnorePath("*.meta");
}
return fhirPatch;
}
}

View File

@ -958,7 +958,7 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu
myPatientDao.search(map).size();
fail();
} catch (InvalidRequestException e) {
assertEquals("Unknown search parameter foo for resource type Patient", e.getMessage());
assertEquals("Unknown search parameter \"foo\" for resource type \"Patient\". Valid search parameters for this search are: [_id, _language, active, address, address-city, address-country, address-postalcode, address-state, address-use, animal-breed, animal-species, birthdate, careprovider, deathdate, deceased, email, family, gender, given, identifier, language, link, name, organization, phone, phonetic, telecom]", e.getMessage());
}
}
@ -995,7 +995,7 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu
myPatientDao.search(map).size();
fail();
} catch (InvalidRequestException e) {
assertEquals("Unknown search parameter foo for resource type Patient", e.getMessage());
assertEquals("Unknown search parameter \"foo\" for resource type \"Patient\". Valid search parameters for this search are: [_id, _language, active, address, address-city, address-country, address-postalcode, address-state, address-use, animal-breed, animal-species, birthdate, careprovider, deathdate, deceased, email, family, gender, given, identifier, language, link, name, organization, phone, phonetic, telecom]", e.getMessage());
}
// Try with normal gender SP

View File

@ -1649,7 +1649,7 @@ public class FhirResourceDaoDstu2Test extends BaseJpaDstu2Test {
found = toList(myPatientDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Patient.SP_BIRTHDATE + "AAAA", new DateParam(ParamPrefixEnum.GREATERTHAN, "2000-01-01"))));
assertEquals(0, found.size());
} catch (InvalidRequestException e) {
assertEquals("Unknown search parameter birthdateAAAA for resource type Patient", e.getMessage());
assertEquals("Unknown search parameter \"birthdateAAAA\" for resource type \"Patient\". Valid search parameters for this search are: [_id, _language, active, address, address-city, address-country, address-postalcode, address-state, address-use, animal-breed, animal-species, birthdate, careprovider, deathdate, deceased, email, family, gender, given, identifier, language, link, name, organization, phone, phonetic, telecom]", e.getMessage());
}
}

View File

@ -1007,7 +1007,7 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu
myPatientDao.search(map).size();
fail();
} catch (InvalidRequestException e) {
assertEquals("Unknown search parameter foo for resource type Patient", e.getMessage());
assertEquals("Unknown search parameter \"foo\" for resource type \"Patient\". Valid search parameters for this search are: [_id, _language, active, address, address-city, address-country, address-postalcode, address-state, address-use, animal-breed, animal-species, birthdate, death-date, deceased, email, family, gender, general-practitioner, given, identifier, language, link, name, organization, phone, phonetic, telecom]", e.getMessage());
}
}
@ -1045,7 +1045,7 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu
myPatientDao.search(map).size();
fail();
} catch (InvalidRequestException e) {
assertEquals("Unknown search parameter foo for resource type Patient", e.getMessage());
assertEquals("Unknown search parameter \"foo\" for resource type \"Patient\". Valid search parameters for this search are: [_id, _language, active, address, address-city, address-country, address-postalcode, address-state, address-use, animal-breed, animal-species, birthdate, death-date, deceased, email, family, gender, general-practitioner, given, identifier, language, link, name, organization, phone, phonetic, telecom]", e.getMessage());
}
// Try with normal gender SP

View File

@ -2129,7 +2129,7 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test {
found = toList(myPatientDao.search(new SearchParameterMap(Patient.SP_BIRTHDATE + "AAAA", new DateParam(ParamPrefixEnum.GREATERTHAN, "2000-01-01")).setLoadSynchronous(true)));
assertEquals(0, found.size());
} catch (InvalidRequestException e) {
assertEquals("Unknown search parameter birthdateAAAA for resource type Patient", e.getMessage());
assertEquals("Unknown search parameter \"birthdateAAAA\" for resource type \"Patient\". Valid search parameters for this search are: [_id, _language, active, address, address-city, address-country, address-postalcode, address-state, address-use, animal-breed, animal-species, birthdate, death-date, deceased, email, family, gender, general-practitioner, given, identifier, language, link, name, organization, phone, phonetic, telecom]", e.getMessage());
}
}

View File

@ -79,6 +79,7 @@ import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.server.BasePagingProvider;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.test.utilities.ITestDataBuilder;
import ca.uhn.fhir.util.TestUtil;
import ca.uhn.fhir.util.UrlUtil;
import ca.uhn.fhir.validation.FhirValidator;
@ -89,6 +90,7 @@ import org.hibernate.search.jpa.Search;
import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport;
import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.AllergyIntolerance;
import org.hl7.fhir.r4.model.Appointment;
import org.hl7.fhir.r4.model.AuditEvent;
@ -175,7 +177,7 @@ import static org.mockito.Mockito.mock;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {TestR4Config.class})
public abstract class BaseJpaR4Test extends BaseJpaTest {
public abstract class BaseJpaR4Test extends BaseJpaTest implements ITestDataBuilder {
private static IValidationSupport ourJpaValidationSupportChainR4;
private static IFhirResourceDaoValueSet<ValueSet, Coding, CodeableConcept> ourValueSetDao;
@ -535,6 +537,23 @@ public abstract class BaseJpaR4Test extends BaseJpaTest {
myFhirCtx.setParserErrorHandler(new StrictErrorHandler());
}
@Override
public IIdType doCreateResource(IBaseResource theResource) {
IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResource.getClass());
return dao.create(theResource, mySrd).getId().toUnqualifiedVersionless();
}
@Override
public IIdType doUpdateResource(IBaseResource theResource) {
IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResource.getClass());
return dao.update(theResource, mySrd).getId().toUnqualifiedVersionless();
}
@Override
public FhirContext getFhirContext() {
return myFhirCtx;
}
@Override
protected FhirContext getContext() {
return myFhirCtx;

View File

@ -1278,7 +1278,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test
myPatientDao.search(map).size();
fail();
} catch (InvalidRequestException e) {
assertEquals("Unknown search parameter foo for resource type Patient", e.getMessage());
assertEquals("Unknown search parameter \"foo\" for resource type \"Patient\". Valid search parameters for this search are: [_id, _language, active, address, address-city, address-country, address-postalcode, address-state, address-use, birthdate, death-date, deceased, email, family, gender, general-practitioner, given, identifier, language, link, name, organization, phone, phonetic, telecom]", e.getMessage());
}
}
@ -1316,7 +1316,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test
myPatientDao.search(map).size();
fail();
} catch (InvalidRequestException e) {
assertEquals("Unknown search parameter foo for resource type Patient", e.getMessage());
assertEquals("Unknown search parameter \"foo\" for resource type \"Patient\". Valid search parameters for this search are: [_id, _language, active, address, address-city, address-country, address-postalcode, address-state, address-use, birthdate, death-date, deceased, email, family, gender, general-practitioner, given, identifier, language, link, name, organization, phone, phonetic, telecom]", e.getMessage());
}
// Try with normal gender SP

View File

@ -2498,7 +2498,7 @@ public class FhirResourceDaoR4Test extends BaseJpaR4Test {
found = toList(myPatientDao.search(new SearchParameterMap(Patient.SP_BIRTHDATE + "AAAA", new DateParam(ParamPrefixEnum.GREATERTHAN, "2000-01-01")).setLoadSynchronous(true)));
assertEquals(0, found.size());
} catch (InvalidRequestException e) {
assertEquals("Unknown search parameter birthdateAAAA for resource type Patient", e.getMessage());
assertEquals("Unknown search parameter \"birthdateAAAA\" for resource type \"Patient\". Valid search parameters for this search are: [_id, _language, active, address, address-city, address-country, address-postalcode, address-state, address-use, birthdate, death-date, deceased, email, family, gender, general-practitioner, given, identifier, language, link, name, organization, phone, phonetic, telecom]", e.getMessage());
}
}

View File

@ -87,7 +87,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@SuppressWarnings("unchecked")
public class PartitioningR4Test extends BaseJpaR4SystemTest implements ITestDataBuilder {
public class PartitioningR4Test extends BaseJpaR4SystemTest {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(PartitioningR4Test.class);
@ -2355,23 +2355,6 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest implements ITestData
};
}
@Override
public IIdType doCreateResource(IBaseResource theResource) {
IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResource.getClass());
return dao.create(theResource, mySrd).getId().toUnqualifiedVersionless();
}
@Override
public IIdType doUpdateResource(IBaseResource theResource) {
IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResource.getClass());
return dao.update(theResource, mySrd).getId().toUnqualifiedVersionless();
}
@Override
public FhirContext getFhirContext() {
return myFhirCtx;
}
@Interceptor
public static class MyReadWriteInterceptor extends MyWriteInterceptor {

View File

@ -0,0 +1,115 @@
package ca.uhn.fhir.jpa.patch;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.test.BaseTest;
import ca.uhn.fhir.util.ClasspathUtil;
import ca.uhn.fhir.util.XmlUtil;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Parameters;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import javax.annotation.Nonnull;
import javax.xml.transform.TransformerException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
@RunWith(Parameterized.class)
public abstract class BaseFhirPatchCoreTest extends BaseTest {
private static final Logger ourLog = LoggerFactory.getLogger(BaseFhirPatchCoreTest.class);
private final String myName;
private final String myMode;
private final IBaseResource myInput;
private final IBaseResource myPatch;
private final IBaseResource myOutput;
public BaseFhirPatchCoreTest(String theName, String theMode, IBaseResource theInput, IBaseResource thePatch, IBaseResource theOutput) {
myName = theName;
myMode = theMode;
myInput = theInput;
myPatch = thePatch;
myOutput = theOutput;
}
@Test
public void testApply() {
ourLog.info("Testing diff in {} mode: {}", myMode, myName);
if (myMode.equals("both") || myMode.equals("forwards")) {
FhirPatch patch = new FhirPatch(getContext());
patch.apply(myInput, myPatch);
String expected = getContext().newJsonParser().setPrettyPrint(true).encodeResourceToString(myOutput);
String actual = getContext().newJsonParser().setPrettyPrint(true).encodeResourceToString(myInput);
assertEquals(expected, actual);
} else {
fail("Unknown mode: " + myMode);
}
}
protected abstract FhirContext getContext();
@Nonnull
public static Collection<Object[]> loadTestSpec(FhirContext theContext, String theTestSpec) throws IOException, SAXException, TransformerException {
List<Object[]> retVal = new ArrayList<>();
String testsString = ClasspathUtil.loadResource(theTestSpec);
Document doc = XmlUtil.parseDocument(testsString);
Element tests = (Element) doc.getElementsByTagName("tests").item(0);
NodeList cases = tests.getElementsByTagName("case");
for (int i = 0; i < cases.getLength(); i++) {
Element next = (Element) cases.item(i);
String name = next.getAttribute("name");
String mode = next.getAttribute("mode");
Element diffElement = (Element) next.getElementsByTagName("diff").item(0);
Element diffParametersElement = getFirstChildElement(diffElement);
String encoded = XmlUtil.encodeDocument(diffParametersElement);
IBaseResource diff = theContext.newXmlParser().parseResource(encoded);
Element inputElement = (Element) next.getElementsByTagName("input").item(0);
Element inputResourceElement = getFirstChildElement(inputElement);
String inputEncoded = XmlUtil.encodeDocument(inputResourceElement);
IBaseResource input = theContext.newXmlParser().parseResource(inputEncoded);
Element outputElement = (Element) next.getElementsByTagName("output").item(0);
Element outputResourceElement = getFirstChildElement(outputElement);
String outputEncoded = XmlUtil.encodeDocument(outputResourceElement);
IBaseResource output = theContext.newXmlParser().parseResource(outputEncoded);
retVal.add(new Object[]{name, mode, input, diff, output});
}
return retVal;
}
private static Element getFirstChildElement(Element theInput) {
for (int i = 0; i < theInput.getChildNodes().getLength(); i++) {
if (theInput.getChildNodes().item(i) instanceof Element) {
return (Element) theInput.getChildNodes().item(i);
}
}
fail("No child of type Element");
throw new Error();
}
}

View File

@ -0,0 +1,185 @@
package ca.uhn.fhir.jpa.patch;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.CodeType;
import org.hl7.fhir.r4.model.IntegerType;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.StringType;
import org.junit.Test;
import java.util.stream.Collectors;
import static org.junit.Assert.assertEquals;
public class FhirPatchApplyR4Test {
private static final FhirContext ourCtx = FhirContext.forR4();
@Test
public void testInvalidOperation() {
FhirPatch svc = new FhirPatch(ourCtx);
Patient patient = new Patient();
Parameters patch = new Parameters();
Parameters.ParametersParameterComponent operation = patch.addParameter();
operation.setName("operation");
operation
.addPart()
.setName("type")
.setValue(new CodeType("foo"));
try {
svc.apply(patient, patch);
} catch (InvalidRequestException e) {
assertEquals("Unknown patch operation type: foo", e.getMessage());
}
}
@Test
public void testInsertToInvalidIndex() {
FhirPatch svc = new FhirPatch(ourCtx);
Patient patient = new Patient();
Parameters patch = new Parameters();
Parameters.ParametersParameterComponent operation = patch.addParameter();
operation.setName("operation");
operation
.addPart()
.setName("type")
.setValue(new CodeType("insert"));
operation
.addPart()
.setName("path")
.setValue(new StringType("Patient.identifier"));
operation
.addPart()
.setName("index")
.setValue(new IntegerType(2));
try {
svc.apply(patient, patch);
} catch (InvalidRequestException e) {
assertEquals("Invalid insert index 2 for path Patient.identifier - Only have 0 existing entries", e.getMessage());
}
}
@Test
public void testMoveFromInvalidIndex() {
FhirPatch svc = new FhirPatch(ourCtx);
Patient patient = new Patient();
Parameters patch = new Parameters();
Parameters.ParametersParameterComponent operation = patch.addParameter();
operation.setName("operation");
operation
.addPart()
.setName("type")
.setValue(new CodeType("move"));
operation
.addPart()
.setName("path")
.setValue(new StringType("Patient.identifier"));
operation
.addPart()
.setName("source")
.setValue(new IntegerType(2));
operation
.addPart()
.setName("destination")
.setValue(new IntegerType(1));
try {
svc.apply(patient, patch);
} catch (InvalidRequestException e) {
assertEquals("Invalid move source index 2 for path Patient.identifier - Only have 0 existing entries", e.getMessage());
}
}
@Test
public void testMoveToInvalidIndex() {
FhirPatch svc = new FhirPatch(ourCtx);
Patient patient = new Patient();
patient.addIdentifier().setSystem("sys");
Parameters patch = new Parameters();
Parameters.ParametersParameterComponent operation = patch.addParameter();
operation.setName("operation");
operation
.addPart()
.setName("type")
.setValue(new CodeType("move"));
operation
.addPart()
.setName("path")
.setValue(new StringType("Patient.identifier"));
operation
.addPart()
.setName("source")
.setValue(new IntegerType(0));
operation
.addPart()
.setName("destination")
.setValue(new IntegerType(1));
try {
svc.apply(patient, patch);
} catch (InvalidRequestException e) {
assertEquals("Invalid move destination index 1 for path Patient.identifier - Only have 0 existing entries", e.getMessage());
}
}
@Test
public void testDeleteItemWithExtension() {
FhirPatch svc = new FhirPatch(ourCtx);
Patient patient = new Patient();
patient.setActive(true);
patient.addIdentifier().addExtension("http://foo", new StringType("abc"));
patient.addIdentifier().setSystem("sys").setValue("val");
Parameters patch = new Parameters();
Parameters.ParametersParameterComponent operation = patch.addParameter();
operation.setName("operation");
operation
.addPart()
.setName("type")
.setValue(new CodeType("delete"));
operation
.addPart()
.setName("path")
.setValue(new StringType("Patient.identifier[0]"));
svc.apply(patient, patch);
assertEquals("{\"resourceType\":\"Patient\",\"identifier\":[{\"system\":\"sys\",\"value\":\"val\"}],\"active\":true}", ourCtx.newJsonParser().encodeResourceToString(patient));
}
public static String extractPartValuePrimitive(Parameters theDiff, int theIndex, String theParameterName, String thePartName) {
Parameters.ParametersParameterComponent component = theDiff.getParameter().stream().filter(t -> t.getName().equals(theParameterName)).collect(Collectors.toList()).get(theIndex);
Parameters.ParametersParameterComponent part = component.getPart().stream().filter(t -> t.getName().equals(thePartName)).findFirst().orElseThrow(() -> new IllegalArgumentException());
return ((IPrimitiveType) part.getValue()).getValueAsString();
}
public static <T extends IBase> T extractPartValue(Parameters theDiff, int theIndex, String theParameterName, String thePartName, Class<T> theExpectedType) {
Parameters.ParametersParameterComponent component = theDiff.getParameter().stream().filter(t -> t.getName().equals(theParameterName)).collect(Collectors.toList()).get(theIndex);
Parameters.ParametersParameterComponent part = component.getPart().stream().filter(t -> t.getName().equals(thePartName)).findFirst().orElseThrow(() -> new IllegalArgumentException());
if (IBaseResource.class.isAssignableFrom(theExpectedType)) {
return (T) part.getResource();
} else {
assert theExpectedType.isAssignableFrom(part.getValue().getClass());
return (T) part.getValue();
}
}
}

View File

@ -0,0 +1,443 @@
package ca.uhn.fhir.jpa.patch;
import ca.uhn.fhir.context.FhirContext;
import org.hl7.fhir.r4.model.BooleanType;
import org.hl7.fhir.r4.model.DateTimeType;
import org.hl7.fhir.r4.model.Extension;
import org.hl7.fhir.r4.model.Identifier;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.StringType;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static ca.uhn.fhir.jpa.patch.FhirPatchApplyR4Test.extractPartValue;
import static ca.uhn.fhir.jpa.patch.FhirPatchApplyR4Test.extractPartValuePrimitive;
import static org.junit.Assert.assertEquals;
public class FhirPatchDiffR4Test {
private static final Logger ourLog = LoggerFactory.getLogger(FhirPatchDiffR4Test.class);
private static final FhirContext ourCtx = FhirContext.forR4();
@Test
public void testReplaceIdentifier() {
Patient oldValue = new Patient();
oldValue.addIdentifier().setSystem("system-0").setValue("value-0");
Patient newValue = new Patient();
newValue.addIdentifier().setSystem("system-1").setValue("value-1");
FhirPatch svc = new FhirPatch(ourCtx);
Parameters diff = (Parameters) svc.diff(oldValue, newValue);
ourLog.info(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(diff));
assertEquals(2, diff.getParameter().size());
assertEquals("replace", extractPartValuePrimitive(diff, 0, "operation", "type"));
assertEquals("Patient.identifier[0].system", extractPartValuePrimitive(diff, 0, "operation", "path"));
assertEquals("system-1", extractPartValuePrimitive(diff, 0, "operation", "value"));
assertEquals("replace", extractPartValuePrimitive(diff, 0, "operation", "type"));
assertEquals("Patient.identifier[0].value", extractPartValuePrimitive(diff, 1, "operation", "path"));
assertEquals("value-1", extractPartValuePrimitive(diff, 1, "operation", "value"));
validateDiffProducesSameResults(oldValue, newValue, svc, diff);
}
@Test
public void testReplaceChoice() {
Patient oldValue = new Patient();
oldValue.setDeceased(new BooleanType(true));
Patient newValue = new Patient();
newValue.setDeceased(new DateTimeType("2020-05-16"));
FhirPatch svc = new FhirPatch(ourCtx);
Parameters diff = (Parameters) svc.diff(oldValue, newValue);
ourLog.info(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(diff));
assertEquals(1, diff.getParameter().size());
assertEquals("replace", extractPartValuePrimitive(diff, 0, "operation", "type"));
assertEquals("Patient.deceased", extractPartValuePrimitive(diff, 0, "operation", "path"));
assertEquals("2020-05-16", extractPartValuePrimitive(diff, 0, "operation", "value"));
validateDiffProducesSameResults(oldValue, newValue, svc, diff);
}
@Test
public void testReplaceChoice2() {
Patient oldValue = new Patient();
oldValue.setDeceased(new DateTimeType("2020-05-16"));
Patient newValue = new Patient();
newValue.setDeceased(new BooleanType(true));
FhirPatch svc = new FhirPatch(ourCtx);
Parameters diff = (Parameters) svc.diff(oldValue, newValue);
ourLog.info(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(diff));
assertEquals(1, diff.getParameter().size());
assertEquals("replace", extractPartValuePrimitive(diff, 0, "operation", "type"));
assertEquals("Patient.deceased", extractPartValuePrimitive(diff, 0, "operation", "path"));
assertEquals("true", extractPartValuePrimitive(diff, 0, "operation", "value"));
validateDiffProducesSameResults(oldValue, newValue, svc, diff);
}
@Test
public void testAddExtensionOnPrimitive() {
Patient oldValue = new Patient();
oldValue.setActive(true);
Patient newValue = new Patient();
newValue.setActive(true);
newValue.getActiveElement().addExtension("http://foo", new StringType("a value"));
FhirPatch svc = new FhirPatch(ourCtx);
Parameters diff = (Parameters) svc.diff(oldValue, newValue);
ourLog.info(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(diff));
assertEquals(1, diff.getParameter().size());
assertEquals("insert", extractPartValuePrimitive(diff, 0, "operation", "type"));
assertEquals("Patient.active.extension", extractPartValuePrimitive(diff, 0, "operation", "path"));
assertEquals("0", extractPartValuePrimitive(diff, 0, "operation", "index"));
assertEquals("http://foo", extractPartValue(diff, 0, "operation", "value", Extension.class).getUrl());
assertEquals("a value", extractPartValue(diff, 0, "operation", "value", Extension.class).getValueAsPrimitive().getValueAsString());
validateDiffProducesSameResults(oldValue, newValue, svc, diff);
}
@Test
public void testRemoveExtensionOnPrimitive() {
Patient oldValue = new Patient();
oldValue.setActive(true);
oldValue.getActiveElement().addExtension("http://foo", new StringType("a value"));
Patient newValue = new Patient();
newValue.setActive(true);
FhirPatch svc = new FhirPatch(ourCtx);
Parameters diff = (Parameters) svc.diff(oldValue, newValue);
ourLog.info(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(diff));
assertEquals(1, diff.getParameter().size());
assertEquals("delete", extractPartValuePrimitive(diff, 0, "operation", "type"));
assertEquals("Patient.active.extension[0]", extractPartValuePrimitive(diff, 0, "operation", "path"));
validateDiffProducesSameResults(oldValue, newValue, svc, diff);
}
@Test
public void testModifyExtensionOnPrimitive() {
Patient oldValue = new Patient();
oldValue.setActive(true);
oldValue.getActiveElement().addExtension("http://foo", new StringType("a value"));
Patient newValue = new Patient();
newValue.setActive(true);
newValue.getActiveElement().addExtension("http://foo", new StringType("a new value"));
FhirPatch svc = new FhirPatch(ourCtx);
Parameters diff = (Parameters) svc.diff(oldValue, newValue);
ourLog.info(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(diff));
assertEquals(1, diff.getParameter().size());
assertEquals("replace", extractPartValuePrimitive(diff, 0, "operation", "type"));
assertEquals("Patient.active.extension[0].value", extractPartValuePrimitive(diff, 0, "operation", "path"));
assertEquals("a new value", extractPartValuePrimitive(diff, 0, "operation", "value"));
validateDiffProducesSameResults(oldValue, newValue, svc, diff);
}
@Test
public void testAddExtensionOnComposite() {
Patient oldValue = new Patient();
oldValue.addName().setFamily("Family");
Patient newValue = new Patient();
newValue.addName().setFamily("Family");
newValue.getNameFirstRep().addExtension("http://foo", new StringType("a value"));
FhirPatch svc = new FhirPatch(ourCtx);
Parameters diff = (Parameters) svc.diff(oldValue, newValue);
ourLog.info(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(diff));
assertEquals(1, diff.getParameter().size());
assertEquals("insert", extractPartValuePrimitive(diff, 0, "operation", "type"));
assertEquals("Patient.name[0].extension", extractPartValuePrimitive(diff, 0, "operation", "path"));
assertEquals("0", extractPartValuePrimitive(diff, 0, "operation", "index"));
assertEquals("http://foo", extractPartValue(diff, 0, "operation", "value", Extension.class).getUrl());
assertEquals("a value", extractPartValue(diff, 0, "operation", "value", Extension.class).getValueAsPrimitive().getValueAsString());
validateDiffProducesSameResults(oldValue, newValue, svc, diff);
}
@Test
public void testRemoveExtensionOnComposite() {
Patient oldValue = new Patient();
oldValue.addName().setFamily("Family");
oldValue.getNameFirstRep().addExtension("http://foo", new StringType("a value"));
Patient newValue = new Patient();
newValue.addName().setFamily("Family");
FhirPatch svc = new FhirPatch(ourCtx);
Parameters diff = (Parameters) svc.diff(oldValue, newValue);
ourLog.info(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(diff));
assertEquals(1, diff.getParameter().size());
assertEquals("delete", extractPartValuePrimitive(diff, 0, "operation", "type"));
assertEquals("Patient.name[0].extension[0]", extractPartValuePrimitive(diff, 0, "operation", "path"));
validateDiffProducesSameResults(oldValue, newValue, svc, diff);
}
@Test
public void testModifyExtensionOnComposite() {
Patient oldValue = new Patient();
oldValue.addName().setFamily("Family");
oldValue.getNameFirstRep().addExtension("http://foo", new StringType("a value"));
Patient newValue = new Patient();
newValue.addName().setFamily("Family");
newValue.getNameFirstRep().addExtension("http://foo", new StringType("a new value"));
FhirPatch svc = new FhirPatch(ourCtx);
Parameters diff = (Parameters) svc.diff(oldValue, newValue);
ourLog.info(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(diff));
assertEquals(1, diff.getParameter().size());
assertEquals("replace", extractPartValuePrimitive(diff, 0, "operation", "type"));
assertEquals("Patient.name[0].extension[0].value", extractPartValuePrimitive(diff, 0, "operation", "path"));
assertEquals("a new value", extractPartValuePrimitive(diff, 0, "operation", "value"));
validateDiffProducesSameResults(oldValue, newValue, svc, diff);
}
@Test
public void testModifyId() {
Patient oldValue = new Patient();
oldValue.setId("http://foo/Patient/123/_history/2");
oldValue.getMeta().setVersionId("2");
oldValue.addName().setFamily("Family");
Patient newValue = new Patient();
newValue.setId("http://bar/Patient/456/_history/667");
newValue.getMeta().setVersionId("667");
newValue.addName().setFamily("Family");
FhirPatch svc = new FhirPatch(ourCtx);
Parameters diff = (Parameters) svc.diff(oldValue, newValue);
ourLog.info(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(diff));
assertEquals(2, diff.getParameter().size());
assertEquals("replace", extractPartValuePrimitive(diff, 0, "operation", "type"));
assertEquals("Patient.id", extractPartValuePrimitive(diff, 0, "operation", "path"));
assertEquals("456", extractPartValuePrimitive(diff, 0, "operation", "value"));
assertEquals("replace", extractPartValuePrimitive(diff, 1, "operation", "type"));
assertEquals("Patient.meta.versionId", extractPartValuePrimitive(diff, 1, "operation", "path"));
assertEquals("667", extractPartValuePrimitive(diff, 1, "operation", "value"));
validateDiffProducesSameResults(oldValue, newValue, svc, diff);
}
@Test
public void testModifyId_OnlyVersionDifferent() {
Patient oldValue = new Patient();
oldValue.setId("http://foo/Patient/123/_history/2");
oldValue.getMeta().setVersionId("2");
oldValue.addName().setFamily("Family");
Patient newValue = new Patient();
newValue.setId("http://foo/Patient/123/_history/3");
newValue.getMeta().setVersionId("3");
newValue.addName().setFamily("Family");
FhirPatch svc = new FhirPatch(ourCtx);
Parameters diff = (Parameters) svc.diff(oldValue, newValue);
ourLog.info(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(diff));
assertEquals(1, diff.getParameter().size());
assertEquals("replace", extractPartValuePrimitive(diff, 0, "operation", "type"));
assertEquals("Patient.meta.versionId", extractPartValuePrimitive(diff, 0, "operation", "path"));
assertEquals("3", extractPartValuePrimitive(diff, 0, "operation", "value"));
validateDiffProducesSameResults(oldValue, newValue, svc, diff);
}
@Test
public void testModifyNarrative() {
Patient oldValue = new Patient();
oldValue.getText().getDiv().setValue("<div>123</div>");
oldValue.addName().setFamily("Family");
Patient newValue = new Patient();
newValue.getText().getDiv().setValue("<div>456</div>");
newValue.addName().setFamily("Family");
FhirPatch svc = new FhirPatch(ourCtx);
Parameters diff = (Parameters) svc.diff(oldValue, newValue);
ourLog.info(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(diff));
assertEquals(1, diff.getParameter().size());
assertEquals("replace", extractPartValuePrimitive(diff, 0, "operation", "type"));
assertEquals("Patient.text.div", extractPartValuePrimitive(diff, 0, "operation", "path"));
assertEquals("<div xmlns=\"http://www.w3.org/1999/xhtml\">456</div>", extractPartValuePrimitive(diff, 0, "operation", "value"));
validateDiffProducesSameResults(oldValue, newValue, svc, diff);
}
@Test
public void testInsertIdentifier() {
Patient oldValue = new Patient();
oldValue.addIdentifier().setSystem("system-0").setValue("value-0");
Patient newValue = new Patient();
newValue.addIdentifier().setSystem("system-0").setValue("value-0");
newValue.addIdentifier().setSystem("system-1").setValue("value-1");
FhirPatch svc = new FhirPatch(ourCtx);
Parameters diff = (Parameters) svc.diff(oldValue, newValue);
ourLog.info(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(diff));
assertEquals(1, diff.getParameter().size());
assertEquals("insert", extractPartValuePrimitive(diff, 0, "operation", "type"));
assertEquals("1", extractPartValuePrimitive(diff, 0, "operation", "index"));
assertEquals("Patient.identifier", extractPartValuePrimitive(diff, 0, "operation", "path"));
assertEquals("system-1", extractPartValue(diff, 0, "operation", "value", Identifier.class).getSystem());
assertEquals("value-1", extractPartValue(diff, 0, "operation", "value", Identifier.class).getValue());
validateDiffProducesSameResults(oldValue, newValue, svc, diff);
}
@Test
public void testIgnoreElementComposite_Resource() {
Patient oldValue = new Patient();
oldValue.setActive(true);
oldValue.getMeta().setSource("123");
Patient newValue = new Patient();
newValue.setActive(false);
newValue.getMeta().setSource("456");
FhirPatch svc = new FhirPatch(ourCtx);
svc.addIgnorePath("Patient.meta");
Parameters diff = (Parameters) svc.diff(oldValue, newValue);
ourLog.info(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(diff));
assertEquals(1, diff.getParameter().size());
assertEquals("replace", extractPartValuePrimitive(diff, 0, "operation", "type"));
assertEquals("Patient.active", extractPartValuePrimitive(diff, 0, "operation", "path"));
assertEquals("false", extractPartValuePrimitive(diff, 0, "operation", "value"));
}
@Test
public void testIgnoreElementComposite_Star() {
Patient oldValue = new Patient();
oldValue.setActive(true);
oldValue.getMeta().setSource("123");
Patient newValue = new Patient();
newValue.setActive(false);
newValue.getMeta().setSource("456");
FhirPatch svc = new FhirPatch(ourCtx);
svc.addIgnorePath("*.meta");
Parameters diff = (Parameters) svc.diff(oldValue, newValue);
ourLog.info(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(diff));
assertEquals(1, diff.getParameter().size());
assertEquals("replace", extractPartValuePrimitive(diff, 0, "operation", "type"));
assertEquals("Patient.active", extractPartValuePrimitive(diff, 0, "operation", "path"));
assertEquals("false", extractPartValuePrimitive(diff, 0, "operation", "value"));
}
@Test
public void testIgnoreElementPrimitive() {
Patient oldValue = new Patient();
oldValue.setActive(true);
oldValue.getMeta().setSource("123");
Patient newValue = new Patient();
newValue.setActive(false);
newValue.getMeta().setSource("456");
FhirPatch svc = new FhirPatch(ourCtx);
svc.addIgnorePath("Patient.meta.source");
Parameters diff = (Parameters) svc.diff(oldValue, newValue);
ourLog.info(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(diff));
assertEquals(1, diff.getParameter().size());
assertEquals("replace", extractPartValuePrimitive(diff, 0, "operation", "type"));
assertEquals("Patient.active", extractPartValuePrimitive(diff, 0, "operation", "path"));
assertEquals("false", extractPartValuePrimitive(diff, 0, "operation", "value"));
}
@Test
public void testIgnoreId() {
Patient oldValue = new Patient();
oldValue.setId("1");
oldValue.setActive(true);
Patient newValue = new Patient();
newValue.setId("2");
newValue.setActive(false);
FhirPatch svc = new FhirPatch(ourCtx);
svc.addIgnorePath("*.id");
Parameters diff = (Parameters) svc.diff(oldValue, newValue);
ourLog.info(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(diff));
assertEquals(1, diff.getParameter().size());
assertEquals("replace", extractPartValuePrimitive(diff, 0, "operation", "type"));
assertEquals("Patient.active", extractPartValuePrimitive(diff, 0, "operation", "path"));
assertEquals("false", extractPartValuePrimitive(diff, 0, "operation", "value"));
}
@Test
public void testDeleteIdentifier() {
Patient oldValue = new Patient();
oldValue.addIdentifier().setSystem("system-0").setValue("value-0");
oldValue.addIdentifier().setSystem("system-1").setValue("value-1");
Patient newValue = new Patient();
newValue.addIdentifier().setSystem("system-0").setValue("value-0");
FhirPatch svc = new FhirPatch(ourCtx);
Parameters diff = (Parameters) svc.diff(oldValue, newValue);
ourLog.info(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(diff));
assertEquals(1, diff.getParameter().size());
assertEquals("delete", extractPartValuePrimitive(diff, 0, "operation", "type"));
assertEquals("Patient.identifier[1]", extractPartValuePrimitive(diff, 0, "operation", "path"));
validateDiffProducesSameResults(oldValue, newValue, svc, diff);
}
public void validateDiffProducesSameResults(Patient theOldValue, Patient theNewValue, FhirPatch theSvc, Parameters theDiff) {
theSvc.apply(theOldValue, theDiff);
String expected = ourCtx.newJsonParser().encodeResourceToString(theNewValue);
String actual = ourCtx.newJsonParser().encodeResourceToString(theOldValue);
assertEquals(expected, actual);
expected = ourCtx.newXmlParser().encodeResourceToString(theNewValue);
actual = ourCtx.newXmlParser().encodeResourceToString(theOldValue);
assertEquals(expected, actual);
}
}

View File

@ -0,0 +1,33 @@
package ca.uhn.fhir.jpa.patch;
import ca.uhn.fhir.context.FhirContext;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.xml.sax.SAXException;
import javax.xml.transform.TransformerException;
import java.io.IOException;
import java.util.Collection;
@RunWith(Parameterized.class)
public class FhirPatchR4CoreTest extends BaseFhirPatchCoreTest {
private static final FhirContext ourCtx = FhirContext.forR4();
public FhirPatchR4CoreTest(String theName, String theMode, IBaseResource theInput, IBaseResource thePatch, IBaseResource theOutput) {
super(theName, theMode, theInput, thePatch, theOutput);
}
@Override
protected FhirContext getContext() {
return ourCtx;
}
@Parameterized.Parameters(name = "{0}")
public static Collection<Object[]> parameters() throws IOException, SAXException, TransformerException {
String testSpec = "/org/hl7/fhir/testcases/r4/patch/fhir-path-tests.xml";
return loadTestSpec(ourCtx, testSpec);
}
}

View File

@ -0,0 +1,33 @@
package ca.uhn.fhir.jpa.patch;
import ca.uhn.fhir.context.FhirContext;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.xml.sax.SAXException;
import javax.xml.transform.TransformerException;
import java.io.IOException;
import java.util.Collection;
@RunWith(Parameterized.class)
public class FhirPatchR5CoreTest extends BaseFhirPatchCoreTest {
private static final FhirContext ourCtx = FhirContext.forR5();
public FhirPatchR5CoreTest(String theName, String theMode, IBaseResource theInput, IBaseResource thePatch, IBaseResource theOutput) {
super(theName, theMode, theInput, thePatch, theOutput);
}
@Override
protected FhirContext getContext() {
return ourCtx;
}
@Parameterized.Parameters(name = "{0}")
public static Collection<Object[]> parameters() throws IOException, SAXException, TransformerException {
String testSpec = "/org/hl7/fhir/testcases/r5/patch/fhir-path-tests.xml";
return loadTestSpec(ourCtx, testSpec);
}
}

View File

@ -4,6 +4,7 @@ import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc;
import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test;
import ca.uhn.fhir.jpa.provider.DiffProvider;
import ca.uhn.fhir.jpa.provider.GraphQLProvider;
import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
@ -105,6 +106,7 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
ourRestServer.registerProviders(mySystemProvider, myTerminologyUploaderProvider);
ourRestServer.registerProvider(myAppCtx.getBean(GraphQLProvider.class));
ourRestServer.registerProvider(myAppCtx.getBean(DiffProvider.class));
ourPagingProvider = myAppCtx.getBean(DatabaseBackedPagingProvider.class);

View File

@ -0,0 +1,252 @@
package ca.uhn.fhir.jpa.provider.r4;
import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
import ca.uhn.fhir.jpa.model.util.ProviderConstants;
import ca.uhn.fhir.model.primitive.BooleanDt;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.BooleanType;
import org.hl7.fhir.r4.model.HumanName;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.StringType;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static ca.uhn.fhir.jpa.patch.FhirPatchApplyR4Test.extractPartValue;
import static ca.uhn.fhir.jpa.patch.FhirPatchApplyR4Test.extractPartValuePrimitive;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
public class DiffProviderR4Test extends BaseResourceProviderR4Test {
private static final Logger ourLog = LoggerFactory.getLogger(DiffProviderR4Test.class);
@Test
public void testMetaIgnoredByDefault() {
// Create and 2 updates
IIdType id = createPatient(withActiveFalse()).toUnqualifiedVersionless();
createPatient(withId(id), withActiveTrue());
createPatient(withId(id), withActiveTrue(), withFamily("SMITH"));
Parameters diff = ourClient
.operation()
.onInstance(id)
.named(ProviderConstants.DIFF_OPERATION_NAME)
.withNoParameters(Parameters.class)
.useHttpGet()
.execute();
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(diff));
assertEquals(2, diff.getParameter().size());
assertEquals("replace", extractPartValuePrimitive(diff, 0, "operation", "type"));
assertEquals("Patient.text.div", extractPartValuePrimitive(diff, 0, "operation", "path"));
assertEquals("<div xmlns=\"http://www.w3.org/1999/xhtml\"><table class=\"hapiPropertyTable\"><tbody/></table></div>", extractPartValuePrimitive(diff, 0, "operation", "previousValue"));
assertEquals("<div xmlns=\"http://www.w3.org/1999/xhtml\"><div class=\"hapiHeaderText\"><b>SMITH </b></div><table class=\"hapiPropertyTable\"><tbody/></table></div>", extractPartValuePrimitive(diff, 0, "operation", "value"));
assertEquals("insert", extractPartValuePrimitive(diff, 1, "operation", "type"));
assertEquals("Patient.name", extractPartValuePrimitive(diff, 1, "operation", "path"));
assertEquals("0", extractPartValuePrimitive(diff, 1, "operation", "index"));
assertEquals("SMITH", extractPartValue(diff, 1, "operation", "value", HumanName.class).getFamily());
}
@Test
public void testLatestVersion_2_to_3() {
// Create and 2 updates
IIdType id = createPatient(withActiveFalse()).toUnqualifiedVersionless();
createPatient(withId(id), withActiveTrue());
createPatient(withId(id), withActiveTrue(), withFamily("SMITH"));
Parameters diff = ourClient
.operation()
.onInstance(id)
.named(ProviderConstants.DIFF_OPERATION_NAME)
.withParameter(Parameters.class, ProviderConstants.DIFF_INCLUDE_META_PARAMETER, new BooleanType(true))
.useHttpGet()
.execute();
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(diff));
assertEquals(4, diff.getParameter().size());
assertEquals("replace", extractPartValuePrimitive(diff, 0, "operation", "type"));
assertEquals("Patient.meta.versionId", extractPartValuePrimitive(diff, 0, "operation", "path"));
assertEquals("2", extractPartValuePrimitive(diff, 0, "operation", "previousValue"));
assertEquals("3", extractPartValuePrimitive(diff, 0, "operation", "value"));
assertEquals("replace", extractPartValuePrimitive(diff, 1, "operation", "type"));
assertEquals("Patient.meta.lastUpdated", extractPartValuePrimitive(diff, 1, "operation", "path"));
assertEquals("replace", extractPartValuePrimitive(diff, 2, "operation", "type"));
assertEquals("Patient.text.div", extractPartValuePrimitive(diff, 2, "operation", "path"));
assertEquals("<div xmlns=\"http://www.w3.org/1999/xhtml\"><table class=\"hapiPropertyTable\"><tbody/></table></div>", extractPartValuePrimitive(diff, 2, "operation", "previousValue"));
assertEquals("<div xmlns=\"http://www.w3.org/1999/xhtml\"><div class=\"hapiHeaderText\"><b>SMITH </b></div><table class=\"hapiPropertyTable\"><tbody/></table></div>", extractPartValuePrimitive(diff, 2, "operation", "value"));
assertEquals("insert", extractPartValuePrimitive(diff, 3, "operation", "type"));
assertEquals("Patient.name", extractPartValuePrimitive(diff, 3, "operation", "path"));
assertEquals("0", extractPartValuePrimitive(diff, 3, "operation", "index"));
assertEquals("SMITH", extractPartValue(diff, 3, "operation", "value", HumanName.class).getFamily());
}
@Test
public void testLatestVersion_PreviousVersionExpunged() {
// Create and 2 updates
IIdType id = createPatient(withActiveFalse()).toUnqualifiedVersionless();
createPatient(withId(id), withActiveTrue());
createPatient(withId(id), withActiveTrue(), withFamily("SMITH"));
runInTransaction(()->{
ResourceHistoryTable version2 = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(id.getIdPartAsLong(), 2);
myResourceHistoryTableDao.deleteByPid(version2.getId());
});
Parameters diff = ourClient
.operation()
.onInstance(id)
.named(ProviderConstants.DIFF_OPERATION_NAME)
.withParameter(Parameters.class, ProviderConstants.DIFF_INCLUDE_META_PARAMETER, new BooleanType(true))
.execute();
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(diff));
assertEquals(5, diff.getParameter().size());
assertEquals("replace", extractPartValuePrimitive(diff, 0, "operation", "type"));
assertEquals("Patient.meta.versionId", extractPartValuePrimitive(diff, 0, "operation", "path"));
assertEquals("1", extractPartValuePrimitive(diff, 0, "operation", "previousValue"));
assertEquals("3", extractPartValuePrimitive(diff, 0, "operation", "value"));
}
@Test
public void testLatestVersion_OnlyOneVersionExists() {
// Create only
IIdType id = createPatient(withActiveTrue()).toUnqualifiedVersionless();
Parameters diff = ourClient
.operation()
.onInstance(id)
.named(ProviderConstants.DIFF_OPERATION_NAME)
.withNoParameters(Parameters.class)
.execute();
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(diff));
assertEquals(1, diff.getParameter().size());
assertEquals("insert", extractPartValuePrimitive(diff, 0, "operation", "type"));
assertEquals("Patient", extractPartValuePrimitive(diff, 0, "operation", "path"));
assertEquals(true, extractPartValue(diff, 0, "operation", "value", Patient.class).getActive());
}
@Test
public void testExplicitFromVersion() {
// Create and 2 updates
IIdType id = createPatient(withActiveFalse()).toUnqualifiedVersionless();
createPatient(withId(id), withActiveTrue());
createPatient(withId(id), withActiveTrue(), withFamily("SMITH"));
Parameters diff = ourClient
.operation()
.onInstance(id)
.named(ProviderConstants.DIFF_OPERATION_NAME)
.withParameter(Parameters.class, ProviderConstants.DIFF_FROM_VERSION_PARAMETER, new StringType("1"))
.andParameter(ProviderConstants.DIFF_INCLUDE_META_PARAMETER, new BooleanType(true))
.execute();
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(diff));
assertEquals(5, diff.getParameter().size());
assertEquals("replace", extractPartValuePrimitive(diff, 0, "operation", "type"));
assertEquals("Patient.meta.versionId", extractPartValuePrimitive(diff, 0, "operation", "path"));
assertEquals("1", extractPartValuePrimitive(diff, 0, "operation", "previousValue"));
assertEquals("3", extractPartValuePrimitive(diff, 0, "operation", "value"));
}
@Test
public void testDifferentResources_Versionless() {
// Create and 2 updates
IIdType id1 = createPatient(withId("A"), withActiveFalse()).toUnqualifiedVersionless();
IIdType id2 = createPatient(withId("B"), withActiveTrue()).toUnqualifiedVersionless();
Parameters diff = ourClient
.operation()
.onServer()
.named(ProviderConstants.DIFF_OPERATION_NAME)
.withParameter(Parameters.class, ProviderConstants.DIFF_FROM_PARAMETER, id1)
.andParameter(ProviderConstants.DIFF_TO_PARAMETER, id2)
.andParameter(ProviderConstants.DIFF_INCLUDE_META_PARAMETER, new BooleanType(true))
.execute();
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(diff));
assertEquals(3, diff.getParameter().size());
assertEquals("replace", extractPartValuePrimitive(diff, 0, "operation", "type"));
assertEquals("Patient.id", extractPartValuePrimitive(diff, 0, "operation", "path"));
assertEquals("A", extractPartValuePrimitive(diff, 0, "operation", "previousValue"));
assertEquals("B", extractPartValuePrimitive(diff, 0, "operation", "value"));
}
@Test
public void testDifferentResources_Versioned() {
// Create and 2 updates
IIdType id1 = createPatient(withId("A"), withActiveTrue()).toUnqualifiedVersionless();
id1 = createPatient(withId(id1), withActiveTrue(), withFamily("SMITH")).toUnqualified();
IIdType id2 = createPatient(withId("B"), withActiveFalse()).toUnqualifiedVersionless();
id2 = createPatient(withId(id2), withActiveTrue(), withFamily("JONES")).toUnqualified();
Parameters diff = ourClient
.operation()
.onServer()
.named(ProviderConstants.DIFF_OPERATION_NAME)
.withParameter(Parameters.class, ProviderConstants.DIFF_FROM_PARAMETER, id1.withVersion("1"))
.andParameter(ProviderConstants.DIFF_TO_PARAMETER, id2.withVersion("1"))
.andParameter(ProviderConstants.DIFF_INCLUDE_META_PARAMETER, new BooleanType(true))
.execute();
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(diff));
assertEquals(3, diff.getParameter().size());
assertEquals("replace", extractPartValuePrimitive(diff, 0, "operation", "type"));
assertEquals("Patient.id", extractPartValuePrimitive(diff, 0, "operation", "path"));
assertEquals("A", extractPartValuePrimitive(diff, 0, "operation", "previousValue"));
assertEquals("B", extractPartValuePrimitive(diff, 0, "operation", "value"));
}
@Test
public void testDifferentResources_DifferentTypes() {
try {
ourClient
.operation()
.onServer()
.named(ProviderConstants.DIFF_OPERATION_NAME)
.withParameter(Parameters.class, ProviderConstants.DIFF_FROM_PARAMETER, new IdType("Patient/123"))
.andParameter(ProviderConstants.DIFF_TO_PARAMETER, new IdType("Observation/456"))
.execute();
fail();
} catch (InvalidRequestException e) {
assertEquals("HTTP 400 Bad Request: Unable to diff two resources of different types", e.getMessage());
}
}
}

View File

@ -1,6 +1,7 @@
package ca.uhn.fhir.jpa.provider.r4;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.MethodOutcome;
import com.google.common.base.Charsets;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
@ -8,6 +9,7 @@ import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.*;
import org.junit.Test;
@ -26,6 +28,81 @@ public class PatchProviderR4Test extends BaseResourceProviderR4Test {
private static final Logger ourLog = LoggerFactory.getLogger(PatchProviderR4Test.class);
@Test
public void testFhirPatch() {
Patient patient = new Patient();
patient.setActive(true);
patient.addIdentifier().addExtension("http://foo", new StringType("abc"));
patient.addIdentifier().setSystem("sys").setValue("val");
IIdType id = ourClient.create().resource(patient).execute().getId().toUnqualifiedVersionless();
Parameters patch = new Parameters();
Parameters.ParametersParameterComponent operation = patch.addParameter();
operation.setName("operation");
operation
.addPart()
.setName("type")
.setValue(new CodeType("delete"));
operation
.addPart()
.setName("path")
.setValue(new StringType("Patient.identifier[0]"));
MethodOutcome outcome = ourClient
.patch()
.withFhirPatch(patch)
.withId(id)
.execute();
Patient resultingResource = (Patient) outcome.getResource();
assertEquals(1, resultingResource.getIdentifier().size());
resultingResource = ourClient.read().resource(Patient.class).withId(id).execute();
assertEquals(1, resultingResource.getIdentifier().size());
}
@Test
public void testFhirPatch_Transaction() throws Exception {
String methodName = "testFhirPatch_Transaction";
IIdType pid1;
{
Patient patient = new Patient();
patient.setActive(true);
patient.addIdentifier().setSystem("urn:system").setValue("0");
patient.addName().setFamily(methodName).addGiven("Joe");
pid1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless();
}
Parameters patch = new Parameters();
Parameters.ParametersParameterComponent op = patch.addParameter().setName("operation");
op.addPart().setName("type").setValue(new CodeType("replace"));
op.addPart().setName("path").setValue(new CodeType("Patient.active"));
op.addPart().setName("value").setValue(new BooleanType(false));
Bundle input = new Bundle();
input.setType(Bundle.BundleType.TRANSACTION);
input.addEntry()
.setFullUrl(pid1.getValue())
.setResource(patch)
.getRequest().setUrl(pid1.getValue())
.setMethod(Bundle.HTTPVerb.PATCH);
HttpPost post = new HttpPost(ourServerBase);
String encodedRequest = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(input);
ourLog.info("Request:\n{}", encodedRequest);
post.setEntity(new StringEntity(encodedRequest, ContentType.parse(Constants.CT_FHIR_JSON_NEW+ Constants.CHARSET_UTF8_CTSUFFIX)));
try (CloseableHttpResponse response = ourHttpClient.execute(post)) {
assertEquals(200, response.getStatusLine().getStatusCode());
String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
assertThat(responseString, containsString("\"resourceType\":\"Bundle\""));
}
Patient newPt = ourClient.read().resource(Patient.class).withId(pid1.getIdPart()).execute();
assertEquals("2", newPt.getIdElement().getVersionIdPart());
assertEquals(false, newPt.getActive());
}
@Test
public void testPatchAddArray() throws IOException {
IIdType id;
@ -121,7 +198,7 @@ public class PatchProviderR4Test extends BaseResourceProviderR4Test {
HttpPost post = new HttpPost(ourServerBase);
String encodedRequest = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(input);
ourLog.info("Requet:\n{}", encodedRequest);
ourLog.info("Request:\n{}", encodedRequest);
post.setEntity(new StringEntity(encodedRequest, ContentType.parse(Constants.CT_FHIR_JSON_NEW+ Constants.CHARSET_UTF8_CTSUFFIX)));
try (CloseableHttpResponse response = ourHttpClient.execute(post)) {
assertEquals(200, response.getStatusLine().getStatusCode());
@ -465,7 +542,45 @@ public class PatchProviderR4Test extends BaseResourceProviderR4Test {
@Test
public void testPatchInTransaction_InvalidContentType() throws Exception {
public void testPatchInTransaction_InvalidContentType_NonFhir() throws Exception {
String methodName = "testPatchUsingJsonPatch_Transaction";
IIdType pid1;
{
Patient patient = new Patient();
patient.setActive(true);
patient.addIdentifier().setSystem("urn:system").setValue("0");
patient.addName().setFamily(methodName).addGiven("Joe");
pid1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless();
}
String patchString = "[ { \"op\":\"replace\", \"path\":\"/active\", \"value\":false } ]";
Binary patch = new Binary();
patch.setContentType("application/octet-stream");
patch.setContent(patchString.getBytes(Charsets.UTF_8));
Bundle input = new Bundle();
input.setType(Bundle.BundleType.TRANSACTION);
input.addEntry()
.setFullUrl(pid1.getValue())
.setResource(patch)
.getRequest().setUrl(pid1.getValue())
.setMethod(Bundle.HTTPVerb.PATCH);
HttpPost post = new HttpPost(ourServerBase);
post.setEntity(new StringEntity(myFhirCtx.newJsonParser().encodeResourceToString(input), ContentType.parse(Constants.CT_FHIR_JSON_NEW+ Constants.CHARSET_UTF8_CTSUFFIX)));
try (CloseableHttpResponse response = ourHttpClient.execute(post)) {
assertEquals(400, response.getStatusLine().getStatusCode());
String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
assertThat(responseString, containsString("Invalid Content-Type for PATCH operation: application/octet-stream"));
}
Patient newPt = ourClient.read().resource(Patient.class).withId(pid1.getIdPart()).execute();
assertEquals("1", newPt.getIdElement().getVersionIdPart());
assertEquals(true, newPt.getActive());
}
@Test
public void testPatchInTransaction_InvalidContentType_Fhir() throws Exception {
String methodName = "testPatchUsingJsonPatch_Transaction";
IIdType pid1;
{
@ -494,12 +609,11 @@ public class PatchProviderR4Test extends BaseResourceProviderR4Test {
try (CloseableHttpResponse response = ourHttpClient.execute(post)) {
assertEquals(400, response.getStatusLine().getStatusCode());
String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
assertThat(responseString, containsString("Invalid Content-Type for PATCH operation: application/fhir+json"));
assertThat(responseString, containsString("Binary PATCH detected with FHIR content type. FHIR Patch should use Parameters resource."));
}
Patient newPt = ourClient.read().resource(Patient.class).withId(pid1.getIdPart()).execute();
assertEquals("1", newPt.getIdElement().getVersionIdPart());
assertEquals(true, newPt.getActive());
}
}

View File

@ -50,6 +50,7 @@ import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import static org.apache.commons.lang3.time.DateUtils.MILLIS_PER_SECOND;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.*;
@ -152,7 +153,7 @@ public class ResourceProviderInterceptorR4Test extends BaseResourceProviderR4Tes
* Server Interceptor
*/
verify(interceptor, timeout(Duration.ofSeconds(10)).times(1)).invoke(eq(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED), myParamsCaptor.capture());
verify(interceptor, timeout(10 * MILLIS_PER_SECOND).times(1)).invoke(eq(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED), myParamsCaptor.capture());
assertEquals(RestOperationTypeEnum.TRANSACTION, myParamsCaptor.getAllValues().get(0).get(RestOperationTypeEnum.class));
verify(interceptor, times(1)).invoke(eq(Pointcut.SERVER_INCOMING_REQUEST_POST_PROCESSED), myParamsCaptor.capture());
@ -182,7 +183,7 @@ public class ResourceProviderInterceptorR4Test extends BaseResourceProviderR4Tes
assertThat(newIdString, startsWith(ourServerBase + "/Patient/"));
}
verify(interceptor, timeout(Duration.ofSeconds(10)).times(1)).invoke(eq(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED), myParamsCaptor.capture());
verify(interceptor, timeout(10 * MILLIS_PER_SECOND).times(1)).invoke(eq(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED), myParamsCaptor.capture());
assertEquals(RestOperationTypeEnum.CREATE, myParamsCaptor.getValue().get(RestOperationTypeEnum.class));
assertEquals("Patient", myParamsCaptor.getValue().get(RequestDetails.class).getResource().getIdElement().getResourceType());
@ -210,11 +211,11 @@ public class ResourceProviderInterceptorR4Test extends BaseResourceProviderR4Tes
transaction(bundle);
verify(interceptor, timeout(Duration.ofSeconds(10)).times(2)).invoke(eq(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED), myParamsCaptor.capture());
verify(interceptor, timeout(10 * MILLIS_PER_SECOND).times(2)).invoke(eq(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED), myParamsCaptor.capture());
assertEquals(RestOperationTypeEnum.CREATE, myParamsCaptor.getValue().get(RestOperationTypeEnum.class));
verify(interceptor, timeout(Duration.ofSeconds(10)).times(1)).invoke(eq(Pointcut.SERVER_INCOMING_REQUEST_POST_PROCESSED), myParamsCaptor.capture());
verify(interceptor, timeout(10 * MILLIS_PER_SECOND).times(1)).invoke(eq(Pointcut.SERVER_INCOMING_REQUEST_POST_PROCESSED), myParamsCaptor.capture());
verify(interceptor, timeout(Duration.ofSeconds(10)).times(1)).invoke(eq(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED), myParamsCaptor.capture());
verify(interceptor, timeout(10 * MILLIS_PER_SECOND).times(1)).invoke(eq(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED), myParamsCaptor.capture());
}
@Test
@ -276,7 +277,7 @@ public class ResourceProviderInterceptorR4Test extends BaseResourceProviderR4Tes
assertThat(newIdString, startsWith(ourServerBase + "/Patient/"));
}
verify(interceptor, timeout(Duration.ofSeconds(10)).times(1)).invoke(eq(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED), myParamsCaptor.capture());
verify(interceptor, timeout(10 * MILLIS_PER_SECOND).times(1)).invoke(eq(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED), myParamsCaptor.capture());
assertEquals(RestOperationTypeEnum.CREATE, myParamsCaptor.getValue().get(RestOperationTypeEnum.class));
Patient patient = (Patient) myParamsCaptor.getValue().get(RequestDetails.class).getResource();
@ -312,7 +313,7 @@ public class ResourceProviderInterceptorR4Test extends BaseResourceProviderR4Tes
entry.getRequest().setIfNoneExist("Patient?name=" + methodName);
transaction(bundle);
verify(interceptor, timeout(Duration.ofSeconds(10)).times(2)).invoke(eq(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED), myParamsCaptor.capture());
verify(interceptor, timeout(10 * MILLIS_PER_SECOND).times(2)).invoke(eq(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED), myParamsCaptor.capture());
assertEquals(RestOperationTypeEnum.TRANSACTION, myParamsCaptor.getAllValues().get(0).get(RestOperationTypeEnum.class));
assertEquals(RestOperationTypeEnum.UPDATE, myParamsCaptor.getAllValues().get(1).get(RestOperationTypeEnum.class));
verify(interceptor, times(0)).invoke(eq(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED), any());

View File

@ -84,7 +84,7 @@ public class SchedulerServiceImplTest {
ourLog.info("Fired {} times", CountingJob.ourCount);
await().until(() -> CountingJob.ourCount, greaterThan(3));
assertThat(CountingJob.ourCount, lessThan(20));
assertThat(CountingJob.ourCount, lessThan(50));
}
@Test

View File

@ -2,6 +2,7 @@ package ca.uhn.fhir.jpa.util.jsonpatch;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.BaseJpaTest;
import ca.uhn.fhir.jpa.patch.JsonPatchUtils;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import org.hl7.fhir.r4.model.Observation;
import org.junit.Test;

View File

@ -44,6 +44,9 @@
<appender-ref ref="STDOUT" />
</logger>
<logger name="org.springframework.test.context.cache" additivity="false" level="debug">
<appender-ref ref="STDOUT" />
</logger>
<root level="info">
<appender-ref ref="STDOUT" />

View File

@ -0,0 +1 @@
spring.test.context.cache.maxSize=2

View File

@ -48,4 +48,13 @@ public class ProviderConstants {
public static final String PARTITION_MANAGEMENT_PARTITION_NAME = "name";
public static final String PARTITION_MANAGEMENT_PARTITION_DESC = "description";
/**
* Operation name: diff
*/
public static final String DIFF_OPERATION_NAME = "$diff";
public static final String DIFF_FROM_VERSION_PARAMETER = "fromVersion";
public static final String DIFF_FROM_PARAMETER = "from";
public static final String DIFF_TO_PARAMETER = "to";
public static final String DIFF_INCLUDE_META_PARAMETER = "includeMeta";
}

View File

@ -248,11 +248,14 @@ public class SearchParamRegistryImpl implements ISearchParamRegistry {
StopWatch sw = new StopWatch();
Map<String, Map<String, RuntimeSearchParam>> searchParams = new HashMap<>();
for (Map.Entry<String, Map<String, RuntimeSearchParam>> nextBuiltInEntry : getBuiltInSearchParams().entrySet()) {
Set<Map.Entry<String, Map<String, RuntimeSearchParam>>> builtInSps = getBuiltInSearchParams().entrySet();
for (Map.Entry<String, Map<String, RuntimeSearchParam>> nextBuiltInEntry : builtInSps) {
for (RuntimeSearchParam nextParam : nextBuiltInEntry.getValue().values()) {
String nextResourceName = nextBuiltInEntry.getKey();
getSearchParamMap(searchParams, nextResourceName).put(nextParam.getName(), nextParam);
}
ourLog.trace("Have {} built-in SPs for: {}", nextBuiltInEntry.getValue().size(), nextBuiltInEntry.getKey());
}
SearchParameterMap params = new SearchParameterMap();

View File

@ -83,7 +83,7 @@ public class SubscriptionActivatingSubscriber extends BaseSubscriberForSubscript
switch (payload.getOperationType()) {
case CREATE:
case UPDATE:
activateOrRegisterSubscriptionIfRequired(payload.getNewPayload(myFhirContext));
activateSubscriptionIfRequired(payload.getNewPayload(myFhirContext));
break;
case DELETE:
case MANUALLY_TRIGGERED:
@ -93,7 +93,7 @@ public class SubscriptionActivatingSubscriber extends BaseSubscriberForSubscript
}
public boolean activateOrRegisterSubscriptionIfRequired(final IBaseResource theSubscription) {
public boolean activateSubscriptionIfRequired(final IBaseResource theSubscription) {
// Grab the value for "Subscription.channel.type" so we can see if this
// subscriber applies..
CanonicalSubscriptionChannelType subscriptionChannelType = mySubscriptionCanonicalizer.getChannelType(theSubscription);

View File

@ -20,13 +20,13 @@ package ca.uhn.fhir.jpa.subscription.match.registry;
* #L%
*/
import ca.uhn.fhir.jpa.api.IDaoRegistry;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.model.sched.HapiJob;
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
import ca.uhn.fhir.jpa.model.sched.ScheduledJobDefinition;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
import ca.uhn.fhir.jpa.searchparam.retry.Retrier;
import ca.uhn.fhir.jpa.subscription.match.matcher.subscriber.SubscriptionActivatingSubscriber;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
@ -61,6 +61,8 @@ public class SubscriptionLoader {
private ISchedulerService mySchedulerService;
@Autowired
private SubscriptionActivatingSubscriber mySubscriptionActivatingInterceptor;
@Autowired
private ISearchParamRegistry mySearchParamRegistry;
/**
* Constructor
@ -122,9 +124,12 @@ public class SubscriptionLoader {
synchronized (mySyncSubscriptionsLock) {
ourLog.debug("Starting sync subscriptions");
SearchParameterMap map = new SearchParameterMap();
map.add(Subscription.SP_STATUS, new TokenOrListParam()
.addOr(new TokenParam(null, Subscription.SubscriptionStatus.REQUESTED.toCode()))
.addOr(new TokenParam(null, Subscription.SubscriptionStatus.ACTIVE.toCode())));
if (mySearchParamRegistry.getActiveSearchParam("Subscription", "status") != null) {
map.add(Subscription.SP_STATUS, new TokenOrListParam()
.addOr(new TokenParam(null, Subscription.SubscriptionStatus.REQUESTED.toCode()))
.addOr(new TokenParam(null, Subscription.SubscriptionStatus.ACTIVE.toCode())));
}
map.setLoadSynchronousUpTo(SubscriptionConstants.MAX_SUBSCRIPTION_RESULTS);
IFhirResourceDao subscriptionDao = myDaoRegistry.getSubscriptionDao();
@ -146,7 +151,7 @@ public class SubscriptionLoader {
String nextId = resource.getIdElement().getIdPart();
allIds.add(nextId);
boolean activated = mySubscriptionActivatingInterceptor.activateOrRegisterSubscriptionIfRequired(resource);
boolean activated = mySubscriptionActivatingInterceptor.activateSubscriptionIfRequired(resource);
if (activated) {
activatedCount++;
}

View File

@ -10,6 +10,7 @@ import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryResourceMatcher;
import ca.uhn.fhir.jpa.searchparam.matcher.SearchParamMatcher;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
import ca.uhn.fhir.jpa.subscription.channel.config.SubscriptionChannelConfig;
import ca.uhn.fhir.jpa.subscription.channel.subscription.SubscriptionChannelFactory;
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription;
@ -63,6 +64,8 @@ public class WebsocketConnectionValidatorTest {
ISchedulerService mySchedulerService;
@MockBean
SubscriptionRegistry mySubscriptionRegistry;
@MockBean
ISearchParamRegistry mySearchParamRegistry;
@Autowired
WebsocketConnectionValidator myWebsocketConnectionValidator;

View File

@ -6,6 +6,7 @@ import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.bulk.BulkDataExportProvider;
import ca.uhn.fhir.jpa.provider.DiffProvider;
import ca.uhn.fhir.jpa.subscription.match.config.WebsocketDispatcherConfig;
import ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor;
import ca.uhn.fhir.jpa.provider.GraphQLProvider;
@ -264,6 +265,11 @@ public class TestRestfulServer extends RestfulServer {
*/
registerProvider(myAppCtx.getBean(BulkDataExportProvider.class));
/*
* $diff operation
*/
registerProvider(myAppCtx.getBean(DiffProvider.class));
}
/**

View File

@ -1,5 +1,6 @@
package ca.uhn.fhirtest.config;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.subscription.match.config.WebsocketDispatcherConfig;
import ca.uhn.fhir.jpa.subscription.channel.config.SubscriptionChannelConfig;
import ca.uhn.fhir.jpa.subscription.match.config.SubscriptionProcessorConfig;
@ -69,6 +70,11 @@ public class CommonConfig {
return retVal;
}
@Bean
public PartitionSettings partitionSettings() {
return new PartitionSettings();
}
public static boolean isLocalTestMode() {
return "true".equalsIgnoreCase(System.getProperty("testmode.local"));
}

View File

@ -67,6 +67,7 @@ public class TestDstu2Config extends BaseJavaConfigDstu2 {
retVal.setFetchSizeDefaultMaximum(10000);
retVal.setWebsocketContextPath("/");
retVal.setFilterParameterEnabled(true);
retVal.setDefaultSearchParamsCanBeOverridden(false);
return retVal;
}

View File

@ -66,6 +66,7 @@ public class TestDstu3Config extends BaseJavaConfigDstu3 {
retVal.setReindexThreadCount(1);
retVal.setExpungeEnabled(true);
retVal.setFilterParameterEnabled(true);
retVal.setDefaultSearchParamsCanBeOverridden(false);
return retVal;
}

View File

@ -66,6 +66,7 @@ public class TestR4Config extends BaseJavaConfigR4 {
retVal.setFetchSizeDefaultMaximum(10000);
retVal.setExpungeEnabled(true);
retVal.setFilterParameterEnabled(true);
retVal.setDefaultSearchParamsCanBeOverridden(false);
return retVal;
}

View File

@ -66,6 +66,7 @@ public class TestR5Config extends BaseJavaConfigR5 {
retVal.setFetchSizeDefaultMaximum(10000);
retVal.setExpungeEnabled(true);
retVal.setFilterParameterEnabled(true);
retVal.setDefaultSearchParamsCanBeOverridden(false);
return retVal;
}

View File

@ -28,6 +28,8 @@ public class UhnFhirTestApp {
System.setProperty("fhir.lucene.location.dstu3", "./target/testlucene_dstu3");
System.setProperty("fhir.db.location.r4", "./target/fhirtest_r4");
System.setProperty("fhir.lucene.location.r4", "./target/testlucene_r4");
System.setProperty("fhir.db.location.r5", "./target/fhirtest_r5");
System.setProperty("fhir.lucene.location.r5", "./target/testlucene_r5");
System.setProperty("fhir.db.location.tdl2", "./target/testdb_tdl2");
System.setProperty("fhir.lucene.location.tdl2", "./target/testlucene_tdl2");
System.setProperty("fhir.db.location.tdl3", "./target/testdb_tdl3");
@ -36,6 +38,7 @@ public class UhnFhirTestApp {
System.setProperty("fhir.baseurl.dstu1", base.replace("Dstu2", "Dstu1"));
System.setProperty("fhir.baseurl.dstu3", base.replace("Dstu2", "Dstu3"));
System.setProperty("fhir.baseurl.r4", base.replace("Dstu2", "R4"));
System.setProperty("fhir.baseurl.r5", base.replace("Dstu2", "R5"));
System.setProperty("fhir.baseurl.tdl2", base.replace("baseDstu2", "testDataLibraryDstu2"));
System.setProperty("fhir.baseurl.tdl3", base.replace("baseDstu2", "testDataLibraryStu3"));
System.setProperty("fhir.tdlpass", "aa,bb");
@ -48,8 +51,8 @@ public class UhnFhirTestApp {
WebAppContext root = new WebAppContext();
root.setContextPath("/");
root.setDescriptor("src/main/webapp/WEB-INF/web.xml");
root.setResourceBase("target/hapi-fhir-jpaserver");
root.setDescriptor("hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/web.xml");
root.setResourceBase("hapi-fhir-jpaserver-uhnfhirtest/target/hapi-fhir-jpaserver");
root.setParentLoaderPriority(true);

View File

@ -59,6 +59,7 @@ import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.Writer;
@ -341,26 +342,21 @@ public class RestfulServerUtils {
return b.toString();
}
/**
* @TODO: this method is only called from one place and should be removed anyway
*/
public static EncodingEnum determineRequestEncoding(RequestDetails theReq) {
EncodingEnum retVal = determineRequestEncodingNoDefault(theReq);
if (retVal != null) {
return retVal;
}
return EncodingEnum.XML;
@Nullable
public static EncodingEnum determineRequestEncodingNoDefault(RequestDetails theReq) {
return determineRequestEncodingNoDefault(theReq, false);
}
public static EncodingEnum determineRequestEncodingNoDefault(RequestDetails theReq) {
ResponseEncoding retVal = determineRequestEncodingNoDefaultReturnRE(theReq);
@Nullable
public static EncodingEnum determineRequestEncodingNoDefault(RequestDetails theReq, boolean theStrict) {
ResponseEncoding retVal = determineRequestEncodingNoDefaultReturnRE(theReq, theStrict);
if (retVal == null) {
return null;
}
return retVal.getEncoding();
}
private static ResponseEncoding determineRequestEncodingNoDefaultReturnRE(RequestDetails theReq) {
private static ResponseEncoding determineRequestEncodingNoDefaultReturnRE(RequestDetails theReq, boolean theStrict) {
ResponseEncoding retVal = null;
List<String> headers = theReq.getHeaders(Constants.HEADER_CONTENT_TYPE);
if (headers != null) {
@ -378,7 +374,12 @@ public class RestfulServerUtils {
nextPart = nextPart.substring(0, scIdx);
}
nextPart = nextPart.trim();
EncodingEnum encoding = EncodingEnum.forContentType(nextPart);
EncodingEnum encoding;
if (theStrict) {
encoding = EncodingEnum.forContentTypeStrict(nextPart);
} else {
encoding = EncodingEnum.forContentType(nextPart);
}
if (encoding != null) {
retVal = new ResponseEncoding(theReq.getServer().getFhirContext(), encoding, nextPart);
break;
@ -512,7 +513,7 @@ public class RestfulServerUtils {
* has a Content-Type header but not an Accept header)
*/
if (retVal == null) {
retVal = determineRequestEncodingNoDefaultReturnRE(theReq);
retVal = determineRequestEncodingNoDefaultReturnRE(theReq, strict);
}
return retVal;

View File

@ -57,7 +57,7 @@ public class ResponseSizeCapturingInterceptor {
public static final String RESPONSE_RESULT_KEY = ResponseSizeCapturingInterceptor.class.getName() + "_RESPONSE_RESULT_KEY";
private static final String COUNTING_WRITER_KEY = ResponseSizeCapturingInterceptor.class.getName() + "_COUNTING_WRITER_KEY";
private List<Consumer<Result>> myConsumers = new ArrayList<>();
private final List<Consumer<Result>> myConsumers = new ArrayList<>();
@Hook(Pointcut.SERVER_OUTGOING_WRITER_CREATED)
public Writer capture(RequestDetails theRequestDetails, Writer theWriter) {

View File

@ -294,7 +294,7 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
operation = RestOperationTypeEnum.DELETE;
} else if (nextPart.getRequestType() == RequestTypeEnum.PATCH) {
operation = RestOperationTypeEnum.PATCH;
} else if (nextPart.getRequestType() == null && theRequestDetails.getServer().getFhirContext().getVersion().getVersion() == FhirVersionEnum.DSTU3 && BundleUtil.isDstu3TransactionPatch(nextPart.getResource())) {
} else if (nextPart.getRequestType() == null && theRequestDetails.getServer().getFhirContext().getVersion().getVersion() == FhirVersionEnum.DSTU3 && BundleUtil.isDstu3TransactionPatch(theRequestDetails.getFhirContext(), nextPart.getResource())) {
// This is a workaround for the fact that there is no PATCH verb in DSTU3's bundle entry verb type ValueSet.
// See BundleUtil#isDstu3TransactionPatch
operation = RestOperationTypeEnum.PATCH;

View File

@ -197,7 +197,8 @@ public class MethodUtil {
throw new ConfigurationException(b.toString());
}
boolean methodIsOperation = theMethod.getAnnotation(Operation.class) != null;
param = new ResourceParameter((Class<? extends IBaseResource>) parameterType, theProvider, mode, methodIsOperation);
boolean methodIsPatch = theMethod.getAnnotation(Patch.class) != null;
param = new ResourceParameter((Class<? extends IBaseResource>) parameterType, theProvider, mode, methodIsOperation, methodIsPatch);
} else if (nextAnnotation instanceof IdParam) {
param = new NullParameter();
} else if (nextAnnotation instanceof ServerBase) {

View File

@ -1,6 +1,7 @@
package ca.uhn.fhir.rest.server.method;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.trim;
/*
* #%L
@ -45,7 +46,15 @@ class PatchTypeParameter implements IParameter {
public static PatchTypeEnum getTypeForRequestOrThrowInvalidRequestException(RequestDetails theRequest) {
String contentTypeAll = defaultString(theRequest.getHeader(Constants.HEADER_CONTENT_TYPE));
return PatchTypeEnum.forContentTypeOrThrowInvalidRequestException(contentTypeAll);
int semicolonIndex = contentTypeAll.indexOf(';');
if (semicolonIndex > 0) {
contentTypeAll = contentTypeAll.substring(0, semicolonIndex);
}
contentTypeAll = trim(contentTypeAll);
return PatchTypeEnum.forContentTypeOrThrowInvalidRequestException(theRequest.getFhirContext(), contentTypeAll);
}
}

View File

@ -26,6 +26,7 @@ import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.IResourceProvider;
@ -53,17 +54,17 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class ResourceParameter implements IParameter {
private final boolean myMethodIsOperation;
private final boolean myMethodIsOperationOrPatch;
private Mode myMode;
private Class<? extends IBaseResource> myResourceType;
public ResourceParameter(Class<? extends IBaseResource> theParameterType, Object theProvider, Mode theMode, boolean theMethodIsOperation) {
public ResourceParameter(Class<? extends IBaseResource> theParameterType, Object theProvider, Mode theMode, boolean theMethodIsOperation, boolean theMethodIsPatch) {
Validate.notNull(theParameterType, "theParameterType can not be null");
Validate.notNull(theMode, "theMode can not be null");
myResourceType = theParameterType;
myMode = theMode;
myMethodIsOperation = theMethodIsOperation;
myMethodIsOperationOrPatch = theMethodIsOperation || theMethodIsPatch;
Class<? extends IBaseResource> providerResourceType = null;
if (theProvider instanceof IResourceProvider) {
@ -107,7 +108,7 @@ public class ResourceParameter implements IParameter {
case RESOURCE:
default:
Class<? extends IBaseResource> resourceTypeToParse = myResourceType;
if (myMethodIsOperation) {
if (myMethodIsOperationOrPatch) {
// Operations typically have a Parameters resource as the body
resourceTypeToParse = null;
}
@ -223,7 +224,15 @@ public class ResourceParameter implements IParameter {
}
}
if (retVal == null) {
boolean isNonFhirPatch = false;
if (theRequest.getRequestType() == RequestTypeEnum.PATCH) {
EncodingEnum requestEncoding = RestfulServerUtils.determineRequestEncodingNoDefault(theRequest, true);
if (requestEncoding == null) {
isNonFhirPatch = true;
}
}
if (retVal == null && !isNonFhirPatch) {
retVal = loadResourceFromRequest(theRequest, theMethodBinding, theResourceType);
}

View File

@ -322,7 +322,7 @@ public class SearchParameter extends BaseQueryParameter {
} else if (HasParam.class.isAssignableFrom(theType)) {
myParamType = RestSearchParameterTypeEnum.STRING;
} else {
throw new ConfigurationException("Unknown search parameter theType: " + theType);
throw new ConfigurationException("Unknown search parameter type: " + theType);
}
// NB: Once this is enabled, we should return true from handlesMissing if

View File

@ -123,7 +123,7 @@ public class PatchDstu2_1Test {
String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(responseContent);
assertEquals(400, status.getStatusLine().getStatusCode());
assertEquals("<OperationOutcome xmlns=\"http://hl7.org/fhir\"><issue><severity value=\"error\"/><code value=\"processing\"/><diagnostics value=\"Invalid Content-Type for PATCH operation: text/plain; charset=UTF-8\"/></issue></OperationOutcome>", responseContent);
assertEquals("<OperationOutcome xmlns=\"http://hl7.org/fhir\"><issue><severity value=\"error\"/><code value=\"processing\"/><diagnostics value=\"Invalid Content-Type for PATCH operation: text/plain\"/></issue></OperationOutcome>", responseContent);
} finally {
IOUtils.closeQuietly(status.getEntity().getContent());
}

View File

@ -201,6 +201,37 @@ public class GenericClientDstu3Test {
assertThat(oo.getText().getDivAsString(), containsString("OK!"));
}
@Test
public void testPatchByIdNoType() {
String patch = "[ { \"op\":\"replace\", \"path\":\"/active\", \"value\":false } ]";
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
try {
client
.patch()
.withBody(patch)
.withId(new IdType("234"))
.execute();
fail();
} catch (NullPointerException e) {
assertEquals("theId must not be blank and must contain a resource type and ID (e.g. \"Patient/123\"), found: 234", e.getMessage());
}
try {
client
.patch()
.withBody(patch)
.withId("234")
.execute();
fail();
} catch (NullPointerException e) {
assertEquals("theId must not be blank and must contain a resource type and ID (e.g. \"Patient/123\"), found: 234", e.getMessage());
}
}
@Test
public void testPatchJsonByConditionalString() throws Exception {
OperationOutcome conf = new OperationOutcome();

View File

@ -157,7 +157,7 @@ public class PatchServerDstu3Test {
String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(responseContent);
assertEquals(400, status.getStatusLine().getStatusCode());
assertEquals("<OperationOutcome xmlns=\"http://hl7.org/fhir\"><issue><severity value=\"error\"/><code value=\"processing\"/><diagnostics value=\"Invalid Content-Type for PATCH operation: text/plain; charset=UTF-8\"/></issue></OperationOutcome>", responseContent);
assertEquals("<OperationOutcome xmlns=\"http://hl7.org/fhir\"><issue><severity value=\"error\"/><code value=\"processing\"/><diagnostics value=\"Invalid Content-Type for PATCH operation: text/plain\"/></issue></OperationOutcome>", responseContent);
} finally {
IOUtils.closeQuietly(status.getEntity().getContent());
}

View File

@ -17,30 +17,18 @@ import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
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.apache.commons.lang3.time.DateUtils.MILLIS_PER_SECOND;
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)
@ -97,7 +85,7 @@ public class ResponseSizeCapturingInterceptorTest {
resource = ourServerRule.getFhirClient().read().resource(Patient.class).withId(id).execute();
assertEquals(true, resource.getActive());
verify(myConsumer, timeout(Duration.ofSeconds(10)).times(1)).accept(myResultCaptor.capture());
verify(myConsumer, timeout(10 * MILLIS_PER_SECOND).times(1)).accept(myResultCaptor.capture());
assertEquals(100, myResultCaptor.getValue().getWrittenChars());
}

View File

@ -106,13 +106,15 @@ public interface ITestDataBuilder {
};
}
default <T extends IBaseResource> Consumer<T> withId(String theId) {
assertThat(theId, matchesPattern("[a-zA-Z0-9-]+"));
return t -> t.setId(theId);
default Consumer<IBaseResource> withId(String theId) {
return t -> {
assertThat(theId, matchesPattern("[a-zA-Z0-9]+"));
t.setId(theId);
};
}
default <T extends IBaseResource> Consumer<T> withId(IIdType theId) {
return withId(theId.getIdPart());
default Consumer<IBaseResource> withId(IIdType theId) {
return t -> t.setId(theId.toUnqualifiedVersionless());
}
default Consumer<IBaseResource> withTag(String theSystem, String theCode) {
@ -153,19 +155,6 @@ public interface ITestDataBuilder {
};
}
/**
* Name chosen to avoid potential for conflict. This is an internal API to this interface.
*/
static void __setPrimitiveChild(FhirContext theFhirContext, IBaseResource theTarget, String theElementName, String theElementType, String theValue) {
RuntimeResourceDefinition def = theFhirContext.getResourceDefinition(theTarget.getClass());
BaseRuntimeChildDefinition activeChild = def.getChildByName(theElementName);
IPrimitiveType<?> booleanType = (IPrimitiveType<?>) activeChild.getChildByName(theElementName).newInstance();
booleanType.setValueAsString(theValue);
activeChild.getMutator().addValue(theTarget, booleanType);
}
/**
* Users of this API must implement this method
*/
@ -181,5 +170,17 @@ public interface ITestDataBuilder {
*/
FhirContext getFhirContext();
/**
* Name chosen to avoid potential for conflict. This is an internal API to this interface.
*/
static void __setPrimitiveChild(FhirContext theFhirContext, IBaseResource theTarget, String theElementName, String theElementType, String theValue) {
RuntimeResourceDefinition def = theFhirContext.getResourceDefinition(theTarget.getClass());
BaseRuntimeChildDefinition activeChild = def.getChildByName(theElementName);
IPrimitiveType<?> booleanType = (IPrimitiveType<?>) activeChild.getChildByName(theElementName).newInstance();
booleanType.setValueAsString(theValue);
activeChild.getMutator().addValue(theTarget, booleanType);
}
}

11
pom.xml
View File

@ -870,10 +870,15 @@
<artifactId>commons-csv</artifactId>
<version>1.7</version>
</dependency>
<dependency>
<groupId>org.hl7.fhir.testcases</groupId>
<artifactId>fhir-test-cases</artifactId>
<version>1.1.14-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>17.0.0</version>
<version>19.0.0</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
@ -1377,12 +1382,12 @@
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.2.0</version>
<version>3.3.3</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.2.10</version>
<version>42.2.12</version>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>