From e69464f34d54145306a19f8cae551f865e625234 Mon Sep 17 00:00:00 2001 From: jamesagnew Date: Sun, 27 Apr 2014 22:17:27 -0400 Subject: [PATCH] Lots of documentation (site and javadoc) updates, and make since and count params work for history operation --- hapi-fhir-base/pom.xml | 2 + .../BaseRuntimeDeclaredChildDefinition.java | 2 +- .../java/ca/uhn/fhir/context/FhirContext.java | 4 + .../ca/uhn/fhir/model/api/BasePrimitive.java | 11 +- .../uhn/fhir/model/api/annotation/Child.java | 2 - .../uhn/fhir/model/primitive/InstantDt.java | 10 + .../ca/uhn/fhir/rest/annotation/Read.java | 18 +- .../ca/uhn/fhir/rest/annotation/Search.java | 20 +- .../ca/uhn/fhir/rest/annotation/Since.java | 2 +- .../fhir/rest/method/BaseMethodBinding.java | 217 +++++++++--------- .../BaseOutcomeReturningMethodBinding.java | 15 +- ...turningMethodBindingWithResourceParam.java | 4 +- .../BaseResourceReturningMethodBinding.java | 34 ++- .../rest/method/HistoryMethodBinding.java | 194 +++++++--------- .../fhir/rest/method/SearchMethodBinding.java | 24 +- .../java/ca/uhn/fhir/rest/method/Util.java | 14 +- .../fhir/rest/param/BaseQueryParameter.java | 6 + .../uhn/fhir/rest/param/CountParameter.java | 83 +++++++ .../ca/uhn/fhir/rest/param/IParameter.java | 4 + .../uhn/fhir/rest/param/IncludeParameter.java | 2 + .../ca/uhn/fhir/rest/param/NullParameter.java | 50 ++++ .../ca/uhn/fhir/rest/param/ParameterUtil.java | 166 ++++++++++---- .../fhir/rest/param/ResourceParameter.java | 7 +- .../fhir/rest/param/ServerBaseParameter.java | 9 +- .../rest/param/ServletRequestParameter.java | 7 + .../rest/param/ServletResponseParameter.java | 8 + .../uhn/fhir/rest/param/SinceParameter.java | 83 +++++++ .../ConfigurationException.java | 2 +- .../uhn/fhir/rest/server/RestfulServer.java | 83 +++++-- .../exceptions/AuthenticationException.java | 8 +- .../exceptions/InternalErrorException.java | 3 + .../exceptions/MethodNotAllowedException.java | 2 +- .../exceptions/MethodNotFoundException.java | 32 --- .../exceptions/ResourceNotFoundException.java | 4 + .../ResourceVersionConflictException.java | 6 +- .../ResourceVersionNotSpecifiedException.java | 4 +- .../UnclassifiedServerFailureException.java | 4 +- .../java/example/ExampleProviders.java | 59 +++++ .../java/example/ExampleRestfulServlet.java | 24 +- .../java/example/FhirContextIntro.java | 16 ++ .../RestfulPatientResourceProviderMore.java | 34 ++- hapi-fhir-base/src/site/xdoc/doc_intro.xml | 23 ++ .../src/site/xdoc/doc_rest_operations.xml | 82 +++++-- .../src/site/xdoc/doc_rest_server.xml | 71 ++++++ .../ca/uhn/fhir/rest/client/ClientTest.java | 52 ++++- .../ca/uhn/fhir/rest/client/ITestClient.java | 19 +- .../server/NonResourceProviderServerTest.java | 176 +++++++++++--- restful-server-example/.classpath | 6 +- .../rest/RestfulPatientResourceProvider.java | 2 - 49 files changed, 1225 insertions(+), 485 deletions(-) create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/CountParameter.java create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/NullParameter.java create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/SinceParameter.java rename hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/{exceptions => }/ConfigurationException.java (95%) delete mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/MethodNotFoundException.java create mode 100644 hapi-fhir-base/src/site/example/java/example/ExampleProviders.java diff --git a/hapi-fhir-base/pom.xml b/hapi-fhir-base/pom.xml index 92cc3122446..182418e0abb 100644 --- a/hapi-fhir-base/pom.xml +++ b/hapi-fhir-base/pom.xml @@ -248,11 +248,13 @@ maven-jxr-plugin 2.4 + org.apache.maven.plugins maven-project-info-reports-plugin diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/BaseRuntimeDeclaredChildDefinition.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/BaseRuntimeDeclaredChildDefinition.java index cbe3d821c60..6bf5e0089fa 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/BaseRuntimeDeclaredChildDefinition.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/BaseRuntimeDeclaredChildDefinition.java @@ -160,7 +160,7 @@ public abstract class BaseRuntimeDeclaredChildDefinition extends BaseRuntimeChil } } - private final class ListAccessor implements IAccessor { + private final static class ListAccessor implements IAccessor { private final Method myAccessorMethod; private ListAccessor(Method theAccessor) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java index fa6a7569035..7e15ad5811d 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java @@ -27,6 +27,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.apache.commons.lang3.Validate; + import ca.uhn.fhir.model.api.IElement; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.dstu.resource.Patient; @@ -117,6 +119,8 @@ public class FhirContext { */ @SuppressWarnings("unchecked") public RuntimeResourceDefinition getResourceDefinition(String theResourceName) { + Validate.notBlank(theResourceName, "Resource name must not be blank"); + RuntimeResourceDefinition retVal = myNameToElementDefinition.get(theResourceName); if (retVal == null) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/BasePrimitive.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/BasePrimitive.java index e97b14362eb..df92ba4cb46 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/BasePrimitive.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/BasePrimitive.java @@ -41,13 +41,16 @@ public abstract class BasePrimitive extends BaseElement implements IPrimitive } @Override - public boolean equals(Object obj) { - if (!(obj.getClass() == getClass())) { + public boolean equals(Object theObj) { + if (theObj == null) { + return false; + } + if (!(theObj.getClass() == getClass())) { return false; } - BasePrimitive o = (BasePrimitive)obj; - + BasePrimitive o = (BasePrimitive) theObj; + EqualsBuilder b = new EqualsBuilder(); b.append(getValue(), o.getValue()); return b.isEquals(); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/annotation/Child.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/annotation/Child.java index 142d2f048bc..934e6e7e5df 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/annotation/Child.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/annotation/Child.java @@ -26,8 +26,6 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import ca.uhn.fhir.model.api.IElement; -import ca.uhn.fhir.model.dstu.composite.HumanNameDt; -import ca.uhn.fhir.model.dstu.resource.Patient; @Retention(RetentionPolicy.RUNTIME) @Target(value= {ElementType.FIELD}) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/InstantDt.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/InstantDt.java index 1904b933265..3a3ff5cac3c 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/InstantDt.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/InstantDt.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.model.primitive; * #L% */ +import java.util.Calendar; import java.util.Date; import java.util.TimeZone; @@ -67,6 +68,15 @@ public class InstantDt extends BaseDateTimeDt { setTimeZone(TimeZone.getDefault()); } + /** + * Create a new DateTimeDt + */ + public InstantDt(Calendar theCalendar) { + setValue(theCalendar.getTime()); + setPrecision(DEFAULT_PRECISION); + setTimeZone(theCalendar.getTimeZone()); + } + /** * Create a new InstantDt from a string value * diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Read.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Read.java index e3500a83745..8cc6780946e 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Read.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Read.java @@ -26,16 +26,26 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import ca.uhn.fhir.model.api.IResource; +import ca.uhn.fhir.rest.client.api.IBasicClient; +import ca.uhn.fhir.rest.client.api.IRestfulClient; +import ca.uhn.fhir.rest.server.IResourceProvider; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Read { /** - * The return type for this search method. This generally does not need - * to be populated for a server implementation, since servers will return - * only one resource per class, but generally does need to be populated - * for client implementations. + * The return type for this method. This generally does not need + * to be populated for {@link IResourceProvider resource providers} in a server implementation, + * but often does need to be populated in client implementations using {@link IBasicClient} or + * {@link IRestfulClient}, or in plain providers on a server. + *

+ * This value also does not need to be populated if the return type for a method annotated with + * this annotation is sufficient to determine the type of resource provided. E.g. if the + * method returns Patient or List<Patient>, the server/client + * will automatically determine that the Patient resource is the return type, and this value + * may be left blank. + *

*/ // NB: Read, Search (maybe others) share this annotation, so update the javadocs everywhere Class type() default IResource.class; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Search.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Search.java index 6f7f0c0ea4d..a91a9075a9a 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Search.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Search.java @@ -26,6 +26,9 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import ca.uhn.fhir.model.api.IResource; +import ca.uhn.fhir.rest.client.api.IBasicClient; +import ca.uhn.fhir.rest.client.api.IRestfulClient; +import ca.uhn.fhir.rest.server.IResourceProvider; /** @@ -49,12 +52,19 @@ public @interface Search { String queryName() default ""; /** - * The return type for this search method. This generally does not need - * to be populated for a server implementation, since servers will return - * only one resource per class, but generally does need to be populated - * for client implementations. + * The return type for this method. This generally does not need + * to be populated for {@link IResourceProvider resource providers} in a server implementation, + * but often does need to be populated in client implementations using {@link IBasicClient} or + * {@link IRestfulClient}, or in plain providers on a server. + *

+ * This value also does not need to be populated if the return type for a method annotated with + * this annotation is sufficient to determine the type of resource provided. E.g. if the + * method returns Patient or List<Patient>, the server/client + * will automatically determine that the Patient resource is the return type, and this value + * may be left blank. + *

*/ - // NB: Read, Search (maybe others) share this annotation, so update the javadocs everywhere + // NB: Read, Search (maybe others) share this annotation method, so update the javadocs everywhere Class type() default IResource.class; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Since.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Since.java index 9ecc526c256..37d02e51e96 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Since.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Since.java @@ -27,7 +27,7 @@ import java.lang.annotation.Target; /** * Parameter annotation for the _since parameter, which indicates to the - * server that only results dated since the given instant will be returned. + * server that only results dated since the given instant will be returned. * * @see History */ diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseMethodBinding.java index 281a3845491..5129d8822f7 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseMethodBinding.java @@ -55,6 +55,8 @@ import ca.uhn.fhir.rest.annotation.Validate; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.client.BaseClientInvocation; import ca.uhn.fhir.rest.client.exceptions.NonFhirResponseException; +import ca.uhn.fhir.rest.param.IParameter; +import ca.uhn.fhir.rest.param.ParameterUtil; import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.EncodingUtil; import ca.uhn.fhir.rest.server.IResourceProvider; @@ -65,8 +67,9 @@ import ca.uhn.fhir.util.ReflectionUtil; public abstract class BaseMethodBinding { - private Method myMethod; private FhirContext myContext; + private Method myMethod; + private List myParameters; private Object myProvider; public BaseMethodBinding(Method theMethod, FhirContext theConetxt, Object theProvider) { @@ -76,31 +79,7 @@ public abstract class BaseMethodBinding { myMethod = theMethod; myContext = theConetxt; myProvider = theProvider; - } - - /** - * Returns the name of the resource this method handles, or null if this - * method is not resource specific - */ - public abstract String getResourceName(); - - protected Object invokeServerMethod(Object theResourceProvider, Object[] theMethodParams) { - try { - return getMethod().invoke(theResourceProvider, theMethodParams); - } catch (InvocationTargetException e) { - if (e.getCause() instanceof BaseServerResponseException) { - throw (BaseServerResponseException)e.getCause(); - } else { - throw new InternalErrorException("Failed to call access method", e); - } - } catch (Exception e) { - throw new InternalErrorException("Failed to call access method", e); - } - } - - - public Object getProvider() { - return myProvider; + myParameters = ParameterUtil.getResourceParameters(theMethod); } public FhirContext getContext() { @@ -111,12 +90,26 @@ public abstract class BaseMethodBinding { return myMethod; } - public abstract BaseClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException; + public Object getProvider() { + return myProvider; + } + + /** + * Returns the name of the resource this method handles, or + * null if this method is not resource specific + */ + public abstract String getResourceName(); public abstract RestfulOperationTypeEnum getResourceOperationType(); public abstract RestfulOperationSystemEnum getSystemOperationType(); + public abstract BaseClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException; + + public abstract Object invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) throws IOException, BaseServerResponseException; + + public abstract void invokeServer(RestfulServer theServer, Request theRequest, HttpServletResponse theResponse) throws BaseServerResponseException, IOException; + public abstract boolean matches(Request theRequest); protected IParser createAppropriateParser(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode) throws IOException { @@ -131,6 +124,29 @@ public abstract class BaseMethodBinding { return parser; } + public List getParameters() { + return myParameters; + } + + protected Object invokeServerMethod(Object theResourceProvider, Object[] theMethodParams) { + try { + return getMethod().invoke(theResourceProvider, theMethodParams); + } catch (InvocationTargetException e) { + if (e.getCause() instanceof BaseServerResponseException) { + throw (BaseServerResponseException) e.getCause(); + } else { + throw new InternalErrorException("Failed to call access method", e); + } + } catch (Exception e) { + throw new InternalErrorException("Failed to call access method", e); + } + } + + /** For unit tests only */ + public void setParameters(List theParameters) { + myParameters = theParameters; + } + @SuppressWarnings("unchecked") public static BaseMethodBinding bindMethod(Method theMethod, FhirContext theContext, Object theProvider) { Read read = theMethod.getAnnotation(Read.class); @@ -152,28 +168,27 @@ public abstract class BaseMethodBinding { if (theProvider instanceof IResourceProvider) { returnTypeFromRp = ((IResourceProvider) theProvider).getResourceType(); if (!verifyIsValidResourceReturnType(returnTypeFromRp)) { - throw new ConfigurationException("getResourceType() from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName() + " returned " - + toLogString(returnTypeFromRp) + " - Must return a resource type"); + throw new ConfigurationException("getResourceType() from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName() + " returned " + toLogString(returnTypeFromRp) + " - Must return a resource type"); } } Class returnTypeFromMethod = theMethod.getReturnType(); if (MethodOutcome.class.equals(returnTypeFromMethod)) { // returns a method outcome - }else if (Bundle.class.equals(returnTypeFromMethod)) { + } else if (Bundle.class.equals(returnTypeFromMethod)) { // returns a bundle - }else if (void.class.equals(returnTypeFromMethod)) { + } else if (void.class.equals(returnTypeFromMethod)) { // returns a bundle } else if (Collection.class.isAssignableFrom(returnTypeFromMethod)) { returnTypeFromMethod = ReflectionUtil.getGenericCollectionTypeOfMethodReturnType(theMethod); - if (!verifyIsValidResourceReturnType(returnTypeFromMethod)) { - throw new ConfigurationException("Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName() - + " returns a collection with generic type " + toLogString(returnTypeFromMethod) + " - Must return a resource type or a collection (List, Set) of a resource type"); + if (!verifyIsValidResourceReturnType(returnTypeFromMethod) && !IResource.class.equals(returnTypeFromMethod)) { + throw new ConfigurationException("Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName() + " returns a collection with generic type " + toLogString(returnTypeFromMethod) + + " - Must return a resource type or a collection (List, Set) of a resource type"); } } else { if (!verifyIsValidResourceReturnType(returnTypeFromMethod)) { - throw new ConfigurationException("Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName() - + " returns " + toLogString(returnTypeFromMethod) + " - Must return a resource type"); + throw new ConfigurationException("Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName() + " returns " + toLogString(returnTypeFromMethod) + + " - Must return a resource type"); } } @@ -197,13 +212,12 @@ public abstract class BaseMethodBinding { if (returnTypeFromRp != null) { if (returnTypeFromAnnotation != null && returnTypeFromAnnotation != IResource.class) { if (!returnTypeFromRp.isAssignableFrom(returnTypeFromAnnotation)) { - throw new ConfigurationException("Method '" + theMethod.getName() + "' in type " + theMethod.getDeclaringClass().getCanonicalName() + " returns type " - + returnTypeFromMethod.getCanonicalName() + " - Must return " + returnTypeFromRp.getCanonicalName() + " (or a subclass of it) per IResourceProvider contract"); + throw new ConfigurationException("Method '" + theMethod.getName() + "' in type " + theMethod.getDeclaringClass().getCanonicalName() + " returns type " + returnTypeFromMethod.getCanonicalName() + " - Must return " + returnTypeFromRp.getCanonicalName() + + " (or a subclass of it) per IResourceProvider contract"); } if (!returnTypeFromRp.isAssignableFrom(returnTypeFromAnnotation)) { - throw new ConfigurationException("Method '" + theMethod.getName() + "' in type " + theMethod.getDeclaringClass().getCanonicalName() + " claims to return type " - + returnTypeFromAnnotation.getCanonicalName() + " per method annotation - Must return " + returnTypeFromRp.getCanonicalName() - + " (or a subclass of it) per IResourceProvider contract"); + throw new ConfigurationException("Method '" + theMethod.getName() + "' in type " + theMethod.getDeclaringClass().getCanonicalName() + " claims to return type " + returnTypeFromAnnotation.getCanonicalName() + " per method annotation - Must return " + + returnTypeFromRp.getCanonicalName() + " (or a subclass of it) per IResourceProvider contract"); } returnType = returnTypeFromAnnotation; } else { @@ -212,10 +226,10 @@ public abstract class BaseMethodBinding { } else { if (returnTypeFromAnnotation != IResource.class) { if (!verifyIsValidResourceReturnType(returnTypeFromAnnotation)) { - throw new ConfigurationException("Method '"+theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName() + " returns " - + toLogString(returnTypeFromAnnotation) + " according to annotation - Must return a resource type"); + throw new ConfigurationException("Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName() + " returns " + toLogString(returnTypeFromAnnotation) + + " according to annotation - Must return a resource type"); } - returnType=returnTypeFromAnnotation; + returnType = returnTypeFromAnnotation; } else { returnType = (Class) returnTypeFromMethod; } @@ -264,63 +278,6 @@ public abstract class BaseMethodBinding { // return sm; } - private static boolean verifyIsValidResourceReturnType(Class theReturnType) { - if (theReturnType == null) { - return false; - } - if (!IResource.class.isAssignableFrom(theReturnType)) { - return false; - } - boolean retVal = Modifier.isAbstract(theReturnType.getModifiers()) == false; - return retVal; - } - - private static String toLogString(Class theType) { - if (theType == null) { - return null; - } - return theType.getCanonicalName(); - } - - public static boolean verifyMethodHasZeroOrOneOperationAnnotation(Method theNextMethod, Object... theAnnotations) { - Object obj1 = null; - for (Object object : theAnnotations) { - if (object != null) { - if (obj1 == null) { - obj1 = object; - } else { - throw new ConfigurationException("Method " + theNextMethod.getName() + " on type '" + theNextMethod.getDeclaringClass().getSimpleName() + " has annotations @" - + obj1.getClass().getSimpleName() + " and @" + object.getClass().getSimpleName() + ". Can not have both."); - } - - } - } - if (obj1 == null) { - return false; - // throw new ConfigurationException("Method '" + - // theNextMethod.getName() + "' on type '" + - // theNextMethod.getDeclaringClass().getSimpleName() + - // " has no FHIR method annotations."); - } - return true; - } - - protected static List toResourceList(Object response) throws InternalErrorException { - if (response == null) { - return Collections.emptyList(); - } else if (response instanceof IResource) { - return Collections.singletonList((IResource) response); - } else if (response instanceof Collection) { - List retVal = new ArrayList(); - for (Object next : ((Collection) response)) { - retVal.add((IResource) next); - } - return retVal; - } else { - throw new InternalErrorException("Unexpected return type: " + response.getClass().getCanonicalName()); - } - } - public static EncodingUtil determineResponseEncoding(HttpServletRequest theRequest, Map theParams) { String[] format = theParams.remove(Constants.PARAM_FORMAT); if (format != null) { @@ -344,9 +301,61 @@ public abstract class BaseMethodBinding { return EncodingUtil.XML; } - public abstract void invokeServer(RestfulServer theServer, Request theRequest, HttpServletResponse theResponse) throws BaseServerResponseException, IOException; + public static boolean verifyMethodHasZeroOrOneOperationAnnotation(Method theNextMethod, Object... theAnnotations) { + Object obj1 = null; + for (Object object : theAnnotations) { + if (object != null) { + if (obj1 == null) { + obj1 = object; + } else { + throw new ConfigurationException("Method " + theNextMethod.getName() + " on type '" + theNextMethod.getDeclaringClass().getSimpleName() + " has annotations @" + obj1.getClass().getSimpleName() + " and @" + object.getClass().getSimpleName() + + ". Can not have both."); + } - public abstract Object invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) throws IOException, - BaseServerResponseException; + } + } + if (obj1 == null) { + return false; + // throw new ConfigurationException("Method '" + + // theNextMethod.getName() + "' on type '" + + // theNextMethod.getDeclaringClass().getSimpleName() + + // " has no FHIR method annotations."); + } + return true; + } + + private static String toLogString(Class theType) { + if (theType == null) { + return null; + } + return theType.getCanonicalName(); + } + + private static boolean verifyIsValidResourceReturnType(Class theReturnType) { + if (theReturnType == null) { + return false; + } + if (!IResource.class.isAssignableFrom(theReturnType)) { + return false; + } + boolean retVal = Modifier.isAbstract(theReturnType.getModifiers()) == false; + return retVal; + } + + protected static List toResourceList(Object response) throws InternalErrorException { + if (response == null) { + return Collections.emptyList(); + } else if (response instanceof IResource) { + return Collections.singletonList((IResource) response); + } else if (response instanceof Collection) { + List retVal = new ArrayList(); + for (Object next : ((Collection) response)) { + retVal.add((IResource) next); + } + return retVal; + } else { + throw new InternalErrorException("Unexpected return type: " + response.getClass().getCanonicalName()); + } + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBinding.java index 6c55c178ce0..cf7db9ef288 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBinding.java @@ -44,7 +44,6 @@ import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.client.BaseClientInvocation; import ca.uhn.fhir.rest.method.SearchMethodBinding.RequestType; import ca.uhn.fhir.rest.param.IParameter; -import ca.uhn.fhir.rest.param.ParameterUtil; import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.EncodingUtil; import ca.uhn.fhir.rest.server.RestfulServer; @@ -59,12 +58,10 @@ import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseOutcomeReturningMethodBinding.class); - private List myParameters; private boolean myReturnVoid; public BaseOutcomeReturningMethodBinding(Method theMethod, FhirContext theContext, Class theMethodAnnotation, Object theProvider) { super(theMethod, theContext, theProvider); - myParameters = ParameterUtil.getResourceParameters(theMethod); if (!theMethod.getReturnType().equals(MethodOutcome.class)) { if (!allowVoidReturnType()) { @@ -143,9 +140,9 @@ abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding { @Override public void invokeServer(RestfulServer theServer, Request theRequest, HttpServletResponse theResponse) throws BaseServerResponseException, IOException { - Object[] params = new Object[myParameters.size()]; - for (int i = 0; i < myParameters.size(); i++) { - IParameter param = myParameters.get(i); + Object[] params = new Object[getParameters().size()]; + for (int i = 0; i < getParameters().size(); i++) { + IParameter param = getParameters().get(i); if (param != null) { params[i] = param.translateQueryParametersIntoServerArgument(theRequest, null); } @@ -182,7 +179,7 @@ abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding { theResponse.setStatus(Constants.STATUS_HTTP_204_NO_CONTENT); } - theServer.addHapiHeader(theResponse); + theServer.addHeadersToResponse(theResponse); Writer writer = theResponse.getWriter(); try { @@ -243,10 +240,6 @@ abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding { protected abstract BaseClientInvocation createClientInvocation(Object[] theArgs, IResource resource, String resourceName); - protected List getParameters() { - return myParameters; - } - protected void parseContentLocation(MethodOutcome theOutcomeToPopulate, String theLocationHeader) { String resourceNamePart = "/" + getResourceName() + "/"; int resourceIndex = theLocationHeader.lastIndexOf(resourceNamePart); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBindingWithResourceParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBindingWithResourceParam.java index 00b8e0dd241..2a89b6bce9a 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBindingWithResourceParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBindingWithResourceParam.java @@ -125,7 +125,7 @@ abstract class BaseOutcomeReturningMethodBindingWithResourceParam extends BaseOu theResponse.setStatus(Constants.STATUS_HTTP_204_NO_CONTENT); } - theServer.addHapiHeader(theResponse); + theServer.addHeadersToResponse(theResponse); if (response != null && response.getOperationOutcome() != null) { theResponse.setContentType(encoding.getResourceContentType()); @@ -147,7 +147,7 @@ abstract class BaseOutcomeReturningMethodBindingWithResourceParam extends BaseOu private void streamOperationOutcome(BaseServerResponseException theE, RestfulServer theServer, EncodingUtil theEncoding, HttpServletResponse theResponse) throws IOException { theResponse.setStatus(theE.getStatusCode()); - theServer.addHapiHeader(theResponse); + theServer.addHeadersToResponse(theResponse); if (theE.getOperationOutcome() != null) { theResponse.setContentType(theEncoding.getResourceContentType()); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseResourceReturningMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseResourceReturningMethodBinding.java index b452673cc57..9408dc2a3b1 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseResourceReturningMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseResourceReturningMethodBinding.java @@ -77,13 +77,11 @@ abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding { private MethodReturnTypeEnum myMethodReturnType; private String myResourceName; - private List myParameters; + public BaseResourceReturningMethodBinding(Class theReturnResourceType, Method theMethod, FhirContext theConetxt, Object theProvider) { super(theMethod, theConetxt, theProvider); - myParameters = ParameterUtil.getResourceParameters(theMethod); - Class methodReturnType = theMethod.getReturnType(); if (Collection.class.isAssignableFrom(methodReturnType)) { myMethodReturnType = MethodReturnTypeEnum.LIST_OF_RESOURCES; @@ -92,8 +90,7 @@ abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding { } else if (Bundle.class.isAssignableFrom(methodReturnType)) { myMethodReturnType = MethodReturnTypeEnum.BUNDLE; } 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) { @@ -200,9 +197,9 @@ abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding { } // Method params - Object[] params = new Object[myParameters.size()]; - for (int i = 0; i < myParameters.size(); i++) { - IParameter param = myParameters.get(i); + Object[] params = new Object[getParameters().size()]; + for (int i = 0; i < getParameters().size(); i++) { + IParameter param = getParameters().get(i); if (param != null) { params[i] = param.translateQueryParametersIntoServerArgument(theRequest, null); } @@ -241,8 +238,7 @@ abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding { return (IdDt) retValObj; } } - throw new InternalErrorException("Found an object of type '" + retValObj.getClass().getCanonicalName() + "' in resource metadata for key " + theKey.name() + " - Expected " - + IdDt.class.getCanonicalName()); + throw new InternalErrorException("Found an object of type '" + retValObj.getClass().getCanonicalName() + "' in resource metadata for key " + theKey.name() + " - Expected " + IdDt.class.getCanonicalName()); } private InstantDt getInstantFromMetadataOrNullIfNone(Map theResourceMetadata, ResourceMetadataKeyEnum theKey) { @@ -258,8 +254,7 @@ abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding { return (InstantDt) retValObj; } } - throw new InternalErrorException("Found an object of type '" + retValObj.getClass().getCanonicalName() + "' in resource metadata for key " + theKey.name() + " - Expected " - + InstantDt.class.getCanonicalName()); + throw new InternalErrorException("Found an object of type '" + retValObj.getClass().getCanonicalName() + "' in resource metadata for key " + theKey.name() + " - Expected " + InstantDt.class.getCanonicalName()); } private IParser getNewParser(EncodingUtil theResponseEncoding, boolean thePrettyPrint, NarrativeModeEnum theNarrativeMode) { @@ -276,8 +271,8 @@ abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding { return parser.setPrettyPrint(thePrettyPrint).setSuppressNarratives(theNarrativeMode == NarrativeModeEnum.SUPPRESS); } - private void streamResponseAsBundle(RestfulServer theServer, HttpServletResponse theHttpResponse, List theResult, EncodingUtil theResponseEncoding, String theServerBase, - String theCompleteUrl, boolean thePrettyPrint, boolean theRequestIsBrowser, NarrativeModeEnum theNarrativeMode) throws IOException { + private void streamResponseAsBundle(RestfulServer theServer, HttpServletResponse theHttpResponse, List theResult, EncodingUtil theResponseEncoding, String theServerBase, String theCompleteUrl, boolean thePrettyPrint, boolean theRequestIsBrowser, + NarrativeModeEnum theNarrativeMode) throws IOException { assert !theServerBase.endsWith("/"); theHttpResponse.setStatus(200); @@ -292,7 +287,7 @@ abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding { theHttpResponse.setCharacterEncoding("UTF-8"); - theServer.addHapiHeader(theHttpResponse); + theServer.addHeadersToResponse(theHttpResponse); Bundle bundle = new Bundle(); bundle.getAuthorName().setValue(getClass().getCanonicalName()); @@ -322,10 +317,10 @@ abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding { b.append(resId); /* - * If this is a history operation, we add the version of the resource to the self link to indicate the version + * If this is a history operation, we add the version of the + * resource to the self link to indicate the version */ - if (getResourceOperationType() == RestfulOperationTypeEnum.HISTORY_INSTANCE || getResourceOperationType() == RestfulOperationTypeEnum.HISTORY_TYPE - || getSystemOperationType() == RestfulOperationSystemEnum.HISTORY_SYSTEM) { + if (getResourceOperationType() == RestfulOperationTypeEnum.HISTORY_INSTANCE || getResourceOperationType() == RestfulOperationTypeEnum.HISTORY_TYPE || getSystemOperationType() == RestfulOperationSystemEnum.HISTORY_SYSTEM) { IdDt versionId = getIdFromMetadataOrNullIfNone(next.getResourceMetadata(), ResourceMetadataKeyEnum.VERSION_ID); if (versionId != null) { b.append('/'); @@ -387,8 +382,7 @@ abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding { } } - private void streamResponseAsResource(RestfulServer theServer, HttpServletResponse theHttpResponse, IResource theResource, EncodingUtil theResponseEncoding, boolean thePrettyPrint, - boolean theRequestIsBrowser, NarrativeModeEnum theNarrativeMode) throws IOException { + private void streamResponseAsResource(RestfulServer theServer, HttpServletResponse theHttpResponse, IResource theResource, EncodingUtil theResponseEncoding, boolean thePrettyPrint, boolean theRequestIsBrowser, NarrativeModeEnum theNarrativeMode) throws IOException { theHttpResponse.setStatus(200); if (theRequestIsBrowser && theServer.isUseBrowserFriendlyContentTypes()) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HistoryMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HistoryMethodBinding.java index 89067ee6d36..3fd13a8bd5e 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HistoryMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HistoryMethodBinding.java @@ -20,26 +20,23 @@ package ca.uhn.fhir.rest.method; * #L% */ -import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.*; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.LinkedHashMap; import java.util.List; import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.StringUtils; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.dstu.valueset.RestfulOperationSystemEnum; import ca.uhn.fhir.model.dstu.valueset.RestfulOperationTypeEnum; import ca.uhn.fhir.model.primitive.IdDt; -import ca.uhn.fhir.model.primitive.InstantDt; -import ca.uhn.fhir.model.primitive.IntegerDt; -import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.rest.annotation.History; import ca.uhn.fhir.rest.client.BaseClientInvocation; import ca.uhn.fhir.rest.client.GetClientInvocation; +import ca.uhn.fhir.rest.param.IParameter; import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; @@ -48,18 +45,14 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; public class HistoryMethodBinding extends BaseResourceReturningMethodBinding { private final Integer myIdParamIndex; + private String myResourceName; private final RestfulOperationTypeEnum myResourceOperationType; private final RestfulOperationSystemEnum mySystemOperationType; - private String myResourceName; - private Integer mySinceParamIndex; - private Integer myCountParamIndex; public HistoryMethodBinding(Method theMethod, FhirContext theConetxt, Object theProvider) { super(toReturnType(theMethod, theProvider), theMethod, theConetxt, theProvider); myIdParamIndex = Util.findIdParameterIndex(theMethod); - mySinceParamIndex = Util.findSinceParameterIndex(theMethod); - myCountParamIndex = Util.findCountParameterIndex(theMethod); History historyAnnotation = theMethod.getAnnotation(History.class); Class type = historyAnnotation.type(); @@ -93,6 +86,89 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding { } + @Override + public RestfulOperationTypeEnum getResourceOperationType() { + return myResourceOperationType; + } + + @Override + public ReturnTypeEnum getReturnType() { + return ReturnTypeEnum.BUNDLE; + } + + @Override + public RestfulOperationSystemEnum getSystemOperationType() { + return mySystemOperationType; + } + + @Override + public BaseClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException { + StringBuilder b = new StringBuilder(); + if (myResourceName != null) { + b.append(myResourceName); + if (myIdParamIndex != null) { + IdDt id = (IdDt) theArgs[myIdParamIndex]; + if (id == null || isBlank(id.getValue())) { + throw new NullPointerException("ID can not be null"); + } + b.append('/'); + b.append(id.getValue()); + } + } + if (b.length() > 0) { + b.append('/'); + } + b.append(Constants.PARAM_HISTORY); + + LinkedHashMap> queryStringArgs=new LinkedHashMap>(); + if (theArgs != null) { + for (int idx = 0; idx < theArgs.length; idx++) { + IParameter nextParam = getParameters().get(idx); + nextParam.translateClientArgumentIntoQueryArgument(theArgs[idx], queryStringArgs); + } + } + + return new GetClientInvocation(queryStringArgs, b.toString()); + } + + @Override + public List invokeServer(Object theResourceProvider, Request theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException { + if (myIdParamIndex != null) { + theMethodParams[myIdParamIndex] = theRequest.getId(); + } + + Object response = invokeServerMethod(theResourceProvider, theMethodParams); + + return toResourceList(response); + } + + // ObjectUtils.equals is replaced by a JDK7 method.. + @SuppressWarnings("deprecation") + @Override + public boolean matches(Request theRequest) { + if (!Constants.PARAM_HISTORY.equals(theRequest.getOperation())) { + return false; + } + if (theRequest.getResourceName() == null) { + return mySystemOperationType == RestfulOperationSystemEnum.HISTORY_SYSTEM; + } + if (!ObjectUtils.equals(theRequest.getResourceName(), myResourceName)) { + return false; + } + + boolean haveIdParam = theRequest.getId() != null && !theRequest.getId().isEmpty(); + boolean wantIdParam = myIdParamIndex != null; + if (haveIdParam != wantIdParam) { + return false; + } + + if (theRequest.getVersion() != null && !theRequest.getVersion().isEmpty()) { + return false; + } + + return true; + } + private static Class toReturnType(Method theMethod, Object theProvider) { if (theProvider instanceof IResourceProvider) { return ((IResourceProvider) theProvider).getResourceType(); @@ -105,100 +181,4 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding { return null; } - @Override - public RestfulOperationTypeEnum getResourceOperationType() { - return myResourceOperationType; - } - - @Override - public RestfulOperationSystemEnum getSystemOperationType() { - return mySystemOperationType; - } - - @Override - public BaseClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException { - StringBuilder b = new StringBuilder(); - if (myResourceName!=null) { - b.append(myResourceName); - if (myIdParamIndex!=null) { - IdDt id = (IdDt)theArgs[myIdParamIndex]; - if (id==null||isBlank(id.getValue())) { - throw new NullPointerException("ID can not be null"); - } - b.append('/'); - b.append(id.getValue()); - } - } - if (b.length()>0) { - b.append('/'); - } - b.append(Constants.PARAM_HISTORY); - - return new GetClientInvocation(b.toString()); - } - - - @SuppressWarnings("deprecation") // ObjectUtils.equals is replaced by a JDK7 method.. - @Override - public boolean matches(Request theRequest) { - if (!Constants.PARAM_HISTORY.equals(theRequest.getOperation())) { - return false; - } - if (theRequest.getResourceName() == null) { - return mySystemOperationType == RestfulOperationSystemEnum.HISTORY_SYSTEM; - } - if (!ObjectUtils.equals(theRequest.getResourceName(),myResourceName)) { - return false; - } - - boolean haveIdParam= theRequest.getId() != null && !theRequest.getId().isEmpty(); - boolean wantIdParam = myIdParamIndex != null; - if (haveIdParam!=wantIdParam) { - return false; - } - - if (theRequest.getVersion() != null && !theRequest.getVersion().isEmpty()) { - return false; - } - - return true; - } - - @Override - public ReturnTypeEnum getReturnType() { - return ReturnTypeEnum.BUNDLE; - } - - @Override - public List invokeServer(Object theResourceProvider, Request theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException { - if (myCountParamIndex != null) { - String[] countValues = theRequest.getParameters().remove(Constants.PARAM_COUNT); - if (countValues.length > 0 && StringUtils.isNotBlank(countValues[0])) { - try { - theMethodParams[myCountParamIndex] = new IntegerDt(countValues[0]); - } catch (DataFormatException e) { - throw new InvalidRequestException("Invalid _count parameter value: " + countValues[0]); - } - } - } - if (mySinceParamIndex != null) { - String[] sinceValues = theRequest.getParameters().remove(Constants.PARAM_SINCE); - if (sinceValues.length > 0 && StringUtils.isNotBlank(sinceValues[0])) { - try { - theMethodParams[mySinceParamIndex] = new InstantDt(sinceValues[0]); - } catch (DataFormatException e) { - throw new InvalidRequestException("Invalid _since parameter value: " + sinceValues[0]); - } - } - } - - if (myIdParamIndex!=null) { - theMethodParams[myIdParamIndex] = theRequest.getId(); - } - - Object response = invokeServerMethod(theResourceProvider, theMethodParams); - - return toResourceList(response); - } - } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SearchMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SearchMethodBinding.java index d3029655954..84a4ec56e37 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SearchMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SearchMethodBinding.java @@ -37,7 +37,6 @@ import ca.uhn.fhir.model.dstu.valueset.RestfulOperationTypeEnum; import ca.uhn.fhir.rest.client.GetClientInvocation; import ca.uhn.fhir.rest.param.BaseQueryParameter; import ca.uhn.fhir.rest.param.IParameter; -import ca.uhn.fhir.rest.param.ParameterUtil; import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; @@ -49,12 +48,10 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchMethodBinding.class); private Class myDeclaredResourceType; - private List myParameters; private String myQueryName; public SearchMethodBinding(Class theReturnResourceType, Method theMethod, String theQueryName, FhirContext theContext, Object theProvider) { super(theReturnResourceType, theMethod, theContext, theProvider); - this.myParameters = ParameterUtil.getResourceParameters(theMethod); this.myQueryName = StringUtils.defaultIfBlank(theQueryName, null); this.myDeclaredResourceType = theMethod.getReturnType(); } @@ -63,10 +60,6 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding { return myDeclaredResourceType.getClass(); } - public List getParameters() { - return myParameters; - } - @Override public RestfulOperationTypeEnum getResourceOperationType() { return RestfulOperationTypeEnum.SEARCH_TYPE; @@ -84,7 +77,7 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding { @Override public GetClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException { - assert (myQueryName == null || ((theArgs != null ? theArgs.length : 0) == myParameters.size())) : "Wrong number of arguments: " + (theArgs!=null?theArgs.length:"null"); + assert (myQueryName == null || ((theArgs != null ? theArgs.length : 0) == getParameters().size())) : "Wrong number of arguments: " + (theArgs != null ? theArgs.length : "null"); Map> queryStringArgs = new LinkedHashMap>(); @@ -94,7 +87,7 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding { if (theArgs != null) { for (int idx = 0; idx < theArgs.length; idx++) { - IParameter nextParam = myParameters.get(idx); + IParameter nextParam = getParameters().get(idx); nextParam.translateClientArgumentIntoQueryArgument(theArgs[idx], queryStringArgs); } } @@ -103,8 +96,7 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding { } @Override - public List invokeServer(Object theResourceProvider, Request theRequest, Object[] theMethodParams) throws InvalidRequestException, - InternalErrorException { + public List invokeServer(Object theResourceProvider, Request theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException { assert theRequest.getId() == null; assert theRequest.getVersion() == null; @@ -138,11 +130,11 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding { } Set methodParamsTemp = new HashSet(); - for (int i = 0; i < this.myParameters.size(); i++) { - if (!(myParameters.get(i) instanceof BaseQueryParameter)) { + for (int i = 0; i < this.getParameters().size(); i++) { + if (!(getParameters().get(i) instanceof BaseQueryParameter)) { continue; } - BaseQueryParameter temp = (BaseQueryParameter) myParameters.get(i); + BaseQueryParameter temp = (BaseQueryParameter) getParameters().get(i); methodParamsTemp.add(temp.getName()); if (temp.isRequired() && !theRequest.getParameters().containsKey(temp.getName())) { ourLog.trace("Method {} doesn't match param '{}' is not present", getMethod().getName(), temp.getName()); @@ -176,10 +168,6 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding { return retVal; } - public void setParameters(List parameters) { - this.myParameters = parameters; - } - public void setResourceType(Class resourceType) { this.myDeclaredResourceType = resourceType; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/Util.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/Util.java index 82f5bc20efe..c58d3789cd4 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/Util.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/Util.java @@ -27,18 +27,16 @@ import java.net.URLDecoder; import java.util.HashMap; import java.util.Map; -import ca.uhn.fhir.rest.annotation.Count; import ca.uhn.fhir.rest.annotation.IdParam; -import ca.uhn.fhir.rest.annotation.Since; import ca.uhn.fhir.rest.annotation.VersionIdParam; /** * Created by dsotnikov on 2/25/2014. */ class Util { - public static Integer findCountParameterIndex(Method theMethod) { - return findParamIndex(theMethod, Count.class); - } +// public static Integer findCountParameterIndex(Method theMethod) { +// return findParamIndex(theMethod, Count.class); +// } public static Integer findIdParameterIndex(Method theMethod) { return findParamIndex(theMethod, IdParam.class); @@ -59,9 +57,9 @@ class Util { return null; } - public static Integer findSinceParameterIndex(Method theMethod) { - return findParamIndex(theMethod, Since.class); - } +// public static Integer findSinceParameterIndex(Method theMethod) { +// return findParamIndex(theMethod, Since.class); +// } public static Integer findVersionIdParameterIndex(Method theMethod) { return findParamIndex(theMethod, VersionIdParam.class); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseQueryParameter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseQueryParameter.java index b6c8a7402d4..08fe8cdd901 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseQueryParameter.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseQueryParameter.java @@ -20,7 +20,9 @@ package ca.uhn.fhir.rest.param; * #L% */ +import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; @@ -98,5 +100,9 @@ public abstract class BaseQueryParameter implements IParameter { } + @Override + public void initializeTypes(Method theMethod, Class> theOuterCollectionType, Class> theInnerCollectionType, Class theParameterType) { + // ignore for now + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/CountParameter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/CountParameter.java new file mode 100644 index 00000000000..8a8dc18bfe7 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/CountParameter.java @@ -0,0 +1,83 @@ +package ca.uhn.fhir.rest.param; + +/* + * #%L + * HAPI FHIR Library + * %% + * Copyright (C) 2014 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; + +import ca.uhn.fhir.context.ConfigurationException; +import ca.uhn.fhir.model.primitive.IntegerDt; +import ca.uhn.fhir.parser.DataFormatException; +import ca.uhn.fhir.rest.annotation.Since; +import ca.uhn.fhir.rest.method.Request; +import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; + +public class CountParameter implements IParameter { + + private Class myType; + + @Override + public void translateClientArgumentIntoQueryArgument(Object theSourceClientArgument, Map> theTargetQueryArguments) throws InternalErrorException { + if (theSourceClientArgument != null) { + IntegerDt since = ParameterUtil.toInteger(theSourceClientArgument); + if (since.isEmpty() == false) { + theTargetQueryArguments.put(Constants.PARAM_COUNT, Collections.singletonList(since.getValueAsString())); + } + } + } + + @Override + public Object translateQueryParametersIntoServerArgument(Request theRequest, Object theRequestContents) throws InternalErrorException, InvalidRequestException { + String[] sinceParams = theRequest.getParameters().remove(Constants.PARAM_COUNT); + if (sinceParams != null) { + if (sinceParams.length > 0) { + if (StringUtils.isNotBlank(sinceParams[0])) { + try { + IntegerDt since = new IntegerDt(sinceParams[0]); + return ParameterUtil.fromInteger(myType, since); + } catch (DataFormatException e) { + throw new InvalidRequestException("Invalid " + Constants.PARAM_COUNT + " value: " + sinceParams[0]); + } + } + } + } + return ParameterUtil.fromInteger(myType, null); + } + + @Override + public void initializeTypes(Method theMethod, Class> theOuterCollectionType, Class> theInnerCollectionType, Class theParameterType) { + if (theOuterCollectionType != null) { + throw new ConfigurationException("Method '" + theMethod.getName() + "' in type '" + "' is annotated with @" + Since.class.getName() + " but can not be of collection type"); + } + if (!ParameterUtil.getBindableIntegerTypes().contains(theParameterType)) { + throw new ConfigurationException("Method '" + theMethod.getName() + "' in type '" + "' is annotated with @" + Since.class.getName() + " but type '" + theParameterType + "' is an invalid type, must be one of: " + ParameterUtil.getBindableInstantTypes()); + } + myType = theParameterType; + } + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/IParameter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/IParameter.java index 9488780017f..f4fbc46faa9 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/IParameter.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/IParameter.java @@ -20,6 +20,8 @@ package ca.uhn.fhir.rest.param; * #L% */ +import java.lang.reflect.Method; +import java.util.Collection; import java.util.List; import java.util.Map; @@ -45,4 +47,6 @@ public interface IParameter { */ Object translateQueryParametersIntoServerArgument(Request theRequest, Object theRequestContents) throws InternalErrorException, InvalidRequestException; + void initializeTypes(Method theMethod, Class> theOuterCollectionType, Class> theInnerCollectionType, Class theParameterType); + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/IncludeParameter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/IncludeParameter.java index 6b9ae5484f4..3ee70e7b4f3 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/IncludeParameter.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/IncludeParameter.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.rest.param; * #L% */ +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -110,4 +111,5 @@ public class IncludeParameter extends BaseQueryParameter { return true; } + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/NullParameter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/NullParameter.java new file mode 100644 index 00000000000..10d37fcd4e2 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/NullParameter.java @@ -0,0 +1,50 @@ +package ca.uhn.fhir.rest.param; + +/* + * #%L + * HAPI FHIR Library + * %% + * Copyright (C) 2014 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import ca.uhn.fhir.rest.method.Request; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; + +class NullParameter implements IParameter { + + @Override + public void translateClientArgumentIntoQueryArgument(Object theSourceClientArgument, Map> theTargetQueryArguments) throws InternalErrorException { + //nothing + } + + @Override + public Object translateQueryParametersIntoServerArgument(Request theRequest, Object theRequestContents) throws InternalErrorException, InvalidRequestException { + // nothing + return null; + } + + @Override + public void initializeTypes(Method theMethod, Class> theOuterCollectionType, Class> theInnerCollectionType, Class theParameterType) { + // nothing + } + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ParameterUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ParameterUtil.java index 0fe75083f48..22785243013 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ParameterUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ParameterUtil.java @@ -23,63 +23,71 @@ package ca.uhn.fhir.rest.param; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Calendar; import java.util.Collection; +import java.util.Date; +import java.util.HashSet; import java.util.List; +import java.util.Set; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.time.DateUtils; + import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.PathSpecification; +import ca.uhn.fhir.model.primitive.InstantDt; +import ca.uhn.fhir.model.primitive.IntegerDt; +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.OptionalParam; import ca.uhn.fhir.rest.annotation.RequiredParam; import ca.uhn.fhir.rest.annotation.ResourceParam; import ca.uhn.fhir.rest.annotation.ServerBase; +import ca.uhn.fhir.rest.annotation.Since; import ca.uhn.fhir.rest.annotation.VersionIdParam; import ca.uhn.fhir.util.ReflectionUtil; public class ParameterUtil { @SuppressWarnings("unchecked") - public static List getResourceParameters(Method method) { + public static List getResourceParameters(Method theMethod) { List parameters = new ArrayList(); - Class[] parameterTypes = method.getParameterTypes(); + Class[] parameterTypes = theMethod.getParameterTypes(); int paramIndex = 0; - for (Annotation[] annotations : method.getParameterAnnotations()) { - boolean haveHandledMethod = false; + for (Annotation[] annotations : theMethod.getParameterAnnotations()) { + IParameter param = null; Class parameterType = parameterTypes[paramIndex]; + Class> outerCollectionType = null; + Class> innerCollectionType = null; + if (Collection.class.isAssignableFrom(parameterType)) { + innerCollectionType = (Class>) parameterType; + parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, paramIndex); + } + if (Collection.class.isAssignableFrom(parameterType)) { + outerCollectionType = innerCollectionType; + innerCollectionType = (Class>) parameterType; + parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, paramIndex); + } + if (Collection.class.isAssignableFrom(parameterType)) { + throw new ConfigurationException("Argument #" + paramIndex + " of Method '" + theMethod.getName() + "' in type '" + theMethod.getDeclaringClass().getCanonicalName() + "' is of an invalid generic type (can not be a collection of a collection of a collection)"); + } + if (parameterType.equals(HttpServletRequest.class) || parameterType.equals(ServletRequest.class)) { - ServletRequestParameter param = new ServletRequestParameter(); - parameters.add(param); + param = new ServletRequestParameter(); } else if (parameterType.equals(HttpServletResponse.class) || parameterType.equals(ServletResponse.class)) { - ServletResponseParameter param = new ServletResponseParameter(); - parameters.add(param); + param = new ServletResponseParameter(); } else { - for (int i = 0; i < annotations.length; i++) { + for (int i = 0; i < annotations.length && param == null; i++) { Annotation nextAnnotation = annotations[i]; - Class> outerCollectionType = null; - Class> innerCollectionType = null; - - if (Collection.class.isAssignableFrom(parameterType)) { - innerCollectionType = (Class>) parameterType; - parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(method, paramIndex); - } - - if (Collection.class.isAssignableFrom(parameterType)) { - outerCollectionType = innerCollectionType; - innerCollectionType = (Class>) parameterType; - parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(method, paramIndex); - } - - IParameter param; if (nextAnnotation instanceof RequiredParam) { SearchParameter parameter = new SearchParameter(); parameter.setName(((RequiredParam) nextAnnotation).name()); @@ -94,43 +102,121 @@ public class ParameterUtil { param = parameter; } else if (nextAnnotation instanceof IncludeParam) { if (parameterType != PathSpecification.class || innerCollectionType == null || outerCollectionType != null) { - throw new ConfigurationException("Method '" + method.getName() + "' is annotated with @" + IncludeParam.class.getSimpleName() + " but has a type other than Collection<" - + PathSpecification.class.getSimpleName() + ">"); + throw new ConfigurationException("Method '" + theMethod.getName() + "' is annotated with @" + IncludeParam.class.getSimpleName() + " but has a type other than Collection<" + PathSpecification.class.getSimpleName() + ">"); } - Class> instantiableCollectionType = (Class>) CollectionBinder.getInstantiableCollectionType( - innerCollectionType, "Method '" + method.getName() + "'"); + Class> instantiableCollectionType = (Class>) CollectionBinder.getInstantiableCollectionType(innerCollectionType, "Method '" + theMethod.getName() + "'"); param = new IncludeParameter((IncludeParam) nextAnnotation, instantiableCollectionType); } else if (nextAnnotation instanceof ResourceParam) { if (!IResource.class.isAssignableFrom(parameterType)) { - throw new ConfigurationException("Method '" + method.getName() + "' is annotated with @" + ResourceParam.class.getSimpleName() - + " but has a type that is not an implemtation of " + IResource.class.getCanonicalName()); + throw new ConfigurationException("Method '" + theMethod.getName() + "' is annotated with @" + ResourceParam.class.getSimpleName() + " but has a type that is not an implemtation of " + IResource.class.getCanonicalName()); } param = new ResourceParameter((Class) parameterType); } else if (nextAnnotation instanceof IdParam || nextAnnotation instanceof VersionIdParam) { - param = null; + param = new NullParameter(); } else if (nextAnnotation instanceof ServerBase) { param = new ServerBaseParameter(); + } else if (nextAnnotation instanceof Since) { + param = new SinceParameter(); + } else if (nextAnnotation instanceof Count) { + param = new CountParameter(); } else { continue; } - haveHandledMethod = true; - parameters.add(param); - break; - } - if (!haveHandledMethod) { - throw new ConfigurationException("Parameter #" + paramIndex + " of method '" + method.getName() + "' on type '" + method.getDeclaringClass().getCanonicalName() - + "' has no recognized FHIR interface parameter annotations. Don't know how to handle this parameter"); - } } + if (param == null) { + throw new ConfigurationException("Parameter #" + paramIndex + " of method '" + theMethod.getName() + "' on type '" + theMethod.getDeclaringClass().getCanonicalName() + + "' has no recognized FHIR interface parameter annotations. Don't know how to handle this parameter"); + } + + param.initializeTypes(theMethod, outerCollectionType, innerCollectionType, parameterType); + parameters.add(param); + paramIndex++; } return parameters; } - + public static InstantDt toInstant(Object theArgument) { + if (theArgument instanceof InstantDt) { + return (InstantDt) theArgument; + } + if (theArgument instanceof Date) { + return new InstantDt((Date) theArgument); + } + if (theArgument instanceof Calendar) { + return new InstantDt((Calendar) theArgument); + } + return null; + } + + public static Set> getBindableInstantTypes() { + // TODO: make this constant + HashSet> retVal = new HashSet>(); + retVal.add(InstantDt.class); + retVal.add(Date.class); + retVal.add(Calendar.class); + return retVal; + } + + public static Object fromInstant(Class theType, InstantDt theArgument) { + if (theType.equals(InstantDt.class)) { + if (theArgument == null) { + return new InstantDt(); + } + return theArgument; + } + if (theType.equals(Date.class)) { + if (theArgument == null) { + return null; + } + return theArgument.getValue(); + } + if (theType.equals(Calendar.class)) { + if (theArgument == null) { + return null; + } + return DateUtils.toCalendar(theArgument.getValue()); + } + throw new IllegalArgumentException("Invalid instant type:" + theType); + } + + public static IntegerDt toInteger(Object theArgument) { + if (theArgument instanceof IntegerDt) { + return (IntegerDt) theArgument; + } + if (theArgument instanceof Integer) { + return new IntegerDt((Integer) theArgument); + } + return null; + } + + public static Set> getBindableIntegerTypes() { + // TODO: make this constant + HashSet> retVal = new HashSet>(); + retVal.add(IntegerDt.class); + retVal.add(Integer.class); + return retVal; + } + + public static Object fromInteger(Class theType, IntegerDt theArgument) { + if (theType.equals(IntegerDt.class)) { + if (theArgument == null) { + return new IntegerDt(); + } + return theArgument; + } + if (theType.equals(Integer.class)) { + if (theArgument == null) { + return null; + } + return theArgument.getValue(); + } + throw new IllegalArgumentException("Invalid Integer type:" + theType); + } + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ResourceParameter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ResourceParameter.java index 4ca57493cfe..b71bb3ce35c 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ResourceParameter.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ResourceParameter.java @@ -20,6 +20,8 @@ package ca.uhn.fhir.rest.param; * #L% */ +import java.lang.reflect.Method; +import java.util.Collection; import java.util.List; import java.util.Map; @@ -52,6 +54,9 @@ public class ResourceParameter implements IParameter { return myResourceName; } -// public IResource + @Override + public void initializeTypes(Method theMethod, Class> theOuterCollectionType, Class> theInnerCollectionType, Class theParameterType) { + // ignore for now + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ServerBaseParameter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ServerBaseParameter.java index 903a7ce7256..149d8394f89 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ServerBaseParameter.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ServerBaseParameter.java @@ -20,6 +20,8 @@ package ca.uhn.fhir.rest.param; * #L% */ +import java.lang.reflect.Method; +import java.util.Collection; import java.util.List; import java.util.Map; @@ -35,7 +37,7 @@ class ServerBaseParameter implements IParameter { /* * Does nothing, since we just ignore serverbase arguments */ - ourLog.trace("Ignoring HttpServletRequest argument: {}", theSourceClientArgument); + ourLog.trace("Ignoring server base argument: {}", theSourceClientArgument); } @Override @@ -43,4 +45,9 @@ class ServerBaseParameter implements IParameter { return theRequest.getFhirServerBase(); } + @Override + public void initializeTypes(Method theMethod, Class> theOuterCollectionType, Class> theInnerCollectionType, Class theParameterType) { + // ignore for now + } + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ServletRequestParameter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ServletRequestParameter.java index f876139389c..9c55ba591dd 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ServletRequestParameter.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ServletRequestParameter.java @@ -20,6 +20,8 @@ package ca.uhn.fhir.rest.param; * #L% */ +import java.lang.reflect.Method; +import java.util.Collection; import java.util.List; import java.util.Map; @@ -43,4 +45,9 @@ class ServletRequestParameter implements IParameter { return theRequest.getServletRequest(); } + @Override + public void initializeTypes(Method theMethod, Class> theOuterCollectionType, Class> theInnerCollectionType, Class theParameterType) { + // ignore + } + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ServletResponseParameter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ServletResponseParameter.java index b4ca91a0379..8b1397a2676 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ServletResponseParameter.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ServletResponseParameter.java @@ -20,6 +20,8 @@ package ca.uhn.fhir.rest.param; * #L% */ +import java.lang.reflect.Method; +import java.util.Collection; import java.util.List; import java.util.Map; @@ -43,4 +45,10 @@ class ServletResponseParameter implements IParameter { return theRequest.getServletResponse(); } + + @Override + public void initializeTypes(Method theMethod, Class> theOuterCollectionType, Class> theInnerCollectionType, Class theParameterType) { + // ignore + } + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/SinceParameter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/SinceParameter.java new file mode 100644 index 00000000000..40a9bcebe52 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/SinceParameter.java @@ -0,0 +1,83 @@ +package ca.uhn.fhir.rest.param; + +/* + * #%L + * HAPI FHIR Library + * %% + * Copyright (C) 2014 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; + +import ca.uhn.fhir.context.ConfigurationException; +import ca.uhn.fhir.model.primitive.InstantDt; +import ca.uhn.fhir.parser.DataFormatException; +import ca.uhn.fhir.rest.annotation.Since; +import ca.uhn.fhir.rest.method.Request; +import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; + +public class SinceParameter implements IParameter { + + private Class myType; + + @Override + public void translateClientArgumentIntoQueryArgument(Object theSourceClientArgument, Map> theTargetQueryArguments) throws InternalErrorException { + if (theSourceClientArgument != null) { + InstantDt since = ParameterUtil.toInstant(theSourceClientArgument); + if (since.isEmpty() == false) { + theTargetQueryArguments.put(Constants.PARAM_SINCE, Collections.singletonList(since.getValueAsString())); + } + } + } + + @Override + public Object translateQueryParametersIntoServerArgument(Request theRequest, Object theRequestContents) throws InternalErrorException, InvalidRequestException { + String[] sinceParams = theRequest.getParameters().remove(Constants.PARAM_SINCE); + if (sinceParams != null) { + if (sinceParams.length > 0) { + if (StringUtils.isNotBlank(sinceParams[0])) { + try { + InstantDt since = new InstantDt(sinceParams[0]); + return ParameterUtil.fromInstant(myType, since); + } catch (DataFormatException e) { + throw new InvalidRequestException("Invalid " + Constants.PARAM_SINCE + " value: " + sinceParams[0]); + } + } + } + } + return ParameterUtil.fromInstant(myType, null); + } + + @Override + public void initializeTypes(Method theMethod, Class> theOuterCollectionType, Class> theInnerCollectionType, Class theParameterType) { + if (theOuterCollectionType != null) { + throw new ConfigurationException("Method '" + theMethod.getName() + "' in type '" + "' is annotated with @" + Since.class.getName() + " but can not be of collection type"); + } + if (!ParameterUtil.getBindableInstantTypes().contains(theParameterType)) { + throw new ConfigurationException("Method '" + theMethod.getName() + "' in type '" + "' is annotated with @" + Since.class.getName() + " but cis an invalid type, must be one of: " + ParameterUtil.getBindableInstantTypes()); + } + myType = theParameterType; + } + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/ConfigurationException.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/ConfigurationException.java similarity index 95% rename from hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/ConfigurationException.java rename to hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/ConfigurationException.java index 7a2a0f74707..c9b12a71f2b 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/ConfigurationException.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/ConfigurationException.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.rest.server.exceptions; +package ca.uhn.fhir.rest.server; /* * #%L diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java index 81d8ee0db30..d7ac51ced0b 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java @@ -48,10 +48,9 @@ import ca.uhn.fhir.rest.method.Request; import ca.uhn.fhir.rest.method.SearchMethodBinding; import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; -import ca.uhn.fhir.rest.server.exceptions.ConfigurationException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.rest.server.exceptions.MethodNotFoundException; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.provider.ServerConformanceProvider; import ca.uhn.fhir.rest.server.provider.ServerProfileProvider; import ca.uhn.fhir.util.VersionUtil; @@ -63,7 +62,7 @@ public class RestfulServer extends HttpServlet { private static final long serialVersionUID = 1L; private FhirContext myFhirContext; - private Collection myProviders; + private Collection myPlainProviders; private Map myResourceNameToProvider = new HashMap(); private Collection myResourceProviders; private ISecurityManager mySecurityManager; @@ -73,16 +72,34 @@ public class RestfulServer extends HttpServlet { private String myServerVersion = VersionUtil.getVersion(); // defaults to // HAPI version private boolean myUseBrowserFriendlyContentTypes; + private ResourceBinding myNullResourceBinding = new ResourceBinding(); + /** + * Constructor + */ public RestfulServer() { myFhirContext = new FhirContext(); myServerConformanceProvider = new ServerConformanceProvider(this); } - public void addHapiHeader(HttpServletResponse theHttpResponse) { - theHttpResponse.addHeader("X-CatchingFhir", "Powered by HAPI FHIR " + VersionUtil.getVersion()); + /** + * This method is called prior to sending a response to incoming requests. It is + * used to add custom headers. + *

+ * Use caution if overriding this method: it is recommended to call + * super.addHeadersToResponse to avoid inadvertantly + * disabling functionality. + *

+ */ + public void addHeadersToResponse(HttpServletResponse theHttpResponse) { + theHttpResponse.addHeader("X-PoweredBy", "HAPI FHIR " + VersionUtil.getVersion() + " RESTful Server"); } + /** + * 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; } @@ -93,8 +110,8 @@ public class RestfulServer extends HttpServlet { * * @see #getResourceProviders() */ - public Collection getProviders() { - return myProviders; + public Collection getPlainProviders() { + return myPlainProviders; } public Collection getResourceBindings() { @@ -152,6 +169,12 @@ public class RestfulServer extends HttpServlet { return myServerVersion; } + /** + * 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 { initialize(); @@ -183,7 +206,7 @@ public class RestfulServer extends HttpServlet { } } - Collection providers = getProviders(); + Collection providers = getPlainProviders(); if (providers != null) { for (Object next : providers) { assertProviderIsValid(next); @@ -214,12 +237,12 @@ public class RestfulServer extends HttpServlet { /** * Sets the non-resource specific providers which implement method calls on - * this server + * this server. * * @see #setResourceProviders(Collection) */ - public void setProviders(Collection theProviders) { - myProviders = theProviders; + public void setPlainProviders(Collection theProviders) { + myPlainProviders = theProviders; } /** @@ -229,7 +252,7 @@ public class RestfulServer extends HttpServlet { * @see #setResourceProviders(Collection) */ public void setProviders(Object... theProviders) { - myProviders = Arrays.asList(theProviders); + myPlainProviders = Arrays.asList(theProviders); } /** @@ -316,16 +339,20 @@ public class RestfulServer extends HttpServlet { BaseMethodBinding foundMethodBinding = BaseMethodBinding.bindMethod(m, myFhirContext, theProvider); if (foundMethodBinding != null) { - - RuntimeResourceDefinition definition = myFhirContext.getResourceDefinition(foundMethodBinding.getResourceName()); + + String resourceName = foundMethodBinding.getResourceName(); ResourceBinding resourceBinding; - if (myResourceNameToProvider.containsKey(definition.getName())) { - resourceBinding = myResourceNameToProvider.get(definition.getName()); + if (resourceName == null) { + resourceBinding = myNullResourceBinding; } else { - resourceBinding = new ResourceBinding(); - String resourceName = definition.getName(); - resourceBinding.setResourceName(resourceName); - myResourceNameToProvider.put(resourceName, resourceBinding); + RuntimeResourceDefinition definition = myFhirContext.getResourceDefinition(resourceName); + if (myResourceNameToProvider.containsKey(definition.getName())) { + resourceBinding = myResourceNameToProvider.get(definition.getName()); + } else { + resourceBinding = new ResourceBinding(); + resourceBinding.setResourceName(resourceName); + myResourceNameToProvider.put(resourceName, resourceBinding); + } } resourceBinding.addMethod(foundMethodBinding); @@ -453,18 +480,24 @@ public class RestfulServer extends HttpServlet { StringTokenizer tok = new StringTokenizer(requestPath, "/"); if (!tok.hasMoreTokens()) { - throw new MethodNotFoundException("No resource name specified"); + throw new ResourceNotFoundException("No resource name specified"); } resourceName = tok.nextToken(); + if (resourceName.startsWith("_")) { + operation = resourceName; + resourceName = null; + } ResourceBinding resourceBinding = null; BaseMethodBinding resourceMethod = null; if ("metadata".equals(resourceName)) { resourceMethod = myServerConformanceMethod; + } else if (resourceName == null) { + resourceBinding = myNullResourceBinding; } else { resourceBinding = myResourceNameToProvider.get(resourceName); if (resourceBinding == null) { - throw new MethodNotFoundException("Unknown resource type '" + resourceName + "' - Server knows how to handle: " + myResourceNameToProvider.keySet()); + throw new ResourceNotFoundException("Unknown resource type '" + resourceName + "' - Server knows how to handle: " + myResourceNameToProvider.keySet()); } } @@ -515,14 +548,14 @@ public class RestfulServer extends HttpServlet { resourceMethod = resourceBinding.getMethod(r); } if (null == resourceMethod) { - throw new MethodNotFoundException("No resource method available for the supplied parameters " + params); + throw new ResourceNotFoundException("No resource method available for the supplied parameters " + params); } resourceMethod.invokeServer(this, r, theResponse); } catch (AuthenticationException e) { theResponse.setStatus(e.getStatusCode()); - addHapiHeader(theResponse); + addHeadersToResponse(theResponse); theResponse.setContentType("text/plain"); theResponse.setCharacterEncoding("UTF-8"); theResponse.getWriter().write(e.getMessage()); @@ -535,7 +568,7 @@ public class RestfulServer extends HttpServlet { } theResponse.setStatus(e.getStatusCode()); - addHapiHeader(theResponse); + addHeadersToResponse(theResponse); theResponse.setContentType("text/plain"); theResponse.setCharacterEncoding("UTF-8"); theResponse.getWriter().append(e.getMessage()); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/AuthenticationException.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/AuthenticationException.java index bcfbba9bd7c..23d0e31434c 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/AuthenticationException.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/AuthenticationException.java @@ -1,5 +1,7 @@ package ca.uhn.fhir.rest.server.exceptions; +import ca.uhn.fhir.model.dstu.resource.OperationOutcome; + /* * #%L * HAPI FHIR Library @@ -22,7 +24,8 @@ package ca.uhn.fhir.rest.server.exceptions; /** - * Created by dsotnikov on 3/10/2014. + * Represents an HTTP 401 Client Unauthorized response, which means that + * the client needs to provide credentials, or has provided invalid credentials. */ public class AuthenticationException extends BaseServerResponseException { @@ -36,7 +39,4 @@ public class AuthenticationException extends BaseServerResponseException { super(401, theMessage); } - public AuthenticationException(int theStatusCode, String theMessage) { - super(theStatusCode, theMessage); - } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/InternalErrorException.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/InternalErrorException.java index 5769634a96c..db3828ab567 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/InternalErrorException.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/InternalErrorException.java @@ -20,6 +20,9 @@ package ca.uhn.fhir.rest.server.exceptions; * #L% */ +/** + * TODO: javadoc this + */ public class InternalErrorException extends BaseServerResponseException { private static final long serialVersionUID = 1L; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/MethodNotAllowedException.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/MethodNotAllowedException.java index bbb0d2333f2..b9619a5efe2 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/MethodNotAllowedException.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/MethodNotAllowedException.java @@ -21,7 +21,7 @@ package ca.uhn.fhir.rest.server.exceptions; */ /** - * Created by dsotnikov on 2/27/2014. + * TODO: javadoc this */ public class MethodNotAllowedException extends BaseServerResponseException { private static final long serialVersionUID = 1L; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/MethodNotFoundException.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/MethodNotFoundException.java deleted file mode 100644 index 00ff523c1b7..00000000000 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/MethodNotFoundException.java +++ /dev/null @@ -1,32 +0,0 @@ -package ca.uhn.fhir.rest.server.exceptions; - -/* - * #%L - * HAPI FHIR Library - * %% - * Copyright (C) 2014 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -/** - * Created by dsotnikov on 2/27/2014. - */ -public class MethodNotFoundException extends BaseServerResponseException { - private static final long serialVersionUID = 1L; - - public MethodNotFoundException(String error) { - super(404, error); - } -} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/ResourceNotFoundException.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/ResourceNotFoundException.java index 416ad4b7a26..7ac76521731 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/ResourceNotFoundException.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/ResourceNotFoundException.java @@ -24,6 +24,10 @@ import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.dstu.composite.IdentifierDt; import ca.uhn.fhir.model.primitive.IdDt; +/** + * Represents an HTTP 404 Resource Not Found response, which means that + * the request is pointing to a resource that does not exist. + */ public class ResourceNotFoundException extends BaseServerResponseException { public ResourceNotFoundException(IdDt theId) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/ResourceVersionConflictException.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/ResourceVersionConflictException.java index 2fde0ce9e09..77bcf2cba62 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/ResourceVersionConflictException.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/ResourceVersionConflictException.java @@ -20,10 +20,14 @@ package ca.uhn.fhir.rest.server.exceptions; * #L% */ +import ca.uhn.fhir.rest.annotation.Delete; +import ca.uhn.fhir.rest.annotation.Update; import ca.uhn.fhir.rest.server.Constants; /** - * Created by dsotnikov on 2/27/2014. + * Represents an HTTP 409 Conflict response. This exception should be + * thrown in methods which accept a version (e.g. {@link Update}, {@link Delete}) + * when the operation fails because of a version conflict as specified in the FHIR specification. */ public class ResourceVersionConflictException extends BaseServerResponseException { private static final long serialVersionUID = 1L; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/ResourceVersionNotSpecifiedException.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/ResourceVersionNotSpecifiedException.java index 51096570243..d221e9149dd 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/ResourceVersionNotSpecifiedException.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/ResourceVersionNotSpecifiedException.java @@ -20,10 +20,12 @@ package ca.uhn.fhir.rest.server.exceptions; * #L% */ +import ca.uhn.fhir.rest.annotation.Update; import ca.uhn.fhir.rest.server.Constants; /** - * Thrown for an Update operation if that operation requires a version to + * Represents an HTTP 412 Precondition Failed response. This exception + * should be thrown for an {@link Update} operation if that operation requires a version to * be specified in an HTTP header, and none was. */ public class ResourceVersionNotSpecifiedException extends BaseServerResponseException { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/UnclassifiedServerFailureException.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/UnclassifiedServerFailureException.java index c96f23ce16c..405f3f131fc 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/UnclassifiedServerFailureException.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/UnclassifiedServerFailureException.java @@ -22,7 +22,9 @@ package ca.uhn.fhir.rest.server.exceptions; /** * Exception for use when a response is received or being sent that - * does not correspond to any other exception type + * does not correspond to any other exception type. An HTTP status code + * must be provided, and will be provided to the caller in the case of a + * server implementation. */ public class UnclassifiedServerFailureException extends BaseServerResponseException { diff --git a/hapi-fhir-base/src/site/example/java/example/ExampleProviders.java b/hapi-fhir-base/src/site/example/java/example/ExampleProviders.java new file mode 100644 index 00000000000..21bf37cad43 --- /dev/null +++ b/hapi-fhir-base/src/site/example/java/example/ExampleProviders.java @@ -0,0 +1,59 @@ +package example; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import ca.uhn.fhir.model.api.Bundle; +import ca.uhn.fhir.model.dstu.resource.Patient; +import ca.uhn.fhir.model.primitive.StringDt; +import ca.uhn.fhir.rest.annotation.RequiredParam; +import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.server.IResourceProvider; +import ca.uhn.fhir.rest.server.RestfulServer; + +@SuppressWarnings(value= {"serial","unused"}) +public class ExampleProviders { + + +//START SNIPPET: plainProvider +public class PlainProvider { + + /** + * This method is a Patient search, but HAPI can not automatically + * determine the resource type so it must be explicitly stated. + */ + @Search(type=Patient.class) + public Bundle searchForPatients(@RequiredParam(name="name") StringDt theName) { + Bundle retVal = new Bundle(); + // perform search + return retVal; + } + +} +//END SNIPPET: plainProvider + + +//START SNIPPET: plainProviderServer +public class ExampleServlet extends RestfulServer { + + public ExampleServlet() { + /* + * Plain providers are passed to the server in the same way + * as resource providers. You may pass both resource providers + * and and plain providers to the same server if you like. + */ + List plainProviders=new ArrayList(); + plainProviders.add(new PlainProvider()); + setPlainProviders(plainProviders); + + List resourceProviders = new ArrayList(); + // ...add some resource providers... + setResourceProviders(resourceProviders); + } + +} +//END SNIPPET: plainProviderServer + + +} diff --git a/hapi-fhir-base/src/site/example/java/example/ExampleRestfulServlet.java b/hapi-fhir-base/src/site/example/java/example/ExampleRestfulServlet.java index 39447f74649..a2d94a4e103 100644 --- a/hapi-fhir-base/src/site/example/java/example/ExampleRestfulServlet.java +++ b/hapi-fhir-base/src/site/example/java/example/ExampleRestfulServlet.java @@ -1,7 +1,6 @@ package example; import java.util.ArrayList; -import java.util.Collection; import java.util.List; import javax.servlet.annotation.WebServlet; @@ -21,18 +20,19 @@ public class ExampleRestfulServlet extends RestfulServer { private static final long serialVersionUID = 1L; /** - * Restful servers must provide an implementation of this method, which - * returns all resource providers to be used by this server. In the example - * below, we are creating a RESTful server which is able to serve - * Patient and Observation resources. + * Constructor */ - @Override - public Collection getResourceProviders() { - List retVal = new ArrayList(); - retVal.add(new RestfulPatientResourceProvider()); - retVal.add(new RestfulObservationResourceProvider()); - return retVal; + public ExampleRestfulServlet() { + /* + * The servlet defines any number of resource providers, and + * configures itself to use them by calling + * setResourceProviders() + */ + List resourceProviders = new ArrayList(); + resourceProviders.add(new RestfulPatientResourceProvider()); + resourceProviders.add(new RestfulObservationResourceProvider()); + setResourceProviders(resourceProviders); } - + } //END SNIPPET: servlet diff --git a/hapi-fhir-base/src/site/example/java/example/FhirContextIntro.java b/hapi-fhir-base/src/site/example/java/example/FhirContextIntro.java index 1ba333336c0..1e1f278ec02 100644 --- a/hapi-fhir-base/src/site/example/java/example/FhirContextIntro.java +++ b/hapi-fhir-base/src/site/example/java/example/FhirContextIntro.java @@ -67,6 +67,22 @@ System.out.println(encoded); } + + +public void fluent() throws DataFormatException, IOException { +FhirContext ctx = new FhirContext(Patient.class, Observation.class); +String encoded; +//START SNIPPET: encodeMsgFluent +Patient patient = new Patient(); +patient.addIdentifier().setSystem("http://example.com/fictitious-mrns").setValue("MRN001"); +patient.addName().setUse(NameUseEnum.OFFICIAL).addFamily("Tester").addGiven("John").addGiven("Q"); + +encoded = ctx.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient); +System.out.println(encoded); +//END SNIPPET: encodeMsgFluent + +} + public static void parseMsg() { FhirContext ctx = new FhirContext(Patient.class, Observation.class); diff --git a/hapi-fhir-base/src/site/example/java/example/RestfulPatientResourceProviderMore.java b/hapi-fhir-base/src/site/example/java/example/RestfulPatientResourceProviderMore.java index fc5bf590148..e6211f7566a 100644 --- a/hapi-fhir-base/src/site/example/java/example/RestfulPatientResourceProviderMore.java +++ b/hapi-fhir-base/src/site/example/java/example/RestfulPatientResourceProviderMore.java @@ -28,6 +28,7 @@ import ca.uhn.fhir.model.dstu.valueset.QuantityCompararatorEnum; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.StringDt; import ca.uhn.fhir.parser.DataFormatException; +import ca.uhn.fhir.rest.annotation.Count; import ca.uhn.fhir.rest.annotation.Create; import ca.uhn.fhir.rest.annotation.History; import ca.uhn.fhir.rest.annotation.IdParam; @@ -38,11 +39,13 @@ import ca.uhn.fhir.rest.annotation.Read; import ca.uhn.fhir.rest.annotation.RequiredParam; import ca.uhn.fhir.rest.annotation.ResourceParam; import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.annotation.Since; import ca.uhn.fhir.rest.annotation.Update; import ca.uhn.fhir.rest.annotation.Validate; import ca.uhn.fhir.rest.annotation.VersionIdParam; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.client.ITestClient; +import ca.uhn.fhir.rest.client.api.IBasicClient; import ca.uhn.fhir.rest.client.api.IRestfulClient; import ca.uhn.fhir.rest.param.CodingListParam; import ca.uhn.fhir.rest.param.DateRangeParam; @@ -443,22 +446,29 @@ public interface MetadataClient extends IRestfulClient { } //END SNIPPET: metadataClient -public interface HistoryClient { //START SNIPPET: historyClient -// Server level (history of ALL resources) -@History -Bundle getHistoryServer(); +public interface HistoryClient extends IBasicClient { + /** Server level (history of ALL resources) */ + @History + Bundle getHistoryServer(); -// Type level (history of all resources of a given type) -@History(type=Patient.class) -Bundle getHistoryPatientType(); + /** Type level (history of all resources of a given type) */ + @History(type=Patient.class) + Bundle getHistoryPatientType(); + + /** Instance level (history of a specific resource instance by type and ID) */ + @History(type=Patient.class) + Bundle getHistoryPatientInstance(@IdParam IdDt theId); + + /** + * Either (or both) of the "since" and "count" paramaters can + * also be included in any of the methods above. + */ + @History + Bundle getHistoryServerWithCriteria(@Since Date theDate, @Count int theCount); -// Instance level (history of a specific resource instance by type and ID) -@History(type=Patient.class) -Bundle getHistoryPatientInstance(@IdParam IdDt theId); -//END SNIPPET: historyClient - } +//END SNIPPET: historyClient public void bbbbb() throws DataFormatException, IOException { diff --git a/hapi-fhir-base/src/site/xdoc/doc_intro.xml b/hapi-fhir-base/src/site/xdoc/doc_intro.xml index 1f87384614d..259b61ce3e5 100644 --- a/hapi-fhir-base/src/site/xdoc/doc_intro.xml +++ b/hapi-fhir-base/src/site/xdoc/doc_intro.xml @@ -109,6 +109,9 @@ + +

This code gives the following output: @@ -129,6 +132,26 @@ + + +

+ Much of the HAPI FHIR API is designed using a fluent style, + where method calls can be chained in a natural way. This + leads to tighter and easier-to-read code. +

+ +

+ The following snippet is functionally identical to the + example above: +

+ + + + + + + +

diff --git a/hapi-fhir-base/src/site/xdoc/doc_rest_operations.xml b/hapi-fhir-base/src/site/xdoc/doc_rest_operations.xml index 7e003ca0da7..96567adbf90 100644 --- a/hapi-fhir-base/src/site/xdoc/doc_rest_operations.xml +++ b/hapi-fhir-base/src/site/xdoc/doc_rest_operations.xml @@ -10,6 +10,14 @@

+

+ Jump To... +

+ +

RESTful Clients and Servers both share the same method pattern, with one key difference: A client @@ -26,6 +34,10 @@ implementations, but client methods will follow the same patterns.

+ +
+ +

The following table lists the operations supported by HAPI FHIR RESTful Servers and Clients. @@ -81,7 +93,7 @@ - Type - Create + Type - Create Create a new resource with a server assigned id @@ -99,14 +111,6 @@ Search the resource type based on some filter criteria - - - Type - Search - - - Search the resource type based on some filter criteria - - Type - History @@ -892,7 +896,7 @@

- Not yet implemented + Not yet implemented - Get in touch if you would like to help!

@@ -905,7 +909,7 @@

- Not yet implemented + Not yet implemented - Get in touch if you would like to help!

@@ -945,18 +949,38 @@ annotated with the @IdParam annotation, indicating the ID of the resource for which to return history. +
  • + For a server + implementation, the method must either be defined in a + resource provider + or have a type() value in the @History annotation if it is + defined in a + plain provider. +
  • - For an + For a Type History method, the method must not have any @IdParam parameter. +
    • + For a server + implementation, the method must either be defined in a + resource provider + or have a type() value in the @History annotation if it is + defined in a + plain provider. +
  • - For an + For a Server History - method, the method must not have any @IdParam parameter - and must not be found in a ResourceProvider definition. - + method, the method must not have any @IdParam parameter, and + must not have a type() value specified in + the @History annotation. +
    • + In a server implementation, the method must + be defined in a plain provider. +
  • @@ -977,6 +1001,32 @@ + +

    + +
    + +

    + When implementing a server operation, there are a number of failure conditions + specified. For example, an + Instance Read request might specify an unknown + resource ID, or a Type Create request might contain an + invalid resource which can not be created. +

    +

    + In these cases, an appropriate exception should be thrown. The HAPI RESTful + API includes a set of exceptions extending + BaseServerResponseException + which represent specific HTTP failure codes. +

    +

    + See the + Exceptions List + for a complete list of these exceptions. Note that these exceptions are all unchecked + exceptions, so they do not need to ne explicitly declared in the method + signature. +

    +
    diff --git a/hapi-fhir-base/src/site/xdoc/doc_rest_server.xml b/hapi-fhir-base/src/site/xdoc/doc_rest_server.xml index 53b1c5c401d..606d78e649f 100644 --- a/hapi-fhir-base/src/site/xdoc/doc_rest_server.xml +++ b/hapi-fhir-base/src/site/xdoc/doc_rest_server.xml @@ -26,6 +26,7 @@ be possible to create a FHIR compliant server quickly and easily.

    +

    @@ -90,8 +91,56 @@ + + + + +

    + Defining one provider per resource is a good strategy to keep + code readable and maintainable, but it is also possible to put + methods for multiple resource types in a provider class. Providers + which do not implement the + IResourceProvider + (and therefore are not bound to one specific resource type) are known as + Plain Providers. +

    +

    + A plain provider may implement any + RESTful operation, but will generally + need to explicitly state what type of resource it applies to. If the method directly + returns a resource or a collection of resources (as in an + instance read or + type search operation) + the resource type will be inferred automatically. If the method returns a + Bundle + resource, it is necessary to explicitly specify the resource type + in the method annotation. The following example shows this: +

    + + + + + +

    + In addition, some methods are not resource specific. For example, the + system history operation + returns historical versions of all resource types on a server, + so it needs to be defined in a plain provider. +

    + +

    + Once you have defined your resource providers, they are passed to the + server in a similar way to the resource providers. +

    + + + + + + +

    @@ -99,6 +148,12 @@ you can bundle these into a WAR file and you are ready to deploy to any JEE container (Tomcat, Websphere, Glassfish, etc).

    + +

    + Bundling a servlet into a WAR file and deploying it to an application server + is beyond the scope of this page, but there are many good tutorials on how + to do this. +

    @@ -167,6 +222,22 @@
    --> + +
    + +

    + Your RESTful server should now support the methods you have declared. Here are a + few helpful tricks for interacting with the server: +

    + +

    + Pretty Printing: The HAPI RESTful server supports a non-standard parameter called + _pretty, which can be used to request that responses be pretty-printed (indented for + easy reading by humans) by setting the value to true. This can be useful in testing. An example URL for this might be:
    + http://example.com/fhir/Patient/_search?name=TESTING&_pretty=true +

    + +
    diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/client/ClientTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/client/ClientTest.java index 18257be1121..d47b6113e15 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/client/ClientTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/client/ClientTest.java @@ -3,6 +3,7 @@ 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.Arrays; @@ -27,6 +28,8 @@ 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.api.Bundle; @@ -41,6 +44,7 @@ import ca.uhn.fhir.model.dstu.resource.Patient; import ca.uhn.fhir.model.dstu.valueset.QuantityCompararatorEnum; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.InstantDt; +import ca.uhn.fhir.model.primitive.IntegerDt; import ca.uhn.fhir.model.primitive.StringDt; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.param.CodingListParam; @@ -249,6 +253,50 @@ public class ClientTest { } } + + @Test + public void testHistoryWithParams() throws Exception { + + //@formatter:off + final String msg = "<id>6c1d93be-027f-468d-9d47-f826cd15cf42</id><link rel=\"self\" href=\"http://localhost:51698/Patient/222/_history\"/><link rel=\"fhir-base\" href=\"http://localhost:51698\"/><os:totalResults xmlns:os=\"http://a9.com/-/spec/opensearch/1.1/\">2</os:totalResults><published>2014-04-13T18:24:50-04:00</published><author><name>ca.uhn.fhir.rest.method.HistoryMethodBinding</name></author><entry><title>Patient 2222221969-12-31T19:00:20.000-05:001969-12-31T19:00:10.000-05:00Patient 2222221969-12-31T19:00:30.000-05:001969-12-31T19:00:10.000-05:00"; + //@formatter:on + + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(httpClient.execute(capt.capture())).thenReturn(httpResponse); + when(httpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(httpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_ATOM_XML + "; charset=UTF-8")); + when(httpResponse.getEntity().getContent()).thenAnswer(new Answer() { + @Override + public InputStream answer(InvocationOnMock theInvocation) throws Throwable { + return new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8")); + } + }); + + + ITestClient client = ctx.newRestfulClient(ITestClient.class, "http://foo"); + + client.getHistoryPatientInstance(new IdDt("111"), new InstantDt("2012-01-02T00:01:02"), new IntegerDt(12)); + assertEquals("http://foo/Patient/111/_history?_since=2012-01-02T00%3A01%3A02&_count=12", capt.getAllValues().get(0).getURI().toString()); + + String expectedDateString= new InstantDt(new InstantDt("2012-01-02T00:01:02").getValue()).getValueAsString(); // ensures the local timezone + expectedDateString=expectedDateString.replace(":", "%3A"); + client.getHistoryPatientInstance(new IdDt("111"), new InstantDt("2012-01-02T00:01:02").getValue(), new IntegerDt(12).getValue()); + assertEquals("http://foo/Patient/111/_history?_since="+expectedDateString+"&_count=12", capt.getAllValues().get(1).getURI().toString()); + + client.getHistoryPatientInstance(new IdDt("111"), null, new IntegerDt(12)); + assertEquals("http://foo/Patient/111/_history?_count=12", capt.getAllValues().get(2).getURI().toString()); + + client.getHistoryPatientInstance(new IdDt("111"), new InstantDt("2012-01-02T00:01:02"), null); + assertEquals("http://foo/Patient/111/_history?_since=2012-01-02T00%3A01%3A02", capt.getAllValues().get(3).getURI().toString()); + + client.getHistoryPatientInstance(new IdDt("111"), new InstantDt(), new IntegerDt(12)); + assertEquals("http://foo/Patient/111/_history?_count=12", capt.getAllValues().get(2).getURI().toString()); + + client.getHistoryPatientInstance(new IdDt("111"), new InstantDt("2012-01-02T00:01:02"), new IntegerDt()); + assertEquals("http://foo/Patient/111/_history?_since=2012-01-02T00%3A01%3A02", capt.getAllValues().get(3).getURI().toString()); + + } + @Test public void testHistoryResourceType() throws Exception { @@ -408,7 +456,7 @@ public class ClientTest { DateRangeParam param = new DateRangeParam(); param.setLowerBound(new QualifiedDateParam(QuantityCompararatorEnum.GREATERTHAN_OR_EQUALS, "2011-01-01")); param.setUpperBound(new QualifiedDateParam(QuantityCompararatorEnum.LESSTHAN_OR_EQUALS, "2021-01-01")); - List response = client.getPatientByDateRange(param); + client.getPatientByDateRange(param); assertEquals("http://foo/Patient?dateRange=%3E%3D2011-01-01&dateRange=%3C%3D2021-01-01", capt.getValue().getURI().toString()); @@ -467,7 +515,7 @@ public class ClientTest { CodingListParam identifiers = new CodingListParam(); identifiers.add(new CodingDt("foo", "bar")); identifiers.add(new CodingDt("baz", "boz")); - List response = client.getPatientMultipleIdentifiers(identifiers); + client.getPatientMultipleIdentifiers(identifiers); assertEquals("http://foo/Patient?ids=foo%7Cbar%2Cbaz%7Cboz", capt.getValue().getURI().toString()); diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/client/ITestClient.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/client/ITestClient.java index b58717d2591..54ca86758dc 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/client/ITestClient.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/client/ITestClient.java @@ -1,5 +1,6 @@ package ca.uhn.fhir.rest.client; +import java.util.Date; import java.util.List; import ca.uhn.fhir.model.api.Bundle; @@ -8,7 +9,10 @@ import ca.uhn.fhir.model.dstu.composite.IdentifierDt; import ca.uhn.fhir.model.dstu.resource.DiagnosticReport; import ca.uhn.fhir.model.dstu.resource.Patient; import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.model.primitive.InstantDt; +import ca.uhn.fhir.model.primitive.IntegerDt; import ca.uhn.fhir.model.primitive.StringDt; +import ca.uhn.fhir.rest.annotation.Count; import ca.uhn.fhir.rest.annotation.Create; import ca.uhn.fhir.rest.annotation.Delete; import ca.uhn.fhir.rest.annotation.History; @@ -19,6 +23,7 @@ import ca.uhn.fhir.rest.annotation.Read; import ca.uhn.fhir.rest.annotation.RequiredParam; import ca.uhn.fhir.rest.annotation.ResourceParam; import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.annotation.Since; import ca.uhn.fhir.rest.annotation.Update; import ca.uhn.fhir.rest.annotation.VersionIdParam; import ca.uhn.fhir.rest.api.MethodOutcome; @@ -34,10 +39,10 @@ public interface ITestClient extends IBasicClient { @Search() public List getPatientByDateRange(@RequiredParam(name = "dateRange") DateRangeParam theIdentifiers); - + @Search() public List getPatientByDob(@RequiredParam(name=Patient.SP_BIRTHDATE) QualifiedDateParam theBirthDate); - + @Search() public List getPatientMultipleIdentifiers(@RequiredParam(name = "ids") CodingListParam theIdentifiers); @@ -46,13 +51,13 @@ public interface ITestClient extends IBasicClient { @Search(queryName="someQueryOneParam") public Patient getPatientOneParam(@RequiredParam(name="param1") StringDt theParam); - + @Search() public Patient getPatientWithIncludes(@RequiredParam(name = "withIncludes") StringDt theString, @IncludeParam List theIncludes); @Update public MethodOutcome updatePatient(@IdParam IdDt theId, @VersionIdParam IdDt theVersion, @ResourceParam Patient thePatient); - + @Update public MethodOutcome updatePatient(@IdParam IdDt theId, @ResourceParam Patient thePatient); @@ -71,6 +76,12 @@ public interface ITestClient extends IBasicClient { @History(type=Patient.class) Bundle getHistoryPatientInstance(@IdParam IdDt theId); + @History(type=Patient.class) + Bundle getHistoryPatientInstance(@IdParam IdDt theId, @Since InstantDt theSince, @Count IntegerDt theCount); + + @History(type=Patient.class) + Bundle getHistoryPatientInstance(@IdParam IdDt theId, @Since Date theSince, @Count Integer theCount); + @History(type=Patient.class) Bundle getHistoryPatientType(); diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/NonResourceProviderServerTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/NonResourceProviderServerTest.java index 5481a2c9919..d30259f76cd 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/NonResourceProviderServerTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/NonResourceProviderServerTest.java @@ -2,7 +2,9 @@ package ca.uhn.fhir.rest.server; import static org.junit.Assert.*; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -15,69 +17,83 @@ 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.BeforeClass; +import org.hamcrest.core.IsEqual; +import org.hamcrest.core.StringStartsWith; +import org.junit.After; +import org.junit.Before; import org.junit.Test; 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.api.ResourceMetadataKeyEnum; import ca.uhn.fhir.model.dstu.composite.HumanNameDt; import ca.uhn.fhir.model.dstu.composite.IdentifierDt; +import ca.uhn.fhir.model.dstu.resource.Organization; import ca.uhn.fhir.model.dstu.resource.Patient; import ca.uhn.fhir.model.dstu.valueset.IdentifierUseEnum; import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.model.primitive.InstantDt; +import ca.uhn.fhir.model.primitive.IntegerDt; import ca.uhn.fhir.model.primitive.UriDt; +import ca.uhn.fhir.rest.annotation.Count; +import ca.uhn.fhir.rest.annotation.History; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Read; import ca.uhn.fhir.rest.annotation.RequiredParam; import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.annotation.Since; import ca.uhn.fhir.testutil.RandomServerPortProvider; public class NonResourceProviderServerTest { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(NonResourceProviderServerTest.class); - private static int ourPort; - private static Server ourServer; - private static CloseableHttpClient ourClient; - private static FhirContext ourCtx; + private int myPort; + private Server myServer; + private CloseableHttpClient myClient; + private FhirContext myCtx; + private RestfulServer myRestfulServer; - @BeforeClass - public static void beforeClass() throws Exception { - ourPort = RandomServerPortProvider.findFreePort(); - ourServer = new Server(ourPort); - ourCtx = new FhirContext(Patient.class); + @Before + public void before() throws Exception { + myPort = RandomServerPortProvider.findFreePort(); + myServer = new Server(myPort); + myCtx = new FhirContext(Patient.class); ServletHandler proxyHandler = new ServletHandler(); - ServletHolder servletHolder = new ServletHolder(new DummyRestfulServer()); + ServletHolder servletHolder = new ServletHolder(); + myRestfulServer = new RestfulServer(); + servletHolder.setServlet(myRestfulServer); proxyHandler.addServletWithMapping(servletHolder, "/fhir/context/*"); - ourServer.setHandler(proxyHandler); - ourServer.start(); + myServer.setHandler(proxyHandler); PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); HttpClientBuilder builder = HttpClientBuilder.create(); builder.setConnectionManager(connectionManager); - ourClient = builder.build(); + myClient = builder.build(); } - @AfterClass - public static void afterClass() throws Exception { - ourServer.stop(); + @After + public void after() throws Exception { + myServer.stop(); } @Test public void testSearchByParamIdentifier() throws Exception { + myRestfulServer.setProviders(new SearchProvider()); + myServer.start(); - String baseUri = "http://localhost:" + ourPort + "/fhir/context"; + String baseUri = "http://localhost:" + myPort + "/fhir/context"; String uri = baseUri + "/Patient?identifier=urn:hapitest:mrns%7C00001"; HttpGet httpGet = new HttpGet(uri); - HttpResponse status = ourClient.execute(httpGet); + HttpResponse status = myClient.execute(httpGet); String responseContent = IOUtils.toString(status.getEntity().getContent()); ourLog.info("Response was:\n{}", responseContent); assertEquals(200, status.getStatusLine().getStatusCode()); - Bundle bundle = ourCtx.newXmlParser().parseBundle(responseContent); + Bundle bundle = myCtx.newXmlParser().parseBundle(responseContent); assertEquals(1, bundle.getEntries().size()); @@ -87,34 +103,60 @@ public class NonResourceProviderServerTest { assertEquals(uri, bundle.getLinkSelf().getValue()); assertEquals(baseUri, bundle.getLinkBase().getValue()); } + + @Test + public void testGlobalHistory() throws Exception { + GlobalHistoryProvider provider = new GlobalHistoryProvider(); + myRestfulServer.setProviders(provider); + myServer.start(); - public static class DummyRestfulServer extends RestfulServer { + String baseUri = "http://localhost:" + myPort + "/fhir/context"; + HttpResponse status = myClient.execute(new HttpGet(baseUri + "/_history?_since=2012-01-02T00%3A01%3A02&_count=12")); - private static final long serialVersionUID = 1L; + String responseContent = IOUtils.toString(status.getEntity().getContent()); + ourLog.info("Response was:\n{}", responseContent); + assertEquals(200, status.getStatusLine().getStatusCode()); + Bundle bundle = myCtx.newXmlParser().parseBundle(responseContent); + assertEquals(3, bundle.getEntries().size()); + + assertThat(provider.myLastSince.getValueAsString(), StringStartsWith.startsWith("2012-01-02T00:01:02")); + assertThat(provider.myLastCount.getValueAsString(), IsEqual.equalTo("12")); - public DummyRestfulServer() { - setProviders(new DummyProvider()); - } + status = myClient.execute(new HttpGet(baseUri + "/_history?&_count=12")); + responseContent = IOUtils.toString(status.getEntity().getContent()); + assertEquals(200, status.getStatusLine().getStatusCode()); + bundle = myCtx.newXmlParser().parseBundle(responseContent); + assertEquals(3, bundle.getEntries().size()); + assertNull(provider.myLastSince.getValueAsString()); + assertThat(provider.myLastCount.getValueAsString(), IsEqual.equalTo("12")); + status =myClient.execute(new HttpGet(baseUri + "/_history?_since=2012-01-02T00%3A01%3A02")); + responseContent = IOUtils.toString(status.getEntity().getContent()); + assertEquals(200, status.getStatusLine().getStatusCode()); + bundle = myCtx.newXmlParser().parseBundle(responseContent); + assertEquals(3, bundle.getEntries().size()); + assertThat(provider.myLastSince.getValueAsString(), StringStartsWith.startsWith("2012-01-02T00:01:02")); + assertNull(provider.myLastCount.getValueAsString()); + + status =myClient.execute(new HttpGet(baseUri + "/_history")); + responseContent = IOUtils.toString(status.getEntity().getContent()); + assertEquals(200, status.getStatusLine().getStatusCode()); + bundle = myCtx.newXmlParser().parseBundle(responseContent); + assertEquals(3, bundle.getEntries().size()); + assertNull(provider.myLastSince.getValueAsString()); + assertNull(provider.myLastCount.getValueAsString()); + } - + /** * Created by dsotnikov on 2/25/2014. */ - public static class DummyProvider { + public static class SearchProvider { public Map getIdToPatient() { Map idToPatient = new HashMap(); { - Patient patient = new Patient(); - patient.addIdentifier(); - patient.getIdentifier().get(0).setUse(IdentifierUseEnum.OFFICIAL); - patient.getIdentifier().get(0).setSystem(new UriDt("urn:hapitest:mrns")); - patient.getIdentifier().get(0).setValue("00001"); - patient.addName(); - patient.getName().get(0).addFamily("Test"); - patient.getName().get(0).addGiven("PatientOne"); - patient.getGender().setText("M"); + Patient patient = createPatient(); idToPatient.put("1", patient); } { @@ -158,4 +200,64 @@ public class NonResourceProviderServerTest { } + public static class GlobalHistoryProvider { + + private InstantDt myLastSince; + private IntegerDt myLastCount; + + @History + public List getGlobalHistory(@Since InstantDt theSince, @Count IntegerDt theCount) { + myLastSince = theSince; + myLastCount = theCount; + ArrayList retVal = new ArrayList(); + + IResource p = createPatient(); + p.setId(new IdDt("1")); + p.getResourceMetadata().put(ResourceMetadataKeyEnum.VERSION_ID, new IdDt("A")); + p.getResourceMetadata().put(ResourceMetadataKeyEnum.PUBLISHED, new InstantDt("2012-01-01T00:00:01")); + p.getResourceMetadata().put(ResourceMetadataKeyEnum.UPDATED, new InstantDt("2012-01-01T01:00:01")); + retVal.add(p); + + p = createPatient(); + p.setId(new IdDt("1")); + p.getResourceMetadata().put(ResourceMetadataKeyEnum.VERSION_ID, new IdDt("B")); + p.getResourceMetadata().put(ResourceMetadataKeyEnum.PUBLISHED, new InstantDt("2012-01-01T00:00:01")); + p.getResourceMetadata().put(ResourceMetadataKeyEnum.UPDATED, new InstantDt("2012-01-01T01:00:03")); + retVal.add(p); + + p = createOrganization(); + p.setId(new IdDt("1")); + p.getResourceMetadata().put(ResourceMetadataKeyEnum.VERSION_ID, new IdDt("A")); + p.getResourceMetadata().put(ResourceMetadataKeyEnum.PUBLISHED, new InstantDt("2013-01-01T00:00:01")); + p.getResourceMetadata().put(ResourceMetadataKeyEnum.UPDATED, new InstantDt("2013-01-01T01:00:01")); + retVal.add(p); + + return retVal; + } + + } + + private static Patient createPatient() { + Patient patient = new Patient(); + patient.addIdentifier(); + patient.getIdentifier().get(0).setUse(IdentifierUseEnum.OFFICIAL); + patient.getIdentifier().get(0).setSystem(new UriDt("urn:hapitest:mrns")); + patient.getIdentifier().get(0).setValue("00001"); + patient.addName(); + patient.getName().get(0).addFamily("Test"); + patient.getName().get(0).addGiven("PatientOne"); + patient.getGender().setText("M"); + return patient; + } + + private static Organization createOrganization() { + Organization retVal = new Organization(); + retVal.addIdentifier(); + retVal.getIdentifier().get(0).setUse(IdentifierUseEnum.OFFICIAL); + retVal.getIdentifier().get(0).setSystem(new UriDt("urn:hapitest:mrns")); + retVal.getIdentifier().get(0).setValue("00001"); + retVal.getName().setValue("Test Org"); + return retVal; + } + } diff --git a/restful-server-example/.classpath b/restful-server-example/.classpath index 3cb26e91427..e526251b0a7 100644 --- a/restful-server-example/.classpath +++ b/restful-server-example/.classpath @@ -5,11 +5,6 @@ - - - - - @@ -42,5 +37,6 @@ + diff --git a/restful-server-example/src/main/java/ca/uhn/example/rest/RestfulPatientResourceProvider.java b/restful-server-example/src/main/java/ca/uhn/example/rest/RestfulPatientResourceProvider.java index e07b266bfc7..13d0f0e52a4 100644 --- a/restful-server-example/src/main/java/ca/uhn/example/rest/RestfulPatientResourceProvider.java +++ b/restful-server-example/src/main/java/ca/uhn/example/rest/RestfulPatientResourceProvider.java @@ -1,7 +1,6 @@ package ca.uhn.example.rest; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -10,7 +9,6 @@ import ca.uhn.fhir.model.dstu.composite.HumanNameDt; import ca.uhn.fhir.model.dstu.resource.OperationOutcome; import ca.uhn.fhir.model.dstu.resource.Patient; import ca.uhn.fhir.model.dstu.valueset.AdministrativeGenderCodesEnum; -import ca.uhn.fhir.model.dstu.valueset.IdentifierUseEnum; import ca.uhn.fhir.model.dstu.valueset.IssueSeverityEnum; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.StringDt;