001/* 002 * The MIT License 003 * Copyright (c) 2012 Microsoft Corporation 004 * 005 * Permission is hereby granted, free of charge, to any person obtaining a copy 006 * of this software and associated documentation files (the "Software"), to deal 007 * in the Software without restriction, including without limitation the rights 008 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 009 * copies of the Software, and to permit persons to whom the Software is 010 * furnished to do so, subject to the following conditions: 011 * 012 * The above copyright notice and this permission notice shall be included in 013 * all copies or substantial portions of the Software. 014 * 015 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 016 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 017 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 018 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 019 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 020 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 021 * THE SOFTWARE. 022 */ 023 024package microsoft.exchange.webservices.data.core; 025 026import microsoft.exchange.webservices.data.core.enumeration.misc.XmlNamespace; 027import microsoft.exchange.webservices.data.core.exception.service.local.ServiceXmlSerializationException; 028import microsoft.exchange.webservices.data.misc.OutParam; 029import microsoft.exchange.webservices.data.property.complex.ISearchStringProvider; 030import org.apache.commons.codec.binary.Base64; 031import org.apache.commons.logging.Log; 032import org.apache.commons.logging.LogFactory; 033import org.w3c.dom.CDATASection; 034import org.w3c.dom.Comment; 035import org.w3c.dom.Document; 036import org.w3c.dom.Element; 037import org.w3c.dom.EntityReference; 038import org.w3c.dom.NamedNodeMap; 039import org.w3c.dom.Node; 040import org.w3c.dom.NodeList; 041import org.w3c.dom.ProcessingInstruction; 042import org.w3c.dom.Text; 043 044import javax.xml.stream.XMLOutputFactory; 045import javax.xml.stream.XMLStreamException; 046import javax.xml.stream.XMLStreamWriter; 047 048import java.io.ByteArrayOutputStream; 049import java.io.IOException; 050import java.io.InputStream; 051import java.io.OutputStream; 052import java.util.Date; 053 054/** 055 * Stax based XML Writer implementation. 056 */ 057public class EwsServiceXmlWriter implements IDisposable { 058 059 private static final Log LOG = LogFactory.getLog(EwsServiceXmlWriter.class); 060 061 /** 062 * The is disposed. 063 */ 064 private boolean isDisposed; 065 066 /** 067 * The service. 068 */ 069 private ExchangeServiceBase service; 070 071 /** 072 * The xml writer. 073 */ 074 private XMLStreamWriter xmlWriter; 075 076 /** 077 * The is time zone header emitted. 078 */ 079 private boolean isTimeZoneHeaderEmitted; 080 081 /** 082 * The Buffer size. 083 */ 084 private static final int BufferSize = 4096; 085 086 /** 087 * The requireWSSecurityUtilityNamespace * 088 */ 089 090 protected boolean requireWSSecurityUtilityNamespace; 091 092 /** 093 * Initializes a new instance. 094 * 095 * @param service the service 096 * @param stream the stream 097 * @throws XMLStreamException the XML stream exception 098 */ 099 public EwsServiceXmlWriter(ExchangeServiceBase service, OutputStream stream) throws XMLStreamException { 100 this.service = service; 101 XMLOutputFactory xmlof = XMLOutputFactory.newInstance(); 102 xmlWriter = xmlof.createXMLStreamWriter(stream, "utf-8"); 103 104 } 105 106 /** 107 * Try to convert object to a string. 108 * 109 * @param value The value. 110 * @param str the str 111 * @return True if object was converted, false otherwise. A null object will 112 * be "successfully" converted to a null string. 113 */ 114 protected boolean tryConvertObjectToString(Object value, 115 OutParam<String> str) { 116 boolean converted = true; 117 str.setParam(null); 118 if (value != null) { 119 if (value.getClass().isEnum()) { 120 str.setParam(EwsUtilities.serializeEnum(value)); 121 } else if (value.getClass().equals(Boolean.class)) { 122 str.setParam(EwsUtilities.boolToXSBool((Boolean) value)); 123 } else if (value instanceof Date) { 124 str 125 .setParam(this.service 126 .convertDateTimeToUniversalDateTimeString( 127 (Date) value)); 128 } else if (value.getClass().isPrimitive()) { 129 str.setParam(value.toString()); 130 } else if (value instanceof String) { 131 str.setParam(value.toString()); 132 } else if (value instanceof ISearchStringProvider) { 133 ISearchStringProvider searchStringProvider = 134 (ISearchStringProvider) value; 135 str.setParam(searchStringProvider.getSearchString()); 136 } else if (value instanceof Number) { 137 str.setParam(value.toString()); 138 } else { 139 converted = false; 140 } 141 } 142 return converted; 143 } 144 145 /** 146 * Performs application-defined tasks associated with freeing, releasing, or 147 * resetting unmanaged resources. 148 */ 149 @Override 150 public void dispose() { 151 if (!this.isDisposed) { 152 try { 153 this.xmlWriter.close(); 154 } catch (XMLStreamException e) { 155 LOG.error(e); 156 } 157 this.isDisposed = true; 158 } 159 } 160 161 /** 162 * Flushes this instance. 163 * 164 * @throws XMLStreamException the XML stream exception 165 */ 166 public void flush() throws XMLStreamException { 167 this.xmlWriter.flush(); 168 } 169 170 /** 171 * Writes the start element. 172 * 173 * @param xmlNamespace the XML namespace 174 * @param localName the local name of the element 175 * @throws XMLStreamException the XML stream exception 176 */ 177 public void writeStartElement(XmlNamespace xmlNamespace, String localName) 178 throws XMLStreamException { 179 String strPrefix = EwsUtilities.getNamespacePrefix(xmlNamespace); 180 String strNameSpace = EwsUtilities.getNamespaceUri(xmlNamespace); 181 this.xmlWriter.writeStartElement(strPrefix, localName, strNameSpace); 182 } 183 184 /** 185 * Writes the end element. 186 * 187 * @throws XMLStreamException the XML stream exception 188 */ 189 public void writeEndElement() throws XMLStreamException { 190 this.xmlWriter.writeEndElement(); 191 } 192 193 /** 194 * Writes the attribute value. 195 * 196 * @param localName the local name of the attribute 197 * @param value the value 198 * @throws ServiceXmlSerializationException the service xml serialization exception 199 */ 200 public void writeAttributeValue(String localName, Object value) 201 throws ServiceXmlSerializationException { 202 this.writeAttributeValue(localName, 203 false /* alwaysWriteEmptyString */, value); 204 } 205 206 /** 207 * Writes the attribute value. Optionally emits empty string values. 208 * 209 * @param localName the local name of the attribute. 210 * @param alwaysWriteEmptyString always emit the empty string as the value. 211 * @param value the value 212 * @throws ServiceXmlSerializationException the service xml serialization exception 213 */ 214 public void writeAttributeValue(String localName, 215 boolean alwaysWriteEmptyString, 216 Object value) throws ServiceXmlSerializationException { 217 OutParam<String> stringOut = new OutParam<String>(); 218 String stringValue = null; 219 if (this.tryConvertObjectToString(value, stringOut)) { 220 stringValue = stringOut.getParam(); 221 if ((null != stringValue) && (alwaysWriteEmptyString || (stringValue.length() != 0))) { 222 this.writeAttributeString(localName, stringValue); 223 } 224 } else { 225 throw new ServiceXmlSerializationException(String.format( 226 "Values of type '%s' can't be used for the '%s' attribute.", value.getClass() 227 .getName(), localName)); 228 } 229 } 230 231 /** 232 * Writes the attribute value. 233 * 234 * @param namespacePrefix the namespace prefix 235 * @param localName the local name of the attribute 236 * @param value the value 237 * @throws ServiceXmlSerializationException the service xml serialization exception 238 */ 239 public void writeAttributeValue(String namespacePrefix, String localName, 240 Object value) throws ServiceXmlSerializationException { 241 OutParam<String> stringOut = new OutParam<String>(); 242 String stringValue = null; 243 if (this.tryConvertObjectToString(value, stringOut)) { 244 stringValue = stringOut.getParam(); 245 if (null != stringValue && !stringValue.isEmpty()) { 246 this.writeAttributeString(namespacePrefix, localName, 247 stringValue); 248 } 249 } else { 250 throw new ServiceXmlSerializationException(String.format( 251 "Values of type '%s' can't be used for the '%s' attribute.", value.getClass() 252 .getName(), localName)); 253 } 254 } 255 256 /** 257 * Writes the attribute value. 258 * 259 * @param localName The local name of the attribute. 260 * @param stringValue The string value. 261 * @throws ServiceXmlSerializationException Thrown if string value isn't valid for XML 262 */ 263 protected void writeAttributeString(String localName, String stringValue) 264 throws ServiceXmlSerializationException { 265 try { 266 this.xmlWriter.writeAttribute(localName, stringValue); 267 } catch (XMLStreamException e) { 268 // Bug E14:65046: XmlTextWriter will throw ArgumentException 269 //if string includes invalid characters. 270 throw new ServiceXmlSerializationException(String.format( 271 "The invalid value '%s' was specified for the '%s' attribute.", stringValue, localName), e); 272 } 273 } 274 275 /** 276 * Writes the attribute value. 277 * 278 * @param namespacePrefix The namespace prefix. 279 * @param localName The local name of the attribute. 280 * @param stringValue The string value. 281 * @throws ServiceXmlSerializationException Thrown if string value isn't valid for XML. 282 */ 283 protected void writeAttributeString(String namespacePrefix, 284 String localName, String stringValue) 285 throws ServiceXmlSerializationException { 286 try { 287 this.xmlWriter.writeAttribute(namespacePrefix, "", localName, 288 stringValue); 289 } catch (XMLStreamException e) { 290 // Bug E14:65046: XmlTextWriter will throw ArgumentException 291 //if string includes invalid characters. 292 throw new ServiceXmlSerializationException(String.format( 293 "The invalid value '%s' was specified for the '%s' attribute.", stringValue, localName), e); 294 } 295 } 296 297 /** 298 * Writes string value. 299 * 300 * @param value The value. 301 * @param name Element name (used for error handling) 302 * @throws ServiceXmlSerializationException Thrown if string value isn't valid for XML. 303 */ 304 public void writeValue(String value, String name) 305 throws ServiceXmlSerializationException { 306 try { 307 this.xmlWriter.writeCharacters(value); 308 } catch (XMLStreamException e) { 309 // Bug E14:65046: XmlTextWriter will throw ArgumentException 310 //if string includes invalid characters. 311 throw new ServiceXmlSerializationException(String.format( 312 "The invalid value '%s' was specified for the '%s' element.", value, name), e); 313 } 314 } 315 316 /** 317 * Writes the element value. 318 * 319 * @param xmlNamespace the XML namespace 320 * @param localName the local name of the element 321 * @param displayName the name that should appear in the exception message when the value can not be serialized 322 * @param value the value 323 * @throws XMLStreamException the XML stream exception 324 * @throws ServiceXmlSerializationException the service xml serialization exception 325 */ 326 public void writeElementValue(XmlNamespace xmlNamespace, String localName, String displayName, Object value) 327 throws XMLStreamException, ServiceXmlSerializationException { 328 String stringValue = null; 329 OutParam<String> strOut = new OutParam<String>(); 330 331 if (this.tryConvertObjectToString(value, strOut)) { 332 stringValue = strOut.getParam(); 333 if (null != stringValue) { 334 // allow an empty string to create an empty element (like <Value 335 // />). 336 this.writeStartElement(xmlNamespace, localName); 337 this.writeValue(stringValue, displayName); 338 this.writeEndElement(); 339 } 340 } else { 341 throw new ServiceXmlSerializationException(String.format( 342 "Values of type '%s' can't be used for the '%s' element.", value.getClass() 343 .getName(), localName)); 344 } 345 } 346 347 public void writeNode(Node xmlNode) throws XMLStreamException { 348 if (xmlNode != null) { 349 writeNode(xmlNode, this.xmlWriter); 350 } 351 } 352 353 /** 354 * @param xmlNode XML node 355 * @param xmlStreamWriter XML stream writer 356 * @throws XMLStreamException the XML stream exception 357 */ 358 public static void writeNode(Node xmlNode, XMLStreamWriter xmlStreamWriter) 359 throws XMLStreamException { 360 if (xmlNode instanceof Element) { 361 addElement((Element) xmlNode, xmlStreamWriter); 362 } else if (xmlNode instanceof Text) { 363 xmlStreamWriter.writeCharacters(xmlNode.getNodeValue()); 364 } else if (xmlNode instanceof CDATASection) { 365 xmlStreamWriter.writeCData(((CDATASection) xmlNode).getData()); 366 } else if (xmlNode instanceof Comment) { 367 xmlStreamWriter.writeComment(((Comment) xmlNode).getData()); 368 } else if (xmlNode instanceof EntityReference) { 369 xmlStreamWriter.writeEntityRef(xmlNode.getNodeValue()); 370 } else if (xmlNode instanceof ProcessingInstruction) { 371 ProcessingInstruction procInst = (ProcessingInstruction) xmlNode; 372 xmlStreamWriter.writeProcessingInstruction(procInst.getTarget(), 373 procInst.getData()); 374 } else if (xmlNode instanceof Document) { 375 writeToDocument((Document) xmlNode, xmlStreamWriter); 376 } 377 } 378 379 /** 380 * @param document XML document 381 * @param xmlStreamWriter XML stream writer 382 * @throws XMLStreamException the XML stream exception 383 */ 384 public static void writeToDocument(Document document, 385 XMLStreamWriter xmlStreamWriter) throws XMLStreamException { 386 387 xmlStreamWriter.writeStartDocument(); 388 Element rootElement = document.getDocumentElement(); 389 addElement(rootElement, xmlStreamWriter); 390 xmlStreamWriter.writeEndDocument(); 391 } 392 393 /** 394 * @param element DOM element 395 * @param writer XML stream writer 396 * @throws XMLStreamException the XML stream exception 397 */ 398 public static void addElement(Element element, XMLStreamWriter writer) 399 throws XMLStreamException { 400 String nameSpace = element.getNamespaceURI(); 401 String prefix = element.getPrefix(); 402 String localName = element.getLocalName(); 403 if (prefix == null) { 404 prefix = ""; 405 } 406 if (localName == null) { 407 localName = element.getNodeName(); 408 409 if (localName == null) { 410 throw new IllegalStateException( 411 "Element's local name cannot be null!"); 412 } 413 } 414 415 String decUri = writer.getNamespaceContext().getNamespaceURI(prefix); 416 boolean declareNamespace = decUri == null || !decUri.equals(nameSpace); 417 418 if (nameSpace == null || nameSpace.length() == 0) { 419 writer.writeStartElement(localName); 420 } else { 421 writer.writeStartElement(prefix, localName, nameSpace); 422 } 423 424 NamedNodeMap attrs = element.getAttributes(); 425 for (int i = 0; i < attrs.getLength(); i++) { 426 Node attr = attrs.item(i); 427 428 String name = attr.getNodeName(); 429 String attrPrefix = ""; 430 int prefixIndex = name.indexOf(':'); 431 if (prefixIndex != -1) { 432 attrPrefix = name.substring(0, prefixIndex); 433 name = name.substring(prefixIndex + 1); 434 } 435 436 if ("xmlns".equals(attrPrefix)) { 437 writer.writeNamespace(name, attr.getNodeValue()); 438 if (name.equals(prefix) 439 && attr.getNodeValue().equals(nameSpace)) { 440 declareNamespace = false; 441 } 442 } else { 443 if ("xmlns".equals(name) && "".equals(attrPrefix)) { 444 writer.writeNamespace("", attr.getNodeValue()); 445 if (attr.getNodeValue().equals(nameSpace)) { 446 declareNamespace = false; 447 } 448 } else { 449 writer.writeAttribute(attrPrefix, attr.getNamespaceURI(), 450 name, attr.getNodeValue()); 451 } 452 } 453 } 454 455 if (declareNamespace) { 456 if (nameSpace == null) { 457 writer.writeNamespace(prefix, ""); 458 } else { 459 writer.writeNamespace(prefix, nameSpace); 460 } 461 } 462 463 NodeList nodes = element.getChildNodes(); 464 for (int i = 0; i < nodes.getLength(); i++) { 465 Node n = nodes.item(i); 466 writeNode(n, writer); 467 } 468 469 470 writer.writeEndElement(); 471 472 } 473 474 475 476 /** 477 * Writes the element value. 478 * 479 * @param xmlNamespace the XML namespace 480 * @param localName the local name of the element 481 * @param value the value 482 * @throws XMLStreamException the XML stream exception 483 * @throws ServiceXmlSerializationException the service xml serialization exception 484 */ 485 public void writeElementValue(XmlNamespace xmlNamespace, String localName, 486 Object value) throws XMLStreamException, 487 ServiceXmlSerializationException { 488 this.writeElementValue(xmlNamespace, localName, localName, value); 489 } 490 491 /** 492 * Writes the base64-encoded element value. 493 * 494 * @param buffer the buffer 495 * @throws XMLStreamException the XML stream exception 496 */ 497 public void writeBase64ElementValue(byte[] buffer) 498 throws XMLStreamException { 499 500 String strValue = Base64.encodeBase64String(buffer); 501 this.xmlWriter.writeCharacters(strValue);//Base64.encode(buffer)); 502 } 503 504 /** 505 * Writes the base64-encoded element value. 506 * 507 * @param stream the stream 508 * @throws IOException signals that an I/O exception has occurred 509 * @throws XMLStreamException the XML stream exception 510 */ 511 public void writeBase64ElementValue(InputStream stream) throws IOException, 512 XMLStreamException { 513 514 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 515 byte[] buf = new byte[BufferSize]; 516 try { 517 for (int readNum; (readNum = stream.read(buf)) != -1; ) { 518 bos.write(buf, 0, readNum); 519 } 520 } catch (IOException ex) { 521 LOG.error(ex); 522 } finally { 523 bos.close(); 524 } 525 byte[] bytes = bos.toByteArray(); 526 String strValue = Base64.encodeBase64String(bytes); 527 this.xmlWriter.writeCharacters(strValue); 528 529 } 530 531 /** 532 * Gets the internal XML writer. 533 * 534 * @return the internal writer 535 */ 536 public XMLStreamWriter getInternalWriter() { 537 return xmlWriter; 538 } 539 540 /** 541 * Gets the service. 542 * 543 * @return The service. 544 */ 545 public ExchangeServiceBase getService() { 546 return service; 547 } 548 549 /** 550 * Gets a value indicating whether the SOAP message need WSSecurity Utility namespace. 551 */ 552 public boolean isRequireWSSecurityUtilityNamespace() { 553 return requireWSSecurityUtilityNamespace; 554 } 555 556 /** 557 * Sets a value indicating whether the SOAP message need WSSecurity Utility namespace. 558 */ 559 public void setRequireWSSecurityUtilityNamespace(boolean requireWSSecurityUtilityNamespace) { 560 this.requireWSSecurityUtilityNamespace = requireWSSecurityUtilityNamespace; 561 } 562 563 /** 564 * Gets a value indicating whether the time zone SOAP header was emitted 565 * through this writer. 566 * 567 * @return true if the time zone SOAP header was emitted; otherwise false. 568 */ 569 public boolean isTimeZoneHeaderEmitted() { 570 return isTimeZoneHeaderEmitted; 571 } 572 573 /** 574 * Sets a value indicating whether the time zone SOAP header was emitted 575 * through this writer. 576 * 577 * @param isTimeZoneHeaderEmitted true if the time zone SOAP header was emitted; otherwise 578 * false. 579 */ 580 public void setTimeZoneHeaderEmitted(boolean isTimeZoneHeaderEmitted) { 581 this.isTimeZoneHeaderEmitted = isTimeZoneHeaderEmitted; 582 } 583 584 /** 585 * Write start document. 586 * 587 * @throws XMLStreamException the XML stream exception 588 */ 589 public void writeStartDocument() throws XMLStreamException { 590 this.xmlWriter.writeStartDocument("utf-8", "1.0"); 591 } 592}