001package org.hl7.fhir.r5.terminologies; 002 003/* 004 Copyright (c) 2011+, HL7, Inc. 005 All rights reserved. 006 007 Redistribution and use in source and binary forms, with or without modification, 008 are permitted provided that the following conditions are met: 009 010 * Redistributions of source code must retain the above copyright notice, this 011 list of conditions and the following disclaimer. 012 * Redistributions in binary form must reproduce the above copyright notice, 013 this list of conditions and the following disclaimer in the documentation 014 and/or other materials provided with the distribution. 015 * Neither the name of HL7 nor the names of its contributors may be used to 016 endorse or promote products derived from this software without specific 017 prior written permission. 018 019 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 020 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 021 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 022 IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 023 INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 024 NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 025 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 026 WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 027 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 028 POSSIBILITY OF SUCH DAMAGE. 029 030 */ 031 032 033 034import java.util.ArrayList; 035import java.util.Calendar; 036import java.util.Collections; 037import java.util.Comparator; 038import java.util.HashSet; 039import java.util.List; 040import java.util.Set; 041 042import org.hl7.fhir.exceptions.FHIRException; 043import org.hl7.fhir.exceptions.FHIRFormatError; 044import org.hl7.fhir.r5.model.BooleanType; 045import org.hl7.fhir.r5.model.CanonicalResource; 046import org.hl7.fhir.r5.model.CanonicalType; 047import org.hl7.fhir.r5.model.CodeSystem; 048import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent; 049import org.hl7.fhir.r5.model.CodeSystem.ConceptPropertyComponent; 050import org.hl7.fhir.r5.model.CodeSystem.PropertyComponent; 051import org.hl7.fhir.r5.model.CodeSystem.PropertyType; 052import org.hl7.fhir.r5.model.CodeType; 053import org.hl7.fhir.r5.model.DataType; 054import org.hl7.fhir.r5.model.DateTimeType; 055import org.hl7.fhir.r5.model.Enumerations.PublicationStatus; 056import org.hl7.fhir.r5.terminologies.CodeSystemUtilities.ConceptDefinitionComponentSorter; 057import org.hl7.fhir.r5.model.Identifier; 058import org.hl7.fhir.r5.model.Meta; 059import org.hl7.fhir.r5.model.UriType; 060import org.hl7.fhir.r5.utils.ToolingExtensions; 061import org.hl7.fhir.utilities.StandardsStatus; 062import org.hl7.fhir.utilities.Utilities; 063 064public class CodeSystemUtilities { 065 066 public static class ConceptDefinitionComponentSorter implements Comparator<ConceptDefinitionComponent> { 067 068 @Override 069 public int compare(ConceptDefinitionComponent o1, ConceptDefinitionComponent o2) { 070 return o1.getCode().compareTo(o2.getCode()); 071 } 072 073 } 074 075 public static final String USER_DATA_CROSS_LINK = "cs.utils.cross.link"; 076 077 public static class CodeSystemNavigator { 078 079 private CodeSystem cs; 080 private boolean restructure; 081 private Set<String> processed = new HashSet<>(); 082 083 public CodeSystemNavigator(CodeSystem cs) { 084 this.cs = cs; 085 restructure = hasExtraRelationships(cs.getConcept()); 086 } 087 088 public boolean isRestructure() { 089 return restructure; 090 } 091 092 private boolean hasExtraRelationships(List<ConceptDefinitionComponent> concept) { 093 for (ConceptDefinitionComponent cd : concept) { 094 if (!getSubsumedBy(cd).isEmpty()) { 095 return true; 096 } 097 for (ConceptDefinitionComponent cdc : cd.getConcept()) { 098 if (hasExtraRelationships(cdc.getConcept())) { 099 return true; 100 } 101 } 102 } 103 return false; 104 } 105 106 public List<ConceptDefinitionComponent> getConcepts(ConceptDefinitionComponent context) { 107 if (context == null) { 108 if (restructure) { 109 List<ConceptDefinitionComponent> res = new ArrayList<>(); 110 for (ConceptDefinitionComponent cd : cs.getConcept()) { 111 if (getSubsumedBy(cd).isEmpty()) { 112 res.add(cd); 113 processed.add(cd.getCode()); 114 } 115 } 116 return res; 117 } else { 118 return cs.getConcept(); 119 } 120 } else { 121 if (restructure) { 122 List<ConceptDefinitionComponent> res = new ArrayList<>(); 123 for (ConceptDefinitionComponent cd : context.getConcept()) { 124 res.add(cd); 125 processed.add(cd.getCode()); 126 } 127 for (ConceptDefinitionComponent cd : cs.getConcept()) { 128 if (getSubsumedBy(cd).contains(context.getCode()) && !processed.contains(cd.getCode())) { 129 res.add(cd); 130 processed.add(cd.getCode()); 131 } 132 } 133 return res; 134 } else { 135 return context.getConcept(); 136 } 137 } 138 } 139 140 private List<String> getSubsumedBy(ConceptDefinitionComponent cd) { 141 List<String> codes = new ArrayList<>(); 142 for (ConceptPropertyComponent cp : cd.getProperty()) { 143 if ("subsumedBy".equals(cp.getCode())) { 144 codes.add(cp.getValue().primitiveValue()); 145 } 146 } 147 return codes; 148 } 149 150 public List<ConceptDefinitionComponent> getOtherChildren(ConceptDefinitionComponent context) { 151 List<ConceptDefinitionComponent> res = new ArrayList<>(); 152 for (ConceptDefinitionComponent cd : cs.getConcept()) { 153 if (getSubsumedBy(cd).contains(context.getCode()) && processed.contains(cd.getCode())) { 154 res.add(cd); 155 } 156 } 157 return res; 158 } 159 } 160 161 162 public static boolean isNotSelectable(CodeSystem cs, ConceptDefinitionComponent def) { 163 for (ConceptPropertyComponent p : def.getProperty()) { 164 if ("notSelectable".equals(p.getCode()) && p.hasValue() && p.getValue() instanceof BooleanType) 165 return ((BooleanType) p.getValue()).getValue(); 166 } 167 return false; 168 } 169 170 public static void setNotSelectable(CodeSystem cs, ConceptDefinitionComponent concept) throws FHIRFormatError { 171 defineNotSelectableProperty(cs); 172 ConceptPropertyComponent p = getProperty(concept, "notSelectable"); 173 if (p != null) 174 p.setValue(new BooleanType(true)); 175 else 176 concept.addProperty().setCode("notSelectable").setValue(new BooleanType(true)); 177 } 178 179 public static void defineNotSelectableProperty(CodeSystem cs) { 180 defineCodeSystemProperty(cs, "notSelectable", "Indicates that the code is abstract - only intended to be used as a selector for other concepts", PropertyType.BOOLEAN); 181 } 182 183 184 public enum ConceptStatus { 185 Active, Experimental, Deprecated, Retired; 186 187 public String toCode() { 188 switch (this) { 189 case Active: return "active"; 190 case Experimental: return "experimental"; 191 case Deprecated: return "deprecated"; 192 case Retired: return "retired"; 193 default: return null; 194 } 195 } 196 } 197 198 public static void setStatus(CodeSystem cs, ConceptDefinitionComponent concept, ConceptStatus status) throws FHIRFormatError { 199 defineStatusProperty(cs); 200 ConceptPropertyComponent p = getProperty(concept, "status"); 201 if (p != null) 202 p.setValue(new CodeType(status.toCode())); 203 else 204 concept.addProperty().setCode("status").setValue(new CodeType(status.toCode())); 205 } 206 207 public static void defineStatusProperty(CodeSystem cs) { 208 defineCodeSystemProperty(cs, "status", "A property that indicates the status of the concept. One of active, experimental, deprecated, retired", PropertyType.CODE); 209 } 210 211 private static void defineDeprecatedProperty(CodeSystem cs) { 212 defineCodeSystemProperty(cs, "deprecationDate", "The date at which a concept was deprecated. Concepts that are deprecated but not inactive can still be used, but their use is discouraged", PropertyType.DATETIME); 213 } 214 215 public static void defineParentProperty(CodeSystem cs) { 216 defineCodeSystemProperty(cs, "parent", "The concept identified in this property is a parent of the concept on which it is a property. The property type will be 'code'. The meaning of parent/child relationships is defined by the hierarchyMeaning attribute", PropertyType.CODE); 217 } 218 219 public static void defineChildProperty(CodeSystem cs) { 220 defineCodeSystemProperty(cs, "child", "The concept identified in this property is a child of the concept on which it is a property. The property type will be 'code'. The meaning of parent/child relationships is defined by the hierarchyMeaning attribute", PropertyType.CODE); 221 } 222 223 public static boolean isDeprecated(CodeSystem cs, ConceptDefinitionComponent def, boolean ignoreStatus) { 224 try { 225 for (ConceptPropertyComponent p : def.getProperty()) { 226 if (!ignoreStatus) { 227 if ("status".equals(p.getCode()) && p.hasValue() && p.hasValueCodeType() && "deprecated".equals(p.getValueCodeType().getCode())) 228 return true; 229 } 230 // this, though status should also be set 231 if ("deprecationDate".equals(p.getCode()) && p.hasValue() && p.getValue() instanceof DateTimeType) 232 return ((DateTimeType) p.getValue()).before(new DateTimeType(Calendar.getInstance())); 233 // legacy 234 if ("deprecated".equals(p.getCode()) && p.hasValue() && p.getValue() instanceof BooleanType) 235 return ((BooleanType) p.getValue()).getValue(); 236 } 237 return false; 238 } catch (FHIRException e) { 239 return false; 240 } 241 } 242 243 public static void setDeprecated(CodeSystem cs, ConceptDefinitionComponent concept, DateTimeType date) throws FHIRFormatError { 244 setStatus(cs, concept, ConceptStatus.Deprecated); 245 defineDeprecatedProperty(cs); 246 concept.addProperty().setCode("deprecationDate").setValue(date); 247 } 248 249 public static boolean isInactive(CodeSystem cs, ConceptDefinitionComponent def) throws FHIRException { 250 for (ConceptPropertyComponent p : def.getProperty()) { 251 if ("status".equals(p.getCode()) && p.hasValueStringType()) 252 return "inactive".equals(p.getValueStringType().primitiveValue()) || "retired".equals(p.getValueStringType().primitiveValue()); 253 } 254 return false; 255 } 256 257 public static boolean isInactive(CodeSystem cs, String code) throws FHIRException { 258 ConceptDefinitionComponent def = findCode(cs.getConcept(), code); 259 if (def == null) 260 return true; 261 return isInactive(cs, def); 262 } 263 264 public static void defineCodeSystemProperty(CodeSystem cs, String code, String description, PropertyType type) { 265 for (PropertyComponent p : cs.getProperty()) { 266 if (p.getCode().equals(code)) 267 return; 268 } 269 cs.addProperty().setCode(code).setDescription(description).setType(type).setUri("http://hl7.org/fhir/concept-properties#"+code); 270 } 271 272 public static String getCodeDefinition(CodeSystem cs, String code) { 273 return getCodeDefinition(cs.getConcept(), code); 274 } 275 276 private static String getCodeDefinition(List<ConceptDefinitionComponent> list, String code) { 277 for (ConceptDefinitionComponent c : list) { 278 if (c.hasCode() && c.getCode().equals(code)) 279 return c.getDefinition(); 280 String s = getCodeDefinition(c.getConcept(), code); 281 if (s != null) 282 return s; 283 } 284 return null; 285 } 286 287 public static CodeSystem makeShareable(CodeSystem cs) { 288 if (!cs.hasExperimental()) { 289 cs.setExperimental(false); 290 } 291 292 if (!cs.hasMeta()) 293 cs.setMeta(new Meta()); 294 for (UriType t : cs.getMeta().getProfile()) 295 if ("http://hl7.org/fhir/StructureDefinition/shareablecodesystem".equals(t.getValue())) 296 return cs; 297 cs.getMeta().getProfile().add(new CanonicalType("http://hl7.org/fhir/StructureDefinition/shareablecodesystem")); 298 return cs; 299 } 300 301 public static boolean makeCSShareable(CodeSystem cs) { 302 if (!cs.hasMeta()) 303 cs.setMeta(new Meta()); 304 for (UriType t : cs.getMeta().getProfile()) 305 if ("http://hl7.org/fhir/StructureDefinition/shareablecodesystem".equals(t.getValue())) 306 return false; 307 cs.getMeta().getProfile().add(new CanonicalType("http://hl7.org/fhir/StructureDefinition/shareablecodesystem")); 308 return true; 309 } 310 311 public static void setOID(CodeSystem cs, String oid) { 312 if (!oid.startsWith("urn:oid:")) 313 oid = "urn:oid:" + oid; 314 if (!cs.hasIdentifier()) 315 cs.addIdentifier(new Identifier().setSystem("urn:ietf:rfc:3986").setValue(oid)); 316 else if ("urn:ietf:rfc:3986".equals(cs.getIdentifierFirstRep().getSystem()) && cs.getIdentifierFirstRep().hasValue() && cs.getIdentifierFirstRep().getValue().startsWith("urn:oid:")) 317 cs.getIdentifierFirstRep().setValue(oid); 318 else 319 throw new Error("unable to set OID on code system"); 320 321 } 322 323 public static boolean hasOID(CanonicalResource cs) { 324 return getOID(cs) != null; 325 } 326 327 public static String getOID(CanonicalResource cs) { 328 if (cs.hasIdentifier() && "urn:ietf:rfc:3986".equals(cs.getIdentifierFirstRep().getSystem()) && cs.getIdentifierFirstRep().hasValue() && cs.getIdentifierFirstRep().getValue().startsWith("urn:oid:")) 329 return cs.getIdentifierFirstRep().getValue().substring(8); 330 return null; 331 } 332 333 public static ConceptDefinitionComponent findCode(List<ConceptDefinitionComponent> list, String code) { 334 for (ConceptDefinitionComponent c : list) { 335 if (c.getCode().equals(code)) 336 return c; 337 ConceptDefinitionComponent s = findCode(c.getConcept(), code); 338 if (s != null) 339 return s; 340 } 341 return null; 342 } 343 344 public static void markStatus(CodeSystem cs, String wg, StandardsStatus status, String pckage, String fmm, String normativeVersion) throws FHIRException { 345 if (wg != null) { 346 if (!ToolingExtensions.hasExtension(cs, ToolingExtensions.EXT_WORKGROUP) || 347 (Utilities.existsInList(ToolingExtensions.readStringExtension(cs, ToolingExtensions.EXT_WORKGROUP), "fhir", "vocab") && !Utilities.existsInList(wg, "fhir", "vocab"))) { 348 ToolingExtensions.setCodeExtension(cs, ToolingExtensions.EXT_WORKGROUP, wg); 349 } 350 } 351 if (status != null) { 352 StandardsStatus ss = ToolingExtensions.getStandardsStatus(cs); 353 if (ss == null || ss.isLowerThan(status)) 354 ToolingExtensions.setStandardsStatus(cs, status, normativeVersion); 355 if (pckage != null) { 356 if (!cs.hasUserData("ballot.package")) 357 cs.setUserData("ballot.package", pckage); 358 else if (!pckage.equals(cs.getUserString("ballot.package"))) 359 if (!"infrastructure".equals(cs.getUserString("ballot.package"))) 360 System.out.println("Code System "+cs.getUrl()+": ownership clash "+pckage+" vs "+cs.getUserString("ballot.package")); 361 } 362 if (status == StandardsStatus.NORMATIVE) { 363 cs.setExperimental(false); 364 cs.setStatus(PublicationStatus.ACTIVE); 365 } 366 } 367 if (fmm != null) { 368 String sfmm = ToolingExtensions.readStringExtension(cs, ToolingExtensions.EXT_FMM_LEVEL); 369 if (Utilities.noString(sfmm) || Integer.parseInt(sfmm) < Integer.parseInt(fmm)) { 370 ToolingExtensions.setIntegerExtension(cs, ToolingExtensions.EXT_FMM_LEVEL, Integer.parseInt(fmm)); 371 } 372 if (Integer.parseInt(fmm) <= 1) { 373 cs.setExperimental(true); 374 } 375 } 376 } 377 378 379 public static DataType readProperty(ConceptDefinitionComponent concept, String code) { 380 for (ConceptPropertyComponent p : concept.getProperty()) 381 if (p.getCode().equals(code)) 382 return p.getValue(); 383 return null; 384 } 385 386 public static ConceptPropertyComponent getProperty(ConceptDefinitionComponent concept, String code) { 387 for (ConceptPropertyComponent p : concept.getProperty()) 388 if (p.getCode().equals(code)) 389 return p; 390 return null; 391 } 392 393 public static List<ConceptPropertyComponent> getPropertyValues(ConceptDefinitionComponent concept, String code) { 394 List<ConceptPropertyComponent> res = new ArrayList<>(); 395 for (ConceptPropertyComponent p : concept.getProperty()) { 396 if (p.getCode().equals(code)) { 397 res.add(p); 398 } 399 } 400 return res; 401 } 402 403 404 // see http://hl7.org/fhir/R4/codesystem.html#hierachy 405 // returns additional parents not in the heirarchy 406 public static List<String> getOtherChildren(CodeSystem cs, ConceptDefinitionComponent c) { 407 List<String> res = new ArrayList<String>(); 408 for (ConceptPropertyComponent p : c.getProperty()) { 409 if ("parent".equals(p.getCode())) { 410 res.add(p.getValue().primitiveValue()); 411 } 412 } 413 return res; 414 } 415 416 // see http://hl7.org/fhir/R4/codesystem.html#hierachy 417 public static void addOtherChild(CodeSystem cs, ConceptDefinitionComponent owner, String code) { 418 defineChildProperty(cs); 419 owner.addProperty().setCode("child").setValue(new CodeType(code)); 420 } 421 422 public static boolean hasProperty(ConceptDefinitionComponent c, String code) { 423 for (ConceptPropertyComponent cp : c.getProperty()) { 424 if (code.equals(cp.getCode())) { 425 return true; 426 } 427 } 428 return false; 429 } 430 431 public static boolean hasCode(CodeSystem cs, String code) { 432 for (ConceptDefinitionComponent cc : cs.getConcept()) { 433 if (hasCode(cc, code)) { 434 return true; 435 } 436 } 437 return false; 438 } 439 440 private static boolean hasCode(ConceptDefinitionComponent cc, String code) { 441 if (code.equals(cc.getCode())) { 442 return true; 443 } 444 for (ConceptDefinitionComponent c : cc.getConcept()) { 445 if (hasCode(c, code)) { 446 return true; 447 } 448 } 449 return false; 450 } 451 452 public static ConceptDefinitionComponent getCode(CodeSystem cs, String code) { 453 if (code == null) { 454 return null; 455 } 456 for (ConceptDefinitionComponent cc : cs.getConcept()) { 457 ConceptDefinitionComponent cd = getCode(cc, code); 458 if (cd != null) { 459 return cd; 460 } 461 } 462 return null; 463 } 464 465 private static ConceptDefinitionComponent getCode(ConceptDefinitionComponent cc, String code) { 466 if (code.equals(cc.getCode())) { 467 return cc; 468 } 469 for (ConceptDefinitionComponent c : cc.getConcept()) { 470 ConceptDefinitionComponent cd = getCode(c, code); 471 if (cd != null) { 472 return cd; 473 } 474 } 475 return null; 476 } 477 478 public static void crossLinkCodeSystem(CodeSystem cs) { 479 String parent = getPropertyByUrl(cs, "http://hl7.org/fhir/concept-properties#parent"); 480 if ((parent != null)) { 481 crossLinkConcepts(cs.getConcept(), cs.getConcept(), parent); 482 } 483 } 484 485 private static String getPropertyByUrl(CodeSystem cs, String url) { 486 for (PropertyComponent pc : cs.getProperty()) { 487 if (url.equals(pc.getUri())) { 488 return pc.getCode(); 489 } 490 } 491 return null; 492 } 493 494 private static void crossLinkConcepts(List<ConceptDefinitionComponent> root, List<ConceptDefinitionComponent> focus, String parent) { 495 for (ConceptDefinitionComponent def : focus) { 496 List<ConceptPropertyComponent> pcl = getPropertyValues(def, parent); 497 for (ConceptPropertyComponent pc : pcl) { 498 String code = pc.getValue().primitiveValue(); 499 ConceptDefinitionComponent tgt = findCode(root, code); 500 if (!tgt.hasUserData(USER_DATA_CROSS_LINK)) { 501 tgt.setUserData(USER_DATA_CROSS_LINK, new ArrayList<>()); 502 } 503 @SuppressWarnings("unchecked") 504 List<ConceptDefinitionComponent> children = (List<ConceptDefinitionComponent>) tgt.getUserData(USER_DATA_CROSS_LINK); 505 children.add(def); 506 } 507 if (def.hasConcept()) { 508 crossLinkConcepts(root, def.getConcept(), parent); 509 } 510 } 511 512 } 513 514 public static boolean hasHierarchy(CodeSystem cs) { 515 for (ConceptDefinitionComponent c : cs.getConcept()) { 516 if (c.hasConcept()) { 517 return true; 518 } 519 } 520 return false; 521 } 522 523 public static void sortAllCodes(CodeSystem cs) { 524 sortAllCodes(cs.getConcept()); 525 } 526 527 private static void sortAllCodes(List<ConceptDefinitionComponent> list) { 528 Collections.sort(list, new ConceptDefinitionComponentSorter()); 529 for (ConceptDefinitionComponent cd : list) { 530 if (cd.hasConcept()) { 531 sortAllCodes(cd.getConcept()); 532 } 533 } 534 } 535 536}