Skip to content

Commit

Permalink
issue #3216 - support bundle reference resolution from FHIRPathResolve
Browse files Browse the repository at this point in the history
This commit also adds support for the `resource:` scheme in our Bundle
validation. This scheme is used in the SMART health cards spec and so I
think it makes sense to explicitly allow it.

This PR does *not* add support for reference rewriting for this
`resource:` scheme at present, but I think that is a logical follow-on
for issue #2512.

Signed-off-by: Lee Surprenant <lmsurpre@us.ibm.com>
  • Loading branch information
lmsurpre committed Feb 11, 2022
1 parent dae4416 commit 741586b
Show file tree
Hide file tree
Showing 6 changed files with 280 additions and 119 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* (C) Copyright IBM Corp. 2020, 2021
* (C) Copyright IBM Corp. 2020, 2022
*
* SPDX-License-Identifier: Apache-2.0
*/
Expand All @@ -23,13 +23,13 @@
import com.ibm.fhir.model.resource.Bundle;
import com.ibm.fhir.model.resource.Bundle.Entry;
import com.ibm.fhir.model.resource.Bundle.Entry.Request;
import com.ibm.fhir.model.resource.Resource;
import com.ibm.fhir.model.type.Uri;
import com.ibm.fhir.model.type.Url;
import com.ibm.fhir.model.type.code.BundleType;
import com.ibm.fhir.model.type.code.HTTPVerb;
import com.ibm.fhir.model.util.ModelSupport;
import com.ibm.fhir.model.util.ReferenceMappingVisitor;
import com.ibm.fhir.model.resource.Resource;


