More work on patch

This commit is contained in:
James Agnew 2016-09-17 16:55:21 -04:00
parent ad447126f2
commit 6e97936eca
15 changed files with 473 additions and 179 deletions

View File

@ -29,69 +29,23 @@ import java.io.PushbackReader;
import java.io.Reader;
import java.io.Writer;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.*;
import java.util.Map.Entry;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.text.WordUtils;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseBinary;
import org.hl7.fhir.instance.model.api.IBaseBooleanDatatype;
import org.hl7.fhir.instance.model.api.IBaseDecimalDatatype;
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.IBaseIntegerDatatype;
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.INarrative;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.instance.model.api.*;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSyntaxException;
import com.google.gson.*;
import com.google.gson.stream.JsonWriter;
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
import ca.uhn.fhir.context.*;
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.RuntimeChildContainedResources;
import ca.uhn.fhir.context.RuntimeChildDeclaredExtensionDefinition;
import ca.uhn.fhir.context.RuntimeChildNarrativeDefinition;
import ca.uhn.fhir.context.RuntimeChildUndeclaredExtensionDefinition;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.model.api.BaseBundle;
import ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.model.api.BundleEntry;
import ca.uhn.fhir.model.api.ExtensionDt;
import ca.uhn.fhir.model.api.IPrimitiveDatatype;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.ISupportsUndeclaredExtensions;
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.api.*;
import ca.uhn.fhir.model.api.annotation.Child;
import ca.uhn.fhir.model.base.composite.BaseCodingDt;
import ca.uhn.fhir.model.base.composite.BaseContainedDt;
import ca.uhn.fhir.model.primitive.DecimalDt;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.model.primitive.IntegerDt;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.model.primitive.*;
import ca.uhn.fhir.narrative.INarrativeGenerator;
import ca.uhn.fhir.rest.server.EncodingEnum;
import ca.uhn.fhir.util.ElementUtil;
@ -170,6 +124,16 @@ public class JsonParser extends BaseParser implements IParser {
}
}
private void addToHeldIds(int theValueIdx, ArrayList<String> theListToAddTo, String theId) {
theListToAddTo.ensureCapacity(theValueIdx);
while (theListToAddTo.size() <= theValueIdx) {
theListToAddTo.add(null);
}
if (theListToAddTo.get(theValueIdx) == null) {
theListToAddTo.set(theValueIdx, theId);
}
}
private void assertObjectOfType(JsonElement theResourceTypeObj, Object theValueType, String thePosition) {
// if (theResourceTypeObj == null) {
// throw new DataFormatException("Invalid JSON content detected, missing required element: '" + thePosition + "'");
@ -180,6 +144,16 @@ public class JsonParser extends BaseParser implements IParser {
// }
}
private void beginArray(JsonWriter theEventWriter, String arrayName) throws IOException {
theEventWriter.name(arrayName);
theEventWriter.beginArray();
}
private void beginObject(JsonWriter theEventWriter, String arrayName) throws IOException {
theEventWriter.name(arrayName);
theEventWriter.beginObject();
}
private JsonWriter createJsonWriter(Writer theWriter) {
JsonWriter retVal = new JsonWriter(theWriter);
retVal.setSerializeNulls(true);
@ -234,37 +208,6 @@ public class JsonParser extends BaseParser implements IParser {
return retVal;
}
private JsonObject parse(Reader theReader) {
PushbackReader pbr = new PushbackReader(theReader);
JsonObject object;
try {
while(true) {
int nextInt;
nextInt = pbr.read();
if (nextInt == -1) {
throw new DataFormatException("Did not find any content to parse");
}
if (nextInt == '{') {
pbr.unread('{');
break;
}
if (Character.isWhitespace(nextInt)) {
continue;
}
throw new DataFormatException("Content does not appear to be FHIR JSON, first non-whitespace character was: '" + (char)nextInt + "' (must be '{')");
}
Gson gson = new GsonBuilder().disableHtmlEscaping().create();
object = gson.fromJson(pbr, JsonObject.class);
} catch (Exception e) {
throw new DataFormatException("Failed to parse JSON content, error was: " + e.getMessage(), e);
}
return object;
}
private void encodeBundleToWriterInDstu1Format(Bundle theBundle, JsonWriter theEventWriter) throws IOException {
theEventWriter.beginObject();
@ -560,23 +503,6 @@ public class JsonParser extends BaseParser implements IParser {
}
private void write(JsonWriter theEventWriter, String theChildName, Boolean theValue) throws IOException {
if (theValue != null) {
theEventWriter.name(theChildName);
theEventWriter.value(theValue.booleanValue());
}
}
private void write(JsonWriter theEventWriter, String theChildName, BigDecimal theDecimalValue) throws IOException {
theEventWriter.name(theChildName);
theEventWriter.value(theDecimalValue);
}
private void write(JsonWriter theEventWriter, String theChildName, Integer theValue) throws IOException {
theEventWriter.name(theChildName);
theEventWriter.value(theValue);
}
private void encodeCompositeElementChildrenToStreamWriter(RuntimeResourceDefinition theResDef, IBaseResource theResource, IBase theElement, JsonWriter theEventWriter, boolean theContainedResource, CompositeChildElement theParent) throws IOException {
{
@ -796,16 +722,6 @@ public class JsonParser extends BaseParser implements IParser {
}
}
private void addToHeldIds(int theValueIdx, ArrayList<String> theListToAddTo, String theId) {
theListToAddTo.ensureCapacity(theValueIdx);
while (theListToAddTo.size() <= theValueIdx) {
theListToAddTo.add(null);
}
if (theListToAddTo.get(theValueIdx) == null) {
theListToAddTo.set(theValueIdx, theId);
}
}
private void encodeCompositeElementToStreamWriter(RuntimeResourceDefinition theResDef, IBaseResource theResource, IBase theNextValue, JsonWriter theEventWriter, boolean theContainedResource, CompositeChildElement theParent) throws IOException, DataFormatException {
writeCommentsPreAndPost(theNextValue, theEventWriter);
@ -1083,6 +999,37 @@ public class JsonParser extends BaseParser implements IParser {
return (JsonArray) object;
}
private JsonObject parse(Reader theReader) {
PushbackReader pbr = new PushbackReader(theReader);
JsonObject object;
try {
while(true) {
int nextInt;
nextInt = pbr.read();
if (nextInt == -1) {
throw new DataFormatException("Did not find any content to parse");
}
if (nextInt == '{') {
pbr.unread('{');
break;
}
if (Character.isWhitespace(nextInt)) {
continue;
}
throw new DataFormatException("Content does not appear to be FHIR JSON, first non-whitespace character was: '" + (char)nextInt + "' (must be '{')");
}
Gson gson = newGson();
object = gson.fromJson(pbr, JsonObject.class);
} catch (Exception e) {
throw new DataFormatException("Failed to parse JSON content, error was: " + e.getMessage(), e);
}
return object;
}
private void parseAlternates(JsonElement theAlternateVal, ParserState<?> theState, String theElementName) {
if (theAlternateVal == null || theAlternateVal instanceof JsonNull) {
return;
@ -1404,6 +1351,17 @@ public class JsonParser extends BaseParser implements IParser {
return state.getObject();
}
@Override
public IParser setPrettyPrint(boolean thePrettyPrint) {
myPrettyPrint = thePrettyPrint;
return this;
}
private void write(JsonWriter theEventWriter, String theChildName, BigDecimal theDecimalValue) throws IOException {
theEventWriter.name(theChildName);
theEventWriter.value(theDecimalValue);
}
// private void parseExtensionInDstu2Style(boolean theModifier, ParserState<?> theState, String
// theParentExtensionUrl, String theExtensionUrl, JsonArray theValues) {
// String extUrl = UrlUtil.constructAbsoluteUrl(theParentExtensionUrl, theExtensionUrl);
@ -1430,10 +1388,16 @@ public class JsonParser extends BaseParser implements IParser {
// theState.endingElement();
// }
@Override
public IParser setPrettyPrint(boolean thePrettyPrint) {
myPrettyPrint = thePrettyPrint;
return this;
private void write(JsonWriter theEventWriter, String theChildName, Boolean theValue) throws IOException {
if (theValue != null) {
theEventWriter.name(theChildName);
theEventWriter.value(theValue.booleanValue());
}
}
private void write(JsonWriter theEventWriter, String theChildName, Integer theValue) throws IOException {
theEventWriter.name(theChildName);
theEventWriter.value(theValue);
}
private boolean writeAtomLinkInDstu1Format(JsonWriter theEventWriter, String theRel, StringDt theLink, boolean theStarted) throws IOException {
@ -1481,16 +1445,6 @@ public class JsonParser extends BaseParser implements IParser {
}
}
private void beginArray(JsonWriter theEventWriter, String arrayName) throws IOException {
theEventWriter.name(arrayName);
theEventWriter.beginArray();
}
private void beginObject(JsonWriter theEventWriter, String arrayName) throws IOException {
theEventWriter.name(arrayName);
theEventWriter.beginObject();
}
private void writeCategories(JsonWriter theEventWriter, TagList categories) throws IOException {
if (categories != null && categories.size() > 0) {
theEventWriter.name("category");
@ -1586,13 +1540,23 @@ public class JsonParser extends BaseParser implements IParser {
// }
}
public static Gson newGson() {
Gson gson = new GsonBuilder().disableHtmlEscaping().create();
return gson;
}
private static void write(JsonWriter theWriter, String theName, String theValue) throws IOException {
theWriter.name(theName);
theWriter.value(theValue);
}
private class HeldExtension implements Comparable<HeldExtension> {
private CompositeChildElement myChildElem;
private RuntimeChildDeclaredExtensionDefinition myDef;
private boolean myModifier;
private IBaseExtension<?, ?> myUndeclaredExtension;
private IBase myValue;
private CompositeChildElement myChildElem;
public HeldExtension(IBaseExtension<?, ?> theUndeclaredExtension, boolean theModifier, CompositeChildElement theChildElem) {
assert theUndeclaredExtension != null;
@ -1697,10 +1661,5 @@ public class JsonParser extends BaseParser implements IParser {
}
}
private static void write(JsonWriter theWriter, String theName, String theValue) throws IOException {
theWriter.name(theName);
theWriter.value(theValue);
}
}

View File

@ -2359,9 +2359,9 @@ public class GenericClient extends BaseClient implements IGenericClient {
BaseHttpClientInvocation invocation;
if (mySearchUrl != null) {
invocation = MethodUtil.createPatchInvocation(myContext, myResource, myResourceBody, mySearchUrl);
invocation = null;//MethodUtil.createPatchInvocation(myContext, myResource, myResourceBody, mySearchUrl);
} else if (myCriterionList != null) {
invocation = MethodUtil.createPatchInvocation(myContext, myResource, myResourceBody, myCriterionList.toParamList());
invocation = null;//MethodUtil.createPatchInvocation(myContext, myResource, myResourceBody, myCriterionList.toParamList());
} else {
if (myId == null) {
myId = myResource.getIdElement();

View File

@ -402,7 +402,7 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
throw new ConfigurationException("Method '" + theMethod.getName() + "' from type " + theMethod.getDeclaringClass().getCanonicalName() + " is annotated with @"
+ GetTags.class.getSimpleName() + " but does not return type " + TagList.class.getName());
}
} else if (MethodOutcome.class.equals(returnTypeFromMethod)) {
} else if (MethodOutcome.class.isAssignableFrom(returnTypeFromMethod)) {
// returns a method outcome
} else if (IBundleProvider.class.equals(returnTypeFromMethod)) {
// returns a bundle provider

View File

@ -22,7 +22,7 @@ public abstract class BaseOutcomeReturningMethodBindingWithResourceIdButNoResour
super(theMethod, theContext, theMethodAnnotationType, theProvider);
Class<? extends IBaseResource> resourceType = theResourceTypeFromAnnotation;
if (resourceType != IResource.class) {
if (resourceType != IBaseResource.class) {
RuntimeResourceDefinition def = theContext.getResourceDefinition(resourceType);
myResourceName = def.getName();
} else {

View File

@ -99,7 +99,18 @@
<groupId>org.jscience</groupId>
<artifactId>jscience</artifactId>
</dependency>
<!-- Patch Dependencies -->
<dependency>
<groupId>net.riotopsys</groupId>
<artifactId>json_patch</artifactId>
</dependency>
<dependency>
<groupId>com.github.dnault</groupId>
<artifactId>xml-patch</artifactId>
<version>0.3.0</version>
</dependency>
<!-- FHIR RI is pulled in for UCUM support, but we don't want any of its dependencies. -->
<!-- <dependency> <groupId>me.fhir</groupId> <artifactId>fhir-dstu1</artifactId> <version>0.0.81.2489</version> <exclusions> <exclusion> <artifactId>Saxon-HE</artifactId> <groupId>net.sf.saxon</groupId>
</exclusion> <exclusion> <artifactId>commons-discovery</artifactId> <groupId>commons-discovery</groupId> </exclusion> <exclusion> <artifactId>commons-codec</artifactId> <groupId>commons-codec</groupId>

View File

@ -22,28 +22,15 @@ package ca.uhn.fhir.jpa.dao;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.Map.Entry;
import java.util.Set;
import javax.annotation.PostConstruct;
import javax.persistence.NoResultException;
import javax.persistence.TypedQuery;
import org.hl7.fhir.dstu3.model.IdType;
import org.hl7.fhir.instance.model.api.IAnyResource;
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.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.instance.model.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.transaction.PlatformTransactionManager;
@ -57,33 +44,21 @@ import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao;
import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamUriDao;
import ca.uhn.fhir.jpa.dao.data.ISearchResultDao;
import ca.uhn.fhir.jpa.entity.BaseHasResource;
import ca.uhn.fhir.jpa.entity.BaseTag;
import ca.uhn.fhir.jpa.entity.ResourceHistoryTable;
import ca.uhn.fhir.jpa.entity.ResourceLink;
import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.jpa.entity.TagDefinition;
import ca.uhn.fhir.jpa.entity.TagTypeEnum;
import ca.uhn.fhir.jpa.entity.*;
import ca.uhn.fhir.jpa.interceptor.IJpaServerInterceptor;
import ca.uhn.fhir.jpa.term.IHapiTerminologySvc;
import ca.uhn.fhir.jpa.util.DeleteConflict;
import ca.uhn.fhir.jpa.util.StopWatch;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.model.api.TagList;
import ca.uhn.fhir.jpa.util.jsonpatch.JsonPatchUtils;
import ca.uhn.fhir.jpa.util.xmlpatch.XmlPatchUtils;
import ca.uhn.fhir.model.api.*;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.PatchTypeEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.method.RequestDetails;
import ca.uhn.fhir.rest.method.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.server.IBundleProvider;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.rest.server.exceptions.*;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.util.FhirTerser;
@ -1100,6 +1075,29 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
return outcome;
}
@Override
public DaoMethodOutcome patch(IIdType theId, PatchTypeEnum thePatchType, String thePatchBody, RequestDetails theRequestDetails) {
ResourceTable entityToUpdate = readEntityLatestVersion(theId);
if (theId.hasVersionIdPart() && theId.getVersionIdPartAsLong() != entityToUpdate.getVersion()) {
throw new PreconditionFailedException("Version " + theId.getVersionIdPart() + " is not the most recent version of this resource, unable to apply patch");
}
validateResourceType(entityToUpdate);
IBaseResource resourceToUpdate = toResource(entityToUpdate, false);
IBaseResource destination;
if (thePatchType == PatchTypeEnum.JSON_PATCH) {
destination = JsonPatchUtils.apply(getContext(), resourceToUpdate, thePatchBody);
} else {
destination = XmlPatchUtils.apply(getContext(), resourceToUpdate, thePatchBody);
}
@SuppressWarnings("unchecked")
T destinationCasted = (T) destination;
return update(destinationCasted, null, true, theRequestDetails);
}
@Override
public DaoMethodOutcome update(T theResource, String theMatchUrl, RequestDetails theRequestDetails) {
return update(theResource, theMatchUrl, true, theRequestDetails);

View File

@ -36,6 +36,7 @@ import ca.uhn.fhir.jpa.util.DeleteConflict;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.model.api.TagList;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.PatchTypeEnum;
import ca.uhn.fhir.rest.api.ValidationModeEnum;
import ca.uhn.fhir.rest.method.RequestDetails;
import ca.uhn.fhir.rest.server.EncodingEnum;
@ -210,6 +211,8 @@ public interface IFhirResourceDao<T extends IBaseResource> extends IDao {
*/
MethodOutcome validate(T theResource, IIdType theId, String theRawResource, EncodingEnum theEncoding, ValidationModeEnum theMode, String theProfile, RequestDetails theRequestDetails);
DaoMethodOutcome patch(IIdType theId, PatchTypeEnum thePatchType, String thePatchBody, RequestDetails theRequestDetails);
// /**
// * Invoke the everything operation
// */

View File

@ -28,14 +28,11 @@ import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.springframework.beans.factory.annotation.Required;
import ca.uhn.fhir.jpa.dao.DaoMethodOutcome;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.model.api.TagList;
import ca.uhn.fhir.rest.annotation.At;
import ca.uhn.fhir.rest.annotation.GetTags;
import ca.uhn.fhir.rest.annotation.History;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Read;
import ca.uhn.fhir.rest.annotation.Since;
import ca.uhn.fhir.rest.annotation.*;
import ca.uhn.fhir.rest.api.PatchTypeEnum;
import ca.uhn.fhir.rest.method.RequestDetails;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.server.IBundleProvider;
@ -128,6 +125,16 @@ public abstract class BaseJpaResourceProvider<T extends IBaseResource> extends B
}
}
@Patch
public DaoMethodOutcome patch(HttpServletRequest theRequest, @IdParam IIdType theId, RequestDetails theRequestDetails, @ResourceParam String theBody, PatchTypeEnum thePatchType) {
startRequest(theRequest);
try {
return myDao.patch(theId, thePatchType, theBody, theRequestDetails);
} finally {
endRequest(theRequest);
}
}
@Required
public void setDao(IFhirResourceDao<T> theDao) {
myDao = theDao;

View File

@ -0,0 +1,61 @@
package ca.uhn.fhir.jpa.util.jsonpatch;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import net.riotopsys.json_patch.JsonPath;
import net.riotopsys.json_patch.operation.AbsOperation;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class CopyOperation extends AbsOperation {
public JsonPath mySourcePath;
public CopyOperation(JsonPath theTargetPath, JsonPath theSourcePath) {
mySourcePath = theSourcePath;
this.path = theTargetPath;
}
@Override
public String getOperationName() {
return "copy";
}
@Override
public JsonElement apply(JsonElement original) {
JsonElement result = duplicate(original);
JsonElement item = path.head().navigate(result);
JsonElement data = mySourcePath.head().navigate(original);
if (item.isJsonObject()) {
item.getAsJsonObject().add(path.tail(), data);
} else if (item.isJsonArray()) {
JsonArray array = item.getAsJsonArray();
int index = (path.tail().equals("-")) ? array.size() : Integer.valueOf(path.tail());
List<JsonElement> temp = new ArrayList<JsonElement>();
Iterator<JsonElement> iter = array.iterator();
while (iter.hasNext()) {
JsonElement stuff = iter.next();
iter.remove();
temp.add(stuff);
}
temp.add(index, data);
for (JsonElement stuff : temp) {
array.add(stuff);
}
}
return result;
}
}

View File

@ -0,0 +1,72 @@
package ca.uhn.fhir.jpa.util.jsonpatch;
import org.hl7.fhir.instance.model.api.IBaseResource;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.parser.JsonParser;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import net.riotopsys.json_patch.JsonPatch;
import net.riotopsys.json_patch.JsonPath;
import net.riotopsys.json_patch.operation.AddOperation;
import net.riotopsys.json_patch.operation.MoveOperation;
import net.riotopsys.json_patch.operation.RemoveOperation;
import net.riotopsys.json_patch.operation.ReplaceOperation;
public class JsonPatchUtils {
public static <T extends IBaseResource> T apply(FhirContext theCtx, T theResourceToUpdate, String thePatchBody) {
JsonPatch parsedPatch = new JsonPatch();
// Parse the patch
Gson gson = JsonParser.newGson();
JsonElement jsonElement = gson.fromJson(thePatchBody, JsonElement.class);
JsonArray array = jsonElement.getAsJsonArray();
for (JsonElement nextElement : array) {
JsonObject nextElementAsObject = (JsonObject) nextElement;
String opName = nextElementAsObject.get("op").getAsString();
if ("add".equals(opName)) {
AddOperation op = new AddOperation(toPath(nextElementAsObject), nextElementAsObject.get("value"));
parsedPatch.add(op);
} else if ("remove".equals(opName)) {
RemoveOperation op = new RemoveOperation(toPath(nextElementAsObject));
parsedPatch.add(op);
} else if ("replace".equals(opName)) {
ReplaceOperation op = new ReplaceOperation(toPath(nextElementAsObject), nextElementAsObject.get("value"));
parsedPatch.add(op);
} else if ("copy".equals(opName)) {
CopyOperation op = new CopyOperation(toPath(nextElementAsObject), toFromPath(nextElementAsObject));
parsedPatch.add(op);
} else if ("move".equals(opName)) {
MoveOperation op = new MoveOperation(toPath(nextElementAsObject), toFromPath(nextElementAsObject));
parsedPatch.add(op);
} else {
throw new InvalidRequestException("Invalid JSON PATCH operation: " + opName);
}
}
@SuppressWarnings("unchecked")
Class<T> clazz = (Class<T>) theResourceToUpdate.getClass();
JsonElement originalJsonDocument = gson.fromJson(theCtx.newJsonParser().encodeResourceToString(theResourceToUpdate), JsonElement.class);
JsonElement target = parsedPatch.apply(originalJsonDocument);
T retVal = theCtx.newJsonParser().parseResource(clazz, gson.toJson(target));
return retVal;
}
private static JsonPath toFromPath(JsonObject nextElementAsObject) {
return new JsonPath(nextElementAsObject.get("from").getAsString());
}
private static JsonPath toPath(JsonObject nextElementAsObject) {
return new JsonPath(nextElementAsObject.get("path").getAsString());
}
}

View File

@ -0,0 +1,38 @@
package ca.uhn.fhir.jpa.util.xmlpatch;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.hl7.fhir.instance.model.api.IBaseResource;
import com.github.dnault.xmlpatch.Patcher;
import com.phloc.commons.io.streams.StringInputStream;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
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 StringInputStream(inputResource, StandardCharsets.UTF_8), new StringInputStream(thePatchBody, StandardCharsets.UTF_8), result);
} catch (IOException e) {
throw new InternalErrorException(e);
}
String resultString = new String(result.toByteArray(), StandardCharsets.UTF_8);
T retVal = theCtx.newXmlParser().parseResource(clazz, resultString);
return retVal;
}
}

View File

@ -36,11 +36,7 @@ import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.*;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.hl7.fhir.dstu3.model.AuditEvent;
@ -130,6 +126,8 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
TestUtil.clearAllStaticFieldsForUnitTest();
}
@Test
public void testSearchPagingKeepsOldSearches() throws Exception {
@ -171,7 +169,67 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
}
}
@Test
public void testPatchUsingJsonPatch() throws Exception {
String methodName = "testPatchUsingJsonPatch";
IIdType pid1;
{
Patient patient = new Patient();
patient.setActive(true);
patient.addIdentifier().setSystem("urn:system").setValue("0");
patient.addName().addFamily(methodName).addGiven("Joe");
pid1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless();
}
HttpPatch patch = new HttpPatch(ourServerBase + "/Patient/" + pid1.getIdPart());
patch.setEntity(new StringEntity("[ { \"op\":\"replace\", \"path\":\"/active\", \"value\":false } ]", ContentType.parse(Constants.CT_JSON_PATCH + Constants.CHARSET_UTF8_CTSUFFIX)));
CloseableHttpResponse response = ourHttpClient.execute(patch);
try {
assertEquals(200, response.getStatusLine().getStatusCode());
String responseString = IOUtils.toString(response.getEntity().getContent(),StandardCharsets.UTF_8);
assertThat(responseString, containsString("<OperationOutcome"));
assertThat(responseString, containsString("INFORMATION"));
} finally {
response.close();
}
Patient newPt = ourClient.read().resource(Patient.class).withId(pid1.getIdPart()).execute();
assertEquals("2", newPt.getIdElement().getVersionIdPart());
assertEquals(false, newPt.getActive());
}
@Test
public void testPatchUsingXmlPatch() throws Exception {
String methodName = "testPatchUsingXmlPatch";
IIdType pid1;
{
Patient patient = new Patient();
patient.setActive(true);
patient.addIdentifier().setSystem("urn:system").setValue("0");
patient.addName().addFamily(methodName).addGiven("Joe");
pid1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless();
}
HttpPatch patch = new HttpPatch(ourServerBase + "/Patient/" + pid1.getIdPart());
String patchString = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><diff xmlns:fhir=\"http://hl7.org/fhir\"><replace sel=\"fhir:Patient/fhir:active/@value\">false</replace></diff>";
patch.setEntity(new StringEntity(patchString, ContentType.parse(Constants.CT_XML_PATCH + Constants.CHARSET_UTF8_CTSUFFIX)));
CloseableHttpResponse response = ourHttpClient.execute(patch);
try {
assertEquals(200, response.getStatusLine().getStatusCode());
String responseString = IOUtils.toString(response.getEntity().getContent(),StandardCharsets.UTF_8);
assertThat(responseString, containsString("<OperationOutcome"));
assertThat(responseString, containsString("INFORMATION"));
} finally {
response.close();
}
Patient newPt = ourClient.read().resource(Patient.class).withId(pid1.getIdPart()).execute();
assertEquals("2", newPt.getIdElement().getVersionIdPart());
assertEquals(false, newPt.getActive());
}
@Test
public void testHasParameter() throws Exception {
IIdType pid0;

View File

@ -0,0 +1,36 @@
package ca.uhn.fhir.jpa.util.jsonpatch;
import static org.junit.Assert.*;
import org.hl7.fhir.dstu3.model.Patient;
import org.junit.AfterClass;
import org.junit.Test;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.util.TestUtil;
public class JsonPatchTest {
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();
}
private static FhirContext ourCtx = FhirContext.forDstu3();
@Test
public void testPatchReplace() {
Patient p = new Patient();
p.setActive(true);
//@formatter:off
String patchBody = "[\n" +
" { \"op\":\"replace\", \"path\":\"/active\", \"value\":false }\n" +
"]";
//@formatter:on
Patient dest = JsonPatchUtils.apply(ourCtx, p, patchBody);
assertEquals(false, dest.getActive());
}
}

View File

@ -0,0 +1,35 @@
package ca.uhn.fhir.jpa.util.xmlpatch;
import static org.junit.Assert.assertEquals;
import org.hl7.fhir.dstu3.model.Patient;
import org.junit.AfterClass;
import org.junit.Test;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.util.xmlpatch.XmlPatchUtils;
import ca.uhn.fhir.util.TestUtil;
public class XmlPatchTest {
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();
}
private static FhirContext ourCtx = FhirContext.forDstu3();
@Test
public void testPatchReplace() {
Patient p = new Patient();
p.setActive(true);
//@formatter:off
String patchBody = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><diff xmlns:fhir=\"http://hl7.org/fhir\"><replace sel=\"fhir:Patient/fhir:active/@value\">false</replace></diff>";
//@formatter:on
Patient dest = XmlPatchUtils.apply(ourCtx, p, patchBody);
assertEquals(false, dest.getActive());
}
}

16
pom.xml
View File

@ -40,6 +40,17 @@
<developerConnection>scm:git:git@github.com:jamesagnew/hapi-fhir.git</developerConnection>
</scm>
<repositories>
<repository>
<snapshots>
<enabled>false</enabled>
</snapshots>
<id>bintray-dnault-maven</id>
<name>bintray</name>
<url>http://dl.bintray.com/dnault/maven</url>
</repository>
</repositories>
<description>
</description>
@ -304,6 +315,11 @@
<artifactId>logback-classic</artifactId>
<version>1.1.7</version>
</dependency>
<dependency>
<groupId>com.github.dnault</groupId>
<artifactId>xml-patch</artifactId>
<version>0.3.0</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>