diff --git a/hapi-fhir-base/pom.xml b/hapi-fhir-base/pom.xml index c164631f413..92cc3122446 100644 --- a/hapi-fhir-base/pom.xml +++ b/hapi-fhir-base/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 0.3 + 0.3-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Validate.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Validate.java new file mode 100644 index 00000000000..50d5b86840e --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Validate.java @@ -0,0 +1,53 @@ +package ca.uhn.fhir.rest.annotation; + +/* + * #%L + * HAPI FHIR Library + * %% + * Copyright (C) 2014 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 java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import ca.uhn.fhir.model.api.IResource; +import ca.uhn.fhir.rest.server.IResourceProvider; + +/** + * RESTful method annotation to be used for the FHIR + * validate method. + * + *

+ * Validate is used to accept a resource, and test whether it would be acceptable for + * storing (e.g. using an update or create method) + *

+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target(value=ElementType.METHOD) +public @interface Validate { + + /** + * The return type for this method. This generally does not need + * to be populated for a server implementation (using a {@link IResourceProvider}, + * since resource providers will return only one resource type per class, + * but generally does need to be populated for client implementations. + */ + // NB: Read, Search (maybe others) share this annotation, so update the javadocs everywhere + Class type() default IResource.class; + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseMethodBinding.java index a19f4f99cfe..281a3845491 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseMethodBinding.java @@ -51,6 +51,7 @@ import ca.uhn.fhir.rest.annotation.Metadata; import ca.uhn.fhir.rest.annotation.Read; import ca.uhn.fhir.rest.annotation.Search; import ca.uhn.fhir.rest.annotation.Update; +import ca.uhn.fhir.rest.annotation.Validate; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.client.BaseClientInvocation; import ca.uhn.fhir.rest.client.exceptions.NonFhirResponseException; @@ -139,8 +140,9 @@ public abstract class BaseMethodBinding { Update update = theMethod.getAnnotation(Update.class); Delete delete = theMethod.getAnnotation(Delete.class); History history = theMethod.getAnnotation(History.class); + Validate validate = theMethod.getAnnotation(Validate.class); // ** if you add another annotation above, also add it to the next line: - if (!verifyMethodHasZeroOrOneOperationAnnotation(theMethod, read, search, conformance, create, update, delete, history)) { + if (!verifyMethodHasZeroOrOneOperationAnnotation(theMethod, read, search, conformance, create, update, delete, history, validate)) { return null; } @@ -188,6 +190,8 @@ public abstract class BaseMethodBinding { returnTypeFromAnnotation = create.type(); } else if (update != null) { returnTypeFromAnnotation = update.type(); + } else if (validate != null) { + returnTypeFromAnnotation = validate.type(); } if (returnTypeFromRp != null) { @@ -232,6 +236,8 @@ public abstract class BaseMethodBinding { return new DeleteMethodBinding(theMethod, theContext, theProvider); } else if (history != null) { return new HistoryMethodBinding(theMethod, theContext, theProvider); + } else if (validate != null) { + return new ValidateMethodBinding(theMethod, theContext, theProvider); } else { throw new ConfigurationException("Did not detect any FHIR annotations on method '" + theMethod.getName() + "' on type: " + theMethod.getDeclaringClass().getCanonicalName()); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBinding.java index 5ff211f5c26..6c55c178ce0 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBinding.java @@ -24,7 +24,6 @@ import java.io.IOException; import java.io.PushbackReader; import java.io.Reader; import java.io.Writer; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.List; import java.util.Map; @@ -50,7 +49,6 @@ import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.EncodingUtil; import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; @@ -220,12 +218,22 @@ abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding { if (!getResourceName().equals(theRequest.getResourceName())) { return false; } - if (StringUtils.isNotBlank(theRequest.getOperation())) { + if (getMatchingOperation() == null && StringUtils.isNotBlank(theRequest.getOperation())) { + return false; + } + if (getMatchingOperation() != null && !getMatchingOperation().equals(theRequest.getOperation())) { return false; } return true; } + /** + * For servers, this method will match only incoming requests + * that match the given operation, or which have no operation in the + * URL if this method returns null. + */ + protected abstract String getMatchingOperation(); + /** * Subclasses may override to allow a void method return type, which is allowable for some methods (e.g. delete) */ diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBindingWithResourceParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBindingWithResourceParam.java index 8c0ec871762..00b8e0dd241 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBindingWithResourceParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBindingWithResourceParam.java @@ -41,7 +41,6 @@ import ca.uhn.fhir.rest.server.EncodingUtil; import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; abstract class BaseOutcomeReturningMethodBindingWithResourceParam extends BaseOutcomeReturningMethodBinding { private int myResourceParameterIndex; @@ -63,8 +62,7 @@ abstract class BaseOutcomeReturningMethodBindingWithResourceParam extends BaseOu } if (resourceParameter == null) { - throw new ConfigurationException("Method " + theMethod.getName() + " in type " + theMethod.getDeclaringClass().getCanonicalName() + " does not have a parameter annotated with @" - + ResourceParam.class.getSimpleName()); + throw new ConfigurationException("Method " + theMethod.getName() + " in type " + theMethod.getDeclaringClass().getCanonicalName() + " does not have a parameter annotated with @" + ResourceParam.class.getSimpleName()); } } @@ -129,14 +127,20 @@ abstract class BaseOutcomeReturningMethodBindingWithResourceParam extends BaseOu theServer.addHapiHeader(theResponse); - theResponse.setContentType(Constants.CT_TEXT); - - Writer writer = theResponse.getWriter(); - try { - writer.append("Resource has been created"); - } finally { + if (response != null && response.getOperationOutcome() != null) { + theResponse.setContentType(encoding.getResourceContentType()); + Writer writer = theResponse.getWriter(); + try { + parser.encodeResourceToWriter(response.getOperationOutcome(), writer); + } finally { + writer.close(); + } + } else { + theResponse.setContentType(Constants.CT_TEXT); + Writer writer = theResponse.getWriter(); writer.close(); } + // getMethod().in } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/CreateMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/CreateMethodBinding.java index 3798136f894..75f01aa4a10 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/CreateMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/CreateMethodBinding.java @@ -64,4 +64,9 @@ public class CreateMethodBinding extends BaseOutcomeReturningMethodBindingWithRe return new PostClientInvocation(getContext(), resource, urlExtension.toString()); } + @Override + protected String getMatchingOperation() { + return null; + } + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/DeleteMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/DeleteMethodBinding.java index 4af8c63646b..10153c4f6ad 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/DeleteMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/DeleteMethodBinding.java @@ -143,4 +143,11 @@ public class DeleteMethodBinding extends BaseOutcomeReturningMethodBinding { theParams[myIdParameterIndex] = new IdDt(id); } + + @Override + protected String getMatchingOperation() { + return null; + } + + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SearchMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SearchMethodBinding.java index 3c417e562df..d3029655954 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SearchMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SearchMethodBinding.java @@ -20,7 +20,6 @@ package ca.uhn.fhir.rest.method; * #L% */ -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Collections; import java.util.HashSet; @@ -36,11 +35,10 @@ import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.dstu.valueset.RestfulOperationSystemEnum; import ca.uhn.fhir.model.dstu.valueset.RestfulOperationTypeEnum; import ca.uhn.fhir.rest.client.GetClientInvocation; -import ca.uhn.fhir.rest.param.IParameter; import ca.uhn.fhir.rest.param.BaseQueryParameter; +import ca.uhn.fhir.rest.param.IParameter; import ca.uhn.fhir.rest.param.ParameterUtil; import ca.uhn.fhir.rest.server.Constants; -import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; @@ -134,6 +132,10 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding { ourLog.trace("Method {} doesn't match because request type is POST but operation is not _search: {}", theRequest.getId(), theRequest.getOperation()); return false; } + if (theRequest.getRequestType() != RequestType.GET && theRequest.getRequestType() != RequestType.POST) { + ourLog.trace("Method {} doesn't match because request type is {}", theRequest.getOperation()); + return false; + } Set methodParamsTemp = new HashSet(); for (int i = 0; i < this.myParameters.size(); i++) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/UpdateMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/UpdateMethodBinding.java index cfc42c9d1c5..b75c26ae676 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/UpdateMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/UpdateMethodBinding.java @@ -20,7 +20,7 @@ package ca.uhn.fhir.rest.method; * #L% */ -import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static org.apache.commons.lang3.StringUtils.*; import java.lang.reflect.Method; import java.util.Collections; @@ -41,7 +41,6 @@ import ca.uhn.fhir.rest.client.BaseClientInvocation; import ca.uhn.fhir.rest.client.PutClientInvocation; import ca.uhn.fhir.rest.method.SearchMethodBinding.RequestType; import ca.uhn.fhir.rest.server.Constants; -import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; class UpdateMethodBinding extends BaseOutcomeReturningMethodBindingWithResourceParam { @@ -136,4 +135,10 @@ class UpdateMethodBinding extends BaseOutcomeReturningMethodBindingWithResourceP return Collections.singleton(RequestType.PUT); } + + @Override + protected String getMatchingOperation() { + return null; + } + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ValidateMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ValidateMethodBinding.java new file mode 100644 index 00000000000..e905ae7193b --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ValidateMethodBinding.java @@ -0,0 +1,102 @@ +package ca.uhn.fhir.rest.method; + +/* + * #%L + * HAPI FHIR Library + * %% + * Copyright (C) 2014 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 java.lang.reflect.Method; +import java.util.Collections; +import java.util.Set; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.api.IResource; +import ca.uhn.fhir.model.dstu.valueset.RestfulOperationSystemEnum; +import ca.uhn.fhir.model.dstu.valueset.RestfulOperationTypeEnum; +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.rest.annotation.Validate; +import ca.uhn.fhir.rest.client.BaseClientInvocation; +import ca.uhn.fhir.rest.client.PutClientInvocation; +import ca.uhn.fhir.rest.method.SearchMethodBinding.RequestType; +import ca.uhn.fhir.rest.server.Constants; + +class ValidateMethodBinding extends BaseOutcomeReturningMethodBindingWithResourceParam { + + private Integer myIdParameterIndex; + + public ValidateMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) { + super(theMethod, theContext, Validate.class, theProvider); + + myIdParameterIndex = Util.findIdParameterIndex(theMethod); + } + + @Override + public RestfulOperationTypeEnum getResourceOperationType() { + return RestfulOperationTypeEnum.VALIDATE; + } + + @Override + public RestfulOperationSystemEnum getSystemOperationType() { + return null; + } + + @Override + protected void addParametersForServerRequest(Request theRequest, Object[] theParams) { + if (myIdParameterIndex != null) { + theParams[myIdParameterIndex] = theRequest.getId(); + } + } + + @Override + protected BaseClientInvocation createClientInvocation(Object[] theArgs, IResource resource, String resourceName) { + StringBuilder urlExtension = new StringBuilder(); + urlExtension.append(resourceName); + urlExtension.append(Constants.PARAM_VALIDATE); + + if (myIdParameterIndex != null) { + IdDt idDt = (IdDt) theArgs[myIdParameterIndex]; + if (idDt != null && idDt.isEmpty() == false) { + String id = idDt.getValue(); + urlExtension.append('/'); + urlExtension.append(id); + } + } + + PutClientInvocation retVal = new PutClientInvocation(getContext(), resource, urlExtension.toString()); + + return retVal; + } + + + @Override + protected boolean allowVoidReturnType() { + return true; + } + + @Override + protected Set provideAllowableRequestTypes() { + return Collections.singleton(RequestType.POST); + } + + + @Override + protected String getMatchingOperation() { + return Constants.PARAM_VALIDATE; + } + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java index 4fc2f8fd983..70e52d86a58 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java @@ -59,6 +59,7 @@ public class Constants { public static final String PARAM_SEARCH = "_search"; public static final String HEADER_LAST_MODIFIED = "Last-Modified"; public static final String HEADER_LAST_MODIFIED_LOWERCASE = HEADER_LAST_MODIFIED.toLowerCase(); + public static final String PARAM_VALIDATE = "_validate"; static { Map valToEncoding = new HashMap(); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/InvalidRequestException.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/InvalidRequestException.java index 56ddf0dcb65..92e7015a2bc 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/InvalidRequestException.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/InvalidRequestException.java @@ -20,6 +20,14 @@ package ca.uhn.fhir.rest.server.exceptions; * #L% */ +/** + * RESTful method exception which corresponds to the HTTP 400 Bad Request status. + * This status indicates that the client's message was invalid (e.g. not a valid FHIR Resource + * per the specifications), as opposed to the {@link InvalidRequestException} which indicates + * that data does not pass business rule validation on the server. + * + * @see UnprocessableEntityException Which should be used for business level validation failures + */ public class InvalidRequestException extends BaseServerResponseException { private static final long serialVersionUID = 1L; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/UnprocessableEntityException.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/UnprocessableEntityException.java index 5a278ac3941..ee76f7169c7 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/UnprocessableEntityException.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/UnprocessableEntityException.java @@ -28,6 +28,8 @@ import ca.uhn.fhir.model.dstu.resource.OperationOutcome; *

