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