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.conformance.profile.ProfileUtilities; 075import org.hl7.fhir.r5.context.IWorkerContext; 076import org.hl7.fhir.r5.model.ElementDefinition; 077import org.hl7.fhir.r5.model.ElementDefinition.PropertyRepresentation; 078import org.hl7.fhir.r5.model.ElementDefinition.TypeRefComponent; 079import org.hl7.fhir.r5.model.StructureDefinition; 080import org.hl7.fhir.r5.utils.ToolingExtensions; 081import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; 082import org.hl7.fhir.utilities.Utilities; 083import org.hl7.fhir.utilities.filesystem.ManagedFileAccess; 084 085 086public class XmlSchemaGenerator { 087 088 public class QName { 089 090 public String type; 091 public String typeNs; 092 093 @Override 094 public String toString() { 095 return typeNs+":"+type; 096 } 097 } 098 099 public class ElementToGenerate { 100 101 private String tname; 102 private StructureDefinition sd; 103 private ElementDefinition ed; 104 105 public ElementToGenerate(String tname, StructureDefinition sd, ElementDefinition edc) { 106 this.tname = tname; 107 this.sd = sd; 108 this.ed = edc; 109 } 110 111 112 } 113 114 115 private String folder; 116 private IWorkerContext context; 117 private boolean single; 118 private String version; 119 private String genDate; 120 private String license; 121 private boolean annotations; 122 private ProfileUtilities profileUtilities; 123 124 public XmlSchemaGenerator(String folder, IWorkerContext context) { 125 this.folder = folder; 126 this.context = context; 127 this.profileUtilities = new ProfileUtilities(context, null, null); 128 } 129 130 public boolean isSingle() { 131 return single; 132 } 133 134 public void setSingle(boolean single) { 135 this.single = single; 136 } 137 138 139 public String getVersion() { 140 return version; 141 } 142 143 public void setVersion(String version) { 144 this.version = version; 145 } 146 147 public String getGenDate() { 148 return genDate; 149 } 150 151 public void setGenDate(String genDate) { 152 this.genDate = genDate; 153 } 154 155 public String getLicense() { 156 return license; 157 } 158 159 public void setLicense(String license) { 160 this.license = license; 161 } 162 163 164 public boolean isAnnotations() { 165 return annotations; 166 } 167 168 public void setAnnotations(boolean annotations) { 169 this.annotations = annotations; 170 } 171 172 173 private Set<ElementDefinition> processed = new HashSet<ElementDefinition>(); 174 private Set<StructureDefinition> processedLibs = new HashSet<StructureDefinition>(); 175 private Set<String> typeNames = new HashSet<String>(); 176 private OutputStreamWriter writer; 177 private Map<String, String> namespaces = new HashMap<String, String>(); 178 private Queue<ElementToGenerate> queue = new LinkedList<ElementToGenerate>(); 179 private Queue<StructureDefinition> queueLib = new LinkedList<StructureDefinition>(); 180 private Map<String, StructureDefinition> library; 181 private boolean useNarrative; 182 183 private void w(String s) throws IOException { 184 writer.write(s); 185 } 186 187 private void ln(String s) throws IOException { 188 writer.write(s); 189 writer.write("\r\n"); 190 } 191 192 private void close() throws IOException { 193 if (writer != null) { 194 ln("</xs:schema>"); 195 writer.flush(); 196 writer.close(); 197 writer = null; 198 } 199 } 200 201 private String start(StructureDefinition sd, String ns) throws IOException, FHIRException { 202 String lang = "en"; 203 if (sd.hasLanguage()) 204 lang = sd.getLanguage(); 205 206 if (single && writer != null) { 207 if (!ns.equals(getNs(sd))) 208 throw new FHIRException("namespace inconsistency: "+ns+" vs "+getNs(sd)); 209 return lang; 210 } 211 close(); 212 213 writer = new OutputStreamWriter(ManagedFileAccess.outStream(Utilities.path(folder, tail(sd.getType()+".xsd"))), "UTF-8"); 214 ln("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); 215 ln("<!-- "); 216 ln(license); 217 ln(""); 218 ln(" Generated on "+genDate+" for FHIR v"+version+" "); 219 ln(""); 220 ln(" Note: this schema does not contain all the knowledge represented in the underlying content model"); 221 ln(""); 222 ln("-->"); 223 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\" "+ 224 "xmlns:lm=\""+ns+"\" targetNamespace=\""+ns+"\" elementFormDefault=\"qualified\" version=\"1.0\">"); 225 ln(" <xs:import schemaLocation=\"fhir-common.xsd\" namespace=\"http://hl7.org/fhir\"/>"); 226 if (useNarrative) { 227 if (ns.equals("urn:hl7-org:v3")) 228 ln(" <xs:include schemaLocation=\"cda-narrative.xsd\"/>"); 229 else 230 ln(" <xs:import schemaLocation=\"cda-narrative.xsd\" namespace=\"urn:hl7-org:v3\"/>"); 231 } 232 namespaces.clear(); 233 namespaces.put(ns, "lm"); 234 namespaces.put("http://hl7.org/fhir", "fhir"); 235 typeNames.clear(); 236 237 return lang; 238 } 239 240 241 private String getNs(StructureDefinition sd) { 242 String ns = "http://hl7.org/fhir"; 243 if (sd.hasExtension(ToolingExtensions.EXT_XML_NAMESPACE, ToolingExtensions.EXT_XML_NAMESPACE_DEPRECATED)) 244 ns = ToolingExtensions.readStringExtension(sd, ToolingExtensions.EXT_XML_NAMESPACE, ToolingExtensions.EXT_XML_NAMESPACE_DEPRECATED); 245 return ns; 246 } 247 248 public void generate(StructureDefinition entry, Map<String, StructureDefinition> library) throws Exception { 249 processedLibs.clear(); 250 251 this.library = library; 252 checkLib(entry); 253 254 String ns = getNs(entry); 255 String lang = start(entry, ns); 256 257 w(" <xs:element name=\""+tail(entry.getType())+"\" type=\"lm:"+tail(entry.getType())+"\""); 258 if (annotations) { 259 ln(">"); 260 ln(" <xs:annotation>"); 261 ln(" <xs:documentation xml:lang=\""+lang+"\">"+Utilities.escapeXml(entry.getDescription())+"</xs:documentation>"); 262 ln(" </xs:annotation>"); 263 ln(" </xs:element>"); 264 } else 265 ln("/>"); 266 267 produceType(entry, entry.getSnapshot().getElement().get(0), tail(entry.getType()), getQN(entry, entry.getBaseDefinition()), lang); 268 while (!queue.isEmpty()) { 269 ElementToGenerate q = queue.poll(); 270 produceType(q.sd, q.ed, q.tname, getQN(q.sd, q.ed, "http://hl7.org/fhir/StructureDefinition/Element", false), lang); 271 } 272 while (!queueLib.isEmpty()) { 273 generateInner(queueLib.poll()); 274 } 275 close(); 276 } 277 278 279 280 281 private void checkLib(StructureDefinition entry) { 282 for (ElementDefinition ed : entry.getSnapshot().getElement()) { 283 if (ed.hasRepresentation(PropertyRepresentation.CDATEXT)) { 284 useNarrative = true; 285 } 286 } 287 for (StructureDefinition sd : library.values()) { 288 for (ElementDefinition ed : sd.getSnapshot().getElement()) { 289 if (ed.hasRepresentation(PropertyRepresentation.CDATEXT)) { 290 useNarrative = true; 291 } 292 } 293 } 294 } 295 296 private void generateInner(StructureDefinition sd) throws IOException, FHIRException { 297 if (processedLibs.contains(sd)) 298 return; 299 processedLibs.add(sd); 300 301 String ns = getNs(sd); 302 String lang = start(sd, ns); 303 304 if (sd.getSnapshot().getElement().isEmpty()) 305 throw new FHIRException("no snap shot on "+sd.getUrl()); 306 307 produceType(sd, sd.getSnapshot().getElement().get(0), tail(sd.getType()), getQN(sd, sd.getBaseDefinition()), lang); 308 while (!queue.isEmpty()) { 309 ElementToGenerate q = queue.poll(); 310 produceType(q.sd, q.ed, q.tname, getQN(q.sd, q.ed, "http://hl7.org/fhir/StructureDefinition/Element", false), lang); 311 } 312 } 313 314 private String tail(String url) { 315 return url.contains("/") ? url.substring(url.lastIndexOf("/")+1) : url; 316 } 317 private String root(String url) { 318 return url.contains("/") ? url.substring(0, url.lastIndexOf("/")) : ""; 319 } 320 321 322 private String tailDot(String url) { 323 return url.contains(".") ? url.substring(url.lastIndexOf(".")+1) : url; 324 } 325 private void produceType(StructureDefinition sd, ElementDefinition ed, String typeName, QName typeParent, String lang) throws IOException, FHIRException { 326 if (processed.contains(ed)) 327 return; 328 processed.add(ed); 329 330 // ok 331 ln(" <xs:complexType name=\""+typeName+"\">"); 332 if (annotations) { 333 ln(" <xs:annotation>"); 334 ln(" <xs:documentation xml:lang=\""+lang+"\">"+Utilities.escapeXml(ed.getDefinition())+"</xs:documentation>"); 335 ln(" </xs:annotation>"); 336 } 337 ln(" <xs:complexContent>"); 338 ln(" <xs:extension base=\""+typeParent.toString()+"\">"); 339 ln(" <xs:sequence>"); 340 341 // hack.... 342 for (ElementDefinition edc : profileUtilities.getChildList(sd, ed)) { 343 if (!(edc.hasRepresentation(PropertyRepresentation.XMLATTR) || edc.hasRepresentation(PropertyRepresentation.XMLTEXT)) && !inheritedElement(edc)) 344 produceElement(sd, ed, edc, lang); 345 } 346 ln(" </xs:sequence>"); 347 for (ElementDefinition edc : profileUtilities.getChildList(sd, ed)) { 348 if ((edc.hasRepresentation(PropertyRepresentation.XMLATTR) || edc.hasRepresentation(PropertyRepresentation.XMLTEXT)) && !inheritedElement(edc)) 349 produceAttribute(sd, ed, edc, lang); 350 } 351 ln(" </xs:extension>"); 352 ln(" </xs:complexContent>"); 353 ln(" </xs:complexType>"); 354 } 355 356 357 private boolean inheritedElement(ElementDefinition edc) { 358 return !edc.getPath().equals(edc.getBase().getPath()); 359 } 360 361 private void produceElement(StructureDefinition sd, ElementDefinition ed, ElementDefinition edc, String lang) throws IOException, FHIRException { 362 if (edc.getType().size() == 0) 363 throw new Error("No type at "+edc.getPath()); 364 365 if (edc.getType().size() > 1 && edc.hasRepresentation(PropertyRepresentation.TYPEATTR)) { 366 // first, find the common base type 367 StructureDefinition lib = getCommonAncestor(edc.getType()); 368 if (lib == null) 369 throw new Error("Common ancester not found at "+edc.getPath()); 370 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); 371 for (TypeRefComponent t : edc.getType()) { 372 b.append(getQN(sd, edc, t.getWorkingCode(), true).toString()); 373 } 374 375 String name = tailDot(edc.getPath()); 376 String min = String.valueOf(edc.getMin()); 377 String max = edc.getMax(); 378 if ("*".equals(max)) 379 max = "unbounded"; 380 381 QName qn = getQN(sd, edc, lib.getUrl(), true); 382 383 ln(" <xs:element name=\""+name+"\" minOccurs=\""+min+"\" maxOccurs=\""+max+"\" type=\""+qn.typeNs+":"+qn.type+"\">"); 384 ln(" <xs:annotation>"); 385 ln(" <xs:appinfo xml:lang=\"en\">Possible types: "+b.toString()+"</xs:appinfo>"); 386 if (annotations && edc.hasDefinition()) 387 ln(" <xs:documentation xml:lang=\""+lang+"\">"+Utilities.escapeXml(edc.getDefinition())+"</xs:documentation>"); 388 ln(" </xs:annotation>"); 389 ln(" </xs:element>"); 390 } else for (TypeRefComponent t : edc.getType()) { 391 String name = tailDot(edc.getPath()); 392 if (edc.getType().size() > 1) 393 name = name + Utilities.capitalize(t.getWorkingCode()); 394 QName qn = getQN(sd, edc, t.getWorkingCode(), true); 395 String min = String.valueOf(edc.getMin()); 396 String max = edc.getMax(); 397 if ("*".equals(max)) 398 max = "unbounded"; 399 400 401 w(" <xs:element name=\""+name+"\" minOccurs=\""+min+"\" maxOccurs=\""+max+"\" type=\""+qn.typeNs+":"+qn.type+"\""); 402 if (annotations && edc.hasDefinition()) { 403 ln(">"); 404 ln(" <xs:annotation>"); 405 ln(" <xs:documentation xml:lang=\""+lang+"\">"+Utilities.escapeXml(edc.getDefinition())+"</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", "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 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) 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) throws IOException, FHIRException { 529 TypeRefComponent t = edc.getTypeFirstRep(); 530 String name = tailDot(edc.getPath()); 531 String min = String.valueOf(edc.getMin()); 532 String max = edc.getMax(); 533 // todo: check it's a code... 534// if (!max.equals("1")) 535// throw new FHIRException("Illegal cardinality \""+max+"\" for attribute "+edc.getPath()); 536 537 String tc = t.getWorkingCode(); 538 if (Utilities.isAbsoluteUrl(tc)) 539 throw new FHIRException("Only FHIR primitive types are supported for attributes ("+tc+")"); 540 String typeNs = namespaces.get("http://hl7.org/fhir"); 541 String type = tc; 542 543 w(" <xs:attribute name=\""+name+"\" use=\""+(min.equals("0") || edc.hasFixed() || edc.hasDefaultValue() ? "optional" : "required")+"\" type=\""+typeNs+":"+type+(typeNs.equals("fhir") ? "-primitive" : "")+"\""+ 544 (edc.hasFixed() ? " fixed=\""+edc.getFixed().primitiveValue()+"\"" : "")+(edc.hasDefaultValue() && !edc.hasFixed() ? " default=\""+edc.getDefaultValue().primitiveValue()+"\"" : "")+""); 545 if (annotations && edc.hasDefinition()) { 546 ln(">"); 547 ln(" <xs:annotation>"); 548 ln(" <xs:documentation xml:lang=\""+lang+"\">"+Utilities.escapeXml(edc.getDefinition())+"</xs:documentation>"); 549 ln(" </xs:annotation>"); 550 ln(" </xs:attribute>"); 551 } else 552 ln("/>"); 553 } 554 555 556}