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