001package org.hl7.fhir.dstu2016may.utils; 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.Collections; 036import java.util.Comparator; 037import java.util.HashSet; 038import java.util.LinkedList; 039import java.util.List; 040import java.util.Set; 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.dstu2016may.model.ElementDefinition; 046import org.hl7.fhir.dstu2016may.model.StructureDefinition; 047import org.stringtemplate.v4.ST; 048 049public class ShExGenerator { 050 051 public enum HTMLLinkPolicy { 052 NONE, EXTERNAL, INTERNAL 053 } 054 055 // An entire definition is a header plus a list of shape definitions 056 private static String SHEX_TEMPLATE = 057 "$header$\n\n" + 058 "$shapeDefinitions$"; 059 060 // A header is a list of prefixes plus a BASE 061 private static String HEADER_TEMPLATE = 062 "PREFIX fhir: <http://hl7.org/fhir/> \n" + 063 "PREFIX xsd: <http://www.w3.org/2001/XMLSchema#> \n" + 064 "BASE <http://hl7.org/fhir/shape/>\n"; 065 066 // A shape definition template -- an id followed by an optional resource declaration (type + treeRoot) + 067 // a list of definition elements 068 private static String SHAPE_DEFINITION_TEMPLATE = "<$id$> {\n$resourceDecl$\t$elements$\n}\n"; 069 070 // Additional declaration that appears only in a resource definition 071 private static String RESOURCE_DECL_TEMPLATE = "\n\ta [fhir:$id$],\n\tfhir:nodeRole [fhir:treeRoot],\n"; 072 073 // An element definition within a shape 074 private static String ELEMENT_TEMPLATE = "fhir:$id$ $defn$$card$"; 075 076 // A simple element definition 077 private static String SIMPLE_ELEMENT_TEMPLATE = "@<$typ$>"; 078 079 // A primitive element definition 080 private static String PRIMITIVE_ELEMENT_TEMPLATE = "xsd:$typ$"; 081 082 // A list of alternative shape definitions 083 private static String ALTERNATIVE_TEMPLATE = "\n\t(\t$altEntries$\n\t)"; 084 085 // A typed reference definition 086 private static String REFERENCE_TEMPLATE = "@<$ref$Reference>"; 087 088 // A choice of different predicate / types 089 private static String CHOICE_TEMPLATE = "\n(\t$choiceEntries$\n\t)$card$"; 090 091 // A typed reference -- a fhir:uri with an optional type and the possibility of a resolvable shape 092 private static String TYPED_REFERENCE_TEMPLATE = "\n<$refType$Reference> {\n" + 093 "\ta [fhir:$refType$Reference]?,\n" + 094 "\tfhir:uri.id @<id>?,\n" + 095 "\tfhir:uri.extension @<Extension>*,\n" + 096 "\tfhir:uri.value (xsd:anyURI OR @<$refType$>)\n" + 097 "}"; 098 099 // TODO: find the literal for this 100 private static String XML_DEFN_TYPE = "http://hl7.org/fhir/StructureDefinition/structuredefinition-xml-type"; 101 102 /** 103 * this makes internal metadata services available to the generator - retrieving structure definitions, and value set expansion etc 104 */ 105 private IWorkerContext context; 106 107 /** 108 * List of typeReferences generated per session 109 */ 110 private LinkedList<Pair<StructureDefinition, ElementDefinition>> typeReferences; 111 private HashSet<String> references; 112 113 public ShExGenerator(IWorkerContext context) { 114 super(); 115 this.context = context; 116 this.typeReferences = new LinkedList<Pair<StructureDefinition, ElementDefinition>>(); 117 this.references = new HashSet<String>(); 118 } 119 120 public String generate(HTMLLinkPolicy links, StructureDefinition structure) { 121 List<StructureDefinition> list = new ArrayList<StructureDefinition>(); 122 list.add(structure); 123 this.typeReferences.clear(); 124 this.references.clear(); 125 return generate(links, list); 126 } 127 128 public class SortById implements Comparator<StructureDefinition> { 129 130 @Override 131 public int compare(StructureDefinition arg0, StructureDefinition arg1) { 132 return arg0.getId().compareTo(arg1.getId()); 133 } 134 135 } 136 137 private ST tmplt(String template) { 138 return new ST(template, '$', '$'); 139 } 140 141 /** 142 * this is called externally to generate a set of structures to a single ShEx file 143 * generally, it will be called with a single structure, or a long list of structures (all of them) 144 * 145 * @param links HTML link rendering policy 146 * @param structures list of structure definitions to render 147 * @return ShEx definition of structures 148 */ 149 public String generate(HTMLLinkPolicy links, List<StructureDefinition> structures) { 150 ST shex_def = tmplt(SHEX_TEMPLATE); 151 shex_def.add("header", genHeader()); 152 153 // Process the requested definitions 154 Collections.sort(structures, new SortById()); 155 StringBuilder shapeDefinitions = new StringBuilder(); 156 for (StructureDefinition sd : structures) { 157 shapeDefinitions.append(genShapeDefinition(sd, true)); 158 } 159 160 // Add the inner types 161 Set<String> seen = new HashSet<String>(); 162 while(!this.typeReferences.isEmpty()) { 163 Pair<StructureDefinition, ElementDefinition> sded = this.typeReferences.poll(); 164 StructureDefinition sd = sded.getLeft(); 165 ElementDefinition ed = sded.getRight(); 166 if (seen.contains(ed.getPath())) 167 break; 168 seen.add(ed.getPath()); 169 shapeDefinitions.append("\n" + genElementReference(sd, ed)); 170 } 171 172 // Add the specific reference types 173 for(String r: this.references) { 174 shapeDefinitions.append("\n" + genReferenceEntry(r) + "\n"); 175 } 176 177 shex_def.add("shapeDefinitions", shapeDefinitions); 178 return shex_def.render(); 179 } 180 181 /** 182 * Generate the ShEx Header 183 * @return String representation 184 */ 185 private String genHeader() { 186 return HEADER_TEMPLATE; 187 } 188 189 /** 190 * Emit a ShEx definition for the supplied StructureDefinition 191 * @param sd Structure definition to emit 192 * @return ShEx definition 193 */ 194 private String genShapeDefinition(StructureDefinition sd, boolean isResource) { 195 ST resource_decl = tmplt(RESOURCE_DECL_TEMPLATE); 196 ST struct_def = tmplt(SHAPE_DEFINITION_TEMPLATE); 197 198 // todo: Figure out why this doesn't identify "Account" as a resource. getResourceNames() returns "Resource", "Resource" 199 // this.context.getResourceNames().contains(sd.getId()) 200 resource_decl.add("id", sd.getId()); 201 struct_def.add("resourceDecl", isResource ? resource_decl.render() : ""); 202 203 struct_def.add("id", sd.getId()); 204 List<String> elements = new ArrayList<String>(); 205 206 for (ElementDefinition ed : sd.getSnapshot().getElement()) { 207 if(StringUtils.countMatches(ed.getPath(), ".") == 1) { 208 elements.add(genElementDefinition(sd, ed)); 209 } 210 } 211 struct_def.add("elements", StringUtils.join(elements, ",\n\t")); 212 return struct_def.render(); 213 } 214 215 /** 216 * Generate a ShEx element definition 217 * @param sd Containing structure definition 218 * @param ed Containing element definition 219 * @return ShEx definition 220 */ 221 private String genElementDefinition(StructureDefinition sd, ElementDefinition ed) { 222 ST element_def = tmplt(ELEMENT_TEMPLATE); 223 224 String id = ed.hasBase() ? ed.getBase().getPath() : ed.getPath(); 225 String card = "*".equals(ed.getMax()) ? (ed.getMin() == 0 ? "*" : "+") : "?"; 226 String defn; 227 element_def.add("id", id); 228 element_def.add("card", card); 229 230 List<ElementDefinition> children = ProfileUtilities.getChildList(sd, ed); 231 if (children.size() > 0) { 232 // inline anonymous type - give it a name and factor it out 233 this.typeReferences.add(new ImmutablePair<StructureDefinition, ElementDefinition>(sd, ed)); 234 ST anon_link = tmplt(SIMPLE_ELEMENT_TEMPLATE); 235 anon_link.add("typ", id); 236 defn = anon_link.render(); 237 } else if (ed.getType().size() == 1) { 238 // Single entry 239 defn = genTypeRef(ed.getType().get(0)); 240 } else { 241 // multiple types 242 // todo: figure out how to do this with the API 243 if(id.endsWith("[x]")) { 244 return genChoiceTypes(ed, id, card); 245 } else { 246 defn = genAlternativeTypes(ed, id, card); 247 } 248 } 249 element_def.add("defn", defn); 250 return element_def.render(); 251 } 252 253 private String genTypeRef(ElementDefinition.TypeRefComponent typ) { 254 ST single_entry = tmplt(SIMPLE_ELEMENT_TEMPLATE); 255 if(typ.getProfile().size() > 0) { 256 if (typ.getProfile().size() != 1) throw new AssertionError("Can't handle multiple profiles"); 257 single_entry.add("typ", getProfiledType(typ)); 258 // TODO: Figure out how to get into the interior codes 259// } else if (typ.code.hasExtension()) { 260// for (Extension ext : typ.getExtension()) { 261// if(ext.getUrl() == XML_DEFN_TYPE ) { 262// ST primitive_entry = tmplt(PRIMITIVE_ELEMENT_TEMPLATE); 263// primitive_entry.add("typ", ext.getValue()); 264// return primitive_entry.render(); 265// } 266// } 267// return "UNKNOWN"; 268 } else if (typ.getCode() == null) { 269 ST primitive_entry = tmplt(PRIMITIVE_ELEMENT_TEMPLATE); 270 primitive_entry.add("typ", "string"); 271 return primitive_entry.render(); 272 } else 273 single_entry.add("typ", typ.getCode()); 274 return single_entry.render(); 275 } 276 277 /** 278 * Generate a set of alternative shapes 279 * @param ed Containing element definition 280 * @param id Element definition identifier 281 * @param card Cardinality 282 * @return ShEx list of alternative anonymous shapes separated by "OR" 283 */ 284 private String genAlternativeTypes(ElementDefinition ed, String id, String card) { 285 ST shex_alt = tmplt(ALTERNATIVE_TEMPLATE); 286 List<String> altEntries = new ArrayList<String>(); 287 288 shex_alt.add("id", id); 289 290 for(ElementDefinition.TypeRefComponent typ : ed.getType()) { 291 altEntries.add(genAltEntry(id, typ)); 292 } 293 shex_alt.add("altEntries", StringUtils.join(altEntries, " OR\n\t\t")); 294 shex_alt.add("card", card); 295 return shex_alt.render(); 296 } 297 298 private String genReference(String id, ElementDefinition.TypeRefComponent typ) { 299 ST shex_ref = tmplt(REFERENCE_TEMPLATE); 300 301 // todo: There has to be a better way to do this 302 String ref; 303 if(typ.getProfile().size() > 0) { 304 String[] els = typ.getProfile().get(0).getValue().split("/"); 305 ref = els[els.length - 1]; 306 } else { 307 ref = ""; 308 } 309 shex_ref.add("id", id); 310 shex_ref.add("ref", ref); 311 this.references.add(ref); 312 return shex_ref.render(); 313 } 314 315 /** 316 * Generate an alternative shape for a reference 317 * @param id reference name 318 * @param typ shape type 319 * @return ShEx equivalent 320 */ 321 private String genAltEntry(String id, ElementDefinition.TypeRefComponent typ) { 322 if(!typ.getCode().equals("Reference")) 323 throw new AssertionError("We do not handle " + typ.getCode() + " alternatives"); 324 325 return genReference(id, typ); 326 } 327 328 /** 329 * Return the type definition for a profiled type 330 * @param typ type to generate the entry for 331 * @return string of type 332 */ 333 private String getProfiledType(ElementDefinition.TypeRefComponent typ) 334 { 335 return typ.getProfile().get(0).fhirType(); 336 } 337 338 /** 339 * Generate a list of type choices for a "name[x]" style id 340 * @param ed containing elmentdefinition 341 * @param id choice identifier 342 * @return ShEx fragment for the set of choices 343 */ 344 private String genChoiceTypes(ElementDefinition ed, String id, String card) { 345 ST shex_choice = tmplt(CHOICE_TEMPLATE); 346 List<String> choiceEntries = new ArrayList<String>(); 347 String base = id.replace("[x]", ""); 348 349 for(ElementDefinition.TypeRefComponent typ : ed.getType()) { 350 choiceEntries.add(genChoiceEntry(base, typ)); 351 } 352 shex_choice.add("choiceEntries", StringUtils.join(choiceEntries, " |\n\t\t")); 353 shex_choice.add("card", card); 354 return shex_choice.render(); 355 } 356 357 /** 358 * Generate an entry in a choice list 359 * @param base base identifier 360 * @param typ type/discriminant 361 * @return ShEx fragment for choice entry 362 */ 363 private String genChoiceEntry(String base, ElementDefinition.TypeRefComponent typ) { 364 ST shex_choice_entry = tmplt(ELEMENT_TEMPLATE); 365 366 String ext = typ.getCode(); 367 shex_choice_entry.add("id", base+ext); 368 shex_choice_entry.add("card", ""); 369 shex_choice_entry.add("defn", genTypeRef(typ)); 370 return shex_choice_entry.render(); 371 } 372 373 /** 374 * Generate a definition for a referenced element 375 * @param sd StructureDefinition in which the element was referenced 376 * @param ed Contained element definition 377 * @return ShEx representation of element reference 378 */ 379 private String genElementReference(StructureDefinition sd, ElementDefinition ed) { 380 ST element_reference = tmplt(SHAPE_DEFINITION_TEMPLATE); 381 element_reference.add("resourceDecl", ""); // Not a resource 382 element_reference.add("id", ed.getPath()); 383 List<String> elements = new ArrayList<String>(); 384 385 for (ElementDefinition child: ProfileUtilities.getChildList(sd, ed)) { 386 elements.add(genElementDefinition(sd, child)); 387 } 388 389 element_reference.add("elements", StringUtils.join(elements, ",\n\t")); 390 return element_reference.render(); 391 } 392 393 /** 394 * Generate a reference to a typed fhir:uri 395 * @param refType reference type name 396 * @return reference 397 */ 398 private String genReferenceEntry(String refType) { 399 ST typed_ref = tmplt(TYPED_REFERENCE_TEMPLATE); 400 typed_ref.add("refType", refType); 401 return typed_ref.render(); 402 } 403 404 405}