001// *************************************************************************************************************************** 002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * 003// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * 004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * 005// * with the License. You may obtain a copy of the License at * 006// * * 007// * http://www.apache.org/licenses/LICENSE-2.0 * 008// * * 009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * 010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 011// * specific language governing permissions and limitations under the License. * 012// *************************************************************************************************************************** 013package org.apache.juneau.xmlschema; 014 015import static org.apache.juneau.internal.ArrayUtils.*; 016import static org.apache.juneau.xml.annotation.XmlFormat.*; 017 018import java.io.*; 019import java.util.*; 020import java.util.regex.*; 021 022import javax.xml.*; 023import javax.xml.transform.stream.*; 024import javax.xml.validation.*; 025 026import org.apache.juneau.*; 027import org.apache.juneau.serializer.*; 028import org.apache.juneau.xml.*; 029import org.apache.juneau.xml.annotation.*; 030import org.w3c.dom.bootstrap.*; 031import org.w3c.dom.ls.*; 032 033/** 034 * Session object that lives for the duration of a single use of {@link XmlSchemaSerializer}. 035 * 036 * <p> 037 * This class is NOT thread safe. 038 * It is typically discarded after one-time use although it can be reused within the same thread. 039 */ 040public class XmlSchemaSerializerSession extends XmlSerializerSession { 041 042 /** 043 * Create a new session using properties specified in the context. 044 * 045 * @param ctx 046 * The context creating this session object. 047 * The context contains all the configuration settings for this object. 048 * @param args 049 * Runtime arguments. 050 * These specify session-level information such as locale and URI context. 051 * It also include session-level properties that override the properties defined on the bean and 052 * serializer contexts. 053 */ 054 protected XmlSchemaSerializerSession(XmlSerializer ctx, SerializerSessionArgs args) { 055 super(ctx, args); 056 } 057 058 @Override /* SerializerSession */ 059 protected void doSerialize(SerializerPipe out, Object o) throws IOException, SerializeException { 060 if (isEnableNamespaces() && isAutoDetectNamespaces()) 061 findNsfMappings(o); 062 063 Namespace xs = getXsNamespace(); 064 Namespace[] allNs = append(new Namespace[]{getDefaultNamespace()}, getNamespaces()); 065 066 Schemas schemas = new Schemas(this, xs, getDefaultNamespace(), allNs); 067 schemas.process(o); 068 schemas.serializeTo(out.getWriter()); 069 } 070 071 /** 072 * Returns an XML-Schema validator based on the output returned by {@link #doSerialize(SerializerPipe, Object)}; 073 * 074 * @param out The target writer. 075 * @param o The object to serialize. 076 * @return The new validator. 077 * @throws Exception If a problem was detected in the XML-Schema output produced by this serializer. 078 */ 079 public Validator getValidator(SerializerPipe out, Object o) throws Exception { 080 doSerialize(out, o); 081 String xmlSchema = out.getWriter().toString(); 082 083 // create a SchemaFactory capable of understanding WXS schemas 084 SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); 085 086 if (xmlSchema.indexOf('\u0000') != -1) { 087 088 // Break it up into a map of namespaceURI->schema document 089 final Map<String,String> schemas = new HashMap<>(); 090 String[] ss = xmlSchema.split("\u0000"); 091 xmlSchema = ss[0]; 092 for (String s : ss) { 093 Matcher m = pTargetNs.matcher(s); 094 if (m.find()) 095 schemas.put(m.group(1), s); 096 } 097 098 // Create a custom resolver 099 factory.setResourceResolver( 100 new LSResourceResolver() { 101 102 @Override /* LSResourceResolver */ 103 public LSInput resolveResource(String type, String namespaceURI, String publicId, String systemId, String baseURI) { 104 105 String schema = schemas.get(namespaceURI); 106 if (schema == null) 107 throw new FormattedRuntimeException("No schema found for namespaceURI ''{0}''", namespaceURI); 108 109 try { 110 DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance(); 111 DOMImplementationLS domImplementationLS = (DOMImplementationLS)registry.getDOMImplementation("LS 3.0"); 112 LSInput in = domImplementationLS.createLSInput(); 113 in.setCharacterStream(new StringReader(schema)); 114 in.setSystemId(systemId); 115 return in; 116 117 } catch (Exception e) { 118 throw new RuntimeException(e); 119 } 120 } 121 } 122 ); 123 } 124 return factory.newSchema(new StreamSource(new StringReader(xmlSchema))).newValidator(); 125 } 126 127 private static Pattern pTargetNs = Pattern.compile("targetNamespace=['\"]([^'\"]+)['\"]"); 128 129 130 /* An instance of a global element, global attribute, or XML type to be serialized. */ 131 private static class QueueEntry { 132 Namespace ns; 133 String name; 134 ClassMeta<?> cm; 135 QueueEntry(Namespace ns, String name, ClassMeta<?> cm) { 136 this.ns = ns; 137 this.name = name; 138 this.cm = cm; 139 } 140 } 141 142 /* An encapsulation of all schemas present in the metamodel of the serialized object. */ 143 final class Schemas extends LinkedHashMap<Namespace,Schema> { 144 145 private static final long serialVersionUID = 1L; 146 147 private Namespace defaultNs; 148 BeanSession session; 149 private LinkedList<QueueEntry> 150 elementQueue = new LinkedList<>(), 151 attributeQueue = new LinkedList<>(), 152 typeQueue = new LinkedList<>(); 153 154 Schemas(BeanSession session, Namespace xs, Namespace defaultNs, Namespace[] allNs) throws IOException { 155 this.session = session; 156 this.defaultNs = defaultNs; 157 for (Namespace ns : allNs) 158 put(ns, new Schema(this, xs, ns, defaultNs, allNs)); 159 } 160 161 Schema getSchema(Namespace ns) { 162 if (ns == null) 163 ns = defaultNs; 164 Schema s = get(ns); 165 if (s == null) 166 throw new FormattedRuntimeException("No schema defined for namespace ''{0}''", ns); 167 return s; 168 } 169 170 void process(Object o) throws IOException { 171 ClassMeta<?> cm = getClassMetaForObject(o); 172 if (cm != null && cm.isOptional()) 173 cm = getClassMetaForObject(((Optional<?>)o).orElse(null)); 174 Namespace ns = defaultNs; 175 if (cm == null) 176 queueElement(ns, "null", object()); 177 else { 178 XmlClassMeta xmlMeta = cm.getExtendedMeta(XmlClassMeta.class); 179 if (cm.getDictionaryName() != null && xmlMeta.getNamespace() != null) 180 ns = xmlMeta.getNamespace(); 181 queueElement(ns, cm.getDictionaryName(), cm); 182 } 183 processQueue(); 184 } 185 186 void processQueue() throws IOException { 187 boolean b; 188 do { 189 b = false; 190 while (! elementQueue.isEmpty()) { 191 QueueEntry q = elementQueue.removeFirst(); 192 b |= getSchema(q.ns).processElement(q.name, q.cm); 193 } 194 while (! typeQueue.isEmpty()) { 195 QueueEntry q = typeQueue.removeFirst(); 196 b |= getSchema(q.ns).processType(q.name, q.cm); 197 } 198 while (! attributeQueue.isEmpty()) { 199 QueueEntry q = attributeQueue.removeFirst(); 200 b |= getSchema(q.ns).processAttribute(q.name, q.cm); 201 } 202 } while (b); 203 } 204 205 void queueElement(Namespace ns, String name, ClassMeta<?> cm) { 206 elementQueue.add(new QueueEntry(ns, name, cm)); 207 } 208 209 void queueType(Namespace ns, String name, ClassMeta<?> cm) { 210 if (name == null) 211 name = XmlUtils.encodeElementName(cm); 212 typeQueue.add(new QueueEntry(ns, name, cm)); 213 } 214 215 void queueAttribute(Namespace ns, String name, ClassMeta<?> cm) { 216 attributeQueue.add(new QueueEntry(ns, name, cm)); 217 } 218 219 void serializeTo(Writer w) throws IOException { 220 boolean b = false; 221 for (Schema s : values()) { 222 if (b) 223 w.append('\u0000'); 224 w.append(s.toString()); 225 b = true; 226 } 227 } 228 } 229 230 @Override /* SerializerSession */ 231 protected boolean isTrimStrings() { 232 return super.isTrimStrings(); 233 } 234 235 /* An encapsulation of a single schema. */ 236 private final class Schema { 237 private StringWriter sw = new StringWriter(); 238 private XmlWriter w; 239 private Namespace defaultNs, targetNs; 240 private Schemas schemas; 241 private Set<String> 242 processedTypes = new HashSet<>(), 243 processedAttributes = new HashSet<>(), 244 processedElements = new HashSet<>(); 245 246 @SuppressWarnings("synthetic-access") 247 public Schema(Schemas schemas, Namespace xs, Namespace targetNs, Namespace defaultNs, Namespace[] allNs) throws IOException { 248 this.schemas = schemas; 249 this.defaultNs = defaultNs; 250 this.targetNs = targetNs; 251 w = new XmlWriter(sw, isUseWhitespace(), getMaxIndent(), isTrimStrings(), getQuoteChar(), null, true, null); 252 int i = indent; 253 w.oTag(i, "schema"); 254 w.attr("xmlns", xs.getUri()); 255 w.attr("targetNamespace", targetNs.getUri()); 256 w.attr("elementFormDefault", "qualified"); 257 if (targetNs != defaultNs) 258 w.attr("attributeFormDefault", "qualified"); 259 for (Namespace ns2 : allNs) 260 w.attr("xmlns", ns2.getName(), ns2.getUri()); 261 w.append('>').nl(i); 262 for (Namespace ns : allNs) { 263 if (ns != targetNs) { 264 w.oTag(i+1, "import") 265 .attr("namespace", ns.getUri()) 266 .attr("schemaLocation", ns.getName()+".xsd") 267 .append("/>").nl(i+1); 268 } 269 } 270 } 271 272 boolean processElement(String name, ClassMeta<?> cm) throws IOException { 273 if (processedElements.contains(name)) 274 return false; 275 processedElements.add(name); 276 277 ClassMeta<?> ft = cm.getSerializedClassMeta(schemas.session); 278 if (name == null) 279 name = getElementName(ft); 280 Namespace ns = first(ft.getExtendedMeta(XmlClassMeta.class).getNamespace(), defaultNs); 281 String type = getXmlType(ns, ft); 282 283 w.oTag(indent+1, "element") 284 .attr("name", XmlUtils.encodeElementName(name)) 285 .attr("type", type) 286 .append('/').append('>').nl(indent+1); 287 288 schemas.queueType(ns, null, ft); 289 schemas.processQueue(); 290 return true; 291 } 292 293 boolean processAttribute(String name, ClassMeta<?> cm) throws IOException { 294 if (processedAttributes.contains(name)) 295 return false; 296 processedAttributes.add(name); 297 298 String type = getXmlAttrType(cm); 299 300 w.oTag(indent+1, "attribute") 301 .attr("name", name) 302 .attr("type", type) 303 .append('/').append('>').nl(indent+1); 304 305 return true; 306 } 307 308 boolean processType(String name, ClassMeta<?> cm) throws IOException { 309 if (processedTypes.contains(name)) 310 return false; 311 processedTypes.add(name); 312 313 int i = indent + 1; 314 315 cm = cm.getSerializedClassMeta(schemas.session); 316 while (cm.isOptional()) 317 cm = cm.getElementType(); 318 319 XmlBeanMeta xbm = cm.isBean() ? cm.getBeanMeta().getExtendedMeta(XmlBeanMeta.class) : null; 320 321 w.oTag(i, "complexType") 322 .attr("name", name); 323 324 // This element can have mixed content if: 325 // 1) It's a generic Object (so it can theoretically be anything) 326 // 2) The bean has a property defined with @XmlFormat.CONTENT. 327 if ((xbm != null && (xbm.getContentFormat() != null && xbm.getContentFormat().isOneOf(TEXT,TEXT_PWS,MIXED,MIXED_PWS,XMLTEXT))) || ! cm.isMapOrBean()) 328 w.attr("mixed", "true"); 329 330 w.cTag().nl(i); 331 332 if (! (cm.isMapOrBean() || cm.isCollectionOrArray() || (cm.isAbstract() && ! cm.isNumber()) || cm.isObject())) { 333 w.oTag(i+1, "attribute").attr("name", getBeanTypePropertyName(cm)).attr("type", "string").ceTag().nl(i+1); 334 335 } else { 336 337 //----- Bean ----- 338 if (cm.isBean()) { 339 BeanMeta<?> bm = cm.getBeanMeta(); 340 341 boolean hasChildElements = false; 342 343 for (BeanPropertyMeta pMeta : bm.getPropertyMetas()) { 344 if (pMeta.canRead()) { 345 XmlFormat bpXml = pMeta.getExtendedMeta(XmlBeanPropertyMeta.class).getXmlFormat(); 346 if (bpXml != XmlFormat.ATTR) 347 hasChildElements = true; 348 } 349 } 350 351 XmlBeanMeta xbm2 = bm.getExtendedMeta(XmlBeanMeta.class); 352 if (xbm2.getContentProperty() != null && xbm2.getContentFormat() == ELEMENTS) { 353 w.sTag(i+1, "sequence").nl(i+1); 354 w.oTag(i+2, "any") 355 .attr("processContents", "skip") 356 .attr("minOccurs", 0) 357 .ceTag().nl(i+2); 358 w.eTag(i+1, "sequence").nl(i+1); 359 360 } else if (hasChildElements) { 361 362 boolean hasOtherNsElement = false; 363 boolean hasCollapsed = false; 364 365 for (BeanPropertyMeta pMeta : bm.getPropertyMetas()) { 366 if (pMeta.canRead()) { 367 XmlBeanPropertyMeta xmlMeta = pMeta.getExtendedMeta(XmlBeanPropertyMeta.class); 368 if (xmlMeta.getXmlFormat() != ATTR) { 369 if (xmlMeta.getNamespace() != null) { 370 ClassMeta<?> ct2 = pMeta.getClassMeta(); 371 Namespace cNs = first(xmlMeta.getNamespace(), ct2.getExtendedMeta(XmlClassMeta.class).getNamespace(), cm.getExtendedMeta(XmlClassMeta.class).getNamespace(), defaultNs); 372 // Child element is in another namespace. 373 schemas.queueElement(cNs, pMeta.getName(), ct2); 374 hasOtherNsElement = true; 375 } 376 if (xmlMeta.getXmlFormat() == COLLAPSED) 377 hasCollapsed = true; 378 } 379 380 } 381 } 382 383 if (hasOtherNsElement || hasCollapsed) { 384 // If this bean has any child elements in another namespace, 385 // we need to add an <any> element. 386 w.oTag(i+1, "choice").attr("maxOccurs", "unbounded").cTag().nl(i+1); 387 w.oTag(i+2, "any") 388 .attr("processContents", "skip") 389 .attr("minOccurs", 0) 390 .ceTag().nl(i+2); 391 w.eTag(i+1, "choice").nl(i+1); 392 393 } else { 394 w.sTag(i+1, "all").nl(i+1); 395 for (BeanPropertyMeta pMeta : bm.getPropertyMetas()) { 396 if (pMeta.canRead()) { 397 XmlBeanPropertyMeta xmlMeta = pMeta.getExtendedMeta(XmlBeanPropertyMeta.class); 398 if (xmlMeta.getXmlFormat() != ATTR) { 399 boolean isCollapsed = xmlMeta.getXmlFormat() == COLLAPSED; 400 ClassMeta<?> ct2 = pMeta.getClassMeta(); 401 String childName = pMeta.getName(); 402 if (isCollapsed) { 403 if (xmlMeta.getChildName() != null) 404 childName = xmlMeta.getChildName(); 405 ct2 = pMeta.getClassMeta().getElementType(); 406 } 407 Namespace cNs = first(xmlMeta.getNamespace(), ct2.getExtendedMeta(XmlClassMeta.class).getNamespace(), cm.getExtendedMeta(XmlClassMeta.class).getNamespace(), defaultNs); 408 if (xmlMeta.getNamespace() == null) { 409 w.oTag(i+2, "element") 410 .attr("name", XmlUtils.encodeElementName(childName), false) 411 .attr("type", getXmlType(cNs, ct2)) 412 .attr("minOccurs", 0); 413 414 w.ceTag().nl(i+2); 415 } else { 416 // Child element is in another namespace. 417 schemas.queueElement(cNs, pMeta.getName(), ct2); 418 hasOtherNsElement = true; 419 } 420 } 421 } 422 } 423 w.eTag(i+1, "all").nl(i+1); 424 } 425 426 } 427 428 for (BeanPropertyMeta pMeta : bm.getExtendedMeta(XmlBeanMeta.class).getAttrProperties().values()) { 429 if (pMeta.canRead()) { 430 Namespace pNs = pMeta.getExtendedMeta(XmlBeanPropertyMeta.class).getNamespace(); 431 if (pNs == null) 432 pNs = defaultNs; 433 434 // If the bean attribute has a different namespace than the bean, then it needs to 435 // be added as a top-level entry in the appropriate schema file. 436 if (pNs != targetNs) { 437 schemas.queueAttribute(pNs, pMeta.getName(), pMeta.getClassMeta()); 438 w.oTag(i+1, "attribute") 439 //.attr("name", pMeta.getName(), true) 440 .attr("ref", pNs.getName() + ':' + pMeta.getName()) 441 .ceTag().nl(i+1); 442 } 443 444 // Otherwise, it's just a plain attribute of this bean. 445 else { 446 w.oTag(i+1, "attribute") 447 .attr("name", pMeta.getName(), true) 448 .attr("type", getXmlAttrType(pMeta.getClassMeta())) 449 .ceTag().nl(i+1); 450 } 451 } 452 } 453 454 //----- Collection ----- 455 } else if (cm.isCollectionOrArray()) { 456 ClassMeta<?> elementType = cm.getElementType(); 457 if (elementType.isObject()) { 458 w.sTag(i+1, "sequence").nl(i+1); 459 w.oTag(i+2, "any") 460 .attr("processContents", "skip") 461 .attr("maxOccurs", "unbounded") 462 .attr("minOccurs", "0") 463 .ceTag().nl(i+2); 464 w.eTag(i+1, "sequence").nl(i+1); 465 } else { 466 Namespace cNs = first(elementType.getExtendedMeta(XmlClassMeta.class).getNamespace(), cm.getExtendedMeta(XmlClassMeta.class).getNamespace(), defaultNs); 467 schemas.queueType(cNs, null, elementType); 468 w.sTag(i+1, "sequence").nl(i+1); 469 w.oTag(i+2, "any") 470 .attr("processContents", "skip") 471 .attr("maxOccurs", "unbounded") 472 .attr("minOccurs", "0") 473 .ceTag().nl(i+2); 474 w.eTag(i+1, "sequence").nl(i+1); 475 } 476 477 //----- Map ----- 478 } else if (cm.isMap() || cm.isAbstract() || cm.isObject()) { 479 w.sTag(i+1, "sequence").nl(i+1); 480 w.oTag(i+2, "any") 481 .attr("processContents", "skip") 482 .attr("maxOccurs", "unbounded") 483 .attr("minOccurs", "0") 484 .ceTag().nl(i+2); 485 w.eTag(i+1, "sequence").nl(i+1); 486 } 487 488 w.oTag(i+1, "attribute") 489 .attr("name", getBeanTypePropertyName(null)) 490 .attr("type", "string") 491 .ceTag().nl(i+1); 492 } 493 494 w.eTag(i, "complexType").nl(i); 495 schemas.processQueue(); 496 497 return true; 498 } 499 500 private String getElementName(ClassMeta<?> cm) { 501 cm = cm.getSerializedClassMeta(schemas.session); 502 String name = cm.getDictionaryName(); 503 504 if (name == null) { 505 if (cm.isBoolean()) 506 name = "boolean"; 507 else if (cm.isNumber()) 508 name = "number"; 509 else if (cm.isCollectionOrArray()) 510 name = "array"; 511 else if (! (cm.isMapOrBean() || cm.isCollectionOrArray() || cm.isObject() || cm.isAbstract())) 512 name = "string"; 513 else 514 name = "object"; 515 } 516 return name; 517 } 518 519 @Override /* Object */ 520 public String toString() { 521 try { 522 w.eTag(indent, "schema").nl(indent); 523 } catch (IOException e) { 524 throw new RuntimeException(e); // Shouldn't happen. 525 } 526 return sw.toString(); 527 } 528 529 private String getXmlType(Namespace currentNs, ClassMeta<?> cm) { 530 String name = null; 531 cm = cm.getSerializedClassMeta(schemas.session); 532 if (currentNs == targetNs) { 533 if (cm.isPrimitive()) { 534 if (cm.isBoolean()) 535 name = "boolean"; 536 else if (cm.isNumber()) { 537 if (cm.isDecimal()) 538 name = "decimal"; 539 else 540 name = "integer"; 541 } 542 } 543 } 544 if (name == null) { 545 name = XmlUtils.encodeElementName(cm); 546 schemas.queueType(currentNs, name, cm); 547 return currentNs.getName() + ":" + name; 548 } 549 550 return name; 551 } 552 } 553 554 @SafeVarargs 555 static <T> T first(T...tt) { 556 for (T t : tt) 557 if (t != null) 558 return t; 559 return null; 560 } 561 562 static String getXmlAttrType(ClassMeta<?> cm) { 563 if (cm.isBoolean()) 564 return "boolean"; 565 if (cm.isNumber()) { 566 if (cm.isDecimal()) 567 return "decimal"; 568 return "integer"; 569 } 570 return "string"; 571 } 572 573 //----------------------------------------------------------------------------------------------------------------- 574 // Other methods 575 //----------------------------------------------------------------------------------------------------------------- 576 577 @Override /* Session */ 578 public ObjectMap toMap() { 579 return super.toMap() 580 .append("XmlSchemaSerializerSession", new DefaultFilteringObjectMap() 581 ); 582 } 583}