* This exception will generally contain an {@link OperationOutcome} instance which details the failure. *

+ * + * @see InvalidRequestException Which corresponds to an HTTP 400 Bad Request failure */ public class UnprocessableEntityException extends BaseServerResponseException { diff --git a/hapi-fhir-base/src/site/example/java/example/RestfulPatientResourceProviderMore.java b/hapi-fhir-base/src/site/example/java/example/RestfulPatientResourceProviderMore.java index 854a807af2f..fc5bf590148 100644 --- a/hapi-fhir-base/src/site/example/java/example/RestfulPatientResourceProviderMore.java +++ b/hapi-fhir-base/src/site/example/java/example/RestfulPatientResourceProviderMore.java @@ -23,6 +23,7 @@ import ca.uhn.fhir.model.dstu.resource.Observation; import ca.uhn.fhir.model.dstu.resource.OperationOutcome; import ca.uhn.fhir.model.dstu.resource.Organization; import ca.uhn.fhir.model.dstu.resource.Patient; +import ca.uhn.fhir.model.dstu.valueset.IssueSeverityEnum; import ca.uhn.fhir.model.dstu.valueset.QuantityCompararatorEnum; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.StringDt; @@ -38,6 +39,7 @@ import ca.uhn.fhir.rest.annotation.RequiredParam; import ca.uhn.fhir.rest.annotation.ResourceParam; import ca.uhn.fhir.rest.annotation.Search; import ca.uhn.fhir.rest.annotation.Update; +import ca.uhn.fhir.rest.annotation.Validate; import ca.uhn.fhir.rest.annotation.VersionIdParam; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.client.ITestClient; @@ -307,6 +309,7 @@ public MethodOutcome createPatient(@ResourceParam Patient thePatient) { } //END SNIPPET: create + //START SNIPPET: createClient @Create public abstract MethodOutcome createNewPatient(@ResourceParam Patient thePatient); @@ -369,6 +372,39 @@ return retVal; +//START SNIPPET: validate +@Validate +public MethodOutcome validatePatient(@ResourceParam Patient thePatient) { + + /* + * Actually do our validation: The UnprocessableEntityException + * results in an HTTP 422, which is appropriate for business rule failure + */ + if (thePatient.getIdentifierFirstRep().isEmpty()) { + /* It is also possible to pass an OperationOutcome resource + * to the UnprocessableEntityException if you want to return + * a custom populated OperationOutcome. Otherwise, a simple one + * is created using the string supplied below. + */ + throw new UnprocessableEntityException("No identifier supplied"); + } + + // This method returns a MethodOutcome object + MethodOutcome retVal = new MethodOutcome(); + + // You may also add an OperationOutcome resource to return + // This part is optional though: + OperationOutcome outcome = new OperationOutcome(); + outcome.addIssue().setSeverity(IssueSeverityEnum.WARNING).setDetails("One minor issue detected"); + retVal.setOperationOutcome(outcome); + + return retVal; +} +//END SNIPPET: validate + + + + public static void main(String[] args) throws DataFormatException, IOException { //nothing } diff --git a/hapi-fhir-base/src/site/xdoc/doc_rest_operations.xml b/hapi-fhir-base/src/site/xdoc/doc_rest_operations.xml index 3f8a92dc99b..7e003ca0da7 100644 --- a/hapi-fhir-base/src/site/xdoc/doc_rest_operations.xml +++ b/hapi-fhir-base/src/site/xdoc/doc_rest_operations.xml @@ -9,26 +9,28 @@
- +

- RESTful Clients and Servers both share the same + RESTful Clients and Servers both share the same method pattern, with one key difference: A client is defined using annotated methods on an interface which are used to retrieve resources, - whereas a server requires concrete method implementations + whereas a server requires concrete method + implementations to actually provide those resources.

- +

- Unless otherwise specified, the examples below show server + Unless otherwise specified, the examples below show + server implementations, but client methods will follow the same patterns.

- +

- The following table lists the operations supported by + The following table lists the operations supported by HAPI FHIR RESTful Servers and Clients.

- + @@ -39,7 +41,7 @@ - +
- Instance - Read + Instance - Read Read the current state of the resource @@ -47,7 +49,7 @@
- Instance - VRead + Instance - VRead Read the state of a specific version of the resource @@ -55,7 +57,7 @@
- Instance - Update + Instance - Update Read the state of a specific version of the resource @@ -63,7 +65,7 @@
- Instance - Delete + Instance - Delete Delete a resource @@ -71,7 +73,7 @@
- Instance - History + Instance - History Retrieve the update history for a particular resource @@ -79,7 +81,7 @@
- Type - Create + Type - Create Create a new resource with a server assigned id @@ -87,10 +89,10 @@
- Type - Search + Type - Search - - + + @@ -99,7 +101,7 @@
- Type - Search + Type - Search Search the resource type based on some filter criteria @@ -107,7 +109,7 @@
- Type - History + Type - History Retrieve the update history for a particular resource type @@ -115,7 +117,7 @@
- Type - Validate + Type - Validate Check that the content would be acceptable as an update @@ -123,7 +125,7 @@
- System - Conformance + System - Conformance Get a conformance statement for the system @@ -131,7 +133,7 @@
- System - Transaction + System - Transaction Update, create or delete a set of resources as a single transaction @@ -139,7 +141,7 @@
- System - History + System - History Retrieve the update history for all resources @@ -147,31 +149,33 @@
- System - Search + System - Search Search across all resource types based on some filter criteria
- - + +
- +
- +

- The - read + The + + read + operation retrieves a resource by ID. It is annotated with the @Read - annotation, and has a single parameter annotated with the + annotation, and has a single parameter annotated with the @IdParam annotation.

@@ -182,11 +186,12 @@

- Example URL to invoke this method:
+ Example URL to invoke this method: +
http://fhir.example.com/Patient/111

- - + +
@@ -194,13 +199,15 @@
- +

- The - vread + The + + vread + operation retrieves a specific version of a resource with a given ID. It looks exactly like a "read" operation, but with a second - parameter annotated with the + parameter annotated with the @VersionIdParam annotation.

@@ -209,49 +216,53 @@ - +

- Example URL to invoke this method:
+ Example URL to invoke this method: +
http://fhir.example.com/Patient/111/_history/2

- - + +
- +
- +

- The - update + The + + update + operation updates a specific resource instance (using its ID), and optionally - accepts a version ID as well (which can be used to detect version conflicts). + accepts a version ID as well (which can be used to detect version conflicts).

- Update methods must be annotated with the + Update methods must be annotated with the @Update - annotation, and have a parameter annotated with the + annotation, and have a parameter annotated with the @Resource annotation. This parameter contains the resource instance to be created.

- In addition, the method must have a parameter annotated with the + In addition, the method must have a parameter annotated with the @IdParam annotation, and optionally may have a parameter annotated with the @VersionIdParam

Update methods must return an object of type - MethodOutcome. This + MethodOutcome + . This object contains the identity of the created resource.

The following snippet shows how to define an update method on a server:

- + @@ -259,44 +270,47 @@

Example URL to invoke this method (this would be invoked using an HTTP PUT, - with the resource in the PUT body):
+ with the resource in the PUT body): +
http://fhir.example.com/Patient

