Skip to content

Commit e2b1fca

Browse files
c-schulerbarhodes
andauthored
Implementing $release-manifest operation for VSP testing (#991)
* This was throwing an NPE for urls like urn:ietf:bcp:47 * Implementing $release-manifest operation for VSP testing * Add ValueSet compose chain walking to $data-requirements key element analysis * Testing and clean up * Clean up expansion parameter logic for $release-manifest * Update docs * Addressing sonar issue * Update cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/visitor/ReleaseManifestVisitor.java Co-authored-by: Brenin Rhodes <brenin@alphora.com> * Update cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/visitor/ValueSetComposeWalker.java Co-authored-by: Brenin Rhodes <brenin@alphora.com> * Applying feedback --------- Co-authored-by: Brenin Rhodes <brenin@alphora.com>
1 parent 92abf9c commit e2b1fca

17 files changed

Lines changed: 1448 additions & 17 deletions

File tree

cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/r4/CrR4Config.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.opencds.cqf.fhir.cr.crmi.R4DraftService;
1111
import org.opencds.cqf.fhir.cr.crmi.R4InferManifestParametersService;
1212
import org.opencds.cqf.fhir.cr.crmi.R4PackageService;
13+
import org.opencds.cqf.fhir.cr.crmi.R4ReleaseManifestService;
1314
import org.opencds.cqf.fhir.cr.crmi.R4ReleaseService;
1415
import org.opencds.cqf.fhir.cr.ecr.r4.R4ERSDTransformService;
1516
import org.opencds.cqf.fhir.cr.hapi.common.StringTimePeriodHandler;
@@ -24,6 +25,7 @@
2425
import org.opencds.cqf.fhir.cr.hapi.r4.IERSDV2ImportServiceFactory;
2526
import org.opencds.cqf.fhir.cr.hapi.r4.IInferManifestParametersServiceFactory;
2627
import org.opencds.cqf.fhir.cr.hapi.r4.IPackageServiceFactory;
28+
import org.opencds.cqf.fhir.cr.hapi.r4.IReleaseManifestServiceFactory;
2729
import org.opencds.cqf.fhir.cr.hapi.r4.IReleaseServiceFactory;
2830
import org.opencds.cqf.fhir.cr.hapi.r4.ISubmitDataProcessorFactory;
2931
import org.opencds.cqf.fhir.cr.hapi.r4.R4MeasureEvaluatorMultipleFactory;
@@ -33,6 +35,7 @@
3335
import org.opencds.cqf.fhir.cr.hapi.r4.crmi.DraftProvider;
3436
import org.opencds.cqf.fhir.cr.hapi.r4.crmi.InferManifestParametersProvider;
3537
import org.opencds.cqf.fhir.cr.hapi.r4.crmi.PackageProvider;
38+
import org.opencds.cqf.fhir.cr.hapi.r4.crmi.ReleaseManifestProvider;
3639
import org.opencds.cqf.fhir.cr.hapi.r4.crmi.ReleaseProvider;
3740
import org.opencds.cqf.fhir.cr.hapi.r4.ecr.ERSDTransformProvider;
3841
import org.opencds.cqf.fhir.cr.hapi.r4.measure.CareGapsOperationProvider;
@@ -201,6 +204,16 @@ ReleaseProvider r4ReleaseProvider(IReleaseServiceFactory r4ReleaseServiceFactory
201204
return new ReleaseProvider(r4ReleaseServiceFactory);
202205
}
203206

207+
@Bean
208+
IReleaseManifestServiceFactory releaseManifestServiceFactory(IRepositoryFactory repositoryFactory) {
209+
return rd -> new R4ReleaseManifestService(repositoryFactory.create(rd));
210+
}
211+
212+
@Bean
213+
ReleaseManifestProvider r4ReleaseManifestProvider(IReleaseManifestServiceFactory r4ReleaseManifestServiceFactory) {
214+
return new ReleaseManifestProvider(r4ReleaseManifestServiceFactory);
215+
}
216+
204217
@Bean
205218
IInferManifestParametersServiceFactory inferManifestParametersServiceFactory(IRepositoryFactory repositoryFactory) {
206219
return rd -> new R4InferManifestParametersService(repositoryFactory.create(rd));
@@ -245,6 +258,7 @@ public ProviderLoader r4PdLoader(
245258
ERSDTransformProvider.class,
246259
PackageProvider.class,
247260
ReleaseProvider.class,
261+
ReleaseManifestProvider.class,
248262
InferManifestParametersProvider.class)));
249263

