Create method almost working

This commit is contained in:
jamesagnew 2014-03-28 07:49:01 -04:00
parent b38be51668
commit b325a5bfb1
20 changed files with 691 additions and 235 deletions

View File

@ -1,10 +0,0 @@
package ca.uhn.fhir.rest.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface PUT {
}

View File

@ -0,0 +1,21 @@
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;
/**
* RESTful method annotation to be used for the FHIR
* <a href="http://hl7.org/implement/standards/fhir/http.html#update">update</a> method.
*
* <p>
* Update is used to save an update to an existing resource (using its ID and optionally
* a version ID). It also may allow a client to save a new resource using an ID of its choosing.
* </p>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(value=ElementType.METHOD)
public @interface Update {
}

View File

@ -6,14 +6,16 @@ public class MethodOutcome {
private IdDt myId;
private IdDt myVersionId;
private boolean myCreated;
public MethodOutcome() {
}
public MethodOutcome(IdDt theId, IdDt theVersionId) {
public MethodOutcome(boolean theCreated, IdDt theId, IdDt theVersionId) {
super();
myId = theId;
myVersionId = theVersionId;
myCreated=theCreated;
}
public IdDt getId() {
@ -32,4 +34,20 @@ public class MethodOutcome {
myVersionId = theVersionId;
}
/**
* Set to <code>true</code> if the method resulted in the creation of a new resource. Set to
* <code>false</code> if the method resulted in an update/modification/removal to an existing resource.
*/
public boolean isCreated() {
return myCreated;
}
/**
* Set to <code>true</code> if the method resulted in the creation of a new resource. Set to
* <code>false</code> if the method resulted in an update/modification/removal to an existing resource.
*/
public void setCreated(boolean theCreated) {
myCreated=theCreated;
}
}

View File

@ -0,0 +1,67 @@
package ca.uhn.fhir.rest.client;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.message.BasicHeader;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.server.Constants;
public abstract class BaseClientInvocationWithContents extends BaseClientInvocation {
private final Bundle myBundle;
private final FhirContext myContext;
private List<Header> myHeaders;
private final IResource myResource;
private String myUrlExtension;
public BaseClientInvocationWithContents(FhirContext theContext, Bundle theBundle) {
super();
myContext = theContext;
myResource = null;
myBundle = theBundle;
}
public BaseClientInvocationWithContents(FhirContext theContext, IResource theResource, String theUrlExtension) {
super();
myContext = theContext;
myResource = theResource;
myBundle = null;
myUrlExtension = theUrlExtension;
}
public void addHeader(String theName, String theValue) {
if (myHeaders == null) {
myHeaders = new ArrayList<Header>();
}
myHeaders.add(new BasicHeader(theName, theValue));
}
@Override
public HttpRequestBase asHttpRequest(String theUrlBase) throws DataFormatException, IOException {
String url = theUrlBase + StringUtils.defaultString(myUrlExtension);
String contents = myContext.newXmlParser().encodeResourceToString(myResource);
StringEntity entity = new StringEntity(contents, ContentType.create(Constants.CT_FHIR_XML, "UTF-8"));
HttpRequestBase http = createRequest(url, entity);
if (myHeaders != null) {
for (Header next : myHeaders) {
http.addHeader(next);
}
}
return http;
}
protected abstract HttpRequestBase createRequest(String url, StringEntity theEntity);
}

View File

