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