Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Aphl 535 rebase to vsm operations #704

Merged
merged 4 commits into from
Apr 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package org.opencds.cqf.ruler.utility;

import java.util.List;
import java.util.Comparator;

public class SemanticVersion {
public static Comparator<String> getVersionComparator() {
return new VersionComparator();
}

public static String findHighestVersion(List<String> versions) {
String highestVersion = null;
Comparator<String> versionComparator = new VersionComparator();

for (String version : versions) {
if (highestVersion == null || versionComparator.compare(version, highestVersion) > 0) {
highestVersion = version;
}
}

return highestVersion;
}

public static class VersionComparator implements Comparator<String> {
@Override
public int compare(String v1, String v2) {
String[] v1Parts = v1.split("\\.");
String[] v2Parts = v2.split("\\.");

for (int i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
int num1 = i < v1Parts.length ? Integer.parseInt(v1Parts[i]) : 0;
int num2 = i < v2Parts.length ? Integer.parseInt(v2Parts[i]) : 0;

if (num1 != num2) {
return num1 - num2;
}
}

return 0;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
package org.opencds.cqf.ruler.cql;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import org.hl7.fhir.r4.model.ActivityDefinition;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.ContactDetail;
import org.hl7.fhir.r4.model.Library;
import org.hl7.fhir.r4.model.Measure;
import org.hl7.fhir.r4.model.MetadataResource;
import org.hl7.fhir.r4.model.PlanDefinition;
import org.hl7.fhir.r4.model.RelatedArtifact;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.ValueSet;
import org.opencds.cqf.ruler.utility.SemanticVersion;

public class KnowledgeArtifactAdapter<T extends MetadataResource> {
protected T resource;
Expand Down Expand Up @@ -262,4 +266,33 @@ public MetadataResource copy() {
//TODO: Is calling MetadataResource.copy() the right default behavior?
}
}

public static Optional<MetadataResource> findLatestVersion(List<MetadataResource> resources) {
Comparator<String> versionComparator = SemanticVersion.getVersionComparator();
MetadataResource latestResource = null;

for (MetadataResource resource : resources) {
String version = resource.getVersion();
if (latestResource == null || versionComparator.compare(version, latestResource.getVersion()) > 0) {
latestResource = resource;
}
}

return Optional.ofNullable(latestResource);
}

public static Optional<MetadataResource> findLatestVersion(Bundle bundle) {
List<Bundle.BundleEntryComponent> entries = bundle.getEntry();
List<MetadataResource> metadataResources = new ArrayList<>();

for (Bundle.BundleEntryComponent entry : entries) {
Resource resource = entry.getResource();
if (resource instanceof MetadataResource) {
MetadataResource metadataResource = (MetadataResource) resource;
metadataResources.add(metadataResource);
}
}

return findLatestVersion(metadataResources);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;

import org.apache.commons.lang3.NotImplementedException;
import org.cqframework.fhir.api.FhirDal;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent;
import org.hl7.fhir.r4.model.Bundle.BundleEntryRequestComponent;
import org.hl7.fhir.r4.model.CanonicalType;
import org.hl7.fhir.r4.model.CodeType;
import org.hl7.fhir.r4.model.ContactDetail;
Expand All @@ -25,6 +29,8 @@
import org.hl7.fhir.r4.model.MetadataResource;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.RelatedArtifact;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.UsageContext;
import org.jetbrains.annotations.Nullable;
import org.opencds.cqf.cql.engine.exception.InvalidOperatorArgument;
import org.opencds.cqf.cql.evaluator.fhir.util.Canonicals;
Expand All @@ -42,11 +48,28 @@
// TODO: This belongs in the Evaluator. Only included in Ruler at dev time for
// shorter cycle.
public class KnowledgeArtifactProcessor {
public static final String CPG_INFERENCEEXPRESSION = "http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-inferenceExpression";
public static final String CPG_ASSERTIONEXPRESSION = "http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-assertionExpression";
public static final String CPG_FEATUREEXPRESSION = "http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-featureExpression";
private BundleEntryComponent createEntry(IBaseResource theResource) {
return new Bundle.BundleEntryComponent()
.setResource((Resource) theResource)
.setRequest(createRequest(theResource));
}

private List<RelatedArtifact> finalRelatedArtifactList = new ArrayList<>();
private List<RelatedArtifact> finalRelatedArtifactListUpdated = new ArrayList<>();
private List<Bundle.BundleEntryComponent> bundleEntryComponentList = new ArrayList<>();

private BundleEntryRequestComponent createRequest(IBaseResource theResource) {
Bundle.BundleEntryRequestComponent request = new Bundle.BundleEntryRequestComponent();
if (theResource.getIdElement().hasValue() && !theResource.getIdElement().getValue().contains("urn:uuid")) {
request
.setMethod(Bundle.HTTPVerb.PUT)
.setUrl(theResource.getIdElement().getValue());
} else {
request
.setMethod(Bundle.HTTPVerb.POST)
.setUrl(theResource.fhirType());
}
return request;
}
private Bundle searchResourceByUrl(String url, FhirDal fhirDal) {
Map<String, List<List<IQueryParameterType>>> searchParams = new HashMap<>();

Expand Down Expand Up @@ -87,6 +110,15 @@ private Bundle searchResourceByUrlAndStatus(String url, String status, FhirDal f
return searchResultsBundle;
}

private MetadataResource retrieveResourcesByCanonical(String reference, FhirDal fhirDal) throws ResourceNotFoundException {
Bundle referencedResourceBundle = searchResourceByUrl(reference, fhirDal);
Optional<MetadataResource> referencedResource = KnowledgeArtifactAdapter.findLatestVersion(referencedResourceBundle);
if (referencedResource.isEmpty()) {
throw new ResourceNotFoundException(String.format("Resource for Canonical '%s' not found.", reference));
}
return referencedResource.get();
}

/* approve */
/*
* The operation sets the date and approvalDate elements of the approved
Expand Down Expand Up @@ -138,15 +170,100 @@ ArtifactAssessment createApprovalAssessment(IdType id, String artifactCommentTyp
}

/* $draft */
public MetadataResource draft(IdType idType, FhirDal fhirDal, String version) {
//TODO: Needs to be transactional
MetadataResource resource = (MetadataResource) fhirDal.read(idType);
if (resource == null) {
throw new ResourceNotFoundException(idType);
}
/*
* The operation creates a draft of the Base Artifact and
* related resources.
*
* This method generates the transaction bundle for this operation.
*
* This bundle consists of:
* 1. A new version of the base artifact where status is changed to
* draft and version changed to a new version number + "-draft"
*
* 2. New versions of related artifacts where status is changed to
* draft and version changed to a new version number + "-draft"
*
* Links and references between Bundle resources are updated to point to
* the new versions.
*/
public Bundle createDraftBundle(IdType baseArtifactId, FhirDal fhirDal, String version) throws ResourceNotFoundException, UnprocessableEntityException {
checkIfVersionIsValid(version);
MetadataResource baseArtifact = (MetadataResource) fhirDal.read(baseArtifactId);

if (version.contains(".")) {
if (baseArtifact == null) {
throw new ResourceNotFoundException(baseArtifactId);
}
String draftVersion = version + "-draft";
String draftVersionUrl = Canonicals.getUrl(baseArtifact.getUrl()) + "|" + draftVersion;

// Root artifact must have status of 'Active'. Existing drafts of
// reference artifacts with the right verison number will be adopted.
// This check is performed here to facilitate that different treatment
// for the root artifact and those referenced by it.
if (baseArtifact.getStatus() != Enumerations.PublicationStatus.ACTIVE) {
throw new UnprocessableEntityException(
String.format("Drafts can only be created from artifacts with status of 'active'. Resource '%s' has a status of: %s", baseArtifact.getUrl(), String.valueOf(baseArtifact.getStatus())));
}
// Ensure only one resource exists with this URL
Bundle existingArtifactsForUrl = searchResourceByUrl(draftVersionUrl, fhirDal);
if(existingArtifactsForUrl.getEntry().size() != 0){
throw new UnprocessableEntityException(
String.format("A draft of Program '%s' already exists with version: '%s'. Only one draft of a program version can exist at a time.", baseArtifact.getUrl(), draftVersionUrl));
}
List<MetadataResource> resourcesToCreate = createDraftsOfArtifactAndRelated(baseArtifact, fhirDal, version, new ArrayList<MetadataResource>());
Bundle transactionBundle = new Bundle()
.setType(Bundle.BundleType.TRANSACTION);
List<IdType> urnList = resourcesToCreate.stream().map(res -> new IdType("urn:uuid:" + UUID.randomUUID().toString())).collect(Collectors.toList());
for(int i = 0; i < resourcesToCreate.size(); i++){
KnowledgeArtifactAdapter<MetadataResource> newResourceAdapter = new KnowledgeArtifactAdapter<MetadataResource>(resourcesToCreate.get(i));
updateUsageContextReferencesWithUrns(resourcesToCreate.get(i), resourcesToCreate, urnList);
updateRelatedArtifactUrlsWithNewVersions(newResourceAdapter, draftVersion);
MetadataResource updateIdForBundle = newResourceAdapter.copy();
updateIdForBundle.setId(urnList.get(i));
transactionBundle.addEntry(createEntry(updateIdForBundle));
}
return transactionBundle;
}
private void updateUsageContextReferencesWithUrns(MetadataResource newResource, List<MetadataResource> resourceListWithOriginalIds, List<IdType> idListForTransactionBundle){
List<UsageContext> useContexts = newResource.getUseContext();
for(UsageContext useContext : useContexts){
if(useContext.hasValueReference()){
Reference useContextRef = useContext.getValueReference();
if(useContextRef != null){
resourceListWithOriginalIds.stream()
.filter(resource -> (resource.getClass().getSimpleName() + "/" + resource.getIdElement().getIdPart()).equals(useContextRef.getReference()))
.findAny()
.ifPresent(resource -> {
int indexOfDraftInIdList = resourceListWithOriginalIds.indexOf(resource);
useContext.setValue(new Reference(idListForTransactionBundle.get(indexOfDraftInIdList)));
});
}
}
}
}
private void updateRelatedArtifactUrlsWithNewVersions(KnowledgeArtifactAdapter<MetadataResource> newResourceAdapter, String draftVersion){
// For each Resource relatedArtifact, update the version of the reference.
newResourceAdapter.getRelatedArtifact().stream()
.filter(ra -> ra.hasResource()).collect(Collectors.toList())
.replaceAll(ra -> ra.setResource(Canonicals.getUrl(ra.getResource()) + "|" + draftVersion));
}
private void checkIfVersionIsValid(String version) throws UnprocessableEntityException{
if (version == null || version.isEmpty()) {
throw new UnprocessableEntityException("The version argument is required");
}
if(version.contains("draft")){
throw new UnprocessableEntityException("The version cannot contain 'draft'");
}
if(version.contains("/") || version.contains("\\") || version.contains("|")){
throw new UnprocessableEntityException("The version contains illegal characters");
}
if (!version.contains(".")) {
throw new UnprocessableEntityException("The version must be in the format MAJOR.MINOR.PATCH");
} else {
String[] versionParts = version.split("\\.");
if(versionParts.length != 3){
throw new UnprocessableEntityException("The version must be in the format MAJOR.MINOR.PATCH");
}
for(int i = 0; i < versionParts.length; i++) {
String section = "";
if(Integer.parseInt(versionParts[i]) < 0) {
Expand All @@ -157,81 +274,57 @@ public MetadataResource draft(IdType idType, FhirDal fhirDal, String version) {
} else if (i == 2) {
section = "Patch";
}
throw new IllegalArgumentException("The " + section + " version part should be greater than 0.");
throw new UnprocessableEntityException("The " + section + " version part should be greater than 0.");
}
}

}

// Root artifact must have status of 'Active'. Existing drafts of reference artifacts will be adopted. This check is
// performed here to facilitate that different treatment for the root artifact and those referenced by it.
if (resource.getStatus() != Enumerations.PublicationStatus.ACTIVE) {
throw new IllegalStateException(
String.format("Drafts can only be created from artifacts with status of 'active'. Resource '%s' has a status of: %s", resource.getUrl(), resource.getStatus().toString()));
}

Bundle existingArtifactsForUrl = searchResourceByUrl(resource.getUrl(), fhirDal);
Optional<Bundle.BundleEntryComponent> existingDrafts = existingArtifactsForUrl.getEntry().stream().filter(
e -> ((MetadataResource) e.getResource()).getStatus() == Enumerations.PublicationStatus.DRAFT).findFirst();

if (existingDrafts.isPresent()) {
throw new IllegalStateException(
String.format("A draft of Program '%s' already exists with ID: '%s'. Only one draft of a program can exist at a time.", resource.getUrl(), ((MetadataResource) existingDrafts.get().getResource()).getId()));
}

return internalDraft(resource, fhirDal, version);
}
private List<MetadataResource> createDraftsOfArtifactAndRelated(MetadataResource resourceToDraft, FhirDal fhirDal, String version, List<MetadataResource> resourcesToCreate) {
String draftVersion = version + "-draft";
String draftVersionUrl = Canonicals.getUrl(resourceToDraft.getUrl()) + "|" + draftVersion;

private MetadataResource internalDraft(MetadataResource resource, FhirDal fhirDal, String version) {
Bundle existingArtifactsForUrl = searchResourceByUrl(resource.getUrl(), fhirDal);
Optional<Bundle.BundleEntryComponent> existingDrafts = existingArtifactsForUrl.getEntry().stream().filter(
e -> ((MetadataResource) e.getResource()).getStatus() == Enumerations.PublicationStatus.DRAFT).findFirst();

// TODO: Decide if we need both of these checks
Optional<MetadataResource> existingArtifactsWithMatchingUrl = KnowledgeArtifactAdapter.findLatestVersion(searchResourceByUrl(draftVersionUrl, fhirDal));
Optional<MetadataResource> draftVersionAlreadyInBundle = resourcesToCreate.stream().filter(res -> res.getUrl().equals(Canonicals.getUrl(draftVersionUrl)) && res.getVersion().equals(draftVersion)).findAny();
MetadataResource newResource = null;
if (existingDrafts.isPresent()) {
newResource = (MetadataResource) existingDrafts.get().getResource();
if (existingArtifactsWithMatchingUrl.isPresent()) {
newResource = existingArtifactsWithMatchingUrl.get();
} else if(draftVersionAlreadyInBundle.isPresent()){
newResource = draftVersionAlreadyInBundle.get();
}

if (newResource == null) {
KnowledgeArtifactAdapter<MetadataResource> sourceResourceAdapter = new KnowledgeArtifactAdapter<>(resource);
KnowledgeArtifactAdapter<MetadataResource> sourceResourceAdapter = new KnowledgeArtifactAdapter<>(resourceToDraft);
newResource = sourceResourceAdapter.copy();
newResource.setStatus(Enumerations.PublicationStatus.DRAFT);
newResource.setId((String)null);
newResource.setVersion(null);
newResource.setVersion(version + "-draft");

KnowledgeArtifactAdapter<MetadataResource> newResourceAdapter = new KnowledgeArtifactAdapter<>(newResource);

// For each Resource relatedArtifact, strip the version of the reference.
newResourceAdapter.getRelatedArtifact().stream().filter(ra -> ra.hasResource()).collect(Collectors.toList())
.replaceAll(ra -> ra.setResource(Canonicals.getUrl(ra.getResource())));

fhirDal.create(newResource);

newResource.setVersion(draftVersion);
resourcesToCreate.add(newResource);
for (RelatedArtifact ra : sourceResourceAdapter.getRelatedArtifact()) {
// If it is a composed-of relation then do a deep copy, else shallow
// If it’s a compose-of then we want to copy it
// If it’s a depends-on, we just want to reference it, but not copy it
// (references are updated in createDraftBundle before adding to the bundle)
if (ra.getType() == RelatedArtifact.RelatedArtifactType.COMPOSEDOF) {
if (ra.hasUrl()) {
Bundle referencedResourceBundle = searchResourceByUrl(ra.getUrl(), fhirDal);
processReferencedResourceForDraft(fhirDal, referencedResourceBundle, ra, version);
processReferencedResourceForDraft(fhirDal, referencedResourceBundle, ra, version, resourcesToCreate);
} else if (ra.hasResource()) {
Bundle referencedResourceBundle = searchResourceByUrl(ra.getResourceElement().getValueAsString(), fhirDal);
processReferencedResourceForDraft(fhirDal, referencedResourceBundle, ra, version);
processReferencedResourceForDraft(fhirDal, referencedResourceBundle, ra, version, resourcesToCreate);
}
}
}
}

return newResource;
return resourcesToCreate;
}

private void processReferencedResourceForDraft(FhirDal fhirDal, Bundle referencedResourceBundle, RelatedArtifact ra, String version) {
private void processReferencedResourceForDraft(FhirDal fhirDal, Bundle referencedResourceBundle, RelatedArtifact ra, String version, List<MetadataResource> transactionBundle) {
if (!referencedResourceBundle.getEntryFirstRep().isEmpty()) {
Bundle.BundleEntryComponent referencedResourceEntry = referencedResourceBundle.getEntry().get(0);
if (referencedResourceEntry.hasResource() && referencedResourceEntry.getResource() instanceof MetadataResource) {
MetadataResource referencedResource = (MetadataResource) referencedResourceEntry.getResource();

internalDraft(referencedResource, fhirDal, version);
createDraftsOfArtifactAndRelated(referencedResource, fhirDal, version, transactionBundle);
}
}
}
Expand Down
Loading