/**
Expand All @@ -40,7 +40,7 @@
public class BundleBreakerResourceProcessor implements IResourceEntryProcessor {
private static final Logger logger = Logger.getLogger(BundleBreakerResourceProcessor.class.getName());
private static final String LOCAL_REF_PREFIX = "urn:";

// to write the processed bundles back to COS
private final COSClient cosClient;

Expand All @@ -49,7 +49,7 @@ public class BundleBreakerResourceProcessor implements IResourceEntryProcessor {

// The COS bucket we want to use to store the resource bundle fragments
private final String targetBucket;

// The COS key prefix where we want to store everything
private final String targetPrefix;

Expand Down Expand Up @@ -82,7 +82,7 @@ public void process(ResourceEntry re) {
re.getJob().operationComplete(success);
}
}

/**
* See fhir-persistence-jdbc TimestampPrefixedUUID.
* TODO refactor to use a common class without having to
Expand All @@ -93,19 +93,19 @@ private String createNewIdentityValue() {
// It's OK to use milli-time here. It doesn't matter too much if the time changes
// because we're not using the timestamp to determine uniqueness in any way. The
// timestamp prefix is purely to help push index writes to the right hand side
// of the btree, minimizing the number of physical reads likely required
// of the btree, minimizing the number of physical reads likely required
// during ingestion when an index is too large to be fully cached.
long millis = System.currentTimeMillis();

// String encoding. Needs to collate correctly, so don't use any
// byte-based encoding which would be sensitive to endian issues. For simplicity,
// hex is sufficient, although a custom encoding using the full character set
// supported by FHIR identifiers would be a little more compact (== smaller indexes).
// Do not use Base64.
String prefix = Long.toHexString(millis);

UUID uuid = UUID.randomUUID();

StringBuilder result = new StringBuilder();
result.append(prefix);
result.append("-"); // redundant, but more visually appealing.
Expand Down Expand Up @@ -137,7 +137,7 @@ protected void process(String originalName, Bundle bundle) {
// identifiers first, before we make the second pass and
// generate the individual bundles with external identifiers
for (Entry requestEntry: bundle.getEntry()) {

String localIdentifier = retrieveLocalIdentifier(requestEntry);
Resource resource = requestEntry.getResource();
if (localIdentifier != null) {
Expand All @@ -149,7 +149,7 @@ protected void process(String originalName, Bundle bundle) {
// be used for a PUT in the new bundle
String logicalId = createNewIdentityValue();
String externalRef = ModelSupport.getTypeName(resource.getClass()) + "/" + logicalId;

if (logger.isLoggable(Level.FINE)) {
logger.fine("Creating POST-to-PUT mapping " + localIdentifier + " --> " + externalRef);
}
Expand All @@ -167,7 +167,7 @@ protected void process(String originalName, Bundle bundle) {

// Update any references internal to the resource
Resource resource = requestEntry.getResource();

String localId = retrieveLocalIdentifier(requestEntry);
if (logger.isLoggable(Level.FINE)) {
logger.fine("Processing localId: " + localId);
Expand All @@ -181,7 +181,7 @@ protected void process(String originalName, Bundle bundle) {
new ReferenceMappingVisitor<Resource>(localRefMap, localId);
resource.accept(visitor);
resource = visitor.getResult();

// Make sure the fullUrl matches the resource id
String url = ModelSupport.getTypeName(resource.getClass()) + "/" + resource.getId();
entryBuilder = Entry.builder();
Expand All @@ -198,7 +198,7 @@ protected void process(String originalName, Bundle bundle) {
IdReferenceMappingVisitor visitor = new IdReferenceMappingVisitor(localRefMap, localId, newId);
resource.accept(visitor);
resource = visitor.getResult();

entryBuilder = Entry.builder();
entryBuilder.resource(resource);
entryBuilder.fullUrl(Uri.of(externalId));
Expand All @@ -220,11 +220,11 @@ protected void process(String originalName, Bundle bundle) {
entryBuilder.fullUrl(Uri.of(localId));
entryBuilder.request(Request.builder().method(HTTPVerb.DELETE).url(Url.of(localId)).build());
}

// If we built a new entry, add it to the current output bundle
if (entryBuilder != null) {
bb.entry(entryBuilder.build());

// Each time we hit the max number of resources we want in a bundle,
// save it off and start a new one
if (++resourceCount == this.maxBundleSize) {
Expand All @@ -235,7 +235,7 @@ protected void process(String originalName, Bundle bundle) {
}
}
}

// process the final chunk of resources
if (resourceCount > 0) {
saveBundle(originalName, bb.build(), bundleCount++);
Expand Down Expand Up @@ -264,12 +264,12 @@ protected void saveBundle(String originalName, Bundle newBundle, int bundleNumbe
if (posn < 0) {
posn = originalName.lastIndexOf(".JSON");
}

if (posn < 0) {
logger.warning("Invalid bundle name: " + originalName);
throw new IllegalArgumentException("Expecting bundle name to end in .json or .JSON");
}

String bundleName;
if (bundleNumber < 0) {
// Bundle isn't modified, so just use the original name under
Expand All @@ -280,7 +280,7 @@ protected void saveBundle(String originalName, Bundle newBundle, int bundleNumbe
// and put it under the new prefix
bundleName = String.format("%s/%s_SUB%03d.json", this.targetPrefix, originalName.substring(0, posn), bundleNumber);
}

writeBundle(newBundle, bundleName);
}

Expand All @@ -300,7 +300,7 @@ protected void writeBundle(Bundle bundle, String bundleName) {
final String payload = new String(os.toByteArray(), StandardCharsets.UTF_8);
cosClient.write(targetBucket, bundleName, payload);
}

/**
* This method will retrieve the local identifier associated with the specified bundle request entry, or return null
* if the fullUrl field is not specified or doesn't contain a local identifier.
Expand All @@ -312,7 +312,7 @@ protected void writeBundle(Bundle bundle, String bundleName) {
* @return
*/
private String retrieveLocalIdentifier(Bundle.Entry bundleEntry) {

String localIdentifier = null;
if (bundleEntry.getFullUrl() != null) {
String fullUrl = bundleEntry.getFullUrl().getValue();
Expand All @@ -335,13 +335,12 @@ private String retrieveLocalIdentifier(Bundle.Entry bundleEntry) {
* @param resource
* the resource for which an external identifier will be built
*/
private void addLocalRefMapping(Map<String, String> localRefMap, String localIdentifier,
Resource resource) {
private void addLocalRefMapping(Map<String, String> localRefMap, String localIdentifier, Resource resource) {
if (localIdentifier != null) {
String externalIdentifier =
ModelSupport.getTypeName(resource.getClass()) + "/" + resource.getId();
localRefMap.put(localIdentifier, externalIdentifier);

if (logger.isLoggable(Level.FINER)) {
logger.finer("Added local/ext identifier mapping: " + localIdentifier + " --> "
+ externalIdentifier);
Expand Down
11 changes: 7 additions & 4 deletions fhir-model/src/main/java/com/ibm/fhir/model/util/FHIRUtil.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* (C) Copyright IBM Corp. 2016, 2021
* (C) Copyright IBM Corp. 2016, 2022
*
* SPDX-License-Identifier: Apache-2.0
*/
Expand Down Expand Up @@ -281,7 +281,7 @@ public static URI buildLocationURI(String type, Resource resource) {
* Builds a relative "Location" header value for the specified resource type/id/version. This will be a string of the form
* <code>"[resource-type]/[id]/_history/[version]"</code>. Note that the server will turn this into an absolute URL prior to
* returning it to the client.
*
*
* @param type the resource type name
* @param id the resource logical id value
* @param version the resource version
Expand Down Expand Up @@ -621,9 +621,12 @@ public static Bundle createStandaloneBundle(BundleType bundleType, Map<String,Re
* Build the reference {@code reference} based on the {@code fullUrlString} value.
*
* @see https://www.hl7.org/fhir/r4/bundle.html#references
* @param reference
* @param ref
* @param fullUrlString
* @throws URISyntaxException
* @return
* An absolute fullUrl string for the literal value of Reference {@code ref},
* or the passed literal value in Reference {@code ref} if it cannot be parsed,
* or null if {@code ref} has no literal value
*/
public static String buildBundleReference(Reference ref, String fullUrlString) {
String referenceUriString = null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* (C) Copyright IBM Corp. 2019, 2021
* (C) Copyright IBM Corp. 2019, 2022
*
* SPDX-License-Identifier: Apache-2.0
*/
Expand Down Expand Up @@ -55,7 +55,8 @@ public final class ValidationSupport {
private static final int RESOURCE_TYPE_GROUP = 4;
private static final int MIN_STRING_LENGTH = 1;
private static final int MAX_STRING_LENGTH = 1048576; // 1024 * 1024 = 1MB
private static final String LOCAL_REF_PREFIX = "urn:";
private static final String LOCAL_REF_URN_PREFIX = "urn:";
private static final String LOCAL_REF_RESOURCE_PREFIX = "resource:";
private static final String HTTP_PREFIX = "http:";
private static final String HTTPS_PREFIX = "https:";
private static final String FHIR_XHTML_XSD = "fhir-xhtml.xsd";
Expand Down Expand Up @@ -751,8 +752,11 @@ public static void checkReferenceType(Reference reference, String elementName, S
String referenceReference = getReferenceReference(reference);
List<String> referenceTypeList = Arrays.asList(referenceTypes);

if (referenceReference != null && !referenceReference.startsWith("#") && !referenceReference.startsWith(LOCAL_REF_PREFIX)
&& !referenceReference.startsWith(HTTP_PREFIX) && !referenceReference.startsWith(HTTPS_PREFIX)) {
if (referenceReference != null && !referenceReference.startsWith("#")
&& !referenceReference.startsWith(LOCAL_REF_URN_PREFIX)
&& !referenceReference.startsWith(LOCAL_REF_RESOURCE_PREFIX)
&& !referenceReference.startsWith(HTTP_PREFIX)
&& !referenceReference.startsWith(HTTPS_PREFIX)) {
int index = referenceReference.indexOf("?");
if (index != -1) {
// conditional reference
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* (C) Copyright IBM Corp. 2019, 2021
* (C) Copyright IBM Corp. 2019, 2022
*
* SPDX-License-Identifier: Apache-2.0
*/
Expand All @@ -12,18 +12,24 @@

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;

import com.ibm.fhir.model.resource.Bundle;
import com.ibm.fhir.model.resource.Bundle.Entry;
import com.ibm.fhir.model.resource.Resource;
import com.ibm.fhir.model.type.Reference;
import com.ibm.fhir.model.type.code.IssueSeverity;
import com.ibm.fhir.model.type.code.IssueType;
import com.ibm.fhir.model.util.FHIRUtil;
import com.ibm.fhir.path.FHIRPathNode;
import com.ibm.fhir.path.FHIRPathResourceNode;
import com.ibm.fhir.path.FHIRPathTree;
import com.ibm.fhir.path.FHIRPathType;
import com.ibm.fhir.path.evaluator.FHIRPathEvaluator.EvaluationContext;
import com.ibm.fhir.path.util.FHIRPathUtil;

public class ResolveFunction extends FHIRPathAbstractFunction {
private static final int BASE_URL_GROUP = 1;
Expand Down Expand Up @@ -72,6 +78,17 @@ public int getMaxArity() {
@Override
public Collection<FHIRPathNode> apply(EvaluationContext evaluationContext, Collection<FHIRPathNode> context, List<Collection<FHIRPathNode>> arguments) {
Collection<FHIRPathNode> result = new ArrayList<>();
FHIRPathNode root = FHIRPathUtil.getSingleton(evaluationContext.getExternalConstant("rootResource"));
boolean isBundleContext = root != null && root.type() == FHIRPathType.FHIR_BUNDLE;
Map<String, Resource> bundleResources = new HashMap<>();
if (isBundleContext) {
Bundle bundle = root.asResourceNode().resource().as(Bundle.class);
for (Entry e : bundle.getEntry()) {
if (e.getResource() != null && e.getFullUrl() != null && e.getFullUrl().hasValue()) {
bundleResources.put(e.getFullUrl().getValue(), e.getResource());
}
}
}
for (FHIRPathNode node : context) {
if (node.isElementNode() && node.asElementNode().element().is(Reference.class)) {
Reference reference = node.asElementNode().element().as(Reference.class);
Expand All @@ -83,6 +100,7 @@ public Collection<FHIRPathNode> apply(EvaluationContext evaluationContext, Colle
Resource resource = null;
FHIRPathResourceNode resourceNode = null;

// if a literal reference
if (referenceReference != null) {
if (referenceReference.startsWith("#")) {
// internal fragment reference
Expand All @@ -94,16 +112,49 @@ public Collection<FHIRPathNode> apply(EvaluationContext evaluationContext, Colle
}
}
} else {
Matcher matcher = REFERENCE_PATTERN.matcher(referenceReference);
if (matcher.matches()) {
resourceType = matcher.group(RESOURCE_TYPE_GROUP);
if (referenceType != null && !resourceType.equals(referenceType)) {
throw new IllegalArgumentException("Resource type found in reference URL does not match reference type");
if (isBundleContext) {
// we know the root of the tree is a Bundle and the current node is of type Reference,
// so walk up the tree until we get to highest Bundle.entry.fullUrl in the tree
// and save the value of its fullUrl
String fullUrl = null;
FHIRPathTree tree = evaluationContext.getTree();
FHIRPathNode nodeUnderTest = tree.getParent(node);
while (nodeUnderTest != root) {
if (nodeUnderTest.isResourceNode()) {
FHIRPathNode sibling = tree.getSibling(nodeUnderTest, "fullUrl");
fullUrl = sibling.getValue().asStringValue().string();
}
nodeUnderTest = tree.getParent(nodeUnderTest);
}
String baseUrl = matcher.group(BASE_URL_GROUP);
if ((baseUrl == null || matchesServiceBaseUrl(baseUrl)) && evaluationContext.resolveRelativeReferences()) {
// relative reference
resource = resolveRelativeReference(evaluationContext, node, resourceType, matcher.group(LOGICAL_ID_GROUP), matcher.group(VERSION_ID_GROUP));

String bundleReference = FHIRUtil.buildBundleReference(reference, fullUrl);
if (bundleReference != null) {
resource = bundleResources.get(bundleReference);
if (resource != null) {
resourceType = resource.getClass().getSimpleName();
}
}
}

if (resource == null) {
Matcher matcher = REFERENCE_PATTERN.matcher(referenceReference);
if (matcher.matches()) {
if (resourceType == null) {
resourceType = matcher.group(RESOURCE_TYPE_GROUP);
}

if (referenceType != null && !resourceType.equals(referenceType)) {
throw new IllegalArgumentException("Resource type found in reference URL does not match reference type");
}

// if we're not in a bundle or the target resource wasn't found in the bundle
if (resource == null) {
String baseUrl = matcher.group(BASE_URL_GROUP);
if ((baseUrl == null || matchesServiceBaseUrl(baseUrl)) && evaluationContext.resolveRelativeReferences()) {
// relative reference
resource = resolveRelativeReference(evaluationContext, node, resourceType, matcher.group(LOGICAL_ID_GROUP), matcher.group(VERSION_ID_GROUP));
}
}
}
}
}
Expand Down
Loading

0 comments on commit 741586b

Please sign in to comment.