The following snippet shows how the corresponding client interface - would look: + would look:

- +

- In the case of a server which is able to do version conflict checking, an + In the case of a server which is able to do version conflict checking, an extra parameter would be added:

- + - - -
- + + + + - +
- +

- The - delete + The + + delete + operation retrieves a specific version of a resource with a given ID. It takes a single - ID parameter annotated with an + ID parameter annotated with an @IdParam annotation, which supplies the ID of the resource to delete.

@@ -305,61 +319,73 @@ - +

Delete methods are allowed to return the following types:

  • - void: This method may return void, in which case - the server will return an empty response and the client will ignore + void + : This method may return + void + , in which case + the server will return an empty response and the client will ignore any successful response from the server (failure responses will still throw - an exception) + an exception)
  • - MethodOutcome: - This method may return MethodOutcome, + + MethodOutcome + + : + This method may return + MethodOutcome + , which is a wrapper for the FHIR OperationOutcome resource, which may optionally be returned by the server according to the FHIR specification.
- +

- Example URL to invoke this method (HTTP DELETE):
+ Example URL to invoke this method (HTTP DELETE): +
http://fhir.example.com/Patient/111

- - + +
- +
- +

- The - create - operation saves a new resource to the server, allowing the server to - give that resource an ID and version ID. + The + + create + + operation saves a new resource to the server, allowing the server to + give that resource an ID and version ID.

