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