SaajStaxWriter.java revision 744:a98174edd246
1/* 2 * Copyright (c) 2014, 2017, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26package com.sun.xml.internal.messaging.saaj.util.stax; 27 28import java.util.Iterator; 29import java.util.Arrays; 30import java.util.List; 31import java.util.LinkedList; 32 33import javax.xml.namespace.NamespaceContext; 34import javax.xml.namespace.QName; 35import javax.xml.soap.SOAPElement; 36import javax.xml.soap.SOAPException; 37import javax.xml.soap.SOAPMessage; 38import javax.xml.stream.XMLStreamException; 39import javax.xml.stream.XMLStreamWriter; 40 41import org.w3c.dom.Comment; 42import org.w3c.dom.Node; 43 44/** 45 * SaajStaxWriter builds a SAAJ SOAPMessage by using XMLStreamWriter interface. 46 * 47 * <p> 48 * Defers creation of SOAPElement until all the aspects of the name of the element are known. 49 * In some cases, the namespace uri is indicated only by the {@link #writeNamespace(String, String)} call. 50 * After opening an element ({@code writeStartElement}, {@code writeEmptyElement} methods), all attributes 51 * and namespace assignments are retained within {@link DeferredElement} object ({@code deferredElement} field). 52 * As soon as any other method than {@code writeAttribute}, {@code writeNamespace}, {@code writeDefaultNamespace} 53 * or {@code setNamespace} is called, the contents of {@code deferredElement} is transformed into new SOAPElement 54 * (which is appropriately inserted into the SOAPMessage under construction). 55 * This mechanism is necessary to fix JDK-8159058 issue. 56 * </p> 57 * 58 * @author shih-chang.chen@oracle.com 59 */ 60public class SaajStaxWriter implements XMLStreamWriter { 61 62 protected SOAPMessage soap; 63 protected String envURI; 64 protected SOAPElement currentElement; 65 protected DeferredElement deferredElement; 66 67 static final protected String Envelope = "Envelope"; 68 static final protected String Header = "Header"; 69 static final protected String Body = "Body"; 70 static final protected String xmlns = "xmlns"; 71 72 public SaajStaxWriter(final SOAPMessage msg, String uri) throws SOAPException { 73 soap = msg; 74 this.envURI = uri; 75 this.deferredElement = new DeferredElement(); 76 } 77 78 public SOAPMessage getSOAPMessage() { 79 return soap; 80 } 81 82 protected SOAPElement getEnvelope() throws SOAPException { 83 return soap.getSOAPPart().getEnvelope(); 84 } 85 86 @Override 87 public void writeStartElement(final String localName) throws XMLStreamException { 88 currentElement = deferredElement.flushTo(currentElement); 89 deferredElement.setLocalName(localName); 90 } 91 92 @Override 93 public void writeStartElement(final String ns, final String ln) throws XMLStreamException { 94 writeStartElement(null, ln, ns); 95 } 96 97 @Override 98 public void writeStartElement(final String prefix, final String ln, final String ns) throws XMLStreamException { 99 currentElement = deferredElement.flushTo(currentElement); 100 101 if (envURI.equals(ns)) { 102 try { 103 if (Envelope.equals(ln)) { 104 currentElement = getEnvelope(); 105 fixPrefix(prefix); 106 return; 107 } else if (Header.equals(ln)) { 108 currentElement = soap.getSOAPHeader(); 109 fixPrefix(prefix); 110 return; 111 } else if (Body.equals(ln)) { 112 currentElement = soap.getSOAPBody(); 113 fixPrefix(prefix); 114 return; 115 } 116 } catch (SOAPException e) { 117 throw new XMLStreamException(e); 118 } 119 120 } 121 122 deferredElement.setLocalName(ln); 123 deferredElement.setNamespaceUri(ns); 124 deferredElement.setPrefix(prefix); 125 126 } 127 128 private void fixPrefix(final String prfx) throws XMLStreamException { 129 fixPrefix(prfx, currentElement); 130 } 131 132 private void fixPrefix(final String prfx, SOAPElement element) throws XMLStreamException { 133 String oldPrfx = element.getPrefix(); 134 if (prfx != null && !prfx.equals(oldPrfx)) { 135 element.setPrefix(prfx); 136 } 137 } 138 139 @Override 140 public void writeEmptyElement(final String uri, final String ln) throws XMLStreamException { 141 writeStartElement(null, ln, uri); 142 } 143 144 @Override 145 public void writeEmptyElement(final String prefix, final String ln, final String uri) throws XMLStreamException { 146 writeStartElement(prefix, ln, uri); 147 } 148 149 @Override 150 public void writeEmptyElement(final String ln) throws XMLStreamException { 151 writeStartElement(null, ln, null); 152 } 153 154 @Override 155 public void writeEndElement() throws XMLStreamException { 156 currentElement = deferredElement.flushTo(currentElement); 157 if (currentElement != null) currentElement = currentElement.getParentElement(); 158 } 159 160 @Override 161 public void writeEndDocument() throws XMLStreamException { 162 currentElement = deferredElement.flushTo(currentElement); 163 } 164 165 @Override 166 public void close() throws XMLStreamException { 167 } 168 169 @Override 170 public void flush() throws XMLStreamException { 171 } 172 173 @Override 174 public void writeAttribute(final String ln, final String val) throws XMLStreamException { 175 writeAttribute(null, null, ln, val); 176 } 177 178 @Override 179 public void writeAttribute(final String prefix, final String ns, final String ln, final String value) throws XMLStreamException { 180 if (ns == null && prefix == null && xmlns.equals(ln)) { 181 writeNamespace("", value); 182 } else { 183 if (deferredElement.isInitialized()) { 184 deferredElement.addAttribute(prefix, ns, ln, value); 185 } else { 186 addAttibuteToElement(currentElement, prefix, ns, ln, value); 187 } 188 } 189 } 190 191 @Override 192 public void writeAttribute(final String ns, final String ln, final String val) throws XMLStreamException { 193 writeAttribute(null, ns, ln, val); 194 } 195 196 @Override 197 public void writeNamespace(String prefix, final String uri) throws XMLStreamException { 198 // make prefix default if null or "xmlns" (according to javadoc) 199 String thePrefix = prefix == null || "xmlns".equals(prefix) ? "" : prefix; 200 if (deferredElement.isInitialized()) { 201 deferredElement.addNamespaceDeclaration(thePrefix, uri); 202 } else { 203 try { 204 currentElement.addNamespaceDeclaration(thePrefix, uri); 205 } catch (SOAPException e) { 206 throw new XMLStreamException(e); 207 } 208 } 209 } 210 211 @Override 212 public void writeDefaultNamespace(final String uri) throws XMLStreamException { 213 writeNamespace("", uri); 214 } 215 216 @Override 217 public void writeComment(final String data) throws XMLStreamException { 218 currentElement = deferredElement.flushTo(currentElement); 219 Comment c = soap.getSOAPPart().createComment(data); 220 currentElement.appendChild(c); 221 } 222 223 @Override 224 public void writeProcessingInstruction(final String target) throws XMLStreamException { 225 currentElement = deferredElement.flushTo(currentElement); 226 Node n = soap.getSOAPPart().createProcessingInstruction(target, ""); 227 currentElement.appendChild(n); 228 } 229 230 @Override 231 public void writeProcessingInstruction(final String target, final String data) throws XMLStreamException { 232 currentElement = deferredElement.flushTo(currentElement); 233 Node n = soap.getSOAPPart().createProcessingInstruction(target, data); 234 currentElement.appendChild(n); 235 } 236 237 @Override 238 public void writeCData(final String data) throws XMLStreamException { 239 currentElement = deferredElement.flushTo(currentElement); 240 Node n = soap.getSOAPPart().createCDATASection(data); 241 currentElement.appendChild(n); 242 } 243 244 @Override 245 public void writeDTD(final String dtd) throws XMLStreamException { 246 currentElement = deferredElement.flushTo(currentElement); 247 } 248 249 @Override 250 public void writeEntityRef(final String name) throws XMLStreamException { 251 currentElement = deferredElement.flushTo(currentElement); 252 Node n = soap.getSOAPPart().createEntityReference(name); 253 currentElement.appendChild(n); 254 } 255 256 @Override 257 public void writeStartDocument() throws XMLStreamException { 258 } 259 260 @Override 261 public void writeStartDocument(final String version) throws XMLStreamException { 262 if (version != null) soap.getSOAPPart().setXmlVersion(version); 263 } 264 265 @Override 266 public void writeStartDocument(final String encoding, final String version) throws XMLStreamException { 267 if (version != null) soap.getSOAPPart().setXmlVersion(version); 268 if (encoding != null) { 269 try { 270 soap.setProperty(SOAPMessage.CHARACTER_SET_ENCODING, encoding); 271 } catch (SOAPException e) { 272 throw new XMLStreamException(e); 273 } 274 } 275 } 276 277 @Override 278 public void writeCharacters(final String text) throws XMLStreamException { 279 currentElement = deferredElement.flushTo(currentElement); 280 try { 281 currentElement.addTextNode(text); 282 } catch (SOAPException e) { 283 throw new XMLStreamException(e); 284 } 285 } 286 287 @Override 288 public void writeCharacters(final char[] text, final int start, final int len) throws XMLStreamException { 289 currentElement = deferredElement.flushTo(currentElement); 290 char[] chr = (start == 0 && len == text.length) ? text : Arrays.copyOfRange(text, start, start + len); 291 try { 292 currentElement.addTextNode(new String(chr)); 293 } catch (SOAPException e) { 294 throw new XMLStreamException(e); 295 } 296 } 297 298 @Override 299 public String getPrefix(final String uri) throws XMLStreamException { 300 return currentElement.lookupPrefix(uri); 301 } 302 303 @Override 304 public void setPrefix(final String prefix, final String uri) throws XMLStreamException { 305 // TODO: this in fact is not what would be expected from XMLStreamWriter 306 // (e.g. XMLStreamWriter for writing to output stream does not write anything as result of 307 // this method, it just rememebers that given prefix is associated with the given uri 308 // for the scope; to actually declare the prefix assignment in the resulting XML, one 309 // needs to call writeNamespace(...) method 310 // Kept for backwards compatibility reasons - this might be worth of further investigation. 311 if (deferredElement.isInitialized()) { 312 deferredElement.addNamespaceDeclaration(prefix, uri); 313 } else { 314 throw new XMLStreamException("Namespace not associated with any element"); 315 } 316 } 317 318 @Override 319 public void setDefaultNamespace(final String uri) throws XMLStreamException { 320 setPrefix("", uri); 321 } 322 323 @Override 324 public void setNamespaceContext(final NamespaceContext context)throws XMLStreamException { 325 throw new UnsupportedOperationException(); 326 } 327 328 @Override 329 public Object getProperty(final String name) throws IllegalArgumentException { 330 //TODO the following line is to make eclipselink happy ... they are aware of this problem - 331 if (javax.xml.stream.XMLOutputFactory.IS_REPAIRING_NAMESPACES.equals(name)) return Boolean.FALSE; 332 return null; 333 } 334 335 @Override 336 public NamespaceContext getNamespaceContext() { 337 return new NamespaceContext() { 338 public String getNamespaceURI(final String prefix) { 339 return currentElement.getNamespaceURI(prefix); 340 } 341 public String getPrefix(final String namespaceURI) { 342 return currentElement.lookupPrefix(namespaceURI); 343 } 344 public Iterator getPrefixes(final String namespaceURI) { 345 return new Iterator<String>() { 346 String prefix = getPrefix(namespaceURI); 347 public boolean hasNext() { 348 return (prefix != null); 349 } 350 public String next() { 351 if (!hasNext()) throw new java.util.NoSuchElementException(); 352 String next = prefix; 353 prefix = null; 354 return next; 355 } 356 public void remove() {} 357 }; 358 } 359 }; 360 } 361 362 static void addAttibuteToElement(SOAPElement element, String prefix, String ns, String ln, String value) 363 throws XMLStreamException { 364 try { 365 if (ns == null) { 366 element.setAttributeNS("", ln, value); 367 } else { 368 QName name = prefix == null ? new QName(ns, ln) : new QName(ns, ln, prefix); 369 element.addAttribute(name, value); 370 } 371 } catch (SOAPException e) { 372 throw new XMLStreamException(e); 373 } 374 } 375 376 /** 377 * Holds details of element that needs to be deferred in order to manage namespace assignments correctly. 378 * 379 * <p> 380 * An instance of can be set with all the aspects of the element name (local name, prefix, namespace uri). 381 * Attributes and namespace declarations (special case of attribute) can be added. 382 * Namespace declarations are handled so that the element namespace is updated if it is implied by the namespace 383 * declaration and the namespace was not set to non-{@code null} value previously. 384 * </p> 385 * 386 * <p> 387 * The state of this object can be {@link #flushTo(SOAPElement) flushed} to SOAPElement - new SOAPElement will 388 * be added a child element; the new element will have exactly the shape as represented by the state of this 389 * object. Note that the {@link #flushTo(SOAPElement)} method does nothing 390 * (and returns the argument immediately) if the state of this object is not initialized 391 * (i.e. local name is null). 392 * </p> 393 * 394 * @author ondrej.cerny@oracle.com 395 */ 396 static class DeferredElement { 397 private String prefix; 398 private String localName; 399 private String namespaceUri; 400 private final List<NamespaceDeclaration> namespaceDeclarations; 401 private final List<AttributeDeclaration> attributeDeclarations; 402 403 DeferredElement() { 404 this.namespaceDeclarations = new LinkedList<NamespaceDeclaration>(); 405 this.attributeDeclarations = new LinkedList<AttributeDeclaration>(); 406 reset(); 407 } 408 409 410 /** 411 * Set prefix of the element. 412 * @param prefix namespace prefix 413 */ 414 public void setPrefix(final String prefix) { 415 this.prefix = prefix; 416 } 417 418 /** 419 * Set local name of the element. 420 * 421 * <p> 422 * This method initializes the element. 423 * </p> 424 * 425 * @param localName local name {@code not null} 426 */ 427 public void setLocalName(final String localName) { 428 if (localName == null) { 429 throw new IllegalArgumentException("localName can not be null"); 430 } 431 this.localName = localName; 432 } 433 434 /** 435 * Set namespace uri. 436 * 437 * @param namespaceUri namespace uri 438 */ 439 public void setNamespaceUri(final String namespaceUri) { 440 this.namespaceUri = namespaceUri; 441 } 442 443 /** 444 * Adds namespace prefix assignment to the element. 445 * 446 * @param prefix prefix (not {@code null}) 447 * @param namespaceUri namespace uri 448 */ 449 public void addNamespaceDeclaration(final String prefix, final String namespaceUri) { 450 if (null == this.namespaceUri && null != namespaceUri && prefix.equals(emptyIfNull(this.prefix))) { 451 this.namespaceUri = namespaceUri; 452 } 453 this.namespaceDeclarations.add(new NamespaceDeclaration(prefix, namespaceUri)); 454 } 455 456 /** 457 * Adds attribute to the element. 458 * @param prefix prefix 459 * @param ns namespace 460 * @param ln local name 461 * @param value value 462 */ 463 public void addAttribute(final String prefix, final String ns, final String ln, final String value) { 464 if (ns == null && prefix == null && xmlns.equals(ln)) { 465 this.addNamespaceDeclaration(prefix, value); 466 } else { 467 this.attributeDeclarations.add(new AttributeDeclaration(prefix, ns, ln, value)); 468 } 469 } 470 471 /** 472 * Flushes state of this element to the {@code target} element. 473 * 474 * <p> 475 * If this element is initialized then it is added with all the namespace declarations and attributes 476 * to the {@code target} element as a child. The state of this element is reset to uninitialized. 477 * The newly added element object is returned. 478 * </p> 479 * <p> 480 * If this element is not initialized then the {@code target} is returned immediately, nothing else is done. 481 * </p> 482 * 483 * @param target target element 484 * @return {@code target} or new element 485 * @throws XMLStreamException on error 486 */ 487 public SOAPElement flushTo(final SOAPElement target) throws XMLStreamException { 488 try { 489 if (this.localName != null) { 490 // add the element appropriately (based on namespace declaration) 491 final SOAPElement newElement; 492 if (this.namespaceUri == null) { 493 // add element with inherited scope 494 newElement = target.addChildElement(this.localName); 495 } else if (prefix == null) { 496 newElement = target.addChildElement(new QName(this.namespaceUri, this.localName)); 497 } else { 498 newElement = target.addChildElement(this.localName, this.prefix, this.namespaceUri); 499 } 500 // add namespace declarations 501 for (NamespaceDeclaration namespace : this.namespaceDeclarations) { 502 target.addNamespaceDeclaration(namespace.prefix, namespace.namespaceUri); 503 } 504 // add attribute declarations 505 for (AttributeDeclaration attribute : this.attributeDeclarations) { 506 addAttibuteToElement(newElement, 507 attribute.prefix, attribute.namespaceUri, attribute.localName, attribute.value); 508 } 509 // reset state 510 this.reset(); 511 512 return newElement; 513 } else { 514 return target; 515 } 516 // else after reset state -> not initialized 517 } catch (SOAPException e) { 518 throw new XMLStreamException(e); 519 } 520 } 521 522 /** 523 * Is the element initialized? 524 * @return boolean indicating whether it was initialized after last flush 525 */ 526 public boolean isInitialized() { 527 return this.localName != null; 528 } 529 530 private void reset() { 531 this.localName = null; 532 this.prefix = null; 533 this.namespaceUri = null; 534 this.namespaceDeclarations.clear(); 535 this.attributeDeclarations.clear(); 536 } 537 538 private static String emptyIfNull(String s) { 539 return s == null ? "" : s; 540 } 541 } 542 543 static class NamespaceDeclaration { 544 final String prefix; 545 final String namespaceUri; 546 547 NamespaceDeclaration(String prefix, String namespaceUri) { 548 this.prefix = prefix; 549 this.namespaceUri = namespaceUri; 550 } 551 } 552 553 static class AttributeDeclaration { 554 final String prefix; 555 final String namespaceUri; 556 final String localName; 557 final String value; 558 559 AttributeDeclaration(String prefix, String namespaceUri, String localName, String value) { 560 this.prefix = prefix; 561 this.namespaceUri = namespaceUri; 562 this.localName = localName; 563 this.value = value; 564 } 565 } 566} 567