JPA server is now able to handle placeholder IDs (e.g. urn:uuid:00....000) being used in Bundle.entry.request.url as a part of the conditional URL within transactions.

This commit is contained in:
James 2017-07-14 05:52:33 -04:00
parent 2e60ff7521
commit b13333c3c0
4 changed files with 522 additions and 262 deletions

View File

@ -63,6 +63,7 @@ import ca.uhn.fhir.util.UrlUtil;
import ca.uhn.fhir.util.UrlUtil.UrlParts; import ca.uhn.fhir.util.UrlUtil.UrlParts;
public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> { public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirSystemDaoDstu3.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirSystemDaoDstu3.class);
@Autowired @Autowired
@ -136,115 +137,6 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
return resp; return resp;
} }
private String extractTransactionUrlOrThrowException(BundleEntryComponent nextEntry, HTTPVerb verb) {
String url = nextEntry.getRequest().getUrl();
if (isBlank(url)) {
throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionMissingUrl", verb.name()));
}
return url;
}
/**
* This method is called for nested bundles (e.g. if we received a transaction with an entry that
* was a GET search, this method is called on the bundle for the search result, that will be placed in the
* outer bundle). This method applies the _summary and _content parameters to the output of
* that bundle.
*
* TODO: This isn't the most efficient way of doing this.. hopefully we can come up with something better in the future.
*/
private IBaseResource filterNestedBundle(RequestDetails theRequestDetails, IBaseResource theResource) {
IParser p = getContext().newJsonParser();
RestfulServerUtils.configureResponseParser(theRequestDetails, p);
return p.parseResource(theResource.getClass(), p.encodeResourceToString(theResource));
}
private IFhirResourceDao<?> getDaoOrThrowException(Class<? extends IBaseResource> theClass) {
IFhirResourceDao<? extends IBaseResource> retVal = getDao(theClass);
if (retVal == null) {
throw new InvalidRequestException("Unable to process request, this server does not know how to handle resources of type " + getContext().getResourceDefinition(theClass).getName());
}
return retVal;
}
@Override
public Meta metaGetOperation(RequestDetails theRequestDetails) {
// Notify interceptors
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails);
notifyInterceptors(RestOperationTypeEnum.META, requestDetails);
String sql = "SELECT d FROM TagDefinition d WHERE d.myId IN (SELECT DISTINCT t.myTagId FROM ResourceTag t)";
TypedQuery<TagDefinition> q = myEntityManager.createQuery(sql, TagDefinition.class);
List<TagDefinition> tagDefinitions = q.getResultList();
Meta retVal = toMeta(tagDefinitions);
return retVal;
}
protected Meta toMeta(Collection<TagDefinition> tagDefinitions) {
Meta retVal = new Meta();
for (TagDefinition next : tagDefinitions) {
switch (next.getTagType()) {
case PROFILE:
retVal.addProfile(next.getCode());
break;
case SECURITY_LABEL:
retVal.addSecurity().setSystem(next.getSystem()).setCode(next.getCode()).setDisplay(next.getDisplay());
break;
case TAG:
retVal.addTag().setSystem(next.getSystem()).setCode(next.getCode()).setDisplay(next.getDisplay());
break;
}
}
return retVal;
}
private ca.uhn.fhir.jpa.dao.IFhirResourceDao<? extends IBaseResource> toDao(UrlParts theParts, String theVerb, String theUrl) {
RuntimeResourceDefinition resType;
try {
resType = getContext().getResourceDefinition(theParts.getResourceType());
} catch (DataFormatException e) {
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl);
throw new InvalidRequestException(msg);
}
IFhirResourceDao<? extends IBaseResource> dao = null;
if (resType != null) {
dao = getDao(resType.getImplementingClass());
}
if (dao == null) {
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl);
throw new InvalidRequestException(msg);
}
// if (theParts.getResourceId() == null && theParts.getParams() == null) {
// String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl);
// throw new InvalidRequestException(msg);
// }
return dao;
}
@Transactional(propagation = Propagation.REQUIRED)
@Override
public Bundle transaction(RequestDetails theRequestDetails, Bundle theRequest) {
if (theRequestDetails != null) {
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, theRequest, "Bundle", null);
notifyInterceptors(RestOperationTypeEnum.TRANSACTION, requestDetails);
}
String actionName = "Transaction";
return transaction((ServletRequestDetails) theRequestDetails, theRequest, actionName);
}
private Bundle transaction(ServletRequestDetails theRequestDetails, Bundle theRequest, String theActionName) {
super.markRequestAsProcessingSubRequest(theRequestDetails);
try {
return doTransaction(theRequestDetails, theRequest, theActionName);
} finally {
super.clearRequestAsProcessingSubRequest(theRequestDetails);
}
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private Bundle doTransaction(ServletRequestDetails theRequestDetails, Bundle theRequest, String theActionName) { private Bundle doTransaction(ServletRequestDetails theRequestDetails, Bundle theRequest, String theActionName) {
BundleType transactionType = theRequest.getTypeElement().getValue(); BundleType transactionType = theRequest.getTypeElement().getValue();
@ -299,7 +191,6 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
getEntries.add(theRequest.getEntry().get(i)); getEntries.add(theRequest.getEntry().get(i));
} }
} }
Collections.sort(theRequest.getEntry(), new TransactionSorter());
Set<String> deletedResources = new HashSet<String>(); Set<String> deletedResources = new HashSet<String>();
List<DeleteConflict> deleteConflicts = new ArrayList<DeleteConflict>(); List<DeleteConflict> deleteConflicts = new ArrayList<DeleteConflict>();
@ -307,17 +198,32 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
Set<ResourceTable> nonUpdatedEntities = new HashSet<ResourceTable>(); Set<ResourceTable> nonUpdatedEntities = new HashSet<ResourceTable>();
Map<String, Class<? extends IBaseResource>> conditionalRequestUrls = new HashMap<String, Class<? extends IBaseResource>>(); Map<String, Class<? extends IBaseResource>> conditionalRequestUrls = new HashMap<String, Class<? extends IBaseResource>>();
List<BundleEntryComponent> entries = new ArrayList<BundleEntryComponent>(theRequest.getEntry());
/*
* See FhirSystemDaoDstu3Test#testTransactionWithPlaceholderIdInMatchUrl
* Basically if the resource has a match URL that references a placeholder,
* we try to handle the resource with the placeholder first.
*/
Set<String> placeholderIds = new HashSet<String>();
for (BundleEntryComponent nextEntry : entries) {
if (isNotBlank(nextEntry.getFullUrl()) && nextEntry.getFullUrl().startsWith(IdType.URN_PREFIX)) {
placeholderIds.add(nextEntry.getFullUrl());
}
}
Collections.sort(entries, new TransactionSorter(placeholderIds));
/* /*
* Loop through the request and process any entries of type * Loop through the request and process any entries of type
* PUT, POST or DELETE * PUT, POST or DELETE
*/ */
for (int i = 0; i < theRequest.getEntry().size(); i++) { for (int i = 0; i < entries.size(); i++) {
if (i % 100 == 0) { if (i % 100 == 0) {
ourLog.info("Processed {} non-GET entries out of {}", i, theRequest.getEntry().size()); ourLog.info("Processed {} non-GET entries out of {}", i, entries.size());
} }
BundleEntryComponent nextReqEntry = theRequest.getEntry().get(i); BundleEntryComponent nextReqEntry = entries.get(i);
Resource res = nextReqEntry.getResource(); Resource res = nextReqEntry.getResource();
IdType nextResourceId = null; IdType nextResourceId = null;
if (res != null) { if (res != null) {
@ -368,6 +274,7 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
res.setId((String) null); res.setId((String) null);
DaoMethodOutcome outcome; DaoMethodOutcome outcome;
String matchUrl = nextReqEntry.getRequest().getIfNoneExist(); String matchUrl = nextReqEntry.getRequest().getIfNoneExist();
matchUrl = performIdSubstitutionsInMatchUrl(idSubstitutions, matchUrl);
outcome = resourceDao.create(res, matchUrl, false, theRequestDetails); outcome = resourceDao.create(res, matchUrl, false, theRequestDetails);
if (nextResourceId != null) { if (nextResourceId != null) {
handleTransactionCreateOrUpdateOutcome(idSubstitutions, idToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res, theRequestDetails); handleTransactionCreateOrUpdateOutcome(idSubstitutions, idToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res, theRequestDetails);
@ -399,7 +306,9 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
} }
} }
} else { } else {
DeleteMethodOutcome deleteOutcome = dao.deleteByUrl(parts.getResourceType() + '?' + parts.getParams(), deleteConflicts, theRequestDetails); String matchUrl = parts.getResourceType() + '?' + parts.getParams();
matchUrl = performIdSubstitutionsInMatchUrl(idSubstitutions, matchUrl);
DeleteMethodOutcome deleteOutcome = dao.deleteByUrl(matchUrl, deleteConflicts, theRequestDetails);
List<ResourceTable> allDeleted = deleteOutcome.getDeletedEntities(); List<ResourceTable> allDeleted = deleteOutcome.getDeletedEntities();
for (ResourceTable deleted : allDeleted) { for (ResourceTable deleted : allDeleted) {
deletedResources.add(deleted.getIdDt().toUnqualifiedVersionless().getValueAsString()); deletedResources.add(deleted.getIdDt().toUnqualifiedVersionless().getValueAsString());
@ -430,6 +339,7 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
} else { } else {
res.setId((String) null); res.setId((String) null);
String matchUrl = parts.getResourceType() + '?' + parts.getParams(); String matchUrl = parts.getResourceType() + '?' + parts.getParams();
matchUrl = performIdSubstitutionsInMatchUrl(idSubstitutions, matchUrl);
outcome = resourceDao.update(res, matchUrl, false, theRequestDetails); outcome = resourceDao.update(res, matchUrl, false, theRequestDetails);
if (Boolean.TRUE.equals(outcome.getCreated())) { if (Boolean.TRUE.equals(outcome.getCreated())) {
conditionalRequestUrls.put(matchUrl, res.getClass()); conditionalRequestUrls.put(matchUrl, res.getClass());
@ -607,6 +517,131 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
return response; return response;
} }
private String extractTransactionUrlOrThrowException(BundleEntryComponent nextEntry, HTTPVerb verb) {
String url = nextEntry.getRequest().getUrl();
if (isBlank(url)) {
throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionMissingUrl", verb.name()));
}
return url;
}
/**
* This method is called for nested bundles (e.g. if we received a transaction with an entry that
* was a GET search, this method is called on the bundle for the search result, that will be placed in the
* outer bundle). This method applies the _summary and _content parameters to the output of
* that bundle.
*
* TODO: This isn't the most efficient way of doing this.. hopefully we can come up with something better in the future.
*/
private IBaseResource filterNestedBundle(RequestDetails theRequestDetails, IBaseResource theResource) {
IParser p = getContext().newJsonParser();
RestfulServerUtils.configureResponseParser(theRequestDetails, p);
return p.parseResource(theResource.getClass(), p.encodeResourceToString(theResource));
}
private IFhirResourceDao<?> getDaoOrThrowException(Class<? extends IBaseResource> theClass) {
IFhirResourceDao<? extends IBaseResource> retVal = getDao(theClass);
if (retVal == null) {
throw new InvalidRequestException("Unable to process request, this server does not know how to handle resources of type " + getContext().getResourceDefinition(theClass).getName());
}
return retVal;
}
@Override
public Meta metaGetOperation(RequestDetails theRequestDetails) {
// Notify interceptors
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails);
notifyInterceptors(RestOperationTypeEnum.META, requestDetails);
String sql = "SELECT d FROM TagDefinition d WHERE d.myId IN (SELECT DISTINCT t.myTagId FROM ResourceTag t)";
TypedQuery<TagDefinition> q = myEntityManager.createQuery(sql, TagDefinition.class);
List<TagDefinition> tagDefinitions = q.getResultList();
Meta retVal = toMeta(tagDefinitions);
return retVal;
}
private String performIdSubstitutionsInMatchUrl(Map<IdType, IdType> theIdSubstitutions, String theMatchUrl) {
String matchUrl = theMatchUrl;
if (isNotBlank(matchUrl)) {
for (Entry<IdType, IdType> nextSubstitutionEntry : theIdSubstitutions.entrySet()) {
IdType nextTemporaryId = nextSubstitutionEntry.getKey();
IdType nextReplacementId = nextSubstitutionEntry.getValue();
String nextTemporaryIdPart = nextTemporaryId.getIdPart();
String nextReplacementIdPart = nextReplacementId.getValueAsString();
if (nextTemporaryId.isUrn() && nextTemporaryIdPart.length() > IdType.URN_PREFIX.length()) {
matchUrl = matchUrl.replace(nextTemporaryIdPart, nextReplacementIdPart);
}
}
}
return matchUrl;
}
private ca.uhn.fhir.jpa.dao.IFhirResourceDao<? extends IBaseResource> toDao(UrlParts theParts, String theVerb, String theUrl) {
RuntimeResourceDefinition resType;
try {
resType = getContext().getResourceDefinition(theParts.getResourceType());
} catch (DataFormatException e) {
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl);
throw new InvalidRequestException(msg);
}
IFhirResourceDao<? extends IBaseResource> dao = null;
if (resType != null) {
dao = getDao(resType.getImplementingClass());
}
if (dao == null) {
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl);
throw new InvalidRequestException(msg);
}
// if (theParts.getResourceId() == null && theParts.getParams() == null) {
// String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl);
// throw new InvalidRequestException(msg);
// }
return dao;
}
protected Meta toMeta(Collection<TagDefinition> tagDefinitions) {
Meta retVal = new Meta();
for (TagDefinition next : tagDefinitions) {
switch (next.getTagType()) {
case PROFILE:
retVal.addProfile(next.getCode());
break;
case SECURITY_LABEL:
retVal.addSecurity().setSystem(next.getSystem()).setCode(next.getCode()).setDisplay(next.getDisplay());
break;
case TAG:
retVal.addTag().setSystem(next.getSystem()).setCode(next.getCode()).setDisplay(next.getDisplay());
break;
}
}
return retVal;
}
@Transactional(propagation = Propagation.REQUIRED)
@Override
public Bundle transaction(RequestDetails theRequestDetails, Bundle theRequest) {
if (theRequestDetails != null) {
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, theRequest, "Bundle", null);
notifyInterceptors(RestOperationTypeEnum.TRANSACTION, requestDetails);
}
String actionName = "Transaction";
return transaction((ServletRequestDetails) theRequestDetails, theRequest, actionName);
}
private Bundle transaction(ServletRequestDetails theRequestDetails, Bundle theRequest, String theActionName) {
super.markRequestAsProcessingSubRequest(theRequestDetails);
try {
return doTransaction(theRequestDetails, theRequest, theActionName);
} finally {
super.clearRequestAsProcessingSubRequest(theRequestDetails);
}
}
private static void handleTransactionCreateOrUpdateOutcome(Map<IdType, IdType> idSubstitutions, Map<IdType, DaoMethodOutcome> idToPersistedOutcome, IdType nextResourceId, DaoMethodOutcome outcome, private static void handleTransactionCreateOrUpdateOutcome(Map<IdType, IdType> idSubstitutions, Map<IdType, DaoMethodOutcome> idToPersistedOutcome, IdType nextResourceId, DaoMethodOutcome outcome,
BundleEntryComponent newEntry, String theResourceType, IBaseResource theRes, ServletRequestDetails theRequestDetails) { BundleEntryComponent newEntry, String theResourceType, IBaseResource theRes, ServletRequestDetails theRequestDetails) {
IdType newId = (IdType) outcome.getId().toUnqualifiedVersionless(); IdType newId = (IdType) outcome.getId().toUnqualifiedVersionless();
@ -655,7 +690,6 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
return Integer.toString(theStatusCode) + " " + defaultString(Constants.HTTP_STATUS_NAMES.get(theStatusCode)); return Integer.toString(theStatusCode) + " " + defaultString(Constants.HTTP_STATUS_NAMES.get(theStatusCode));
} }
//@formatter:off
/** /**
* Transaction Order, per the spec: * Transaction Order, per the spec:
* *
@ -664,37 +698,94 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
* Process any PUT interactions * Process any PUT interactions
* Process any GET interactions * Process any GET interactions
*/ */
//@formatter:off
public class TransactionSorter implements Comparator<BundleEntryComponent> { public class TransactionSorter implements Comparator<BundleEntryComponent> {
private Set<String> myPlaceholderIds;
public TransactionSorter(Set<String> thePlaceholderIds) {
myPlaceholderIds = thePlaceholderIds;
}
@Override @Override
public int compare(BundleEntryComponent theO1, BundleEntryComponent theO2) { public int compare(BundleEntryComponent theO1, BundleEntryComponent theO2) {
int o1 = toOrder(theO1); int o1 = toOrder(theO1);
int o2 = toOrder(theO2); int o2 = toOrder(theO2);
if (o1 == o2) {
String matchUrl1 = toMatchUrl(theO1);
String matchUrl2 = toMatchUrl(theO2);
if (isBlank(matchUrl1) && isBlank(matchUrl2)) {
return 0;
}
if (isBlank(matchUrl1)) {
return -1;
}
if (isBlank(matchUrl2)) {
return 1;
}
boolean match1containsSubstitutions = false;
boolean match2containsSubstitutions = false;
for (String nextPlaceholder : myPlaceholderIds) {
if (matchUrl1.contains(nextPlaceholder)) {
match1containsSubstitutions = true;
}
if (matchUrl2.contains(nextPlaceholder)) {
match2containsSubstitutions = true;
}
}
if (match1containsSubstitutions && match2containsSubstitutions) {
return 0;
}
if (!match1containsSubstitutions && !match2containsSubstitutions) {
return 0;
}
if (match1containsSubstitutions) {
return 1;
} else {
return -1;
}
}
return o1 - o2; return o1 - o2;
} }
private String toMatchUrl(BundleEntryComponent theEntry) {
HTTPVerb verb = theEntry.getRequest().getMethod();
if (verb == HTTPVerb.POST) {
return theEntry.getRequest().getIfNoneExist();
}
if (verb == HTTPVerb.PUT || verb == HTTPVerb.DELETE) {
String url = extractTransactionUrlOrThrowException(theEntry, verb);
UrlParts parts = UrlUtil.parseUrl(url);
if (isBlank(parts.getResourceId())) {
return parts.getResourceType() + '?' + parts.getParams();
}
}
return null;
}
private int toOrder(BundleEntryComponent theO1) { private int toOrder(BundleEntryComponent theO1) {
int o1 = 0; int o1 = 0;
if (theO1.getRequest().getMethodElement().getValue() != null) { if (theO1.getRequest().getMethodElement().getValue() != null) {
switch (theO1.getRequest().getMethodElement().getValue()) { switch (theO1.getRequest().getMethodElement().getValue()) {
case DELETE: case DELETE:
o1 = 1; o1 = 1;
break; break;
case POST: case POST:
o1 = 2; o1 = 2;
break; break;
case PUT: case PUT:
o1 = 3; o1 = 3;
break; break;
case GET: case GET:
o1 = 4; o1 = 4;
break; break;
case NULL: case NULL:
o1 = 0; o1 = 0;
break; break;
} }
} }
return o1; return o1;
} }

View File

@ -7,12 +7,21 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import java.io.*; import java.io.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.*;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.hl7.fhir.dstu3.model.*; import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.dstu3.model.Bundle.*; import org.hl7.fhir.dstu3.model.Bundle.*;
import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent;
import org.hl7.fhir.dstu3.model.Bundle.BundleEntryRequestComponent;
import org.hl7.fhir.dstu3.model.Bundle.BundleEntryResponseComponent;
import org.hl7.fhir.dstu3.model.Bundle.BundleType;
import org.hl7.fhir.dstu3.model.Bundle.HTTPVerb;
import org.hl7.fhir.dstu3.model.Observation.ObservationStatus; import org.hl7.fhir.dstu3.model.Observation.ObservationStatus;
import org.hl7.fhir.dstu3.model.OperationOutcome.IssueSeverity; import org.hl7.fhir.dstu3.model.OperationOutcome.IssueSeverity;
import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IAnyResource;
@ -51,6 +60,100 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest {
myDaoConfig.setReuseCachedSearchResultsForMillis(null); myDaoConfig.setReuseCachedSearchResultsForMillis(null);
} }
private Bundle createInputTransactionWithPlaceholderIdInMatchUrl(HTTPVerb theVerb) {
Patient pat = new Patient();
pat
.addIdentifier()
.setSystem("http://acme.org")
.setValue("ID1");
Observation obs = new Observation();
obs
.getCode()
.addCoding()
.setSystem("http://loinc.org")
.setCode("29463-7");
obs.setEffective(new DateTimeType("2011-09-03T11:13:00-04:00"));
obs.setValue(new Quantity()
.setValue(new BigDecimal("123.4"))
.setCode("kg")
.setSystem("http://unitsofmeasure.org")
.setUnit("kg"));
obs.getSubject().setReference("urn:uuid:0001");
Observation obs2 = new Observation();
obs2
.getCode()
.addCoding()
.setSystem("http://loinc.org")
.setCode("29463-7");
obs2.setEffective(new DateTimeType("2017-09-03T11:13:00-04:00"));
obs2.setValue(new Quantity()
.setValue(new BigDecimal("123.4"))
.setCode("kg")
.setSystem("http://unitsofmeasure.org")
.setUnit("kg"));
obs2.getSubject().setReference("urn:uuid:0001");
/*
* Put one observation before the patient it references, and
* one after it just to make sure that order doesn't matter
*/
Bundle input = new Bundle();
input.setType(BundleType.TRANSACTION);
if (theVerb == HTTPVerb.PUT) {
input
.addEntry()
.setFullUrl("urn:uuid:0002")
.setResource(obs)
.getRequest()
.setMethod(HTTPVerb.PUT)
.setUrl("Observation?subject=urn:uuid:0001&code=http%3A%2F%2Floinc.org|29463-7&date=2011-09-03T11:13:00-04:00");
input
.addEntry()
.setFullUrl("urn:uuid:0001")
.setResource(pat)
.getRequest()
.setMethod(HTTPVerb.PUT)
.setUrl("Patient?identifier=http%3A%2F%2Facme.org|ID1");
input
.addEntry()
.setFullUrl("urn:uuid:0003")
.setResource(obs2)
.getRequest()
.setMethod(HTTPVerb.PUT)
.setUrl("Observation?subject=urn:uuid:0001&code=http%3A%2F%2Floinc.org|29463-7&date=2017-09-03T11:13:00-04:00");
} else if (theVerb == HTTPVerb.POST) {
input
.addEntry()
.setFullUrl("urn:uuid:0002")
.setResource(obs)
.getRequest()
.setMethod(HTTPVerb.POST)
.setUrl("Observation")
.setIfNoneExist("Observation?subject=urn:uuid:0001&code=http%3A%2F%2Floinc.org|29463-7&date=2011-09-03T11:13:00-04:00");
input
.addEntry()
.setFullUrl("urn:uuid:0001")
.setResource(pat)
.getRequest()
.setMethod(HTTPVerb.POST)
.setUrl("Patient")
.setIfNoneExist("Patient?identifier=http%3A%2F%2Facme.org|ID1");
input
.addEntry()
.setFullUrl("urn:uuid:0003")
.setResource(obs2)
.getRequest()
.setMethod(HTTPVerb.POST)
.setUrl("Observation")
.setIfNoneExist("Observation?subject=urn:uuid:0001&code=http%3A%2F%2Floinc.org|29463-7&date=2017-09-03T11:13:00-04:00");
}
return input;
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private <T extends org.hl7.fhir.dstu3.model.Resource> T find(Bundle theBundle, Class<T> theType, int theIndex) { private <T extends org.hl7.fhir.dstu3.model.Resource> T find(Bundle theBundle, Class<T> theType, int theIndex) {
int count = 0; int count = 0;
@ -200,36 +303,36 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest {
rpt = myDiagnosticReportDao.read(rptId); rpt = myDiagnosticReportDao.read(rptId);
assertThat(rpt.getResult(), empty()); assertThat(rpt.getResult(), empty());
} }
@Test @Test
public void testMultipleUpdatesWithNoChangesDoesNotResultInAnUpdateForTransaction() { public void testMultipleUpdatesWithNoChangesDoesNotResultInAnUpdateForTransaction() {
Bundle bundle; Bundle bundle;
// First time // First time
Patient p = new Patient(); Patient p = new Patient();
p.setActive(true); p.setActive(true);
bundle = new Bundle(); bundle = new Bundle();
bundle.setType(BundleType.TRANSACTION); bundle.setType(BundleType.TRANSACTION);
bundle bundle
.addEntry() .addEntry()
.setResource(p) .setResource(p)
.setFullUrl("Patient/A") .setFullUrl("Patient/A")
.getRequest() .getRequest()
.setMethod(HTTPVerb.PUT) .setMethod(HTTPVerb.PUT)
.setUrl("Patient/A"); .setUrl("Patient/A");
Bundle resp = mySystemDao.transaction(mySrd, bundle); Bundle resp = mySystemDao.transaction(mySrd, bundle);
assertThat(resp.getEntry().get(0).getResponse().getLocation(), endsWith("Patient/A/_history/1")); assertThat(resp.getEntry().get(0).getResponse().getLocation(), endsWith("Patient/A/_history/1"));
// Second time should not result in an update // Second time should not result in an update
p = new Patient(); p = new Patient();
p.setActive(true); p.setActive(true);
bundle = new Bundle(); bundle = new Bundle();
bundle.setType(BundleType.TRANSACTION); bundle.setType(BundleType.TRANSACTION);
bundle bundle
.addEntry() .addEntry()
.setResource(p) .setResource(p)
.setFullUrl("Patient/A") .setFullUrl("Patient/A")
.getRequest() .getRequest()
.setMethod(HTTPVerb.PUT) .setMethod(HTTPVerb.PUT)
.setUrl("Patient/A"); .setUrl("Patient/A");
resp = mySystemDao.transaction(mySrd, bundle); resp = mySystemDao.transaction(mySrd, bundle);
@ -241,10 +344,10 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest {
bundle = new Bundle(); bundle = new Bundle();
bundle.setType(BundleType.TRANSACTION); bundle.setType(BundleType.TRANSACTION);
bundle bundle
.addEntry() .addEntry()
.setResource(p) .setResource(p)
.setFullUrl("Patient/A") .setFullUrl("Patient/A")
.getRequest() .getRequest()
.setMethod(HTTPVerb.PUT) .setMethod(HTTPVerb.PUT)
.setUrl("Patient/A"); .setUrl("Patient/A");
resp = mySystemDao.transaction(mySrd, bundle); resp = mySystemDao.transaction(mySrd, bundle);
@ -256,13 +359,13 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest {
myPatientDao.read(new IdType("Patient/A/_history/2")); myPatientDao.read(new IdType("Patient/A/_history/2"));
fail(); fail();
} catch (ResourceNotFoundException e) { } catch (ResourceNotFoundException e) {
//good // good
} }
try { try {
myPatientDao.read(new IdType("Patient/A/_history/3")); myPatientDao.read(new IdType("Patient/A/_history/3"));
fail(); fail();
} catch (ResourceNotFoundException e) { } catch (ResourceNotFoundException e) {
//good // good
} }
} }
@ -487,60 +590,6 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest {
} }
} }
@Test
public void testTransactionWithReferenceUuid() {
Bundle request = new Bundle();
Patient p = new Patient();
p.setActive(true);
p.setId(IdType.newRandomUuid());
request.addEntry().setResource(p).getRequest().setMethod(HTTPVerb.POST).setUrl(p.getId());
Observation o = new Observation();
o.getCode().setText("Some Observation");
o.getSubject().setReference(p.getId());
request.addEntry().setResource(o).getRequest().setMethod(HTTPVerb.POST);
Bundle resp = mySystemDao.transaction(mySrd, request);
ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(resp));
String patientId = new IdType(resp.getEntry().get(0).getResponse().getLocation()).toUnqualifiedVersionless().getValue();
assertThat(patientId, startsWith("Patient/"));
SearchParameterMap params = new SearchParameterMap();
params.setLoadSynchronous(true);
params.add("subject", new ReferenceParam(patientId));
IBundleProvider found = myObservationDao.search(params);
assertEquals(1, found.size().intValue());
}
@Test
public void testTransactionWithReferenceResource() {
Bundle request = new Bundle();
Patient p = new Patient();
p.setActive(true);
p.setId(IdType.newRandomUuid());
request.addEntry().setResource(p).getRequest().setMethod(HTTPVerb.POST).setUrl(p.getId());
Observation o = new Observation();
o.getCode().setText("Some Observation");
o.getSubject().setResource(p);
request.addEntry().setResource(o).getRequest().setMethod(HTTPVerb.POST);
Bundle resp = mySystemDao.transaction(mySrd, request);
ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(resp));
String patientId = new IdType(resp.getEntry().get(0).getResponse().getLocation()).toUnqualifiedVersionless().getValue();
assertThat(patientId, startsWith("Patient/"));
SearchParameterMap params = new SearchParameterMap();
params.setLoadSynchronous(true);
params.add("subject", new ReferenceParam(patientId));
IBundleProvider found = myObservationDao.search(params);
assertEquals(1, found.size().intValue());
}
@Test @Test
public void testTransactionCreateInlineMatchUrlWithOneMatch() { public void testTransactionCreateInlineMatchUrlWithOneMatch() {
String methodName = "testTransactionCreateInlineMatchUrlWithOneMatch"; String methodName = "testTransactionCreateInlineMatchUrlWithOneMatch";
@ -1335,7 +1384,8 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest {
mySystemDao.transaction(mySrd, inputBundle); mySystemDao.transaction(mySrd, inputBundle);
fail(); fail();
} catch (InvalidRequestException e) { } catch (InvalidRequestException e) {
assertEquals("Unable to process Transaction - Request would cause multiple resources to match URL: \"Encounter?identifier=urn:foo|12345\". Does transaction request contain duplicates?", e.getMessage()); assertEquals("Unable to process Transaction - Request would cause multiple resources to match URL: \"Encounter?identifier=urn:foo|12345\". Does transaction request contain duplicates?",
e.getMessage());
} }
} }
@ -1365,9 +1415,10 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest {
mySystemDao.transaction(mySrd, inputBundle); mySystemDao.transaction(mySrd, inputBundle);
fail(); fail();
} catch (InvalidRequestException e) { } catch (InvalidRequestException e) {
assertEquals("Unable to process Transaction - Request would cause multiple resources to match URL: \"Encounter?identifier=urn:foo|12345\". Does transaction request contain duplicates?", e.getMessage()); assertEquals("Unable to process Transaction - Request would cause multiple resources to match URL: \"Encounter?identifier=urn:foo|12345\". Does transaction request contain duplicates?",
e.getMessage());
} }
} }
@Test(expected = InvalidRequestException.class) @Test(expected = InvalidRequestException.class)
@ -2026,8 +2077,7 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest {
ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(resp)); ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(resp));
assertEquals("201 Created", resp.getEntry().get(0).getResponse().getStatus()); assertEquals("201 Created", resp.getEntry().get(0).getResponse().getStatus());
new TransactionTemplate(myTxManager).execute(new TransactionCallbackWithoutResult() { new TransactionTemplate(myTxManager).execute(new TransactionCallbackWithoutResult() {
@Override @Override
protected void doInTransactionWithoutResult(TransactionStatus theStatus) { protected void doInTransactionWithoutResult(TransactionStatus theStatus) {
@ -2040,7 +2090,7 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest {
} }
} }
}); });
} }
@Test @Test
@ -2101,6 +2151,66 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest {
assertEquals(id0.toUnqualifiedVersionless().getValue(), app2.getParticipant().get(1).getActor().getReference()); assertEquals(id0.toUnqualifiedVersionless().getValue(), app2.getParticipant().get(1).getActor().getReference());
} }
/*
* Make sure we are able to handle placeholder IDs in match URLs, e.g.
*
* "request": {
* "method": "PUT",
* "url": "Observation?subject=urn:uuid:8dba64a8-2aca-48fe-8b4e-8c7bf2ab695a&code=http%3A%2F%2Floinc.org|29463-7&date=2011-09-03T11:13:00-04:00"
* }
* </pre>
*/
@Test
public void testTransactionWithPlaceholderIdInMatchUrlPut() {
Bundle input = createInputTransactionWithPlaceholderIdInMatchUrl(HTTPVerb.PUT);
Bundle output = mySystemDao.transaction(null, input);
assertEquals("201 Created", output.getEntry().get(0).getResponse().getStatus());
assertEquals("201 Created", output.getEntry().get(1).getResponse().getStatus());
assertEquals("201 Created", output.getEntry().get(2).getResponse().getStatus());
Bundle input2 = createInputTransactionWithPlaceholderIdInMatchUrl(HTTPVerb.PUT);
Bundle output2 = mySystemDao.transaction(null, input2);
ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(output2));
assertEquals("200 OK", output2.getEntry().get(0).getResponse().getStatus());
assertEquals("200 OK", output2.getEntry().get(1).getResponse().getStatus());
assertEquals("200 OK", output2.getEntry().get(2).getResponse().getStatus());
}
/*
* Make sure we are able to handle placeholder IDs in match URLs, e.g.
*
* "request": {
* "method": "PUT",
* "url": "Observation?subject=urn:uuid:8dba64a8-2aca-48fe-8b4e-8c7bf2ab695a&code=http%3A%2F%2Floinc.org|29463-7&date=2011-09-03T11:13:00-04:00"
* }
* </pre>
*/
@Test
public void testTransactionWithPlaceholderIdInMatchUrlPost() {
Bundle input = createInputTransactionWithPlaceholderIdInMatchUrl(HTTPVerb.POST);
Bundle output = mySystemDao.transaction(null, input);
assertEquals("201 Created", output.getEntry().get(0).getResponse().getStatus());
assertEquals("201 Created", output.getEntry().get(1).getResponse().getStatus());
assertEquals("201 Created", output.getEntry().get(2).getResponse().getStatus());
Bundle input2 = createInputTransactionWithPlaceholderIdInMatchUrl(HTTPVerb.POST);
Bundle output2 = mySystemDao.transaction(null, input2);
ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(output2));
assertEquals("200 OK", output2.getEntry().get(0).getResponse().getStatus());
assertEquals("200 OK", output2.getEntry().get(1).getResponse().getStatus());
assertEquals("200 OK", output2.getEntry().get(2).getResponse().getStatus());
}
/** /**
* Per a message on the mailing list * Per a message on the mailing list
*/ */
@ -2137,6 +2247,86 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest {
assertEquals("Joshua", patient.getNameFirstRep().getGivenAsSingleString()); assertEquals("Joshua", patient.getNameFirstRep().getGivenAsSingleString());
} }
@Test
public void testTransactionWithReferenceResource() {
Bundle request = new Bundle();
Patient p = new Patient();
p.setActive(true);
p.setId(IdType.newRandomUuid());
request.addEntry().setResource(p).getRequest().setMethod(HTTPVerb.POST).setUrl(p.getId());
Observation o = new Observation();
o.getCode().setText("Some Observation");
o.getSubject().setResource(p);
request.addEntry().setResource(o).getRequest().setMethod(HTTPVerb.POST);
Bundle resp = mySystemDao.transaction(mySrd, request);
ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(resp));
String patientId = new IdType(resp.getEntry().get(0).getResponse().getLocation()).toUnqualifiedVersionless().getValue();
assertThat(patientId, startsWith("Patient/"));
SearchParameterMap params = new SearchParameterMap();
params.setLoadSynchronous(true);
params.add("subject", new ReferenceParam(patientId));
IBundleProvider found = myObservationDao.search(params);
assertEquals(1, found.size().intValue());
}
@Test
public void testTransactionWithReferenceToCreateIfNoneExist() {
Bundle bundle = new Bundle();
bundle.setType(BundleType.TRANSACTION);
Medication med = new Medication();
IdType medId = IdType.newRandomUuid();
med.setId(medId);
med.getCode().addCoding().setSystem("billscodes").setCode("theCode");
bundle.addEntry().setResource(med).setFullUrl(medId.getValue()).getRequest().setMethod(HTTPVerb.POST).setIfNoneExist("Medication?code=billscodes|theCode");
MedicationRequest mo = new MedicationRequest();
mo.setMedication(new Reference(medId));
bundle.addEntry().setResource(mo).setFullUrl(mo.getIdElement().getValue()).getRequest().setMethod(HTTPVerb.POST);
ourLog.info("Request:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(bundle));
Bundle outcome = mySystemDao.transaction(mySrd, bundle);
ourLog.info("Response:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome));
IdType medId1 = new IdType(outcome.getEntry().get(0).getResponse().getLocation());
IdType medOrderId1 = new IdType(outcome.getEntry().get(1).getResponse().getLocation());
/*
* Again!
*/
bundle = new Bundle();
bundle.setType(BundleType.TRANSACTION);
med = new Medication();
medId = IdType.newRandomUuid();
med.getCode().addCoding().setSystem("billscodes").setCode("theCode");
bundle.addEntry().setResource(med).setFullUrl(medId.getValue()).getRequest().setMethod(HTTPVerb.POST).setIfNoneExist("Medication?code=billscodes|theCode");
mo = new MedicationRequest();
mo.setMedication(new Reference(medId));
bundle.addEntry().setResource(mo).setFullUrl(mo.getIdElement().getValue()).getRequest().setMethod(HTTPVerb.POST);
outcome = mySystemDao.transaction(mySrd, bundle);
IdType medId2 = new IdType(outcome.getEntry().get(0).getResponse().getLocation());
IdType medOrderId2 = new IdType(outcome.getEntry().get(1).getResponse().getLocation());
assertTrue(medId1.isIdPartValidLong());
assertTrue(medId2.isIdPartValidLong());
assertTrue(medOrderId1.isIdPartValidLong());
assertTrue(medOrderId2.isIdPartValidLong());
assertEquals(medId1, medId2);
assertNotEquals(medOrderId1, medOrderId2);
}
// //
// //
// /** // /**
@ -2240,59 +2430,32 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest {
// } // }
@Test @Test
public void testTransactionWithReferenceToCreateIfNoneExist() { public void testTransactionWithReferenceUuid() {
Bundle bundle = new Bundle(); Bundle request = new Bundle();
bundle.setType(BundleType.TRANSACTION);
Medication med = new Medication(); Patient p = new Patient();
IdType medId = IdType.newRandomUuid(); p.setActive(true);
med.setId(medId); p.setId(IdType.newRandomUuid());
med.getCode().addCoding().setSystem("billscodes").setCode("theCode"); request.addEntry().setResource(p).getRequest().setMethod(HTTPVerb.POST).setUrl(p.getId());
bundle.addEntry().setResource(med).setFullUrl(medId.getValue()).getRequest().setMethod(HTTPVerb.POST).setIfNoneExist("Medication?code=billscodes|theCode");
MedicationRequest mo = new MedicationRequest(); Observation o = new Observation();
mo.setMedication(new Reference(medId)); o.getCode().setText("Some Observation");
bundle.addEntry().setResource(mo).setFullUrl(mo.getIdElement().getValue()).getRequest().setMethod(HTTPVerb.POST); o.getSubject().setReference(p.getId());
request.addEntry().setResource(o).getRequest().setMethod(HTTPVerb.POST);
ourLog.info("Request:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(bundle)); Bundle resp = mySystemDao.transaction(mySrd, request);
ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(resp));
Bundle outcome = mySystemDao.transaction(mySrd, bundle); String patientId = new IdType(resp.getEntry().get(0).getResponse().getLocation()).toUnqualifiedVersionless().getValue();
ourLog.info("Response:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); assertThat(patientId, startsWith("Patient/"));
IdType medId1 = new IdType(outcome.getEntry().get(0).getResponse().getLocation()); SearchParameterMap params = new SearchParameterMap();
IdType medOrderId1 = new IdType(outcome.getEntry().get(1).getResponse().getLocation()); params.setLoadSynchronous(true);
params.add("subject", new ReferenceParam(patientId));
/* IBundleProvider found = myObservationDao.search(params);
* Again! assertEquals(1, found.size().intValue());
*/
bundle = new Bundle();
bundle.setType(BundleType.TRANSACTION);
med = new Medication();
medId = IdType.newRandomUuid();
med.getCode().addCoding().setSystem("billscodes").setCode("theCode");
bundle.addEntry().setResource(med).setFullUrl(medId.getValue()).getRequest().setMethod(HTTPVerb.POST).setIfNoneExist("Medication?code=billscodes|theCode");
mo = new MedicationRequest();
mo.setMedication(new Reference(medId));
bundle.addEntry().setResource(mo).setFullUrl(mo.getIdElement().getValue()).getRequest().setMethod(HTTPVerb.POST);
outcome = mySystemDao.transaction(mySrd, bundle);
IdType medId2 = new IdType(outcome.getEntry().get(0).getResponse().getLocation());
IdType medOrderId2 = new IdType(outcome.getEntry().get(1).getResponse().getLocation());
assertTrue(medId1.isIdPartValidLong());
assertTrue(medId2.isIdPartValidLong());
assertTrue(medOrderId1.isIdPartValidLong());
assertTrue(medOrderId2.isIdPartValidLong());
assertEquals(medId1, medId2);
assertNotEquals(medOrderId1, medOrderId2);
} }
@Test @Test
public void testTransactionWithRelativeOidIds() throws Exception { public void testTransactionWithRelativeOidIds() throws Exception {
Bundle res = new Bundle(); Bundle res = new Bundle();
@ -2330,7 +2493,6 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest {
assertThat(o2.getSubject().getReferenceElement().getValue(), endsWith("Patient/" + p1.getIdElement().getIdPart())); assertThat(o2.getSubject().getReferenceElement().getValue(), endsWith("Patient/" + p1.getIdElement().getIdPart()));
} }
/** /**
* This is not the correct way to do it, but we'll allow it to be lenient * This is not the correct way to do it, but we'll allow it to be lenient

View File

@ -86,6 +86,8 @@ import ca.uhn.fhir.model.api.annotation.DatatypeDef;
*/ */
@DatatypeDef(name = "id", profileOf=StringType.class) @DatatypeDef(name = "id", profileOf=StringType.class)
public final class IdType extends UriType implements IPrimitiveType<String>, IIdType { public final class IdType extends UriType implements IPrimitiveType<String>, IIdType {
public static final String URN_PREFIX = "urn:";
/** /**
* This is the maximum length for the ID * This is the maximum length for the ID
*/ */
@ -486,8 +488,8 @@ public final class IdType extends UriType implements IPrimitiveType<String>, IId
return defaultString(myUnqualifiedId).startsWith("#"); return defaultString(myUnqualifiedId).startsWith("#");
} }
private boolean isUrn() { public boolean isUrn() {
return defaultString(myUnqualifiedId).startsWith("urn:"); return defaultString(myUnqualifiedId).startsWith(URN_PREFIX);
} }
@Override @Override
@ -526,7 +528,7 @@ public final class IdType extends UriType implements IPrimitiveType<String>, IId
myUnqualifiedVersionId = null; myUnqualifiedVersionId = null;
myResourceType = null; myResourceType = null;
myHaveComponentParts = true; myHaveComponentParts = true;
} else if (theValue.startsWith("urn:")) { } else if (theValue.startsWith(URN_PREFIX)) {
myBaseUrl = null; myBaseUrl = null;
myUnqualifiedId = theValue; myUnqualifiedId = theValue;
myUnqualifiedVersionId = null; myUnqualifiedVersionId = null;

View File

@ -13,6 +13,11 @@
meant that any project using ookhttp would import both structures meant that any project using ookhttp would import both structures
JARs. This has been removed. JARs. This has been removed.
</action> </action>
<action type="add">
JPA server is now able to handle placeholder IDs (e.g. urn:uuid:00....000)
being used in Bundle.entry.request.url as a part of the conditional URL
within transactions.
</action>
</release </release
<release version="2.6" date="TBD"> <release version="2.6" date="TBD">
<action type="add"> <action type="add">