001package org.hl7.fhir.r4.conformance; 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.Arrays; 036import java.util.Collections; 037import java.util.Comparator; 038import java.util.HashSet; 039import java.util.LinkedList; 040import java.util.List; 041 042import org.apache.commons.lang3.StringUtils; 043import org.apache.commons.lang3.tuple.ImmutablePair; 044import org.apache.commons.lang3.tuple.Pair; 045import org.hl7.fhir.exceptions.FHIRException; 046import org.hl7.fhir.r4.context.IWorkerContext; 047import org.hl7.fhir.r4.model.Constants; 048import org.hl7.fhir.r4.model.DomainResource; 049import org.hl7.fhir.r4.model.ElementDefinition; 050import org.hl7.fhir.r4.model.Enumerations; 051import org.hl7.fhir.r4.model.StructureDefinition; 052import org.hl7.fhir.r4.model.Type; 053import org.hl7.fhir.r4.model.ValueSet; 054import org.hl7.fhir.r4.terminologies.ValueSetExpander; 055import org.hl7.fhir.r4.utils.ToolingExtensions; 056import org.stringtemplate.v4.ST; 057 058public class ShExGenerator { 059 060 public enum HTMLLinkPolicy { 061 NONE, EXTERNAL, INTERNAL 062 } 063 public boolean doDatatypes = true; // add data types 064 public boolean withComments = true; // include comments 065 public boolean completeModel = false; // doing complete build (fhir.shex) 066 067 068 private static String SHEX_TEMPLATE = "$header$\n\n" + 069 "$shapeDefinitions$"; 070 071 // A header is a list of prefixes, a base declaration and a start node 072 private static String FHIR = "http://hl7.org/fhir/"; 073 private static String FHIR_VS = FHIR + "ValueSet/"; 074 private static String HEADER_TEMPLATE = 075 "PREFIX fhir: <$fhir$> \n" + 076 "PREFIX fhirvs: <$fhirvs$>\n" + 077 "PREFIX xsd: <http://www.w3.org/2001/XMLSchema#> \n" + 078 "BASE <http://hl7.org/fhir/shape/>\n$start$"; 079 080 // Start template for single (open) entry 081 private static String START_TEMPLATE = "\n\nstart=@<$id$> AND {fhir:nodeRole [fhir:treeRoot]}\n"; 082 083 // Start template for complete (closed) model 084 private static String ALL_START_TEMPLATE = "\n\nstart=@<All>\n"; 085 086 private static String ALL_TEMPLATE = "\n<All> $all_entries$\n"; 087 088 private static String ALL_ENTRY_TEMPLATE = "(NOT { fhir:nodeRole [fhir:treeRoot] ; a [fhir:$id$] } OR @<$id$>)"; 089 090 091 // Shape Definition 092 // the shape name 093 // an optional resource declaration (type + treeRoot) 094 // the list of element declarations 095 // an optional index element (for appearances inside ordered lists) 096 private static String SHAPE_DEFINITION_TEMPLATE = 097 "$comment$\n<$id$> CLOSED {\n $resourceDecl$" + 098 "\n $elements$" + 099 "\n fhir:index xsd:integer? # Relative position in a list\n}\n"; 100 101 // Resource Definition 102 // an open shape of type Resource. Used when completeModel = false. 103 private static String RESOURCE_SHAPE_TEMPLATE = 104 "$comment$\n<Resource> {a .+;" + 105 "\n $elements$" + 106 "\n fhir:index xsd:integer?" + 107 "\n}\n"; 108 109 // If we have knowledge of all of the possible resources available to us (completeModel = true), we can build 110 // a model of all possible resources. 111 private static String COMPLETE_RESOURCE_TEMPLATE = 112 "<Resource> @<$resources$>" + 113 "\n\n"; 114 115 // Resource Declaration 116 // a type node 117 // an optional treeRoot declaration (identifies the entry point) 118 private static String RESOURCE_DECL_TEMPLATE = "\na [fhir:$id$];$root$"; 119 120 // Root Declaration. 121 private static String ROOT_TEMPLATE = "\nfhir:nodeRole [fhir:treeRoot]?;"; 122 123 // Element 124 // a predicate, type and cardinality triple 125 private static String ELEMENT_TEMPLATE = "$id$$defn$$card$$comment$"; 126 private static int COMMENT_COL = 40; 127 private static int MAX_CHARS = 35; 128 private static int MIN_COMMENT_SEP = 2; 129 130 // Inner Shape Definition 131 private static String INNER_SHAPE_TEMPLATE = "($comment$\n $defn$\n)$card$"; 132 133 // Simple Element 134 // a shape reference 135 private static String SIMPLE_ELEMENT_DEFN_TEMPLATE = "@<$typ$>$vsdef$"; 136 137 // Value Set Element 138 private static String VALUESET_DEFN_TEMPLATE = " AND\n\t{fhir:value @$vsn$}"; 139 140 // Fixed Value Template 141 private static String FIXED_VALUE_TEMPLATE = " AND\n\t{fhir:value [\"$val$\"]}"; 142 143 // A primitive element definition 144 // the actual type reference 145 private static String PRIMITIVE_ELEMENT_DEFN_TEMPLATE = "$typ$$facets$"; 146 147 // Facets 148 private static String MINVALUE_TEMPLATE = " MININCLUSIVE $val$"; 149 private static String MAXVALUE_TEMPLATE = " MAXINCLUSIVE $val$"; 150 private static String MAXLENGTH_TEMPLATE = " MAXLENGTH $val$"; 151 private static String PATTERN_TEMPLATE = " PATTERN \"$val$\""; 152 153 // A choice of alternative shape definitions 154 // rendered as an inner anonymous shape 155 private static String ALTERNATIVE_SHAPES_TEMPLATE = "fhir:$id$$comment$\n( $altEntries$\n)$card$"; 156 157 // A typed reference definition 158 private static String REFERENCE_DEFN_TEMPLATE = "@<$ref$Reference>"; 159 160 // What we emit for an xhtml 161 private static String XHTML_TYPE_TEMPLATE = "xsd:string"; 162 163 // Additional type for Coding 164 private static String CONCEPT_REFERENCE_TEMPLATE = "a NONLITERAL?;"; 165 166 // Additional type for CodedConcept 167 private static String CONCEPT_REFERENCES_TEMPLATE = "a NONLITERAL*;"; 168 169 // Untyped resource has the extra link entry 170 private static String RESOURCE_LINK_TEMPLATE = "fhir:link IRI?;"; 171 172 // Extension template 173 // No longer used -- we emit the actual definition 174// private static String EXTENSION_TEMPLATE = "<Extension> {fhir:extension @<Extension>*;" + 175// "\n fhir:index xsd:integer?" + 176// "\n}\n"; 177 178 // A typed reference -- a fhir:uri with an optional type and the possibility of a resolvable shape 179 private static String TYPED_REFERENCE_TEMPLATE = "\n<$refType$Reference> CLOSED {" + 180 "\n fhir:Element.id @<id>?;" + 181 "\n fhir:Element.extension @<Extension>*;" + 182 "\n fhir:link @<$refType$> OR CLOSED {a [fhir:$refType$]}?;" + 183 "\n fhir:Reference.reference @<string>?;" + 184 "\n fhir:Reference.display @<string>?;" + 185 "\n fhir:index xsd:integer?" + 186 "\n}"; 187 188 private static String TARGET_REFERENCE_TEMPLATE = "\n<$refType$> {" + 189 "\n a [fhir:$refType$];" + 190 "\n fhir:nodeRole [fhir:treeRoot]?" + 191 "\n}"; 192 193 // A value set definition 194 private static String VALUE_SET_DEFINITION = "# $comment$\n$vsuri$$val_list$\n"; 195 196 197 /** 198 * this makes internal metadata services available to the generator - retrieving structure definitions, and value set expansion etc 199 */ 200 private IWorkerContext context; 201 202 /** 203 * innerTypes -- inner complex types. Currently flattened in ShEx (doesn't have to be, btw) 204 * emittedInnerTypes -- set of inner types that have been generated 205 * datatypes, emittedDatatypes -- types used in the definition, types that have been generated 206 * references -- Reference types (Patient, Specimen, etc) 207 * uniq_structures -- set of structures on the to be generated list... 208 * doDataTypes -- whether or not to emit the data types. 209 */ 210 private HashSet<Pair<StructureDefinition, ElementDefinition>> innerTypes, emittedInnerTypes; 211 private HashSet<String> datatypes, emittedDatatypes; 212 private HashSet<String> references; 213 private LinkedList<StructureDefinition> uniq_structures; 214 private HashSet<String> uniq_structure_urls; 215 private HashSet<ValueSet> required_value_sets; 216 private HashSet<String> known_resources; // Used when generating a full definition 217 218 public ShExGenerator(IWorkerContext context) { 219 super(); 220 this.context = context; 221 innerTypes = new HashSet<Pair<StructureDefinition, ElementDefinition>>(); 222 emittedInnerTypes = new HashSet<Pair<StructureDefinition, ElementDefinition>>(); 223 datatypes = new HashSet<String>(); 224 emittedDatatypes = new HashSet<String>(); 225 references = new HashSet<String>(); 226 required_value_sets = new HashSet<ValueSet>(); 227 known_resources = new HashSet<String>(); 228 } 229 230 public String generate(HTMLLinkPolicy links, StructureDefinition structure) { 231 List<StructureDefinition> list = new ArrayList<StructureDefinition>(); 232 list.add(structure); 233 innerTypes.clear(); 234 emittedInnerTypes.clear(); 235 datatypes.clear(); 236 emittedDatatypes.clear(); 237 references.clear(); 238 required_value_sets.clear(); 239 known_resources.clear(); 240 return generate(links, list); 241 } 242 243 public class SortById implements Comparator<StructureDefinition> { 244 245 @Override 246 public int compare(StructureDefinition arg0, StructureDefinition arg1) { 247 return arg0.getId().compareTo(arg1.getId()); 248 } 249 250 } 251 252 private ST tmplt(String template) { 253 return new ST(template, '$', '$'); 254 } 255 256 /** 257 * this is called externally to generate a set of structures to a single ShEx file 258 * generally, it will be called with a single structure, or a long list of structures (all of them) 259 * 260 * @param links HTML link rendering policy 261 * @param structures list of structure definitions to render 262 * @return ShEx definition of structures 263 */ 264 public String generate(HTMLLinkPolicy links, List<StructureDefinition> structures) { 265 266 ST shex_def = tmplt(SHEX_TEMPLATE); 267 String start_cmd; 268 if(completeModel || structures.get(0).getKind().equals(StructureDefinition.StructureDefinitionKind.RESOURCE)) 269// || structures.get(0).getKind().equals(StructureDefinition.StructureDefinitionKind.COMPLEXTYPE)) 270 start_cmd = completeModel? tmplt(ALL_START_TEMPLATE).render() : 271 tmplt(START_TEMPLATE).add("id", structures.get(0).getId()).render(); 272 else 273 start_cmd = ""; 274 shex_def.add("header", tmplt(HEADER_TEMPLATE). 275 add("start", start_cmd). 276 add("fhir", FHIR). 277 add("fhirvs", FHIR_VS).render()); 278 279 Collections.sort(structures, new SortById()); 280 StringBuilder shapeDefinitions = new StringBuilder(); 281 282 // For unknown reasons, the list of structures carries duplicates. We remove them 283 // Also, it is possible for the same sd to have multiple hashes... 284 uniq_structures = new LinkedList<StructureDefinition>(); 285 uniq_structure_urls = new HashSet<String>(); 286 for (StructureDefinition sd : structures) { 287 if (!uniq_structure_urls.contains(sd.getUrl())) { 288 uniq_structures.add(sd); 289 uniq_structure_urls.add(sd.getUrl()); 290 } 291 } 292 293 294 for (StructureDefinition sd : uniq_structures) { 295 shapeDefinitions.append(genShapeDefinition(sd, true)); 296 } 297 298 shapeDefinitions.append(emitInnerTypes()); 299 if(doDatatypes) { 300 shapeDefinitions.append("\n#---------------------- Data Types -------------------\n"); 301 while (emittedDatatypes.size() < datatypes.size() || 302 emittedInnerTypes.size() < innerTypes.size()) { 303 shapeDefinitions.append(emitDataTypes()); 304 shapeDefinitions.append(emitInnerTypes()); 305 } 306 } 307 308 shapeDefinitions.append("\n#---------------------- Reference Types -------------------\n"); 309 for(String r: references) { 310 shapeDefinitions.append("\n").append(tmplt(TYPED_REFERENCE_TEMPLATE).add("refType", r).render()).append("\n"); 311 if (!"Resource".equals(r) && !known_resources.contains(r)) 312 shapeDefinitions.append("\n").append(tmplt(TARGET_REFERENCE_TEMPLATE).add("refType", r).render()).append("\n"); 313 } 314 shex_def.add("shapeDefinitions", shapeDefinitions); 315 316 if(completeModel && known_resources.size() > 0) { 317 shapeDefinitions.append("\n").append(tmplt(COMPLETE_RESOURCE_TEMPLATE) 318 .add("resources", StringUtils.join(known_resources, "> OR\n\t@<")).render()); 319 List<String> all_entries = new ArrayList<String>(); 320 for(String kr: known_resources) 321 all_entries.add(tmplt(ALL_ENTRY_TEMPLATE).add("id", kr).render()); 322 shapeDefinitions.append("\n").append(tmplt(ALL_TEMPLATE) 323 .add("all_entries", StringUtils.join(all_entries, " OR\n\t")).render()); 324 } 325 326 shapeDefinitions.append("\n#---------------------- Value Sets ------------------------\n"); 327 for(ValueSet vs: required_value_sets) 328 shapeDefinitions.append("\n").append(genValueSet(vs)); 329 return shex_def.render(); 330 } 331 332 333 /** 334 * Emit a ShEx definition for the supplied StructureDefinition 335 * @param sd Structure definition to emit 336 * @param top_level True means outermost type, False means recursively called 337 * @return ShEx definition 338 */ 339 private String genShapeDefinition(StructureDefinition sd, boolean top_level) { 340 // xhtml is treated as an atom 341 if("xhtml".equals(sd.getName()) || (completeModel && "Resource".equals(sd.getName()))) 342 return ""; 343 344 ST shape_defn; 345 // Resources are either incomplete items or consist of everything that is defined as a resource (completeModel) 346 if("Resource".equals(sd.getName())) { 347 shape_defn = tmplt(RESOURCE_SHAPE_TEMPLATE); 348 known_resources.add(sd.getName()); 349 } else { 350 shape_defn = tmplt(SHAPE_DEFINITION_TEMPLATE).add("id", sd.getId()); 351 if (sd.getKind().equals(StructureDefinition.StructureDefinitionKind.RESOURCE)) { 352// || sd.getKind().equals(StructureDefinition.StructureDefinitionKind.COMPLEXTYPE)) { 353 known_resources.add(sd.getName()); 354 ST resource_decl = tmplt(RESOURCE_DECL_TEMPLATE). 355 add("id", sd.getId()). 356 add("root", tmplt(ROOT_TEMPLATE)); 357// add("root", top_level ? tmplt(ROOT_TEMPLATE) : ""); 358 shape_defn.add("resourceDecl", resource_decl.render()); 359 } else { 360 shape_defn.add("resourceDecl", ""); 361 } 362 } 363 364 // Generate the defining elements 365 List<String> elements = new ArrayList<String>(); 366 367 // Add the additional entries for special types 368 String sdn = sd.getName(); 369 if (sdn.equals("Coding")) 370 elements.add(tmplt(CONCEPT_REFERENCE_TEMPLATE).render()); 371 else if (sdn.equals("CodeableConcept")) 372 elements.add(tmplt(CONCEPT_REFERENCES_TEMPLATE).render()); 373 else if (sdn.equals("Reference")) 374 elements.add(tmplt(RESOURCE_LINK_TEMPLATE).render()); 375// else if (sdn.equals("Extension")) 376// return tmplt(EXTENSION_TEMPLATE).render(); 377 378 String root_comment = null; 379 for (ElementDefinition ed : sd.getSnapshot().getElement()) { 380 if(!ed.getPath().contains(".")) 381 root_comment = ed.getShort(); 382 else if (StringUtils.countMatches(ed.getPath(), ".") == 1 && !"0".equals(ed.getMax())) { 383 elements.add(genElementDefinition(sd, ed)); 384 } 385 } 386 shape_defn.add("elements", StringUtils.join(elements, "\n")); 387 shape_defn.add("comment", root_comment == null? " " : "# " + root_comment); 388 return shape_defn.render(); 389 } 390 391 392 /** 393 * Generate a flattened definition for the inner types 394 * @return stringified inner type definitions 395 */ 396 private String emitInnerTypes() { 397 StringBuilder itDefs = new StringBuilder(); 398 while(emittedInnerTypes.size() < innerTypes.size()) { 399 for (Pair<StructureDefinition, ElementDefinition> it : new HashSet<Pair<StructureDefinition, ElementDefinition>>(innerTypes)) { 400 if (!emittedInnerTypes.contains(it)) { 401 itDefs.append("\n").append(genInnerTypeDef(it.getLeft(), it.getRight())); 402 emittedInnerTypes.add(it); 403 } 404 } 405 } 406 return itDefs.toString(); 407 } 408 409 /** 410 * Generate a shape definition for the current set of datatypes 411 * @return stringified data type definitions 412 */ 413 private String emitDataTypes() { 414 StringBuilder dtDefs = new StringBuilder(); 415 while (emittedDatatypes.size() < datatypes.size()) { 416 for (String dt : new HashSet<String>(datatypes)) { 417 if (!emittedDatatypes.contains(dt)) { 418 StructureDefinition sd = context.fetchResource(StructureDefinition.class, 419 ProfileUtilities.sdNs(dt, null)); 420 // TODO: Figure out why the line below doesn't work 421 // if (sd != null && !uniq_structures.contains(sd)) 422 if(sd != null && !uniq_structure_urls.contains(sd.getUrl())) 423 dtDefs.append("\n").append(genShapeDefinition(sd, false)); 424 emittedDatatypes.add(dt); 425 } 426 } 427 } 428 return dtDefs.toString(); 429 } 430 431 private ArrayList<String> split_text(String text, int max_col) { 432 int pos = 0; 433 ArrayList<String> rval = new ArrayList<String>(); 434 if (text.length() <= max_col) { 435 rval.add(text); 436 } else { 437 String[] words = text.split(" "); 438 int word_idx = 0; 439 while(word_idx < words.length) { 440 StringBuilder accum = new StringBuilder(); 441 while (word_idx < words.length && accum.length() + words[word_idx].length() < max_col) 442 accum.append(words[word_idx++] + " "); 443 if (accum.length() == 0) { 444 accum.append(words[word_idx].substring(0, max_col - 3) + "-"); 445 words[word_idx] = words[word_idx].substring(max_col - 3); 446 } 447 rval.add(accum.toString()); 448 accum = new StringBuilder(); 449 } 450 } 451 return rval; 452 } 453 454 private void addComment(ST tmplt, ElementDefinition ed) { 455 if(withComments && ed.hasShort() && !ed.getId().startsWith("Extension.")) { 456 int nspaces; 457 char[] sep; 458 nspaces = Integer.max(COMMENT_COL - tmplt.add("comment", "#").render().indexOf('#'), MIN_COMMENT_SEP); 459 tmplt.remove("comment"); 460 sep = new char[nspaces]; 461 Arrays.fill(sep, ' '); 462 ArrayList<String> comment_lines = split_text(ed.getShort().replace("\n", " "), MAX_CHARS); 463 StringBuilder comment = new StringBuilder("# "); 464 char[] indent = new char[COMMENT_COL]; 465 Arrays.fill(indent, ' '); 466 for(int i = 0; i < comment_lines.size();) { 467 comment.append(comment_lines.get(i++)); 468 if(i < comment_lines.size()) 469 comment.append("\n" + new String(indent) + "# "); 470 } 471 tmplt.add("comment", new String(sep) + comment.toString()); 472 } else { 473 tmplt.add("comment", " "); 474 } 475 } 476 477 478 /** 479 * Generate a ShEx element definition 480 * @param sd Containing structure definition 481 * @param ed Containing element definition 482 * @return ShEx definition 483 */ 484 private String genElementDefinition(StructureDefinition sd, ElementDefinition ed) { 485 String id = ed.hasBase() ? ed.getBase().getPath() : ed.getPath(); 486 String shortId = id.substring(id.lastIndexOf(".") + 1); 487 String defn; 488 ST element_def; 489 String card = ("*".equals(ed.getMax()) ? (ed.getMin() == 0 ? "*" : "+") : (ed.getMin() == 0 ? "?" : "")) + ";"; 490 491 if(id.endsWith("[x]")) { 492 element_def = ed.getType().size() > 1? tmplt(INNER_SHAPE_TEMPLATE) : tmplt(ELEMENT_TEMPLATE); 493 element_def.add("id", ""); 494 } else { 495 element_def = tmplt(ELEMENT_TEMPLATE); 496 element_def.add("id", "fhir:" + (id.charAt(0) == id.toLowerCase().charAt(0)? shortId : id) + " "); 497 } 498 499 List<ElementDefinition> children = ProfileUtilities.getChildList(sd, ed); 500 if (children.size() > 0) { 501 innerTypes.add(new ImmutablePair<StructureDefinition, ElementDefinition>(sd, ed)); 502 defn = simpleElement(sd, ed, id); 503 } else if(id.endsWith("[x]")) { 504 defn = genChoiceTypes(sd, ed, id); 505 } 506 else if (ed.getType().size() == 1) { 507 // Single entry 508 defn = genTypeRef(sd, ed, id, ed.getType().get(0)); 509 } else if (ed.getContentReference() != null) { 510 // Reference to another element 511 String ref = ed.getContentReference(); 512 if(!ref.startsWith("#")) 513 throw new AssertionError("Not equipped to deal with absolute path references: " + ref); 514 String refPath = null; 515 for(ElementDefinition ed1: sd.getSnapshot().getElement()) { 516 if(ed1.getId() != null && ed1.getId().equals(ref.substring(1))) { 517 refPath = ed1.getPath(); 518 break; 519 } 520 } 521 if(refPath == null) 522 throw new AssertionError("Reference path not found: " + ref); 523 // String typ = id.substring(0, id.indexOf(".") + 1) + ed.getContentReference().substring(1); 524 defn = simpleElement(sd, ed, refPath); 525 } else if(id.endsWith("[x]")) { 526 defn = genChoiceTypes(sd, ed, id); 527 } else { 528 // TODO: Refactoring required here 529 element_def = genAlternativeTypes(ed, id, shortId); 530 element_def.add("id", id.charAt(0) == id.toLowerCase().charAt(0)? shortId : id); 531 element_def.add("card", card); 532 addComment(element_def, ed); 533 return element_def.render(); 534 } 535 element_def.add("defn", defn); 536 element_def.add("card", card); 537 addComment(element_def, ed); 538 return element_def.render(); 539 } 540 541 /** 542 * Generate a type reference and optional value set definition 543 * @param sd Containing StructureDefinition 544 * @param ed Element being defined 545 * @param typ Element type 546 * @return Type definition 547 */ 548 private String simpleElement(StructureDefinition sd, ElementDefinition ed, String typ) { 549 String addldef = ""; 550 ElementDefinition.ElementDefinitionBindingComponent binding = ed.getBinding(); 551 if(binding.hasStrength() && binding.getStrength() == Enumerations.BindingStrength.REQUIRED && "code".equals(typ)) { 552 ValueSet vs = resolveBindingReference(sd, binding.getValueSet()); 553 if (vs != null) { 554 addldef = tmplt(VALUESET_DEFN_TEMPLATE).add("vsn", vsprefix(vs.getUrl())).render(); 555 required_value_sets.add(vs); 556 } 557 } 558 // TODO: check whether value sets and fixed are mutually exclusive 559 if(ed.hasFixed()) { 560 addldef = tmplt(FIXED_VALUE_TEMPLATE).add("val", ed.getFixed().primitiveValue()).render(); 561 } 562 return tmplt(SIMPLE_ELEMENT_DEFN_TEMPLATE).add("typ", typ).add("vsdef", addldef).render(); 563 } 564 565 private String vsprefix(String uri) { 566 if(uri.startsWith(FHIR_VS)) 567 return "fhirvs:" + uri.replace(FHIR_VS, ""); 568 return "<" + uri + ">"; 569 } 570 571 /** 572 * Generate a type reference 573 * @param sd Containing structure definition 574 * @param ed Containing element definition 575 * @param id Element id 576 * @param typ Element type 577 * @return Type reference string 578 */ 579 private String genTypeRef(StructureDefinition sd, ElementDefinition ed, String id, ElementDefinition.TypeRefComponent typ) { 580 581 if(typ.hasProfile()) { 582 if(typ.getWorkingCode().equals("Reference")) 583 return genReference("", typ); 584 else if(ProfileUtilities.getChildList(sd, ed).size() > 0) { 585 // inline anonymous type - give it a name and factor it out 586 innerTypes.add(new ImmutablePair<StructureDefinition, ElementDefinition>(sd, ed)); 587 return simpleElement(sd, ed, id); 588 } 589 else { 590 String ref = getTypeName(typ); 591 datatypes.add(ref); 592 return simpleElement(sd, ed, ref); 593 } 594 595 } else if (typ.getWorkingCode().startsWith(Constants.NS_SYSTEM_TYPE)) { 596 String xt = typ.getWorkingCode(); 597 // TODO: Remove the next line when the type of token gets switched to string 598 // TODO: Add a rdf-type entry for valueInteger to xsd:integer (instead of int) 599 ST td_entry = tmplt(PRIMITIVE_ELEMENT_DEFN_TEMPLATE).add("typ", 600 xt.replace("xsd:token", "xsd:string").replace("xsd:int", "xsd:integer")); 601 StringBuilder facets = new StringBuilder(); 602 if(ed.hasMinValue()) { 603 Type mv = ed.getMinValue(); 604 facets.append(tmplt(MINVALUE_TEMPLATE).add("val", mv.primitiveValue()).render()); 605 } 606 if(ed.hasMaxValue()) { 607 Type mv = ed.getMaxValue(); 608 facets.append(tmplt(MAXVALUE_TEMPLATE).add("val", mv.primitiveValue()).render()); 609 } 610 if(ed.hasMaxLength()) { 611 int ml = ed.getMaxLength(); 612 facets.append(tmplt(MAXLENGTH_TEMPLATE).add("val", ml).render()); 613 } 614 if(ed.hasPattern()) { 615 Type pat = ed.getPattern(); 616 facets.append(tmplt(PATTERN_TEMPLATE).add("val",pat.primitiveValue()).render()); 617 } 618 td_entry.add("facets", facets.toString()); 619 return td_entry.render(); 620 621 } else if (typ.getWorkingCode() == null) { 622 ST primitive_entry = tmplt(PRIMITIVE_ELEMENT_DEFN_TEMPLATE); 623 primitive_entry.add("typ", "xsd:string"); 624 return primitive_entry.render(); 625 626 } else if(typ.getWorkingCode().equals("xhtml")) { 627 return tmplt(XHTML_TYPE_TEMPLATE).render(); 628 } else { 629 datatypes.add(typ.getWorkingCode()); 630 return simpleElement(sd, ed, typ.getWorkingCode()); 631 } 632 } 633 634 /** 635 * Generate a set of alternative shapes 636 * @param ed Containing element definition 637 * @param id Element definition identifier 638 * @param shortId id to use in the actual definition 639 * @return ShEx list of alternative anonymous shapes separated by "OR" 640 */ 641 private ST genAlternativeTypes(ElementDefinition ed, String id, String shortId) { 642 ST shex_alt = tmplt(ALTERNATIVE_SHAPES_TEMPLATE); 643 List<String> altEntries = new ArrayList<String>(); 644 645 646 for(ElementDefinition.TypeRefComponent typ : ed.getType()) { 647 altEntries.add(genAltEntry(id, typ)); 648 } 649 shex_alt.add("altEntries", StringUtils.join(altEntries, " OR\n ")); 650 return shex_alt; 651 } 652 653 654 655 /** 656 * Generate an alternative shape for a reference 657 * @param id reference name 658 * @param typ shape type 659 * @return ShEx equivalent 660 */ 661 private String genAltEntry(String id, ElementDefinition.TypeRefComponent typ) { 662 if(!typ.getWorkingCode().equals("Reference")) 663 throw new AssertionError("We do not handle " + typ.getWorkingCode() + " alternatives"); 664 665 return genReference(id, typ); 666 } 667 668 /** 669 * Generate a list of type choices for a "name[x]" style id 670 * @param sd Structure containing ed 671 * @param ed element definition 672 * @param id choice identifier 673 * @return ShEx fragment for the set of choices 674 */ 675 private String genChoiceTypes(StructureDefinition sd, ElementDefinition ed, String id) { 676 List<String> choiceEntries = new ArrayList<String>(); 677 String base = id.replace("[x]", ""); 678 679 for(ElementDefinition.TypeRefComponent typ : ed.getType()) 680 choiceEntries.add(genChoiceEntry(sd, ed, id, base, typ)); 681 682 return StringUtils.join(choiceEntries, " |\n"); 683 } 684 685 /** 686 * Generate an entry in a choice list 687 * @param base base identifier 688 * @param typ type/discriminant 689 * @return ShEx fragment for choice entry 690 */ 691 private String genChoiceEntry(StructureDefinition sd, ElementDefinition ed, String id, String base, ElementDefinition.TypeRefComponent typ) { 692 ST shex_choice_entry = tmplt(ELEMENT_TEMPLATE); 693 694 String ext = typ.getWorkingCode(); 695 shex_choice_entry.add("id", "fhir:" + base+Character.toUpperCase(ext.charAt(0)) + ext.substring(1) + " "); 696 shex_choice_entry.add("card", ""); 697 shex_choice_entry.add("defn", genTypeRef(sd, ed, id, typ)); 698 shex_choice_entry.add("comment", " "); 699 return shex_choice_entry.render(); 700 } 701 702 /** 703 * Generate a definition for a referenced element 704 * @param sd Containing structure definition 705 * @param ed Inner element 706 * @return ShEx representation of element reference 707 */ 708 private String genInnerTypeDef(StructureDefinition sd, ElementDefinition ed) { 709 String path = ed.hasBase() ? ed.getBase().getPath() : ed.getPath();; 710 ST element_reference = tmplt(SHAPE_DEFINITION_TEMPLATE); 711 element_reference.add("resourceDecl", ""); // Not a resource 712 element_reference.add("id", path); 713 String comment = ed.getShort(); 714 element_reference.add("comment", comment == null? " " : "# " + comment); 715 716 List<String> elements = new ArrayList<String>(); 717 for (ElementDefinition child: ProfileUtilities.getChildList(sd, path, null)) 718 elements.add(genElementDefinition(sd, child)); 719 720 element_reference.add("elements", StringUtils.join(elements, "\n")); 721 return element_reference.render(); 722 } 723 724 /** 725 * Generate a reference to a resource 726 * @param id attribute identifier 727 * @param typ possible reference types 728 * @return string that represents the result 729 */ 730 private String genReference(String id, ElementDefinition.TypeRefComponent typ) { 731 ST shex_ref = tmplt(REFERENCE_DEFN_TEMPLATE); 732 733 String ref = getTypeName(typ); 734 shex_ref.add("id", id); 735 shex_ref.add("ref", ref); 736 references.add(ref); 737 return shex_ref.render(); 738 } 739 740 /** 741 * Return the type name for typ 742 * @param typ type to get name for 743 * @return name 744 */ 745 private String getTypeName(ElementDefinition.TypeRefComponent typ) { 746 // TODO: This is brittle. There has to be a utility to do this... 747 if (typ.hasTargetProfile()) { 748 String[] els = typ.getTargetProfile().get(0).getValue().split("/"); 749 return els[els.length - 1]; 750 } else if (typ.hasProfile()) { 751 String[] els = typ.getProfile().get(0).getValue().split("/"); 752 return els[els.length - 1]; 753 } else { 754 return typ.getWorkingCode(); 755 } 756 } 757 758 private String genValueSet(ValueSet vs) { 759 ST vsd = tmplt(VALUE_SET_DEFINITION).add("vsuri", vsprefix(vs.getUrl())).add("comment", vs.getDescription()); 760 ValueSetExpander.ValueSetExpansionOutcome vse = context.expandVS(vs, true, false); 761 List<String> valid_codes = new ArrayList<String>(); 762 if(vse != null && 763 vse.getValueset() != null && 764 vse.getValueset().hasExpansion() && 765 vse.getValueset().getExpansion().hasContains()) { 766 for(ValueSet.ValueSetExpansionContainsComponent vsec : vse.getValueset().getExpansion().getContains()) 767 valid_codes.add("\"" + vsec.getCode() + "\""); 768 } 769 return vsd.add("val_list", valid_codes.size() > 0? " [" + StringUtils.join(valid_codes, " ") + ']' : " EXTERNAL").render(); 770 } 771 772 773 // TODO: find a utility that implements this 774 private ValueSet resolveBindingReference(DomainResource ctxt, String reference) { 775 try { 776 return context.fetchResource(ValueSet.class, reference); 777 } catch (Throwable e) { 778 return null; 779 } 780 } 781}