From b43b6c6d2f10da003bcae15fccc1c4bfc14fa82d Mon Sep 17 00:00:00 2001 From: James Agnew Date: Fri, 8 Aug 2014 14:07:03 -0400 Subject: [PATCH] Fix #6: Allow update operation to create a resource if it doesn't already exist --- hapi-fhir-base/pom.xml | 8 +- hapi-fhir-base/src/changes/changes.xml | 18 +++- .../java/ca/uhn/fhir/model/api/Bundle.java | 40 +++++++-- .../ca/uhn/fhir/model/primitive/IdDt.java | 9 -- .../java/ca/uhn/fhir/parser/JsonParser.java | 20 ++++- .../ca/uhn/fhir/rest/api/MethodOutcome.java | 82 +++++++++++++++-- .../uhn/fhir/rest/client/GenericClient.java | 3 + .../uhn/fhir/rest/client/IGenericClient.java | 2 + .../BaseOutcomeReturningMethodBinding.java | 12 ++- .../rest/method/TransactionMethodBinding.java | 14 ++- .../uhn/fhir/rest/server/RestfulServer.java | 4 +- .../RestfulPatientResourceProviderMore.java | 27 ++++-- .../ca/uhn/fhir/parser/JsonParserTest.java | 11 +++ .../uhn/fhir/rest/server/TransactionTest.java | 89 ++++++++++++++++++- .../ca/uhn/fhir/rest/server/UpdateTest.java | 30 ++++++- .../java/ca/uhn/fhir/jpa/dao/BaseFhirDao.java | 60 ++++++------- .../ca/uhn/fhir/jpa/dao/FhirResourceDao.java | 20 ++++- .../ca/uhn/fhir/jpa/dao/FhirSystemDao.java | 13 ++- .../ca/uhn/fhir/jpa/dao/IFhirSystemDao.java | 2 +- .../jpa/provider/JpaResourceProvider.java | 9 ++ .../fhir/jpa/provider/JpaSystemProvider.java | 3 +- .../uhn/fhir/jpa/dao/FhirResourceDaoTest.java | 61 ++++++++++++- .../test/CompleteResourceProviderTest.java | 47 ++++++++++ .../org.eclipse.wst.common.component | 2 +- hapi-fhir-testpage-overlay/.classpath | 6 +- hapi-fhir-testpage-overlay/pom.xml | 7 +- restful-server-example/.project | 4 +- .../org.eclipse.core.resources.prefs | 3 + .../org.eclipse.wst.common.component | 2 +- 29 files changed, 513 insertions(+), 95 deletions(-) create mode 100644 restful-server-example/.settings/org.eclipse.core.resources.prefs diff --git a/hapi-fhir-base/pom.xml b/hapi-fhir-base/pom.xml index be914510a09..86510343f36 100644 --- a/hapi-fhir-base/pom.xml +++ b/hapi-fhir-base/pom.xml @@ -59,7 +59,9 @@ 2.0.2.RELEASE true - + + org.thymeleaf @@ -253,6 +255,7 @@ ${hamcrest_version} test + diff --git a/hapi-fhir-base/src/changes/changes.xml b/hapi-fhir-base/src/changes/changes.xml index e83e9f8c85f..cdce32954f8 100644 --- a/hapi-fhir-base/src/changes/changes.xml +++ b/hapi-fhir-base/src/changes/changes.xml @@ -36,12 +36,28 @@ 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! IResource interface did not expose the getLanguage/setLanguage methods from BaseResource, so the resource language was difficult to access. + + JSON Parser now gives a more friendly error message if it tries to parse JSON with invalid use + of single quotes + + + 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. + + + 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. + diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/Bundle.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/Bundle.java index 18f9de23fb0..02f8a7e5e58 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/Bundle.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/Bundle.java @@ -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 = "\n" + +// " \n" + +// " \n" + +// " \n" + +// " \n" + +// " \n" + +// "
\n" + +// " \n" + +// " \n" + +// " \n" + +// " \n" + +// " \n" + +// ""; +// +// IGenericClient c = ctx.newRestfulGenericClient("http://fhirtest.uhn.ca/base"); +// c.registerInterceptor(new LoggingInterceptor(true)); +// c.update().resource(txt).withId("1665").execute(); +// } +// public List getEntries() { if (myEntries == null) { myEntries = new ArrayList(); @@ -248,13 +275,14 @@ public class Bundle extends BaseBundle /* implements IElement */{ RuntimeResourceDefinition def = theContext.getResourceDefinition(theResource); + String title = ResourceMetadataKeyEnum.TITLE.get(theResource); + if (title != null) { + entry.getTitle().setValue(title); + } else { + entry.getTitle().setValue(def.getName() + " " + StringUtils.defaultString(theResource.getId().getValue(), "(no ID)")); + } + 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()); - } StringBuilder b = new StringBuilder(); b.append(theServerBase); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/IdDt.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/IdDt.java index 9d645f707d1..dc36af0bc05 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/IdDt.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/IdDt.java @@ -162,15 +162,6 @@ public class IdDt extends BasePrimitive { return ObjectUtils.equals(getResourceType(), theId.getResourceType()) && ObjectUtils.equals(getIdPart(), theId.getIdPart()) && ObjectUtils.equals(getVersionIdPart(), theId.getVersionIdPart()); } - /** - * Returns a reference to this 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; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java index 7da253813aa..6e5d1e2069d 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java @@ -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 Bundle parseBundle(Class 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(); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/MethodOutcome.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/MethodOutcome.java index 5d8991f5f52..ff106e411e5 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/MethodOutcome.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/MethodOutcome.java @@ -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 - * null if none. + * Returns the {@link OperationOutcome} resource to return to the client or null if none. * - * @return This method will return null, unlike many methods in the - * API. + * @return This method will return null, 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 null (which is the default) if none. + * Sets the {@link OperationOutcome} resource to return to the client. Set to null (which is the default) if none. */ public void setOperationOutcome(OperationOutcome theOperationOutcome) { myOperationOutcome = theOperationOutcome; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java index e7c8695189a..e88f3f89527 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java @@ -745,6 +745,9 @@ public class GenericClient extends BaseClient implements IGenericClient { public MethodOutcome invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> 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; } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/IGenericClient.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/IGenericClient.java index 2f9a7971548..1d7f9f8efd6 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/IGenericClient.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/IGenericClient.java @@ -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); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBinding.java index 61fe90a169f..cc8028d595f 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBinding.java @@ -157,12 +157,20 @@ abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding 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 retResources = retVal.getResources(0, retVal.size()); + List 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); } } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java index 98b881768c6..5e18e26483b 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java @@ -1054,7 +1054,9 @@ public class RestfulServer extends HttpServlet { for (IResource next : resourceList) { if (next.getId() == null || next.getId().isEmpty()) { - throw new InternalErrorException("Server method returned resource of type[" + next.getClass().getSimpleName() + "] with no ID specified (IResource#setId(IdDt) must be called)"); + 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)"); + } } } diff --git a/hapi-fhir-base/src/site/example/java/example/RestfulPatientResourceProviderMore.java b/hapi-fhir-base/src/site/example/java/example/RestfulPatientResourceProviderMore.java index 6db2c71a892..6f4c3c1d6f8 100644 --- a/hapi-fhir-base/src/site/example/java/example/RestfulPatientResourceProviderMore.java +++ b/hapi-fhir-base/src/site/example/java/example/RestfulPatientResourceProviderMore.java @@ -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 transaction(@TransactionParam List 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 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 retVal = new ArrayList(theResources); for (IResource next : theResources) { /* * Populate each returned resource with the new ID for that resource, @@ -880,6 +883,12 @@ public List transaction(@TransactionParam List theResource IdDt newId = new IdDt("Patient", "1", "2"); 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; } diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/parser/JsonParserTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/parser/JsonParserTest.java index 738076980ec..b5f7d601f2f 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/parser/JsonParserTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/parser/JsonParserTest.java @@ -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() { diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/TransactionTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/TransactionTest.java index 8ac820c1f0e..bae1c49484e 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/TransactionTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/TransactionTest.java @@ -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,11 +40,20 @@ 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 Server ourServer; - private static FhirContext ourCtx = new FhirContext(); + private static boolean ourReturnOperationOutcome; + private static Server ourServer; + + + + @Before + public void before() { + ourReturnOperationOutcome = false; + } + @Test public void testTransaction() throws Exception { Bundle b = new Bundle(); @@ -94,6 +106,65 @@ public class TransactionTest { assertEquals("http://localhost:" + ourPort + "/Patient/3/_history/93", entry2.getLinkSelf().getValue()); 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 { @@ -125,7 +196,7 @@ public class TransactionTest { ourClient = builder.build(); } - + /** * Created by dsotnikov on 2/25/2014. */ @@ -142,7 +213,17 @@ public class TransactionTest { next.setId(new IdDt("Patient", newId, "9"+Integer.toString(index))); index++; } - return theResources; + + List retVal = theResources; + if (ourReturnOperationOutcome) { + retVal = new ArrayList(); + OperationOutcome oo = new OperationOutcome(); + oo.addIssue().setDetails("AAAAA"); + retVal.add(oo); + retVal.addAll(theResources); + } + + return retVal; } diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/UpdateTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/UpdateTest.java index 64875fd847d..bf0b5450f63 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/UpdateTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/UpdateTest.java @@ -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); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseFhirDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseFhirDao.java index 384e2a80e75..da1cf63d807 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseFhirDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseFhirDao.java @@ -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; @@ -454,10 +455,10 @@ public abstract class BaseFhirDao implements IDao { if (nextValue.getValue().isEmpty()) { continue; } - + if (new UriDt(UCUM_NS).equals(nextValue.getSystem())) { if (isNotBlank(nextValue.getCode().getValue())) { - + Unit unit = Unit.valueOf(nextValue.getCode().getValue()); javax.measure.converter.UnitConverter dayConverter = unit.getConverterTo(NonSI.DAY); double dayValue = dayConverter.convert(nextValue.getValue().getValue().doubleValue()); @@ -465,35 +466,28 @@ public abstract class BaseFhirDao implements IDao { newValue.setSystem(UCUM_NS); newValue.setCode(NonSI.DAY.toString()); newValue.setValue(dayValue); - nextValue=newValue; + nextValue = newValue; /* - @SuppressWarnings("unchecked") - PhysicsUnit> unit = (PhysicsUnit>) UCUMFormat.getCaseInsensitiveInstance().parse(nextValue.getCode().getValue(), null); - if (unit.isCompatible(UCUM.DAY)) { - @SuppressWarnings("unchecked") - PhysicsUnit timeUnit = (PhysicsUnit