Add trasnaction BATCH support to JPA

This commit is contained in:
James Agnew 2015-07-31 09:29:25 -04:00
parent d9d192cc04
commit 5b09a3d2b6
8 changed files with 385 additions and 61 deletions

View File

@ -33,10 +33,9 @@ public class Constants {
public static final String CHARSETNAME_UTF_8 = "UTF-8";
public static final String CT_ATOM_XML = "application/atom+xml";
public static final String CT_FHIR_JSON = "application/json+fhir";
public static final String CTSUFFIX_CHARSET_UTF8 = "; charset=" + CHARSETNAME_UTF_8;
public static final String CT_FHIR_XML = "application/xml+fhir";
public static final String CT_HTML = "text/html";
public static final String CTSUFFIX_CHARSET_UTF8 = "; charset=" + CHARSETNAME_UTF_8;
public static final String CT_HTML_WITH_UTF8 = "text/html" + CTSUFFIX_CHARSET_UTF8;
public static final String CT_JSON = "application/json";
public static final String CT_OCTET_STREAM = "application/octet-stream";
@ -82,8 +81,13 @@ public class Constants {
public static final String HEADER_LAST_MODIFIED_LOWERCASE = HEADER_LAST_MODIFIED.toLowerCase();
public static final String HEADER_LOCATION = "Location";
public static final String HEADER_LOCATION_LC = HEADER_LOCATION.toLowerCase();
public static final String HEADER_PREFER = "Prefer";
public static final String HEADER_PREFER_RETURN = "return";
public static final String HEADER_PREFER_RETURN_MINIMAL = "minimal";
public static final String HEADER_PREFER_RETURN_REPRESENTATION = "representation";
public static final String HEADER_SUFFIX_CT_UTF_8 = "; charset=UTF-8";
public static final String HEADERVALUE_CORS_ALLOW_METHODS_ALL = "GET, POST, PUT, DELETE, OPTIONS";
public static final Map<Integer, String> HTTP_STATUS_NAMES;
public static final String LINK_FHIR_BASE = "fhir-base";
public static final String LINK_FIRST = "first";
public static final String LINK_LAST = "last";
@ -109,8 +113,8 @@ public class Constants {
public static final String PARAM_SORT = "_sort";
public static final String PARAM_SORT_ASC = "_sort:asc";
public static final String PARAM_SORT_DESC = "_sort:desc";
public static final String PARAM_TAGS = "_tags";
public static final String PARAM_TAG = "_tag";
public static final String PARAM_TAGS = "_tags";
public static final String PARAM_VALIDATE = "_validate";
public static final String PARAMQUALIFIER_MISSING = ":missing";
public static final String PARAMQUALIFIER_MISSING_FALSE = "false";
@ -123,7 +127,7 @@ public class Constants {
public static final int STATUS_HTTP_304_NOT_MODIFIED = 304;
public static final int STATUS_HTTP_400_BAD_REQUEST = 400;
public static final int STATUS_HTTP_401_CLIENT_UNAUTHORIZED = 401;
public static final int STATUS_HTTP_403_FORBIDDEN= 403;
public static final int STATUS_HTTP_403_FORBIDDEN = 403;
public static final int STATUS_HTTP_404_NOT_FOUND = 404;
public static final int STATUS_HTTP_405_METHOD_NOT_ALLOWED = 405;
public static final int STATUS_HTTP_409_CONFLICT = 409;
@ -134,10 +138,6 @@ public class Constants {
public static final int STATUS_HTTP_501_NOT_IMPLEMENTED = 501;
public static final String URL_TOKEN_HISTORY = "_history";
public static final String URL_TOKEN_METADATA = "metadata";
public static final String HEADER_PREFER = "Prefer";
public static final String HEADER_PREFER_RETURN = "return";
public static final String HEADER_PREFER_RETURN_MINIMAL = "minimal";
public static final String HEADER_PREFER_RETURN_REPRESENTATION = "representation";
static {
Map<String, EncodingEnum> valToEncoding = new HashMap<String, EncodingEnum>();
@ -161,8 +161,74 @@ public class Constants {
}
FORMAT_VAL_TO_ENCODING = Collections.unmodifiableMap(valToEncoding);
CHARSET_UTF8 = Charset.forName(CHARSETNAME_UTF_8);
HashMap<Integer, String> statusNames = new HashMap<Integer, String>();
statusNames.put(200, "OK");
statusNames.put(201, "Created");
statusNames.put(202, "Accepted");
statusNames.put(203, "Non-Authoritative Information");
statusNames.put(204, "No Content");
statusNames.put(205, "Reset Content");
statusNames.put(206, "Partial Content");
statusNames.put(207, "Multi-Status");
statusNames.put(208, "Already Reported");
statusNames.put(226, "IM Used");
statusNames.put(300, "Multiple Choices");
statusNames.put(301, "Moved Permanently");
statusNames.put(302, "Found");
statusNames.put(302, "Moved Temporarily");
statusNames.put(303, "See Other");
statusNames.put(304, "Not Modified");
statusNames.put(305, "Use Proxy");
statusNames.put(307, "Temporary Redirect");
statusNames.put(308, "Permanent Redirect");
statusNames.put(400, "Bad Request");
statusNames.put(401, "Unauthorized");
statusNames.put(402, "Payment Required");
statusNames.put(403, "Forbidden");
statusNames.put(404, "Not Found");
statusNames.put(405, "Method Not Allowed");
statusNames.put(406, "Not Acceptable");
statusNames.put(407, "Proxy Authentication Required");
statusNames.put(408, "Request Timeout");
statusNames.put(409, "Conflict");
statusNames.put(410, "Gone");
statusNames.put(411, "Length Required");
statusNames.put(412, "Precondition Failed");
statusNames.put(413, "Payload Too Large");
statusNames.put(413, "Request Entity Too Large");
statusNames.put(414, "URI Too Long");
statusNames.put(414, "Request-URI Too Long");
statusNames.put(415, "Unsupported Media Type");
statusNames.put(416, "Requested range not satisfiable");
statusNames.put(417, "Expectation Failed");
statusNames.put(418, "I'm a teapot");
statusNames.put(419, "Insufficient Space On Resource");
statusNames.put(420, "Method Failure");
statusNames.put(421, "Destination Locked");
statusNames.put(422, "Unprocessable Entity");
statusNames.put(423, "Locked");
statusNames.put(424, "Failed Dependency");
statusNames.put(426, "Upgrade Required");
statusNames.put(428, "Precondition Required");
statusNames.put(429, "Too Many Requests");
statusNames.put(431, "Request Header Fields Too Large");
statusNames.put(500, "Internal Server Error");
statusNames.put(501, "Not Implemented");
statusNames.put(502, "Bad Gateway");
statusNames.put(503, "Service Unavailable");
statusNames.put(504, "Gateway Timeout");
statusNames.put(505, "HTTP Version not supported");
statusNames.put(506, "Variant Also Negotiates");
statusNames.put(507, "Insufficient Storage");
statusNames.put(508, "Loop Detected");
statusNames.put(509, "Bandwidth Limit Exceeded");
statusNames.put(510, "Not Extended");
statusNames.put(511, "Network Authentication Required");
HTTP_STATUS_NAMES = Collections.unmodifiableMap(statusNames);
}
}

View File

@ -358,6 +358,11 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
return pids;
}
@Override
public Set<Long> processMatchUrl(String theMatchUrl) {
return processMatchUrl(theMatchUrl, getResourceType());
}
private boolean addPredicateMissingFalseIfPresent(CriteriaBuilder theBuilder, String theParamName, Root<? extends BaseResourceIndexedSearchParam> from, List<Predicate> codePredicates,
IQueryParameterType nextOr) {
boolean missingFalse = false;

View File

@ -65,6 +65,5 @@ public class FhirResourceDaoDstu1<T extends IResource> extends BaseHapiFhirResou
oo.getIssueFirstRep().getDetailsElement().setValue(theMessage);
return oo;
}
}

View File

@ -46,7 +46,7 @@ public class FhirResourceDaoQuestionnaireAnswersDstu2 extends FhirResourceDaoDst
super.validateResourceForStorage(theResource);
QuestionnaireAnswers qa = (QuestionnaireAnswers) theResource;
if (qa.getQuestionnaire().getReference().isEmpty()) {
if (qa == null || qa.getQuestionnaire() == null || qa.getQuestionnaire().getReference() == null || qa.getQuestionnaire().getReference().isEmpty()) {
return;
}

View File

@ -19,7 +19,9 @@ package ca.uhn.fhir.jpa.dao;
* limitations under the License.
* #L%
*/
import static org.apache.commons.lang3.StringUtils.*;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.util.Date;
import java.util.HashMap;
@ -31,8 +33,13 @@ import java.util.Set;
import javax.persistence.TypedQuery;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.entity.TagDefinition;
@ -47,18 +54,24 @@ import ca.uhn.fhir.model.dstu2.resource.OperationOutcome;
import ca.uhn.fhir.model.dstu2.valueset.BundleTypeEnum;
import ca.uhn.fhir.model.dstu2.valueset.HTTPVerbEnum;
import ca.uhn.fhir.model.dstu2.valueset.IssueSeverityEnum;
import ca.uhn.fhir.model.dstu2.valueset.IssueTypeEnum;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.method.MethodUtil;
import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.rest.server.IBundleProvider;
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.util.FhirTerser;
public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle> {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirSystemDaoDstu2.class);
@Autowired
private PlatformTransactionManager myTxManager;
private String extractTransactionUrlOrThrowException(Entry nextEntry, HTTPVerbEnum verb) {
String url = nextEntry.getRequest().getUrl();
if (isBlank(url)) {
@ -67,6 +80,79 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle> {
return url;
}
private Bundle batch(Bundle theRequest) {
ourLog.info("Beginning batch with {} resources", theRequest.getEntry().size());
long start = System.currentTimeMillis();
TransactionTemplate txTemplate = new TransactionTemplate(myTxManager);
txTemplate.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW);
Bundle resp = new Bundle();
resp.setType(BundleTypeEnum.BATCH_RESPONSE);
OperationOutcome ooResp = new OperationOutcome();
resp.addEntry().setResource(ooResp);
/*
* For batch, we handle each entry as a mini-transaction in its own
* database transaction so that if one fails, it doesn't prevent others
*/
for (final Entry nextRequestEntry : theRequest.getEntry()) {
TransactionCallback<Bundle> callback = new TransactionCallback<Bundle>() {
@Override
public Bundle doInTransaction(TransactionStatus theStatus) {
Bundle subRequestBundle = new Bundle();
subRequestBundle.setType(BundleTypeEnum.TRANSACTION);
subRequestBundle.addEntry(nextRequestEntry);
Bundle subResponseBundle = transaction(subRequestBundle, "Batch sub-request");
return subResponseBundle;
}
};
BaseServerResponseException caughtEx;
try {
Bundle nextResponseBundle = txTemplate.execute(callback);
caughtEx = null;
Entry subResponseEntry = nextResponseBundle.getEntry().get(1);
resp.addEntry(subResponseEntry);
/*
* If the individual entry didn't have a resource in its response, bring the
* sub-transaction's OperationOutcome across so the client can see it
*/
if (subResponseEntry.getResource() == null) {
subResponseEntry.setResource(nextResponseBundle.getEntry().get(0).getResource());
}
} catch (BaseServerResponseException e) {
caughtEx = e;
} catch (Throwable t) {
ourLog.error("Failure during BATCH sub transaction processing", t);
caughtEx = new InternalErrorException(t);
}
if (caughtEx != null) {
Entry nextEntry = resp.addEntry();
OperationOutcome oo = new OperationOutcome();
oo.addIssue().setSeverity(IssueSeverityEnum.ERROR).setDetails(caughtEx.getMessage());
nextEntry.setResource(oo);
EntryResponse nextEntryResp = nextEntry.getResponse();
nextEntryResp.setStatus(toStatusString(caughtEx.getStatusCode()));
}
}
long delay = System.currentTimeMillis() - start;
ourLog.info("Batch completed in {}ms", new Object[] { delay });
ooResp.addIssue().setSeverity(IssueSeverityEnum.INFORMATION).setDetails("Batch completed in " + delay + "ms");
return resp;
}
@Override
public MetaDt metaGetOperation() {
@ -148,11 +234,33 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle> {
return retVal;
}
@SuppressWarnings("unchecked")
@Transactional(propagation = Propagation.REQUIRED)
@Override
public Bundle transaction(Bundle theResources) {
ourLog.info("Beginning transaction with {} resources", theResources.getEntry().size());
public Bundle transaction(Bundle theRequest) {
String theActionName = "Transaction";
return transaction(theRequest, theActionName);
}
@SuppressWarnings("unchecked")
private Bundle transaction(Bundle theRequest, String theActionName) {
BundleTypeEnum transactionType = theRequest.getTypeElement().getValueAsEnum();
if (transactionType == BundleTypeEnum.BATCH) {
return batch(theRequest);
}
OperationOutcome statusOperationOutcome = new OperationOutcome();
if (transactionType == null) {
String message = "Transactiion Bundle did not specify valid Bundle.type, assuming " + BundleTypeEnum.TRANSACTION.getCode();
statusOperationOutcome.addIssue().setCode(IssueTypeEnum.INVALID_CONTENT).setSeverity(IssueSeverityEnum.WARNING).setDetails(message);
ourLog.warn(message);
transactionType = BundleTypeEnum.TRANSACTION;
}
if (transactionType != BundleTypeEnum.TRANSACTION) {
throw new InvalidRequestException("Unable to process transaction where incoming Bundle.type = " + transactionType.getCode());
}
ourLog.info("Beginning {} with {} resources", theActionName, theRequest.getEntry().size());
long start = System.currentTimeMillis();
Set<IdDt> allIds = new LinkedHashSet<IdDt>();
@ -160,11 +268,12 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle> {
Map<IdDt, DaoMethodOutcome> idToPersistedOutcome = new HashMap<IdDt, DaoMethodOutcome>();
Bundle response = new Bundle();
OperationOutcome oo = new OperationOutcome();
response.addEntry().setResource(oo);
response.addEntry().setResource(statusOperationOutcome);
for (int i = 0; i < theResources.getEntry().size(); i++) {
Entry nextEntry = theResources.getEntry().get(i);
// TODO: process verbs in the correct order
for (int i = 0; i < theRequest.getEntry().size(); i++) {
Entry nextEntry = theRequest.getEntry().get(i);
IResource res = nextEntry.getResource();
IdDt nextResourceId = null;
if (res != null) {
@ -221,7 +330,7 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle> {
parts.getDao().deleteByUrl(parts.getResourceType() + '?' + parts.getParams());
}
newEntry.getResponse().setStatus(Integer.toString(Constants.STATUS_HTTP_204_NO_CONTENT));
newEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_204_NO_CONTENT));
break;
}
case PUT: {
@ -258,7 +367,7 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle> {
if (isNotBlank(ifNoneMatch)) {
ifNoneMatch = MethodUtil.parseETagValue(ifNoneMatch);
}
if (parts.getResourceId() != null && parts.getParams() == null) {
IResource found;
boolean notChanged = false;
@ -281,9 +390,9 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle> {
resp.setLocation(found.getId().toUnqualified().getValue());
resp.setEtag(found.getId().getVersionIdPart());
if (!notChanged) {
resp.setStatus(Integer.toString(Constants.STATUS_HTTP_200_OK));
resp.setStatus(toStatusString(Constants.STATUS_HTTP_200_OK));
} else {
resp.setStatus(Integer.toString(Constants.STATUS_HTTP_304_NOT_MODIFIED));
resp.setStatus(toStatusString(Constants.STATUS_HTTP_304_NOT_MODIFIED));
}
} else if (parts.getParams() != null) {
RuntimeResourceDefinition def = getContext().getResourceDefinition(parts.getDao().getResourceType());
@ -295,7 +404,7 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle> {
int configuredMax = 100; // this should probably be configurable or something
if (bundle.size() > configuredMax) {
oo.addIssue().setSeverity(IssueSeverityEnum.WARNING)
statusOperationOutcome.addIssue().setSeverity(IssueSeverityEnum.WARNING)
.setDetails("Search nested within transaction found more than " + configuredMax + " matches, but paging is not supported in nested transactions");
}
List<IBaseResource> resourcesToAdd = bundle.getResources(0, Math.min(bundle.size(), configuredMax));
@ -305,7 +414,7 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle> {
Entry newEntry = response.addEntry();
newEntry.setResource(searchBundle);
newEntry.getResponse().setStatus(Integer.toString(Constants.STATUS_HTTP_200_OK));
newEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_200_OK));
}
}
}
@ -314,6 +423,10 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle> {
FhirTerser terser = getContext().newTerser();
/*
* Perform ID substitutions and then index each resource we have saved
*/
for (DaoMethodOutcome nextOutcome : idToPersistedOutcome.values()) {
IResource nextResource = (IResource) nextOutcome.getResource();
if (nextResource == null) {
@ -337,10 +450,29 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle> {
updateEntity(nextResource, nextOutcome.getEntity(), false, deletedTimestampOrNull, true, false);
}
long delay = System.currentTimeMillis() - start;
ourLog.info("Transaction completed in {}ms", new Object[] { delay });
myEntityManager.flush();
oo.addIssue().setSeverity(IssueSeverityEnum.INFORMATION).setDetails("Transaction completed in " + delay + "ms");
/*
* Double check we didn't allow any duplicates we shouldn't have
*/
for (Entry nextEntry : theRequest.getEntry()) {
if (nextEntry.getRequest().getMethodElement().getValueAsEnum() == HTTPVerbEnum.POST) {
String matchUrl = nextEntry.getRequest().getIfNoneExist();
if (isNotBlank(matchUrl)) {
IFhirResourceDao<?> resourceDao = getDao(nextEntry.getResource().getClass());
Set<Long> val = resourceDao.processMatchUrl(matchUrl);
if (val.size() > 1) {
throw new InvalidRequestException(
"Unable to process " + theActionName + " - Request would cause multiple resources to match URL: \"" + matchUrl + "\". Does transaction request contain duplicates?");
}
}
}
}
long delay = System.currentTimeMillis() - start;
ourLog.info(theActionName + " completed in {}ms", new Object[] { delay });
statusOperationOutcome.addIssue().setSeverity(IssueSeverityEnum.INFORMATION).setDetails(theActionName + " completed in " + delay + "ms");
for (IdDt next : allIds) {
IdDt replacement = idSubstitutions.get(next);
@ -350,7 +482,7 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle> {
if (replacement.equals(next)) {
continue;
}
oo.addIssue().setSeverity(IssueSeverityEnum.INFORMATION).setDetails("Placeholder resource ID \"" + next + "\" was replaced with permanent ID \"" + replacement + "\"");
statusOperationOutcome.addIssue().setSeverity(IssueSeverityEnum.INFORMATION).setDetails("Placeholder resource ID \"" + next + "\" was replaced with permanent ID \"" + replacement + "\"");
}
notifyWriteCompleted();
@ -374,9 +506,9 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle> {
}
idToPersistedOutcome.put(newId, outcome);
if (outcome.getCreated().booleanValue()) {
newEntry.getResponse().setStatus(Long.toString(Constants.STATUS_HTTP_201_CREATED));
newEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_201_CREATED));
} else {
newEntry.getResponse().setStatus(Long.toString(Constants.STATUS_HTTP_200_OK));
newEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_200_OK));
}
newEntry.getResponse().setLocation(outcome.getId().toUnqualified().getValue());
newEntry.getResponse().setEtag(outcome.getId().getVersionIdPart());
@ -389,6 +521,10 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle> {
return false;
}
private static String toStatusString(int theStatusCode) {
return Integer.toString(theStatusCode) + " " + defaultString(Constants.HTTP_STATUS_NAMES.get(theStatusCode));
}
private static class UrlParts {
private IFhirResourceDao<? extends IBaseResource> myDao;
private String myParams;

View File

@ -90,6 +90,8 @@ public interface IFhirResourceDao<T extends IBaseResource> extends IDao {
*/
MetaDt metaGetOperation(IIdType theId);
Set<Long> processMatchUrl(String theMatchUrl);
/**
*
* @param theId

View File

@ -29,13 +29,16 @@ import ca.uhn.fhir.model.dstu2.composite.MetaDt;
import ca.uhn.fhir.model.dstu2.composite.ResourceReferenceDt;
import ca.uhn.fhir.model.dstu2.resource.Bundle;
import ca.uhn.fhir.model.dstu2.resource.Bundle.Entry;
import ca.uhn.fhir.model.dstu2.resource.Bundle.EntryResponse;
import ca.uhn.fhir.model.dstu2.resource.Observation;
import ca.uhn.fhir.model.dstu2.resource.OperationOutcome;
import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.dstu2.valueset.BundleTypeEnum;
import ca.uhn.fhir.model.dstu2.valueset.HTTPVerbEnum;
import ca.uhn.fhir.model.dstu2.valueset.IssueSeverityEnum;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.UriDt;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.rest.server.IBundleProvider;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
@ -172,12 +175,12 @@ public class FhirSystemDaoDstu2Test extends BaseJpaTest {
assertEquals(3, resp.getEntry().size());
Entry respEntry = resp.getEntry().get(1);
assertEquals(Constants.STATUS_HTTP_200_OK + "", respEntry.getResponse().getStatus());
assertEquals(Constants.STATUS_HTTP_200_OK + " OK", respEntry.getResponse().getStatus());
assertThat(respEntry.getResponse().getLocation(), endsWith("Patient/" + id.getIdPart() + "/_history/1"));
assertEquals("1", respEntry.getResponse().getEtag());
respEntry = resp.getEntry().get(2);
assertEquals(Constants.STATUS_HTTP_201_CREATED + "", respEntry.getResponse().getStatus());
assertEquals(Constants.STATUS_HTTP_201_CREATED + " Created", respEntry.getResponse().getStatus());
assertThat(respEntry.getResponse().getLocation(), containsString("Observation/"));
assertThat(respEntry.getResponse().getLocation(), endsWith("/_history/1"));
assertEquals("1", respEntry.getResponse().getEtag());
@ -188,6 +191,116 @@ public class FhirSystemDaoDstu2Test extends BaseJpaTest {
}
@Test
public void testTransactionWithInvalidType() {
Bundle request = new Bundle();
request.setType(BundleTypeEnum.SEARCH_RESULTS);
Patient p = new Patient();
request.addEntry().setResource(p).getRequest().setMethod(HTTPVerbEnum.POST);
try {
ourSystemDao.transaction(request);
fail();
} catch (InvalidRequestException e) {
assertEquals("Unable to process transaction where incoming Bundle.type = searchset", e.getMessage());
}
}
@Test
public void testTransactionBatchWithFailingRead() {
String methodName = "testTransactionBatchWithFailingRead";
Bundle request = new Bundle();
request.setType(BundleTypeEnum.BATCH);
Patient p = new Patient();
p.addName().addFamily(methodName);
request.addEntry().setResource(p).getRequest().setMethod(HTTPVerbEnum.POST);
request.addEntry().getRequest().setMethod(HTTPVerbEnum.GET).setUrl("Patient/THIS_ID_DOESNT_EXIST");
Bundle resp = ourSystemDao.transaction(request);
assertEquals(3, resp.getEntry().size());
ourLog.info(ourFhirContext.newXmlParser().setPrettyPrint(true).encodeResourceToString(resp));
EntryResponse respEntry;
// Bundle.entry[0] is operation outcome
assertEquals(OperationOutcome.class, resp.getEntry().get(0).getResource().getClass());
assertEquals(IssueSeverityEnum.INFORMATION, ((OperationOutcome)resp.getEntry().get(0).getResource()).getIssue().get(0).getSeverityElement().getValueAsEnum());
assertThat(((OperationOutcome)resp.getEntry().get(0).getResource()).getIssue().get(0).getDetails(), startsWith("Batch completed in "));
// Bundle.entry[1] is create response
assertEquals(OperationOutcome.class, resp.getEntry().get(1).getResource().getClass());
assertEquals(IssueSeverityEnum.INFORMATION, ((OperationOutcome)resp.getEntry().get(1).getResource()).getIssue().get(0).getSeverityElement().getValueAsEnum());
assertThat(((OperationOutcome)resp.getEntry().get(1).getResource()).getIssue().get(0).getDetails(), startsWith("Batch sub-request completed in"));
assertEquals("201 Created", resp.getEntry().get(1).getResponse().getStatus());
assertThat(resp.getEntry().get(1).getResponse().getLocation(), startsWith("Patient/"));
// Bundle.entry[2] is failed read response
assertEquals(OperationOutcome.class, resp.getEntry().get(2).getResource().getClass());
assertEquals(IssueSeverityEnum.ERROR, ((OperationOutcome)resp.getEntry().get(2).getResource()).getIssue().get(0).getSeverityElement().getValueAsEnum());
assertEquals("Resource Patient/THIS_ID_DOESNT_EXIST is not known", ((OperationOutcome)resp.getEntry().get(2).getResource()).getIssue().get(0).getDetails());
assertEquals("404 Not Found", resp.getEntry().get(2).getResponse().getStatus());
// Check POST
respEntry = resp.getEntry().get(1).getResponse();
assertThat(respEntry.getStatus(), startsWith("201"));
IdDt createdId = new IdDt(respEntry.getLocation());
assertEquals("Patient", createdId.getResourceType());
ourPatientDao.read(createdId); // shouldn't fail
// Check GET
respEntry = resp.getEntry().get(2).getResponse();
assertThat(respEntry.getStatus(), startsWith("404"));
}
@Test
public void testTransactionCreateWithDuplicateMatchUrl01() {
String methodName = "testTransactionCreateWithDuplicateMatchUrl01";
Bundle request = new Bundle();
Patient p;
p = new Patient();
p.addIdentifier().setSystem("urn:system").setValue(methodName);
request.addEntry().setResource(p).getRequest().setMethod(HTTPVerbEnum.POST).setIfNoneExist("Patient?identifier=urn%3Asystem%7C" + methodName);
p = new Patient();
p.addIdentifier().setSystem("urn:system").setValue(methodName);
request.addEntry().setResource(p).getRequest().setMethod(HTTPVerbEnum.POST).setIfNoneExist("Patient?identifier=urn%3Asystem%7C" + methodName);
try {
ourSystemDao.transaction(request);
fail();
} catch (InvalidRequestException e) {
assertEquals(e.getMessage(),
"Unable to process Transaction - Request would cause multiple resources to match URL: \"Patient?identifier=urn%3Asystem%7CtestTransactionCreateWithDuplicateMatchUrl01\". Does transaction request contain duplicates?");
}
}
public void testTransactionCreateWithDuplicateMatchUrl02() {
String methodName = "testTransactionCreateWithDuplicateMatchUrl02";
Bundle request = new Bundle();
Patient p;
p = new Patient();
p.addIdentifier().setSystem("urn:system").setValue(methodName);
request.addEntry().setResource(p).getRequest().setMethod(HTTPVerbEnum.POST).setIfNoneExist("Patient?identifier=urn%3Asystem%7C" + methodName);
p = new Patient();
p.addIdentifier().setSystem("urn:system").setValue(methodName);
request.addEntry().setResource(p).getRequest().setMethod(HTTPVerbEnum.POST);
try {
ourSystemDao.transaction(request);
fail();
} catch (InvalidRequestException e) {
assertEquals(e.getMessage(),
"Unable to process Transaction - Request would cause multiple resources to match URL: \"Patient?identifier=urn%3Asystem%7CtestTransactionCreateWithDuplicateMatchUrl02\". Does transaction request contain duplicates?");
}
}
@Test
public void testTransactionCreateMatchUrlWithTwoMatch() {
String methodName = "testTransactionCreateMatchUrlWithTwoMatch";
@ -239,10 +352,11 @@ public class FhirSystemDaoDstu2Test extends BaseJpaTest {
request.addEntry().setResource(o).getRequest().setMethod(HTTPVerbEnum.POST);
Bundle resp = ourSystemDao.transaction(request);
assertEquals(BundleTypeEnum.TRANSACTION_RESPONSE, resp.getTypeElement().getValueAsEnum());
assertEquals(3, resp.getEntry().size());
Entry respEntry = resp.getEntry().get(1);
assertEquals(Constants.STATUS_HTTP_201_CREATED + "", respEntry.getResponse().getStatus());
assertEquals(Constants.STATUS_HTTP_201_CREATED + " Created", respEntry.getResponse().getStatus());
String patientId = respEntry.getResponse().getLocation();
assertThat(patientId, not(endsWith("Patient/" + methodName + "/_history/1")));
assertThat(patientId, (endsWith("/_history/1")));
@ -250,7 +364,7 @@ public class FhirSystemDaoDstu2Test extends BaseJpaTest {
assertEquals("1", respEntry.getResponse().getEtag());
respEntry = resp.getEntry().get(2);
assertEquals(Constants.STATUS_HTTP_201_CREATED + "", respEntry.getResponse().getStatus());
assertEquals(Constants.STATUS_HTTP_201_CREATED + " Created", respEntry.getResponse().getStatus());
assertThat(respEntry.getResponse().getLocation(), containsString("Observation/"));
assertThat(respEntry.getResponse().getLocation(), endsWith("/_history/1"));
assertEquals("1", respEntry.getResponse().getEtag());
@ -273,7 +387,7 @@ public class FhirSystemDaoDstu2Test extends BaseJpaTest {
assertEquals(2, resp.getEntry().size());
Entry respEntry = resp.getEntry().get(1);
assertEquals(Constants.STATUS_HTTP_201_CREATED + "", respEntry.getResponse().getStatus());
assertEquals(Constants.STATUS_HTTP_201_CREATED + " Created", respEntry.getResponse().getStatus());
String patientId = respEntry.getResponse().getLocation();
assertThat(patientId, not(containsString("test")));
}
@ -342,8 +456,8 @@ public class FhirSystemDaoDstu2Test extends BaseJpaTest {
Bundle resp = ourSystemDao.transaction(request);
assertEquals(3, resp.getEntry().size());
assertEquals("204", resp.getEntry().get(1).getResponse().getStatus());
assertEquals("204", resp.getEntry().get(2).getResponse().getStatus());
assertEquals("204 No Content", resp.getEntry().get(1).getResponse().getStatus());
assertEquals("204 No Content", resp.getEntry().get(2).getResponse().getStatus());
try {
ourPatientDao.read(id1.toVersionless());
@ -361,7 +475,6 @@ public class FhirSystemDaoDstu2Test extends BaseJpaTest {
}
@Test
public void testTransactionDeleteMatchUrlWithOneMatch() {
String methodName = "testTransactionDeleteMatchUrlWithOneMatch";
@ -378,7 +491,7 @@ public class FhirSystemDaoDstu2Test extends BaseJpaTest {
assertEquals(2, resp.getEntry().size());
Entry nextEntry = resp.getEntry().get(1);
assertEquals(Constants.STATUS_HTTP_204_NO_CONTENT + "", nextEntry.getResponse().getStatus());
assertEquals(Constants.STATUS_HTTP_204_NO_CONTENT + " No Content", nextEntry.getResponse().getStatus());
try {
ourPatientDao.read(id.toVersionless());
@ -464,7 +577,7 @@ public class FhirSystemDaoDstu2Test extends BaseJpaTest {
Bundle res = ourSystemDao.transaction(request);
assertEquals(2, res.getEntry().size());
assertEquals(Constants.STATUS_HTTP_204_NO_CONTENT + "", res.getEntry().get(1).getResponse().getStatus());
assertEquals(Constants.STATUS_HTTP_204_NO_CONTENT + " No Content", res.getEntry().get(1).getResponse().getStatus());
try {
ourPatientDao.read(id.toVersionless());
@ -584,17 +697,17 @@ public class FhirSystemDaoDstu2Test extends BaseJpaTest {
assertThat(respBundle.getTotal().intValue(), greaterThan(0));
// Invalid _count
request = new Bundle();
request.addEntry().getRequest().setMethod(HTTPVerbEnum.GET).setUrl("Patient?" + Constants.PARAM_COUNT + "=GKJGKJG");
try {
ourSystemDao.transaction(request);
ourSystemDao.transaction(request);
} catch (InvalidRequestException e) {
assertEquals(e.getMessage(), ("Invalid _count value: GKJGKJG"));
}
// Empty _count
request = new Bundle();
request.addEntry().getRequest().setMethod(HTTPVerbEnum.GET).setUrl("Patient?" + Constants.PARAM_COUNT + "=");
respBundle = ourSystemDao.transaction(request);
@ -622,7 +735,7 @@ public class FhirSystemDaoDstu2Test extends BaseJpaTest {
request.addEntry().getRequest().setMethod(HTTPVerbEnum.GET).setUrl(idv1.toUnqualifiedVersionless().getValue());
request.addEntry().getRequest().setMethod(HTTPVerbEnum.GET).setUrl(idv1.toUnqualifiedVersionless().getValue()).setIfNoneMatch("W/\"" + idv1.getVersionIdPart() + "\"");
request.addEntry().getRequest().setMethod(HTTPVerbEnum.GET).setUrl(idv1.toUnqualifiedVersionless().getValue()).setIfNoneMatch("W/\"" + idv2.getVersionIdPart() + "\"");
Bundle resp = ourSystemDao.transaction(request);
assertEquals(4, resp.getEntry().size());
@ -633,17 +746,17 @@ public class FhirSystemDaoDstu2Test extends BaseJpaTest {
assertNotNull(nextEntry.getResource());
assertEquals(Patient.class, nextEntry.getResource().getClass());
assertEquals(idv2.toUnqualified(), nextEntry.getResource().getId().toUnqualified());
assertEquals("200", nextEntry.getResponse().getStatus());
assertEquals("200 OK", nextEntry.getResponse().getStatus());
nextEntry = resp.getEntry().get(2);
assertNotNull(nextEntry.getResource());
assertEquals(Patient.class, nextEntry.getResource().getClass());
assertEquals(idv2.toUnqualified(), nextEntry.getResource().getId().toUnqualified());
assertEquals("200", nextEntry.getResponse().getStatus());
assertEquals("200 OK", nextEntry.getResponse().getStatus());
nextEntry = resp.getEntry().get(3);
assertNull(nextEntry.getResource());
assertEquals("304", nextEntry.getResponse().getStatus());
assertEquals("304 Not Modified", nextEntry.getResponse().getStatus());
}
@Test
@ -671,14 +784,14 @@ public class FhirSystemDaoDstu2Test extends BaseJpaTest {
assertEquals(3, resp.getEntry().size());
Entry nextEntry = resp.getEntry().get(1);
assertEquals("200", nextEntry.getResponse().getStatus());
assertEquals("200 OK", nextEntry.getResponse().getStatus());
assertThat(nextEntry.getResponse().getLocation(), not(containsString("test")));
assertEquals(id.toVersionless(), p.getId().toVersionless());
assertNotEquals(id, p.getId());
assertThat(p.getId().toString(), endsWith("/_history/2"));
nextEntry = resp.getEntry().get(1);
assertEquals("" + Constants.STATUS_HTTP_200_OK, nextEntry.getResponse().getStatus());
assertEquals(Constants.STATUS_HTTP_200_OK + " OK", nextEntry.getResponse().getStatus());
assertThat(nextEntry.getResponse().getLocation(), not(emptyString()));
nextEntry = resp.getEntry().get(2);
@ -745,7 +858,7 @@ public class FhirSystemDaoDstu2Test extends BaseJpaTest {
assertEquals(3, resp.getEntry().size());
Entry nextEntry = resp.getEntry().get(1);
assertEquals(Constants.STATUS_HTTP_201_CREATED + "", nextEntry.getResponse().getStatus());
assertEquals(Constants.STATUS_HTTP_201_CREATED + " Created", nextEntry.getResponse().getStatus());
IdDt patientId = new IdDt(nextEntry.getResponse().getLocation());
assertThat(nextEntry.getResponse().getLocation(), not(containsString("test")));
@ -785,7 +898,7 @@ public class FhirSystemDaoDstu2Test extends BaseJpaTest {
assertEquals(3, resp.getEntry().size());
Entry nextEntry = resp.getEntry().get(1);
assertEquals("200", nextEntry.getResponse().getStatus());
assertEquals("200 OK", nextEntry.getResponse().getStatus());
assertThat(nextEntry.getResponse().getLocation(), (containsString("test")));
assertEquals(id.toVersionless(), new IdDt(nextEntry.getResponse().getLocation()).toVersionless());
@ -793,7 +906,7 @@ public class FhirSystemDaoDstu2Test extends BaseJpaTest {
assertThat(nextEntry.getResponse().getLocation(), endsWith("/_history/2"));
nextEntry = resp.getEntry().get(2);
assertEquals("" + Constants.STATUS_HTTP_201_CREATED, nextEntry.getResponse().getStatus());
assertEquals(Constants.STATUS_HTTP_201_CREATED + " Created", nextEntry.getResponse().getStatus());
o = ourObservationDao.read(new IdDt(resp.getEntry().get(2).getResponse().getLocation()));
assertEquals(id.toVersionless(), o.getSubject().getReference());
@ -925,17 +1038,17 @@ public class FhirSystemDaoDstu2Test extends BaseJpaTest {
res.addEntry().setResource(o2).getRequest().setMethod(HTTPVerbEnum.POST).setUrl("Observation");
Bundle resp = ourSystemDao.transaction(res);
ourLog.info(ourFhirContext.newXmlParser().setPrettyPrint(true).encodeResourceToString(resp));
assertEquals(BundleTypeEnum.TRANSACTION_RESPONSE, resp.getTypeElement().getValueAsEnum());
assertEquals(4, resp.getEntry().size());
assertEquals(OperationOutcome.class, resp.getEntry().get(0).getResource().getClass());
OperationOutcome outcome = (OperationOutcome) resp.getEntry().get(0).getResource();
assertThat(outcome.getIssue().get(1).getDetails(), containsString("Placeholder resource ID \"urn:oid:0.1.2.3\" was replaced with permanent ID \"Patient/"));
assertTrue(resp.getEntry().get(1).getResponse().getLocation(), new IdDt(resp.getEntry().get(1).getResponse().getLocation()).getIdPart().matches("^[0-9]+$"));
assertTrue(resp.getEntry().get(2).getResponse().getLocation(), new IdDt(resp.getEntry().get(2).getResponse().getLocation()).getIdPart().matches("^[0-9]+$"));
assertTrue(resp.getEntry().get(3).getResponse().getLocation(), new IdDt(resp.getEntry().get(3).getResponse().getLocation()).getIdPart().matches("^[0-9]+$"));
@ -973,17 +1086,17 @@ public class FhirSystemDaoDstu2Test extends BaseJpaTest {
res.addEntry().setResource(o2).getRequest().setMethod(HTTPVerbEnum.POST).setUrl("Observation");
Bundle resp = ourSystemDao.transaction(res);
ourLog.info(ourFhirContext.newXmlParser().setPrettyPrint(true).encodeResourceToString(resp));
assertEquals(BundleTypeEnum.TRANSACTION_RESPONSE, resp.getTypeElement().getValueAsEnum());
assertEquals(4, resp.getEntry().size());
assertEquals(OperationOutcome.class, resp.getEntry().get(0).getResource().getClass());
OperationOutcome outcome = (OperationOutcome) resp.getEntry().get(0).getResource();
assertThat(outcome.getIssue().get(1).getDetails(), containsString("Placeholder resource ID \"urn:oid:0.1.2.3\" was replaced with permanent ID \"Patient/"));
assertTrue(resp.getEntry().get(1).getResponse().getLocation(), new IdDt(resp.getEntry().get(1).getResponse().getLocation()).getIdPart().matches("^[0-9]+$"));
assertTrue(resp.getEntry().get(2).getResponse().getLocation(), new IdDt(resp.getEntry().get(2).getResponse().getLocation()).getIdPart().matches("^[0-9]+$"));
assertTrue(resp.getEntry().get(3).getResponse().getLocation(), new IdDt(resp.getEntry().get(3).getResponse().getLocation()).getIdPart().matches("^[0-9]+$"));

View File

@ -67,6 +67,9 @@
<action type="add">
JPA server and generic client now both support the _tag search parameter
</action>
<action type="add">
Add support for BATCH mode to JPA server transaction operation
</action>
</release>
<release version="1.1" date="2015-07-13">
<action type="add">