Skip to content

Commit 37a4a89

Browse files
committed
tooling-core tests; aligned CanonicalUtils with clinical-reasoning implementation
1 parent 3148c95 commit 37a4a89

9 files changed

Lines changed: 1112 additions & 28 deletions

File tree

tooling-core/pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,5 +88,12 @@
8888
<artifactId>commons-io</artifactId>
8989
<version>2.15.1</version>
9090
</dependency>
91+
<!-- Test -->
92+
<dependency>
93+
<groupId>org.testng</groupId>
94+
<artifactId>testng</artifactId>
95+
<version>7.7.0</version>
96+
<scope>test</scope>
97+
</dependency>
9198
</dependencies>
9299
</project>

tooling-core/src/main/java/org/opencds/cqf/tooling/utilities/CanonicalUtils.java

Lines changed: 236 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,155 @@
11
package org.opencds.cqf.tooling.utilities;
22

3+
import java.util.Objects;
34
import org.hl7.elm.r1.VersionedIdentifier;
4-
import org.hl7.fhir.r4.model.CanonicalType;
5+
import org.hl7.fhir.instance.model.api.IPrimitiveType;
56
import org.opencds.cqf.tooling.exception.InvalidCanonical;
67

8+
/**
9+
* Utility class for parsing FHIR canonical URLs into their constituent components.
10+
*
11+
* <p>A canonical URL has the general form:
12+
* <pre> [base]/[ResourceType]/[id]|[version]#[fragment]</pre>
13+
*
14+
* <p>This class is aligned with {@code org.opencds.cqf.fhir.utility.Canonicals} from the
15+
* clinical-reasoning project and additionally provides {@link #toVersionedIdentifier} and
16+
* {@link #toVersionedIdentifierAnyResource} for CQL ELM integration.
17+
*/
718
public class CanonicalUtils {
819

920
private CanonicalUtils() {}
1021

22+
// ---- URL component extraction (aligned with Canonicals) ----
23+
24+
/**
25+
* Gets the URL component of a canonical url — everything before the version and fragment.
26+
* Includes the base url, resource type, and id if present.
27+
*
28+
* @return the URL, or null if the canonical contains no path separator and is not a URN
29+
*/
30+
public static String getUrl(String canonical) {
31+
Objects.requireNonNull(canonical);
32+
if (!canonical.contains("/") && !canonical.startsWith("urn:uuid") && !canonical.startsWith("urn:oid")) {
33+
return null;
34+
}
35+
return canonical.substring(0, calculateLastIndex(canonical));
36+
}
37+
38+
public static <T extends IPrimitiveType<String>> String getUrl(T canonicalType) {
39+
Objects.requireNonNull(canonicalType);
40+
requireValue(canonicalType, "url extraction");
41+
return getUrl(canonicalType.getValue());
42+
}
43+
44+
/**
45+
* Gets the Resource type component of a canonical url.
46+
*
47+
* @return the resource type, or null if one cannot be parsed
48+
*/
49+
public static String getResourceType(String canonical) {
50+
Objects.requireNonNull(canonical);
51+
if (!canonical.contains("/")) {
52+
return null;
53+
}
54+
String withoutTail = canonical.replace(canonical.substring(canonical.lastIndexOf("/")), "");
55+
return withoutTail.contains("/") ? withoutTail.substring(withoutTail.lastIndexOf("/") + 1) : withoutTail;
56+
}
57+
58+
public static <T extends IPrimitiveType<String>> String getResourceType(T canonicalType) {
59+
Objects.requireNonNull(canonicalType);
60+
requireValue(canonicalType, "resource type extraction");
61+
return getResourceType(canonicalType.getValue());
62+
}
63+
64+
/**
65+
* Gets the ID component of a canonical url. Requires a path separator; returns null for
66+
* bare identifiers. Strips version and fragment.
67+
*
68+
* @return the id, or null if the canonical contains no path separator
69+
*/
70+
public static String getIdPart(String canonical) {
71+
Objects.requireNonNull(canonical);
72+
if (!canonical.contains("/")) {
73+
return null;
74+
}
75+
int lastIndex = calculateLastIndex(canonical);
76+
return canonical.substring(canonical.lastIndexOf("/") + 1, lastIndex);
77+
}
78+
79+
public static <T extends IPrimitiveType<String>> String getIdPart(T canonicalType) {
80+
Objects.requireNonNull(canonicalType);
81+
requireValue(canonicalType, "id extraction");
82+
return getIdPart(canonicalType.getValue());
83+
}
84+
85+
/**
86+
* Gets the Version component of a canonical url. Strips any trailing fragment.
87+
*
88+
* @return the version, or null if no version separator is present
89+
*/
90+
public static String getVersion(String canonical) {
91+
Objects.requireNonNull(canonical);
92+
if (!canonical.contains("|")) {
93+
return null;
94+
}
95+
int lastIndex = canonical.lastIndexOf("#");
96+
if (lastIndex == -1) {
97+
lastIndex = canonical.length();
98+
}
99+
return canonical.substring(canonical.lastIndexOf("|") + 1, lastIndex);
100+
}
101+
102+
public static <T extends IPrimitiveType<String>> String getVersion(T canonicalType) {
103+
Objects.requireNonNull(canonicalType);
104+
requireValue(canonicalType, "version extraction");
105+
return getVersion(canonicalType.getValue());
106+
}
107+
108+
/**
109+
* Gets the Fragment component of a canonical url.
110+
*
111+
* @return the fragment, or null if no fragment separator is present
112+
*/
113+
public static String getFragment(String canonical) {
114+
Objects.requireNonNull(canonical);
115+
if (!canonical.contains("#")) {
116+
return null;
117+
}
118+
return canonical.substring(canonical.lastIndexOf("#") + 1);
119+
}
120+
121+
public static <T extends IPrimitiveType<String>> String getFragment(T canonicalType) {
122+
Objects.requireNonNull(canonicalType);
123+
requireValue(canonicalType, "fragment extraction");
124+
return getFragment(canonicalType.getValue());
125+
}
126+
127+
/**
128+
* Parses a canonical url into all of its constituent parts.
129+
*/
130+
public static CanonicalParts getParts(String canonical) {
131+
Objects.requireNonNull(canonical);
132+
return new CanonicalParts(
133+
getUrl(canonical),
134+
getIdPart(canonical),
135+
getResourceType(canonical),
136+
getVersion(canonical),
137+
getFragment(canonical));
138+
}
139+
140+
public static <T extends IPrimitiveType<String>> CanonicalParts getParts(T canonicalType) {
141+
Objects.requireNonNull(canonicalType);
142+
requireValue(canonicalType, "parts extraction");
143+
return getParts(canonicalType.getValue());
144+
}
145+
146+
// ---- Path splitting utilities ----
147+
148+
/**
149+
* Returns everything before the last path separator.
150+
*
151+
* @return the head, empty string if the separator is at position 0, or null if absent or input is null
152+
*/
11153
public static String getHead(String url) {
12154
if (url == null) {
13155
return null;
@@ -22,6 +164,11 @@ public static String getHead(String url) {
22164
}
23165
}
24166

167+
/**
168+
* Returns everything after the last path separator.
169+
*
170+
* @return the tail, empty string if the separator is at position 0, or null if absent or input is null
171+
*/
25172
public static String getTail(String url) {
26173
if (url == null) {
27174
return null;
@@ -36,44 +183,41 @@ public static String getTail(String url) {
36183
}
37184
}
38185

39-
public static String getVersion(String url) {
40-
int index = url.lastIndexOf("|");
41-
if (index == -1) {
42-
return null;
43-
} else if (index > 0) {
44-
return url.substring(index + 1);
45-
} else {
46-
return "";
47-
}
48-
}
186+
// ---- Legacy methods (backward compatibility) ----
49187

188+
/**
189+
* Gets the ID component of a canonical url. Unlike {@link #getIdPart}, this method accepts
190+
* bare identifiers without a path separator, returning them directly.
191+
*/
50192
public static String getId(String url) {
193+
Objects.requireNonNull(url);
51194
String temp = url.contains("/") ? url.substring(url.lastIndexOf("/") + 1) : url;
52195
return temp.split("\\|")[0];
53196
}
54197

55-
public static String getId(CanonicalType canonical) {
56-
if (canonical.hasValue()) {
57-
return getId(canonical.getValue());
58-
}
59-
60-
throw new InvalidCanonical("CanonicalType must have a value for id extraction");
198+
public static <T extends IPrimitiveType<String>> String getId(T canonicalType) {
199+
Objects.requireNonNull(canonicalType);
200+
requireValue(canonicalType, "id extraction");
201+
return getId(canonicalType.getValue());
61202
}
62203

63-
public static String getResourceName(CanonicalType canonical) {
64-
if (canonical.hasValue()) {
65-
String id = canonical.getValue();
66-
if (id.contains("/")) {
67-
id = id.replace(id.substring(id.lastIndexOf("/")), "");
68-
return id.contains("/") ? id.substring(id.lastIndexOf("/") + 1) : id;
69-
}
70-
return null;
71-
}
72-
73-
throw new InvalidCanonical("CanonicalType must have a value for resource name extraction");
204+
/**
205+
* @deprecated Use {@link #getResourceType(IPrimitiveType)} instead
206+
*/
207+
@Deprecated
208+
public static <T extends IPrimitiveType<String>> String getResourceName(T canonicalType) {
209+
return getResourceType(canonicalType);
74210
}
75211

212+
// ---- CQL ELM integration (unique to this codebase) ----
213+
214+
/**
215+
* Parses a Library canonical url into a CQL {@link VersionedIdentifier}.
216+
*
217+
* @throws InvalidCanonical if the canonical is not a Library resource
218+
*/
76219
public static VersionedIdentifier toVersionedIdentifier(String url) {
220+
Objects.requireNonNull(url);
77221
String version = getVersion(url);
78222
if ("".equals(version)) {
79223
version = null;
@@ -92,7 +236,11 @@ public static VersionedIdentifier toVersionedIdentifier(String url) {
92236
return new VersionedIdentifier().withSystem(base).withId(id).withVersion(version);
93237
}
94238

239+
/**
240+
* Parses a canonical url for any resource type into a CQL {@link VersionedIdentifier}.
241+
*/
95242
public static VersionedIdentifier toVersionedIdentifierAnyResource(String url) {
243+
Objects.requireNonNull(url);
96244
String version = getVersion(url);
97245
if ("".equals(version)) {
98246
version = null;
@@ -109,4 +257,64 @@ public static VersionedIdentifier toVersionedIdentifierAnyResource(String url) {
109257

110258
return new VersionedIdentifier().withSystem(base).withId(id).withVersion(version);
111259
}
260+
261+
// ---- Internal helpers ----
262+
263+
private static int calculateLastIndex(String canonical) {
264+
int lastIndexOfBar = canonical.lastIndexOf("|");
265+
int lastIndexOfHash = canonical.lastIndexOf("#");
266+
267+
int lastIndex = canonical.length();
268+
int mul = lastIndexOfBar * lastIndexOfHash;
269+
if (mul > 1) {
270+
lastIndex = Math.min(lastIndexOfBar, lastIndexOfHash);
271+
} else if (mul < 0) {
272+
lastIndex = Math.max(lastIndexOfBar, lastIndexOfHash);
273+
}
274+
return lastIndex;
275+
}
276+
277+
private static void requireValue(IPrimitiveType<String> canonicalType, String operation) {
278+
if (!canonicalType.hasValue()) {
279+
throw new InvalidCanonical("CanonicalType must have a value for " + operation);
280+
}
281+
}
282+
283+
// ---- CanonicalParts ----
284+
285+
public static final class CanonicalParts {
286+
private final String url;
287+
private final String idPart;
288+
private final String resourceType;
289+
private final String version;
290+
private final String fragment;
291+
292+
CanonicalParts(String url, String idPart, String resourceType, String version, String fragment) {
293+
this.url = url;
294+
this.idPart = idPart;
295+
this.resourceType = resourceType;
296+
this.version = version;
297+
this.fragment = fragment;
298+
}
299+
300+
public String url() {
301+
return url;
302+
}
303+
304+
public String idPart() {
305+
return idPart;
306+
}
307+
308+
public String resourceType() {
309+
return resourceType;
310+
}
311+
312+
public String version() {
313+
return version;
314+
}
315+
316+
public String fragment() {
317+
return fragment;
318+
}
319+
}
112320
}

0 commit comments

Comments
 (0)