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