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:
parent
b044d05332
commit
5b2181a563
|
@ -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'
|
||||
|
|
|
@ -164,6 +164,9 @@ public abstract class BaseRuntimeDeclaredChildDefinition extends BaseRuntimeChil
|
|||
}
|
||||
if (theClear) {
|
||||
existingList.clear();
|
||||
if (theValue == null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
existingList.add(theValue);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
/**
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
|||
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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."
|
|
@ -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."
|
|
@ -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.
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
} ]
|
||||
} ]
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
spring.test.context.cache.maxSize=2
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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++;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
|
|
|
@ -67,6 +67,7 @@ public class TestDstu2Config extends BaseJavaConfigDstu2 {
|
|||
retVal.setFetchSizeDefaultMaximum(10000);
|
||||
retVal.setWebsocketContextPath("/");
|
||||
retVal.setFilterParameterEnabled(true);
|
||||
retVal.setDefaultSearchParamsCanBeOverridden(false);
|
||||
return retVal;
|
||||
}
|
||||
|
||||
|
|
|
@ -66,6 +66,7 @@ public class TestDstu3Config extends BaseJavaConfigDstu3 {
|
|||
retVal.setReindexThreadCount(1);
|
||||
retVal.setExpungeEnabled(true);
|
||||
retVal.setFilterParameterEnabled(true);
|
||||
retVal.setDefaultSearchParamsCanBeOverridden(false);
|
||||
return retVal;
|
||||
}
|
||||
|
||||
|
|
|
@ -66,6 +66,7 @@ public class TestR4Config extends BaseJavaConfigR4 {
|
|||
retVal.setFetchSizeDefaultMaximum(10000);
|
||||
retVal.setExpungeEnabled(true);
|
||||
retVal.setFilterParameterEnabled(true);
|
||||
retVal.setDefaultSearchParamsCanBeOverridden(false);
|
||||
return retVal;
|
||||
}
|
||||
|
||||
|
|
|
@ -66,6 +66,7 @@ public class TestR5Config extends BaseJavaConfigR5 {
|
|||
retVal.setFetchSizeDefaultMaximum(10000);
|
||||
retVal.setExpungeEnabled(true);
|
||||
retVal.setFilterParameterEnabled(true);
|
||||
retVal.setDefaultSearchParamsCanBeOverridden(false);
|
||||
return retVal;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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
11
pom.xml
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue