Merge branch 'master' of github.com:jamesagnew/hapi-fhir

This commit is contained in:
jamesagnew 2014-08-09 12:18:54 -04:00
commit 04bc96ff8e
37 changed files with 687 additions and 105 deletions

View File

@ -59,6 +59,8 @@
<version>2.0.2.RELEASE</version>
<optional>true</optional>
</dependency>
<!--
-->
<!-- Only required for narrative generator support -->
<dependency>
@ -253,6 +255,7 @@
<version>${hamcrest_version}</version>
<scope>test</scope>
</dependency>
<!--
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
@ -269,8 +272,9 @@
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.2.RELEASE</version>
<scope>test</scope>
</dependency>
-->
</dependencies>

View File

@ -36,12 +36,34 @@
Add documentation on how to use eBay CORS Filter to support Cross Origin Resource
Sharing (CORS) to server. CORS support that was built in to the server itself has
been removed, as it did not work correctly (and was reinventing a wheel that others
have done a great job inventing).
have done a great job inventing). Thanks to Peter Bernhardt of Relay Health for all the assistance
in testing this!
</action>
<action type="fix">
IResource interface did not expose the getLanguage/setLanguage methods from BaseResource,
so the resource language was difficult to access.
</action>
<action type="fix">
JSON Parser now gives a more friendly error message if it tries to parse JSON with invalid use
of single quotes
</action>
<action type="add">
Transaction server method is now allowed to return an OperationOutcome in addition to the
incoming resources. The public test server now does this in ordeer to return status information
about the transaction processing.
</action>
<action type="add">
Update method in the server can now flag (via a field on the MethodOutcome object being returned)
that the result was actually a creation, and Create method can indicate that it was actually an
update. This has no effect other than to switch between the HTTP 200 and HTTP 201 status codes on the
response, but this may be useful in some circumstances.
</action>
<action type="fix">
Annotation client search methods with a specific resource type (e.g. List&lt;Patient&gt; search())
won't return any resources that aren't of the correct type that are received in a response
bundle (generally these are referenced resources, so they are populated in the reference fields instead).
Thanks to Tahura Chaudhry of University Health Network for the unit test!
</action>
</release>
<release version="0.5" date="2014-Jul-30">
<action type="add">

View File

