diff --git a/hapi-fhir-base/.classpath b/hapi-fhir-base/.classpath index 3e3200180cf..d0b602b45e1 100644 --- a/hapi-fhir-base/.classpath +++ b/hapi-fhir-base/.classpath @@ -30,7 +30,7 @@ - + diff --git a/hapi-fhir-base/.settings/org.eclipse.jdt.core.prefs b/hapi-fhir-base/.settings/org.eclipse.jdt.core.prefs index 5e625f15742..201420a2537 100644 --- a/hapi-fhir-base/.settings/org.eclipse.jdt.core.prefs +++ b/hapi-fhir-base/.settings/org.eclipse.jdt.core.prefs @@ -6,13 +6,8 @@ org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annota org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.debug.lineNumber=generate -org.eclipse.jdt.core.compiler.debug.localVariable=generate -org.eclipse.jdt.core.compiler.debug.sourceFile=generate org.eclipse.jdt.core.compiler.doc.comment.support=enabled org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning org.eclipse.jdt.core.compiler.problem.assertIdentifier=error diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java index 895e795a5f1..d9c4f2382b7 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java @@ -107,6 +107,11 @@ import ca.uhn.fhir.rest.gclient.IOperationUntyped; import ca.uhn.fhir.rest.gclient.IOperationUntypedWithInput; import ca.uhn.fhir.rest.gclient.IOperationUntypedWithInputAndPartialOutput; import ca.uhn.fhir.rest.gclient.IParam; +import ca.uhn.fhir.rest.gclient.IPatch; +import ca.uhn.fhir.rest.gclient.IPatchExecutable; +import ca.uhn.fhir.rest.gclient.IPatchTyped; +import ca.uhn.fhir.rest.gclient.IPatchWithQuery; +import ca.uhn.fhir.rest.gclient.IPatchWithQueryTyped; import ca.uhn.fhir.rest.gclient.IQuery; import ca.uhn.fhir.rest.gclient.IRead; import ca.uhn.fhir.rest.gclient.IReadExecutable; @@ -519,6 +524,32 @@ public class GenericClient extends BaseClient implements IGenericClient { return new ArrayList(resp.toListOfResources()); } + + + @Override + public IPatch patch() { + return new PatchInternal(); + } + + @Override + public MethodOutcome patch(IdDt theIdDt, IBaseResource theResource) { + BaseHttpClientInvocation invocation = MethodUtil.createUpdateInvocation(theResource, null, theIdDt, myContext); + if (isKeepResponses()) { + myLastRequest = invocation.asHttpRequest(getServerBase(), createExtraParams(), getEncoding(), isPrettyPrint()); + } + + RuntimeResourceDefinition def = myContext.getResourceDefinition(theResource); + final String resourceName = def.getName(); + + OutcomeResponseHandler binding = new OutcomeResponseHandler(resourceName); + MethodOutcome resp = invokeClient(myContext, binding, invocation, myLogRequestAndResponse); + return resp; + } + + @Override + public MethodOutcome patch(String theId, IBaseResource theResource) { + return update(new IdDt(theId), theResource); + } @Override public IUpdate update() { @@ -2287,6 +2318,124 @@ public class GenericClient extends BaseClient implements IGenericClient { } } + + private class PatchInternal extends BaseClientExecutable implements IPatch, IPatchTyped, IPatchExecutable, IPatchWithQuery, IPatchWithQueryTyped { + + private CriterionList myCriterionList; + private IIdType myId; + private PreferReturnEnum myPrefer; + private IBaseResource myResource; + private String myResourceBody; + private String mySearchUrl; + + @Override + public IPatchWithQueryTyped and(ICriterion theCriterion) { + myCriterionList.add((ICriterionInternal) theCriterion); + return this; + } + + @Override + public IPatchWithQuery conditional() { + myCriterionList = new CriterionList(); + return this; + } + + @Override + public IPatchTyped conditionalByUrl(String theSearchUrl) { + mySearchUrl = validateAndEscapeConditionalUrl(theSearchUrl); + return this; + } + + @Override + public MethodOutcome execute() { + if (myResource == null) { + myResource = parseResourceBody(myResourceBody); + } + + // If an explicit encoding is chosen, we will re-serialize to ensure the right encoding + if (getParamEncoding() != null) { + myResourceBody = null; + } + + BaseHttpClientInvocation invocation; + if (mySearchUrl != null) { + invocation = MethodUtil.createPatchInvocation(myContext, myResource, myResourceBody, mySearchUrl); + } else if (myCriterionList != null) { + invocation = MethodUtil.createPatchInvocation(myContext, myResource, myResourceBody, myCriterionList.toParamList()); + } else { + if (myId == null) { + myId = myResource.getIdElement(); + } + + if (myId == null || myId.hasIdPart() == false) { + throw new InvalidRequestException("No ID supplied for resource to update, can not invoke server"); + } + invocation = MethodUtil.createUpdateInvocation(myResource, myResourceBody, myId, myContext); + } + + addPreferHeader(myPrefer, invocation); + + RuntimeResourceDefinition def = myContext.getResourceDefinition(myResource); + final String resourceName = def.getName(); + + OutcomeResponseHandler binding = new OutcomeResponseHandler(resourceName, myPrefer); + + Map> params = new HashMap>(); + return invoke(params, binding, invocation); + + } + + @Override + public IPatchExecutable prefer(PreferReturnEnum theReturn) { + myPrefer = theReturn; + return this; + } + + @Override + public IPatchTyped resource(IBaseResource theResource) { + Validate.notNull(theResource, "Resource can not be null"); + myResource = theResource; + return this; + } + + @Override + public IPatchTyped resource(String theResourceBody) { + Validate.notBlank(theResourceBody, "Body can not be null or blank"); + myResourceBody = theResourceBody; + return this; + } + + @Override + public IPatchWithQueryTyped where(ICriterion theCriterion) { + myCriterionList.add((ICriterionInternal) theCriterion); + 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()); + } + myId = theId; + return this; + } + + @Override + public IPatchExecutable withId(String theId) { + 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; + } + + } private class UpdateInternal extends BaseClientExecutable implements IUpdate, IUpdateTyped, IUpdateExecutable, IUpdateWithQuery, IUpdateWithQueryTyped { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/IGenericClient.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/IGenericClient.java index ac49faf4b8d..7d79af36e3e 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/IGenericClient.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/IGenericClient.java @@ -46,6 +46,7 @@ import ca.uhn.fhir.rest.gclient.IGetTags; import ca.uhn.fhir.rest.gclient.IHistory; import ca.uhn.fhir.rest.gclient.IMeta; import ca.uhn.fhir.rest.gclient.IOperation; +import ca.uhn.fhir.rest.gclient.IPatch; import ca.uhn.fhir.rest.gclient.IRead; import ca.uhn.fhir.rest.gclient.ITransaction; import ca.uhn.fhir.rest.gclient.IUntypedQuery; @@ -253,6 +254,35 @@ public interface IGenericClient extends IRestfulClient { @Override void registerInterceptor(IClientInterceptor theInterceptor); + + /** + * Fluent method for the "patch" operation, which performs a logical patch on a server resource + */ + IPatch patch(); + + /** + * Implementation of the "instance patch" method. + * + * @param theId + * The ID to update + * @param theResource + * The new resource body + * @return An outcome containing the results and possibly the new version ID + */ + MethodOutcome patch(IdDt theId, IBaseResource theResource); + + /** + * Implementation of the "instance update" method. + * + * @param theId + * The ID to update + * @param theResource + * The new resource body + * @return An outcome containing the results and possibly the new version ID + */ + MethodOutcome patch(String theId, IBaseResource theResource); + + /** * Search for resources matching a given set of criteria. Searching is a very powerful * feature in FHIR with many features for specifying exactly what should be seaerched for diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IPatch.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IPatch.java new file mode 100644 index 00000000000..7fc26ed0d7f --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IPatch.java @@ -0,0 +1,31 @@ +package ca.uhn.fhir.rest.gclient; + +/* + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2016 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 org.hl7.fhir.instance.model.api.IBaseResource; + +public interface IPatch { + + IPatchTyped resource(IBaseResource theResource); + + IPatchTyped resource(String theResourceBody); + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IPatchExecutable.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IPatchExecutable.java new file mode 100644 index 00000000000..3fb718598f3 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IPatchExecutable.java @@ -0,0 +1,37 @@ +package ca.uhn.fhir.rest.gclient; + +/* + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2016 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.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.api.PreferReturnEnum; + +public interface IPatchExecutable extends IClientExecutable{ + + /** + * Add a Prefer header to the request, which requests that the server include + * or suppress the resource body as a part of the result. If a resource is returned by the server + * it will be parsed an accessible to the client via {@link MethodOutcome#getResource()} + * + * @since HAPI 1.1 + */ + IPatchExecutable prefer(PreferReturnEnum theReturn); + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IPatchTyped.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IPatchTyped.java new file mode 100644 index 00000000000..edd47a09a71 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IPatchTyped.java @@ -0,0 +1,46 @@ +package ca.uhn.fhir.rest.gclient; + +/* + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2016 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 org.hl7.fhir.instance.model.api.IIdType; + +public interface IPatchTyped extends IPatchExecutable { + + IPatchExecutable withId(IIdType theId); + + IPatchExecutable withId(String theId); + + /** + * Specifies that the update should be performed as a conditional create + * against a given search URL. + * + * @param theSearchUrl The search URL to use. The format of this URL should be of the form [ResourceType]?[Parameters], + * for example: Patient?name=Smith&identifier=13.2.4.11.4%7C847366 + * @since HAPI 0.9 / FHIR DSTU 2 + */ + IPatchTyped conditionalByUrl(String theSearchUrl); + + /** + * @since HAPI 0.9 / FHIR DSTU 2 + */ + IPatchWithQuery conditional(); + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IPatchWithQuery.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IPatchWithQuery.java new file mode 100644 index 00000000000..32b94c5c02c --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IPatchWithQuery.java @@ -0,0 +1,26 @@ +package ca.uhn.fhir.rest.gclient; + +/* + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2016 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 interface IPatchWithQuery extends IBaseQuery { + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IPatchWithQueryTyped.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IPatchWithQueryTyped.java new file mode 100644 index 00000000000..cdc639f1dfa --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IPatchWithQueryTyped.java @@ -0,0 +1,25 @@ +package ca.uhn.fhir.rest.gclient; + +/* + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2016 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 interface IPatchWithQueryTyped extends IPatchTyped, IPatchWithQuery { + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/MethodUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/MethodUtil.java index ae040610e87..3db12c7f2a0 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/MethodUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/MethodUtil.java @@ -155,6 +155,90 @@ public class MethodUtil { retVal.setIfNoneExistString(theIfNoneExistUrl); return retVal; } + + /** Patch **/ + + public static HttpPatchClientInvocation createPatchInvocation(FhirContext theContext, IBaseResource theResource, String theResourceBody, Map> theMatchParams) { + StringBuilder b = new StringBuilder(); + + String resourceType = theContext.getResourceDefinition(theResource).getName(); + b.append(resourceType); + + boolean haveQuestionMark = false; + for (Entry> nextEntry : theMatchParams.entrySet()) { + for (String nextValue : nextEntry.getValue()) { + b.append(haveQuestionMark ? '&' : '?'); + haveQuestionMark = true; + b.append(UrlUtil.escape(nextEntry.getKey())); + b.append('='); + b.append(UrlUtil.escape(nextValue)); + } + } + + HttpPatchClientInvocation retVal; + if (StringUtils.isBlank(theResourceBody)) { + retVal = new HttpPatchClientInvocation(theContext, theResource, b.toString()); + } else { + retVal = new HttpPatchClientInvocation(theContext, theResourceBody, false, b.toString()); + } + + addTagsToPostOrPut(theContext, theResource, retVal); + + return retVal; + } + + + public static HttpPatchClientInvocation createPatchInvocation(FhirContext theContext, IBaseResource theResource, String theResourceBody, String theMatchUrl) { + HttpPatchClientInvocation retVal; + if (StringUtils.isBlank(theResourceBody)) { + retVal = new HttpPatchClientInvocation(theContext, theResource, theMatchUrl); + } else { + retVal = new HttpPatchClientInvocation(theContext, theResourceBody, false, theMatchUrl); + } + + addTagsToPostOrPut(theContext, theResource, retVal); + + return retVal; + } + + public static HttpPatchClientInvocation createPatchInvocation(IBaseResource theResource, String theResourceBody, IIdType theId, FhirContext theContext) { + String resourceName = theContext.getResourceDefinition(theResource).getName(); + StringBuilder urlBuilder = new StringBuilder(); + urlBuilder.append(resourceName); + urlBuilder.append('/'); + urlBuilder.append(theId.getIdPart()); + String urlExtension = urlBuilder.toString(); + + HttpPatchClientInvocation retVal; + if (StringUtils.isBlank(theResourceBody)) { + retVal = new HttpPatchClientInvocation(theContext, theResource, urlExtension); + } else { + retVal = new HttpPatchClientInvocation(theContext, theResourceBody, false, urlExtension); + } + + retVal.setForceResourceId(theId); + + if (theId.hasVersionIdPart()) { + if (theContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU1)) { + retVal.addHeader(Constants.HEADER_IF_MATCH, '"' + theId.getVersionIdPart() + '"'); + } else { + String versionId = theId.getVersionIdPart(); + if (StringUtils.isNotBlank(versionId)) { + urlBuilder.append('/'); + urlBuilder.append(Constants.PARAM_HISTORY); + urlBuilder.append('/'); + urlBuilder.append(versionId); + retVal.addHeader(Constants.HEADER_CONTENT_LOCATION, urlBuilder.toString()); + } + } + } + + addTagsToPostOrPut(theContext, theResource, retVal); + // addContentTypeHeaderBasedOnDetectedType(retVal, theResourceBody); + + return retVal; + } + /** End Patch **/ public static HttpPutClientInvocation createUpdateInvocation(FhirContext theContext, IBaseResource theResource, String theResourceBody, Map> theMatchParams) { StringBuilder b = new StringBuilder(); @@ -185,6 +269,7 @@ public class MethodUtil { return retVal; } + public static HttpPutClientInvocation createUpdateInvocation(FhirContext theContext, IBaseResource theResource, String theResourceBody, String theMatchUrl) { HttpPutClientInvocation retVal; if (StringUtils.isBlank(theResourceBody)) {