Skip to content

Commit d4c8dcc

Browse files
committed
Merge branch 'do-20260501-dependency-updates' into do-20260626-dependency-updates
2 parents 55f8537 + a312868 commit d4c8dcc

46 files changed

Lines changed: 3146 additions & 131 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,4 +243,4 @@ shell-scripts/go-publish-fhir.txt
243243

244244
.run/JUnit Validation.run.xml
245245

246-
.run/
246+
.run/

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ Setup a launch.json file in the .vscode folder with the following content:
154154
"cwd": "c:/git/hl7/utg",
155155
"args": [
156156
"-ig", ".",
157-
"-tx", "http://tx.fhir.org/r4"
157+
"-tx", "https://tx.fhir.org/r4"
158158
]
159159
}
160160
]

multi-version-IGs.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# Multi-version IGs
2+
3+
## 1 Introduction
4+
5+
An Implementation Guide is authored against a single primary FHIR version, but the
6+
IG Publisher can, in a single run, additionally emit **downgraded variant packages**
7+
for other FHIR versions so that implementers on those versions can consume the IG.
8+
9+
This document describes emitting **R4 (4.0.1)** and **R4B (4.3.0)** consumable packages
10+
from an IG **authored in R5 (5.0.0)**. Existing R4↔R4B behaviour (see
11+
[the legacy path](#6-relationship-to-the-legacy-r4r4b-path)) is unchanged.
12+
13+
The variant packages are produced by the semantic `generate-version` conversion path,
14+
which uses the shared version-conversion machinery (`ProfileVersionAdaptor` and the
15+
`VersionConvertorFactory_*` factories). No new element-level converters are involved -
16+
this is about enabling R5 as a source version, per-version wiring, configuration,
17+
analysis and reporting.
18+
19+
## 2 Selecting the target versions
20+
21+
Add one `generate-version` guidance parameter to the IG for each FHIR version you want
22+
a variant package for. For an R5 IG that should ship R4 and R4B:
23+
24+
```json
25+
"parameter" : [
26+
{ "code" : "generate-version", "value" : "r4" },
27+
{ "code" : "generate-version", "value" : "r4b" }
28+
]
29+
```
30+
31+
Each target produces an extra package `<packageId>.<token>.tgz` (e.g. `<id>.r4.tgz`,
32+
`<id>.r4b.tgz`) alongside the R5 `package.tgz`.
33+
34+
### Version tokens
35+
36+
Everywhere a FHIR version is named (the `generate-version` value, the per-version
37+
dependency extension, and the inclusion parameters) the token is matched by **version
38+
family**, so all of the following are accepted and collate to the same target:
39+
40+
| Family | Accepted tokens | Recommended |
41+
|--------|---------------------------------|-------------|
42+
| R4 | `r4`, `4.0`, `4.0.1` | `r4` |
43+
| R4B | `r4b`, `4.3`, `4.3.0` | `r4b` |
44+
| R5 | `r5`, `5.0.0` | `r5` |
45+
46+
Use `r4` / `r4b` as the `generate-version` tokens so the produced file names are
47+
`<id>.r4.tgz` / `<id>.r4b.tgz`.
48+
49+
## 3 Per-version dependencies
50+
51+
The dependencies a downgraded R4/R4B package needs are usually **not** a mechanical
52+
rename of the R5 dependencies - they frequently have different package ids and/or
53+
versions, and some dependencies only make sense for a particular FHIR version.
54+
55+
Per-version dependencies are declared with a single repeating extension on each
56+
`ImplementationGuide.dependsOn` entry:
57+
58+
`http://hl7.org/fhir/tools/StructureDefinition/ig-dependency-for-version`
59+
60+
with these sub-extensions:
61+
62+
| Part | Card. | Type | Meaning |
63+
|---------------|-------|--------|------------------------------------------------------------|
64+
| `fhirVersion` | 1..1 | code | the target version this occurrence describes |
65+
| `packageId` | 0..1 | id | override the dependency's package id for that version |
66+
| `version` | 0..1 | string | override the dependency's package version for that version |
67+
| `use` | 0..1 | code | `override` (default) or `remove` |
68+
69+
Semantics for a given target version `V` (a `generate-version` target, or the base
70+
version for the R5 package itself):
71+
72+
* **Override** - the entry has an occurrence for `V`: apply its `packageId`/`version`
73+
overrides (a bare occurrence keeps the authored values).
74+
* **Remove** - the entry has an occurrence for `V` with `use = remove`: the entry is
75+
absent from `V`.
76+
* **Add (version-specific)** - the entry has occurrences for some versions but **not**
77+
`V`: the entry is absent from `V`. This is how a dependency that applies only to R4
78+
is declared - author a normal `dependsOn` row and add one occurrence for `r4`; it is
79+
then present only in the R4 package and absent from the R5 and R4B packages.
80+
* **Legacy** - the entry has **no** such extension: it applies to every version, and for
81+
each variant the historical package-id suffix rename applies (`.r5` &rarr; `.r4`,
82+
with R4B forced to `.r4` for wire-compatibility).
83+
84+
The effective per-version dependencies drive each variant package's `dependsOn` and are
85+
surfaced in the rendered dependency table. A version-scoped entry is never loaded,
86+
validated, rendered, or packaged for a version it does not apply to.
87+
88+
## 4 Per-version resource membership
89+
90+
By default every resource is written into every generated package. To scope a resource
91+
to particular version(s), use the inclusion parameters:
92+
93+
* `r4-inclusion` - value is a resource `Type/id` or canonical URL
94+
* `r4b-inclusion`
95+
* `r5-inclusion`
96+
97+
These use **tag-membership** semantics:
98+
99+
* A resource listed in **any** inclusion set appears **only** in the listed version(s).
100+
* A resource listed in **no** inclusion set appears in **all** versions (the default).
101+
102+
`r5-inclusion` also gates the base R5 package, so a resource can be scoped away from R5
103+
entirely (e.g. an R4-only resource authored in R5). Membership governs *package*
104+
membership only; the R5 site rendering is unchanged.
105+
106+
> The legacy `r4-exclusion` / `r4b-exclusion` parameters are unrelated to this feature -
107+
> they continue to drive the legacy R4↔R4B path only and are not consulted for an R5 base.
108+
109+
## 5 Cross-version analysis
110+
111+
For an R5 base with `generate-version`, a `cross-version-analysis` page reports, per
112+
target version:
113+
114+
* conversion problems (from the `ProfileVersionAdaptor` conversion log for
115+
StructureDefinition/SearchParameter, and from conversion success/failure for the other
116+
conformance types and examples), and
117+
* resources intentionally omitted from that target via the inclusion parameters.
118+
119+
A resource that uses an R5-only type that cannot be represented in R4/R4B yields a
120+
**warning** on this page (not a build failure); use the inclusion parameters to exclude
121+
it from the target(s) where it does not belong.
122+
123+
## 6 Relationship to the legacy R4/R4B path
124+
125+
An IG whose base version is **R4 or R4B** continues to use the original cross-version
126+
mechanism (`R4ToR4BAnalyser`), which re-parses the built package with the R4B parser and
127+
clones it into `.r4.tgz` / `.r4b.tgz`. That path is unchanged and is only entered for an
128+
R4/R4B base; an R5 base always uses the `generate-version` path described above.

org.hl7.fhir.publisher.cli/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<parent>
66
<artifactId>org.hl7.fhir.publisher</artifactId>
77
<groupId>org.hl7.fhir.publisher</groupId>
8-
<version>2.2.11-SNAPSHOT</version>
8+
<version>2.2.12-SNAPSHOT</version>
99
<relativePath>../pom.xml</relativePath>
1010
</parent>
1111
<modelVersion>4.0.0</modelVersion>

org.hl7.fhir.publisher.core/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<parent>
66
<groupId>org.hl7.fhir.publisher</groupId>
77
<artifactId>org.hl7.fhir.publisher</artifactId>
8-
<version>2.2.11-SNAPSHOT</version>
8+
<version>2.2.12-SNAPSHOT</version>
99
<relativePath>../pom.xml</relativePath>
1010
</parent>
1111
<modelVersion>4.0.0</modelVersion>
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package org.hl7.fhir.igtools.publisher;
2+
3+
import java.util.ArrayList;
4+
import java.util.LinkedHashMap;
5+
import java.util.LinkedHashSet;
6+
import java.util.List;
7+
import java.util.Map;
8+
import java.util.Set;
9+
10+
import org.hl7.fhir.convertors.misc.ProfileVersionAdaptor;
11+
import org.hl7.fhir.r5.context.IWorkerContext;
12+
import org.hl7.fhir.utilities.Utilities;
13+
import org.hl7.fhir.utilities.VersionUtilities;
14+
15+
/**
16+
* Produces the {@code cross-version-analysis} fragment for an <b>R5 base</b> that emits downgraded
17+
* R4/R4B variant packages via {@code generate-version}. It is the R5-capable analogue of
18+
* {@link R4ToR4BAnalyser} (which stays untouched for the legacy R4/R4B base): rather than checking
19+
* wire-compatibility, it reports the actual per-target conversion outcomes observed while the variant
20+
* packages are built - the {@link ProfileVersionAdaptor} {@code ConversionMessage} log for
21+
* StructureDefinition/SearchParameter, and {@code convVersion} success/failure for every other type -
22+
* plus resources intentionally omitted from a target version via the {@code *-inclusion} params.
23+
* <p>
24+
* All target versions are keyed by their family ({@code r4}/{@code r4b}/{@code r5} via
25+
* {@link PublisherIGLoader#canonicalTarget}) so tokens like {@code 4.0}, {@code 4.0.1} and {@code r4}
26+
* collate. Strings are English-only for now; i18n is deferred (the existing
27+
* {@code RenderingI18nContext.R44B_*} phrases are R4/R4B-specific and live in org.hl7.fhir.core).
28+
*/
29+
public class CrossVersionAnalyser {
30+
31+
private IWorkerContext context;
32+
private List<String> targets = new ArrayList<>();
33+
private final Map<String, Map<String, List<String>>> problems = new LinkedHashMap<>();
34+
private final Map<String, Set<String>> omissions = new LinkedHashMap<>();
35+
36+
public CrossVersionAnalyser() {
37+
super();
38+
}
39+
40+
public void setContext(IWorkerContext context) {
41+
this.context = context;
42+
}
43+
44+
/** The generate-version targets (raw tokens); used for iteration order and the "all clean" summary. */
45+
public void setTargets(List<String> targets) {
46+
if (targets != null) {
47+
this.targets = new ArrayList<>(targets);
48+
}
49+
}
50+
51+
/** Record the {@link ProfileVersionAdaptor} conversion log for an SD/SP conversion to a target. */
52+
public void record(String targetVer, String resourceKey, List<ProfileVersionAdaptor.ConversionMessage> log) {
53+
if (log == null) {
54+
return;
55+
}
56+
for (ProfileVersionAdaptor.ConversionMessage m : log) {
57+
if (m.getStatus() != ProfileVersionAdaptor.ConversionMessageStatus.NOTE) {
58+
addProblem(targetVer, resourceKey, m.getMessage());
59+
}
60+
}
61+
}
62+
63+
/** Record a single conversion problem (e.g. a caught {@code convVersion} failure) for a target. */
64+
public void recordProblem(String targetVer, String resourceKey, String message) {
65+
addProblem(targetVer, resourceKey, message);
66+
}
67+
68+
/** Record that a resource was intentionally omitted from a target version via {@code *-inclusion}. */
69+
public void recordOmission(String targetVer, String resourceKey) {
70+
omissions.computeIfAbsent(key(targetVer), k -> new LinkedHashSet<>()).add(resourceKey);
71+
}
72+
73+
private void addProblem(String targetVer, String resourceKey, String message) {
74+
if (message == null) {
75+
return;
76+
}
77+
problems.computeIfAbsent(key(targetVer), k -> new LinkedHashMap<>())
78+
.computeIfAbsent(resourceKey, k -> new ArrayList<>())
79+
.add(message);
80+
}
81+
82+
private static String key(String v) {
83+
return PublisherIGLoader.canonicalTarget(v);
84+
}
85+
86+
private static String name(String familyKey) {
87+
return VersionUtilities.getNameForVersion(familyKey);
88+
}
89+
90+
public boolean hasContent() {
91+
return !problems.isEmpty() || !omissions.isEmpty();
92+
}
93+
94+
/**
95+
* The analysis fragment. {@code inline} yields a terse single-line variant; otherwise a block of
96+
* {@code <p>}/{@code <ul>} per target version. An all-clean set of targets yields the "OK" summary
97+
* (non-inline) or "" (inline); no targets at all yields "".
98+
*/
99+
public String generate(String pid, boolean inline) {
100+
LinkedHashSet<String> keys = new LinkedHashSet<>();
101+
for (String t : targets) {
102+
keys.add(key(t));
103+
}
104+
keys.addAll(problems.keySet());
105+
keys.addAll(omissions.keySet());
106+
if (keys.isEmpty()) {
107+
return "";
108+
}
109+
StringBuilder b = new StringBuilder();
110+
boolean any = false;
111+
for (String k : keys) {
112+
Map<String, List<String>> probs = problems.get(k);
113+
Set<String> oms = omissions.get(k);
114+
boolean hasProbs = probs != null && !probs.isEmpty();
115+
boolean hasOms = oms != null && !oms.isEmpty();
116+
if (!hasProbs && !hasOms) {
117+
continue;
118+
}
119+
any = true;
120+
String nm = name(k);
121+
if (inline) {
122+
List<String> parts = new ArrayList<>();
123+
if (hasProbs) {
124+
for (Map.Entry<String, List<String>> e : probs.entrySet()) {
125+
parts.add(Utilities.escapeXml(e.getKey()) + " (" + Utilities.escapeXml(String.join("; ", e.getValue())) + ")");
126+
}
127+
}
128+
if (hasOms) {
129+
for (String o : oms) {
130+
parts.add(Utilities.escapeXml(o) + " not included");
131+
}
132+
}
133+
b.append("Conversion to " + nm + ": " + String.join(", ", parts) + ". ");
134+
} else {
135+
b.append("<p>Conversion to " + nm + ":</p>\r\n<ul>\r\n");
136+
if (hasProbs) {
137+
for (Map.Entry<String, List<String>> e : probs.entrySet()) {
138+
b.append("<li>" + Utilities.escapeXml(e.getKey()) + ": " + Utilities.escapeXml(String.join("; ", e.getValue())) + "</li>\r\n");
139+
}
140+
}
141+
if (hasOms) {
142+
for (String o : oms) {
143+
b.append("<li>" + Utilities.escapeXml(o) + " - intentionally not included in " + nm + "</li>\r\n");
144+
}
145+
}
146+
b.append("</ul>\r\n");
147+
}
148+
}
149+
if (!any) {
150+
return inline ? "" : "<p>All resources convert cleanly to the generated version(s): " + versionList() + ".</p>\r\n";
151+
}
152+
return b.toString();
153+
}
154+
155+
private String versionList() {
156+
List<String> names = new ArrayList<>();
157+
for (String t : targets) {
158+
String n = name(key(t));
159+
if (!names.contains(n)) {
160+
names.add(n);
161+
}
162+
}
163+
return String.join(", ", names);
164+
}
165+
}

org.hl7.fhir.publisher.core/src/main/java/org/hl7/fhir/igtools/publisher/DependentIGFinder.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import org.hl7.fhir.utilities.FileUtilities;
1818
import org.hl7.fhir.utilities.Utilities;
1919
import org.hl7.fhir.utilities.VersionUtilities;
20+
import org.hl7.fhir.utilities.http.ManagedWebAccess;
2021
import org.hl7.fhir.utilities.json.model.JsonObject;
2122
import org.hl7.fhir.utilities.json.parser.JsonParser;
2223
import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager;
@@ -346,8 +347,10 @@ private void checkIGDependencies(JsonObject guide) {
346347

347348
// we only check the latest published version, and the CI build
348349
try {
349-
PackageList pl = PackageList.fromUrl(Utilities.pathURL(guide.asString("canonical"), "package-list.json"));
350350
String canonical = guide.asString("canonical");
351+
final String secureCanonical = ManagedWebAccess.makeSecureRef(canonical);
352+
PackageList pl = PackageList.fromUrl(Utilities.pathURL(secureCanonical, "package-list.json"));
353+
351354
DepInfo dep = new DepInfo(pid, Utilities.path(canonical, "history.html"));
352355
deplist.add(dep);
353356
for (PackageListEntry e : pl.versions()) {

org.hl7.fhir.publisher.core/src/main/java/org/hl7/fhir/igtools/publisher/GitUtilities.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import java.io.InputStream;
77
import java.io.InputStreamReader;
88
import java.net.MalformedURLException;
9+
import java.net.URI;
10+
import java.net.URISyntaxException;
911
import java.net.URL;
1012

1113
public class GitUtilities {
@@ -76,16 +78,16 @@ public static String getGitSource(File gitDir) {
7678
*/
7779
protected static String getURLWithNoUserInfo(final String url, final String urlSource) {
7880
try {
79-
URL newUrl = new URL(url);
81+
URL newUrl = URI.create(url).toURL();
8082
if (newUrl.getUserInfo() != null) {
8183
System.out.println("Info @ Removing user info from GIT URL. Source: " + urlSource);
82-
return new URL(newUrl.getProtocol(), newUrl.getHost(), newUrl.getPort(), newUrl.getFile()).toString();
84+
return new URI(newUrl.getProtocol(), null, newUrl.getHost(), newUrl.getPort(), newUrl.getPath(), newUrl.getQuery(), null).toURL().toString();
8385
}
8486
return url;
85-
} catch (MalformedURLException e) {
87+
} catch (MalformedURLException | URISyntaxException | IllegalArgumentException e ) {
8688
System.out.println("Warning @ Git URL is not a valid URl. Source: " + urlSource);
8789
return null;
8890
}
89-
}
91+
}
9092

9193
}

0 commit comments

Comments
 (0)