@ -37,6 +37,8 @@ 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.client.IGenericClient;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.server.Constants;
public class Bundle extends BaseBundle /* implements IElement */{
@ -103,6 +105,31 @@ public class Bundle extends BaseBundle /* implements IElement */{
return map.get(theId.toUnqualified());
}
// public static void main(String[] args) {
//
// FhirContext ctx = new FhirContext();
// String txt = "<Organization xmlns=\"http://hl7.org/fhir\">\n" +
// " <extension url=\"http://fhir.connectinggta.ca/Profile/organization#providerIdPool\">\n" +
// " <valueUri value=\"urn:oid:2.16.840.1.113883.3.239.23.21.1\"/>\n" +
// " </extension>\n" +
// " <text>\n" +
// " <status value=\"generated\"/>\n" +
// " <div xmlns=\"http://www.w3.org/1999/xhtml\"/>\n" +
// " </text>\n" +
// " <identifier>\n" +
// " <use value=\"official\"/>\n" +
// " <label value=\"HSP 2.16.840.1.113883.3.239.23.21\"/>\n" +
// " <system value=\"urn:cgta:hsp_ids\"/>\n" +
// " <value value=\"urn:oid:2.16.840.1.113883.3.239.23.21\"/>\n" +
// " </identifier>\n" +
// " <name value=\"火星第五人民医院\"/>\n" +
// "</Organization>";
//
// IGenericClient c = ctx.newRestfulGenericClient("http://fhirtest.uhn.ca/base");
// c.registerInterceptor(new LoggingInterceptor(true));
// c.update().resource(txt).withId("1665").execute();
// }
//
public List<BundleEntry> getEntries() {
if (myEntries == null) {
myEntries = new ArrayList<BundleEntry>();
@ -248,14 +275,15 @@ public class Bundle extends BaseBundle /* implements IElement */{
RuntimeResourceDefinition def = theContext.getResourceDefinition(theResource);
if (theResource.getId() != null && StringUtils.isNotBlank(theResource.getId().getValue())) {
String title = ResourceMetadataKeyEnum.TITLE.get(theResource);
if (title != null) {
entry.getTitle().setValue(title);
} else {
entry.getTitle().setValue(def.getName() + " " + theResource.getId().getValue());
entry.getTitle().setValue(def.getName() + " " + StringUtils.defaultString(theResource.getId().getValue(), "(no ID)"));
}
if (theResource.getId() != null && StringUtils.isNotBlank(theResource.getId().getValue())) {
StringBuilder b = new StringBuilder();
b.append(theServerBase);
if (b.length() > 0 && b.charAt(b.length() - 1) != '/') {

View File

@ -162,15 +162,6 @@ public class IdDt extends BasePrimitive<String> {
return ObjectUtils.equals(getResourceType(), theId.getResourceType()) && ObjectUtils.equals(getIdPart(), theId.getIdPart()) && ObjectUtils.equals(getVersionIdPart(), theId.getVersionIdPart());
}
/**
* Returns a reference to <code>this</code> IdDt. It is generally not neccesary to use this method but it is
* provided for consistency with the rest of the API.
*/
@Override
public IdDt getId() {
return this;
}
public String getIdPart() {
return myUnqualifiedId;
}

View File

@ -47,6 +47,7 @@ import javax.json.JsonValue;
import javax.json.JsonValue.ValueType;
import javax.json.stream.JsonGenerator;
import javax.json.stream.JsonGeneratorFactory;
import javax.json.stream.JsonParsingException;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
@ -135,6 +136,10 @@ public class JsonParser extends BaseParser implements IParser {
}
private void assertObjectOfType(JsonValue theResourceTypeObj, ValueType theValueType, String thePosition) {
if (theResourceTypeObj == null) {
throw new DataFormatException("Invalid JSON content detected, missing required element: '" + thePosition + "'");
}
if (theResourceTypeObj.getValueType() != theValueType) {
throw new DataFormatException("Invalid content of element " + thePosition + ", expected " + theValueType);
}
@ -620,9 +625,18 @@ public class JsonParser extends BaseParser implements IParser {
@Override
public <T extends IResource> Bundle parseBundle(Class<T> theResourceType, Reader theReader) {
JsonReader reader = Json.createReader(theReader);
JsonObject object = reader.readObject();
JsonReader reader;
JsonObject object;
try {
reader = Json.createReader(theReader);
object = reader.readObject();
} catch (JsonParsingException e) {
if (e.getMessage().startsWith("Unexpected char 39")) {
throw new DataFormatException("Failed to parse JSON encoded FHIR content: " + e.getMessage() + " - This may indicate that single quotes are being used as JSON escapes where double quotes are required", e);
}
throw new DataFormatException("Failed to parse JSON encoded FHIR content: " + e.getMessage(), e);
}
JsonValue resourceTypeObj = object.get("resourceType");
assertObjectOfType(resourceTypeObj, JsonValue.ValueType.STRING, "resourceType");
String resourceType = ((JsonString) resourceTypeObj).getString();

View File

@ -28,19 +28,72 @@ public class MethodOutcome {
private IdDt myId;
private OperationOutcome myOperationOutcome;
private IdDt myVersionId;
private Boolean myCreated;
/**
* Constructor
*/
public MethodOutcome() {
}
/**
* Constructor
*
* @param theId
* The ID of the created/updated resource
*/
public MethodOutcome(IdDt theId) {
myId = theId;
}
/**
* Constructor
*
* @param theId
* The ID of the created/updated resource
*
* @param theCreated
* If not null, indicates whether the resource was created (as opposed to being updated). This is generally not needed, since the server can assume based on the method being called
* whether the result was a creation or an update. However, it can be useful if you are implementing an update method that does a create if the ID doesn't already exist.
*/
public MethodOutcome(IdDt theId, Boolean theCreated) {
myId = theId;
myCreated = theCreated;
}
/**
* Constructor
*
* @param theId
* The ID of the created/updated resource
*
* @param theOperationOutcome
* The operation outcome to return with the response (or null for none)
*/
public MethodOutcome(IdDt theId, OperationOutcome theOperationOutcome) {
myId = theId;
myOperationOutcome = theOperationOutcome;
}
/**
* Constructor
*
* @param theId
* The ID of the created/updated resource
*
* @param theOperationOutcome
* The operation outcome to return with the response (or null for none)
*
* @param theCreated
* If not null, indicates whether the resource was created (as opposed to being updated). This is generally not needed, since the server can assume based on the method being called
* whether the result was a creation or an update. However, it can be useful if you are implementing an update method that does a create if the ID doesn't already exist.
*/
public MethodOutcome(IdDt theId, OperationOutcome theOperationOutcome, Boolean theCreated) {
myId = theId;
myOperationOutcome = theOperationOutcome;
myCreated = theCreated;
}
/**
* @deprecated Use the constructor which accepts a single IdDt parameter, and include the logical ID and version ID in that IdDt instance
*/
@ -63,11 +116,9 @@ public class MethodOutcome {
}
/**
* Returns the {@link OperationOutcome} resource to return to the client or
* <code>null</code> if none.
* Returns the {@link OperationOutcome} resource to return to the client or <code>null</code> if none.
*
* @return This method <b>will return null</b>, unlike many methods in the
* API.
* @return This method <b>will return null</b>, unlike many methods in the API.
*/
public OperationOutcome getOperationOutcome() {
return myOperationOutcome;
@ -80,13 +131,32 @@ public class MethodOutcome {
return myVersionId;
}
public Boolean getCreated() {
return myCreated;
}
/**
* If not null, indicates whether the resource was created (as opposed to being updated). This is generally not needed, since the server can assume based on the method being called whether the
* result was a creation or an update. However, it can be useful if you are implementing an update method that does a create if the ID doesn't already exist.
*
* @param theCreated
* If not null, indicates whether the resource was created (as opposed to being updated). This is generally not needed, since the server can assume based on the method being called
* whether the result was a creation or an update. However, it can be useful if you are implementing an update method that does a create if the ID doesn't already exist.
*/
public void setCreated(Boolean theCreated) {
myCreated = theCreated;
}
/**
* @param theId
* The ID of the created/updated resource
*/
public void setId(IdDt theId) {
myId = theId;
}
/**
* Sets the {@link OperationOutcome} resource to return to the client. Set
* to <code>null</code> (which is the default) if none.
* Sets the {@link OperationOutcome} resource to return to the client. Set to <code>null</code> (which is the default) if none.
*/
public void setOperationOutcome(OperationOutcome theOperationOutcome) {
myOperationOutcome = theOperationOutcome;

View File

@ -745,6 +745,9 @@ public class GenericClient extends BaseClient implements IGenericClient {
public MethodOutcome invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws IOException,
BaseServerResponseException {
MethodOutcome response = MethodUtil.process2xxResponse(myContext, myResourceName, theResponseStatusCode, theResponseMimeType, theResponseReader, theHeaders);
if (theResponseStatusCode == Constants.STATUS_HTTP_201_CREATED) {
response.setCreated(true);
}
return response;
}
}

View File

@ -62,6 +62,8 @@ public interface IGenericClient {
* @param theResource
* The resource to create
* @return An outcome
* @deprecated Use {@link #create() fluent method instead}. This method will be removed.
*
*/
MethodOutcome create(IResource theResource);

View File

@ -157,12 +157,20 @@ abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding<Metho
throw new InternalErrorException("Method " + getMethod().getName() + " in type " + getMethod().getDeclaringClass().getCanonicalName()
+ " returned null, which is not allowed for create operation");
}
if (response.getCreated() == null || response.getCreated() == Boolean.TRUE) {
theResponse.setStatus(Constants.STATUS_HTTP_201_CREATED);
} else {
theResponse.setStatus(Constants.STATUS_HTTP_200_OK);
}
addLocationHeader(theRequest, theResponse, response);
break;
case UPDATE:
if (response.getCreated() == null || response.getCreated() == Boolean.FALSE) {
theResponse.setStatus(Constants.STATUS_HTTP_200_OK);
} else {
theResponse.setStatus(Constants.STATUS_HTTP_201_CREATED);
}
addLocationHeader(theRequest, theResponse, response);
break;

View File

@ -23,6 +23,7 @@ package ca.uhn.fhir.rest.method;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
@ -48,9 +49,12 @@ 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.util.ReflectionUtil;
abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding<Object> {
protected static final Set<String> ALLOWED_PARAMS;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseResourceReturningMethodBinding.class);
static {
HashSet<String> set = new HashSet<String>();
set.add(Constants.PARAM_FORMAT);
@ -58,9 +62,10 @@ abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding<Obje
set.add(Constants.PARAM_PRETTY);
ALLOWED_PARAMS = Collections.unmodifiableSet(set);
}
private MethodReturnTypeEnum myMethodReturnType;
private Class<?> myResourceListCollectionType;
private String myResourceName;
private Class<? extends IResource> myResourceType;
public BaseResourceReturningMethodBinding(Class<? extends IResource> theReturnResourceType, Method theMethod, FhirContext theConetxt, Object theProvider) {
@ -68,7 +73,17 @@ abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding<Obje
Class<?> methodReturnType = theMethod.getReturnType();
if (Collection.class.isAssignableFrom(methodReturnType)) {
myMethodReturnType = MethodReturnTypeEnum.LIST_OF_RESOURCES;
Class<?> collectionType = ReflectionUtil.getGenericCollectionTypeOfMethodReturnType(theMethod);
if (collectionType != null) {
if (!Object.class.equals(collectionType) && !IResource.class.isAssignableFrom(collectionType)) {
throw new ConfigurationException("Method " + theMethod.getDeclaringClass().getSimpleName() + "#" + theMethod.getName() + " returns an invalid collection generic type: "
+ collectionType);
}
}
myResourceListCollectionType = collectionType;
} else if (IResource.class.isAssignableFrom(methodReturnType)) {
myMethodReturnType = MethodReturnTypeEnum.RESOURCE;
} else if (Bundle.class.isAssignableFrom(methodReturnType)) {
@ -76,7 +91,8 @@ abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding<Obje
} else if (IBundleProvider.class.isAssignableFrom(methodReturnType)) {
myMethodReturnType = MethodReturnTypeEnum.BUNDLE_PROVIDER;
} else {
throw new ConfigurationException("Invalid return type '" + methodReturnType.getCanonicalName() + "' on method '" + theMethod.getName() + "' on type: " + theMethod.getDeclaringClass().getCanonicalName());
throw new ConfigurationException("Invalid return type '" + methodReturnType.getCanonicalName() + "' on method '" + theMethod.getName() + "' on type: "
+ theMethod.getDeclaringClass().getCanonicalName());
}
if (theReturnResourceType != null) {
@ -118,7 +134,20 @@ abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding<Obje
case BUNDLE:
return bundle;
case LIST_OF_RESOURCES:
return bundle.toListOfResources();
List<IResource> listOfResources;
if (myResourceListCollectionType != null) {
listOfResources = new ArrayList<IResource>();
for (IResource next : bundle.toListOfResources()) {
if (!myResourceListCollectionType.isAssignableFrom(next.getClass())) {
ourLog.debug("Not returning resource of type {} because it is not a subclass or instance of {}", next.getClass(), myResourceListCollectionType);
continue;
}
listOfResources.add(next);
}
} else {
listOfResources = bundle.toListOfResources();
}
return listOfResources;
case RESOURCE:
List<IResource> list = bundle.toListOfResources();
if (list.size() == 0) {
@ -199,7 +228,8 @@ abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding<Obje
IBundleProvider result = invokeServer(theRequest, params);
switch (getReturnType()) {
case BUNDLE:
RestfulServer.streamResponseAsBundle(theServer, theResponse, result, responseEncoding, theRequest.getFhirServerBase(), theRequest.getCompleteUrl(), prettyPrint, requestIsBrowser, narrativeMode, 0, count, null, respondGzip);
RestfulServer.streamResponseAsBundle(theServer, theResponse, result, responseEncoding, theRequest.getFhirServerBase(), theRequest.getCompleteUrl(), prettyPrint, requestIsBrowser,
narrativeMode, 0, count, null, respondGzip);
break;
case RESOURCE:
if (result.size() == 0) {
@ -207,7 +237,8 @@ abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding<Obje
} else if (result.size() > 1) {
throw new InternalErrorException("Method returned multiple resources");
}
RestfulServer.streamResponseAsResource(theServer, theResponse, result.getResources(0, 1).get(0), responseEncoding, prettyPrint, requestIsBrowser, narrativeMode, respondGzip, theRequest.getFhirServerBase());
RestfulServer.streamResponseAsResource(theServer, theResponse, result.getResources(0, 1).get(0), responseEncoding, prettyPrint, requestIsBrowser, narrativeMode, respondGzip,
theRequest.getFhirServerBase());
break;
}
}
@ -223,7 +254,7 @@ abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding<Obje
}
public enum MethodReturnTypeEnum {
BUNDLE, LIST_OF_RESOURCES, RESOURCE, BUNDLE_PROVIDER
BUNDLE, BUNDLE_PROVIDER, LIST_OF_RESOURCES, RESOURCE
}
public enum ReturnTypeEnum {

View File

@ -67,8 +67,11 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
} else {
myDescription = StringUtils.defaultIfBlank(desc.shortDefinition(), null);
}
}
}
public String getDescription() {

View File

@ -32,6 +32,7 @@ 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.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;
@ -104,11 +105,16 @@ public class TransactionMethodBinding extends BaseResourceReturningMethodBinding
Object response= invokeServerMethod(theMethodParams);
IBundleProvider retVal = toResourceList(response);
int offset = 0;
if (retVal.size() != resources.size()) {
if (retVal.size() > 0 && retVal.getResources(0, 1).get(0) instanceof OperationOutcome) {
offset = 1;
} else {
throw new InternalErrorException("Transaction bundle contained " + resources.size() + " entries, but server method response contained " + retVal.size() + " entries (must be the same)");
}
}
List<IResource> retResources = retVal.getResources(0, retVal.size());
List<IResource> retResources = retVal.getResources(offset, retVal.size());
for (int i =0; i < resources.size(); i++) {
IdDt oldId = oldIds.get(i);
IResource newRes = retResources.get(i);
@ -117,8 +123,8 @@ public class TransactionMethodBinding extends BaseResourceReturningMethodBinding
}
if (oldId != null && !oldId.isEmpty()) {
if (!oldId.getId().equals(newRes.getId())) {
newRes.getResourceMetadata().put(ResourceMetadataKeyEnum.PREVIOUS_ID, oldId.getId());
if (!oldId.equals(newRes.getId())) {
newRes.getResourceMetadata().put(ResourceMetadataKeyEnum.PREVIOUS_ID, oldId);
}
}
}

View File

@ -1054,9 +1054,11 @@ public class RestfulServer extends HttpServlet {
for (IResource next : resourceList) {
if (next.getId() == null || next.getId().isEmpty()) {
if (!(next instanceof OperationOutcome)) {
throw new InternalErrorException("Server method returned resource of type[" + next.getClass().getSimpleName() + "] with no ID specified (IResource#setId(IdDt) must be called)");
}
}
}
Bundle bundle = createBundleFromResourceList(theServer.getFhirContext(), theServer.getServerName(), resourceList, theServerBase, theCompleteUrl, theResult.size());

View File

@ -606,6 +606,11 @@ public MethodOutcome updatePatient(@IdParam IdDt theId, @ResourceParam Patient t
outcome.addIssue().setDetails("One minor issue detected");
retVal.setOperationOutcome(outcome);
// If your server supports creating resources during an update if they don't already exist
// (this is not mandatory and may not be desirable anyhow) you can flag in the response
// that this was a creation as follows:
// retVal.setCreated(true);
return retVal;
}
//END SNIPPET: update
@ -863,15 +868,13 @@ public List<IResource> transaction(@TransactionParam List<IResource> theResource
}
}
/*
* According to the specification, a bundle must be returned. This bundle will contain
* all of the created/updated/deleted resources, including their new/updated identities.
*
* The returned list must be the exact same size as the list of resources
* passed in, and it is acceptable to return the same list instance that was
* passed in.
*/
List<IResource> retVal = theResources;
// According to the specification, a bundle must be returned. This bundle will contain
// all of the created/updated/deleted resources, including their new/updated identities.
//
// The returned list must be the exact same size as the list of resources
// passed in, and it is acceptable to return the same list instance that was
// passed in.
List<IResource> retVal = new ArrayList<IResource>(theResources);
for (IResource next : theResources) {
/*
* Populate each returned resource with the new ID for that resource,
@ -881,6 +884,12 @@ public List<IResource> transaction(@TransactionParam List<IResource> theResource
next.setId(newId);
}
// If wanted, you may optionally also return an OperationOutcome resource
// If present, the OperationOutcome must come first in the returned list.
OperationOutcome oo = new OperationOutcome();
oo.addIssue().setSeverity(IssueSeverityEnum.INFORMATION).setDetails("Completed successfully");
retVal.add(0, oo);
return retVal;
}
//END SNIPPET: transaction

View File

@ -76,7 +76,7 @@
<init-param>
<description>A comma separated list non-standard response headers that will be exposed to XHR2 object.</description>
<param-name>cors.exposed.headers</param-name>
<param-value></param-value>
<param-value>Location,Content-Location</param-value>
</init-param>
<init-param>
<description>A flag that suggests if CORS is supported with cookies</description>

View File

@ -83,6 +83,17 @@ public class JsonParserTest {
}
@Test
public void testParseSingleQuotes() {
try {
ourCtx.newJsonParser().parseBundle("{ 'resourceType': 'Bundle' }");
fail();
} catch (DataFormatException e) {
// Should be an error message about how single quotes aren't valid JSON
assertThat(e.getMessage(), containsString("double quote"));
}
}
@Test
public void testEncodeExtensionInCompositeElement() {

View File

@ -692,6 +692,8 @@ public class ClientTest {
}
@Test
public void testSearchByDateRange() throws Exception {

File diff suppressed because one or more lines are too long

View File

@ -12,6 +12,8 @@ import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpOptions;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
@ -26,6 +28,7 @@ import org.junit.Test;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.model.dstu.resource.Patient;
import ca.uhn.fhir.rest.server.ResfulServerSelfReferenceTest.DummyPatientResourceProvider;
import ca.uhn.fhir.testutil.RandomServerPortProvider;
@ -54,6 +57,7 @@ public class CorsTest {
fh.setInitParameter("cors.logging.enabled", "true");
fh.setInitParameter("cors.allowed.origins", "*");
fh.setInitParameter("cors.allowed.headers", "x-fhir-starter,Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers");
fh.setInitParameter("cors.exposed.headers", "Location,Content-Location");
fh.setInitParameter("cors.allowed.methods", "GET,POST,PUT,DELETE,OPTIONS");
ServletContextHandler ch = new ServletContextHandler();
@ -100,6 +104,20 @@ public class CorsTest {
assertEquals(1, bundle.getEntries().size());
}
{
HttpPost httpOpt = new HttpPost(baseUri + "/Patient");
httpOpt.addHeader("Access-Control-Request-Method", "POST");
httpOpt.addHeader("Origin", "http://www.fhir-starter.com");
httpOpt.addHeader("Access-Control-Request-Headers", "accept, x-fhir-starter, content-type");
httpOpt.setEntity(new StringEntity(ourCtx.newXmlParser().encodeResourceToString(new Patient())));
HttpResponse status = ourClient.execute(httpOpt);
String responseContent = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
ourLog.info("Response: {}", status);
ourLog.info("Response was:\n{}", responseContent);
assertEquals("POST", status.getFirstHeader(Constants.HEADER_CORS_ALLOW_METHODS).getValue());
assertEquals("http://www.fhir-starter.com", status.getFirstHeader(Constants.HEADER_CORS_ALLOW_ORIGIN).getValue());
}
} finally {
server.stop();
}

View File

@ -28,10 +28,13 @@ 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.UriDt;
import ca.uhn.fhir.rest.annotation.Create;
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.ResourceParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.server.provider.ServerProfileProvider;
import ca.uhn.fhir.testutil.RandomServerPortProvider;
@ -174,6 +177,11 @@ public class ResfulServerSelfReferenceTest {
return idToPatient;
}
@Create
public MethodOutcome create(@ResourceParam Patient thePatient) {
return new MethodOutcome(thePatient.getId());
}
@Search()
public Patient getPatient(@RequiredParam(name = Patient.SP_IDENTIFIER) IdentifierDt theIdentifier) {
for (Patient next : getIdToPatient().values()) {

View File

@ -2,6 +2,7 @@ package ca.uhn.fhir.rest.server;
import static org.junit.Assert.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@ -16,6 +17,7 @@ import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHolder;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
@ -24,6 +26,7 @@ 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.ResourceMetadataKeyEnum;
import ca.uhn.fhir.model.dstu.resource.OperationOutcome;
import ca.uhn.fhir.model.dstu.resource.Patient;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.InstantDt;
@ -37,10 +40,19 @@ import ca.uhn.fhir.testutil.RandomServerPortProvider;
public class TransactionTest {
private static CloseableHttpClient ourClient;
private static FhirContext ourCtx = new FhirContext();
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TransactionTest.class);
private static int ourPort;
private static boolean ourReturnOperationOutcome;
private static Server ourServer;
private static FhirContext ourCtx = new FhirContext();
@Before
public void before() {
ourReturnOperationOutcome = false;
}
@Test
public void testTransaction() throws Exception {
@ -95,6 +107,65 @@ public class TransactionTest {
assertEquals(nowInstant.getValueAsString(), entry2.getDeletedAt().getValueAsString());
}
@Test
public void testTransactionWithOperationOutcome() throws Exception {
ourReturnOperationOutcome = true;
Bundle b = new Bundle();
InstantDt nowInstant = InstantDt.withCurrentTime();
Patient p1 = new Patient();
p1.addName().addFamily("Family1");
BundleEntry entry = b.addEntry();
entry.getId().setValue("1");
entry.setResource(p1);
Patient p2 = new Patient();
p2.addName().addFamily("Family2");
entry = b.addEntry();
entry.getId().setValue("2");
entry.setResource(p2);
BundleEntry deletedEntry = b.addEntry();
deletedEntry.setId(new IdDt("Patient/3"));
deletedEntry.setDeleted(nowInstant);
String bundleString = ourCtx.newXmlParser().setPrettyPrint(true).encodeBundleToString(b);
ourLog.info(bundleString);
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/");
httpPost.addHeader("Accept", Constants.CT_ATOM_XML + "; pretty=true");
httpPost.setEntity(new StringEntity(bundleString, ContentType.create(Constants.CT_ATOM_XML, "UTF-8")));
HttpResponse status = ourClient.execute(httpPost);
String responseContent = IOUtils.toString(status.getEntity().getContent()); IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
ourLog.info(responseContent);
Bundle bundle = new FhirContext().newXmlParser().parseBundle(responseContent);
assertEquals(4, bundle.size());
assertEquals(OperationOutcome.class, bundle.getEntries().get(0).getResource().getClass());
assertEquals("OperationOutcome (no ID)", bundle.getEntries().get(0).getTitle().getValue());
BundleEntry entry0 = bundle.getEntries().get(1);
assertEquals("http://localhost:" + ourPort + "/Patient/81", entry0.getId().getValue());
assertEquals("http://localhost:" + ourPort + "/Patient/81/_history/91", entry0.getLinkSelf().getValue());
assertEquals("http://localhost:" + ourPort + "/Patient/1", entry0.getLinkAlternate().getValue());
BundleEntry entry1 = bundle.getEntries().get(2);
assertEquals("http://localhost:" + ourPort + "/Patient/82", entry1.getId().getValue());
assertEquals("http://localhost:" + ourPort + "/Patient/82/_history/92", entry1.getLinkSelf().getValue());
assertEquals("http://localhost:" + ourPort + "/Patient/2", entry1.getLinkAlternate().getValue());
BundleEntry entry2 = bundle.getEntries().get(3);
assertEquals("http://localhost:" + ourPort + "/Patient/3", entry2.getId().getValue());
assertEquals("http://localhost:" + ourPort + "/Patient/3/_history/93", entry2.getLinkSelf().getValue());
assertEquals(nowInstant.getValueAsString(), entry2.getDeletedAt().getValueAsString());
}
@AfterClass
public static void afterClass() throws Exception {
ourServer.stop();
@ -142,7 +213,17 @@ public class TransactionTest {
next.setId(new IdDt("Patient", newId, "9"+Integer.toString(index)));
index++;
}
return theResources;
List<IResource> retVal = theResources;
if (ourReturnOperationOutcome) {
retVal = new ArrayList<IResource>();
OperationOutcome oo = new OperationOutcome();
oo.addIssue().setDetails("AAAAA");
retVal.add(oo);
retVal.addAll(theResources);
}
return retVal;
}

View File

@ -1,7 +1,6 @@
package ca.uhn.fhir.rest.server;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;
import java.util.concurrent.TimeUnit;
@ -73,6 +72,31 @@ public class UpdateTest {
}
@Test
public void testUpdateWhichReturnsCreate() throws Exception {
Patient patient = new Patient();
patient.addIdentifier().setValue("002");
HttpPut httpPost = new HttpPut("http://localhost:" + ourPort + "/Patient/001CREATE");
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());
IOUtils.closeQuietly(status.getEntity().getContent());
ourLog.info("Response was:\n{}", responseContent);
OperationOutcome oo = new FhirContext().newXmlParser().parseResource(OperationOutcome.class, responseContent);
assertEquals("OODETAILS", oo.getIssueFirstRep().getDetails().getValue());
assertEquals(201, status.getStatusLine().getStatusCode());
assertEquals("http://localhost:" + ourPort + "/Patient/001CREATE/_history/002", status.getFirstHeader("location").getValue());
}
@Test
public void testUpdateMethodReturnsInvalidId() throws Exception {
@ -346,6 +370,10 @@ public class UpdateTest {
IdDt id = theId.withVersion(thePatient.getIdentifierFirstRep().getValue().getValue());
OperationOutcome oo = new OperationOutcome();
oo.addIssue().setDetails("OODETAILS");
if (theId.getValueAsString().contains("CREATE")) {
return new MethodOutcome(id,oo, true);
}
return new MethodOutcome(id,oo);
}

View File

@ -86,6 +86,7 @@ import ca.uhn.fhir.rest.server.IBundleProvider;
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.UnprocessableEntityException;
import ca.uhn.fhir.util.FhirTerser;
import com.google.common.base.Function;
@ -468,19 +469,12 @@ public abstract class BaseFhirDao implements IDao {
nextValue = newValue;
/*
@SuppressWarnings("unchecked")
PhysicsUnit<? extends org.unitsofmeasurement.quantity.Quantity<?>> unit = (PhysicsUnit<? extends org.unitsofmeasurement.quantity.Quantity<?>>) UCUMFormat.getCaseInsensitiveInstance().parse(nextValue.getCode().getValue(), null);
if (unit.isCompatible(UCUM.DAY)) {
@SuppressWarnings("unchecked")
PhysicsUnit<org.unitsofmeasurement.quantity.Time> timeUnit = (PhysicsUnit<Time>) unit;
UnitConverter conv = timeUnit.getConverterTo(UCUM.DAY);
double dayValue = conv.convert(nextValue.getValue().getValue().doubleValue());
DurationDt newValue = new DurationDt();
newValue.setSystem(UCUM_NS);
newValue.setCode(UCUM.DAY.getSymbol());
newValue.setValue(dayValue);
nextValue=newValue;
}
* @SuppressWarnings("unchecked") PhysicsUnit<? extends org.unitsofmeasurement.quantity.Quantity<?>> unit = (PhysicsUnit<? extends
* org.unitsofmeasurement.quantity.Quantity<?>>) UCUMFormat.getCaseInsensitiveInstance().parse(nextValue.getCode().getValue(), null); if (unit.isCompatible(UCUM.DAY)) {
*
* @SuppressWarnings("unchecked") PhysicsUnit<org.unitsofmeasurement.quantity.Time> timeUnit = (PhysicsUnit<Time>) unit; UnitConverter conv =
* timeUnit.getConverterTo(UCUM.DAY); double dayValue = conv.convert(nextValue.getValue().getValue().doubleValue()); DurationDt newValue = new DurationDt();
* newValue.setSystem(UCUM_NS); newValue.setCode(UCUM.DAY.getSymbol()); newValue.setValue(dayValue); nextValue=newValue; }
*/
}
}
@ -512,7 +506,6 @@ public abstract class BaseFhirDao implements IDao {
return retVal;
}
protected List<ResourceIndexedSearchParamQuantity> extractSearchParamQuantity(ResourceTable theEntity, IResource theResource) {
ArrayList<ResourceIndexedSearchParamQuantity> retVal = new ArrayList<ResourceIndexedSearchParamQuantity>();
@ -542,7 +535,8 @@ public abstract class BaseFhirDao implements IDao {
continue;
}
ResourceIndexedSearchParamQuantity nextEntity = new ResourceIndexedSearchParamQuantity(resourceName, nextValue.getValue().getValue(), nextValue.getSystem().getValueAsString(), nextValue.getUnits().getValue());
ResourceIndexedSearchParamQuantity nextEntity = new ResourceIndexedSearchParamQuantity(resourceName, nextValue.getValue().getValue(), nextValue.getSystem().getValueAsString(),
nextValue.getUnits().getValue());
nextEntity.setResource(theEntity);
retVal.add(nextEntity);
} else {
@ -560,7 +554,6 @@ public abstract class BaseFhirDao implements IDao {
return retVal;
}
protected List<ResourceIndexedSearchParamString> extractSearchParamStrings(ResourceTable theEntity, IResource theResource) {
ArrayList<ResourceIndexedSearchParamString> retVal = new ArrayList<ResourceIndexedSearchParamString>();
@ -631,7 +624,8 @@ public abstract class BaseFhirDao implements IDao {
} else if (nextObject instanceof ContactDt) {
ContactDt nextContact = (ContactDt) nextObject;
if (nextContact.getValue().isEmpty() == false) {
ResourceIndexedSearchParamString nextEntity = new ResourceIndexedSearchParamString(resourceName, normalizeString(nextContact.getValue().getValueAsString()), nextContact.getValue().getValueAsString());
ResourceIndexedSearchParamString nextEntity = new ResourceIndexedSearchParamString(resourceName, normalizeString(nextContact.getValue().getValueAsString()), nextContact
.getValue().getValueAsString());
nextEntity.setResource(theEntity);
retVal.add(nextEntity);
}
@ -690,7 +684,8 @@ public abstract class BaseFhirDao implements IDao {
} else if (nextObject instanceof CodeableConceptDt) {
CodeableConceptDt nextCC = (CodeableConceptDt) nextObject;
if (!nextCC.getText().isEmpty()) {
ResourceIndexedSearchParamString nextEntity = new ResourceIndexedSearchParamString(nextSpDef.getName(), normalizeString(nextCC.getText().getValue()), nextCC.getText().getValue());
ResourceIndexedSearchParamString nextEntity = new ResourceIndexedSearchParamString(nextSpDef.getName(), normalizeString(nextCC.getText().getValue()), nextCC.getText()
.getValue());
nextEntity.setResource(theEntity);
retVal.add(nextEntity);
}
@ -1105,6 +1100,14 @@ public abstract class BaseFhirDao implements IDao {
entity.setPublished(new Date());
}
if (theResource != null) {
String resourceType = myContext.getResourceDefinition(theResource).getName();
if (isNotBlank(entity.getResourceType()) && !entity.getResourceType().equals(resourceType)) {
throw new UnprocessableEntityException("Existing resource ID[" + entity.getIdDt().toUnqualifiedVersionless() + "] is of type[" + entity.getResourceType() + "] - Cannot update with ["
+ resourceType + "]");
}
}
if (theUpdateHistory) {
final ResourceHistoryTable historyEntry = entity.toHistory();
myEntityManager.persist(historyEntry);
@ -1138,7 +1141,6 @@ public abstract class BaseFhirDao implements IDao {
} else {
stringParams = extractSearchParamStrings(entity, theResource);
numberParams = extractSearchParamNumber(entity, theResource);
quantityParams = extractSearchParamQuantity(entity, theResource);
@ -1253,6 +1255,4 @@ public abstract class BaseFhirDao implements IDao {
return InstantDt.withCurrentTime();
}
}

View File

@ -357,7 +357,7 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
if (entity == null) {
if (theId.hasVersionIdPart()) {
TypedQuery<ResourceHistoryTable> q = myEntityManager.createQuery("SELECT t from ResourceHistoryTable t WHERE t.myResourceId = :RID AND t.myResourceType = :RTYP AND t.myResourceVersion = :RVER", ResourceHistoryTable.class);
q.setParameter("RID", theId.getIdPartAsLong());
q.setParameter("RID", pid);
q.setParameter("RTYP", myResourceName);
q.setParameter("RVER", theId.getVersionIdPartAsLong());
entity = q.getSingleResult();
@ -367,11 +367,24 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
}
}
validateGivenIdIsAppropriateToRetrieveResource(theId, entity);
validateResourceType(entity);
return entity;
}
private void validateGivenIdIsAppropriateToRetrieveResource(IdDt theId, BaseHasResource entity) {
if (entity.getForcedId() != null) {
if (theId.isIdPartValidLong()) {
// This means that the resource with the given numeric ID exists, but it has a "forced ID", meaning that
// as far as the outside world is concerned, the given ID doesn't exist (it's just an internal pointer to the
// forced ID)
throw new ResourceNotFoundException(theId);
}
}
}
@Override
public void removeTag(IdDt theId, String theScheme, String theTerm) {
StopWatch w = new StopWatch();
@ -1433,6 +1446,7 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
if (entity == null) {
throw new ResourceNotFoundException(theId);
}
validateGivenIdIsAppropriateToRetrieveResource(theId, entity);
return entity;
}

View File

@ -22,6 +22,8 @@ import ca.uhn.fhir.jpa.util.StopWatch;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.TagList;
import ca.uhn.fhir.model.dstu.composite.ResourceReferenceDt;
import ca.uhn.fhir.model.dstu.resource.OperationOutcome;
import ca.uhn.fhir.model.dstu.valueset.IssueSeverityEnum;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.server.IBundleProvider;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
@ -35,7 +37,7 @@ public class FhirSystemDao extends BaseFhirDao implements IFhirSystemDao {
@Transactional(propagation = Propagation.REQUIRED)
@Override
public void transaction(List<IResource> theResources) {
public List<IResource> transaction(List<IResource> theResources) {
ourLog.info("Beginning transaction with {} resources", theResources.size());
long start = System.currentTimeMillis();
@ -150,7 +152,16 @@ public class FhirSystemDao extends BaseFhirDao implements IFhirSystemDao {
long delay = System.currentTimeMillis() - start;
ourLog.info("Transaction completed in {}ms with {} creations and {} updates", new Object[] { delay, creations, updates });
OperationOutcome oo = new OperationOutcome();
oo.addIssue().setSeverity(IssueSeverityEnum.INFORMATION).setDetails("Transaction completed in "+delay+"ms with "+creations+" creations and "+updates+" updates");
ArrayList<IResource> retVal = new ArrayList<IResource>();
retVal.add(oo);
retVal.addAll(theResources);
notifyWriteCompleted();
return retVal;
}
@Override

View File

@ -10,7 +10,7 @@ import ca.uhn.fhir.rest.server.IBundleProvider;
public interface IFhirSystemDao extends IDao {
void transaction(List<IResource> theResources);
List<IResource> transaction(List<IResource> theResources);
IBundleProvider history(Date theDate);

View File

@ -27,9 +27,12 @@ import ca.uhn.fhir.rest.annotation.Validate;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.server.IBundleProvider;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
public class JpaResourceProvider<T extends IResource> extends BaseJpaProvider implements IResourceProvider {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(JpaResourceProvider.class);
@Autowired(required = true)
private FhirContext myContext;
@ -140,6 +143,12 @@ public class JpaResourceProvider<T extends IResource> extends BaseJpaProvider im
startRequest(theRequest);
try {
return myDao.update(theResource, theId);
} catch (ResourceNotFoundException e) {
ourLog.info("Can't update resource with ID[" + theId.getValue() + "] because it doesn't exist, going to create it instead");
theResource.setId(theId);
MethodOutcome retVal = myDao.create(theResource);
retVal.setCreated(true);
return retVal;
} finally {
endRequest(theRequest);
}

View File

@ -38,8 +38,7 @@ public class JpaSystemProvider extends BaseJpaProvider {
public List<IResource> transaction(HttpServletRequest theRequest, @TransactionParam List<IResource> theResources) {
startRequest(theRequest);
try {
myDao.transaction(theResources);
return theResources;
return myDao.transaction(theResources);
} finally {
endRequest(theRequest);
}

View File

@ -56,6 +56,7 @@ import ca.uhn.fhir.rest.server.IBundleProvider;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
public class FhirResourceDaoTest {
@ -973,7 +974,8 @@ public class FhirResourceDaoTest {
Patient patient = new Patient();
patient.addIdentifier().setSystem("urn:system").setValue("testSearchTokenParam001");
patient.addName().addFamily("Tester").addGiven("testSearchTokenParam1");
patient.addCommunication().setText("testSearchTokenParamComText").addCoding().setCode("testSearchTokenParamCode").setSystem("testSearchTokenParamSystem").setDisplay("testSearchTokenParamDisplay");
patient.addCommunication().setText("testSearchTokenParamComText").addCoding().setCode("testSearchTokenParamCode").setSystem("testSearchTokenParamSystem")
.setDisplay("testSearchTokenParamDisplay");
ourPatientDao.create(patient);
patient = new Patient();
@ -1327,6 +1329,86 @@ public class FhirResourceDaoTest {
}
@Test
public void testUpdateRejectsInvalidTypes() throws InterruptedException {
Patient p1 = new Patient();
p1.addIdentifier("urn:system", "testUpdateRejectsInvalidTypes");
p1.addName().addFamily("Tester").addGiven("testUpdateRejectsInvalidTypes");
IdDt p1id = ourPatientDao.create(p1).getId();
Organization p2 = new Organization();
p2.getName().setValue("testUpdateRejectsInvalidTypes");
try {
ourOrganizationDao.update(p2, new IdDt("Organization/" + p1id.getIdPart()));
fail();
} catch (UnprocessableEntityException e) {
// good
}
try {
ourOrganizationDao.update(p2, new IdDt("Patient/" + p1id.getIdPart()));
fail();
} catch (UnprocessableEntityException e) {
// good
}
}
@Test
public void testUpdateRejectsIdWhichPointsToForcedId() throws InterruptedException {
Patient p1 = new Patient();
p1.addIdentifier("urn:system", "testUpdateRejectsIdWhichPointsToForcedId01");
p1.addName().addFamily("Tester").addGiven("testUpdateRejectsIdWhichPointsToForcedId01");
p1.setId("ABABA");
IdDt p1id = ourPatientDao.create(p1).getId();
assertEquals("ABABA", p1id.getIdPart());
Patient p2 = new Patient();
p2.addIdentifier("urn:system", "testUpdateRejectsIdWhichPointsToForcedId02");
p2.addName().addFamily("Tester").addGiven("testUpdateRejectsIdWhichPointsToForcedId02");
IdDt p2id = ourPatientDao.create(p2).getId();
long p1longId = p2id.getIdPartAsLong() - 1;
try {
ourPatientDao.read(new IdDt("Patient/" + p1longId));
fail();
} catch (ResourceNotFoundException e) {
// good
}
try {
ourPatientDao.update(p1, new IdDt("Patient/" + p1longId));
fail();
} catch (ResourceNotFoundException e) {
// good
}
}
@Test
public void testReadForcedIdVersionHistory() throws InterruptedException {
Patient p1 = new Patient();
p1.addIdentifier("urn:system", "testReadVorcedIdVersionHistory01");
p1.setId("testReadVorcedIdVersionHistory");
IdDt p1id = ourPatientDao.create(p1).getId();
assertEquals("testReadVorcedIdVersionHistory", p1id.getIdPart());
p1.addIdentifier("urn:system", "testReadVorcedIdVersionHistory02");
IdDt p1idv2 = ourPatientDao.update(p1, p1id).getId();
assertEquals("testReadVorcedIdVersionHistory", p1idv2.getIdPart());
assertNotEquals(p1id.getValue(), p1idv2.getValue());
Patient v1 = ourPatientDao.read(p1id);
assertEquals(1, v1.getIdentifier().size());
Patient v2 = ourPatientDao.read(p1idv2);
assertEquals(2, v2.getIdentifier().size());
}
@SuppressWarnings("unchecked")
private <T extends IResource> List<T> toList(IBundleProvider theSearch) {
return (List<T>) theSearch.getResources(0, theSearch.size());

View File

@ -33,6 +33,7 @@ import ca.uhn.fhir.model.dstu.valueset.EncounterStateEnum;
import ca.uhn.fhir.model.dstu.valueset.NarrativeStatusEnum;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.client.IGenericClient;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.gclient.StringClientParam;
@ -82,6 +83,24 @@ public class CompleteResourceProviderTest {
private static IFhirResourceDao<Questionnaire> questionnaireDao;
@Test
public void testUpdateWithClientSuppliedIdWhichDoesntExist() {
deleteToken("Patient", Patient.SP_IDENTIFIER, "urn:system", "testUpdateWithClientSuppliedIdWhichDoesntExist");
Patient p1 = new Patient();
p1.addIdentifier().setSystem("urn:system").setValue("testUpdateWithClientSuppliedIdWhichDoesntExist");
MethodOutcome outcome = ourClient.update().resource(p1).withId("testUpdateWithClientSuppliedIdWhichDoesntExist").execute();
assertEquals(true, outcome.getCreated().booleanValue());
IdDt p1Id = outcome.getId();
assertThat(p1Id.getValue(), containsString("Patient/testUpdateWithClientSuppliedIdWhichDoesntExist/_history"));
Bundle actual = ourClient.search().forResource(Patient.class).where(Patient.IDENTIFIER.exactly().systemAndCode("urn:system", "testUpdateWithClientSuppliedIdWhichDoesntExist")).encodedJson().prettyPrint().execute();
assertEquals(1, actual.size());
assertEquals(p1Id.getIdPart(), actual.getEntries().get(0).getId().getIdPart());
}
@Test
public void testCreateWithClientSuppliedId() {
deleteToken("Patient", Patient.SP_IDENTIFIER, "urn:system", "testCreateWithId01");
@ -189,6 +208,34 @@ public class CompleteResourceProviderTest {
}
@Test
public void testUpdateRejectsInvalidTypes() throws InterruptedException {
deleteToken("Patient", Patient.SP_IDENTIFIER, "urn:system", "testUpdateRejectsInvalidTypes");
Patient p1 = new Patient();
p1.addIdentifier("urn:system", "testUpdateRejectsInvalidTypes");
p1.addName().addFamily("Tester").addGiven("testUpdateRejectsInvalidTypes");
IdDt p1id = ourClient.create().resource(p1).execute().getId();
Organization p2 = new Organization();
p2.getName().setValue("testUpdateRejectsInvalidTypes");
try {
ourClient.update().resource(p2).withId("Organization/" + p1id.getIdPart()).execute();
fail();
} catch (UnprocessableEntityException e) {
// good
}
try {
ourClient.update().resource(p2).withId("Patient/" + p1id.getIdPart()).execute();
fail();
} catch (UnprocessableEntityException e) {
// good
}
}
@Test
public void testDeepChaining() {
delete("Location", Location.SP_NAME, "testDeepChainingL1");

View File

@ -12,7 +12,7 @@
<dependent-module archiveName="hapi-fhir-base-0.6-SNAPSHOT.jar" deploy-path="/WEB-INF/lib" handle="module:/resource/hapi-fhir-base/hapi-fhir-base">
<dependency-type>uses</dependency-type>
</dependent-module>
<dependent-module deploy-path="/" handle="module:/overlay/prj/hapi-fhir-tester-overlay?includes=**/**&amp;excludes=META-INF/MANIFEST.MF">
<dependent-module deploy-path="/" handle="module:/overlay/prj/hapi-fhir-testpage-overlay?includes=**/**&amp;excludes=META-INF/MANIFEST.MF">
<dependency-type>consumes</dependency-type>
</dependent-module>
<dependent-module deploy-path="/" handle="module:/overlay/slf/?includes=**/**&amp;excludes=META-INF/MANIFEST.MF">

View File

@ -76,7 +76,7 @@
<init-param>
<description>A comma separated list non-standard response headers that will be exposed to XHR2 object.</description>
<param-name>cors.exposed.headers</param-name>
<param-value></param-value>
<param-value>Location,Content-Location</param-value>
</init-param>
<init-param>
<description>A flag that suggests if CORS is supported with cookies</description>

View File

@ -19,13 +19,13 @@
<attribute name="org.eclipse.jst.component.dependency" value="/WEB-INF/lib"/>
</attributes>
</classpathentry>
<classpathentry combineaccessrules="false" kind="src" path="/hapi-fhir-base"/>
<classpathentry combineaccessrules="false" kind="src" path="/hapi-fhir-jpaserver-base"/>
<classpathentry combineaccessrules="false" kind="src" path="/hapi-fhir-jpaserver-test"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry combineaccessrules="false" kind="src" path="/hapi-fhir-base"/>
<classpathentry combineaccessrules="false" kind="src" path="/hapi-fhir-jpaserver-base"/>
<classpathentry combineaccessrules="false" kind="src" path="/hapi-fhir-jpaserver-test"/>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@ -129,6 +129,11 @@
<artifactId>spring-context-support</artifactId>
<version>${spring_version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring_version}</version>
</dependency>
<dependency>
@ -167,8 +172,6 @@
<version>${jetty_version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>

View File

@ -6,7 +6,7 @@
<dependent-module archiveName="hapi-fhir-base-0.6-SNAPSHOT.jar" deploy-path="/WEB-INF/lib" handle="module:/resource/hapi-fhir-base/hapi-fhir-base">
<dependency-type>uses</dependency-type>
</dependent-module>
<dependent-module deploy-path="/" handle="module:/overlay/prj/hapi-fhir-tester-overlay?includes=**/**&amp;excludes=META-INF/MANIFEST.MF">
<dependent-module deploy-path="/" handle="module:/overlay/prj/hapi-fhir-testpage-overlay?includes=**/**&amp;excludes=META-INF/MANIFEST.MF">
<dependency-type>consumes</dependency-type>
</dependent-module>
<dependent-module deploy-path="/" handle="module:/overlay/slf/?includes=**/**&amp;excludes=META-INF/MANIFEST.MF">

View File

@ -51,7 +51,7 @@
<init-param>
<description>A comma separated list non-standard response headers that will be exposed to XHR2 object.</description>
<param-name>cors.exposed.headers</param-name>
<param-value></param-value>
<param-value>Location,Content-Location</param-value>
</init-param>
<init-param>
<description>A flag that suggests if CORS is supported with cookies</description>