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 }