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