diff --git a/hapi-fhir-base/pom.xml b/hapi-fhir-base/pom.xml index ef50c521aef..c1f81bbcf68 100644 --- a/hapi-fhir-base/pom.xml +++ b/hapi-fhir-base/pom.xml @@ -303,7 +303,7 @@ - + ]]> diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/IQueryParameterAnd.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/IQueryParameterAnd.java index 4dc2f5fee06..f314975d1d4 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/IQueryParameterAnd.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/IQueryParameterAnd.java @@ -8,17 +8,21 @@ public interface IQueryParameterAnd { /** * - * @see See FHIR specification + *

+ * See FHIR specification * 2.2.2 Search SearchParameter Types * for information on the token format + *

*/ public void setValuesAsQueryTokens(List> theParameters) throws InvalidRequestException; /** * - * @see See FHIR specification + *

+ * See FHIR specification * 2.2.2 Search SearchParameter Types * for information on the token format + *

*/ public List> getValuesAsQueryTokens(); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/IQueryParameterOr.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/IQueryParameterOr.java index bceb25cbef8..674c801a154 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/IQueryParameterOr.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/IQueryParameterOr.java @@ -7,10 +7,11 @@ public interface IQueryParameterOr { /** * Sets the value of this type using the token format. This * format is used in HTTP queries as a parameter format. - * - * @see See FHIR specification + *

+ * See FHIR specification * 2.2.2 Search SearchParameter Types * for information on the token format + *

*/ public void setValuesAsQueryTokens(List theParameters); @@ -18,9 +19,11 @@ public interface IQueryParameterOr { * Returns the value of this type using the token format. This * format is used in HTTP queries as a parameter format. * - * @see See FHIR specification + *

+ * See FHIR specification * 2.2.2 Search SearchParameter Types * for information on the token format + *

*/ public List getValuesAsQueryTokens(); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/IQueryParameterType.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/IQueryParameterType.java index 1cf5406f790..eea6794422b 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/IQueryParameterType.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/IQueryParameterType.java @@ -6,9 +6,10 @@ public interface IQueryParameterType { * Sets the value of this type using the token format. This * format is used in HTTP queries as a parameter format. * - * @see See FHIR specification + *

See FHIR specification * 2.2.2 Search SearchParameter Types * for information on the token format + *

*/ public void setValueAsQueryToken(String theParameter); @@ -16,9 +17,10 @@ public interface IQueryParameterType { * Returns the value of this type using the token format. This * format is used in HTTP queries as a parameter format. * - * @see See FHIR specification + *

See FHIR specification * 2.2.2 Search SearchParameter Types * for information on the token format + *

*/ public String getValueAsQueryToken(); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/ISupportsUndeclaredExtensions.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/ISupportsUndeclaredExtensions.java index c334840ff83..42e477c87ad 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/ISupportsUndeclaredExtensions.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/ISupportsUndeclaredExtensions.java @@ -37,7 +37,7 @@ public interface ISupportsUndeclaredExtensions extends IElement { * * * - * @param The extension to add. Can not be null. + * @param theExtension The extension to add. Can not be null. */ void addUndeclaredExtension(ExtensionDt theExtension); 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 09d2b61dcbe..ac77cbf93d9 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 @@ -6,6 +6,7 @@ import java.util.TimeZone; import ca.uhn.fhir.model.api.TemporalPrecisionEnum; import ca.uhn.fhir.model.api.annotation.DatatypeDef; import ca.uhn.fhir.model.api.annotation.SimpleSetter; +import ca.uhn.fhir.parser.DataFormatException; @DatatypeDef(name="instant") public class InstantDt extends BaseDateTimeDt { @@ -46,6 +47,17 @@ public class InstantDt extends BaseDateTimeDt { setTimeZone(TimeZone.getDefault()); } + /** + * Create a new InstantDt from a string value + * + * @param theString The string representation of the string. Must be in + * a valid format according to the FHIR specification + * @throws DataFormatException + */ + public InstantDt(String theString) { + setValueAsString(theString); + } + @Override boolean isPrecisionAllowed(TemporalPrecisionEnum thePrecision) { switch (thePrecision) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/IntegerDt.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/IntegerDt.java index 409e0f4e336..b10d9f3f7ff 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/IntegerDt.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/IntegerDt.java @@ -5,7 +5,7 @@ import ca.uhn.fhir.model.api.annotation.DatatypeDef; import ca.uhn.fhir.model.api.annotation.SimpleSetter; import ca.uhn.fhir.parser.DataFormatException; -@DatatypeDef(name="integer") +@DatatypeDef(name = "integer") public class IntegerDt extends BasePrimitive { private Integer myValue; @@ -24,7 +24,17 @@ public class IntegerDt extends BasePrimitive { public IntegerDt(@SimpleSetter.Parameter(name = "theInteger") int theInteger) { setValue(theInteger); } - + + /** + * Constructor + * + * @param theIntegerAsString A string representation of an integer + * @throws DataFormatException If the string is not a valid integer representation + */ + public IntegerDt(String theIntegerAsString) { + setValueAsString(theIntegerAsString); + } + @Override public Integer getValue() { return myValue; @@ -39,17 +49,21 @@ public class IntegerDt extends BasePrimitive { public void setValueAsString(String theValue) throws DataFormatException { if (theValue == null) { myValue = null; - }else { - myValue = Integer.parseInt(theValue); + } else { + try { + myValue = Integer.parseInt(theValue); + } catch (NumberFormatException e) { + throw new DataFormatException(e); + } } } @Override public String getValueAsString() { - if (myValue==null) { + if (myValue == null) { return null; } return Integer.toString(myValue); } - + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Count.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Count.java new file mode 100644 index 00000000000..b0e40ff5e13 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Count.java @@ -0,0 +1,18 @@ +package ca.uhn.fhir.rest.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Parameter annotation for the _count parameter, which indicates to the + * server the maximum number of desired results. + * + * @see History + */ +@Target(value=ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface Count { + //nothing +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/History.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/History.java new file mode 100644 index 00000000000..b839648224d --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/History.java @@ -0,0 +1,67 @@ +package ca.uhn.fhir.rest.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import ca.uhn.fhir.model.api.IResource; +import ca.uhn.fhir.model.dstu.resource.Patient; +import ca.uhn.fhir.model.primitive.IdDt; + +/** + * RESTful method annotation to be used for the FHIR + * history method. + * + *

+ * History returns a feed containing all versions (or a selected range of versions) of + * a resource or a specific set of resources. + *

+ *

+ * The history command supports three usage patterns, as described in the + * FHIR history documentation: + *

    + *
  • + * A search for the history of all resources on a server. In this case, {@link #resourceType()} + * should be set to {@link AllResources} (as is the default) and the method should not have an ID parameter. + *
    • + * To invoke this pattern: GET [base]/_history{?[parameters]&_format=[mime-type]} + *
    + *
  • + *
  • + * A search for the history of all instances of a specific resource type on a server. In this case, {@link #resourceType()} + * should be set to the specific resource type (e.g. {@link Patient Patient.class} and the method should not have an ID parameter. + *
    • + * To invoke this pattern: GET [base]/[type]/_history{?[parameters]&_format=[mime-type]} + *
    + *
  • + *
  • + * A search for the history of a specific instances of a specific resource type on a server. In this case, {@link #resourceType()} + * should be set to the specific resource type (e.g. {@link Patient Patient.class} and the method should + * have one parameter of type {@link IdDt} annotated with the {@link IdParam} annotation. + *
    • + * To invoke this pattern: GET [base]/[type]/[id]/_history{?[parameters]&_format=[mime-type]} + *
    + *
  • + *
+ *

+ * + * @see Count + * @see Since + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(value=ElementType.METHOD) +public @interface History { + + /** + * The resource type that this method applies to. See the {@link History History annotation type documentation} + * for information on usage patterns. + */ + Class resourceType() default AllResources.class; + + + interface AllResources extends IResource { + // nothing + } + +} \ No newline at end of file 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 e68ff8e4dec..238361d1542 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 @@ -21,8 +21,10 @@ public @interface Search { /** * If specified, this the name for the Named Query * - * @see See the FHIR specification section on + *

+ * See the FHIR specification section on * named queries + *

*/ String queryName() default ""; 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 new file mode 100644 index 00000000000..ca0c164883b --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Since.java @@ -0,0 +1,18 @@ +package ca.uhn.fhir.rest.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Parameter annotation for the _since parameter, which indicates to the + * server that only results dated since the given instant will be returned. + * + * @see History + */ +@Target(value=ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface Since { + //nothing +} 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 5186b6a27f3..0ba73755edc 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 @@ -1,6 +1,7 @@ package ca.uhn.fhir.rest.method; import java.io.IOException; +import java.io.PushbackReader; import java.io.Reader; import java.io.Writer; import java.lang.reflect.InvocationTargetException; @@ -48,7 +49,8 @@ public abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBindin if (!theMethod.getReturnType().equals(MethodOutcome.class)) { if (!allowVoidReturnType()) { - throw new ConfigurationException("Method " + theMethod.getName() + " in type " + theMethod.getDeclaringClass().getCanonicalName() + " is a @" + theMethodAnnotation.getSimpleName() + " method but it does not return " + MethodOutcome.class); + throw new ConfigurationException("Method " + theMethod.getName() + " in type " + theMethod.getDeclaringClass().getCanonicalName() + " is a @" + theMethodAnnotation.getSimpleName() + + " method but it does not return " + MethodOutcome.class); } else if (theMethod.getReturnType() == void.class) { myReturnVoid = true; } @@ -76,9 +78,27 @@ public abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBindin if (theResponseStatusCode != Constants.STATUS_HTTP_204_NO_CONTENT) { EncodingUtil ct = EncodingUtil.forContentType(theResponseMimeType); if (ct != null) { - IParser parser = ct.newParser(getContext()); - OperationOutcome outcome = parser.parseResource(OperationOutcome.class, theResponseReader); - retVal.setOperationOutcome(outcome); + PushbackReader reader = new PushbackReader(theResponseReader); + + try { + int firstByte = reader.read(); + if (firstByte == -1) { + ourLog.debug("No content in response, not going to read"); + reader = null; + } else { + reader.unread(firstByte); + } + } catch (IOException e) { + ourLog.debug("No content in response, not going to read", e); + reader = null; + } + + if (reader != null) { + IParser parser = ct.newParser(getContext()); + OperationOutcome outcome = parser.parseResource(OperationOutcome.class, reader); + retVal.setOperationOutcome(outcome); + } + } else { ourLog.debug("Ignoring response content of type: {}", theResponseMimeType); } @@ -191,8 +211,7 @@ public abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBindin } /** - * Subclasses may override to allow a void method return type, which is - * allowable for some methods (e.g. delete) + * Subclasses may override to allow a void method return type, which is allowable for some methods (e.g. delete) */ protected boolean allowVoidReturnType() { return false; 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 new file mode 100644 index 00000000000..e4dce42aeed --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HistoryMethodBinding.java @@ -0,0 +1,142 @@ +package ca.uhn.fhir.rest.method; + +import java.io.IOException; +import java.io.Reader; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +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.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.server.Constants; +import ca.uhn.fhir.rest.server.IResourceProvider; +import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; + +public class HistoryMethodBinding extends BaseMethodBinding { + + private final Integer myIdParamIndex; + private final RestfulOperationTypeEnum myResourceOperationType; + private final Class myType; + private final RestfulOperationSystemEnum mySystemOperationType; + private String myResourceName; + private Integer mySinceParamIndex; + private Integer myCountParamIndex; + + public HistoryMethodBinding(Method theMethod, FhirContext theConetxt, IResourceProvider theProvider) { + super(theMethod, theConetxt); + + myIdParamIndex = Util.findIdParameterIndex(theMethod); + mySinceParamIndex = Util.findSinceParameterIndex(theMethod); + myCountParamIndex = Util.findCountParameterIndex(theMethod); + + History historyAnnotation = theMethod.getAnnotation(History.class); + Class type = historyAnnotation.resourceType(); + if (type == History.AllResources.class) { + if (theProvider != null) { + type = theProvider.getResourceType(); + if (myIdParamIndex != null) { + myResourceOperationType = RestfulOperationTypeEnum.HISTORY_INSTANCE; + } else { + myResourceOperationType = RestfulOperationTypeEnum.HISTORY_TYPE; + } + mySystemOperationType = null; + } else { + myResourceOperationType = null; + mySystemOperationType = RestfulOperationSystemEnum.HISTORY_SYSTEM; + } + } else { + if (myIdParamIndex != null) { + myResourceOperationType = RestfulOperationTypeEnum.HISTORY_INSTANCE; + } else { + myResourceOperationType = RestfulOperationTypeEnum.HISTORY_TYPE; + } + mySystemOperationType = null; + } + + if (type != History.AllResources.class) { + myResourceName = theConetxt.getResourceDefinition(type).getName(); + myType = type; + } else { + myResourceName = null; + myType = null; + } + + } + + @Override + public RestfulOperationTypeEnum getResourceOperationType() { + return myResourceOperationType; + } + + @Override + public RestfulOperationSystemEnum getSystemOperationType() { + return mySystemOperationType; + } + + @Override + public BaseClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Object invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) throws IOException, BaseServerResponseException { + // TODO Auto-generated method stub + return null; + } + + @Override + public void invokeServer(RestfulServer theServer, Request theRequest, HttpServletResponse theResponse) throws BaseServerResponseException, IOException { + Object[] args = new Object[getMethod().getParameterTypes().length]; + if (myCountParamIndex != null) { + String[] countValues = theRequest.getParameters().remove(Constants.PARAM_COUNT); + if (countValues.length > 0 && StringUtils.isNotBlank(countValues[0])) { + try { + args[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 { + args[mySinceParamIndex] = new InstantDt(sinceValues[0]); + } catch (DataFormatException e) { + throw new InvalidRequestException("Invalid _since parameter value: " + sinceValues[0]); + } + } + } + } + + @Override + public boolean matches(Request theRequest) { + if (!theRequest.getOperation().equals(Constants.PARAM_HISTORY)) { + return false; + } + if (theRequest.getResourceName() == null) { + return mySystemOperationType == RestfulOperationSystemEnum.HISTORY_SYSTEM; + } + if (!theRequest.getResourceName().equals(myResourceName)) { + return false; + } + + return false; + } + +} 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 d4aba4dfca2..b618873ac13 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 @@ -13,11 +13,13 @@ import java.util.Map; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.PathSpecification; +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.Since; import ca.uhn.fhir.rest.annotation.VersionIdParam; import ca.uhn.fhir.rest.param.CollectionBinder; import ca.uhn.fhir.rest.param.IParameter; @@ -30,6 +32,37 @@ import ca.uhn.fhir.util.ReflectionUtil; * Created by dsotnikov on 2/25/2014. */ class Util { + public static Integer findCountParameterIndex(Method theMethod) { + return findParamIndex(theMethod, Count.class); + } + + public static Integer findIdParameterIndex(Method theMethod) { + return findParamIndex(theMethod, IdParam.class); + } + + private static Integer findParamIndex(Method theMethod, Class toFind) { + int paramIndex = 0; + for (Annotation[] annotations : theMethod.getParameterAnnotations()) { + for (int annotationIndex = 0; annotationIndex < annotations.length; annotationIndex++) { + Annotation nextAnnotation = annotations[annotationIndex]; + Class class1 = nextAnnotation.getClass(); + if (toFind.isAssignableFrom(class1)) { + return paramIndex; + } + } + paramIndex++; + } + return null; + } + + public static Integer findSinceParameterIndex(Method theMethod) { + return findParamIndex(theMethod, Since.class); + } + + public static Integer findVersionIdParameterIndex(Method theMethod) { + return findParamIndex(theMethod, VersionIdParam.class); + } + public static Map getQueryParams(String query) { try { @@ -58,10 +91,10 @@ class Util { for (int i = 0; i < annotations.length; i++) { Annotation nextAnnotation = annotations[i]; Class parameterType = parameterTypes[paramIndex]; - + Class> outerCollectionType = null; Class> innerCollectionType = null; - + if (Collection.class.isAssignableFrom(parameterType)) { innerCollectionType = (Class>) parameterType; parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(method, paramIndex); @@ -88,14 +121,17 @@ class Util { 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 '" + method.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 '" + method.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 '" + method.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) { @@ -103,41 +139,20 @@ class Util { } else { continue; } - - haveHandledMethod= true; + + haveHandledMethod = true; parameters.add(param); - + } - + 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"); + 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"); } - + paramIndex++; } return parameters; } - public static Integer findIdParameterIndex(Method theMethod) { - return findParamIndex(theMethod, IdParam.class); - } - - public static Integer findVersionIdParameterIndex(Method theMethod) { - return findParamIndex(theMethod, VersionIdParam.class); - } - - private static Integer findParamIndex(Method theMethod, Class toFind) { - int paramIndex = 0; - for (Annotation[] annotations : theMethod.getParameterAnnotations()) { - for (int annotationIndex = 0; annotationIndex < annotations.length; annotationIndex++) { - Annotation nextAnnotation = annotations[annotationIndex]; - Class class1 = nextAnnotation.getClass(); - if (toFind.isAssignableFrom(class1)) { - return paramIndex; - } - } - paramIndex++; - } - return null; - } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java index 864dbc5c749..4079daad3e4 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java @@ -34,6 +34,8 @@ public class Constants { public static final int STATUS_HTTP_412_PRECONDITION_FAILED = 412; public static final String HEADER_CONTENT_LOCATION = "Content-Location"; public static final int STATUS_HTTP_204_NO_CONTENT = 204; + public static final String PARAM_COUNT = "_count"; + public static final String PARAM_SINCE = "_since"; static { Map valToEncoding = new HashMap(); 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 2336de75ca7..2fbab06a08c 100644 --- a/hapi-fhir-base/src/site/example/java/example/RestfulPatientResourceProviderMore.java +++ b/hapi-fhir-base/src/site/example/java/example/RestfulPatientResourceProviderMore.java @@ -39,6 +39,7 @@ import ca.uhn.fhir.rest.param.CodingListParam; import ca.uhn.fhir.rest.param.DateRangeParam; import ca.uhn.fhir.rest.param.QualifiedDateParam; import ca.uhn.fhir.rest.server.IResourceProvider; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; @@ -46,6 +47,8 @@ import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; public abstract class RestfulPatientResourceProviderMore implements IResourceProvider { private boolean detectedVersionConflict; +private boolean conflictHappened; +private boolean couldntFindThisId; //START SNIPPET: searchAll @Search public List getAllOrganizations() { @@ -63,6 +66,22 @@ public Patient getResourceById(@IdParam IdDt theId) { } //END SNIPPET: read +//START SNIPPET: delete +@Read() +public void deletePatient(@IdParam IdDt theId) { + // .. Delete the patient .. + if (couldntFindThisId) { + throw new ResourceNotFoundException("Unknown version"); + } + if (conflictHappened) { + throw new ResourceVersionConflictException("Couldn't delete because [foo]"); + } + // otherwise, delete was successful + return; // can also return MethodOutcome +} +//END SNIPPET: delete + + //START SNIPPET: vread @Read() public Patient getResourceById(@IdParam IdDt theId, diff --git a/hapi-fhir-base/src/site/resources/hapi.css b/hapi-fhir-base/src/site/resources/hapi.css index 42056b7fa04..2e58f368b31 100644 --- a/hapi-fhir-base/src/site/resources/hapi.css +++ b/hapi-fhir-base/src/site/resources/hapi.css @@ -81,6 +81,10 @@ background-color: #F8F8F8 !important; } +.table th, .table td { + padding: 2px; +} + tt { margin-left: 10px; white-space: pre; 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 154994670ae..d46b5f48a97 100644 --- a/hapi-fhir-base/src/site/xdoc/doc_rest_operations.xml +++ b/hapi-fhir-base/src/site/xdoc/doc_rest_operations.xml @@ -168,7 +168,7 @@

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

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

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

+ + + + + + +

+ Delete methods are allowed to return the following types: +

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

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

@@ -372,7 +405,7 @@

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

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 f0c743d73cc..e2745aa9633 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 @@ -104,7 +104,7 @@ public class ClientTest { ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); when(httpClient.execute(capt.capture())).thenReturn(httpResponse); when(httpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); - when(httpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(httpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_TEXT + "; charset=UTF-8")); when(httpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); when(httpResponse.getAllHeaders()).thenReturn(toHeaderArray("Location" , "http://example.com/fhir/Patient/100/_history/200")); @@ -166,7 +166,7 @@ public class ClientTest { ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); when(httpClient.execute(capt.capture())).thenReturn(httpResponse); when(httpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); - when(httpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(httpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_TEXT + "; charset=UTF-8")); when(httpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); when(httpResponse.getAllHeaders()).thenReturn(toHeaderArray("Location" , "http://example.com/fhir/Patient/100/_history/200")); @@ -181,6 +181,26 @@ public class ClientTest { assertEquals("200", response.getVersionId().getValue()); } + /** + * Return a FHIR content type, but no content and make sure we handle this without crashing + */ + @Test + public void testUpdateWithEmptyResponse() throws Exception { + + Patient patient = new Patient(); + patient.addIdentifier("urn:foo", "123"); + + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(httpClient.execute(capt.capture())).thenReturn(httpResponse); + when(httpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); + when(httpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(httpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); + when(httpResponse.getAllHeaders()).thenReturn(toHeaderArray("Location" , "http://example.com/fhir/Patient/100/_history/200")); + + ITestClient client = ctx.newRestfulClient(ITestClient.class, "http://foo"); + client.updatePatient(new IdDt("100"), new IdDt("200"), patient); + } + @Test public void testUpdateWithVersion() throws Exception { @@ -190,7 +210,7 @@ public class ClientTest { ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); when(httpClient.execute(capt.capture())).thenReturn(httpResponse); when(httpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); - when(httpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(httpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_TEXT + "; charset=UTF-8")); when(httpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); when(httpResponse.getAllHeaders()).thenReturn(toHeaderArray("Location" , "http://example.com/fhir/Patient/100/_history/200")); @@ -206,6 +226,7 @@ public class ClientTest { assertEquals("200", response.getVersionId().getValue()); } + @Test(expected=ResourceVersionConflictException.class) public void testUpdateWithResourceConflict() throws Exception {