Complete Extended Operations support

This commit is contained in:
jamesagnew 2015-03-08 15:40:04 -04:00
parent 06ea9a1453
commit 32ad3ab22c
49 changed files with 1617 additions and 478 deletions

View File

@ -3,25 +3,20 @@ package example;
import java.util.ArrayList;
import java.util.List;
import org.omg.Dynamic.Parameter;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.base.resource.BaseConformance;
import ca.uhn.fhir.model.base.resource.BaseOperationOutcome;
import ca.uhn.fhir.model.dstu2.valueset.AdministrativeGenderEnum;
import ca.uhn.fhir.model.dstu2.resource.Observation;
import ca.uhn.fhir.model.dstu2.resource.OperationOutcome;
import ca.uhn.fhir.model.dstu2.resource.Organization;
import ca.uhn.fhir.model.dstu2.resource.Parameters;
import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.dstu2.resource.ValueSet;
import ca.uhn.fhir.model.primitive.CodeDt;
import ca.uhn.fhir.model.dstu2.valueset.AdministrativeGenderEnum;
import ca.uhn.fhir.model.primitive.DateDt;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.model.primitive.UriDt;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.client.IGenericClient;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
@ -378,10 +373,12 @@ public class GenericClientExample {
IGenericClient client = ctx.newRestfulGenericClient("http://fhir-dev.healthintersections.com.au/open");
client.registerInterceptor(new LoggingInterceptor(true));
// Create the input parameters to pass to the server
Parameters inParams = new Parameters();
inParams.addParameter().setName("start").setValue(new DateDt("2001-01-01"));
inParams.addParameter().setName("end").setValue(new DateDt("2015-03-01"));
// Invoke $everything on "Patient/1"
Parameters outParams = client
.operation()
.onInstance(new IdDt("Patient", "1"))

View File

@ -34,7 +34,7 @@ import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.annotation.AddTags;
import ca.uhn.fhir.rest.annotation.ConditionalOperationParam;
import ca.uhn.fhir.rest.annotation.ConditionalUrlParam;
import ca.uhn.fhir.rest.annotation.Count;
import ca.uhn.fhir.rest.annotation.Create;
import ca.uhn.fhir.rest.annotation.DeleteTags;
@ -327,7 +327,7 @@ public void deletePatient(@IdParam IdDt theId) {
//START SNIPPET: deleteConditional
@Read()
public void deletePatientConditional(@IdParam IdDt theId, @ConditionalOperationParam String theConditionalUrl) {
public void deletePatientConditional(@IdParam IdDt theId, @ConditionalUrlParam String theConditionalUrl) {
// Only one of theId or theConditionalUrl will have a value depending
// on whether the URL receieved was a logical ID, or a conditional
// search string
@ -698,7 +698,7 @@ public MethodOutcome createPatient(@ResourceParam Patient thePatient) {
@Create
public MethodOutcome createPatientConditional(
@ResourceParam Patient thePatient,
@ConditionalOperationParam String theConditionalUrl) {
@ConditionalUrlParam String theConditionalUrl) {
if (theConditionalUrl != null) {
// We are doing a conditional create
@ -727,7 +727,7 @@ public abstract MethodOutcome createNewPatient(@ResourceParam Patient thePatient
public MethodOutcome updatePatientConditional(
@ResourceParam Patient thePatient,
@IdParam IdDt theId,
@ConditionalOperationParam String theConditional) {
@ConditionalUrlParam String theConditional) {
// Only one of theId or theConditional will have a value and the other will be null,
// depending on the URL passed into the server.

View File

@ -0,0 +1,56 @@
package example;
import java.util.List;
import ca.uhn.fhir.model.dstu2.composite.CodingDt;
import ca.uhn.fhir.model.dstu2.resource.Bundle;
import ca.uhn.fhir.model.dstu2.resource.ConceptMap;
import ca.uhn.fhir.model.primitive.DateDt;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
public class ServerOperations {
//START SNIPPET: patientTypeOperation
@Operation(name="$everything")
public Bundle patientTypeOperation(
@OperationParam(name="start") DateDt theStart,
@OperationParam(name="end") DateDt theEnd) {
Bundle retVal = new Bundle();
// Populate bundle with matching resources
return retVal;
}
//END SNIPPET: patientTypeOperation
//START SNIPPET: patientInstanceOperation
@Operation(name="$everything")
public Bundle patientInstanceOperation(
@IdParam IdDt thePatientId,
@OperationParam(name="start") DateDt theStart,
@OperationParam(name="end") DateDt theEnd) {
Bundle retVal = new Bundle();
// Populate bundle with matching resources
return retVal;
}
//END SNIPPET: patientInstanceOperation
//START SNIPPET: serverOperation
@Operation(name="$closure")
public ConceptMap closureOperation(
@OperationParam(name="name") StringDt theStart,
@OperationParam(name="concept") List<CodingDt> theEnd,
@OperationParam(name="version") IdDt theVersion) {
ConceptMap retVal = new ConceptMap();
// Populate bundle with matching resources
return retVal;
}
//END SNIPPET: serverOperation
}

View File

@ -25,8 +25,15 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* On the {@link Update}, {@link Create} and {@link Delete} operation methods, this annotation
* can be used to mark a {@link String} parameter which will be populated with the
* conditional "search" URL for the operation, if an incoming client invocation is
* a conditional operation. For non-conditional invocations, the value will be set to
* <code>null</code> so it is important to handle <code>null</code>.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface ConditionalOperationParam {
public @interface ConditionalUrlParam {
// just a marker
}

View File

@ -25,13 +25,13 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.hl7.fhir.instance.model.IBaseResource;
/**
* RESTful method annotation used for a method which provides
* FHIR "operations".
* RESTful method annotation used for a method which provides FHIR "operations".
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(value=ElementType.METHOD)
@Target(value = ElementType.METHOD)
public @interface Operation {
/**
@ -39,4 +39,14 @@ public @interface Operation {
*/
String name();
/**
* On a client, this value should be populated with the resource type that the operation applies to. If set to
* {@link IBaseResource} (which is the default) than the operation applies to the server and not to a specific
* resource type.
* <p>
* This value should not be populated on server implementations.
* </p>
*/
Class<? extends IBaseResource> type() default IBaseResource.class;
}

View File

@ -0,0 +1,19 @@
package ca.uhn.fhir.rest.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(value=ElementType.PARAMETER)
public @interface OperationParam {
/**
* The name of the parameter
*/
String name();
}

View File

@ -153,7 +153,7 @@ abstract class BaseAddOrDeleteTagsMethodBinding extends BaseMethodBinding<Void>
}
for (int idx = 0; idx < theArgs.length; idx++) {
IParameter nextParam = getParameters().get(idx);
nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], null);
nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], null, null);
}
return retVal;

View File

@ -20,7 +20,7 @@ package ca.uhn.fhir.rest.method;
* #L%
*/
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.*;
import java.io.IOException;
import java.io.Reader;
@ -51,6 +51,7 @@ import ca.uhn.fhir.rest.annotation.DeleteTags;
import ca.uhn.fhir.rest.annotation.GetTags;
import ca.uhn.fhir.rest.annotation.History;
import ca.uhn.fhir.rest.annotation.Metadata;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.Read;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.annotation.Transaction;
@ -257,9 +258,10 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
AddTags addTags = theMethod.getAnnotation(AddTags.class);
DeleteTags deleteTags = theMethod.getAnnotation(DeleteTags.class);
Transaction transaction = theMethod.getAnnotation(Transaction.class);
Operation operation = theMethod.getAnnotation(Operation.class);
// ** if you add another annotation above, also add it to the next line:
if (!verifyMethodHasZeroOrOneOperationAnnotation(theMethod, read, search, conformance, create, update, delete, history, validate, getTags, addTags, deleteTags, transaction)) {
if (!verifyMethodHasZeroOrOneOperationAnnotation(theMethod, read, search, conformance, create, update, delete, history, validate, getTags, addTags, deleteTags, transaction, operation)) {
return null;
}
@ -343,8 +345,15 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
+ " according to annotation - Must return a resource type");
}
returnType = returnTypeFromAnnotation;
} else {
} else {
//if (IRestfulClient.class.isAssignableFrom(theMethod.getDeclaringClass())) {
// Clients don't define their methods in resource specific types, so they can
// infer their resource type from the method return type.
returnType = (Class<? extends IResource>) returnTypeFromMethod;
// } else {
// This is a plain provider method returning a resource, so it should be
// an operation or global search presumably
// returnType = null;
}
}
@ -377,6 +386,8 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
return new DeleteTagsMethodBinding(theMethod, theContext, theProvider, deleteTags);
} else if (transaction != null) {
return new TransactionMethodBinding(theMethod, theContext, theProvider);
} else if (operation != null) {
return new OperationMethodBinding(returnType, returnTypeFromRp, theMethod, theContext, theProvider, operation);
} else {
throw new ConfigurationException("Did not detect any FHIR annotations on method '" + theMethod.getName() + "' on type: " + theMethod.getDeclaringClass().getCanonicalName());
}

View File