250264
return new ProviderLoader(restfulServer, applicationContext, selector);
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.opencds.cqf.fhir.cr.hapi.r4;
2+
3+
import ca.uhn.fhir.rest.api.server.RequestDetails;
4+
import org.opencds.cqf.fhir.cr.crmi.R4ReleaseManifestService;
5+
6+
@FunctionalInterface
7+
public interface IReleaseManifestServiceFactory {
8+
R4ReleaseManifestService create(RequestDetails requestDetails);
9+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package org.opencds.cqf.fhir.cr.hapi.r4.crmi;
2+
3+
import static org.opencds.cqf.fhir.cr.hapi.common.ParameterHelper.getStringValue;
4+
import static org.opencds.cqf.fhir.utility.Constants.CRMI_OPERATION_RELEASE_MANIFEST;
5+
import static org.opencds.cqf.fhir.utility.EndpointHelper.getEndpoint;
6+
7+
import ca.uhn.fhir.context.FhirVersionEnum;
8+
import ca.uhn.fhir.model.api.annotation.Description;
9+
import ca.uhn.fhir.rest.annotation.IdParam;
10+
import ca.uhn.fhir.rest.annotation.Operation;
11+
import ca.uhn.fhir.rest.annotation.OperationParam;
12+
import ca.uhn.fhir.rest.api.server.RequestDetails;
13+
import org.hl7.fhir.exceptions.FHIRException;
14+
import org.hl7.fhir.instance.model.api.IPrimitiveType;
15+
import org.hl7.fhir.r4.model.Bundle;
16+
import org.hl7.fhir.r4.model.CodeType;
17+
import org.hl7.fhir.r4.model.Endpoint;
18+
import org.hl7.fhir.r4.model.IdType;
19+
import org.hl7.fhir.r4.model.Library;
20+
import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
21+
import org.hl7.fhir.r4.model.StringType;
22+
import org.opencds.cqf.fhir.cr.hapi.r4.IReleaseManifestServiceFactory;
23+
24+
public class ReleaseManifestProvider {
25+
26+
private final IReleaseManifestServiceFactory serviceFactory;
27+
private final FhirVersionEnum fhirVersion;
28+
29+
public ReleaseManifestProvider(IReleaseManifestServiceFactory serviceFactory) {
30+
this.serviceFactory = serviceFactory;
31+
fhirVersion = FhirVersionEnum.R4;
32+
}
33+
34+
/**
35+
* The $release-manifest operation releases a manifest Library (asset-collection) that has
36+
* pre-computed depends-on entries. Unlike $release, this operation does not re-discover
37+
* dependencies through component traversal. Instead, it resolves unversioned dependency
38+
* references using the terminology endpoint and updates manifest metadata for release.
39+
* <p>
40+
* This operation is designed to be used as part of the following workflow:
41+
* <ol>
42+
* <li>{@code ImplementationGuide/$data-requirements} — analyzes an IG and produces a
43+
* module-definition Library with all dependencies, including key element classification
44+
* and ValueSet compose chain walking</li>
45+
* <li>{@code Library/$infer-manifest-parameters} — converts the module-definition Library
46+
* into an asset-collection manifest with expansion parameters and depends-on entries</li>
47+
* <li>{@code Library/$release-manifest} — releases the manifest by resolving unversioned
48+
* dependencies via the terminology endpoint and updating metadata (version, status, date)</li>
49+
* </ol>
50+
* <p>
51+
* The {@code terminologyEndpoint} parameter should provide an {@link org.hl7.fhir.r4.model.Endpoint}
52+
* with authentication headers for resolving terminology resources (e.g., VSAC). Set
53+
* {@code latestFromTxServer=true} to enable terminology server resolution of unversioned
54+
* ValueSet and CodeSystem references.
55+
*/
56+
@Operation(name = CRMI_OPERATION_RELEASE_MANIFEST, idempotent = true, type = Library.class)
57+
@Description(
58+
shortDefinition = CRMI_OPERATION_RELEASE_MANIFEST,
59+
value = "Release a manifest Library with pre-computed dependencies")
60+
public Bundle releaseManifestOperation(
61+
@IdParam IdType id,
62+
@OperationParam(name = "version") StringType version,
63+
@OperationParam(name = "versionBehavior") CodeType versionBehavior,
64+
@OperationParam(name = "latestFromTxServer", typeName = "Boolean")
65+
IPrimitiveType<Boolean> latestFromTxServer,
66+
@OperationParam(name = "terminologyEndpoint") ParametersParameterComponent terminologyEndpoint,
67+
@OperationParam(name = "releaseLabel") StringType releaseLabel,
68+
RequestDetails requestDetails)
69+
throws FHIRException {
70+
return serviceFactory
71+
.create(requestDetails)
72+
.releaseManifest(
73+
id,
74+
getStringValue(version),
75+
versionBehavior,
76+
latestFromTxServer,
77+
(Endpoint) getEndpoint(fhirVersion, terminologyEndpoint),
78+
getStringValue(releaseLabel));
79+
}
80+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package org.opencds.cqf.fhir.cr.crmi;
2+
3+
import ca.uhn.fhir.context.FhirVersionEnum;
4+
import ca.uhn.fhir.repository.IRepository;
5+
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
6+
import org.hl7.fhir.exceptions.FHIRException;
7+
import org.hl7.fhir.instance.model.api.IPrimitiveType;
8+
import org.hl7.fhir.r4.model.Bundle;
9+
import org.hl7.fhir.r4.model.CodeType;
10+
import org.hl7.fhir.r4.model.Endpoint;
11+
import org.hl7.fhir.r4.model.IdType;
12+
import org.hl7.fhir.r4.model.Library;
13+
import org.hl7.fhir.r4.model.Parameters;
14+
import org.opencds.cqf.fhir.cr.visitor.ReleaseManifestVisitor;
15+
import org.opencds.cqf.fhir.utility.SearchHelper;
16+
import org.opencds.cqf.fhir.utility.adapter.IAdapterFactory;
17+
18+
/**
19+
* Service for the $release-manifest operation. Operates on manifest Libraries (asset-collection)
20+
* that have pre-computed depends-on entries from $infer-manifest-parameters.
21+
*/
22+
public class R4ReleaseManifestService {
23+
24+
private final IAdapterFactory adapterFactory = IAdapterFactory.forFhirVersion(FhirVersionEnum.R4);
25+
private final IRepository repository;
26+
27+
public R4ReleaseManifestService(IRepository repository) {
28+
this.repository = repository;
29+
}
30+
31+
public Bundle releaseManifest(
32+
IdType id,
33+
String version,
34+
CodeType versionBehavior,
35+
IPrimitiveType<Boolean> latestFromTxServer,
36+
Endpoint terminologyEndpoint,
37+
String releaseLabel)
38+
throws FHIRException {
39+
var baseResource = SearchHelper.readRepository(repository, id);
40+
if (baseResource == null) {
41+
throw new ResourceNotFoundException(id);
42+
}
43+
if (!(baseResource instanceof Library)) {
44+
throw new FHIRException("$release-manifest requires a Library resource, found: " + baseResource.fhirType());
45+
}
46+
var resource = (Library) baseResource;
47+
var params = new Parameters();
48+
if (version != null) {
49+
params.addParameter("version", version);
50+
}
51+
if (versionBehavior != null) {
52+
params.addParameter("versionBehavior", versionBehavior);
53+
}
54+
if (latestFromTxServer != null && latestFromTxServer.hasValue()) {
55+
params.addParameter("latestFromTxServer", latestFromTxServer.getValue());
56+
}
57+
if (releaseLabel != null) {
58+
params.addParameter("releaseLabel", releaseLabel);
59+
}
60+
if (terminologyEndpoint != null) {
61+
params.addParameter().setName("terminologyEndpoint").setResource(terminologyEndpoint);
62+
}
63+
var adapter = adapterFactory.createKnowledgeArtifactAdapter(resource);
64+
var visitor = new ReleaseManifestVisitor(repository);
65+
return (Bundle) adapter.accept(visitor, params);
66+
}
67+
}

cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/visitor/ConformanceResourceResolver.java

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public class ConformanceResourceResolver {
3838
private final IRepository federatedRepository;
3939
private final DefaultProfileValidationSupport coreSupport;
4040
private Map<String, IBaseResource> packageCache; // lazy, built on first SD miss
41+
private Map<String, IBaseResource> resourceCache; // lazy, built on first VS/CS miss
4142

4243
public ConformanceResourceResolver(IRepository repository) {
4344
this(repository, Collections.emptyList(), Collections.emptyList());
@@ -182,6 +183,109 @@ private void loadStructureDefinition(
182183
}
183184
}
184185

186+
/**
187+
* Resolve a resource by canonical URL and resource type.
188+
* Resolution order: federated repository → NPM resource cache → core FHIR
189+
*/
190+
public IBaseResource resolveResource(String canonicalUrl, String resourceType) {
191+
if (canonicalUrl == null || canonicalUrl.isEmpty() || resourceType == null) {
192+
return null;
193+
}
194+
195+
// Tier 1: Federated repository (search by canonical)
196+
var result = resolveResourceFromRepository(canonicalUrl, resourceType);
197+
if (result != null) {
198+
return result;
199+
}
200+
201+
// Tier 2: NPM resource cache (lazy-built)
202+
result = resolveFromResourceCache(canonicalUrl, resourceType);
203+
if (result != null) {
204+
return result;
205+
}
206+
207+
// Tier 3: Core FHIR (for base spec resources)
208+
return resolveResourceFromCoreSupport(canonicalUrl, resourceType);
209+
}
210+
211+
private IBaseResource resolveResourceFromRepository(String canonicalUrl, String resourceType) {
212+
try {
213+
var bundle = SearchHelper.searchRepositoryByCanonicalWithPaging(federatedRepository, canonicalUrl);
214+
if (bundle != null) {
215+
var entries = org.opencds.cqf.fhir.utility.BundleHelper.getEntry(bundle);
216+
if (entries != null && !entries.isEmpty()) {
217+
var resource = org.opencds.cqf.fhir.utility.BundleHelper.getEntryResource(
218+
fhirContext.getVersion().getVersion(), entries.get(0));
219+
if (resource != null && resource.fhirType().equals(resourceType)) {
220+
return resource;
221+
}
222+
}
223+
}
224+
} catch (Exception e) {
225+
logger.debug("Could not resolve {} from repository: {}", resourceType, canonicalUrl, e);
226+
}
227+
return null;
228+
}
229+
230+
private IBaseResource resolveResourceFromCoreSupport(String canonicalUrl, String resourceType) {
231+
try {
232+
var resourceClass = fhirContext.getResourceDefinition(resourceType).getImplementingClass();
233+
return coreSupport.fetchResource(resourceClass, canonicalUrl);
234+
} catch (Exception e) {
235+
logger.debug("Could not resolve {} from core support: {}", resourceType, canonicalUrl, e);
236+
}
237+
return null;
238+
}
239+
240+
private IBaseResource resolveFromResourceCache(String canonicalUrl, String resourceType) {
241+
var key = resourceType + "|" + canonicalUrl;
242+
if (resourceCache == null) {
243+
resourceCache = buildResourceCache(npmRepository.getLoadedPackages());
244+
}
245+
return resourceCache.get(key);
246+
}
247+
248+
private Map<String, IBaseResource> buildResourceCache(List<NpmPackage> packages) {
249+
Map<String, IBaseResource> cache = new HashMap<>();
250+
if (packages == null || packages.isEmpty()) {
251+
return cache;
252+
}
253+
var parser = fhirContext.newJsonParser();
254+
for (var npmPackage : packages) {
255+
for (var resType : List.of("ValueSet", "CodeSystem")) {
256+
try {
257+
var files = npmPackage.listResources(resType);
258+
for (var filename : files) {
259+
try (InputStream is = npmPackage.load("package", filename)) {
260+
var resource = parser.parseResource(is);
261+
var url = extractUrl(resource);
262+
if (url != null) {
263+
cache.putIfAbsent(resType + "|" + url, resource);
264+
}
265+
} catch (Exception e) {
266+
logger.debug("Error loading {} from package {}: {}", resType, npmPackage.id(), filename, e);
267+
}
268+
}
269+
} catch (Exception e) {
270+
logger.debug("Error listing {} from package {}", resType, npmPackage.id(), e);
271+
}
272+
}
273+
}
274+
logger.debug("Built resource cache with {} entries", cache.size());
275+
return cache;
276+
}
277+
278+
private String extractUrl(IBaseResource resource) {
279+
try {
280+
var adapter = IAdapterFactory.forFhirVersion(
281+
fhirContext.getVersion().getVersion())
282+
.createKnowledgeArtifactAdapter((org.hl7.fhir.instance.model.api.IDomainResource) resource);
283+
return adapter.getUrl();
284+
} catch (Exception e) {
285+
return null;
286+
}
287+
}
288+
185289
/**
186290
* Creates a StructureDefinition adapter for the given resource.
187291
*/

0 commit comments

Comments
 (0)