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.context.IWorkerContext; 046import org.hl7.fhir.r5.model.Constants; 047import org.hl7.fhir.r5.model.DataType; 048import org.hl7.fhir.r5.model.DomainResource; 049import org.hl7.fhir.r5.model.ElementDefinition; 050import org.hl7.fhir.r5.model.Enumerations; 051import org.hl7.fhir.r5.model.StructureDefinition; 052import org.hl7.fhir.r5.model.ValueSet; 053import org.hl7.fhir.r5.terminologies.ValueSetExpander; 054import org.stringtemplate.v4.ST; 055 056public class ShExGenerator { 057 058 public enum HTMLLinkPolicy { 059 NONE, EXTERNAL, INTERNAL 060 } 061 public boolean doDatatypes = true; // add data types 062 public boolean withComments = true; // include comments 063 public boolean completeModel = false; // doing complete build (fhir.shex) 064 065 066 private static String SHEX_TEMPLATE = "$header$\n\n" + 067 "$shapeDefinitions$"; 068 069 // A header is a list of prefixes, a base declaration and a start node 070 private static String FHIR = "http://hl7.org/fhir/"; 071 private static String FHIR_VS = FHIR + "ValueSet/"; 072 private static String HEADER_TEMPLATE = 073 "PREFIX fhir: <$fhir$> \n" + 074 "PREFIX fhirvs: <$fhirvs$>\n" + 075 "PREFIX xsd: <http://www.w3.org/2001/XMLSchema#> \n" + 076 "BASE <http://hl7.org/fhir/shape/>\n$start$"; 077 078 // Start template for single (open) entry 079 private static String START_TEMPLATE = "\n\nstart=@<$id$> AND {fhir:nodeRole [fhir:treeRoot]}\n"; 080 081 // Start template for complete (closed) model 082 private static String ALL_START_TEMPLATE = "\n\nstart=@<All>\n"; 083 084 private static String ALL_TEMPLATE = "\n<All> $all_entries$\n"; 085 086 private static String ALL_ENTRY_TEMPLATE = "(NOT { fhir:nodeRole [fhir:treeRoot] ; a [fhir:$id$] } OR @<$id$>)"; 087 088 089 // Shape Definition 090 // the shape name 091 // an optional resource declaration (type + treeRoot) 092 // the list of element declarations 093 // an optional index element (for appearances inside ordered lists) 094 private static String SHAPE_DEFINITION_TEMPLATE = 095 "$comment$\n<$id$> CLOSED {\n $resourceDecl$" + 096 "\n $elements$" + 097 "\n fhir:index xsd:integer? # Relative position in a list\n}\n"; 098 099 // Resource Definition 100 // an open shape of type Resource. Used when completeModel = false. 101 private static String RESOURCE_SHAPE_TEMPLATE = 102 "$comment$\n<Resource> {a .+;" + 103 "\n $elements$" + 104 "\n fhir:index xsd:integer?" + 105 "\n}\n"; 106 107 // If we have knowledge of all of the possible resources available to us (completeModel = true), we can build 108 // a model of all possible resources. 109 private static String COMPLETE_RESOURCE_TEMPLATE = 110 "<Resource> @<$resources$>" + 111 "\n\n"; 112 113 // Resource Declaration 114 // a type node 115 // an optional treeRoot declaration (identifies the entry point) 116 private static String RESOURCE_DECL_TEMPLATE = "\na [fhir:$id$];$root$"; 117 118 // Root Declaration. 119 private static String ROOT_TEMPLATE = "\nfhir:nodeRole [fhir:treeRoot]?;"; 120 121 // Element 122 // a predicate, type and cardinality triple 123 private static String ELEMENT_TEMPLATE = "$id$$defn$$card$$comment$"; 124 private static int COMMENT_COL = 40; 125 private static int MAX_CHARS = 35; 126 private static int MIN_COMMENT_SEP = 2; 127 128 // Inner Shape Definition 129 private static String INNER_SHAPE_TEMPLATE = "($comment$\n $defn$\n)$card$"; 130 131 // Simple Element 132 // a shape reference 133 private static String SIMPLE_ELEMENT_DEFN_TEMPLATE = "@<$typ$>$vsdef$"; 134 135 // Value Set Element 136 private static String VALUESET_DEFN_TEMPLATE = " AND\n\t{fhir:value @$vsn$}"; 137 138 // Fixed Value Template 139 private static String FIXED_VALUE_TEMPLATE = " AND\n\t{fhir:value [\"$val$\"]}"; 140 141 // A primitive element definition 142 // the actual type reference 143 private static String PRIMITIVE_ELEMENT_DEFN_TEMPLATE = "$typ$$facets$"; 144 145 // Facets 146 private static String MINVALUE_TEMPLATE = " MININCLUSIVE $val$"; 147 private static String MAXVALUE_TEMPLATE = " MAXINCLUSIVE $val$"; 148 private static String MAXLENGTH_TEMPLATE = " MAXLENGTH $val$"; 149 private static String PATTERN_TEMPLATE = " PATTERN \"$val$\""; 150 151 // A choice of alternative shape definitions 152 // rendered as an inner anonymous shape 153 private static String ALTERNATIVE_SHAPES_TEMPLATE = "fhir:$id$$comment$\n( $altEntries$\n)$card$"; 154 155 // A typed reference definition 156 private static String REFERENCE_DEFN_TEMPLATE = "@<$ref$Reference>"; 157 158 // What we emit for an xhtml 159 private static String XHTML_TYPE_TEMPLATE = "xsd:string"; 160 161 // Additional type for Coding 162 private static String CONCEPT_REFERENCE_TEMPLATE = "a NONLITERAL?;"; 163 164 // Additional type for CodedConcept 165 private static String CONCEPT_REFERENCES_TEMPLATE = "a NONLITERAL*;"; 166 167 // Untyped resource has the extra link entry 168 private static String RESOURCE_LINK_TEMPLATE = "fhir:link IRI?;"; 169 170 // Extension template 171 // No longer used -- we emit the actual definition 172// private static String EXTENSION_TEMPLATE = "<Extension> {fhir:extension @<Extension>*;" + 173// "\n fhir:index xsd:integer?" + 174// "\n}\n"; 175 176 // A typed reference -- a fhir:uri with an optional type and the possibility of a resolvable shape 177 private static String TYPED_REFERENCE_TEMPLATE = "\n<$refType$Reference> CLOSED {" + 178 "\n fhir:Element.id @<id>?;" + 179 "\n fhir:Element.extension @<Extension>*;" + 180 "\n fhir:link @<$refType$> OR CLOSED {a [fhir:$refType$]}?;" + 181 "\n fhir:Reference.reference @<string>?;" + 182 "\n fhir:Reference.display @<string>?;" + 183 "\n fhir:index xsd:integer?" + 184 "\n}"; 185 186 private static String TARGET_REFERENCE_TEMPLATE = "\n<$refType$> {" + 187 "\n a [fhir:$refType$];" + 188 "\n fhir:nodeRole [fhir:treeRoot]?" + 189 "\n}"; 190 191 // A value set definition 192 private static String VALUE_SET_DEFINITION = "# $comment$\n$vsuri$$val_list$\n"; 193 194 195 /** 196 * this makes internal metadata services available to the generator - retrieving structure definitions, and value set expansion etc 197 */ 198 private IWorkerContext context; 199 private ProfileUtilities profileUtilities; 200 201 /** 202 * innerTypes -- inner complex types. Currently flattened in ShEx (doesn't have to be, btw) 203 * emittedInnerTypes -- set of inner types that have been generated 204 * datatypes, emittedDatatypes -- types used in the definition, types that have been generated 205 * references -- Reference types (Patient, Specimen, etc) 206 * uniq_structures -- set of structures on the to be generated list... 207 * doDataTypes -- whether or not to emit the data types. 208 */ 209 private HashSet<Pair<StructureDefinition, ElementDefinition>> innerTypes, emittedInnerTypes; 210 private HashSet<String> datatypes, emittedDatatypes; 211 private HashSet<String> references; 212 private LinkedList<StructureDefinition> uniq_structures; 213 private HashSet<String> uniq_structure_urls; 214 private HashSet<ValueSet> required_value_sets; 215 private HashSet<String> known_resources; // Used when generating a full definition 216 217 public ShExGenerator(IWorkerContext context) { 218 super(); 219 this.context = context; 220 profileUtilities = new ProfileUtilities(context, null, null); 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 DataType mv = ed.getMinValue(); 604 facets.append(tmplt(MINVALUE_TEMPLATE).add("val", mv.primitiveValue()).render()); 605 } 606 if(ed.hasMaxValue()) { 607 DataType 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 DataType 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}