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}