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