@ -90,8 +90,7 @@ abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding<Obje
Class<?> collectionType = ReflectionUtil.getGenericCollectionTypeOfMethodReturnType(theMethod);
if (collectionType != null) {
if (!Object.class.equals(collectionType) && !IResource.class.isAssignableFrom(collectionType)) {
throw new ConfigurationException("Method " + theMethod.getDeclaringClass().getSimpleName() + "#" + theMethod.getName() + " returns an invalid collection generic type: "
+ collectionType);
throw new ConfigurationException("Method " + theMethod.getDeclaringClass().getSimpleName() + "#" + theMethod.getName() + " returns an invalid collection generic type: " + collectionType);
}
}
myResourceListCollectionType = collectionType;
@ -107,8 +106,7 @@ abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding<Obje
} else if (IBundleProvider.class.isAssignableFrom(methodReturnType)) {
myMethodReturnType = MethodReturnTypeEnum.BUNDLE_PROVIDER;
} else {
throw new ConfigurationException("Invalid return type '" + methodReturnType.getCanonicalName() + "' on method '" + theMethod.getName() + "' on type: "
+ theMethod.getDeclaringClass().getCanonicalName());
throw new ConfigurationException("Invalid return type '" + methodReturnType.getCanonicalName() + "' on method '" + theMethod.getName() + "' on type: " + theMethod.getDeclaringClass().getCanonicalName());
}
if (theReturnResourceType != null) {
@ -137,6 +135,11 @@ abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding<Obje
return myResourceName;
}
/**
* If the response is a bundle, this type will be placed in the root of the bundle (can be null)
*/
protected abstract BundleTypeEnum getResponseBundleType();
public abstract ReturnTypeEnum getReturnType();
@Override
@ -259,24 +262,22 @@ abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding<Obje
} else {
resource = (IResource) resultObj;
}
/*
* We assume that the bundle we got back from the handling method
* may not have everything populated (e.g. self links, bundle type,
* etc) so we do that here.
* We assume that the bundle we got back from the handling method may not have everything populated
* (e.g. self links, bundle type, etc) so we do that here.
*/
IVersionSpecificBundleFactory bundleFactory = theServer.getFhirContext().newBundleFactory();
bundleFactory.initializeWithBundleResource(resource);
bundleFactory.addRootPropertiesToBundle(null, theRequest.getFhirServerBase(), theRequest.getCompleteUrl(), count, getResponseBundleType());
RestfulServerUtils.streamResponseAsResource(theServer, response, resource, responseEncoding, prettyPrint, requestIsBrowser, narrativeMode, respondGzip, theRequest.getFhirServerBase());
break;
} else {
IBundleProvider result = (IBundleProvider) resultObj;
IVersionSpecificBundleFactory bundleFactory = theServer.getFhirContext().newBundleFactory();
bundleFactory.initializeBundleFromBundleProvider(theServer, result, responseEncoding, theRequest.getFhirServerBase(), theRequest.getCompleteUrl(), prettyPrint, 0, count, null,
getResponseBundleType());
bundleFactory.initializeBundleFromBundleProvider(theServer, result, responseEncoding, theRequest.getFhirServerBase(), theRequest.getCompleteUrl(), prettyPrint, 0, count, null, getResponseBundleType());
Bundle bundle = bundleFactory.getDstu1Bundle();
if (bundle != null) {
for (int i = theServer.getInterceptors().size() - 1; i >= 0; i--) {
@ -298,8 +299,7 @@ abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding<Obje
return;
}
}
RestfulServerUtils.streamResponseAsResource(theServer, response, (IResource) resBundle, responseEncoding, prettyPrint, requestIsBrowser, narrativeMode,
Constants.STATUS_HTTP_200_OK, theRequest.isRespondGzip(), theRequest.getFhirServerBase());
RestfulServerUtils.streamResponseAsResource(theServer, response, (IResource) resBundle, responseEncoding, prettyPrint, requestIsBrowser, narrativeMode, Constants.STATUS_HTTP_200_OK, theRequest.isRespondGzip(), theRequest.getFhirServerBase());
}
break;
@ -329,11 +329,6 @@ abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding<Obje
}
}
/**
* If the response is a bundle, this type will be placed in the root of the bundle (can be null)
*/
protected abstract BundleTypeEnum getResponseBundleType();
/**
* Subclasses may override
*
@ -346,8 +341,12 @@ abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding<Obje
return null;
}
protected void setResourceName(String theResourceName) {
myResourceName = theResourceName;
}
public enum MethodReturnTypeEnum {
BUNDLE, BUNDLE_PROVIDER, LIST_OF_RESOURCES, RESOURCE, BUNDLE_RESOURCE
BUNDLE, BUNDLE_PROVIDER, BUNDLE_RESOURCE, LIST_OF_RESOURCES, RESOURCE
}
public enum ReturnTypeEnum {

View File

@ -28,6 +28,7 @@ import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.IBaseResource;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.dstu.valueset.RestfulOperationTypeEnum;
@ -45,7 +46,7 @@ class ConditionalParamBinder implements IParameter {
}
@Override
public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map<String, List<String>> theTargetQueryArguments) throws InternalErrorException {
public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map<String, List<String>> theTargetQueryArguments, IBaseResource theTargetResource) throws InternalErrorException {
throw new UnsupportedOperationException();
}

View File

@ -61,7 +61,7 @@ public class ConformanceMethodBinding extends BaseResourceReturningMethodBinding
if (theArgs != null) {
for (int idx = 0; idx < theArgs.length; idx++) {
IParameter nextParam = getParameters().get(idx);
nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], null);
nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], null, null);
}
}

View File

@ -27,6 +27,7 @@ import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.IBaseResource;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
@ -43,7 +44,7 @@ public class CountParameter implements IParameter {
private Class<?> myType;
@Override
public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map<String, List<String>> theTargetQueryArguments) throws InternalErrorException {
public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map<String, List<String>> theTargetQueryArguments, IBaseResource theTargetResource) throws InternalErrorException {
if (theSourceClientArgument != null) {
IntegerDt since = ParameterUtil.toInteger(theSourceClientArgument);
if (since.isEmpty() == false) {

View File

@ -75,7 +75,7 @@ public class CreateMethodBinding extends BaseOutcomeReturningMethodBindingWithRe
if (theArgs != null) {
for (int idx = 0; idx < theArgs.length; idx++) {
IParameter nextParam = getParameters().get(idx);
nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], null);
nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], null, null);
}
}

View File

@ -134,7 +134,7 @@ public class DeleteMethodBinding extends BaseOutcomeReturningMethodBinding {
for (int idx = 0; idx < theArgs.length; idx++) {
IParameter nextParam = getParameters().get(idx);
nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], null);
nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], null, null);
}
return retVal;

View File

@ -26,6 +26,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.hl7.fhir.instance.model.IBaseResource;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.model.api.IQueryParameterType;
@ -58,7 +60,7 @@ public class DynamicSearchParameter implements IParameter {
}
@Override
public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map<String, List<String>> theTargetQueryArguments) throws InternalErrorException {
public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map<String, List<String>> theTargetQueryArguments, IBaseResource theTargetResource) throws InternalErrorException {
throw new UnsupportedOperationException("Dynamic search is not supported in client mode (use fluent client for dynamic-like searches)");
}

View File

@ -140,7 +140,7 @@ public class GetTagsMethodBinding extends BaseMethodBinding<TagList> {
if (theArgs != null) {
for (int idx = 0; idx < theArgs.length; idx++) {
IParameter nextParam = getParameters().get(idx);
nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], null);
nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], null, null);
}
}

View File

@ -157,7 +157,7 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
if (theArgs != null) {
for (int idx = 0; idx < theArgs.length; idx++) {
IParameter nextParam = getParameters().get(idx);
nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], retVal.getParameters());
nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], retVal.getParameters(), null);
}
}

View File

@ -25,6 +25,8 @@ import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.hl7.fhir.instance.model.IBaseResource;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.rest.server.IResourceProvider;
@ -33,7 +35,7 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
public interface IParameter {
void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map<String, List<String>> theTargetQueryArguments) throws InternalErrorException;
void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map<String, List<String>> theTargetQueryArguments, IBaseResource theTargetResource) throws InternalErrorException;
/**
* This <b>server method</b> method takes the data received by the server in an incoming request, and translates that data into a single argument for a server method invocation. Note that all

View File

@ -48,10 +48,11 @@ import ca.uhn.fhir.model.dstu.valueset.RestfulOperationTypeEnum;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.annotation.ConditionalOperationParam;
import ca.uhn.fhir.rest.annotation.ConditionalUrlParam;
import ca.uhn.fhir.rest.annotation.Count;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.IncludeParam;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.annotation.OptionalParam;
import ca.uhn.fhir.rest.annotation.RequiredParam;
import ca.uhn.fhir.rest.annotation.ResourceParam;
@ -292,7 +293,7 @@ public class MethodUtil {
}
public static Integer findConditionalOperationParameterIndex(Method theMethod) {
return MethodUtil.findParamAnnotationIndex(theMethod, ConditionalOperationParam.class);
return MethodUtil.findParamAnnotationIndex(theMethod, ConditionalUrlParam.class);
}
@SuppressWarnings("deprecation")
@ -396,8 +397,10 @@ public class MethodUtil {
param = new SortParameter();
} else if (nextAnnotation instanceof TransactionParam) {
param = new TransactionParamBinder(theContext);
} else if (nextAnnotation instanceof ConditionalOperationParam) {
} else if (nextAnnotation instanceof ConditionalUrlParam) {
param = new ConditionalParamBinder(theRestfulOperationTypeEnum);
} else if (nextAnnotation instanceof OperationParam) {
param = new OperationParamBinder((OperationParam)nextAnnotation);
} else {
continue;
}

View File

@ -25,6 +25,8 @@ import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.hl7.fhir.instance.model.IBaseResource;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
@ -32,7 +34,7 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
class NullParameter implements IParameter {
@Override
public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map<String, List<String>> theTargetQueryArguments) throws InternalErrorException {
public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map<String, List<String>> theTargetQueryArguments, IBaseResource theTargetResource) throws InternalErrorException {
//nothing
}

View File

@ -22,11 +22,145 @@ package ca.uhn.fhir.rest.method;
import static org.apache.commons.lang3.StringUtils.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import org.hl7.fhir.instance.model.IBaseResource;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.model.api.Bundle;
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.model.valueset.BundleTypeEnum;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.client.BaseHttpClientInvocation;
import ca.uhn.fhir.rest.server.EncodingEnum;
import ca.uhn.fhir.rest.server.IBundleProvider;
import ca.uhn.fhir.rest.server.RestfulServerUtils;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
public class OperationMethodBinding {
public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
private Integer myIdParamIndex;
private String myName;
public OperationMethodBinding(Class<?> theReturnResourceType, Class<? extends IBaseResource> theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider, Operation theAnnotation) {
super(theReturnResourceType, theMethod, theContext, theProvider);
myIdParamIndex = MethodUtil.findIdParameterIndex(theMethod);
myName = theAnnotation.name();
if (isBlank(myName)) {
throw new ConfigurationException("Method '" + theMethod.getName() + "' on type " + theMethod.getDeclaringClass().getName() + " is annotated with @" + Operation.class.getSimpleName() + " but this annotation has no name defined");
}
if (myName.startsWith("$") == false) {
myName = "$" + myName;
}
if (theContext.getVersion().getVersion().isEquivalentTo(FhirVersionEnum.DSTU1)) {
throw new ConfigurationException("@" + Operation.class.getSimpleName() + " methods are not supported on servers for FHIR version " + theContext.getVersion().getVersion().name());
}
if (theReturnTypeFromRp != null) {
setResourceName(theContext.getResourceDefinition(theReturnTypeFromRp).getName());
} else {
if (Modifier.isAbstract(theAnnotation.type().getModifiers())== false) {
setResourceName(theContext.getResourceDefinition(theAnnotation.type()).getName());
} else {
setResourceName(null);
}
}
if (theMethod.getReturnType().isAssignableFrom(Bundle.class)) {
throw new ConfigurationException("Can not return a DSTU1 bundle from an @" + Operation.class.getSimpleName() + " method. Found in method " + theMethod.getName() + " defined in type " + theMethod.getDeclaringClass().getName());
}
}
@Override
public RestfulOperationTypeEnum getResourceOperationType() {
return null;
}
@Override
protected BundleTypeEnum getResponseBundleType() {
return null;
}
@Override
public ReturnTypeEnum getReturnType() {
return ReturnTypeEnum.RESOURCE;
}
@Override
public RestfulOperationSystemEnum getSystemOperationType() {
return null;
}
@Override
public boolean incomingServerRequestMatchesMethod(Request theRequest) {
if (getResourceName() == null) {
if (isNotBlank(theRequest.getResourceName())) {
return false;
}
} else if (!getResourceName().equals(theRequest.getResourceName())) {
return false;
}
boolean requestHasId = theRequest.getId() != null;
if (requestHasId != (myIdParamIndex != null)) {
return false;
}
return myName.equals(theRequest.getOperation());
}
@Override
protected Object parseRequestObject(Request theRequest) throws IOException {
EncodingEnum encoding = RestfulServerUtils.determineRequestEncoding(theRequest);
IParser parser = encoding.newParser(getContext());
BufferedReader requestReader = theRequest.getServletRequest().getReader();
Class<? extends IBaseResource> wantedResourceType = getContext().getResourceDefinition("Parameters").getImplementingClass();
return parser.parseResource(wantedResourceType, requestReader);
}
@Override
public BaseHttpClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException {
String id = null;
if (myIdParamIndex != null) {
IdDt idDt = (IdDt) theArgs[myIdParamIndex];
id = idDt.getValue();
}
IBaseParameters parameters = (IBaseParameters) getContext().getResourceDefinition("Parameters").newInstance();
if (theArgs != null) {
for (int idx = 0; idx < theArgs.length; idx++) {
IParameter nextParam = getParameters().get(idx);
nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], null, parameters);
}
}
return createOperationInvocation(getContext(), getResourceName(), id, myName, parameters);
}
@Override
public Object invokeServer(RequestDetails theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException {
if (myIdParamIndex != null) {
theMethodParams[myIdParamIndex] = theRequest.getId();
}
Object response = invokeServerMethod(theMethodParams);
IBundleProvider retVal = toResourceList(response);
return retVal;
}
public static HttpPostClientInvocation createOperationInvocation(FhirContext theContext, String theResourceName, String theId, String theOperationName, IBaseParameters theInput) {
StringBuilder b = new StringBuilder();
@ -44,7 +178,7 @@ public class OperationMethodBinding {
b.append("$");
}
b.append(theOperationName);
return new HttpPostClientInvocation(theContext, theInput, b.toString());
}

View File

@ -0,0 +1,174 @@
package ca.uhn.fhir.rest.method;
/*
* #%L
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2015 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.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.hl7.fhir.instance.model.IBase;
import org.hl7.fhir.instance.model.IBaseResource;
import org.hl7.fhir.instance.model.IPrimitiveType;
import org.hl7.fhir.instance.model.api.IBaseDatatype;
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
import ca.uhn.fhir.context.BaseRuntimeChildDefinition.IAccessor;
import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeChildPrimitiveDatatypeDefinition;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.param.CollectionBinder;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
class OperationParamBinder implements IParameter {
private String myName;
private Class<?> myParameterType;
private Class<? extends Collection> myInnerCollectionType;
OperationParamBinder(OperationParam theAnnotation) {
myName = theAnnotation.name();
}
@Override
public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map<String, List<String>> theTargetQueryArguments, IBaseResource theTargetResource) throws InternalErrorException {
assert theTargetResource != null;
if (theSourceClientArgument == null) {
return;
}
RuntimeResourceDefinition def = theContext.getResourceDefinition(theTargetResource);
BaseRuntimeChildDefinition paramChild = def.getChildByName("parameter");
BaseRuntimeElementCompositeDefinition<?> paramChildElem = (BaseRuntimeElementCompositeDefinition<?>) paramChild.getChildByName("parameter");
addClientParameter(theSourceClientArgument, theTargetResource, paramChild, paramChildElem);
}
private void addClientParameter(Object theSourceClientArgument, IBaseResource theTargetResource, BaseRuntimeChildDefinition paramChild, BaseRuntimeElementCompositeDefinition<?> paramChildElem) {
if (theSourceClientArgument instanceof IBaseResource) {
IBase parameter = createParameterRepetition(theTargetResource, paramChild, paramChildElem);
paramChildElem.getChildByName("resource").getMutator().addValue(parameter, (IBaseResource)theSourceClientArgument);
}else if (theSourceClientArgument instanceof IBaseDatatype) {
IBase parameter = createParameterRepetition(theTargetResource, paramChild, paramChildElem);
paramChildElem.getChildByName("value[x]").getMutator().addValue(parameter, (IBaseDatatype)theSourceClientArgument);
} else if (theSourceClientArgument instanceof Collection) {
Collection<?> collection = (Collection<?>) theSourceClientArgument;
for (Object next : collection) {
addClientParameter(next, theTargetResource, paramChild, paramChildElem);
}
} else {
throw new IllegalArgumentException("Don't know how to handle value of type " + theSourceClientArgument.getClass() + " for paramater " + myName);
}
}
private IBase createParameterRepetition(IBaseResource theTargetResource, BaseRuntimeChildDefinition paramChild, BaseRuntimeElementCompositeDefinition<?> paramChildElem) {
IBase parameter = paramChildElem.newInstance();
paramChild.getMutator().addValue(theTargetResource, parameter);
paramChildElem.getChildByName("name").getMutator().addValue(parameter, new StringDt(myName));
return parameter;
}
@SuppressWarnings("unchecked")
@Override
public Object translateQueryParametersIntoServerArgument(Request theRequest, Object theRequestContents) throws InternalErrorException, InvalidRequestException {
FhirContext ctx = theRequest.getServer().getFhirContext();
IBaseResource requestContents = (IBaseResource) theRequestContents;
RuntimeResourceDefinition def = ctx.getResourceDefinition(requestContents);
BaseRuntimeChildDefinition paramChild = def.getChildByName("parameter");
BaseRuntimeElementCompositeDefinition<?> paramChildElem = (BaseRuntimeElementCompositeDefinition<?>) paramChild.getChildByName("parameter");
RuntimeChildPrimitiveDatatypeDefinition nameChild = (RuntimeChildPrimitiveDatatypeDefinition) paramChildElem.getChildByName("name");
BaseRuntimeChildDefinition valueChild = paramChildElem.getChildByName("value[x]");
BaseRuntimeChildDefinition resourceChild = paramChildElem.getChildByName("resource");
IAccessor paramChildAccessor = paramChild.getAccessor();
List<IBase> values = paramChildAccessor.getValues(requestContents);
List<Object> matchingParamValues = new ArrayList<Object>();
for (IBase nextParameter : values) {
List<IBase> nextNames = nameChild.getAccessor().getValues(nextParameter);
if (nextNames != null && nextNames.size() > 0) {
IPrimitiveType<?> nextName = (IPrimitiveType<?>) nextNames.get(0);
if (myName.equals(nextName.getValueAsString())) {
if (myParameterType.isAssignableFrom(nextParameter.getClass())) {
matchingParamValues.add(nextParameter);
} else {
List<IBase> paramValues = valueChild.getAccessor().getValues(nextParameter);
List<IBase> paramResources = resourceChild.getAccessor().getValues(nextParameter);
if (paramValues != null && paramValues.size() > 0) {
tryToAddValues(paramValues, matchingParamValues);
} else if (paramResources != null && paramResources.size() > 0) {
tryToAddValues(paramResources, matchingParamValues);
}
}
}
}
}
if (matchingParamValues.isEmpty()) {
return null;
}
if (myInnerCollectionType == null) {
return matchingParamValues.get(0);
}
try {
@SuppressWarnings("rawtypes")
Collection retVal = myInnerCollectionType.newInstance();
retVal.addAll(matchingParamValues);
return retVal;
} catch (InstantiationException e) {
throw new InternalErrorException("Failed to instantiate " + myInnerCollectionType, e);
} catch (IllegalAccessException e) {
throw new InternalErrorException("Failed to instantiate " + myInnerCollectionType, e);
}
}
private void tryToAddValues(List<IBase> theParamValues, List<Object> theMatchingParamValues) {
for (IBase nextValue : theParamValues) {
if (nextValue == null) {
continue;
}
if (!myParameterType.isAssignableFrom(nextValue.getClass())) {
throw new InvalidRequestException("Request has parameter " + myName + " of type " + nextValue.getClass().getSimpleName() + " but method expects type " + myParameterType.getSimpleName());
}
theMatchingParamValues.add(nextValue);
}
}
@Override
public void initializeTypes(Method theMethod, Class<? extends Collection<?>> theOuterCollectionType, Class<? extends Collection<?>> theInnerCollectionType, Class<?> theParameterType) {
myParameterType = theParameterType;
if (theInnerCollectionType != null) {
myInnerCollectionType = CollectionBinder.getInstantiableCollectionType(theInnerCollectionType, myName);
}
}
}

View File

@ -164,7 +164,7 @@ public class ReadMethodBinding extends BaseResourceReturningMethodBinding implem
for (int idx = 0; idx < theArgs.length; idx++) {
IParameter nextParam = getParameters().get(idx);
nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], null);
nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], null, null);
}
return retVal;

View File

@ -267,7 +267,7 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
if (theArgs != null) {
for (int idx = 0; idx < theArgs.length; idx++) {
IParameter nextParam = getParameters().get(idx);
nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], queryStringArgs);
nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], queryStringArgs, null);
}
}

View File

@ -25,6 +25,8 @@ import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.hl7.fhir.instance.model.IBaseResource;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
@ -33,7 +35,7 @@ class ServerBaseParamBinder implements IParameter {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServerBaseParamBinder.class);
@Override
public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map<String, List<String>> theTargetQueryArguments) throws InternalErrorException {
public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map<String, List<String>> theTargetQueryArguments, IBaseResource theTargetResource) throws InternalErrorException {
/*
* Does nothing, since we just ignore serverbase arguments
*/

