Working in-place bundle sort
This commit is contained in:
parent
ff2690b74e
commit
3dee0c8ab6
|
@ -20,12 +20,14 @@ import org.hl7.fhir.instance.model.api.IBaseResource;
|
|||
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
/*
|
||||
|
@ -186,13 +188,47 @@ public class BundleUtil {
|
|||
static int GRAY = 2;
|
||||
static int BLACK = 3;
|
||||
|
||||
public static List<BundleEntryParts> topologicalSort(FhirContext theContext, IBaseBundle theBundle, RequestTypeEnum theRequestTypeEnum) {
|
||||
public static void sortEntriesIntoProcessingOrder(FhirContext theContext, IBaseBundle theBundle) {
|
||||
Map<BundleEntryParts, IBase> partsToIBaseMap = getPartsToIBaseMap(theContext, theBundle);
|
||||
LinkedHashSet<IBase> retVal = new LinkedHashSet<>();
|
||||
|
||||
//Get all deletions.
|
||||
LinkedHashSet<IBase> deleteParts = sortEntriesIntoProcessingOrder(theContext, theBundle, RequestTypeEnum.DELETE, partsToIBaseMap);
|
||||
if (deleteParts == null) {
|
||||
throw new IllegalStateException("Cycle!");
|
||||
} else {
|
||||
retVal.addAll(deleteParts);
|
||||
}
|
||||
|
||||
//Get all Creations
|
||||
LinkedHashSet<IBase> createParts= sortEntriesIntoProcessingOrder(theContext, theBundle, RequestTypeEnum.POST, partsToIBaseMap);
|
||||
if (createParts== null) {
|
||||
throw new IllegalStateException("Cycle!");
|
||||
} else {
|
||||
retVal.addAll(createParts);
|
||||
}
|
||||
|
||||
// Get all Updates
|
||||
LinkedHashSet<IBase> updateParts= sortEntriesIntoProcessingOrder(theContext, theBundle, RequestTypeEnum.PUT, partsToIBaseMap);
|
||||
if (updateParts == null) {
|
||||
throw new IllegalStateException("Cycle!");
|
||||
} else {
|
||||
retVal.addAll(updateParts);
|
||||
}
|
||||
//Once we are done adding all DELETE, POST, PUT operations, add everything else.
|
||||
retVal.addAll(partsToIBaseMap.values());
|
||||
|
||||
//Blow away the entries and reset them in the right order.
|
||||
TerserUtil.clearField(theContext, "entry", theBundle);
|
||||
TerserUtil.setField(theContext, "entry", theBundle, retVal.toArray(new IBase[0]));
|
||||
}
|
||||
|
||||
public static LinkedHashSet<IBase> sortEntriesIntoProcessingOrder(FhirContext theContext, IBaseBundle theBundle, RequestTypeEnum theRequestTypeEnum, Map<BundleEntryParts, IBase> thePartsToIBaseMap) {
|
||||
SortLegality legality = new SortLegality();
|
||||
HashMap<String, Integer> color = new HashMap<String, Integer>();
|
||||
HashMap<String, Integer> color = new HashMap<>();
|
||||
HashMap<String, List<String>> adjList = new HashMap<>();
|
||||
List<String> topologicalOrder = new ArrayList<>();
|
||||
List<BundleEntryParts> bundleEntryParts = toListOfEntries(theContext, theBundle);
|
||||
bundleEntryParts.removeIf(bep -> !bep.getRequestType().equals(theRequestTypeEnum));
|
||||
Set<BundleEntryParts> bundleEntryParts = thePartsToIBaseMap.keySet().stream().filter(part -> part.getRequestType().equals(theRequestTypeEnum)).collect(Collectors.toSet());
|
||||
HashMap<String, BundleEntryParts> resourceIdToBundleEntryMap = new HashMap<>();
|
||||
|
||||
for (BundleEntryParts bundleEntryPart : bundleEntryParts) {
|
||||
|
@ -204,7 +240,9 @@ public class BundleUtil {
|
|||
resourceId = bundleEntryPart.getFullUrl();
|
||||
}
|
||||
}
|
||||
|
||||
color.put(resourceId, WHITE);
|
||||
|
||||
}
|
||||
|
||||
for (BundleEntryParts bundleEntryPart : bundleEntryParts) {
|
||||
|
@ -229,30 +267,33 @@ public class BundleUtil {
|
|||
}
|
||||
});
|
||||
}
|
||||
//All nodes are now white
|
||||
//Adjacency List has been built.
|
||||
|
||||
for (Map.Entry<String, Integer> entry:color.entrySet()) {
|
||||
if (entry.getValue() == WHITE) {
|
||||
depthFirstSearch(entry.getKey(), color, adjList, topologicalOrder, legality);
|
||||
}
|
||||
}
|
||||
|
||||
if (legality.isLegal()) {
|
||||
if (ourLog.isDebugEnabled()) {
|
||||
ourLog.debug("Topological order is: {}", String.join(",", topologicalOrder));
|
||||
}
|
||||
List<BundleEntryParts> beps = new ArrayList<>();
|
||||
|
||||
for (int i = 0;i < topologicalOrder.size(); i++) {
|
||||
BundleEntryParts bep = resourceIdToBundleEntryMap.get(topologicalOrder.get(i));
|
||||
beps.add(bep);
|
||||
LinkedHashSet<IBase> orderedEntries = new LinkedHashSet<>();
|
||||
for (int i = 0; i < topologicalOrder.size(); i++) {
|
||||
BundleEntryParts bep;
|
||||
if (theRequestTypeEnum.equals(RequestTypeEnum.DELETE)) {
|
||||
int index = topologicalOrder.size() - i - 1;
|
||||
bep = resourceIdToBundleEntryMap.get(topologicalOrder.get(index));
|
||||
} else {
|
||||
bep = resourceIdToBundleEntryMap.get(topologicalOrder.get(i));
|
||||
}
|
||||
IBase base = thePartsToIBaseMap.get(bep);
|
||||
orderedEntries.add(base);
|
||||
}
|
||||
|
||||
//In case of delete, we want to delete child elements LAST.
|
||||
if (theRequestTypeEnum.equals(RequestTypeEnum.DELETE)) {
|
||||
Collections.reverse(beps);
|
||||
}
|
||||
return beps;
|
||||
return orderedEntries;
|
||||
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
@ -295,15 +336,38 @@ public class BundleUtil {
|
|||
theTopologicalOrder.add(theResourceId);
|
||||
}
|
||||
|
||||
private static Map<BundleEntryParts, IBase> getPartsToIBaseMap(FhirContext theContext, IBaseBundle theBundle) {
|
||||
RuntimeResourceDefinition bundleDef = theContext.getResourceDefinition(theBundle);
|
||||
BaseRuntimeChildDefinition entryChildDef = bundleDef.getChildByName("entry");
|
||||
List<IBase> entries = entryChildDef.getAccessor().getValues(theBundle);
|
||||
|
||||
BaseRuntimeElementCompositeDefinition<?> entryChildContentsDef = (BaseRuntimeElementCompositeDefinition<?>) entryChildDef.getChildByName("entry");
|
||||
BaseRuntimeChildDefinition fullUrlChildDef = entryChildContentsDef.getChildByName("fullUrl");
|
||||
BaseRuntimeChildDefinition resourceChildDef = entryChildContentsDef.getChildByName("resource");
|
||||
BaseRuntimeChildDefinition requestChildDef = entryChildContentsDef.getChildByName("request");
|
||||
BaseRuntimeElementCompositeDefinition<?> requestChildContentsDef = (BaseRuntimeElementCompositeDefinition<?>) requestChildDef.getChildByName("request");
|
||||
BaseRuntimeChildDefinition requestUrlChildDef = requestChildContentsDef.getChildByName("url");
|
||||
BaseRuntimeChildDefinition requestIfNoneExistChildDef = requestChildContentsDef.getChildByName("ifNoneExist");
|
||||
BaseRuntimeChildDefinition methodChildDef = requestChildContentsDef.getChildByName("method");
|
||||
Map<BundleEntryParts, IBase> map = new HashMap<>();
|
||||
for (IBase nextEntry : entries) {
|
||||
BundleEntryParts parts = getBundleEntryParts(fullUrlChildDef, resourceChildDef, requestChildDef, requestUrlChildDef, requestIfNoneExistChildDef, methodChildDef, nextEntry);
|
||||
/*
|
||||
* All 3 might be null - That's ok because we still want to know the
|
||||
* order in the original bundle.
|
||||
*/
|
||||
map.put(parts, nextEntry);
|
||||
}
|
||||
return map;
|
||||
|
||||
}
|
||||
public static void processEntries(FhirContext theContext, IBaseBundle theBundle, Consumer<ModifiableBundleEntry> theProcessor) {
|
||||
RuntimeResourceDefinition bundleDef = theContext.getResourceDefinition(theBundle);
|
||||
BaseRuntimeChildDefinition entryChildDef = bundleDef.getChildByName("entry");
|
||||
List<IBase> entries = entryChildDef.getAccessor().getValues(theBundle);
|
||||
|
||||
BaseRuntimeElementCompositeDefinition<?> entryChildContentsDef = (BaseRuntimeElementCompositeDefinition<?>) entryChildDef.getChildByName("entry");
|
||||
|
||||
BaseRuntimeChildDefinition fullUrlChildDef = entryChildContentsDef.getChildByName("fullUrl");
|
||||
|
||||
BaseRuntimeChildDefinition resourceChildDef = entryChildContentsDef.getChildByName("resource");
|
||||
BaseRuntimeChildDefinition requestChildDef = entryChildContentsDef.getChildByName("request");
|
||||
BaseRuntimeElementCompositeDefinition<?> requestChildContentsDef = (BaseRuntimeElementCompositeDefinition<?>) requestChildDef.getChildByName("request");
|
||||
|
@ -312,57 +376,62 @@ public class BundleUtil {
|
|||
BaseRuntimeChildDefinition methodChildDef = requestChildContentsDef.getChildByName("method");
|
||||
|
||||
for (IBase nextEntry : entries) {
|
||||
IBaseResource resource = null;
|
||||
String url = null;
|
||||
RequestTypeEnum requestType = null;
|
||||
String conditionalUrl = null;
|
||||
String fullUrl = fullUrlChildDef
|
||||
.getAccessor()
|
||||
.getFirstValueOrNull(nextEntry)
|
||||
.map(t->((IPrimitiveType<?>)t).getValueAsString())
|
||||
.orElse(null);
|
||||
|
||||
for (IBase nextResource : resourceChildDef.getAccessor().getValues(nextEntry)) {
|
||||
resource = (IBaseResource) nextResource;
|
||||
}
|
||||
for (IBase nextRequest : requestChildDef.getAccessor().getValues(nextEntry)) {
|
||||
for (IBase nextUrl : requestUrlChildDef.getAccessor().getValues(nextRequest)) {
|
||||
url = ((IPrimitiveType<?>) nextUrl).getValueAsString();
|
||||
}
|
||||
for (IBase nextMethod : methodChildDef.getAccessor().getValues(nextRequest)) {
|
||||
String methodString = ((IPrimitiveType<?>) nextMethod).getValueAsString();
|
||||
if (isNotBlank(methodString)) {
|
||||
requestType = RequestTypeEnum.valueOf(methodString);
|
||||
}
|
||||
}
|
||||
|
||||
if (requestType != null) {
|
||||
//noinspection EnumSwitchStatementWhichMissesCases
|
||||
switch (requestType) {
|
||||
case PUT:
|
||||
conditionalUrl = url != null && url.contains("?") ? url : null;
|
||||
break;
|
||||
case POST:
|
||||
List<IBase> ifNoneExistReps = requestIfNoneExistChildDef.getAccessor().getValues(nextRequest);
|
||||
if (ifNoneExistReps.size() > 0) {
|
||||
IPrimitiveType<?> ifNoneExist = (IPrimitiveType<?>) ifNoneExistReps.get(0);
|
||||
conditionalUrl = ifNoneExist.getValueAsString();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BundleEntryParts parts = getBundleEntryParts(fullUrlChildDef, resourceChildDef, requestChildDef, requestUrlChildDef, requestIfNoneExistChildDef, methodChildDef, nextEntry);
|
||||
/*
|
||||
* All 3 might be null - That's ok because we still want to know the
|
||||
* order in the original bundle.
|
||||
*/
|
||||
BundleEntryMutator mutator = new BundleEntryMutator(theContext, nextEntry, requestChildDef, requestChildContentsDef, entryChildContentsDef);
|
||||
ModifiableBundleEntry entry = new ModifiableBundleEntry(new BundleEntryParts(fullUrl, requestType, url, resource, conditionalUrl), mutator);
|
||||
ModifiableBundleEntry entry = new ModifiableBundleEntry(parts, mutator);
|
||||
theProcessor.accept(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private static BundleEntryParts getBundleEntryParts(BaseRuntimeChildDefinition fullUrlChildDef, BaseRuntimeChildDefinition resourceChildDef, BaseRuntimeChildDefinition requestChildDef, BaseRuntimeChildDefinition requestUrlChildDef, BaseRuntimeChildDefinition requestIfNoneExistChildDef, BaseRuntimeChildDefinition methodChildDef, IBase nextEntry) {
|
||||
IBaseResource resource = null;
|
||||
String url = null;
|
||||
RequestTypeEnum requestType = null;
|
||||
String conditionalUrl = null;
|
||||
String fullUrl = fullUrlChildDef
|
||||
.getAccessor()
|
||||
.getFirstValueOrNull(nextEntry)
|
||||
.map(t->((IPrimitiveType<?>)t).getValueAsString())
|
||||
.orElse(null);
|
||||
|
||||
for (IBase nextResource : resourceChildDef.getAccessor().getValues(nextEntry)) {
|
||||
resource = (IBaseResource) nextResource;
|
||||
}
|
||||
for (IBase nextRequest : requestChildDef.getAccessor().getValues(nextEntry)) {
|
||||
for (IBase nextUrl : requestUrlChildDef.getAccessor().getValues(nextRequest)) {
|
||||
url = ((IPrimitiveType<?>) nextUrl).getValueAsString();
|
||||
}
|
||||
for (IBase nextMethod : methodChildDef.getAccessor().getValues(nextRequest)) {
|
||||
String methodString = ((IPrimitiveType<?>) nextMethod).getValueAsString();
|
||||
if (isNotBlank(methodString)) {
|
||||
requestType = RequestTypeEnum.valueOf(methodString);
|
||||
}
|
||||
}
|
||||
|
||||
if (requestType != null) {
|
||||
//noinspection EnumSwitchStatementWhichMissesCases
|
||||
switch (requestType) {
|
||||
case PUT:
|
||||
conditionalUrl = url != null && url.contains("?") ? url : null;
|
||||
break;
|
||||
case POST:
|
||||
List<IBase> ifNoneExistReps = requestIfNoneExistChildDef.getAccessor().getValues(nextRequest);
|
||||
if (ifNoneExistReps.size() > 0) {
|
||||
IPrimitiveType<?> ifNoneExist = (IPrimitiveType<?>) ifNoneExistReps.get(0);
|
||||
conditionalUrl = ifNoneExist.getValueAsString();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
BundleEntryParts parts = new BundleEntryParts(fullUrl, requestType, url, resource, conditionalUrl);
|
||||
return parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all of the resources from a given bundle
|
||||
*/
|
||||
|
|
|
@ -4,12 +4,10 @@ import ca.uhn.fhir.interceptor.api.HookParams;
|
|||
import ca.uhn.fhir.interceptor.api.IInterceptorService;
|
||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
|
||||
import ca.uhn.fhir.rest.api.RequestTypeEnum;
|
||||
import ca.uhn.fhir.rest.api.server.storage.DeferredInterceptorBroadcasts;
|
||||
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
|
||||
import ca.uhn.fhir.util.BundleUtil;
|
||||
import ca.uhn.fhir.util.bundle.BundleEntryParts;
|
||||
import ca.uhn.test.concurrency.PointcutLatch;
|
||||
import com.google.common.collect.ListMultimap;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
@ -33,10 +31,8 @@ import static org.hamcrest.MatcherAssert.assertThat;
|
|||
import static org.hamcrest.Matchers.empty;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.matchesPattern;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
||||
public class TransactionHookTest extends BaseJpaR4SystemTest {
|
||||
|
||||
|
@ -92,13 +88,14 @@ public class TransactionHookTest extends BaseJpaR4SystemTest {
|
|||
organizationComponent.setResource(org1);
|
||||
organizationComponent.getRequest().setMethod(Bundle.HTTPVerb.POST).setUrl("Patient");
|
||||
|
||||
List<BundleEntryParts> postBundleEntries = BundleUtil.topologicalSort(myFhirCtx, b, RequestTypeEnum.POST);
|
||||
BundleUtil.sortEntriesIntoProcessingOrder(myFhirCtx, b);
|
||||
|
||||
assertThat(postBundleEntries, hasSize(4));
|
||||
assertThat(b.getEntry(), hasSize(4));
|
||||
|
||||
int observationIndex = getIndexOfEntryWithId("Observation/O1", postBundleEntries);
|
||||
int patientIndex = getIndexOfEntryWithId("Patient/P1", postBundleEntries);
|
||||
int organizationIndex = getIndexOfEntryWithId("Organization/Org1", postBundleEntries);
|
||||
List<Bundle.BundleEntryComponent> entry = b.getEntry();
|
||||
int observationIndex = getIndexOfEntryWithId("Observation/O1", b);
|
||||
int patientIndex = getIndexOfEntryWithId("Patient/P1", b);
|
||||
int organizationIndex = getIndexOfEntryWithId("Organization/Org1", b);
|
||||
|
||||
assertTrue(organizationIndex < patientIndex);
|
||||
assertTrue(patientIndex < observationIndex);
|
||||
|
@ -125,10 +122,12 @@ public class TransactionHookTest extends BaseJpaR4SystemTest {
|
|||
obs2.setHasMember(Collections.singletonList(new Reference("Observation/O1")));
|
||||
bundleEntryComponent.setResource(obs2);
|
||||
bundleEntryComponent.getRequest().setMethod(Bundle.HTTPVerb.POST).setUrl("Observation");
|
||||
List<BundleEntryParts> postBundleEntries = BundleUtil.topologicalSort(myFhirCtx, b, RequestTypeEnum.POST);
|
||||
try {
|
||||
BundleUtil.sortEntriesIntoProcessingOrder(myFhirCtx, b);
|
||||
fail();
|
||||
} catch (IllegalStateException e ) {
|
||||
|
||||
//Null value indicates that we hit a cycle, and could not process the deletions in order
|
||||
assertThat(postBundleEntries, is(nullValue()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -164,21 +163,22 @@ public class TransactionHookTest extends BaseJpaR4SystemTest {
|
|||
organizationComponent.setResource(org1);
|
||||
organizationComponent.getRequest().setMethod(Bundle.HTTPVerb.DELETE).setUrl("Organization");
|
||||
|
||||
List<BundleEntryParts> postBundleEntries = BundleUtil.topologicalSort(myFhirCtx, b, RequestTypeEnum.DELETE);
|
||||
BundleUtil.sortEntriesIntoProcessingOrder(myFhirCtx, b);
|
||||
|
||||
assertThat(postBundleEntries, hasSize(4));
|
||||
assertThat(b.getEntry(), hasSize(4));
|
||||
|
||||
int observationIndex = getIndexOfEntryWithId("Observation/O1", postBundleEntries);
|
||||
int patientIndex = getIndexOfEntryWithId("Patient/P1", postBundleEntries);
|
||||
int organizationIndex = getIndexOfEntryWithId("Organization/Org1", postBundleEntries);
|
||||
int observationIndex = getIndexOfEntryWithId("Observation/O1", b);
|
||||
int patientIndex = getIndexOfEntryWithId("Patient/P1", b);
|
||||
int organizationIndex = getIndexOfEntryWithId("Organization/Org1", b);
|
||||
|
||||
assertTrue(patientIndex < organizationIndex);
|
||||
assertTrue(observationIndex < patientIndex);
|
||||
}
|
||||
|
||||
private int getIndexOfEntryWithId(String theResourceId, List<BundleEntryParts> theBundleEntryParts) {
|
||||
for (int i = 0; i < theBundleEntryParts.size(); i++) {
|
||||
String id = theBundleEntryParts.get(i).getResource().getIdElement().toUnqualifiedVersionless().toString();
|
||||
private int getIndexOfEntryWithId(String theResourceId, Bundle theBundle) {
|
||||
List<Bundle.BundleEntryComponent> entries = theBundle.getEntry();
|
||||
for (int i = 0; i < entries.size(); i++) {
|
||||
String id = entries.get(i).getResource().getIdElement().toUnqualifiedVersionless().toString();
|
||||
if (id.equals(theResourceId)) {
|
||||
return i;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue