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    
024    package microsoft.exchange.webservices.data.core;
025    
026    import microsoft.exchange.webservices.data.core.enumeration.misc.XmlNamespace;
027    import microsoft.exchange.webservices.data.core.exception.service.local.ServiceXmlSerializationException;
028    import microsoft.exchange.webservices.data.misc.OutParam;
029    import microsoft.exchange.webservices.data.property.complex.ISearchStringProvider;
030    import org.apache.commons.codec.binary.Base64;
031    import org.apache.commons.logging.Log;
032    import org.apache.commons.logging.LogFactory;
033    import org.w3c.dom.CDATASection;
034    import org.w3c.dom.Comment;
035    import org.w3c.dom.Document;
036    import org.w3c.dom.Element;
037    import org.w3c.dom.EntityReference;
038    import org.w3c.dom.NamedNodeMap;
039    import org.w3c.dom.Node;
040    import org.w3c.dom.NodeList;
041    import org.w3c.dom.ProcessingInstruction;
042    import org.w3c.dom.Text;
043    
044    import javax.xml.stream.XMLOutputFactory;
045    import javax.xml.stream.XMLStreamException;
046    import javax.xml.stream.XMLStreamWriter;
047    
048    import java.io.ByteArrayOutputStream;
049    import java.io.IOException;
050    import java.io.InputStream;
051    import java.io.OutputStream;
052    import java.util.Date;
053    
054    /**
055     * Stax based XML Writer implementation.
056     */
057    public 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    }