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