- Create methods must be annotated with the + Create methods must be annotated with the @Create - annotation, and have a single parameter annotated with the + annotation, and have a single parameter annotated with the @Resource annotation. This parameter contains the resource instance to be created.

Create methods must return an object of type - MethodOutcome. This + MethodOutcome + . This object contains the identity of the created resource.

The following snippet shows how to define a server create method:

- + @@ -367,78 +393,87 @@

Example URL to invoke this method (this would be invoked using an HTTP POST, - with the resource in the POST body):
+ with the resource in the POST body): +
http://fhir.example.com/Patient

The following snippet shows how the corresponding client interface - would look: + would look:

- + - - + +
- + - +
- +

The - search operation returns a bundle - with zero-to-many resources of a given type, matching a given set of parameters. + + search + + operation returns a bundle + with zero-to-many resources of a given type, matching a given set of parameters.

- + - +

The following example shows a search with no parameters. This operation should return all resources of a given type (this obviously doesn't make - sense in all contexts, but does for some resource types). + sense in all contexts, but + does for some resource types).

- + - +

- Example URL to invoke this method:
+ Example URL to invoke this method: +
http://fhir.example.com/Patient

- +

To allow a search using given search parameters, add one or more parameters - to your search method and tag these parameters as either + to your search method and tag these parameters as either @RequiredParam or - @OptionalParam. + @OptionalParam + .

- +

- This annotation takes a "name" parameter which specifies the parameter's + This annotation takes a "name" parameter which specifies the parameter's name (as it will appear in the search URL). FHIR defines standardized parameter names for each resource, and these are available as constants on the - individual HAPI resource classes. + individual HAPI resource + classes.

- + - +

- Example URL to invoke this method:
+ Example URL to invoke this method: +
http://fhir.example.com/Patient?family=SMITH

@@ -451,47 +486,55 @@ interface. For example, the search below can be used to search by identifier (e.g. search for an MRN).

- + - +

- Example URL to invoke this method:
+ Example URL to invoke this method: +
http://fhir.example.com/Patient?identifier=urn:foo|7000135

- +

Search methods may take multiple parameters, and these parameters may be optional. To add a second required parameter, annotate the - parameter with - @RequiredParam. - To add an optional parameter (which will be passed in as null if no value + parameter with + @RequiredParam + . + To add an optional parameter (which will be passed in as + null + if no value is supplied), annotate the method with - @OptionalParam. + @OptionalParam + .

- +

You may annotate a method with any combination of as many @RequiredParam and as many @OptionalParam - parameters as you want. It is valid to have only @RequiredParam parameters, or + parameters as you want. It is valid to have only @RequiredParam + parameters, or only @OptionalParam parameters, or any combination of the two.

- + - +

- Example URLs to invoke this method:
- http://fhir.example.com/Patient?family=SMITH
+ Example URLs to invoke this method: +
+ http://fhir.example.com/Patient?family=SMITH +
http://fhir.example.com/Patient?family=SMITH&given=JOHN

- +
@@ -499,135 +542,157 @@

It is possible to accept multiple values of a single parameter as well. This is useful in cases when you want to return a list - of resources with criteria matching a list of possible values. - See the FHIR Specification + of resources with criteria matching a list of + possible values. + See the + FHIR Specification for more information.

- +

The FHIR specification allows two types of composite parameters:

  • - Where a parameter may accept multiple comma separated values within a single value string - (e.g. ?language=FR,NL) this is treated as an OR relationship, and - the search should return elements matching either one or the other. + Where a parameter may accept multiple comma separated values within a single value string + (e.g. + ?language=FR,NL + ) this is treated as an + OR + relationship, and + the search should return elements matching either one or the other.
  • - Where a parameter may accept multiple value strings for the same parameter name - (e.g. ?language=FR&language=NL) this is treated as an AND relationship, - and the search should return only elements matching both. + Where a parameter may accept multiple value strings for the same parameter name + (e.g. + ?language=FR&language=NL + ) this is treated as an + AND + relationship, + and the search should return only elements matching both.
  • -
- + +

OR Relationship Query Parameters

- +

To accept a composite parameter, use a parameter type which implements the IQueryParameterOr interface. For example, to accept searches for Observations matching any of a collection of names:

- + - +

- Example URL to invoke this method:
+ Example URL to invoke this method: +
http://fhir.example.com/Observation?name=urn:fakenames|123,urn:fakenames|456

- +

AND Relationship Query Parameters

- +

To accept a composite parameter, use a parameter type which implements the IQueryParameterAnd interface.

- +

- See the section below on date ranges for + See the section below on + date ranges + for one example of an AND parameter.

- +
- +

The FHIR specification provides a sytax for specifying - dates (and date/times as well, but for simplicity we will just say dates here) + dates (and date/times as well, but for simplicity we will just say dates here) as search criteria.

- Dates may be optionally prefixed with a qualifier. For example, the - string >=2011-01-02 means any date on or after 2011-01-02. -

- + Dates may be optionally prefixed with a qualifier. For example, the + string + >=2011-01-02 + means any date on or after 2011-01-02. +

+

To accept a qualified date parameter, use the QualifiedDateParam - parameter type. + parameter type.

- + - +

- Example URL to invoke this method:
+ Example URL to invoke this method: +
http://fhir.example.com/Observation?birthdate=>=2011-01-02

- +

Invoking a client of thie type involves the following syntax:

- +
- - + +

- A common scenario in searches is to allow searching for resources - with values (i.e timestamps) within a range of dates. + A common scenario in searches is to allow searching for resources + with values (i.e timestamps) within a range of dates.

FHIR allows for multiple parameters with the same key, and interprets - these as being an "AND" set. So, for example, a range of
- DATE >= 2011-01-01 and DATE < 2011-02-01
+ these as being an "AND" set. So, for example, a range of +
+ DATE >= 2011-01-01 + and + DATE < 2011-02-01 +
can be interpreted as any date within January 2011.

The following snippet shows how to accept such a range, and combines it with a specific identifier, which is a common scenario. (i.e. Give me a list - of observations for a specific patient within a given date range) -

+ of observations for a + specific patient within a given date range) +

- +

- Example URL to invoke this method:
+ Example URL to invoke this method: +
http://fhir.example.com/Observation?subject.identifier=7000135&date=>=2011-01-01&date=<2011-02-01

- +

Invoking a client of this type involves the following syntax:

- + @@ -636,13 +701,14 @@
- +

FHIR allows clients to request that specific linked resources be included as contained resources, which means that they will be "embedded" in a special - container called "contained" within the parent resource. + container called + "contained" within the parent resource.

- +

HAPI allows you to add a parameter for accepting includes if you wish to support them for specific search methods. @@ -652,79 +718,141 @@ - +

- Example URL to invoke this method:
+ Example URL to invoke this method: +
http://fhir.example.com/DiagnosticReport?subject.identifier=7000135&_include=DiagnosticReport.subject

- +
- +

- FHIR supports named queries, + FHIR supports + named queries + , which may have specific behaviour defined. The following example shows how to create a Search - operation with a name. + operation with a name.

- +

This operation can only be invoked by explicitly specifying the given query name - in the request URL. Note that the query does not need to take any parameters. + in the request URL. Note that the query does not need to take any parameters.

- +

- Example URL to invoke this method:
+ Example URL to invoke this method: +
http://fhir.example.com/Patient?_query=namedQuery1&someparam=value

- + - +
- +
- +

- Not yet implemented + The + + validate + + tests whether a resource passes business validation, and would be + acceptable for saving to a server (e.g. by a create or update method). +

+

+ Validate methods must be annotated with the + @Validate + annotation, and have a parameter annotated with the + @Resource + annotation. This parameter contains the resource instance to be created. +

+

+ Validate methods may optionally also have a parameter + oftype IdDt annotated with the + @IdParam + annotation. This parameter contains the resource ID (see the + FHIR specification + for details on how this is used) +

+

+ Validate methods must return normally if the resource validates successfully, + or throw an + UnprocessableEntityException + or + InvalidRequestException + if the validation fails. +

+

+ Validate methods must return either: +

+
    +
  • + void + - The method should throw an exception for a validation failure, or return normally. +
  • +
  • + An object of type + MethodOutcome. The + MethodOutcome may optionally be populated with an OperationOutcome resource, which + will be returned to the client if it exists. +
  • +
+

+ The following snippet shows how to define a server validate method: +

+ + + + + + +

+ Example URL to invoke this method (this would be invoked using an HTTP POST, + with the resource in the POST body): +
+ http://fhir.example.com/Patient/_validate

- +
- +
- +

FHIR defines that a FHIR Server must be able to export a conformance statement, which is an instance of the Conformance resource describing the server itself.

- +

The HAPI FHIR RESTful server will automatically export such a conformance statement. See the RESTful Server documentation for more information.

- +

If you wish to override this default behaviour by creating your own metadata provider, you simply need to define a class - with a method annotated using the + with a method annotated using the @Metadata annotation.

@@ -732,11 +860,11 @@ - +

- To create a Client which can retrieve a Server's conformance + To create a Client which can retrieve a Server's conformance statement is simple. First, define your Client Interface, using - the @Metadata annotation: + the @Metadata annotation:

@@ -745,74 +873,88 @@

Then use the standard - RESTful Client mechanism for instantiating + RESTful Client + mechanism for instantiating a client:

- - + +
- + - +
- +

Not yet implemented

- -
+ +
- + - +
- +

Not yet implemented

- -
+ +
- + - +
- +

- The - history - operation retrieves a historical collection of all versions of a single resource - (instance history), all resources of a given type (type history), - or all resources of any type on a server (server history). + The + + history + + operation retrieves a historical collection of all versions of a single resource + (instance history) + , all resources of a given type + (type history) + , + or all resources of any type on a server + (server history) + .

- History methods must be annotated with the + History methods must be annotated with the @History annotation, and will have additional requirements depending on the kind of history method intended:

  • - For an Instance History method, the method must have a parameter - annotated with the + For an + Instance History + method, the method must have a parameter + annotated with the @IdParam - annotation, indicating the ID of the resource for which to return history. + annotation, indicating the ID of the resource for which to return history.
  • - For an Type History method, the method must not have any @IdParam parameter. + For an + Type History + method, the method must not have any @IdParam parameter.
  • - For an Server History method, the method must not have any @IdParam parameter + For an + Server History + method, the method must not have any @IdParam parameter and must not be found in a ResourceProvider definition.
  • @@ -820,7 +962,7 @@

    The following snippet shows how to define a history method on a server:

    - + @@ -829,14 +971,14 @@

    The following snippet shows how to define various history methods in a client.

    - + - +
- + diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/ResfulServerMethodTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/ResfulServerMethodTest.java index ee650991cf1..752119a411f 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/ResfulServerMethodTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/ResfulServerMethodTest.java @@ -69,6 +69,7 @@ import ca.uhn.fhir.rest.annotation.RequiredParam; import ca.uhn.fhir.rest.annotation.ResourceParam; import ca.uhn.fhir.rest.annotation.Search; import ca.uhn.fhir.rest.annotation.Update; +import ca.uhn.fhir.rest.annotation.Validate; import ca.uhn.fhir.rest.annotation.VersionIdParam; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.param.CodingListParam; @@ -854,7 +855,60 @@ public class ResfulServerMethodTest { assertEquals("http://localhost:" + ourPort + "/Patient/001/_history/002", status.getFirstHeader("Location").getValue()); } + + @Test + public void testValidate() throws Exception { + Patient patient = new Patient(); + patient.addName().addFamily("FOO"); + + HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/_validate"); + httpPost.setEntity(new StringEntity(new FhirContext().newXmlParser().encodeResourceToString(patient), ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); + + HttpResponse status = ourClient.execute(httpPost); + + String responseContent = IOUtils.toString(status.getEntity().getContent()); + ourLog.info("Response was:\n{}", responseContent); + + assertEquals(200, status.getStatusLine().getStatusCode()); + OperationOutcome oo = new FhirContext().newXmlParser().parseResource(OperationOutcome.class, responseContent); + assertEquals("it passed", oo.getIssueFirstRep().getDetails().getValue()); + + // Now should fail + + patient = new Patient(); + patient.addName().addFamily("BAR"); + + httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/_validate"); + httpPost.setEntity(new StringEntity(new FhirContext().newXmlParser().encodeResourceToString(patient), ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); + + status = ourClient.execute(httpPost); + + responseContent = IOUtils.toString(status.getEntity().getContent()); + ourLog.info("Response was:\n{}", responseContent); + + assertEquals(422, status.getStatusLine().getStatusCode()); + oo = new FhirContext().newXmlParser().parseResource(OperationOutcome.class, responseContent); + assertEquals("it failed", oo.getIssueFirstRep().getDetails().getValue()); + + // Should fail with outcome + + patient = new Patient(); + patient.addName().addFamily("BAZ"); + + httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/_validate"); + httpPost.setEntity(new StringEntity(new FhirContext().newXmlParser().encodeResourceToString(patient), ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); + + status = ourClient.execute(httpPost); + + responseContent = IOUtils.toString(status.getEntity().getContent()); + ourLog.info("Response was:\n{}", responseContent); + + assertEquals(200, status.getStatusLine().getStatusCode()); + assertEquals("", responseContent); + + } + @Test public void testUpdateWithVersion() throws Exception { @@ -973,6 +1027,22 @@ public class ResfulServerMethodTest { return new MethodOutcome(true, id, version); } + @Validate() + public MethodOutcome validatePatient(@ResourceParam Patient thePatient) { + if (thePatient.getNameFirstRep().getFamilyFirstRep().getValueNotNull().equals("FOO")) { + MethodOutcome methodOutcome = new MethodOutcome(); + OperationOutcome oo = new OperationOutcome(); + oo.addIssue().setDetails("it passed"); + methodOutcome.setOperationOutcome(oo); + return methodOutcome; + } + if (thePatient.getNameFirstRep().getFamilyFirstRep().getValueNotNull().equals("BAR")) { + throw new UnprocessableEntityException("it failed"); + } + return new MethodOutcome(); + } + + private Patient createPatient1() { Patient patient = new Patient(); patient.addIdentifier(); diff --git a/hapi-fhir-structures-dstu/pom.xml b/hapi-fhir-structures-dstu/pom.xml index 76ae9c62592..e5375fbd32c 100644 --- a/hapi-fhir-structures-dstu/pom.xml +++ b/hapi-fhir-structures-dstu/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 0.3 + 0.3-SNAPSHOT ../pom.xml @@ -19,7 +19,7 @@ ca.uhn.hapi.fhir hapi-fhir-base - 0.3 + 0.3-SNAPSHOT @@ -28,7 +28,7 @@ ca.uhn.hapi.fhir hapi-tinder-plugin - 0.3 + 0.3-SNAPSHOT diff --git a/hapi-fhir-structures-dstu/pom.xml.versionsBackup b/hapi-fhir-structures-dstu/pom.xml.versionsBackup new file mode 100644 index 00000000000..76ae9c62592 --- /dev/null +++ b/hapi-fhir-structures-dstu/pom.xml.versionsBackup @@ -0,0 +1,146 @@ + + 4.0.0 + + + ca.uhn.hapi.fhir + hapi-fhir + 0.3 + ../pom.xml + + + ca.uhn.hapi.fhir + hapi-fhir-structures-dstu + jar + + HAPI FHIR Structures - DSTU (FHIR 0.80) + + + + ca.uhn.hapi.fhir + hapi-fhir-base + 0.3 + + + + + + + ca.uhn.hapi.fhir + hapi-tinder-plugin + 0.3 + + + + generate-structures + + + + + ca.uhn.fhir.model.dstu + + + + adversereaction + alert + allergyintolerance + appointmentresponse + appointment + availability + careplan + claim + composition + conceptmap + condition + conformance + coverage + deviceobservationreport + device + diagnosticorder + diagnosticreport + documentmanifest + documentreference + encounter + + familyhistory + geneexpression + geneticanalysis + group + gvfmeta + gvfvariant + imagingstudy + immunizationrecommendation + immunization + list + location + media + medicationadministration + medicationdispense + medicationprescription + medication + medicationstatement + messageheader + microarray + + observation + operationoutcome + orderresponse + order + organization + other + patient + + practitioner + procedure + profile + + + provenance + query + + questionnaire + relatedperson + remittance + + securityevent + + sequencinganalysis + sequencinglab + slot + specimen + substance + supply + + test + user + + valueset + + + + true + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + ${maven_javadoc_plugin_version} + + + + + + + diff --git a/hapi-tinder-plugin/pom.xml b/hapi-tinder-plugin/pom.xml index 4ce15d4bf39..0b877553187 100644 --- a/hapi-tinder-plugin/pom.xml +++ b/hapi-tinder-plugin/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 0.3 + 0.3-SNAPSHOT ../pom.xml @@ -20,7 +20,7 @@ ca.uhn.hapi.fhir hapi-fhir-base - 0.3 + 0.3-SNAPSHOT diff --git a/hapi-tinder-test/pom.xml b/hapi-tinder-test/pom.xml index 133ccc6c102..1edd5213080 100644 --- a/hapi-tinder-test/pom.xml +++ b/hapi-tinder-test/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 0.3 + 0.3-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index f5a3ef50ad3..85763654f8a 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ ca.uhn.hapi.fhir hapi-fhir pom - 0.3 + 0.3-SNAPSHOT HAPI http://hl7api.sourceforge.net/hapi-fhir/