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.misc;
025
026import microsoft.exchange.webservices.data.core.EwsServiceXmlReader;
027import microsoft.exchange.webservices.data.core.EwsServiceXmlWriter;
028import microsoft.exchange.webservices.data.core.EwsUtilities;
029import microsoft.exchange.webservices.data.core.ExchangeService;
030import microsoft.exchange.webservices.data.core.XmlAttributeNames;
031import microsoft.exchange.webservices.data.core.XmlElementNames;
032import microsoft.exchange.webservices.data.core.enumeration.misc.ExchangeVersion;
033import microsoft.exchange.webservices.data.core.enumeration.misc.UserConfigurationProperties;
034import microsoft.exchange.webservices.data.core.enumeration.property.WellKnownFolderName;
035import microsoft.exchange.webservices.data.core.enumeration.misc.XmlNamespace;
036import microsoft.exchange.webservices.data.core.exception.misc.InvalidOperationException;
037import microsoft.exchange.webservices.data.core.exception.service.local.PropertyException;
038import microsoft.exchange.webservices.data.core.exception.service.local.ServiceVersionException;
039import microsoft.exchange.webservices.data.core.exception.service.local.ServiceXmlSerializationException;
040import microsoft.exchange.webservices.data.property.complex.FolderId;
041import microsoft.exchange.webservices.data.property.complex.ItemId;
042import microsoft.exchange.webservices.data.property.complex.UserConfigurationDictionary;
043import microsoft.exchange.webservices.data.security.XmlNodeType;
044import org.apache.commons.codec.binary.Base64;
045import org.apache.commons.logging.Log;
046import org.apache.commons.logging.LogFactory;
047
048import javax.xml.stream.XMLStreamException;
049
050import java.util.EnumSet;
051
052/**
053 * Represents an object that can be used to store user-defined configuration
054 * settings.
055 */
056public class UserConfiguration {
057
058  private static final Log LOG = LogFactory.getLog(UserConfiguration.class);
059
060  /**
061   * The object version.
062   */
063  private static ExchangeVersion ObjectVersion = ExchangeVersion.Exchange2010;
064
065  /**
066   * For consistency with ServiceObject behavior, access to ItemId is
067   * permitted for a new object.
068   */
069  /**
070   * The Constant PropertiesAvailableForNewObject.
071   */
072  private final static EnumSet<UserConfigurationProperties>
073      PropertiesAvailableForNewObject =
074      EnumSet.of(UserConfigurationProperties.BinaryData,
075          UserConfigurationProperties.Dictionary,
076          UserConfigurationProperties.XmlData);
077
078  /**
079   * The No property.
080   */
081  private final UserConfigurationProperties NoProperties =
082      UserConfigurationProperties.values()[0];
083
084  /**
085   * The service.
086   */
087  private ExchangeService service;
088
089  /**
090   * The name.
091   */
092  private String name;
093
094  /**
095   * The parent folder id.
096   */
097  private FolderId parentFolderId = null;
098
099  /**
100   * The item id.
101   */
102  private ItemId itemId = null;
103
104  /**
105   * The dictionary.
106   */
107  private UserConfigurationDictionary dictionary = null;
108
109  /**
110   * The xml data.
111   */
112  private byte[] xmlData = null;
113
114  /**
115   * The binary data.
116   */
117  private byte[] binaryData = null;
118
119  /**
120   * The property available for access.
121   */
122  private EnumSet<UserConfigurationProperties> propertiesAvailableForAccess;
123
124  /**
125   * The updated property.
126   */
127  private EnumSet<UserConfigurationProperties> updatedProperties;
128
129  /**
130   * Indicates whether changes trigger an update or create operation.
131   */
132  private boolean isNew = false;
133
134  /**
135   * Initializes a new instance of <see cref="UserConfiguration"/> class.
136   *
137   * @param service The service to which the user configuration is bound.
138   * @throws Exception the exception
139   */
140  public UserConfiguration(ExchangeService service) throws Exception {
141    this(service, PropertiesAvailableForNewObject);
142  }
143
144  /**
145   * Writes a byte array to Xml.
146   *
147   * @param writer         the writer
148   * @param byteArray      byte array to write
149   * @param xmlElementName name of the Xml element
150   * @throws XMLStreamException the XML stream exception
151   * @throws ServiceXmlSerializationException the service xml serialization exception
152   */
153  private static void writeByteArrayToXml(EwsServiceXmlWriter writer,
154      byte[] byteArray, String xmlElementName) throws XMLStreamException, ServiceXmlSerializationException {
155    EwsUtilities.ewsAssert(writer != null, "UserConfiguration.WriteByteArrayToXml", "writer is null");
156    EwsUtilities.ewsAssert(xmlElementName != null, "UserConfiguration.WriteByteArrayToXml",
157                           "xmlElementName is null");
158
159    writer.writeStartElement(XmlNamespace.Types, xmlElementName);
160
161    if (byteArray != null && byteArray.length > 0) {
162      writer.writeValue(Base64.encodeBase64String(byteArray), xmlElementName);
163    }
164
165    writer.writeEndElement();
166  }
167
168
169  /**
170   * Writes to Xml.
171   *
172   * @param writer         The writer.
173   * @param xmlNamespace   The XML namespace.
174   * @param name           The user configuration name.
175   * @param parentFolderId The Id of the folder containing the user configuration.
176   * @throws Exception the exception
177   */
178  public static void writeUserConfigurationNameToXml(EwsServiceXmlWriter writer, XmlNamespace xmlNamespace,
179      String name, FolderId parentFolderId) throws Exception {
180    EwsUtilities.ewsAssert(writer != null, "UserConfiguration.WriteUserConfigurationNameToXml",
181                           "writer is null");
182    EwsUtilities.ewsAssert(name != null, "UserConfiguration.WriteUserConfigurationNameToXml", "name is null");
183    EwsUtilities.ewsAssert(parentFolderId != null, "UserConfiguration.WriteUserConfigurationNameToXml",
184                           "parentFolderId is null");
185
186    writer.writeStartElement(xmlNamespace,
187        XmlElementNames.UserConfigurationName);
188
189    writer.writeAttributeValue(XmlAttributeNames.Name, name);
190
191    parentFolderId.writeToXml(writer);
192
193    writer.writeEndElement();
194  }
195
196  /**
197   * Initializes a new instance of <see cref="UserConfiguration"/> class.
198   *
199   * @param service             The service to which the user configuration is bound.
200   * @param requestedProperties The property requested for this user configuration.
201   * @throws Exception the exception
202   */
203  public UserConfiguration(ExchangeService service, EnumSet<UserConfigurationProperties> requestedProperties)
204      throws Exception {
205    EwsUtilities.validateParam(service, "service");
206
207    if (service.getRequestedServerVersion().ordinal() < UserConfiguration.ObjectVersion.ordinal()) {
208      throw new ServiceVersionException(String.format(
209          "The object type %s is only valid for Exchange Server version %s or later versions.", this
210              .getClass().getName(), UserConfiguration.ObjectVersion));
211    }
212
213    this.service = service;
214    this.isNew = true;
215
216    this.initializeProperties(requestedProperties);
217  }
218
219  /**
220   * Gets the name of the user configuration.
221   *
222   * @return the name
223   */
224  public String getName() {
225    return this.name;
226  }
227
228  /**
229   * Sets the name.
230   *
231   * @param value the new name
232   */
233  public void setName(String value) {
234    this.name = value;
235  }
236
237  /**
238   * Gets the Id of the folder containing the user configuration.
239   *
240   * @return the parent folder id
241   */
242  public FolderId getParentFolderId() {
243    return this.parentFolderId;
244  }
245
246  /**
247   * Sets the parent folder id.
248   *
249   * @param value the new parent folder id
250   */
251  public void setParentFolderId(FolderId value) {
252    this.parentFolderId = value;
253  }
254
255  /**
256   * Gets the Id of the user configuration.
257   *
258   * @return the item id
259   */
260  public ItemId getItemId() {
261    return this.itemId;
262  }
263
264  /**
265   * Gets the dictionary of the user configuration.
266   *
267   * @return the dictionary
268   */
269  public UserConfigurationDictionary getDictionary() {
270    return this.dictionary;
271  }
272
273  /**
274   * Gets the xml data of the user configuration.
275   *
276   * @return the xml data
277   * @throws PropertyException the property exception
278   */
279  public byte[] getXmlData() throws PropertyException {
280
281    this.validatePropertyAccess(UserConfigurationProperties.XmlData);
282
283    return this.xmlData;
284  }
285
286  /**
287   * Sets the xml data.
288   *
289   * @param value the new xml data
290   */
291  public void setXmlData(byte[] value) {
292    this.xmlData = value;
293
294    this.markPropertyForUpdate(UserConfigurationProperties.XmlData);
295  }
296
297  /**
298   * Gets the binary data of the user configuration.
299   *
300   * @return the binary data
301   * @throws PropertyException the property exception
302   */
303  public byte[] getBinaryData() throws PropertyException {
304    this.validatePropertyAccess(UserConfigurationProperties.BinaryData);
305
306    return this.binaryData;
307
308  }
309
310  /**
311   * Sets the binary data.
312   *
313   * @param value the new binary data
314   */
315  public void setBinaryData(byte[] value) {
316    this.binaryData = value;
317    this.markPropertyForUpdate(UserConfigurationProperties.BinaryData);
318  }
319
320  /**
321   * Gets a value indicating whether this user configuration has been
322   * modified.
323   *
324   * @return the checks if is dirty
325   */
326  public boolean getIsDirty() {
327    return (!this.updatedProperties.contains(NoProperties))
328        || this.dictionary.getIsDirty();
329  }
330
331  /**
332   * Binds to an existing user configuration and loads the specified
333   * property. Calling this method results in a call to EWS.
334   *
335   * @param service        The service to which the user configuration is bound.
336   * @param name           The name of the user configuration.
337   * @param parentFolderId The Id of the folder containing the user configuration.
338   * @param properties     The property to load.
339   * @return A user configuration instance.
340   * @throws IndexOutOfBoundsException the index out of bounds exception
341   * @throws Exception                 the exception
342   */
343  public static UserConfiguration bind(ExchangeService service, String name,
344      FolderId parentFolderId, UserConfigurationProperties properties)
345      throws IndexOutOfBoundsException, Exception {
346
347    UserConfiguration result = service.getUserConfiguration(name,
348        parentFolderId, properties);
349    result.isNew = false;
350    return result;
351  }
352
353  /**
354   * Binds to an existing user configuration and loads the specified
355   * property.
356   *
357   * @param service          The service to which the user configuration is bound.
358   * @param name             The name of the user configuration.
359   * @param parentFolderName The name of the folder containing the user configuration.
360   * @param properties       The property to load.
361   * @return A user configuration instance.
362   * @throws IndexOutOfBoundsException the index out of bounds exception
363   * @throws Exception                 the exception
364   */
365  public static UserConfiguration bind(ExchangeService service, String name,
366      WellKnownFolderName parentFolderName,
367      UserConfigurationProperties properties)
368      throws IndexOutOfBoundsException, Exception {
369    return UserConfiguration.bind(service, name, new FolderId(
370        parentFolderName), properties);
371  }
372
373  /**
374   * Saves the user configuration. Calling this method results in a call to
375   * EWS.
376   *
377   * @param name           The name of the user configuration.
378   * @param parentFolderId The Id of the folder in which to save the user configuration.
379   * @throws Exception the exception
380   */
381  public void save(String name, FolderId parentFolderId) throws Exception {
382    EwsUtilities.validateParam(name, "name");
383    EwsUtilities.validateParam(parentFolderId, "parentFolderId");
384
385    parentFolderId.validate(this.service.getRequestedServerVersion());
386
387    if (!this.isNew) {
388      throw new InvalidOperationException(
389          "Calling Save isn't allowed because this user configuration isn't new. To apply local changes to this user configuration, call Update instead.");
390    }
391
392    this.parentFolderId = parentFolderId;
393    this.name = name;
394
395    this.service.createUserConfiguration(this);
396
397    this.isNew = false;
398
399    this.resetIsDirty();
400  }
401
402  /**
403   * Saves the user configuration. Calling this method results in a call to
404   * EWS.
405   *
406   * @param name             The name of the user configuration.
407   * @param parentFolderName The name of the folder in which to save the user
408   *                         configuration.
409   * @throws Exception the exception
410   */
411  public void save(String name, WellKnownFolderName parentFolderName)
412      throws Exception {
413    this.save(name, new FolderId(parentFolderName));
414  }
415
416  /**
417   * Updates the user configuration by applying local changes to the Exchange
418   * server. Calling this method results in a call to EWS
419   *
420   * @throws Exception the exception
421   */
422
423  public void update() throws Exception {
424    if (this.isNew) {
425      throw new InvalidOperationException(
426          "This user configuration can't be updated because it's never been saved.");
427    }
428
429    if (this.isPropertyUpdated(UserConfigurationProperties.BinaryData)
430        || this
431        .isPropertyUpdated(UserConfigurationProperties.
432            Dictionary)
433        || this.isPropertyUpdated(UserConfigurationProperties.
434        XmlData)) {
435
436      this.service.updateUserConfiguration(this);
437    }
438
439    this.resetIsDirty();
440  }
441
442  /**
443   * Deletes the user configuration. Calling this method results in a call to
444   * EWS.
445   *
446   * @throws Exception the exception
447   */
448  public void delete() throws Exception {
449    if (this.isNew) {
450      throw new InvalidOperationException(
451          "This user configuration object can't be deleted because it's never been saved.");
452    } else {
453      this.service
454          .deleteUserConfiguration(this.name, this.parentFolderId);
455    }
456  }
457
458  /**
459   * Loads the specified property on the user configuration. Calling this
460   * method results in a call to EWS.
461   *
462   * @param properties The property to load.
463   * @throws Exception the exception
464   */
465  public void load(UserConfigurationProperties properties) throws Exception {
466    this.initializeProperties(EnumSet.of(properties));
467    this.service.loadPropertiesForUserConfiguration(this, properties);
468  }
469
470  /**
471   * Writes to XML.
472   *
473   * @param writer         The writer.
474   * @param xmlNamespace   The XML namespace.
475   * @param xmlElementName Name of the XML element.
476   * @throws Exception the exception
477   */
478  public void writeToXml(EwsServiceXmlWriter writer, XmlNamespace xmlNamespace, String xmlElementName) throws Exception {
479    EwsUtilities.ewsAssert(writer != null, "UserConfiguration.WriteToXml", "writer is null");
480    EwsUtilities.ewsAssert(xmlElementName != null, "UserConfiguration.WriteToXml", "xmlElementName is null");
481
482    writer.writeStartElement(xmlNamespace, xmlElementName);
483
484    // Write the UserConfigurationName element
485    writeUserConfigurationNameToXml(writer, XmlNamespace.Types, this.name,
486        this.parentFolderId);
487
488    // Write the Dictionary element
489    if (this.isPropertyUpdated(UserConfigurationProperties.Dictionary)) {
490      this.dictionary.writeToXml(writer, XmlElementNames.Dictionary);
491    }
492
493    // Write the XmlData element
494    if (this.isPropertyUpdated(UserConfigurationProperties.XmlData)) {
495      this.writeXmlDataToXml(writer);
496    }
497
498    // Write the BinaryData element
499    if (this.isPropertyUpdated(UserConfigurationProperties.BinaryData)) {
500      this.writeBinaryDataToXml(writer);
501    }
502
503    writer.writeEndElement();
504  }
505
506  /**
507   * Determines whether the specified property was updated.
508   *
509   * @param property property to evaluate.
510   * @return Boolean indicating whether to send the property Xml.
511   */
512  private boolean isPropertyUpdated(UserConfigurationProperties property) {
513    boolean isPropertyDirty = false;
514    boolean isPropertyEmpty = false;
515
516    switch (property) {
517      case Dictionary:
518        isPropertyDirty = this.getDictionary().getIsDirty();
519        isPropertyEmpty = this.getDictionary().getCount() == 0;
520        break;
521      case XmlData:
522        isPropertyDirty = this.updatedProperties.contains(property);
523        isPropertyEmpty = (this.xmlData == null) ||
524            (this.xmlData.length == 0);
525        break;
526      case BinaryData:
527        isPropertyDirty = this.updatedProperties.contains(property);
528        isPropertyEmpty = (this.binaryData == null) ||
529            (this.binaryData.length == 0);
530        break;
531      default:
532        EwsUtilities.ewsAssert(false, "UserConfiguration.IsPropertyUpdated",
533                               "property not supported: " + property.toString());
534        break;
535    }
536
537    // Consider the property updated, if it's been modified, and either
538    // . there's a value or
539    // . there's no value but the operation is update.
540    return isPropertyDirty && ((!isPropertyEmpty) || (!this.isNew));
541  }
542
543  /**
544   * Writes the XmlData property to Xml.
545   *
546   * @param writer the writer
547   * @throws XMLStreamException the XML stream exception
548   * @throws ServiceXmlSerializationException the service xml serialization exception
549   */
550  private void writeXmlDataToXml(EwsServiceXmlWriter writer)
551      throws XMLStreamException, ServiceXmlSerializationException {
552    EwsUtilities.ewsAssert(writer != null, "UserConfiguration.WriteXmlDataToXml", "writer is null");
553
554    writeByteArrayToXml(writer, this.xmlData, XmlElementNames.XmlData);
555  }
556
557  /**
558   * Writes the BinaryData property to Xml.
559   *
560   * @param writer the writer
561   * @throws XMLStreamException the XML stream exception
562   * @throws ServiceXmlSerializationException the service xml serialization exception
563   */
564  private void writeBinaryDataToXml(EwsServiceXmlWriter writer)
565      throws XMLStreamException, ServiceXmlSerializationException {
566    EwsUtilities.ewsAssert(writer != null, "UserConfiguration.WriteBinaryDataToXml", "writer is null");
567
568    writeByteArrayToXml(writer, this.binaryData,
569        XmlElementNames.BinaryData);
570  }
571
572
573
574  /**
575   * Loads from XML.
576   *
577   * @param reader The reader.
578   * @throws Exception the exception
579   */
580  public void loadFromXml(EwsServiceXmlReader reader) throws Exception {
581    EwsUtilities.ewsAssert(reader != null, "UserConfiguration.loadFromXml", "reader is null");
582
583    reader.readStartElement(XmlNamespace.Messages,
584        XmlElementNames.UserConfiguration);
585    reader.read(); // Position at first property element
586
587    do {
588      if (reader.getNodeType().getNodeType() == XmlNodeType.START_ELEMENT) {
589        if (reader.getLocalName().equals(
590            XmlElementNames.UserConfigurationName)) {
591          String responseName = reader
592              .readAttributeValue(XmlAttributeNames.Name);
593
594          EwsUtilities.ewsAssert(this.name.equals(responseName), "UserConfiguration.loadFromXml",
595                                 "UserConfigurationName does not match: Expected: " + this.name
596                                 + " Name in response: " + responseName);
597
598          reader.skipCurrentElement();
599        } else if (reader.getLocalName().equals(XmlElementNames.ItemId)) {
600          this.itemId = new ItemId();
601          this.itemId.loadFromXml(reader, XmlElementNames.ItemId);
602        } else if (reader.getLocalName().equals(
603            XmlElementNames.Dictionary)) {
604          this.dictionary.loadFromXml(reader,
605              XmlElementNames.Dictionary);
606        } else if (reader.getLocalName()
607            .equals(XmlElementNames.XmlData)) {
608          this.xmlData = Base64.decodeBase64(reader.readElementValue());
609        } else if (reader.getLocalName().equals(
610            XmlElementNames.BinaryData)) {
611          this.binaryData = Base64.decodeBase64(reader.readElementValue());
612        } else {
613          EwsUtilities.ewsAssert(false, "UserConfiguration.loadFromXml",
614                                 "Xml element not supported: " + reader.getLocalName());
615        }
616      }
617
618      // If XmlData was loaded, read is skipped because GetXmlData
619      // positions the reader at the next property.
620      reader.read();
621    } while (!reader.isEndElement(XmlNamespace.Messages,
622        XmlElementNames.UserConfiguration));
623  }
624
625  /**
626   * Initializes property.
627   *
628   * @param requestedProperties The property requested for this UserConfiguration.
629   */
630  // / InitializeProperties is called in 3 cases:
631  // / . Create new object: From the UserConfiguration constructor.
632  // / . Bind to existing object: Again from the constructor. The constructor
633  // is called eventually by the GetUserConfiguration request.
634  // / . Refresh property: From the Load method.
635  private void initializeProperties(
636      EnumSet<UserConfigurationProperties> requestedProperties) {
637    this.itemId = null;
638    this.dictionary = new UserConfigurationDictionary();
639    this.xmlData = null;
640    this.binaryData = null;
641    this.propertiesAvailableForAccess = requestedProperties;
642
643    this.resetIsDirty();
644  }
645
646  /**
647   * Resets flags to indicate that property haven't been modified.
648   */
649  private void resetIsDirty() {
650    try {
651      this.updatedProperties = EnumSet.of(NoProperties);
652    } catch (Exception e) {
653      LOG.error(e);
654    }
655    this.dictionary.setIsDirty(false);
656  }
657
658  /**
659   * Determines whether the specified property may be accessed.
660   *
661   * @param property Property to access.
662   * @throws PropertyException the property exception
663   */
664  private void validatePropertyAccess(UserConfigurationProperties property)
665      throws PropertyException {
666    if (!this.propertiesAvailableForAccess.contains(property)) {
667      throw new PropertyException("You must load or assign this property before you can read its value.", property
668          .toString());
669    }
670  }
671
672  /**
673   * Adds the passed property to updatedProperties.
674   *
675   * @param property Property to update.
676   */
677  private void markPropertyForUpdate(UserConfigurationProperties property) {
678    this.updatedProperties.add(property);
679    this.propertiesAvailableForAccess.add(property);
680
681  }
682
683}