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 032import java.io.FileOutputStream; 033/* 034Copyright (c) 2011+, HL7, Inc 035All rights reserved. 036 037Redistribution and use in source and binary forms, with or without modification, 038are permitted provided that the following conditions are met: 039 040 * Redistributions of source code must retain the above copyright notice, this 041 list of conditions and the following disclaimer. 042 * Redistributions in binary form must reproduce the above copyright notice, 043 this list of conditions and the following disclaimer in the documentation 044 and/or other materials provided with the distribution. 045 * Neither the name of HL7 nor the names of its contributors may be used to 046 endorse or promote products derived from this software without specific 047 prior written permission. 048 049THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 050ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 051WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 052IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 053INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 054NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 055PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 056WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 057ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 058POSSIBILITY OF SUCH DAMAGE. 059 060 */ 061import java.io.IOException; 062import java.io.OutputStreamWriter; 063import java.util.ArrayList; 064import java.util.HashMap; 065import java.util.HashSet; 066import java.util.LinkedList; 067import java.util.List; 068import java.util.Map; 069import java.util.Queue; 070import java.util.Set; 071 072import org.hl7.fhir.exceptions.FHIRException; 073import org.hl7.fhir.r4.context.IWorkerContext; 074import org.hl7.fhir.r4.model.ElementDefinition; 075import org.hl7.fhir.r4.model.ElementDefinition.PropertyRepresentation; 076import org.hl7.fhir.r4.model.ElementDefinition.TypeRefComponent; 077import org.hl7.fhir.r4.model.StructureDefinition; 078import org.hl7.fhir.r4.utils.ToolingExtensions; 079import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; 080import org.hl7.fhir.utilities.Utilities; 081import org.hl7.fhir.utilities.filesystem.ManagedFileAccess; 082 083public class XmlSchemaGenerator { 084 085 public class QName { 086 087 public String type; 088 public String typeNs; 089 090 @Override 091 public String toString() { 092 return typeNs + ":" + type; 093 } 094 } 095 096 public class ElementToGenerate { 097 098 private String tname; 099 private StructureDefinition sd; 100 private ElementDefinition ed; 101 102 public ElementToGenerate(String tname, StructureDefinition sd, ElementDefinition edc) { 103 this.tname = tname; 104 this.sd = sd; 105 this.ed = edc; 106 } 107 108 } 109 110 private String folder; 111 private IWorkerContext context; 112 private boolean single; 113 private String version; 114 private String genDate; 115 private String license; 116 private boolean annotations; 117 118 public XmlSchemaGenerator(String folder, IWorkerContext context) { 119 this.folder = folder; 120 this.context = context; 121 } 122 123 public boolean isSingle() { 124 return single; 125 } 126 127 public void setSingle(boolean single) { 128 this.single = single; 129 } 130 131 public String getVersion() { 132 return version; 133 } 134 135 public void setVersion(String version) { 136 this.version = version; 137 } 138 139 public String getGenDate() { 140 return genDate; 141 } 142 143 public void setGenDate(String genDate) { 144 this.genDate = genDate; 145 } 146 147 public String getLicense() { 148 return license; 149 } 150 151 public void setLicense(String license) { 152 this.license = license; 153 } 154 155 public boolean isAnnotations() { 156 return annotations; 157 } 158 159 public void setAnnotations(boolean annotations) { 160 this.annotations = annotations; 161 } 162 163 private Set<ElementDefinition> processed = new HashSet<ElementDefinition>(); 164 private Set<StructureDefinition> processedLibs = new HashSet<StructureDefinition>(); 165 private Set<String> typeNames = new HashSet<String>(); 166 private OutputStreamWriter writer; 167 private Map<String, String> namespaces = new HashMap<String, String>(); 168 private Queue<ElementToGenerate> queue = new LinkedList<ElementToGenerate>(); 169 private Queue<StructureDefinition> queueLib = new LinkedList<StructureDefinition>(); 170 private Map<String, StructureDefinition> library; 171 private boolean useNarrative; 172 173 private void w(String s) throws IOException { 174 writer.write(s); 175 } 176 177 private void ln(String s) throws IOException { 178 writer.write(s); 179 writer.write("\r\n"); 180 } 181 182 private void close() throws IOException { 183 if (writer != null) { 184 ln("</xs:schema>"); 185 writer.flush(); 186 writer.close(); 187 writer = null; 188 } 189 } 190 191 private String start(StructureDefinition sd, String ns) throws IOException, FHIRException { 192 String lang = "en"; 193 if (sd.hasLanguage()) 194 lang = sd.getLanguage(); 195 196 if (single && writer != null) { 197 if (!ns.equals(getNs(sd))) 198 throw new FHIRException("namespace inconsistency: " + ns + " vs " + getNs(sd)); 199 return lang; 200 } 201 close(); 202 203 writer = new OutputStreamWriter(ManagedFileAccess.outStream(Utilities.path(folder, tail(sd.getType() + ".xsd"))), "UTF-8"); 204 ln("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); 205 ln("<!-- "); 206 ln(license); 207 ln(""); 208 ln(" Generated on " + genDate + " for FHIR v" + version + " "); 209 ln(""); 210 ln(" Note: this schema does not contain all the knowledge represented in the underlying content model"); 211 ln(""); 212 ln("-->"); 213 ln("<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:fhir=\"http://hl7.org/fhir\" xmlns:xhtml=\"http://www.w3.org/1999/xhtml\" " 214 + "xmlns:lm=\"" + ns + "\" targetNamespace=\"" + ns + "\" elementFormDefault=\"qualified\" version=\"1.0\">"); 215 ln(" <xs:import schemaLocation=\"fhir-common.xsd\" namespace=\"http://hl7.org/fhir\"/>"); 216 if (useNarrative) { 217 if (ns.equals("urn:hl7-org:v3")) 218 ln(" <xs:include schemaLocation=\"cda-narrative.xsd\"/>"); 219 else 220 ln(" <xs:import schemaLocation=\"cda-narrative.xsd\" namespace=\"urn:hl7-org:v3\"/>"); 221 } 222 namespaces.clear(); 223 namespaces.put(ns, "lm"); 224 namespaces.put("http://hl7.org/fhir", "fhir"); 225 typeNames.clear(); 226 227 return lang; 228 } 229 230 private String getNs(StructureDefinition sd) { 231 String ns = "http://hl7.org/fhir"; 232 if (sd.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-namespace")) 233 ns = ToolingExtensions.readStringExtension(sd, 234 "http://hl7.org/fhir/StructureDefinition/elementdefinition-namespace"); 235 return ns; 236 } 237 238 public void generate(StructureDefinition entry, Map<String, StructureDefinition> library) throws Exception { 239 processedLibs.clear(); 240 241 this.library = library; 242 checkLib(entry); 243 244 String ns = getNs(entry); 245 String lang = start(entry, ns); 246 247 w(" <xs:element name=\"" + tail(entry.getType()) + "\" type=\"lm:" + tail(entry.getType()) + "\""); 248 if (annotations) { 249 ln(">"); 250 ln(" <xs:annotation>"); 251 ln(" <xs:documentation xml:lang=\"" + lang + "\">" + Utilities.escapeXml(entry.getDescription()) 252 + "</xs:documentation>"); 253 ln(" </xs:annotation>"); 254 ln(" </xs:element>"); 255 } else 256 ln("/>"); 257 258 produceType(entry, entry.getSnapshot().getElement().get(0), tail(entry.getType()), 259 getQN(entry, entry.getBaseDefinition()), lang); 260 while (!queue.isEmpty()) { 261 ElementToGenerate q = queue.poll(); 262 produceType(q.sd, q.ed, q.tname, getQN(q.sd, q.ed, "http://hl7.org/fhir/StructureDefinition/Element", false), 263 lang); 264 } 265 while (!queueLib.isEmpty()) { 266 generateInner(queueLib.poll()); 267 } 268 close(); 269 } 270 271 private void checkLib(StructureDefinition entry) { 272 for (ElementDefinition ed : entry.getSnapshot().getElement()) { 273 if (ed.hasRepresentation(PropertyRepresentation.CDATEXT)) { 274 useNarrative = true; 275 } 276 } 277 for (StructureDefinition sd : library.values()) { 278 for (ElementDefinition ed : sd.getSnapshot().getElement()) { 279 if (ed.hasRepresentation(PropertyRepresentation.CDATEXT)) { 280 useNarrative = true; 281 } 282 } 283 } 284 } 285 286 private void generateInner(StructureDefinition sd) throws IOException, FHIRException { 287 if (processedLibs.contains(sd)) 288 return; 289 processedLibs.add(sd); 290 291 String ns = getNs(sd); 292 String lang = start(sd, ns); 293 294 if (sd.getSnapshot().getElement().isEmpty()) 295 throw new FHIRException("no snap shot on " + sd.getUrl()); 296 297 produceType(sd, sd.getSnapshot().getElement().get(0), tail(sd.getType()), getQN(sd, sd.getBaseDefinition()), lang); 298 while (!queue.isEmpty()) { 299 ElementToGenerate q = queue.poll(); 300 produceType(q.sd, q.ed, q.tname, getQN(q.sd, q.ed, "http://hl7.org/fhir/StructureDefinition/Element", false), 301 lang); 302 } 303 } 304 305 private String tail(String url) { 306 return url.contains("/") ? url.substring(url.lastIndexOf("/") + 1) : url; 307 } 308 309 private String root(String url) { 310 return url.contains("/") ? url.substring(0, url.lastIndexOf("/")) : ""; 311 } 312 313 private String tailDot(String url) { 314 return url.contains(".") ? url.substring(url.lastIndexOf(".") + 1) : url; 315 } 316 317 private void produceType(StructureDefinition sd, ElementDefinition ed, String typeName, QName typeParent, String lang) 318 throws IOException, FHIRException { 319 if (processed.contains(ed)) 320 return; 321 processed.add(ed); 322 323 // ok 324 ln(" <xs:complexType name=\"" + typeName + "\">"); 325 if (annotations) { 326 ln(" <xs:annotation>"); 327 ln(" <xs:documentation xml:lang=\"" + lang + "\">" + Utilities.escapeXml(ed.getDefinition()) 328 + "</xs:documentation>"); 329 ln(" </xs:annotation>"); 330 } 331 ln(" <xs:complexContent>"); 332 ln(" <xs:extension base=\"" + typeParent.toString() + "\">"); 333 ln(" <xs:sequence>"); 334 335 // hack.... 336 for (ElementDefinition edc : ProfileUtilities.getChildList(sd, ed)) { 337 if (!(edc.hasRepresentation(PropertyRepresentation.XMLATTR) 338 || edc.hasRepresentation(PropertyRepresentation.XMLTEXT)) && !inheritedElement(edc)) 339 produceElement(sd, ed, edc, lang); 340 } 341 ln(" </xs:sequence>"); 342 for (ElementDefinition edc : ProfileUtilities.getChildList(sd, ed)) { 343 if ((edc.hasRepresentation(PropertyRepresentation.XMLATTR) 344 || edc.hasRepresentation(PropertyRepresentation.XMLTEXT)) && !inheritedElement(edc)) 345 produceAttribute(sd, ed, edc, lang); 346 } 347 ln(" </xs:extension>"); 348 ln(" </xs:complexContent>"); 349 ln(" </xs:complexType>"); 350 } 351 352 private boolean inheritedElement(ElementDefinition edc) { 353 return !edc.getPath().equals(edc.getBase().getPath()); 354 } 355 356 private void produceElement(StructureDefinition sd, ElementDefinition ed, ElementDefinition edc, String lang) 357 throws IOException, FHIRException { 358 if (edc.getType().size() == 0) 359 throw new Error("No type at " + edc.getPath()); 360 361 if (edc.getType().size() > 1 && edc.hasRepresentation(PropertyRepresentation.TYPEATTR)) { 362 // first, find the common base type 363 StructureDefinition lib = getCommonAncestor(edc.getType()); 364 if (lib == null) 365 throw new Error("Common ancester not found at " + edc.getPath()); 366 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); 367 for (TypeRefComponent t : edc.getType()) { 368 b.append(getQN(sd, edc, t.getWorkingCode(), true).toString()); 369 } 370 371 String name = tailDot(edc.getPath()); 372 String min = String.valueOf(edc.getMin()); 373 String max = edc.getMax(); 374 if ("*".equals(max)) 375 max = "unbounded"; 376 377 QName qn = getQN(sd, edc, lib.getUrl(), true); 378 379 ln(" <xs:element name=\"" + name + "\" minOccurs=\"" + min + "\" maxOccurs=\"" + max + "\" type=\"" 380 + qn.typeNs + ":" + qn.type + "\">"); 381 ln(" <xs:annotation>"); 382 ln(" <xs:appinfo xml:lang=\"en\">Possible types: " + b.toString() + "</xs:appinfo>"); 383 if (annotations && edc.hasDefinition()) 384 ln(" <xs:documentation xml:lang=\"" + lang + "\">" + Utilities.escapeXml(edc.getDefinition()) 385 + "</xs:documentation>"); 386 ln(" </xs:annotation>"); 387 ln(" </xs:element>"); 388 } else 389 for (TypeRefComponent t : edc.getType()) { 390 String name = tailDot(edc.getPath()); 391 if (edc.getType().size() > 1) 392 name = name + Utilities.capitalize(t.getWorkingCode()); 393 QName qn = getQN(sd, edc, t.getWorkingCode(), true); 394 String min = String.valueOf(edc.getMin()); 395 String max = edc.getMax(); 396 if ("*".equals(max)) 397 max = "unbounded"; 398 399 w(" <xs:element name=\"" + name + "\" minOccurs=\"" + min + "\" maxOccurs=\"" + max + "\" type=\"" 400 + qn.typeNs + ":" + qn.type + "\""); 401 if (annotations && edc.hasDefinition()) { 402 ln(">"); 403 ln(" <xs:annotation>"); 404 ln(" <xs:documentation xml:lang=\"" + lang + "\">" + Utilities.escapeXml(edc.getDefinition()) 405 + "</xs:documentation>"); 406 ln(" </xs:annotation>"); 407 ln(" </xs:element>"); 408 } else 409 ln("/>"); 410 } 411 } 412 413 public QName getQN(StructureDefinition sd, String type) throws FHIRException { 414 return getQN(sd, sd.getSnapshot().getElementFirstRep(), type, false); 415 } 416 417 public QName getQN(StructureDefinition sd, ElementDefinition edc, String t, boolean chase) throws FHIRException { 418 QName qn = new QName(); 419 qn.type = Utilities.isAbsoluteUrl(t) ? tail(t) : t; 420 if (Utilities.isAbsoluteUrl(t)) { 421 String ns = root(t); 422 if (ns.equals(root(sd.getUrl()))) 423 ns = getNs(sd); 424 if (ns.equals("http://hl7.org/fhir/StructureDefinition")) 425 ns = "http://hl7.org/fhir"; 426 if (!namespaces.containsKey(ns)) 427 throw new FHIRException("Unknown type namespace " + ns + " for " + edc.getPath()); 428 qn.typeNs = namespaces.get(ns); 429 StructureDefinition lib = library.get(t); 430 if (lib == null && !Utilities.existsInList(t, "http://hl7.org/fhir/cda/StructureDefinition/StrucDoc.Text", 431 "http://hl7.org/fhir/StructureDefinition/Element")) 432 throw new FHIRException("Unable to resolve " + t + " for " + edc.getPath()); 433 if (lib != null) 434 queueLib.add(lib); 435 } else 436 qn.typeNs = namespaces.get("http://hl7.org/fhir"); 437 438 if (chase && qn.type.equals("Element")) { 439 String tname = typeNameFromPath(edc); 440 if (typeNames.contains(tname)) { 441 int i = 1; 442 while (typeNames.contains(tname + i)) 443 i++; 444 tname = tname + i; 445 } 446 queue.add(new ElementToGenerate(tname, sd, edc)); 447 qn.typeNs = "lm"; 448 qn.type = tname; 449 } 450 return qn; 451 } 452 453 private StructureDefinition getCommonAncestor(List<TypeRefComponent> type) throws FHIRException { 454 StructureDefinition sd = library.get(type.get(0).getWorkingCode()); 455 if (sd == null) 456 throw new FHIRException("Unable to find definition for " + type.get(0).getWorkingCode()); 457 for (int i = 1; i < type.size(); i++) { 458 StructureDefinition t = library.get(type.get(i).getWorkingCode()); 459 if (t == null) 460 throw new FHIRException("Unable to find definition for " + type.get(i).getWorkingCode()); 461 sd = getCommonAncestor(sd, t); 462 } 463 return sd; 464 } 465 466 private StructureDefinition getCommonAncestor(StructureDefinition sd1, StructureDefinition sd2) throws FHIRException { 467 // this will always return something because everything comes from Element 468 List<StructureDefinition> chain1 = new ArrayList<>(); 469 List<StructureDefinition> chain2 = new ArrayList<>(); 470 chain1.add(sd1); 471 chain2.add(sd2); 472 StructureDefinition root = library.get("Element"); 473 StructureDefinition common = findIntersection(chain1, chain2); 474 boolean chain1Done = false; 475 boolean chain2Done = false; 476 while (common == null) { 477 chain1Done = checkChain(chain1, root, chain1Done); 478 chain2Done = checkChain(chain2, root, chain2Done); 479 if (chain1Done && chain2Done) 480 return null; 481 common = findIntersection(chain1, chain2); 482 } 483 return common; 484 } 485 486 private StructureDefinition findIntersection(List<StructureDefinition> chain1, List<StructureDefinition> chain2) { 487 for (StructureDefinition sd1 : chain1) 488 for (StructureDefinition sd2 : chain2) 489 if (sd1 == sd2) 490 return sd1; 491 return null; 492 } 493 494 public boolean checkChain(List<StructureDefinition> chain1, StructureDefinition root, boolean chain1Done) 495 throws FHIRException { 496 if (!chain1Done) { 497 StructureDefinition sd = chain1.get(chain1.size() - 1); 498 String bu = sd.getBaseDefinition(); 499 if (bu == null) 500 throw new FHIRException("No base definition for " + sd.getUrl()); 501 StructureDefinition t = library.get(bu); 502 if (t == null) 503 chain1Done = true; 504 else 505 chain1.add(t); 506 } 507 return chain1Done; 508 } 509 510 private StructureDefinition getBase(StructureDefinition structureDefinition) { 511 return null; 512 } 513 514 private String typeNameFromPath(ElementDefinition edc) { 515 StringBuilder b = new StringBuilder(); 516 boolean up = true; 517 for (char ch : edc.getPath().toCharArray()) { 518 if (ch == '.') 519 up = true; 520 else if (up) { 521 b.append(Character.toUpperCase(ch)); 522 up = false; 523 } else 524 b.append(ch); 525 } 526 return b.toString(); 527 } 528 529 private void produceAttribute(StructureDefinition sd, ElementDefinition ed, ElementDefinition edc, String lang) 530 throws IOException, FHIRException { 531 TypeRefComponent t = edc.getTypeFirstRep(); 532 String name = tailDot(edc.getPath()); 533 String min = String.valueOf(edc.getMin()); 534 String max = edc.getMax(); 535 // todo: check it's a code... 536// if (!max.equals("1")) 537// throw new FHIRException("Illegal cardinality \""+max+"\" for attribute "+edc.getPath()); 538 539 String tc = t.getWorkingCode(); 540 if (Utilities.isAbsoluteUrl(tc)) 541 throw new FHIRException("Only FHIR primitive types are supported for attributes (" + tc + ")"); 542 String typeNs = namespaces.get("http://hl7.org/fhir"); 543 String type = tc; 544 545 w(" <xs:attribute name=\"" + name + "\" use=\"" 546 + (min.equals("0") || edc.hasFixed() || edc.hasDefaultValue() ? "optional" : "required") + "\" type=\"" + typeNs 547 + ":" + type + (typeNs.equals("fhir") ? "-primitive" : "") + "\"" 548 + (edc.hasFixed() ? " fixed=\"" + edc.getFixed().primitiveValue() + "\"" : "") 549 + (edc.hasDefaultValue() && !edc.hasFixed() ? " default=\"" + edc.getDefaultValue().primitiveValue() + "\"" 550 : "") 551 + ""); 552 if (annotations && edc.hasDefinition()) { 553 ln(">"); 554 ln(" <xs:annotation>"); 555 ln(" <xs:documentation xml:lang=\"" + lang + "\">" + Utilities.escapeXml(edc.getDefinition()) 556 + "</xs:documentation>"); 557 ln(" </xs:annotation>"); 558 ln(" </xs:attribute>"); 559 } else 560 ln("/>"); 561 } 562 563}