Fix #6: Allow update operation to create a resource if it doesn't

already exist
This commit is contained in:
James Agnew 2014-08-08 14:07:03 -04:00
parent e17af44a09
commit b43b6c6d2f
29 changed files with 513 additions and 95 deletions

View File

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

View File

@ -36,12 +36,28 @@
Add documentation on how to use eBay CORS Filter to support Cross Origin Resource 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 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 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>
<action type="fix"> <action type="fix">
IResource interface did not expose the getLanguage/setLanguage methods from BaseResource, IResource interface did not expose the getLanguage/setLanguage methods from BaseResource,
so the resource language was difficult to access. so the resource language was difficult to access.
</action> </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>
</release> </release>
<release version="0.5" date="2014-Jul-30"> <release version="0.5" date="2014-Jul-30">
<action type="add"> <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.InstantDt;
import ca.uhn.fhir.model.primitive.IntegerDt; import ca.uhn.fhir.model.primitive.IntegerDt;
import ca.uhn.fhir.model.primitive.StringDt; 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; import ca.uhn.fhir.rest.server.Constants;
public class Bundle extends BaseBundle /* implements IElement */{ public class Bundle extends BaseBundle /* implements IElement */{
@ -103,6 +105,31 @@ public class Bundle extends BaseBundle /* implements IElement */{
return map.get(theId.toUnqualified()); 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() { public List<BundleEntry> getEntries() {
if (myEntries == null) { if (myEntries == null) {
myEntries = new ArrayList<BundleEntry>(); myEntries = new ArrayList<BundleEntry>();
@ -248,14 +275,15 @@ public class Bundle extends BaseBundle /* implements IElement */{
RuntimeResourceDefinition def = theContext.getResourceDefinition(theResource); RuntimeResourceDefinition def = theContext.getResourceDefinition(theResource);
if (theResource.getId() != null && StringUtils.isNotBlank(theResource.getId().getValue())) {
String title = ResourceMetadataKeyEnum.TITLE.get(theResource); String title = ResourceMetadataKeyEnum.TITLE.get(theResource);
if (title != null) { if (title != null) {
entry.getTitle().setValue(title); entry.getTitle().setValue(title);
} else { } 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(); StringBuilder b = new StringBuilder();
b.append(theServerBase); b.append(theServerBase);
if (b.length() > 0 && b.charAt(b.length() - 1) != '/') { 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()); 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() { public String getIdPart() {
return myUnqualifiedId; return myUnqualifiedId;
} }

View File

@ -47,6 +47,7 @@ import javax.json.JsonValue;
import javax.json.JsonValue.ValueType; import javax.json.JsonValue.ValueType;
import javax.json.stream.JsonGenerator; import javax.json.stream.JsonGenerator;
import javax.json.stream.JsonGeneratorFactory; import javax.json.stream.JsonGeneratorFactory;
import javax.json.stream.JsonParsingException;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate; 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) { 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) { if (theResourceTypeObj.getValueType() != theValueType) {
throw new DataFormatException("Invalid content of element " + thePosition + ", expected " + theValueType); throw new DataFormatException("Invalid content of element " + thePosition + ", expected " + theValueType);
} }
@ -620,9 +625,18 @@ public class JsonParser extends BaseParser implements IParser {
@Override @Override
public <T extends IResource> Bundle parseBundle(Class<T> theResourceType, Reader theReader) { public <T extends IResource> Bundle parseBundle(Class<T> theResourceType, Reader theReader) {
JsonReader reader = Json.createReader(theReader); JsonReader reader;
JsonObject object = reader.readObject(); 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"); JsonValue resourceTypeObj = object.get("resourceType");
assertObjectOfType(resourceTypeObj, JsonValue.ValueType.STRING, "resourceType"); assertObjectOfType(resourceTypeObj, JsonValue.ValueType.STRING, "resourceType");
String resourceType = ((JsonString) resourceTypeObj).getString(); String resourceType = ((JsonString) resourceTypeObj).getString();

View File

@ -28,19 +28,72 @@ public class MethodOutcome {
private IdDt myId; private IdDt myId;
private OperationOutcome myOperationOutcome; private OperationOutcome myOperationOutcome;
private IdDt myVersionId; private IdDt myVersionId;
private Boolean myCreated;
/**
* Constructor
*/
public MethodOutcome() { public MethodOutcome() {
} }
/**
* Constructor
*
* @param theId
* The ID of the created/updated resource
*/
public MethodOutcome(IdDt theId) { public MethodOutcome(IdDt theId) {
myId = 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) { public MethodOutcome(IdDt theId, OperationOutcome theOperationOutcome) {
myId = theId; myId = theId;
myOperationOutcome = theOperationOutcome; 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 * @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 * Returns the {@link OperationOutcome} resource to return to the client or <code>null</code> if none.
* <code>null</code> if none.
* *
* @return This method <b>will return null</b>, unlike many methods in the * @return This method <b>will return null</b>, unlike many methods in the API.
* API.
*/ */
public OperationOutcome getOperationOutcome() { public OperationOutcome getOperationOutcome() {
return myOperationOutcome; return myOperationOutcome;
@ -80,13 +131,32 @@ public class MethodOutcome {
return myVersionId; 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) { public void setId(IdDt theId) {
myId = theId; myId = theId;
} }
/** /**
* Sets the {@link OperationOutcome} resource to return to the client. Set * Sets the {@link OperationOutcome} resource to return to the client. Set to <code>null</code> (which is the default) if none.
* to <code>null</code> (which is the default) if none.
*/ */
public void setOperationOutcome(OperationOutcome theOperationOutcome) { public void setOperationOutcome(OperationOutcome theOperationOutcome) {
myOperationOutcome = 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, public MethodOutcome invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws IOException,
BaseServerResponseException { BaseServerResponseException {
MethodOutcome response = MethodUtil.process2xxResponse(myContext, myResourceName, theResponseStatusCode, theResponseMimeType, theResponseReader, theHeaders); MethodOutcome response = MethodUtil.process2xxResponse(myContext, myResourceName, theResponseStatusCode, theResponseMimeType, theResponseReader, theHeaders);
if (theResponseStatusCode == Constants.STATUS_HTTP_201_CREATED) {
response.setCreated(true);
}
return response; return response;
} }
} }

View File

@ -62,6 +62,8 @@ public interface IGenericClient {
* @param theResource * @param theResource
* The resource to create * The resource to create
* @return An outcome * @return An outcome
* @deprecated Use {@link #create() fluent method instead}. This method will be removed.
*
*/ */
MethodOutcome create(IResource theResource); 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() throw new InternalErrorException("Method " + getMethod().getName() + " in type " + getMethod().getDeclaringClass().getCanonicalName()
+ " returned null, which is not allowed for create operation"); + " returned null, which is not allowed for create operation");
} }
if (response.getCreated() == null || response.getCreated() == Boolean.TRUE) {
theResponse.setStatus(Constants.STATUS_HTTP_201_CREATED); theResponse.setStatus(Constants.STATUS_HTTP_201_CREATED);
} else {
theResponse.setStatus(Constants.STATUS_HTTP_200_OK);
}
addLocationHeader(theRequest, theResponse, response); addLocationHeader(theRequest, theResponse, response);
break; break;
case UPDATE: case UPDATE:
if (response.getCreated() == null || response.getCreated() == Boolean.FALSE) {
theResponse.setStatus(Constants.STATUS_HTTP_200_OK); theResponse.setStatus(Constants.STATUS_HTTP_200_OK);
} else {
theResponse.setStatus(Constants.STATUS_HTTP_201_CREATED);
}
addLocationHeader(theRequest, theResponse, response); addLocationHeader(theRequest, theResponse, response);
break; break;

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.Bundle;
import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; 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.RestfulOperationSystemEnum;
import ca.uhn.fhir.model.dstu.valueset.RestfulOperationTypeEnum; import ca.uhn.fhir.model.dstu.valueset.RestfulOperationTypeEnum;
import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.IdDt;
@ -104,11 +105,16 @@ public class TransactionMethodBinding extends BaseResourceReturningMethodBinding
Object response= invokeServerMethod(theMethodParams); Object response= invokeServerMethod(theMethodParams);
IBundleProvider retVal = toResourceList(response); IBundleProvider retVal = toResourceList(response);
int offset = 0;
if (retVal.size() != resources.size()) { 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)"); 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++) { for (int i =0; i < resources.size(); i++) {
IdDt oldId = oldIds.get(i); IdDt oldId = oldIds.get(i);
IResource newRes = retResources.get(i); IResource newRes = retResources.get(i);
@ -117,8 +123,8 @@ public class TransactionMethodBinding extends BaseResourceReturningMethodBinding
} }
if (oldId != null && !oldId.isEmpty()) { if (oldId != null && !oldId.isEmpty()) {
if (!oldId.getId().equals(newRes.getId())) { if (!oldId.equals(newRes.getId())) {
newRes.getResourceMetadata().put(ResourceMetadataKeyEnum.PREVIOUS_ID, oldId.getId()); newRes.getResourceMetadata().put(ResourceMetadataKeyEnum.PREVIOUS_ID, oldId);
} }
} }
} }

View File

@ -1054,9 +1054,11 @@ public class RestfulServer extends HttpServlet {
for (IResource next : resourceList) { for (IResource next : resourceList) {
if (next.getId() == null || next.getId().isEmpty()) { 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)"); 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()); 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"); outcome.addIssue().setDetails("One minor issue detected");
retVal.setOperationOutcome(outcome); 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; return retVal;
} }
//END SNIPPET: update //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
* 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.
* 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
* 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, and it is acceptable to return the same list instance that was // passed in.
* passed in. List<IResource> retVal = new ArrayList<IResource>(theResources);
*/
List<IResource> retVal = theResources;
for (IResource next : theResources) { for (IResource next : theResources) {
/* /*
* Populate each returned resource with the new ID for that resource, * 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); 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; return retVal;
} }
//END SNIPPET: transaction //END SNIPPET: transaction

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 @Test
public void testEncodeExtensionInCompositeElement() { public void testEncodeExtensionInCompositeElement() {

View File

@ -2,6 +2,7 @@ package ca.uhn.fhir.rest.server;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; 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.server.Server;
import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlet.ServletHolder;
import org.junit.AfterClass; import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Test; 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.BundleEntry;
import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; 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.dstu.resource.Patient;
import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.model.primitive.InstantDt;
@ -37,10 +40,19 @@ import ca.uhn.fhir.testutil.RandomServerPortProvider;
public class TransactionTest { public class TransactionTest {
private static CloseableHttpClient ourClient; 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 final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TransactionTest.class);
private static int ourPort; private static int ourPort;
private static boolean ourReturnOperationOutcome;
private static Server ourServer; private static Server ourServer;
private static FhirContext ourCtx = new FhirContext();
@Before
public void before() {
ourReturnOperationOutcome = false;
}
@Test @Test
public void testTransaction() throws Exception { public void testTransaction() throws Exception {
@ -95,6 +107,65 @@ public class TransactionTest {
assertEquals(nowInstant.getValueAsString(), entry2.getDeletedAt().getValueAsString()); 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 @AfterClass
public static void afterClass() throws Exception { public static void afterClass() throws Exception {
ourServer.stop(); ourServer.stop();
@ -142,7 +213,17 @@ public class TransactionTest {
next.setId(new IdDt("Patient", newId, "9"+Integer.toString(index))); next.setId(new IdDt("Patient", newId, "9"+Integer.toString(index)));
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; package ca.uhn.fhir.rest.server;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import java.util.concurrent.TimeUnit; 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 @Test
public void testUpdateMethodReturnsInvalidId() throws Exception { public void testUpdateMethodReturnsInvalidId() throws Exception {
@ -346,6 +370,10 @@ public class UpdateTest {
IdDt id = theId.withVersion(thePatient.getIdentifierFirstRep().getValue().getValue()); IdDt id = theId.withVersion(thePatient.getIdentifierFirstRep().getValue().getValue());
OperationOutcome oo = new OperationOutcome(); OperationOutcome oo = new OperationOutcome();
oo.addIssue().setDetails("OODETAILS"); oo.addIssue().setDetails("OODETAILS");
if (theId.getValueAsString().contains("CREATE")) {
return new MethodOutcome(id,oo, true);
}
return new MethodOutcome(id,oo); 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.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.FhirTerser; import ca.uhn.fhir.util.FhirTerser;
import com.google.common.base.Function; import com.google.common.base.Function;
@ -465,22 +466,15 @@ public abstract class BaseFhirDao implements IDao {
newValue.setSystem(UCUM_NS); newValue.setSystem(UCUM_NS);
newValue.setCode(NonSI.DAY.toString()); newValue.setCode(NonSI.DAY.toString());
newValue.setValue(dayValue); newValue.setValue(dayValue);
nextValue=newValue; nextValue = newValue;
/* /*
@SuppressWarnings("unchecked") * @SuppressWarnings("unchecked") PhysicsUnit<? extends org.unitsofmeasurement.quantity.Quantity<?>> unit = (PhysicsUnit<? extends
PhysicsUnit<? extends org.unitsofmeasurement.quantity.Quantity<?>> unit = (PhysicsUnit<? extends org.unitsofmeasurement.quantity.Quantity<?>>) UCUMFormat.getCaseInsensitiveInstance().parse(nextValue.getCode().getValue(), null); * org.unitsofmeasurement.quantity.Quantity<?>>) UCUMFormat.getCaseInsensitiveInstance().parse(nextValue.getCode().getValue(), null); if (unit.isCompatible(UCUM.DAY)) {
if (unit.isCompatible(UCUM.DAY)) { *
@SuppressWarnings("unchecked") * @SuppressWarnings("unchecked") PhysicsUnit<org.unitsofmeasurement.quantity.Time> timeUnit = (PhysicsUnit<Time>) unit; UnitConverter conv =
PhysicsUnit<org.unitsofmeasurement.quantity.Time> timeUnit = (PhysicsUnit<Time>) unit; * timeUnit.getConverterTo(UCUM.DAY); double dayValue = conv.convert(nextValue.getValue().getValue().doubleValue()); DurationDt newValue = new DurationDt();
UnitConverter conv = timeUnit.getConverterTo(UCUM.DAY); * newValue.setSystem(UCUM_NS); newValue.setCode(UCUM.DAY.getSymbol()); newValue.setValue(dayValue); nextValue=newValue; }
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;
}
*/ */
} }
} }
@ -488,7 +482,7 @@ public abstract class BaseFhirDao implements IDao {
ResourceIndexedSearchParamNumber nextEntity = new ResourceIndexedSearchParamNumber(resourceName, nextValue.getValue().getValue()); ResourceIndexedSearchParamNumber nextEntity = new ResourceIndexedSearchParamNumber(resourceName, nextValue.getValue().getValue());
nextEntity.setResource(theEntity); nextEntity.setResource(theEntity);
retVal.add(nextEntity); retVal.add(nextEntity);
}else if (nextObject instanceof QuantityDt) { } else if (nextObject instanceof QuantityDt) {
QuantityDt nextValue = (QuantityDt) nextObject; QuantityDt nextValue = (QuantityDt) nextObject;
if (nextValue.getValue().isEmpty()) { if (nextValue.getValue().isEmpty()) {
continue; continue;
@ -512,7 +506,6 @@ public abstract class BaseFhirDao implements IDao {
return retVal; return retVal;
} }
protected List<ResourceIndexedSearchParamQuantity> extractSearchParamQuantity(ResourceTable theEntity, IResource theResource) { protected List<ResourceIndexedSearchParamQuantity> extractSearchParamQuantity(ResourceTable theEntity, IResource theResource) {
ArrayList<ResourceIndexedSearchParamQuantity> retVal = new ArrayList<ResourceIndexedSearchParamQuantity>(); ArrayList<ResourceIndexedSearchParamQuantity> retVal = new ArrayList<ResourceIndexedSearchParamQuantity>();
@ -542,7 +535,8 @@ public abstract class BaseFhirDao implements IDao {
continue; 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); nextEntity.setResource(theEntity);
retVal.add(nextEntity); retVal.add(nextEntity);
} else { } else {
@ -560,7 +554,6 @@ public abstract class BaseFhirDao implements IDao {
return retVal; return retVal;
} }
protected List<ResourceIndexedSearchParamString> extractSearchParamStrings(ResourceTable theEntity, IResource theResource) { protected List<ResourceIndexedSearchParamString> extractSearchParamStrings(ResourceTable theEntity, IResource theResource) {
ArrayList<ResourceIndexedSearchParamString> retVal = new ArrayList<ResourceIndexedSearchParamString>(); ArrayList<ResourceIndexedSearchParamString> retVal = new ArrayList<ResourceIndexedSearchParamString>();
@ -631,7 +624,8 @@ public abstract class BaseFhirDao implements IDao {
} else if (nextObject instanceof ContactDt) { } else if (nextObject instanceof ContactDt) {
ContactDt nextContact = (ContactDt) nextObject; ContactDt nextContact = (ContactDt) nextObject;
if (nextContact.getValue().isEmpty() == false) { 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); nextEntity.setResource(theEntity);
retVal.add(nextEntity); retVal.add(nextEntity);
} }
@ -690,7 +684,8 @@ public abstract class BaseFhirDao implements IDao {
} else if (nextObject instanceof CodeableConceptDt) { } else if (nextObject instanceof CodeableConceptDt) {
CodeableConceptDt nextCC = (CodeableConceptDt) nextObject; CodeableConceptDt nextCC = (CodeableConceptDt) nextObject;
if (!nextCC.getText().isEmpty()) { 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); nextEntity.setResource(theEntity);
retVal.add(nextEntity); retVal.add(nextEntity);
} }
@ -1105,6 +1100,14 @@ public abstract class BaseFhirDao implements IDao {
entity.setPublished(new Date()); 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) { if (theUpdateHistory) {
final ResourceHistoryTable historyEntry = entity.toHistory(); final ResourceHistoryTable historyEntry = entity.toHistory();
myEntityManager.persist(historyEntry); myEntityManager.persist(historyEntry);
@ -1138,7 +1141,6 @@ public abstract class BaseFhirDao implements IDao {
} else { } else {
stringParams = extractSearchParamStrings(entity, theResource); stringParams = extractSearchParamStrings(entity, theResource);
numberParams = extractSearchParamNumber(entity, theResource); numberParams = extractSearchParamNumber(entity, theResource);
quantityParams = extractSearchParamQuantity(entity, theResource); quantityParams = extractSearchParamQuantity(entity, theResource);
@ -1253,6 +1255,4 @@ public abstract class BaseFhirDao implements IDao {
return InstantDt.withCurrentTime(); return InstantDt.withCurrentTime();
} }
} }

View File

@ -367,11 +367,24 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
} }
} }
validateGivenIdIsAppropriateToRetrieveResource(theId, entity);
validateResourceType(entity); validateResourceType(entity);
return 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 @Override
public void removeTag(IdDt theId, String theScheme, String theTerm) { public void removeTag(IdDt theId, String theScheme, String theTerm) {
StopWatch w = new StopWatch(); StopWatch w = new StopWatch();
@ -1433,6 +1446,7 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
if (entity == null) { if (entity == null) {
throw new ResourceNotFoundException(theId); throw new ResourceNotFoundException(theId);
} }
validateGivenIdIsAppropriateToRetrieveResource(theId, entity);
return 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.IResource;
import ca.uhn.fhir.model.api.TagList; import ca.uhn.fhir.model.api.TagList;
import ca.uhn.fhir.model.dstu.composite.ResourceReferenceDt; 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.model.primitive.IdDt;
import ca.uhn.fhir.rest.server.IBundleProvider; import ca.uhn.fhir.rest.server.IBundleProvider;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
@ -35,7 +37,7 @@ public class FhirSystemDao extends BaseFhirDao implements IFhirSystemDao {
@Transactional(propagation = Propagation.REQUIRED) @Transactional(propagation = Propagation.REQUIRED)
@Override @Override
public void transaction(List<IResource> theResources) { public List<IResource> transaction(List<IResource> theResources) {
ourLog.info("Beginning transaction with {} resources", theResources.size()); ourLog.info("Beginning transaction with {} resources", theResources.size());
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
@ -150,7 +152,16 @@ public class FhirSystemDao extends BaseFhirDao implements IFhirSystemDao {
long delay = System.currentTimeMillis() - start; long delay = System.currentTimeMillis() - start;
ourLog.info("Transaction completed in {}ms with {} creations and {} updates", new Object[] { delay, creations, updates }); 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(); notifyWriteCompleted();
return retVal;
} }
@Override @Override

View File

@ -10,7 +10,7 @@ import ca.uhn.fhir.rest.server.IBundleProvider;
public interface IFhirSystemDao extends IDao { public interface IFhirSystemDao extends IDao {
void transaction(List<IResource> theResources); List<IResource> transaction(List<IResource> theResources);
IBundleProvider history(Date theDate); 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.api.MethodOutcome;
import ca.uhn.fhir.rest.server.IBundleProvider; import ca.uhn.fhir.rest.server.IBundleProvider;
import ca.uhn.fhir.rest.server.IResourceProvider; 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 { 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) @Autowired(required = true)
private FhirContext myContext; private FhirContext myContext;
@ -140,6 +143,12 @@ public class JpaResourceProvider<T extends IResource> extends BaseJpaProvider im
startRequest(theRequest); startRequest(theRequest);
try { try {
return myDao.update(theResource, theId); 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 { } finally {
endRequest(theRequest); endRequest(theRequest);
} }

View File

@ -38,8 +38,7 @@ public class JpaSystemProvider extends BaseJpaProvider {
public List<IResource> transaction(HttpServletRequest theRequest, @TransactionParam List<IResource> theResources) { public List<IResource> transaction(HttpServletRequest theRequest, @TransactionParam List<IResource> theResources) {
startRequest(theRequest); startRequest(theRequest);
try { try {
myDao.transaction(theResources); return myDao.transaction(theResources);
return theResources;
} finally { } finally {
endRequest(theRequest); 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.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
public class FhirResourceDaoTest { public class FhirResourceDaoTest {
@ -973,7 +974,8 @@ public class FhirResourceDaoTest {
Patient patient = new Patient(); Patient patient = new Patient();
patient.addIdentifier().setSystem("urn:system").setValue("testSearchTokenParam001"); patient.addIdentifier().setSystem("urn:system").setValue("testSearchTokenParam001");
patient.addName().addFamily("Tester").addGiven("testSearchTokenParam1"); 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); ourPatientDao.create(patient);
patient = new Patient(); patient = new Patient();
@ -1327,6 +1329,63 @@ 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
}
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private <T extends IResource> List<T> toList(IBundleProvider theSearch) { private <T extends IResource> List<T> toList(IBundleProvider theSearch) {
return (List<T>) theSearch.getResources(0, theSearch.size()); 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.dstu.valueset.NarrativeStatusEnum;
import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; 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.IGenericClient;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.gclient.StringClientParam; import ca.uhn.fhir.rest.gclient.StringClientParam;
@ -82,6 +83,24 @@ public class CompleteResourceProviderTest {
private static IFhirResourceDao<Questionnaire> questionnaireDao; 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 @Test
public void testCreateWithClientSuppliedId() { public void testCreateWithClientSuppliedId() {
deleteToken("Patient", Patient.SP_IDENTIFIER, "urn:system", "testCreateWithId01"); 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 @Test
public void testDeepChaining() { public void testDeepChaining() {
delete("Location", Location.SP_NAME, "testDeepChainingL1"); 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"> <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> <dependency-type>uses</dependency-type>
</dependent-module> </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> <dependency-type>consumes</dependency-type>
</dependent-module> </dependent-module>
<dependent-module deploy-path="/" handle="module:/overlay/slf/?includes=**/**&amp;excludes=META-INF/MANIFEST.MF"> <dependent-module deploy-path="/" handle="module:/overlay/slf/?includes=**/**&amp;excludes=META-INF/MANIFEST.MF">

View File

@ -19,13 +19,13 @@
<attribute name="org.eclipse.jst.component.dependency" value="/WEB-INF/lib"/> <attribute name="org.eclipse.jst.component.dependency" value="/WEB-INF/lib"/>
</attributes> </attributes>
</classpathentry> </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"> <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6">
<attributes> <attributes>
<attribute name="maven.pomderived" value="true"/> <attribute name="maven.pomderived" value="true"/>
</attributes> </attributes>
</classpathentry> </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"/> <classpathentry kind="output" path="target/classes"/>
</classpath> </classpath>

View File

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

View File

@ -16,12 +16,12 @@
</arguments> </arguments>
</buildCommand> </buildCommand>
<buildCommand> <buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name> <name>org.eclipse.wst.validation.validationbuilder</name>
<arguments> <arguments>
</arguments> </arguments>
</buildCommand> </buildCommand>
<buildCommand> <buildCommand>
<name>org.eclipse.wst.validation.validationbuilder</name> <name>org.eclipse.m2e.core.maven2Builder</name>
<arguments> <arguments>
</arguments> </arguments>
</buildCommand> </buildCommand>

View File

@ -0,0 +1,3 @@
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding/<project>=UTF-8

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"> <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> <dependency-type>uses</dependency-type>
</dependent-module> </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> <dependency-type>consumes</dependency-type>
</dependent-module> </dependent-module>
<dependent-module deploy-path="/" handle="module:/overlay/slf/?includes=**/**&amp;excludes=META-INF/MANIFEST.MF"> <dependent-module deploy-path="/" handle="module:/overlay/slf/?includes=**/**&amp;excludes=META-INF/MANIFEST.MF">