Skip to content
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
8 changes: 4 additions & 4 deletions DotNet/Terminology/Controllers/FhirController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,12 @@ public ActionResult<ValueSet> GetValueSetById([FromRoute] string id)
/// nor <paramref name="summary"/> are provided.
/// </returns>
[HttpGet("ValueSet")]
public ActionResult<Bundle> GetValueSets([FromQuery] string url,
public ActionResult<Bundle> GetValueSets([FromQuery] string? url,
[FromQuery(Name = "_summary")] SummaryType? summary)
{
try
{
return Ok(fhirService.GetValueSets(url.Sanitize(), summary));
return Ok(fhirService.GetValueSets(url?.Sanitize(), summary));
}
catch (ArgumentException ex)
{
Expand Down Expand Up @@ -156,11 +156,11 @@ public ActionResult<CodeSystem> GetCodeSystemById([FromRoute] string id)
/// nor <paramref name="summary"/> is provided.
/// </returns>
[HttpGet("CodeSystem")]
public ActionResult<Bundle> GetCodeSystems([FromQuery] string url, [FromQuery(Name = "_summary")] SummaryType? summary)
public ActionResult<Bundle> GetCodeSystems([FromQuery] string? url, [FromQuery(Name = "_summary")] SummaryType? summary)
{
try
{
return Ok(fhirService.GetCodeSystems(url.Sanitize(), summary));
return Ok(fhirService.GetCodeSystems(url?.Sanitize(), summary));
}
catch (ArgumentException ex)
{
Expand Down
8 changes: 4 additions & 4 deletions DotNet/Terminology/Services/FhirService.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using Hl7.Fhir.Model;
using LantanaGroup.Link.Terminology.Application.Models;
using Amazon.Runtime.Internal;
using Hl7.Fhir.Model;
using Hl7.Fhir.Rest;
using LantanaGroup.Link.Shared.Application.Services.Security;
using Amazon.Runtime.Internal;
using LantanaGroup.Link.Terminology.Application.Models;

namespace LantanaGroup.Link.Terminology.Services;

Expand Down Expand Up @@ -165,7 +165,7 @@ public CodeSystem GetCodeSystemById(string id)
return codeGroup.Resource as CodeSystem;
}

public Bundle GetCodeSystems(string url, SummaryType? summary)
public Bundle GetCodeSystems(string? url, SummaryType? summary)
{
if (string.IsNullOrEmpty(url) && (summary == null))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
import ca.uhn.fhir.context.FhirContext;
import com.fasterxml.jackson.annotation.JsonView;
import com.lantanagroup.link.measureeval.entities.MeasureDefinition;
import com.lantanagroup.link.measureeval.models.RelatedArtifactInfo;
import com.lantanagroup.link.measureeval.repositories.MeasureDefinitionRepository;
import com.lantanagroup.link.measureeval.services.MeasureDefinitionBundleValidator;
import com.lantanagroup.link.measureeval.services.MeasureEvaluator;
import com.lantanagroup.link.measureeval.services.MeasureEvaluatorCache;
import com.lantanagroup.link.measureeval.services.MeasureValidationService;
import com.lantanagroup.link.measureeval.utils.CqlUtils;
import com.lantanagroup.link.shared.auth.PrincipalUser;
import com.lantanagroup.link.shared.serdes.Views;
Expand Down Expand Up @@ -38,6 +40,7 @@ public class MeasureDefinitionController {
private final MeasureDefinitionRepository repository;
private final MeasureDefinitionBundleValidator bundleValidator;
private final MeasureEvaluatorCache evaluatorCache;
private final MeasureValidationService validationService;

final String[] DISALLOWED_FIELDS = new String[]{};
@InitBinder
Expand All @@ -48,10 +51,12 @@ public void initBinder(WebDataBinder binder) {
public MeasureDefinitionController(
MeasureDefinitionRepository repository,
MeasureDefinitionBundleValidator bundleValidator,
MeasureEvaluatorCache evaluatorCache){
MeasureEvaluatorCache evaluatorCache,
MeasureValidationService validationService){
this.repository = repository;
this.bundleValidator = bundleValidator;
this.evaluatorCache = evaluatorCache;
this.validationService = validationService;
}

@GetMapping
Expand Down Expand Up @@ -158,4 +163,11 @@ public MeasureReport evaluate(@AuthenticationPrincipal PrincipalUser user, @Path
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage(), e);
}
}

@GetMapping("/{id}/relatedArtifact")
@Operation(summary = "Get related artifacts for a measure definition", tags = {"Measure Definitions"})
@Parameter(name = "id", description = "The ID of the measure definition", required = true)
public List<RelatedArtifactInfo> getRelatedArtifacts(@PathVariable String id) {
return validationService.getRelatedArtifacts(id);
}
Comment thread
seanmcilvenna marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.lantanagroup.link.measureeval.models;

import lombok.Getter;
import lombok.Setter;

import java.util.HashMap;
import java.util.Map;

@Getter
@Setter
public class RelatedArtifactInfo {
private String name;
private String url;
private boolean found;
private Map<String, String> libraries = new HashMap<>();
private String version;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package com.lantanagroup.link.measureeval.services;

import com.lantanagroup.link.measureeval.entities.MeasureDefinition;
import com.lantanagroup.link.measureeval.models.RelatedArtifactInfo;
import com.lantanagroup.link.measureeval.repositories.MeasureDefinitionRepository;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Library;
import org.hl7.fhir.r4.model.RelatedArtifact;
import org.hl7.fhir.r4.model.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@Service
public class MeasureValidationService {
private static final Logger logger = LoggerFactory.getLogger(MeasureValidationService.class);
private static final List<String> IGNORED_ARTIFACT_URL_PREFIXES = List.of(
"http://fhir.org/guides/cqf/"
);
private final MeasureDefinitionRepository repository;

public MeasureValidationService(MeasureDefinitionRepository repository) {
this.repository = repository;
}

/**
* Retrieves related artifact information for a measure, indicating if each artifact is present in the
* measure definition, and where the artifact is used by other libraries.
*/
public List<RelatedArtifactInfo> getRelatedArtifacts(String id) {
MeasureDefinition measureDefinition = repository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Measure definition not found"));

Bundle bundle = measureDefinition.getBundle();
if (bundle == null) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Measure definition does not contain a bundle");
}

Set<String> bundleResourceUrls = new HashSet<>();

// Collects URLs of resources within the bundle
for (Bundle.BundleEntryComponent entry : bundle.getEntry()) {
Resource resource = entry.getResource();
if (resource == null) continue;

// Extract canonical URL if available
String url = getResourceUrl(resource);
if (url != null) {
bundleResourceUrls.add(url);
}
}

logger.debug("Found {} resources in measure bundle", bundleResourceUrls.size());

List<RelatedArtifactInfo> relatedArtifactInfos = new ArrayList<>();

for (Bundle.BundleEntryComponent entry : bundle.getEntry()) {
if (entry.getResource() instanceof Library library) {
for (RelatedArtifact relatedArtifact : library.getRelatedArtifact()) {
// Processes related artifacts to extract dependency information
if (relatedArtifact.getType() == RelatedArtifact.RelatedArtifactType.DEPENDSON) {
String artifactUrl = relatedArtifact.getResource();
if (artifactUrl == null || artifactUrl.isEmpty()) {
continue;
}

// Skip artifacts with ignored URL prefixes
if (IGNORED_ARTIFACT_URL_PREFIXES.stream().anyMatch(artifactUrl::startsWith)) {
continue;
}

// Extract version from URL if present (format: url|version)
String baseUrl = artifactUrl;
String version = null;
if (artifactUrl.contains("|")) {
String[] parts = artifactUrl.split("\\|", 2);
baseUrl = parts[0];
version = parts[1];
}

// Check if the artifact exists in the bundle
boolean found = bundleResourceUrls.contains(artifactUrl);

String finalBaseUrl = baseUrl;
String finalVersion = version;
// Finds or creates artifact info; populates if needed
RelatedArtifactInfo info = relatedArtifactInfos.stream()
.filter(i -> finalBaseUrl.equals(i.getUrl()))
.findFirst()
.orElseGet(() -> {
RelatedArtifactInfo newInfo = new RelatedArtifactInfo();
newInfo.setName(relatedArtifact.getDisplay());
newInfo.setUrl(finalBaseUrl);
newInfo.setVersion(finalVersion);
newInfo.setFound(found);
relatedArtifactInfos.add(newInfo);
return newInfo;
});

String libUrl = library.getUrl();
String libName = library.getName();
if (libUrl != null) {
info.getLibraries().put(libUrl, libName);
}
}
}
}
}

logger.debug("Found {} related artifacts in measure definition {}", relatedArtifactInfos.size(), id);

int notFoundCount = relatedArtifactInfos.stream()
.filter(info -> !info.isFound())
.mapToInt(info -> 1)
.sum();

logger.debug("Found {} artifacts not present in measure bundle {}", notFoundCount, id);

return relatedArtifactInfos;
}

/**
* Extracts resource URL, appending version if present
*/
private String getResourceUrl(Resource resource) {
if (resource instanceof org.hl7.fhir.r4.model.MetadataResource metadataResource) {
String url = metadataResource.getUrl();
String version = metadataResource.getVersion();
if (url != null && version != null && !version.isEmpty()) {
return url + "|" + version;
}
return url;
}
return null;
}
}
Loading
Loading