11package org .opencds .cqf .tooling .utilities ;
22
3+ import java .util .Objects ;
34import org .hl7 .elm .r1 .VersionedIdentifier ;
4- import org .hl7 .fhir .r4 .model .CanonicalType ;
5+ import org .hl7 .fhir .instance .model .api . IPrimitiveType ;
56import 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+ */
718public 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