View File

@ -25,6 +25,8 @@ import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.hl7.fhir.instance.model.IBaseResource;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
@ -33,7 +35,7 @@ class ServletRequestParameter implements IParameter {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServletRequestParameter.class);
@Override
public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map<String, List<String>> theTargetQueryArguments) throws InternalErrorException {
public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map<String, List<String>> theTargetQueryArguments, IBaseResource theTargetResource) throws InternalErrorException {
/*
* Does nothing, since we just ignore HttpServletRequest arguments
*/

View File

@ -25,6 +25,8 @@ import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.hl7.fhir.instance.model.IBaseResource;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
@ -33,7 +35,7 @@ class ServletResponseParameter implements IParameter {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServletResponseParameter.class);
@Override
public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map<String, List<String>> theTargetQueryArguments) throws InternalErrorException {
public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map<String, List<String>> theTargetQueryArguments, IBaseResource theTargetResource) throws InternalErrorException {
/*
* Does nothing, since we just ignore HttpServletResponse arguments
*/

View File

@ -27,6 +27,7 @@ import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.IBaseResource;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
@ -43,7 +44,7 @@ class SinceParameter implements IParameter {
private Class<?> myType;
@Override
public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map<String, List<String>> theTargetQueryArguments) throws InternalErrorException {
public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map<String, List<String>> theTargetQueryArguments, IBaseResource theTargetResource) throws InternalErrorException {
if (theSourceClientArgument != null) {
InstantDt since = ParameterUtil.toInstant(theSourceClientArgument);
if (since.isEmpty() == false) {

View File

@ -28,6 +28,8 @@ import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.hl7.fhir.instance.model.IBaseResource;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.annotation.Sort;
@ -40,7 +42,7 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
public class SortParameter implements IParameter {
@Override
public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map<String, List<String>> theTargetQueryArguments) throws InternalErrorException {
public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map<String, List<String>> theTargetQueryArguments, IBaseResource theTargetResource) throws InternalErrorException {
SortSpec ss = (SortSpec) theSourceClientArgument;
while (ss != null) {
String name;

View File

@ -90,7 +90,7 @@ class TransactionParamBinder implements IParameter {
}
@Override
public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map<String, List<String>> theTargetQueryArguments) throws InternalErrorException {
public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map<String, List<String>> theTargetQueryArguments, IBaseResource theTargetResource) throws InternalErrorException {
// nothing
}

View File

@ -115,7 +115,7 @@ class UpdateMethodBinding extends BaseOutcomeReturningMethodBindingWithResourceP
for (int idx = 0; idx < theArgs.length; idx++) {
IParameter nextParam = getParameters().get(idx);
nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], null);
nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], null, null);
}
return retVal;

View File

@ -74,7 +74,7 @@ public class ValidateMethodBinding extends BaseOutcomeReturningMethodBindingWith
for (int idx = 0; idx < theArgs.length; idx++) {
IParameter nextParam = getParameters().get(idx);
nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], null);
nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], null, null);
}
return retVal;

View File