@ -1,47 +1,27 @@
package ca.uhn.fhir.rest.client;
import java.io.IOException;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.server.Constants;
public class PostClientInvocation extends BaseClientInvocation {
private final IResource myResource;
private final Bundle myBundle;
private final FhirContext myContext;
private String myUrlExtension;
public class PostClientInvocation extends BaseClientInvocationWithContents {
public PostClientInvocation(FhirContext theContext, Bundle theBundle) {
super();
myContext = theContext;
myResource = null;
myBundle = theBundle;
super(theContext, theBundle);
}
public PostClientInvocation(FhirContext theContext, IResource theResource, String theUrlExtension) {
super();
myContext = theContext;
myResource = theResource;
myBundle = null;
myUrlExtension = theUrlExtension;
super(theContext, theResource, theUrlExtension);
}
@Override
public HttpRequestBase asHttpRequest(String theUrlBase) throws DataFormatException, IOException {
HttpPost httpPost = new HttpPost(theUrlBase + StringUtils.defaultString(myUrlExtension));
String contents = myContext.newXmlParser().encodeResourceToString(myResource);
httpPost.setEntity(new StringEntity(contents, ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
return httpPost;
protected HttpPost createRequest(String url, StringEntity theEntity) {
HttpPost retVal = new HttpPost(url);
retVal.setEntity(theEntity);
return retVal;
}
}

View File

@ -0,0 +1,29 @@
package ca.uhn.fhir.rest.client;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.entity.StringEntity;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.model.api.IResource;
public class PutClientInvocation extends BaseClientInvocationWithContents {
public PutClientInvocation(FhirContext theContext, Bundle theBundle) {
super(theContext, theBundle);
}
public PutClientInvocation(FhirContext theContext, IResource theResource, String theUrlExtension) {
super(theContext, theResource, theUrlExtension);
}
@Override
protected HttpRequestBase createRequest(String url, StringEntity theEntity) {
HttpPut retVal = new HttpPut(url);
retVal.setEntity(theEntity);
return retVal;
}
}

View File

@ -25,6 +25,7 @@ import ca.uhn.fhir.rest.annotation.Create;
import ca.uhn.fhir.rest.annotation.Metadata;
import ca.uhn.fhir.rest.annotation.Read;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.annotation.Update;
import ca.uhn.fhir.rest.client.BaseClientInvocation;
import ca.uhn.fhir.rest.client.exceptions.NonFhirResponseException;
import ca.uhn.fhir.rest.server.Constants;
@ -79,7 +80,8 @@ public abstract class BaseMethodBinding {
Search search = theMethod.getAnnotation(Search.class);
Metadata conformance = theMethod.getAnnotation(Metadata.class);
Create create = theMethod.getAnnotation(Create.class);
if (!verifyMethodHasZeroOrOneOperationAnnotation(theMethod, read, search, conformance,create)) {
Update update = theMethod.getAnnotation(Update.class);
if (!verifyMethodHasZeroOrOneOperationAnnotation(theMethod, read, search, conformance,create,update)) {
return null;
}
@ -105,6 +107,8 @@ public abstract class BaseMethodBinding {
return new ConformanceMethodBinding(theMethod, theContext);
} else if (create != null) {
return new CreateMethodBinding(theMethod, theContext);
} else if (update != null) {
return new UpdateMethodBinding(theMethod, theContext);
} else {
throw new ConfigurationException("Did not detect any FHIR annotations on method '" + theMethod.getName() + "' on type: " + theMethod.getDeclaringClass().getCanonicalName());
}

View File

@ -0,0 +1,241 @@
package ca.uhn.fhir.rest.method;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.dstu.resource.OperationOutcome;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.annotation.ResourceParam;
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.ResourceParameter;
import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.rest.server.EncodingUtil;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionNotSpecifiedException;
import ca.uhn.fhir.rest.server.exceptions.UnclassifiedServerFailureException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
public abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding {
private static Set<String> ALLOWED_PARAMS;
static {
HashSet<String> set = new HashSet<String>();
set.add(Constants.PARAM_FORMAT);
ALLOWED_PARAMS = Collections.unmodifiableSet(set);
}
private int myResourceParameterIndex;
private List<IParameter> myParameters;
private String myResourceName;
public BaseOutcomeReturningMethodBinding(Method theMethod, FhirContext theContext, Class<?> theMethodAnnotation) {
super(theMethod, theContext);
myParameters = Util.getResourceParameters(theMethod);
ResourceParameter resourceParameter = null;
int index = 0;
for (IParameter next : myParameters) {
if (next instanceof ResourceParameter) {
resourceParameter = (ResourceParameter) next;
myResourceName = theContext.getResourceDefinition(resourceParameter.getResourceType()).getName();
myResourceParameterIndex = index;
index++;
}
}
if (resourceParameter == null) {
throw new ConfigurationException("Method " + theMethod.getName() + " in type " + theMethod.getDeclaringClass().getCanonicalName() + " does not have a parameter annotated with @" + ResourceParam.class.getSimpleName());
}
if (!theMethod.getReturnType().equals(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);
}
}
@Override
public BaseClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException {
IResource resource = (IResource) theArgs[myResourceParameterIndex];
if (resource == null) {
throw new NullPointerException("Resource can not be null");
}
RuntimeResourceDefinition def = getContext().getResourceDefinition(resource);
String resourceName = def.getName();
return createClientInvocation(theArgs, resource, resourceName);
}
protected abstract BaseClientInvocation createClientInvocation(Object[] theArgs, IResource resource, String resourceName);
@Override
public Object invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws IOException, BaseServerResponseException {
switch (theResponseStatusCode) {
case Constants.STATUS_HTTP_200_OK:
case Constants.STATUS_HTTP_201_CREATED:
List<String> locationHeaders = theHeaders.get("location");
MethodOutcome retVal = new MethodOutcome();
if (locationHeaders != null && locationHeaders.size() > 0) {
String locationHeader = locationHeaders.get(0);
parseContentLocation(retVal, locationHeader);
}
return retVal;
case Constants.STATUS_HTTP_400_BAD_REQUEST:
throw new InvalidRequestException("Server responded with: " + IOUtils.toString(theResponseReader));
case Constants.STATUS_HTTP_404_NOT_FOUND:
throw new ResourceNotFoundException("Server responded with: " + IOUtils.toString(theResponseReader));
case Constants.STATUS_HTTP_405_METHOD_NOT_ALLOWED:
throw new MethodNotAllowedException("Server responded with: " + IOUtils.toString(theResponseReader));
case Constants.STATUS_HTTP_409_CONFLICT:
throw new ResourceVersionConflictException("Server responded with: " + IOUtils.toString(theResponseReader));
case Constants.STATUS_HTTP_412_PRECONDITION_FAILED:
throw new ResourceVersionNotSpecifiedException("Server responded with: " + IOUtils.toString(theResponseReader));
case Constants.STATUS_HTTP_422_UNPROCESSABLE_ENTITY:
IParser parser = createAppropriateParser(theResponseMimeType, theResponseReader, theResponseStatusCode);
OperationOutcome operationOutcome = parser.parseResource(OperationOutcome.class, theResponseReader);
throw new UnprocessableEntityException(operationOutcome);
default:
throw new UnclassifiedServerFailureException(theResponseStatusCode, IOUtils.toString(theResponseReader));
}
}
protected void parseContentLocation(MethodOutcome theOutcomeToPopulate, String theLocationHeader) {
String resourceNamePart = "/" + myResourceName + "/";
int resourceIndex = theLocationHeader.lastIndexOf(resourceNamePart);
if (resourceIndex > -1) {
int idIndexStart = resourceIndex + resourceNamePart.length();
int idIndexEnd = theLocationHeader.indexOf('/', idIndexStart);
if (idIndexEnd == -1) {
theOutcomeToPopulate.setId(new IdDt(theLocationHeader.substring(idIndexStart)));
} else {
theOutcomeToPopulate.setId(new IdDt(theLocationHeader.substring(idIndexStart, idIndexEnd)));
String versionIdPart = "/_history/";
int historyIdStart = theLocationHeader.indexOf(versionIdPart, idIndexEnd);
if (historyIdStart != -1) {
theOutcomeToPopulate.setVersionId(new IdDt(theLocationHeader.substring(historyIdStart + versionIdPart.length())));
}
}
}
}
public String getResourceName() {
return myResourceName;
}
@Override
public void invokeServer(RestfulServer theServer, Request theRequest, HttpServletResponse theResponse) throws BaseServerResponseException, IOException {
EncodingUtil encoding = determineResponseEncoding(theRequest.getServletRequest(), theRequest.getParameters());
IParser parser = encoding.newParser(getContext());
IResource resource = parser.parseResource(theRequest.getInputReader());
Object[] params = new Object[myParameters.size()];
for (int i = 0; i < myParameters.size(); i++) {
IParameter param = myParameters.get(i);
if (param == null) {
continue;
}
params[i] = param.translateQueryParametersIntoServerArgument(theRequest.getParameters(), resource);
}
addAdditionalParams(theRequest, params);
MethodOutcome response;
try {
response = (MethodOutcome) this.getMethod().invoke(theRequest.getResourceProvider(), params);
} catch (IllegalAccessException e) {
throw new InternalErrorException(e);
} catch (IllegalArgumentException e) {
throw new InternalErrorException(e);
} catch (InvocationTargetException e) {
throw new InternalErrorException(e);
}
if (response == null) {
throw new ConfigurationException("Method " + getMethod().getName() + " in type " + getMethod().getDeclaringClass().getCanonicalName() + " returned null");
}
if (response.isCreated()) {
theResponse.setStatus(Constants.STATUS_HTTP_201_CREATED);
StringBuilder b = new StringBuilder();
b.append(theRequest.getFhirServerBase());
b.append('/');
b.append(myResourceName);
b.append('/');
b.append(response.getId().getValue());
if (response.getVersionId() != null && response.getVersionId().isEmpty() == false) {
b.append("/_history/");
b.append(response.getVersionId().getValue());
}
theResponse.addHeader("Location", b.toString());
} else {
theResponse.setStatus(Constants.STATUS_HTTP_200_OK);
}
theServer.addHapiHeader(theResponse);
theResponse.setContentType(Constants.CT_TEXT);
Writer writer = theResponse.getWriter();
try {
writer.append("Resource has been created");
} finally {
writer.close();
}
// getMethod().in
}
/**
* For subclasses to override
*/
@SuppressWarnings("unused")
protected void addAdditionalParams(Request theRequest, Object[] theParams) {
// nothing
}
protected abstract Set<RequestType> provideAllowableRequestTypes();
@Override
public boolean matches(Request theRequest) {
Set<RequestType> allowableRequestTypes = provideAllowableRequestTypes();
RequestType requestType = theRequest.getRequestType();
if (!allowableRequestTypes.contains(requestType)) {
return false;
}
if (!myResourceName.equals(theRequest.getResourceName())) {
return false;
}
if (StringUtils.isNotBlank(theRequest.getOperation())) {
return false;
}
return true;
}
}

View File

@ -1,86 +1,24 @@
package ca.uhn.fhir.rest.method;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.dstu.resource.OperationOutcome;
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.parser.IParser;
import ca.uhn.fhir.rest.annotation.Create;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.client.BaseClientInvocation;
import ca.uhn.fhir.rest.client.PostClientInvocation;
import ca.uhn.fhir.rest.method.SearchMethodBinding.RequestType;
import ca.uhn.fhir.rest.param.IParameter;
import ca.uhn.fhir.rest.param.ResourceParameter;
import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.rest.server.EncodingUtil;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.UnclassifiedServerFailureException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
public class CreateMethodBinding extends BaseMethodBinding {
public class CreateMethodBinding extends BaseOutcomeReturningMethodBinding {
private static Set<String> ALLOWED_PARAMS;
static {
HashSet<String> set = new HashSet<String>();
set.add(Constants.PARAM_FORMAT);
ALLOWED_PARAMS = Collections.unmodifiableSet(set);
}
private int myResourceParameterIndex;
private List<IParameter> myParameters;
private String myResourceName;
public CreateMethodBinding(Method theMethod, FhirContext theContext) {
super(theMethod, theContext);
myParameters = Util.getResourceParameters(theMethod);
ResourceParameter resourceParameter = null;
int index = 0;
for (IParameter next : myParameters) {
if (next instanceof ResourceParameter) {
resourceParameter = (ResourceParameter) next;
myResourceName = theContext.getResourceDefinition(resourceParameter.getResourceType()).getName();
myResourceParameterIndex = index;
index++;
}
}
if (resourceParameter == null) {
throw new ConfigurationException("Method " + theMethod.getName() + " in type " + theMethod.getDeclaringClass().getCanonicalName() + " does not have a parameter annotated with @"
+ ResourceParam.class.getSimpleName());
}
if (!theMethod.getReturnType().equals(MethodOutcome.class)) {
throw new ConfigurationException("Method " + theMethod.getName() + " in type " + theMethod.getDeclaringClass().getCanonicalName() + " is a @" + Create.class.getSimpleName()
+ " method but it does not return " + MethodOutcome.class);
}
super(theMethod, theContext, Create.class);
}
@Override
@ -94,127 +32,16 @@ public class CreateMethodBinding extends BaseMethodBinding {
}
@Override
public BaseClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException {
IResource resource = (IResource) theArgs[myResourceParameterIndex];
if (resource == null) {
throw new NullPointerException("Resource can not be null");
}
RuntimeResourceDefinition def = getContext().getResourceDefinition(resource);
protected Set<RequestType> provideAllowableRequestTypes() {
return Collections.singleton(RequestType.POST);
}
@Override
protected BaseClientInvocation createClientInvocation(Object[] theArgs, IResource resource, String resourceName) {
StringBuilder urlExtension = new StringBuilder();
urlExtension.append(def.getName());
urlExtension.append(resourceName);
return new PostClientInvocation(getContext(), resource, urlExtension.toString());
}
@Override
public Object invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode,Map<String, List<String>> theHeaders) throws IOException, BaseServerResponseException {
switch (theResponseStatusCode) {
case 400:
throw new InvalidRequestException("Server responded with: " + IOUtils.toString(theResponseReader));
case 404:
throw new ResourceNotFoundException("Server responded with: " + IOUtils.toString(theResponseReader));
case 422:
IParser parser = createAppropriateParser(theResponseMimeType, theResponseReader, theResponseStatusCode);
OperationOutcome operationOutcome = parser.parseResource(OperationOutcome.class, theResponseReader);
throw new UnprocessableEntityException(operationOutcome);
case 200: // Support 200 just to be lenient
case 201: // but it should be 201
MethodOutcome retVal = new MethodOutcome();
List<String> locationHeaders = theHeaders.get("location");
if (locationHeaders != null && locationHeaders.size() > 0) {
String h = locationHeaders.get(0);
String resourceNamePart = "/" + myResourceName + "/";
int resourceIndex = h.lastIndexOf(resourceNamePart);
if (resourceIndex > -1) {
int idIndexStart = resourceIndex + resourceNamePart.length();
int idIndexEnd = h.indexOf('/', idIndexStart);
if (idIndexEnd == -1) {
retVal.setId(new IdDt(h.substring(idIndexStart)));
}else {
retVal.setId(new IdDt(h.substring(idIndexStart, idIndexEnd)));
String versionIdPart = "/_history/";
int historyIdStart = h.indexOf(versionIdPart, idIndexEnd);
if (historyIdStart != -1) {
retVal.setVersionId(new IdDt(h.substring(historyIdStart + versionIdPart.length())));
}
}
}
}
return retVal;
default:
throw new UnclassifiedServerFailureException(theResponseStatusCode, IOUtils.toString(theResponseReader));
}
}
@Override
public void invokeServer(RestfulServer theServer, Request theRequest, HttpServletResponse theResponse) throws BaseServerResponseException, IOException {
EncodingUtil encoding = determineResponseEncoding(theRequest.getServletRequest(), theRequest.getParameters());
IParser parser = encoding.newParser(getContext());
IResource resource = parser.parseResource(theRequest.getInputReader());
Object[] params = new Object[myParameters.size()];
for (int i = 0; i < myParameters.size(); i++) {
IParameter param = myParameters.get(i);
params[i] = param.translateQueryParametersIntoServerArgument(theRequest.getParameters(), resource);
}
MethodOutcome response;
try {
response = (MethodOutcome) this.getMethod().invoke(theRequest.getResourceProvider(), params);
} catch (IllegalAccessException e) {
throw new InternalErrorException(e);
} catch (IllegalArgumentException e) {
throw new InternalErrorException(e);
} catch (InvocationTargetException e) {
throw new InternalErrorException(e);
}
if (response == null) {
throw new ConfigurationException("Method " + getMethod().getName() + " in type " + getMethod().getDeclaringClass().getCanonicalName() + " returned null");
}
theResponse.setStatus(Constants.STATUS_HTTP_201_CREATED);
StringBuilder b = new StringBuilder();
b.append(theRequest.getFhirServerBase());
b.append('/');
b.append(myResourceName);
b.append('/');
b.append(response.getId().getValue());
if (response.getVersionId().isEmpty() == false) {
b.append("/_history/");
b.append(response.getVersionId().getValue());
}
theResponse.addHeader("Location", b.toString());
theServer.addHapiHeader(theResponse);
theResponse.setContentType(Constants.CT_TEXT);
Writer writer = theResponse.getWriter();
try {
writer.append("Resource has been created");
} finally {
writer.close();
}
// getMethod().in
}
@Override
public boolean matches(Request theRequest) {
if (theRequest.getRequestType() != RequestType.POST) {
return false;
}
if (!myResourceName.equals(theRequest.getResourceName())) {
return false;
}
if (StringUtils.isNotBlank(theRequest.getOperation())) {
return false;
}
return true;
}
}

View File

@ -31,8 +31,8 @@ public class ReadMethodBinding extends BaseResourceReturningMethodBinding {
Validate.notNull(theMethod, "Method must not be null");
Integer idIndex = Util.findReadIdParameterIndex(theMethod);
Integer versionIdIndex = Util.findReadVersionIdParameterIndex(theMethod);
Integer idIndex = Util.findIdParameterIndex(theMethod);
Integer versionIdIndex = Util.findVersionIdParameterIndex(theMethod);
myIdIndex = idIndex;
myVersionIdIndex = versionIdIndex;

View File

@ -0,0 +1,118 @@
package ca.uhn.fhir.rest.method;
import static org.apache.commons.lang3.StringUtils.*;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.dstu.valueset.RestfulOperationSystemEnum;
import ca.uhn.fhir.model.dstu.valueset.RestfulOperationTypeEnum;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Update;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.client.BaseClientInvocation;
import ca.uhn.fhir.rest.client.PutClientInvocation;
import ca.uhn.fhir.rest.method.SearchMethodBinding.RequestType;
import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
public class UpdateMethodBinding extends BaseOutcomeReturningMethodBinding {
private Integer myIdParameterIndex;
private Integer myVersionIdParameterIndex;
public UpdateMethodBinding(Method theMethod, FhirContext theContext) {
super(theMethod, theContext, Update.class);
myIdParameterIndex = Util.findIdParameterIndex(theMethod);
if (myIdParameterIndex == null) {
throw new ConfigurationException("Method '" + theMethod.getName() + "' on type '" + theMethod.getDeclaringClass().getCanonicalName() + "' has no parameter annotated with the @" + IdParam.class.getSimpleName() + " annotation");
}
myVersionIdParameterIndex = Util.findVersionIdParameterIndex(theMethod);
}
@Override
public RestfulOperationTypeEnum getResourceOperationType() {
return RestfulOperationTypeEnum.UPDATE;
}
@Override
public RestfulOperationSystemEnum getSystemOperationType() {
return null;
}
@Override
protected void addAdditionalParams(Request theRequest, Object[] theParams) {
/*
* We are being a bit lenient here, since technically the client is
* supposed to include the version in the Content-Location header, but
* we allow it in the PUT URL as well..
*/
String locationHeader = theRequest.getServletRequest().getHeader(Constants.HEADER_CONTENT_LOCATION);
if (isNotBlank(locationHeader)) {
MethodOutcome mo = new MethodOutcome();
parseContentLocation(mo, locationHeader);
if (mo.getId() == null || mo.getId().isEmpty()) {
throw new InvalidRequestException("Invalid Content-Location header for resource " + getResourceName()+ ": " + locationHeader);
}
if (mo.getVersionId() != null && mo.getVersionId().isEmpty() == false) {
theRequest.setVersion(mo.getVersionId());
}
}
// TODO: we should send an HTTP 412 automatically if the server
// only has a method which requires a version ID, and no
// Content-Location header is present
theParams[myIdParameterIndex] = theRequest.getId();
if (myVersionIdParameterIndex != null) {
theParams[myVersionIdParameterIndex] = theRequest.getVersion();
}
}
@Override
protected BaseClientInvocation createClientInvocation(Object[] theArgs, IResource resource, String resourceName) {
IdDt idDt = (IdDt) theArgs[myIdParameterIndex];
if (idDt == null) {
throw new NullPointerException("ID can not be null");
}
String id = idDt.getValue();
StringBuilder urlExtension = new StringBuilder();
urlExtension.append(resourceName);
urlExtension.append('/');
urlExtension.append(id);
PutClientInvocation retVal = new PutClientInvocation(getContext(), resource, urlExtension.toString());
if (myVersionIdParameterIndex != null) {
IdDt versionIdDt = (IdDt) theArgs[myIdParameterIndex];
if (versionIdDt != null) {
String versionId = versionIdDt.getValue();
if (StringUtils.isNotBlank(versionId)) {
StringBuilder b = new StringBuilder();
b.append('/');
b.append(urlExtension);
b.append('/');
b.append(versionId);
retVal.addHeader(Constants.HEADER_CONTENT_LOCATION, b.toString());
}
}
}
return retVal;
}
@Override
protected Set<RequestType> provideAllowableRequestTypes() {
return Collections.singleton(RequestType.PUT);
}
}

View File

@ -98,16 +98,19 @@ class Util {
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<? extends IResource>) parameterType);
} else if (nextAnnotation instanceof IdParam || nextAnnotation instanceof VersionIdParam) {
param = null;
} else {
continue;
}
haveHandledMethod= true;
parameters.add(param);
}
if (!haveHandledMethod) {
throw new ConfigurationException("Parameter # " + paramIndex + " of method '" + method.getName() + "' 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++;
@ -115,11 +118,11 @@ class Util {
return parameters;
}
public static Integer findReadIdParameterIndex(Method theMethod) {
public static Integer findIdParameterIndex(Method theMethod) {
return findParamIndex(theMethod, IdParam.class);
}
public static Integer findReadVersionIdParameterIndex(Method theMethod) {
public static Integer findVersionIdParameterIndex(Method theMethod) {
return findParamIndex(theMethod, VersionIdParam.class);
}

View File

@ -25,6 +25,14 @@ public class Constants {
public static final String PARAM_QUERY = "_query";
public static final int STATUS_HTTP_201_CREATED = 201;
public static final String CT_TEXT = "text/plain";
public static final int STATUS_HTTP_200_OK = 200;
public static final int STATUS_HTTP_422_UNPROCESSABLE_ENTITY = 422;
public static final int STATUS_HTTP_404_NOT_FOUND = 404;
public static final int STATUS_HTTP_400_BAD_REQUEST = 400;
public static final int STATUS_HTTP_405_METHOD_NOT_ALLOWED = 405;
public static final int STATUS_HTTP_409_CONFLICT = 409;
public static final int STATUS_HTTP_412_PRECONDITION_FAILED = 412;
public static final String HEADER_CONTENT_LOCATION = "Content-Location";
static {
Map<String, EncodingUtil> valToEncoding = new HashMap<String, EncodingUtil>();

View File

@ -95,10 +95,14 @@ public abstract class RestfulServer extends HttpServlet {
Collection<IResourceProvider> resourceProvider = getResourceProviders();
for (IResourceProvider nextProvider : resourceProvider) {
if (myTypeToProvider.containsKey(nextProvider.getResourceType())) {
throw new ServletException("Multiple providers for type: " + nextProvider.getResourceType().getCanonicalName());
Class<? extends IResource> resourceType = nextProvider.getResourceType();
if (resourceType==null) {
throw new NullPointerException("getResourceType() on class '" + nextProvider.getClass().getCanonicalName() + "' returned null");
}
myTypeToProvider.put(nextProvider.getResourceType(), nextProvider);
if (myTypeToProvider.containsKey(resourceType)) {
throw new ServletException("Multiple providers for type: " + resourceType.getCanonicalName());
}
myTypeToProvider.put(resourceType, nextProvider);
}
ourLog.info("Got {} resource providers", myTypeToProvider.size());
@ -266,7 +270,7 @@ public abstract class RestfulServer extends HttpServlet {
}
if (resourceBinding == null) {
throw new MethodNotFoundException("Unknown resource type: " + resourceName);
throw new MethodNotFoundException("Unknown resource type '" + resourceName+"' - Server knows how to handle: "+resources.keySet());
}
if (tok.hasMoreTokens()) {

View File

@ -0,0 +1,12 @@
package ca.uhn.fhir.rest.server.exceptions;
/**
* Created by dsotnikov on 2/27/2014.
*/
public class MethodNotAllowedException extends BaseServerResponseException {
private static final long serialVersionUID = 1L;
public MethodNotAllowedException(String error) {
super(405, error);
}
}

View File

@ -0,0 +1,14 @@
package ca.uhn.fhir.rest.server.exceptions;
import ca.uhn.fhir.rest.server.Constants;
/**
* Created by dsotnikov on 2/27/2014.
*/
public class ResourceVersionConflictException extends BaseServerResponseException {
private static final long serialVersionUID = 1L;
public ResourceVersionConflictException(String error) {
super(Constants.STATUS_HTTP_409_CONFLICT, error);
}
}

View File

@ -0,0 +1,15 @@
package ca.uhn.fhir.rest.server.exceptions;
import ca.uhn.fhir.rest.server.Constants;
/**
* Thrown for an Update operation if that operation requires a version to
* be specified in an HTTP header, and none was.
*/
public class ResourceVersionNotSpecifiedException extends BaseServerResponseException {
private static final long serialVersionUID = 1L;
public ResourceVersionNotSpecifiedException(String error) {
super(Constants.STATUS_HTTP_412_PRECONDITION_FAILED, error);
}
}

View File

@ -223,6 +223,7 @@ public MethodOutcome createPatient(@ResourceParam Patient thePatient) {
// This method returns a MethodOutcome object which contains
// the ID and Version ID for the newly saved resource
MethodOutcome retVal = new MethodOutcome();
retVal.setCreated(true);
retVal.setId(new IdDt("3746"));
retVal.setVersionId(new IdDt("1"));
return retVal;

View File

@ -16,6 +16,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.Update;
import ca.uhn.fhir.rest.annotation.VersionIdParam;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.client.api.IBasicClient;
@ -58,4 +59,7 @@ public interface ITestClient extends IBasicClient {
@Create
public MethodOutcome createPatient(@ResourceParam Patient thePatient);
@Update
public MethodOutcome updatePatient(@IdParam IdDt theId, @ResourceParam Patient thePatient);
}

View File

@ -1,8 +1,6 @@
package ca.uhn.fhir.rest.server;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.*;
import java.util.ArrayList;
import java.util.Arrays;
@ -15,8 +13,10 @@ import java.util.concurrent.TimeUnit;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
@ -34,6 +34,7 @@ import org.junit.Test;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.model.api.BundleEntry;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.PathSpecification;
import ca.uhn.fhir.model.dstu.composite.CodingDt;
import ca.uhn.fhir.model.dstu.composite.HumanNameDt;
@ -54,6 +55,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.Update;
import ca.uhn.fhir.rest.annotation.VersionIdParam;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.param.CodingListParam;
@ -82,9 +84,10 @@ public class ResfulServerMethodTest {
DummyPatientResourceProvider patientProvider = new DummyPatientResourceProvider();
ServerProfileProvider profProvider = new ServerProfileProvider(ourCtx);
DummyDiagnosticReportResourceProvider reportProvider = new DummyDiagnosticReportResourceProvider();
ServletHandler proxyHandler = new ServletHandler();
ServletHolder servletHolder = new ServletHolder(new DummyRestfulServer(patientProvider, profProvider));
ServletHolder servletHolder = new ServletHolder(new DummyRestfulServer(patientProvider, profProvider,reportProvider));
proxyHandler.addServletWithMapping(servletHolder, "/*");
ourServer.setHandler(proxyHandler);
ourServer.start();
@ -419,6 +422,66 @@ public class ResfulServerMethodTest {
}
@Test
public void testUpdate() throws Exception {
Patient patient = new Patient();
patient.addIdentifier().setValue("002");
HttpPut httpPost = new HttpPut("http://localhost:" + ourPort + "/Patient/001");
httpPost.setEntity(new StringEntity(new FhirContext().newXmlParser().encodeResourceToString(patient), ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
HttpResponse status = ourClient.execute(httpPost);
String responseContent = IOUtils.toString(status.getEntity().getContent());
ourLog.info("Response was:\n{}", responseContent);
assertEquals(201, status.getStatusLine().getStatusCode());
assertEquals("http://localhost:" + ourPort + "/Patient/001/_history/002", status.getFirstHeader("Location").getValue());
}
@Test
public void testUpdateWithVersion() throws Exception {
DiagnosticReport patient = new DiagnosticReport();
patient.getIdentifier().setValue("001");
HttpPut httpPut = new HttpPut("http://localhost:" + ourPort + "/DiagnosticReport/001");
httpPut.addHeader("Content-Location","/DiagnosticReport/001/_history/002");
httpPut.setEntity(new StringEntity(new FhirContext().newXmlParser().encodeResourceToString(patient), ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
HttpResponse status = ourClient.execute(httpPut);
String responseContent = IOUtils.toString(status.getEntity().getContent());
ourLog.info("Response was:\n{}", responseContent);
assertEquals(201, status.getStatusLine().getStatusCode());
assertEquals("http://localhost:" + ourPort + "/DiagnosticReport/001/_history/002", status.getFirstHeader("Location").getValue());
}
@Test()
public void testUpdateWithVersionBadContentLocationHeader() throws Exception {
DiagnosticReport patient = new DiagnosticReport();
patient.getIdentifier().setValue("001");
HttpPut httpPut = new HttpPut("http://localhost:" + ourPort + "/DiagnosticReport/001");
httpPut.addHeader("Content-Location","/Patient/001/_history/002");
httpPut.setEntity(new StringEntity(new FhirContext().newXmlParser().encodeResourceToString(patient), ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
CloseableHttpResponse results = ourClient.execute(httpPut);
String responseContent = IOUtils.toString(results.getEntity().getContent());
ourLog.info("Response was:\n{}", responseContent);
assertEquals(400, results.getStatusLine().getStatusCode());
}
@Test
public void testFormatParamXml() throws Exception {
@ -639,6 +702,24 @@ public class ResfulServerMethodTest {
}
public static class DummyDiagnosticReportResourceProvider implements IResourceProvider{
@Override
public Class<? extends IResource> getResourceType() {
return DiagnosticReport.class;
}
@SuppressWarnings("unused")
@Update()
public MethodOutcome updateDiagnosticReportWithVersion(@IdParam IdDt theId, @VersionIdParam IdDt theVersionId, @ResourceParam DiagnosticReport thePatient) {
IdDt id = theId;
IdDt version = theVersionId;
return new MethodOutcome(true, id, version);
}
}
/**
* Created by dsotnikov on 2/25/2014.
*/
@ -675,6 +756,18 @@ public class ResfulServerMethodTest {
return idToPatient;
}
@SuppressWarnings("unused")
@Update()
public MethodOutcome updateDiagnosticReportWithVersion(@IdParam IdDt theId, @VersionIdParam IdDt theVersionId, @ResourceParam DiagnosticReport thePatient) {
/* TODO: THIS METHOD IS NOT USED. It's the wrong type (DiagnosticReport), so
* it should cause an exception on startup. Also we should detect if there
* are multiple resource params on an update/create/etc method
*/
IdDt id = theId;
IdDt version = theVersionId;
return new MethodOutcome(true, id, version);
}
@Search(queryName="someQueryNoParams")
public Patient getPatientNoParams() {
Patient next = getIdToPatient().get("1");
@ -707,10 +800,17 @@ public class ResfulServerMethodTest {
public MethodOutcome createPatient(@ResourceParam Patient thePatient) {
IdDt id = new IdDt(thePatient.getIdentifier().get(0).getValue().getValue());
IdDt version = new IdDt(thePatient.getIdentifier().get(1).getValue().getValue());
return new MethodOutcome(id, version);
return new MethodOutcome(true, id, version);
}
@Update()
public MethodOutcome updatePatient(@IdParam IdDt theId, @ResourceParam Patient thePatient) {
IdDt id = theId;
IdDt version = new IdDt(thePatient.getIdentifierFirstRep().getValue().getValue());
return new MethodOutcome(true, id, version);
}
@Search()
public Patient getPatient(@RequiredParam(name = Patient.SP_IDENTIFIER) IdentifierDt theIdentifier) {
for (Patient next : getIdToPatient().values()) {