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