@ -28,6 +28,7 @@ import java.util.Map;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.IBaseResource;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.dstu.valueset.SearchParamTypeEnum;
@ -98,7 +99,7 @@ public abstract class BaseQueryParameter implements IParameter {
}
@Override
public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map<String, List<String>> theTargetQueryArguments) throws InternalErrorException {
public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map<String, List<String>> theTargetQueryArguments, IBaseResource theTargetResource) throws InternalErrorException {
if (theSourceClientArgument == null) {
if (isRequired()) {
throw new NullPointerException("SearchParameter '" + getName() + "' is required and may not be null");

View File

@ -25,6 +25,8 @@ import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.hl7.fhir.instance.model.IBaseResource;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.rest.method.IParameter;
@ -41,7 +43,7 @@ public class ResourceParameter implements IParameter {
}
@Override
public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map<String, List<String>> theTargetQueryArguments) throws InternalErrorException {
public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map<String, List<String>> theTargetQueryArguments, IBaseResource theTargetResource) throws InternalErrorException {
// TODO Auto-generated method stub
}

View File

@ -89,7 +89,6 @@ public class RestfulServer extends HttpServlet {
private FhirContext myFhirContext;
private String myImplementationDescription;
private final List<IServerInterceptor> myInterceptors = new ArrayList<IServerInterceptor>();
private ResourceBinding myNullResourceBinding = new ResourceBinding();
private IPagingProvider myPagingProvider;
private Collection<Object> myPlainProviders;
private Map<String, ResourceBinding> myResourceNameToProvider = new HashMap<String, ResourceBinding>();
@ -119,7 +118,8 @@ public class RestfulServer extends HttpServlet {
/**
* This method is called prior to sending a response to incoming requests. It is used to add custom headers.
* <p>
* Use caution if overriding this method: it is recommended to call <code>super.addHeadersToResponse</code> to avoid inadvertantly disabling functionality.
* Use caution if overriding this method: it is recommended to call <code>super.addHeadersToResponse</code> to avoid
* inadvertantly disabling functionality.
* </p>
*/
public void addHeadersToResponse(HttpServletResponse theHttpResponse) {
@ -221,7 +221,7 @@ public class RestfulServer extends HttpServlet {
String resourceName = foundMethodBinding.getResourceName();
ResourceBinding resourceBinding;
if (resourceName == null) {
resourceBinding = myNullResourceBinding;
resourceBinding = myServerBinding;
} else {
RuntimeResourceDefinition definition = myFhirContext.getResourceDefinition(resourceName);
if (myResourceNameToProvider.containsKey(definition.getName())) {
@ -263,6 +263,8 @@ public class RestfulServer extends HttpServlet {
}
private ResourceBinding myServerBinding = new ResourceBinding();
private void findSystemMethods(Object theSystemProvider, Class<?> clazz) {
Class<?> supertype = clazz.getSuperclass();
if (!Object.class.equals(supertype)) {
@ -277,6 +279,8 @@ public class RestfulServer extends HttpServlet {
if (foundMethodBinding != null) {
if (foundMethodBinding instanceof ConformanceMethodBinding) {
myServerConformanceMethod = foundMethodBinding;
} else {
myServerBinding.addMethod(foundMethodBinding);
}
ourLog.info(" * Method: {}#{} is a handler", theSystemProvider.getClass(), m.getName());
} else {
@ -296,8 +300,9 @@ public class RestfulServer extends HttpServlet {
}
/**
* Returns the default encoding to return (XML/JSON) if an incoming request does not specify a preference (either with the <code>_format</code> URL parameter, or with an <code>Accept</code> header
* in the request. The default is {@link EncodingEnum#XML}.
* Returns the default encoding to return (XML/JSON) if an incoming request does not specify a preference (either
* with the <code>_format</code> URL parameter, or with an <code>Accept</code> header in the request. The default is
* {@link EncodingEnum#XML}.
*/
public EncodingEnum getDefaultResponseEncoding() {
return myDefaultResponseEncoding;
@ -311,8 +316,8 @@ public class RestfulServer extends HttpServlet {
}
/**
* Gets the {@link FhirContext} associated with this server. For efficient processing, resource providers and plain providers should generally use this context if one is needed, as opposed to
* creating their own.
* Gets the {@link FhirContext} associated with this server. For efficient processing, resource providers and plain
* providers should generally use this context if one is needed, as opposed to creating their own.
*/
public FhirContext getFhirContext() {
return myFhirContext;
@ -343,7 +348,8 @@ public class RestfulServer extends HttpServlet {
}
/**
* Allows users of RestfulServer to override the getRequestPath method to let them build their custom request path implementation
* Allows users of RestfulServer to override the getRequestPath method to let them build their custom request path
* implementation
*
* @param requestFullPath
* the full request path
@ -369,7 +375,8 @@ public class RestfulServer extends HttpServlet {
}
/**
* Get the server address strategy, which is used to determine what base URL to provide clients to refer to this server. Defaults to an instance of {@link IncomingRequestAddressStrategy}
* Get the server address strategy, which is used to determine what base URL to provide clients to refer to this
* server. Defaults to an instance of {@link IncomingRequestAddressStrategy}
*/
public IServerAddressStrategy getServerAddressStrategy() {
return myServerAddressStrategy;
@ -386,9 +393,11 @@ public class RestfulServer extends HttpServlet {
}
/**
* Returns the server conformance provider, which is the provider that is used to generate the server's conformance (metadata) statement if one has been explicitly defined.
* Returns the server conformance provider, which is the provider that is used to generate the server's conformance
* (metadata) statement if one has been explicitly defined.
* <p>
* By default, the ServerConformanceProvider for the declared version of FHIR is used, but this can be changed, or set to <code>null</code> to use the appropriate one for the given FHIR version.
* By default, the ServerConformanceProvider for the declared version of FHIR is used, but this can be changed, or
* set to <code>null</code> to use the appropriate one for the given FHIR version.
* </p>
*/
public Object getServerConformanceProvider() {
@ -396,7 +405,8 @@ public class RestfulServer extends HttpServlet {
}
/**
* Gets the server's name, as exported in conformance profiles exported by the server. This is informational only, but can be helpful to set with something appropriate.
* Gets the server's name, as exported in conformance profiles exported by the server. This is informational only,
* but can be helpful to set with something appropriate.
*
* @see RestfulServer#setServerName(String)
*/
@ -409,7 +419,8 @@ public class RestfulServer extends HttpServlet {
}
/**
* Gets the server's version, as exported in conformance profiles exported by the server. This is informational only, but can be helpful to set with something appropriate.
* Gets the server's version, as exported in conformance profiles exported by the server. This is informational
* only, but can be helpful to set with something appropriate.
*/
public String getServerVersion() {
return myServerVersion;
@ -449,8 +460,7 @@ public class RestfulServer extends HttpServlet {
boolean respondGzip = theRequest.isRespondGzip();
IVersionSpecificBundleFactory bundleFactory = myFhirContext.newBundleFactory();
bundleFactory.initializeBundleFromBundleProvider(this, resultList, responseEncoding, theRequest.getFhirServerBase(), theRequest.getCompleteUrl(), prettyPrint, start, count, thePagingAction,
null);
bundleFactory.initializeBundleFromBundleProvider(this, resultList, responseEncoding, theRequest.getFhirServerBase(), theRequest.getCompleteUrl(), prettyPrint, start, count, thePagingAction, null);
Bundle bundle = bundleFactory.getDstu1Bundle();
if (bundle != null) {
@ -473,8 +483,7 @@ public class RestfulServer extends HttpServlet {
return;
}
}
RestfulServerUtils.streamResponseAsResource(this, theResponse, (IResource) resBundle, responseEncoding, prettyPrint, requestIsBrowser, narrativeMode, Constants.STATUS_HTTP_200_OK,
theRequest.isRespondGzip(), theRequest.getFhirServerBase());
RestfulServerUtils.streamResponseAsResource(this, theResponse, (IResource) resBundle, responseEncoding, prettyPrint, requestIsBrowser, narrativeMode, Constants.STATUS_HTTP_200_OK, theRequest.isRespondGzip(), theRequest.getFhirServerBase());
}
}
@ -530,7 +539,7 @@ public class RestfulServer extends HttpServlet {
StringTokenizer tok = new StringTokenizer(requestPath, "/");
if (tok.hasMoreTokens()) {
resourceName = tok.nextToken();
if (resourceName.startsWith("_")) {
if (partIsOperation(resourceName)) {
operation = resourceName;
resourceName = null;
}
@ -541,7 +550,7 @@ public class RestfulServer extends HttpServlet {
if (Constants.URL_TOKEN_METADATA.equals(resourceName) || theRequestType == RequestType.OPTIONS) {
resourceMethod = myServerConformanceMethod;
} else if (resourceName == null) {
resourceBinding = myNullResourceBinding;
resourceBinding = myServerBinding;
} else {
resourceBinding = myResourceNameToProvider.get(resourceName);
if (resourceBinding == null) {
@ -551,7 +560,7 @@ public class RestfulServer extends HttpServlet {
if (tok.hasMoreTokens()) {
String nextString = tok.nextToken();
if (nextString.startsWith("_")) {
if (partIsOperation(nextString)) {
operation = nextString;
} else {
id = new IdDt(resourceName, UrlUtil.unescape(nextString));
@ -570,10 +579,10 @@ public class RestfulServer extends HttpServlet {
} else {
operation = Constants.PARAM_HISTORY;
}
} else if (nextString.startsWith("_")) {
} else if (partIsOperation(nextString)) {
// FIXME: this would be untrue for _meta/_delete
if (operation != null) {
throw new InvalidRequestException("URL Path contains two operations (part beginning with _): " + requestPath);
throw new InvalidRequestException("URL Path contains two operations: " + requestPath);
}
operation = nextString;
} else {
@ -637,8 +646,10 @@ public class RestfulServer extends HttpServlet {
return;
}
if (resourceMethod == null && resourceBinding != null) {
resourceMethod = resourceBinding.getMethod(r);
if (resourceMethod == null) {
if (resourceBinding != null) {
resourceMethod = resourceBinding.getMethod(r);
}
}
if (resourceMethod == null) {
StringBuilder b = new StringBuilder();
@ -766,8 +777,9 @@ public class RestfulServer extends HttpServlet {
}
/**
* Initializes the server. Note that this method is final to avoid accidentally introducing bugs in implementations, but subclasses may put initialization code in {@link #initialize()}, which is
* called immediately before beginning initialization of the restful server's internal init.
* Initializes the server. Note that this method is final to avoid accidentally introducing bugs in implementations,
* but subclasses may put initialization code in {@link #initialize()}, which is called immediately before beginning
* initialization of the restful server's internal init.
*/
@Override
public final void init() throws ServletException {
@ -790,8 +802,7 @@ public class RestfulServer extends HttpServlet {
String resourceName = myFhirContext.getResourceDefinition(resourceType).getName();
if (typeToProvider.containsKey(resourceName)) {
throw new ServletException("Multiple resource providers return resource type[" + resourceName + "]: First[" + typeToProvider.get(resourceName).getClass().getCanonicalName()
+ "] and Second[" + nextProvider.getClass().getCanonicalName() + "]");
throw new ServletException("Multiple resource providers return resource type[" + resourceName + "]: First[" + typeToProvider.get(resourceName).getClass().getCanonicalName() + "] and Second[" + nextProvider.getClass().getCanonicalName() + "]");
}
typeToProvider.put(resourceName, nextProvider);
providedResourceScanner.scanForProvidedResources(nextProvider);
@ -829,11 +840,13 @@ public class RestfulServer extends HttpServlet {
}
/**
* This method may be overridden by subclasses to do perform initialization that needs to be performed prior to the server being used.
* This method may be overridden by subclasses to do perform initialization that needs to be performed prior to the
* server being used.
*
* @throws ServletException
* If the initialization failed. Note that you should consider throwing {@link UnavailableException} (which extends {@link ServletException}), as this is a flag to the servlet
* container that the servlet is not usable.
* If the initialization failed. Note that you should consider throwing {@link UnavailableException}
* (which extends {@link ServletException}), as this is a flag to the servlet container that the servlet
* is not usable.
*/
protected void initialize() throws ServletException {
// nothing by default
@ -880,8 +893,9 @@ public class RestfulServer extends HttpServlet {
}
/**
* Sets the profile tagging behaviour for the server. When set to a value other than {@link AddProfileTagEnum#NEVER} (which is the default), the server will automatically add a profile tag based
* on the class of the resource(s) being returned.
* Sets the profile tagging behaviour for the server. When set to a value other than {@link AddProfileTagEnum#NEVER}
* (which is the default), the server will automatically add a profile tag based on the class of the resource(s)
* being returned.
*
* @param theAddProfileTag
* The behaviour enum (must not be null)
@ -892,8 +906,9 @@ public class RestfulServer extends HttpServlet {
}
/**
* Sets the default encoding to return (XML/JSON) if an incoming request does not specify a preference (either with the <code>_format</code> URL parameter, or with an <code>Accept</code> header in
* the request. The default is {@link EncodingEnum#XML}.
* Sets the default encoding to return (XML/JSON) if an incoming request does not specify a preference (either with
* the <code>_format</code> URL parameter, or with an <code>Accept</code> header in the request. The default is
* {@link EncodingEnum#XML}.
*/
public void setDefaultResponseEncoding(EncodingEnum theDefaultResponseEncoding) {
Validate.notNull(theDefaultResponseEncoding, "theDefaultResponseEncoding can not be null");
@ -901,7 +916,8 @@ public class RestfulServer extends HttpServlet {
}
/**
* Sets (enables/disables) the server support for ETags. Must not be <code>null</code>. Default is {@link #DEFAULT_ETAG_SUPPORT}
* Sets (enables/disables) the server support for ETags. Must not be <code>null</code>. Default is
* {@link #DEFAULT_ETAG_SUPPORT}
*
* @param theETagSupport
* The ETag support mode
@ -997,7 +1013,8 @@ public class RestfulServer extends HttpServlet {
}
/**
* Provide a server address strategy, which is used to determine what base URL to provide clients to refer to this server. Defaults to an instance of {@link IncomingRequestAddressStrategy}
* Provide a server address strategy, which is used to determine what base URL to provide clients to refer to this
* server. Defaults to an instance of {@link IncomingRequestAddressStrategy}
*/
public void setServerAddressStrategy(IServerAddressStrategy theServerAddressStrategy) {
Validate.notNull(theServerAddressStrategy, "Server address strategy can not be null");
@ -1005,15 +1022,17 @@ public class RestfulServer extends HttpServlet {
}
/**
* Returns the server conformance provider, which is the provider that is used to generate the server's conformance (metadata) statement.
* Returns the server conformance provider, which is the provider that is used to generate the server's conformance
* (metadata) statement.
* <p>
* By default, the ServerConformanceProvider implementation for the declared version of FHIR is used, but this can be changed, or set to <code>null</code> if you do not wish to export a
* conformance statement.
* By default, the ServerConformanceProvider implementation for the declared version of FHIR is used, but this can
* be changed, or set to <code>null</code> if you do not wish to export a conformance statement.
* </p>
* Note that this method can only be called before the server is initialized.
*
* @throws IllegalStateException
* Note that this method can only be called prior to {@link #init() initialization} and will throw an {@link IllegalStateException} if called after that.
* Note that this method can only be called prior to {@link #init() initialization} and will throw an
* {@link IllegalStateException} if called after that.
*/
public void setServerConformanceProvider(Object theServerConformanceProvider) {
if (myStarted) {
@ -1023,22 +1042,24 @@ public class RestfulServer extends HttpServlet {
}
/**
* Sets the server's name, as exported in conformance profiles exported by the server. This is informational only, but can be helpful to set with something appropriate.
* Sets the server's name, as exported in conformance profiles exported by the server. This is informational only,
* but can be helpful to set with something appropriate.
*/
public void setServerName(String theServerName) {
myServerName = theServerName;
}
/**
* Gets the server's version, as exported in conformance profiles exported by the server. This is informational only, but can be helpful to set with something appropriate.
* Gets the server's version, as exported in conformance profiles exported by the server. This is informational
* only, but can be helpful to set with something appropriate.
*/
public void setServerVersion(String theServerVersion) {
myServerVersion = theServerVersion;
}
/**
* If set to <code>true</code> (default is false), the server will use browser friendly content-types (instead of standard FHIR ones) when it detects that the request is coming from a browser
* instead of a FHIR
* If set to <code>true</code> (default is false), the server will use browser friendly content-types (instead of
* standard FHIR ones) when it detects that the request is coming from a browser instead of a FHIR
*/
public void setUseBrowserFriendlyContentTypes(boolean theUseBrowserFriendlyContentTypes) {
myUseBrowserFriendlyContentTypes = theUseBrowserFriendlyContentTypes;
@ -1057,6 +1078,10 @@ public class RestfulServer extends HttpServlet {
theResponse.getWriter().write(theException.getMessage());
}
private static boolean partIsOperation(String nextString) {
return nextString.length() > 0 && (nextString.charAt(0) == '_' || nextString.charAt(0) == '$');
}
public enum NarrativeModeEnum {
NORMAL, ONLY, SUPPRESS;

View File

@ -25,7 +25,7 @@ import javax.servlet.http.HttpServletRequest;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.annotation.ConditionalOperationParam;
import ca.uhn.fhir.rest.annotation.ConditionalUrlParam;
import ca.uhn.fhir.rest.annotation.Create;
import ca.uhn.fhir.rest.annotation.Delete;
import ca.uhn.fhir.rest.annotation.IdParam;
@ -47,7 +47,7 @@ public class JpaResourceProviderDstu2<T extends IResource> extends BaseJpaResour
}
@Create
public MethodOutcome create(HttpServletRequest theRequest, @ResourceParam T theResource, @ConditionalOperationParam String theConditional) {
public MethodOutcome create(HttpServletRequest theRequest, @ResourceParam T theResource, @ConditionalUrlParam String theConditional) {
startRequest(theRequest);
try {
if (theConditional != null) {
@ -61,7 +61,7 @@ public class JpaResourceProviderDstu2<T extends IResource> extends BaseJpaResour
}
@Delete
public MethodOutcome delete(HttpServletRequest theRequest, @IdParam IdDt theResource, @ConditionalOperationParam String theConditional) {
public MethodOutcome delete(HttpServletRequest theRequest, @IdParam IdDt theResource, @ConditionalUrlParam String theConditional) {
startRequest(theRequest);
try {
if (theConditional != null) {
@ -75,7 +75,7 @@ public class JpaResourceProviderDstu2<T extends IResource> extends BaseJpaResour
}
@Update
public MethodOutcome update(HttpServletRequest theRequest, @ResourceParam T theResource, @IdParam IdDt theId, @ConditionalOperationParam String theConditional) {
public MethodOutcome update(HttpServletRequest theRequest, @ResourceParam T theResource, @IdParam IdDt theId, @ConditionalUrlParam String theConditional) {
startRequest(theRequest);
try {
if (theConditional != null) {

View File

@ -71,6 +71,7 @@ public class BundleTypeTest {
assertTrue("Expected request of type POST on long params list", value instanceof HttpPost);
HttpPost post = (HttpPost) value;
String body = IOUtils.toString(post.getEntity().getContent());
IOUtils.closeQuietly(post.getEntity().getContent());
ourLog.info(body);
assertThat(body, Matchers.containsString("<type value=\"" + BundleTypeEnum.TRANSACTION.getCode()));

View File

@ -0,0 +1,306 @@
package ca.uhn.fhir.rest.client;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import java.io.InputStream;
import java.io.StringReader;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.ReaderInputStream;
import org.apache.http.HttpResponse;
import org.apache.http.ProtocolVersion;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicStatusLine;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.internal.stubbing.defaultanswers.ReturnsDeepStubs;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.dstu2.resource.Parameters;
import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.client.api.IBasicClient;
import ca.uhn.fhir.rest.server.Constants;
public class OperationClientTest {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OperationClientTest.class);
private FhirContext ourCtx;
private HttpClient ourHttpClient;
private HttpResponse ourHttpResponse;
@Before
public void before() {
ourCtx = FhirContext.forDstu2();
ourHttpClient = mock(HttpClient.class, new ReturnsDeepStubs());
ourCtx.getRestfulClientFactory().setHttpClient(ourHttpClient);
ourCtx.getRestfulClientFactory().setServerValidationModeEnum(ServerValidationModeEnum.NEVER);
ourHttpResponse = mock(HttpResponse.class, new ReturnsDeepStubs());
}
@Test
public void testOpInstance() throws Exception {
Parameters outParams = new Parameters();
outParams.addParameter().setName("FOO");
final String retVal = ourCtx.newXmlParser().encodeResourceToString(outParams);
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(ourHttpClient.execute(capt.capture())).thenReturn(ourHttpResponse);
when(ourHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
when(ourHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(ourHttpResponse.getEntity().getContent()).thenAnswer(new Answer<InputStream>() {
@Override
public InputStream answer(InvocationOnMock theInvocation) throws Throwable {
return new ReaderInputStream(new StringReader(retVal), Charset.forName("UTF-8"));
}
});
IOpClient client = ourCtx.newRestfulClient(IOpClient.class, "http://foo");
int idx = 0;
Parameters response = client.opInstance(new IdDt("222"), new StringDt("PARAM1str"), new Patient().setActive(true));
assertEquals("FOO", response.getParameter().get(0).getName());
HttpPost value = (HttpPost) capt.getAllValues().get(idx);
String requestBody = IOUtils.toString(((HttpPost) value).getEntity().getContent());
IOUtils.closeQuietly(((HttpPost) value).getEntity().getContent());
ourLog.info(requestBody);
Parameters request = ourCtx.newXmlParser().parseResource(Parameters.class, requestBody);
assertEquals("http://foo/Patient/222/$OP_INSTANCE", value.getURI().toASCIIString());
assertEquals(2, request.getParameter().size());
assertEquals("PARAM1", request.getParameter().get(0).getName());
assertEquals("PARAM1str", ((StringDt) request.getParameter().get(0).getValue()).getValue());
assertEquals("PARAM2", request.getParameter().get(1).getName());
assertEquals(Boolean.TRUE, ((Patient) request.getParameter().get(1).getResource()).getActive());
idx++;
}
@Test
public void testOpServer() throws Exception {
Parameters outParams = new Parameters();
outParams.addParameter().setName("FOO");
final String retVal = ourCtx.newXmlParser().encodeResourceToString(outParams);
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(ourHttpClient.execute(capt.capture())).thenReturn(ourHttpResponse);
when(ourHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
when(ourHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(ourHttpResponse.getEntity().getContent()).thenAnswer(new Answer<InputStream>() {
@Override
public InputStream answer(InvocationOnMock theInvocation) throws Throwable {
return new ReaderInputStream(new StringReader(retVal), Charset.forName("UTF-8"));
}
});
IOpClient client = ourCtx.newRestfulClient(IOpClient.class, "http://foo");
int idx = 0;
Parameters response = client.opServer(new StringDt("PARAM1str"), new Patient().setActive(true));
assertEquals("FOO", response.getParameter().get(0).getName());
HttpPost value = (HttpPost) capt.getAllValues().get(idx);
String requestBody = IOUtils.toString(((HttpPost) value).getEntity().getContent());
IOUtils.closeQuietly(((HttpPost) value).getEntity().getContent());
ourLog.info(requestBody);
Parameters request = ourCtx.newXmlParser().parseResource(Parameters.class, requestBody);
assertEquals("http://foo/$OP_SERVER", value.getURI().toASCIIString());
assertEquals(2, request.getParameter().size());
assertEquals("PARAM1", request.getParameter().get(0).getName());
assertEquals("PARAM1str", ((StringDt) request.getParameter().get(0).getValue()).getValue());
assertEquals("PARAM2", request.getParameter().get(1).getName());
assertEquals(Boolean.TRUE, ((Patient) request.getParameter().get(1).getResource()).getActive());
idx++;
response = client.opServer(null, new Patient().setActive(true));
assertEquals("FOO", response.getParameter().get(0).getName());
value = (HttpPost) capt.getAllValues().get(idx);
requestBody = IOUtils.toString(((HttpPost) value).getEntity().getContent());
IOUtils.closeQuietly(((HttpPost) value).getEntity().getContent());
ourLog.info(requestBody);
request = ourCtx.newXmlParser().parseResource(Parameters.class, requestBody);
assertEquals(1, request.getParameter().size());
assertEquals("PARAM2", request.getParameter().get(0).getName());
assertEquals(Boolean.TRUE, ((Patient) request.getParameter().get(0).getResource()).getActive());
idx++;
response = client.opServer(null, null);
assertEquals("FOO", response.getParameter().get(0).getName());
value = (HttpPost) capt.getAllValues().get(idx);
requestBody = IOUtils.toString(((HttpPost) value).getEntity().getContent());
IOUtils.closeQuietly(((HttpPost) value).getEntity().getContent());
ourLog.info(requestBody);
request = ourCtx.newXmlParser().parseResource(Parameters.class, requestBody);
assertEquals(0, request.getParameter().size());
idx++;
}
@Test
public void testOpWithListParam() throws Exception {
Parameters outParams = new Parameters();
outParams.addParameter().setName("FOO");
final String retVal = ourCtx.newXmlParser().encodeResourceToString(outParams);
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(ourHttpClient.execute(capt.capture())).thenReturn(ourHttpResponse);
when(ourHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
when(ourHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(ourHttpResponse.getEntity().getContent()).thenAnswer(new Answer<InputStream>() {
@Override
public InputStream answer(InvocationOnMock theInvocation) throws Throwable {
return new ReaderInputStream(new StringReader(retVal), Charset.forName("UTF-8"));
}
});
IOpClient client = ourCtx.newRestfulClient(IOpClient.class, "http://foo");
int idx = 0;
Parameters response = client.opServerListParam(new Patient().setActive(true), Arrays.asList(new StringDt("PARAM3str1"), new StringDt("PARAM3str2")));
assertEquals("FOO", response.getParameter().get(0).getName());
HttpPost value = (HttpPost) capt.getAllValues().get(idx);
String requestBody = IOUtils.toString(((HttpPost) value).getEntity().getContent());
IOUtils.closeQuietly(((HttpPost) value).getEntity().getContent());
ourLog.info(requestBody);
Parameters request = ourCtx.newXmlParser().parseResource(Parameters.class, requestBody);
assertEquals("http://foo/$OP_SERVER_LIST_PARAM", value.getURI().toASCIIString());
assertEquals(3, request.getParameter().size());
assertEquals("PARAM2", request.getParameter().get(0).getName());
assertEquals(Boolean.TRUE, ((Patient) request.getParameter().get(0).getResource()).getActive());
assertEquals("PARAM3", request.getParameter().get(1).getName());
assertEquals("PARAM3str1", ((StringDt) request.getParameter().get(1).getValue()).getValue());
assertEquals("PARAM3", request.getParameter().get(2).getName());
assertEquals("PARAM3str2", ((StringDt) request.getParameter().get(2).getValue()).getValue());
idx++;
response = client.opServerListParam(null, Arrays.asList(new StringDt("PARAM3str1"), new StringDt("PARAM3str2")));
assertEquals("FOO", response.getParameter().get(0).getName());
value = (HttpPost) capt.getAllValues().get(idx);
requestBody = IOUtils.toString(((HttpPost) value).getEntity().getContent());
IOUtils.closeQuietly(((HttpPost) value).getEntity().getContent());
ourLog.info(requestBody);
request = ourCtx.newXmlParser().parseResource(Parameters.class, requestBody);
assertEquals("http://foo/$OP_SERVER_LIST_PARAM", value.getURI().toASCIIString());
assertEquals(2, request.getParameter().size());
assertEquals("PARAM3", request.getParameter().get(0).getName());
assertEquals("PARAM3str1", ((StringDt) request.getParameter().get(0).getValue()).getValue());
assertEquals("PARAM3", request.getParameter().get(1).getName());
assertEquals("PARAM3str2", ((StringDt) request.getParameter().get(1).getValue()).getValue());
idx++;
response = client.opServerListParam(null, new ArrayList<StringDt>());
assertEquals("FOO", response.getParameter().get(0).getName());
value = (HttpPost) capt.getAllValues().get(idx);
requestBody = IOUtils.toString(((HttpPost) value).getEntity().getContent());
IOUtils.closeQuietly(((HttpPost) value).getEntity().getContent());
ourLog.info(requestBody);
request = ourCtx.newXmlParser().parseResource(Parameters.class, requestBody);
assertEquals("http://foo/$OP_SERVER_LIST_PARAM", value.getURI().toASCIIString());
assertEquals(0, request.getParameter().size());
idx++;
response = client.opServerListParam(null, null);
assertEquals("FOO", response.getParameter().get(0).getName());
value = (HttpPost) capt.getAllValues().get(idx);
requestBody = IOUtils.toString(((HttpPost) value).getEntity().getContent());
IOUtils.closeQuietly(((HttpPost) value).getEntity().getContent());
ourLog.info(requestBody);
request = ourCtx.newXmlParser().parseResource(Parameters.class, requestBody);
assertEquals("http://foo/$OP_SERVER_LIST_PARAM", value.getURI().toASCIIString());
assertEquals(0, request.getParameter().size());
idx++;
}
@Test
public void testOpType() throws Exception {
Parameters outParams = new Parameters();
outParams.addParameter().setName("FOO");
final String retVal = ourCtx.newXmlParser().encodeResourceToString(outParams);
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(ourHttpClient.execute(capt.capture())).thenReturn(ourHttpResponse);
when(ourHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
when(ourHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(ourHttpResponse.getEntity().getContent()).thenAnswer(new Answer<InputStream>() {
@Override
public InputStream answer(InvocationOnMock theInvocation) throws Throwable {
return new ReaderInputStream(new StringReader(retVal), Charset.forName("UTF-8"));
}
});
IOpClient client = ourCtx.newRestfulClient(IOpClient.class, "http://foo");
int idx = 0;
Parameters response = client.opType(new StringDt("PARAM1str"), new Patient().setActive(true));
assertEquals("FOO", response.getParameter().get(0).getName());
HttpPost value = (HttpPost) capt.getAllValues().get(idx);
String requestBody = IOUtils.toString(((HttpPost) value).getEntity().getContent());
IOUtils.closeQuietly(((HttpPost) value).getEntity().getContent());
ourLog.info(requestBody);
Parameters request = ourCtx.newXmlParser().parseResource(Parameters.class, requestBody);
assertEquals("http://foo/Patient/$OP_TYPE", value.getURI().toASCIIString());
assertEquals(2, request.getParameter().size());
assertEquals("PARAM1", request.getParameter().get(0).getName());
assertEquals("PARAM1str", ((StringDt) request.getParameter().get(0).getValue()).getValue());
assertEquals("PARAM2", request.getParameter().get(1).getName());
assertEquals(Boolean.TRUE, ((Patient) request.getParameter().get(1).getResource()).getActive());
idx++;
}
public interface IOpClient extends IBasicClient {
//@formatter:off
@Operation(name="$OP_INSTANCE", type=Patient.class)
public Parameters opInstance(
@IdParam IdDt theId,
@OperationParam(name="PARAM1") StringDt theParam1,
@OperationParam(name="PARAM2") Patient theParam2
);
//@formatter:on
//@formatter:off
@Operation(name="$OP_SERVER")
public Parameters opServer(
@OperationParam(name="PARAM1") StringDt theParam1,
@OperationParam(name="PARAM2") Patient theParam2
);
//@formatter:on
//@formatter:off
@Operation(name="$OP_SERVER_LIST_PARAM")
public Parameters opServerListParam(
@OperationParam(name="PARAM2") Patient theParam2,
@OperationParam(name="PARAM3") List<StringDt> theParam3
);
//@formatter:on
//@formatter:off
@Operation(name="$OP_TYPE", type=Patient.class)
public Parameters opType(
@OperationParam(name="PARAM1") StringDt theParam1,
@OperationParam(name="PARAM2") Patient theParam2
);
//@formatter:on
}
}

View File

@ -36,7 +36,7 @@ import ca.uhn.fhir.model.dstu2.resource.Organization;
import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.rest.annotation.ConditionalOperationParam;
import ca.uhn.fhir.rest.annotation.ConditionalUrlParam;
import ca.uhn.fhir.rest.annotation.Create;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.OptionalParam;
@ -188,7 +188,7 @@ public class CreateConditionalTest {
}
@Create()
public MethodOutcome createPatient(@ResourceParam Patient thePatient, @ConditionalOperationParam String theConditional, @IdParam IdDt theIdParam) {
public MethodOutcome createPatient(@ResourceParam Patient thePatient, @ConditionalUrlParam String theConditional, @IdParam IdDt theIdParam) {
ourLastConditionalUrl = theConditional;
ourLastId = thePatient.getId();
ourLastIdParam = theIdParam;

View File

@ -21,7 +21,7 @@ import org.junit.Test;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.annotation.ConditionalOperationParam;
import ca.uhn.fhir.rest.annotation.ConditionalUrlParam;
import ca.uhn.fhir.rest.annotation.Delete;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.api.MethodOutcome;
@ -114,7 +114,7 @@ public class DeleteConditionalTest {
@Delete()
public MethodOutcome updatePatient(@ConditionalOperationParam String theConditional, @IdParam IdDt theIdParam) {
public MethodOutcome updatePatient(@ConditionalUrlParam String theConditional, @IdParam IdDt theIdParam) {
ourLastConditionalUrl = theConditional;
ourLastIdParam = theIdParam;
return new MethodOutcome(new IdDt("Patient/001/_history/002"));

View File

@ -0,0 +1,333 @@
package ca.uhn.fhir.rest.server;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.dstu2.resource.Bundle;
import ca.uhn.fhir.model.dstu2.resource.Parameters;
import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.IntegerDt;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.util.PortUtil;
/**
* Created by dsotnikov on 2/25/2014.
*/
public class OperationServerTest {
private static CloseableHttpClient ourClient;
private static FhirContext ourCtx;
private static StringDt ourLastParam1;
private static Patient ourLastParam2;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OperationServerTest.class);
private static int ourPort;
private static IdDt ourLastId;
private static Server ourServer;
private static String ourLastMethod;
private static List<StringDt> ourLastParam3;
@Before
public void before() {
ourLastParam1 = null;
ourLastParam2 = null;
ourLastParam3 = null;
ourLastId = null;
ourLastMethod = "";
}
@Test
public void testOperationOnType() throws Exception {
Parameters p = new Parameters();
p.addParameter().setName("PARAM1").setValue(new StringDt("PARAM1val"));
p.addParameter().setName("PARAM2").setResource(new Patient().setActive(true));
String inParamsStr = ourCtx.newXmlParser().encodeResourceToString(p);
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/$OP_TYPE");
httpPost.setEntity(new StringEntity(inParamsStr, ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
HttpResponse status = ourClient.execute(httpPost);
assertEquals(200, status.getStatusLine().getStatusCode());
String response = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals("PARAM1val", ourLastParam1.getValue());
assertEquals(true, ourLastParam2.getActive().booleanValue());
assertEquals("$OP_TYPE", ourLastMethod);
Parameters resp = ourCtx.newXmlParser().parseResource(Parameters.class, response);
assertEquals("RET1", resp.getParameter().get(0).getName());
}
@Test
public void testOperationOnTypeReturnBundle() throws Exception {
Parameters p = new Parameters();
p.addParameter().setName("PARAM1").setValue(new StringDt("PARAM1val"));
p.addParameter().setName("PARAM2").setResource(new Patient().setActive(true));
String inParamsStr = ourCtx.newXmlParser().encodeResourceToString(p);
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/$OP_TYPE_RET_BUNDLE");
httpPost.setEntity(new StringEntity(inParamsStr, ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
HttpResponse status = ourClient.execute(httpPost);
assertEquals(200, status.getStatusLine().getStatusCode());
String response = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals("PARAM1val", ourLastParam1.getValue());
assertEquals(true, ourLastParam2.getActive().booleanValue());
assertEquals("$OP_TYPE_RET_BUNDLE", ourLastMethod);
Bundle resp = ourCtx.newXmlParser().parseResource(Bundle.class, response);
assertEquals("100", resp.getEntryFirstRep().getTransactionResponse().getStatus());
}
@Test
public void testOperationOnServer() throws Exception {
Parameters p = new Parameters();
p.addParameter().setName("PARAM1").setValue(new StringDt("PARAM1val"));
p.addParameter().setName("PARAM2").setResource(new Patient().setActive(true));
String inParamsStr = ourCtx.newXmlParser().encodeResourceToString(p);
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/$OP_SERVER");
httpPost.setEntity(new StringEntity(inParamsStr, ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
HttpResponse status = ourClient.execute(httpPost);
assertEquals(200, status.getStatusLine().getStatusCode());
String response = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals("PARAM1val", ourLastParam1.getValue());
assertEquals(true, ourLastParam2.getActive().booleanValue());
assertEquals("$OP_SERVER", ourLastMethod);
Parameters resp = ourCtx.newXmlParser().parseResource(Parameters.class, response);
assertEquals("RET1", resp.getParameter().get(0).getName());
}
@Test
public void testOperationWithListParam() throws Exception {
Parameters p = new Parameters();
p.addParameter().setName("PARAM2").setResource(new Patient().setActive(true));
p.addParameter().setName("PARAM3").setValue(new StringDt("PARAM3val1"));
p.addParameter().setName("PARAM3").setValue(new StringDt("PARAM3val2"));
String inParamsStr = ourCtx.newXmlParser().encodeResourceToString(p);
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/$OP_SERVER_LIST_PARAM");
httpPost.setEntity(new StringEntity(inParamsStr, ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
HttpResponse status = ourClient.execute(httpPost);
assertEquals(200, status.getStatusLine().getStatusCode());
String response = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals("$OP_SERVER_LIST_PARAM", ourLastMethod);
assertEquals(true, ourLastParam2.getActive().booleanValue());
assertEquals(null, ourLastParam1);
assertEquals(2, ourLastParam3.size());
assertEquals("PARAM3val1", ourLastParam3.get(0).getValue());
assertEquals("PARAM3val2", ourLastParam3.get(1).getValue());
Parameters resp = ourCtx.newXmlParser().parseResource(Parameters.class, response);
assertEquals("RET1", resp.getParameter().get(0).getName());
}
@Test
public void testOperationOnInstance() throws Exception {
Parameters p = new Parameters();
p.addParameter().setName("PARAM1").setValue(new StringDt("PARAM1val"));
p.addParameter().setName("PARAM2").setResource(new Patient().setActive(true));
String inParamsStr = ourCtx.newXmlParser().encodeResourceToString(p);
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/123/$OP_INSTANCE");
httpPost.setEntity(new StringEntity(inParamsStr, ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
HttpResponse status = ourClient.execute(httpPost);
assertEquals(200, status.getStatusLine().getStatusCode());
String response = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals("PARAM1val", ourLastParam1.getValue());
assertEquals(true, ourLastParam2.getActive().booleanValue());
assertEquals("123", ourLastId.getIdPart());
assertEquals("$OP_INSTANCE", ourLastMethod);
Parameters resp = ourCtx.newXmlParser().parseResource(Parameters.class, response);
assertEquals("RET1", resp.getParameter().get(0).getName());
}
@Test
public void testOperationWrongParamType() throws Exception {
Parameters p = new Parameters();
p.addParameter().setName("PARAM1").setValue(new IntegerDt("123"));
p.addParameter().setName("PARAM2").setResource(new Patient().setActive(true));
String inParamsStr = ourCtx.newXmlParser().encodeResourceToString(p);
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/$OP_TYPE");
httpPost.setEntity(new StringEntity(inParamsStr, ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
HttpResponse status = ourClient.execute(httpPost);
assertEquals(400, status.getStatusLine().getStatusCode());
String response = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
ourLog.info(status.getStatusLine().toString());
ourLog.info(response);
assertThat(response, containsString("Request has parameter PARAM1 of type IntegerDt but method expects type StringDt"));
}
@AfterClass
public static void afterClass() throws Exception {
ourServer.stop();
}
@BeforeClass
public static void beforeClass() throws Exception {
ourCtx = FhirContext.forDstu2();
ourPort = PortUtil.findFreePort();
ourServer = new Server(ourPort);
ServletHandler proxyHandler = new ServletHandler();
RestfulServer servlet = new RestfulServer();
servlet.setFhirContext(ourCtx);
servlet.setResourceProviders(new PatientProvider());
servlet.setPlainProviders(new PlainProvider());
ServletHolder servletHolder = new ServletHolder(servlet);
proxyHandler.addServletWithMapping(servletHolder, "/*");
ourServer.setHandler(proxyHandler);
ourServer.start();
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS);
HttpClientBuilder builder = HttpClientBuilder.create();
builder.setConnectionManager(connectionManager);
ourClient = builder.build();
}
public static class PlainProvider {
//@formatter:off
@Operation(name="$OP_SERVER")
public Parameters opServer(
@OperationParam(name="PARAM1") StringDt theParam1,
@OperationParam(name="PARAM2") Patient theParam2
) {
//@formatter:on
ourLastMethod = "$OP_SERVER";
ourLastParam1 = theParam1;
ourLastParam2 = theParam2;
Parameters retVal = new Parameters();
retVal.addParameter().setName("RET1").setValue(new StringDt("RETVAL1"));
return retVal;
}
//@formatter:off
@Operation(name="$OP_SERVER_LIST_PARAM")
public Parameters opServerListParam(
@OperationParam(name="PARAM2") Patient theParam2,
@OperationParam(name="PARAM3") List<StringDt> theParam3
) {
//@formatter:on
ourLastMethod = "$OP_SERVER_LIST_PARAM";
ourLastParam2 = theParam2;
ourLastParam3 = theParam3;
Parameters retVal = new Parameters();
retVal.addParameter().setName("RET1").setValue(new StringDt("RETVAL1"));
return retVal;
}
}
public static class PatientProvider implements IResourceProvider {
@Override
public Class<? extends IResource> getResourceType() {
return Patient.class;
}
//@formatter:off
@Operation(name="$OP_TYPE")
public Parameters opType(
@OperationParam(name="PARAM1") StringDt theParam1,
@OperationParam(name="PARAM2") Patient theParam2
) {
//@formatter:on
ourLastMethod = "$OP_TYPE";
ourLastParam1 = theParam1;
ourLastParam2 = theParam2;
Parameters retVal = new Parameters();
retVal.addParameter().setName("RET1").setValue(new StringDt("RETVAL1"));
return retVal;
}
//@formatter:off
@Operation(name="$OP_TYPE_RET_BUNDLE")
public Bundle opTypeRetBundle(
@OperationParam(name="PARAM1") StringDt theParam1,
@OperationParam(name="PARAM2") Patient theParam2
) {
//@formatter:on
ourLastMethod = "$OP_TYPE_RET_BUNDLE";
ourLastParam1 = theParam1;
ourLastParam2 = theParam2;
Bundle retVal = new Bundle();
retVal.addEntry().getTransactionResponse().setStatus("100");
return retVal;
}
//@formatter:off
@Operation(name="$OP_INSTANCE")
public Parameters opInstance(
@IdParam IdDt theId,
@OperationParam(name="PARAM1") StringDt theParam1,
@OperationParam(name="PARAM2") Patient theParam2
) {
//@formatter:on
ourLastMethod = "$OP_INSTANCE";
ourLastId = theId;
ourLastParam1 = theParam1;
ourLastParam2 = theParam2;
Parameters retVal = new Parameters();
retVal.addParameter().setName("RET1").setValue(new StringDt("RETVAL1"));
return retVal;
}
}
}

View File

@ -0,0 +1,50 @@
package ca.uhn.fhir.rest.server;
import static org.junit.Assert.*;
import javax.servlet.ServletException;
import org.hamcrest.core.StringContains;
import org.hl7.fhir.instance.model.IBaseResource;
import org.junit.Test;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
public class ServerInvalidDefinitionDstu2Test {
private static FhirContext ourCtx = FhirContext.forDstu2();
@Test
public void testOperationReturningOldBundleProvider() {
RestfulServer srv = new RestfulServer();
srv.setFhirContext(ourCtx);
srv.setResourceProviders(new OperationReturningOldBundleProvider());
try {
srv.init();
fail();
} catch (ServletException e) {
assertThat(e.getCause().toString(), StringContains.containsString("ConfigurationException"));
assertThat(e.getCause().toString(), StringContains.containsString("Can not return a DSTU1 bundle"));
}
}
public static class OperationReturningOldBundleProvider implements IResourceProvider {
@Override
public Class<? extends IBaseResource> getResourceType() {
return Patient.class;
}
@Operation(name = "$OP_TYPE_RET_OLD_BUNDLE")
public ca.uhn.fhir.model.api.Bundle opTypeRetOldBundle(@OperationParam(name = "PARAM1") StringDt theParam1, @OperationParam(name = "PARAM2") Patient theParam2) {
return null;
}
}
}

View File

@ -35,7 +35,7 @@ import ca.uhn.fhir.model.dstu2.resource.Organization;
import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.rest.annotation.ConditionalOperationParam;
import ca.uhn.fhir.rest.annotation.ConditionalUrlParam;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.OptionalParam;
import ca.uhn.fhir.rest.annotation.ResourceParam;
@ -185,7 +185,7 @@ public class UpdateConditionalTest {
}
@Update()
public MethodOutcome updatePatient(@ResourceParam Patient thePatient, @ConditionalOperationParam String theConditional, @IdParam IdDt theIdParam) {
public MethodOutcome updatePatient(@ResourceParam Patient thePatient, @ConditionalUrlParam String theConditional, @IdParam IdDt theIdParam) {
ourLastConditionalUrl = theConditional;
ourLastId = thePatient.getId();
ourLastIdParam = theIdParam;

View File

@ -130,6 +130,11 @@
<action type="add">
Add support for quantity search params in FHIR tester UI
</action>
<action type="add">
Add support for FHIR "extended operations" as defined in the FHIR DSTU2
specification, for the Generic Client, Annotation Client, and
Server.
</action>
</release>
<release version="0.8" date="2014-Dec-17">
<action type="add">

View File

@ -82,7 +82,8 @@
<item name="Validation" href="./doc_validation.html" />
</item>
<item name="RESTful Client" href="./doc_rest_client.html" >
<item name="Using RESTful Client" href="./doc_rest_client.html" />
<item name="Fluent/Generic Client" href="./doc_rest_client.html" />
<item name="Annotation Client" href="./doc_rest_client_annotation.html" />
<item name="Interceptors (client)" href="./doc_rest_client_interceptor.html"/>
</item>
<item name="RESTful Server" href="./doc_rest_server.html" >

View File

@ -21,17 +21,22 @@
</p>
<p>
There are two types of clients provided by HAPI: Generic and Annotation-driven.
The generic client (introduced in HAPI-FHIR 0.3) is much simpler to create
There are two types of RESTful clients provided by HAPI:
The Fluent/Generic client (described below) and
the <a href="./doc_rest_client_annotation.html">Annotation</a>
client.
The generic client is simpler to use
and generally provides the faster way to get started. The annotation-driven
client can rely on code generation and static binding to specific operations to
client relies on static binding to specific operations to
give better compile-time checking against servers with a specific set of capabilities
exposed.
exposed. This second model takes more effort to use, but can be useful
if the person defining the specific methods to invoke is not the same person
who is using those methods.
</p>
</section>
<section name="The Generic Client">
<section name="The Fluent/Generic Client">
<p>
Creating a generic client simply requires you to create an instance of
@ -56,7 +61,7 @@
(although there is no requirement to do so, clients are reusable and thread-safe as well).
</p>
<subsection name="Fluent Operations">
<subsection name="Fluent Calls">
<p>
The generic client supports queries using a fluent interface
which is inspired by the fantastic
@ -373,190 +378,30 @@
value="examples/src/main/java/example/GenericClientExample.java" />
</macro>
</subsection>
</section>
<section name="The Annotation-Driven Client">
<section name="Extended Operations">
<p>
HAPI also provides a second style of client, called the <b>annotation-driven</b> client.
</p>
<p>
The design of the annotation-driven client
is intended to be similar to that of
JAX-WS, so users of that
specification should be comfortable with
this one. It uses a user-defined interface containing special
annotated methods which HAPI binds to calls against a server.
</p>
<p>
The annotation-driven client is particularly useful if you have a server that
exposes a set of specific operations (search parameter combinations, named queries, etc.)
and you want to let developers have a stongly/statically typed interface to that
server.
In the FHIR DSTU2 version, operations (referred to as "extended operations")
were added. These operations are an RPC style of invocation, with a set of
named input parameters passed to the server and a set of named output
parameters returned back.
</p>
<p>
There is no difference in terms of capability between the two styles of
client. There is simply a difference in programming style and complexity. It
is probably safe to say that the generic client is easier to use and leads to
more readable code, at the expense of not giving any visibility into the
specific capabilities of the server you are interacting with.
To invoke an operation using the client, you simply need to create the
input
<a href="./apidocs-dstu2/ca/uhn/fhir/model/dstu2/resource/Parameters.html">Parameters</a>
resource, then pass that to the <code>operation()</code> fluent method.
</p>
<subsection name="Defining A Restful Client Interface">
<p>
The first step in creating an annotation-driven client is to define a
restful client interface.
</p>
<p>
A restful client interface class must extend the
<a href="./apidocs/ca/uhn/fhir/rest/client/api/IRestfulClient.html">IRestfulClient</a>
interface,
and will contain one or more methods which have been
annotated with special annotations indicating which RESTful
operation
that method supports. Below is a simple example of a
resource provider
which supports the
<a href="http://hl7.org/implement/standards/fhir/http.html#read">read</a>
operation (i.e. retrieve a single resource by ID) as well as the
<a href="http://hl7.org/implement/standards/fhir/http.html#search">search</a>
operation (i.e. find any resources matching a given criteria) for a
specific
search criteria.
</p>
<p>
You may notice that this interface looks a lot like the Resource
Provider
which is defined for use by the RESTful server. In fact, it
supports all
of the same annotations and is essentially identical,
other than the
fact that for a client you must use an interface but for a server you
must use a concrete class with method implementations.
</p>
<macro name="snippet">
<param name="id" value="provider" />
<param name="file"
value="examples/src/main/java/example/IRestfulClient.java" />
</macro>
<p>
You will probably want to add more methods
to your client interface.
See
<a href="./doc_rest_operations.html">RESTful Operations</a>
for
lots more examples of how to add methods for various operations.
</p>
</subsection>
<subsection name="Instantiate the Client">
<p>
Once your client interface is created, all that is left is to
create a FhirContext and instantiate the client and you are
ready to
start using it.
</p>
<macro name="snippet">
<param name="id" value="client" />
<param name="file"
value="examples/src/main/java/example/ExampleRestfulClient.java" />
</macro>
</subsection>
<subsection name="Configuring Encoding (JSON/XML)">
<p>
Restful client interfaces that you create will also extend
the interface
<a href="./apidocs/ca/uhn/fhir/rest/client/api/IRestfulClient.html">IRestfulClient</a>,
which comes with some helpful methods for configuring the way that
the client will interact with the server.
</p>
<p>
The following snippet shows how to configure the cliet to explicitly
request JSON or XML responses, and how to request "pretty printed" responses
on servers that support this (HAPI based servers currently).
</p>
<macro name="snippet">
<param name="id" value="clientConfig" />
<param name="file" value="examples/src/main/java/example/ClientExamples.java" />
</macro>
</subsection>
<subsection name="A Complete Example">
<p>
The following is a complete example showing a RESTful client
using
HAPI FHIR.
</p>
<macro name="snippet">
<param name="id" value="client" />
<param name="file"
value="examples/src/main/java/example/CompleteExampleClient.java" />
</macro>
</subsection>
</section>
<section name="Configuring the HTTP Client">
<p>
RESTful clients (both Generic and Annotation-Driven) use
<a href="http://hc.apache.org/httpcomponents-client-ga/">Apache HTTP Client</a>
as a provider. The Apache HTTP Client is very powerful and extremely flexible,
but can be confusing at first to configure, because of the low-level approach that
the library uses.
The example below shows a simple operation call.
</p>
<p>
In many cases, the default configuration should suffice. However, if you require anything
more sophisticated (username/password, HTTP proxy settings, etc.) you will need
to configure the underlying client.
</p>
<p>
The underlying client configuration is provided by accessing the
<a href="./apidocs/ca/uhn/fhir/rest/client/IRestfulClientFactory.html">IRestfulClientFactory</a>
class from the FhirContext.
</p>
<p>
Note that individual requests and responses
can be tweaked using <a href="./doc_rest_client_interceptor.html">Client Interceptors</a>.
</p>
<subsection name="Configuring an HTTP Proxy">
<p>
The following example shows how to configure the use of an HTTP
proxy in the client.
</p>
<macro name="snippet">
<param name="id" value="proxy" />
<param name="file" value="examples/src/main/java/example/ClientExamples.java" />
</macro>
</subsection>
</section>
<macro name="snippet">
<param name="id" value="operation" />
<param name="file"
value="examples/src/main/java/example/GenericClientExample.java" />
</macro>
</section>
</body>

View File

@ -0,0 +1,199 @@
<?xml version="1.0" encoding="UTF-8"?>
<document xmlns="http://maven.apache.org/XDOC/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
<properties>
<title>Annotation Client</title>
<author email="jamesagnew@users.sourceforge.net">James Agnew</author>
</properties>
<body>
<section name="The Annotation-Driven Client">
<p>
HAPI also provides a second style of client, called the <b>annotation-driven</b> client.
If you are using the
<a href="./doc_rest_client.html">Fluent/Generic Client</a>
do not need to read this page.
</p>
<p>
The design of the annotation-driven client
is intended to be similar to that of
JAX-WS, so users of that
specification should be comfortable with
this one. It uses a user-defined interface containing special
annotated methods which HAPI binds to calls against a server.
</p>
<p>
The annotation-driven client is particularly useful if you have a server that
exposes a set of specific operations (search parameter combinations, named queries, etc.)
and you want to let developers have a stongly/statically typed interface to that
server.
</p>
<p>
There is no difference in terms of capability between the two styles of
client. There is simply a difference in programming style and complexity. It
is probably safe to say that the generic client is easier to use and leads to
more readable code, at the expense of not giving any visibility into the
specific capabilities of the server you are interacting with.
</p>
<subsection name="Defining A Restful Client Interface">
<p>
The first step in creating an annotation-driven client is to define a
restful client interface.
</p>
<p>
A restful client interface class must extend the
<a href="./apidocs/ca/uhn/fhir/rest/client/api/IRestfulClient.html">IRestfulClient</a>
interface,
and will contain one or more methods which have been
annotated with special annotations indicating which RESTful
operation
that method supports. Below is a simple example of a
resource provider
which supports the
<a href="http://hl7.org/implement/standards/fhir/http.html#read">read</a>
operation (i.e. retrieve a single resource by ID) as well as the
<a href="http://hl7.org/implement/standards/fhir/http.html#search">search</a>
operation (i.e. find any resources matching a given criteria) for a
specific
search criteria.
</p>
<p>
You may notice that this interface looks a lot like the Resource
Provider
which is defined for use by the RESTful server. In fact, it
supports all
of the same annotations and is essentially identical,
other than the
fact that for a client you must use an interface but for a server you
must use a concrete class with method implementations.
</p>
<macro name="snippet">
<param name="id" value="provider" />
<param name="file"
value="examples/src/main/java/example/IRestfulClient.java" />
</macro>
<p>
You will probably want to add more methods
to your client interface.
See
<a href="./doc_rest_operations.html">RESTful Operations</a>
for
lots more examples of how to add methods for various operations.
</p>
</subsection>
<subsection name="Instantiate the Client">
<p>
Once your client interface is created, all that is left is to
create a FhirContext and instantiate the client and you are
ready to
start using it.
</p>
<macro name="snippet">
<param name="id" value="client" />
<param name="file"
value="examples/src/main/java/example/ExampleRestfulClient.java" />
</macro>
</subsection>
<subsection name="Configuring Encoding (JSON/XML)">
<p>
Restful client interfaces that you create will also extend
the interface
<a href="./apidocs/ca/uhn/fhir/rest/client/api/IRestfulClient.html">IRestfulClient</a>,
which comes with some helpful methods for configuring the way that
the client will interact with the server.
</p>
<p>
The following snippet shows how to configure the cliet to explicitly
request JSON or XML responses, and how to request "pretty printed" responses
on servers that support this (HAPI based servers currently).
</p>
<macro name="snippet">
<param name="id" value="clientConfig" />
<param name="file" value="examples/src/main/java/example/ClientExamples.java" />
</macro>
</subsection>
<subsection name="A Complete Example">
<p>
The following is a complete example showing a RESTful client
using
HAPI FHIR.
</p>
<macro name="snippet">
<param name="id" value="client" />
<param name="file"
value="examples/src/main/java/example/CompleteExampleClient.java" />
</macro>
</subsection>
</section>
<section name="Configuring the HTTP Client">
<p>
RESTful clients (both Generic and Annotation-Driven) use
<a href="http://hc.apache.org/httpcomponents-client-ga/">Apache HTTP Client</a>
as a provider. The Apache HTTP Client is very powerful and extremely flexible,
but can be confusing at first to configure, because of the low-level approach that
the library uses.
</p>
<p>
In many cases, the default configuration should suffice. However, if you require anything
more sophisticated (username/password, HTTP proxy settings, etc.) you will need
to configure the underlying client.
</p>
<p>
The underlying client configuration is provided by accessing the
<a href="./apidocs/ca/uhn/fhir/rest/client/IRestfulClientFactory.html">IRestfulClientFactory</a>
class from the FhirContext.
</p>
<p>
Note that individual requests and responses
can be tweaked using <a href="./doc_rest_client_interceptor.html">Client Interceptors</a>.
</p>
<subsection name="Configuring an HTTP Proxy">
<p>
The following example shows how to configure the use of an HTTP
proxy in the client.
</p>
<macro name="snippet">
<param name="id" value="proxy" />
<param name="file" value="examples/src/main/java/example/ClientExamples.java" />
</macro>
</subsection>
</section>
</body>
</document>

View File

@ -1,190 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<document xmlns="http://maven.apache.org/XDOC/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
<properties>
<title>RESTful Operations - HAPI FHIR</title>
<author email="jamesagnew@users.sourceforge.net">James Agnew</author>
</properties>
<body>
<section name="Implementing Resource Provider Operations">
<!--
<p>
Jump To...
</p>
<ul>
<li>
<a href="#operations">Operations</a>
</li>
<li>
<a href="#exceptions">Exceptions</a>
</li>
<li>
<a href="#tags">Tags</a>
</li>
<li>
<a href="#compartments">Compartments</a>
</li>
</ul>
<p>
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
to actually provide those resources.
</p>
<p>
Unless otherwise specified, the examples below show
<b>server</b>
implementations, but client methods will follow the same patterns.
</p>
-->
</section>
<section name="Operations">
<a name="operations" />
<p>
The following table lists the operations supported by
HAPI FHIR RESTful Servers and Clients.
This page shows the operations which can be implemented on
HAPI
<a href="./doc_rest_server.html">RESTful Servers</a>, as well as
<a href="./doc_rest_client_annotation.html">Annotation Clients</a>.
Most of the examples shown here show how to implement a server
method, but to perform an equivalent call on an annotation
client you simply put a method with the same signature in your
client interface.
</p>
<!--
<table>
<thead>
<tr style="font-weight: bold; font-size: 1.2em;">
<td>Operation</td>
<td>Definition</td>
</tr>
</thead>
<tbody>
<tr>
<td>
<a href="#instance_read">Instance - Read</a>
</td>
<td>
Read the current state of the resource
</td>
</tr>
<tr>
<td>
<a href="#instance_vread">Instance - VRead</a>
</td>
<td>
Read the state of a specific version of the resource
</td>
</tr>
<tr>
<td>
<a href="#instance_update">Instance - Update</a>
</td>
<td>
Updates the resource on the server
</td>
</tr>
<tr>
<td>
<a href="#instance_delete">Instance - Delete</a>
</td>
<td>
Delete a resource
</td>
</tr>
<tr>
<td>
<a href="#history">Instance - History</a>
</td>
<td>
Retrieve the update history for a particular resource
</td>
</tr>
<tr>
<td>
<a href="#type_create">Type - Create</a>
</td>
<td>
Create a new resource with a server assigned id
</td>
</tr>
<tr>
<td>
<a href="#type_search">Type - Search</a>
<macro name="toc" >
<param name="section" value="8" />
<param name="fromDepth" value="2" />
</macro>
</td>
<td>
Search the resource type based on some filter criteria
</td>
</tr>
<tr>
<td>
<a href="#history">Type - History</a>
</td>
<td>
Retrieve the update history for a particular resource type
</td>
</tr>
<tr>
<td>
<a href="#type_validate">Type - Validate</a>
</td>
<td>
Check that the content would be acceptable as an update
</td>
</tr>
<tr>
<td>
<a href="#system_conformance">System - Conformance</a>
</td>
<td>
Get a conformance statement for the system
</td>
</tr>
<tr>
<td>
<a href="#system_transaction">System - Transaction</a>
</td>
<td>
Update, create or delete a set of resources as a single transaction
</td>
</tr>
<tr>
<td>
<a href="#history">System - History</a>
</td>
<td>
Retrieve the update history for all resources
</td>
</tr>
<tr>
<td>
<a href="#system_search">System - Search</a>
</td>
<td>
Search across all resource types based on some filter criteria
</td>
</tr>
<tr>
<td>
<a href="#tags">Tag Operations</a>
</td>
<td>
Search across all resource types based on some filter criteria
</td>
</tr>
</tbody>
</table>
-->
<a name="instance_read" />
</section>
@ -1632,7 +1468,114 @@
</p>
</section>
<section name="Extended Operations">
<p>
FHIR extended operations are a special type of RPC-style invocation you
can perform against a FHIR server, type, or resource instance. These invocations
take a
<a href="./apidocs-dstu2/ca/uhn/fhir/model/dstu2/resource/Parameters.html">Parameters</a>
resource as input, and return either another Parameters resource or a different resource type.
</p>
<p>
To define an operation, a method should be placed in a
<a href="./doc_rest_server.html#resource_providers">Resource Provider</a>
class if the operation works against a resource type (e.g. <code>Patient</code>)
or a resource instance (e.g. <code>Patient/123</code>), or on a
Plain Provider
if the operation works against the server (i.e. it is global and not resource specific).
</p>
<subsection name="Type-Specific Operations">
<p>
To implement a type-specific operation,
the method should be annotated with the
<code>@Operation</code> tag, and should have an
<code>@OperationParam</code> tag for each named parameter that
the input Parameters resource may be populated with. The following
example shows how to implement the <code>Patient/$everything</code>
method, defined in the FHIR specification.
</p>
<macro name="snippet">
<param name="id" value="patientTypeOperation" />
<param name="file"
value="examples/src/main/java/example/ServerOperations.java" />
</macro>
<p>
Example URL to invoke this operation (HTTP request body is Parameters resource):
<br />
<code>POST http://fhir.example.com/Patient/$everything</code>
</p>
</subsection>
<subsection name="Instance-Specific Operations">
<p>
To create an instance-specific operation (an operation which takes the
ID of a specific resource instance as a part of its request URL),
you can add a parameter annotated with the <code>@IdParam</code> annotation,
of type <code>IdDt</code>. The following example show how to implement the
<code>Patient/[id]/$everything</code> operation.
</p>
<macro name="snippet">
<param name="id" value="patientInstanceOperation" />
<param name="file"
value="examples/src/main/java/example/ServerOperations.java" />
</macro>
<p>
Example URL to invoke this operation (HTTP request body is Parameters resource):
<br />
<code>http://fhir.example.com/Patient/123/$everything</code>
</p>
</subsection>
<subsection name="Server Operations">
<p>
Server operations do not operate on a specific resource type or
instance, but rather operate globally on the server itself. The following
example show how to implement the
<code>$closure</code> operation. Note that the <code>concept</code> parameter
in the example has a cardinality of <code>0..*</code> according to the
FHIR specification, so a <code>List&lt;Coding&gt;</code>
is used as the parameter type.
</p>
<macro name="snippet">
<param name="id" value="serverOperation" />
<param name="file"
value="examples/src/main/java/example/ServerOperations.java" />
</macro>
<p>
Example URL to invoke this operation (HTTP request body is Parameters resource):
<br />
<code>http://fhir.example.com/$closure</code>
</p>
</subsection>
<subsection name="Returning Multiple OUT Parameters">
<p>
In all of the Extended Operation examples above, the return
type specified for the operation is a single Resource instance. This is
a common pattern in FHIR defined operations. However, it is also
possible for an extended operation to be defined with multiple
and/or repeating OUT parameters. In this case, you can return
a <code>Parameters</code> resource directly.
</p>
</subsection>
</section>
</body>
</document>

View File

@ -31,7 +31,8 @@
<h4>RESTful Client</h4>
<ul>
<li><a href="./doc_rest_client.html">Using RESTful Client</a></li>
<li><a href="./doc_rest_client.html">Fluent/Generic Client</a></li>
<li><a href="./doc_rest_client_annotation.html">Annotation Client</a></li>
<li><a href="./doc_rest_client_interceptor.html">Interceptors (client)</a></li>
</ul>