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.misc;
025    
026    import microsoft.exchange.webservices.data.core.EwsServiceXmlReader;
027    import microsoft.exchange.webservices.data.core.EwsServiceXmlWriter;
028    import microsoft.exchange.webservices.data.core.EwsUtilities;
029    import microsoft.exchange.webservices.data.core.ExchangeService;
030    import microsoft.exchange.webservices.data.core.XmlAttributeNames;
031    import microsoft.exchange.webservices.data.core.XmlElementNames;
032    import microsoft.exchange.webservices.data.core.enumeration.misc.ExchangeVersion;
033    import microsoft.exchange.webservices.data.core.enumeration.misc.UserConfigurationProperties;
034    import microsoft.exchange.webservices.data.core.enumeration.property.WellKnownFolderName;
035    import microsoft.exchange.webservices.data.core.enumeration.misc.XmlNamespace;
036    import microsoft.exchange.webservices.data.core.exception.misc.InvalidOperationException;
037    import microsoft.exchange.webservices.data.core.exception.service.local.PropertyException;
038    import microsoft.exchange.webservices.data.core.exception.service.local.ServiceVersionException;
039    import microsoft.exchange.webservices.data.core.exception.service.local.ServiceXmlSerializationException;
040    import microsoft.exchange.webservices.data.property.complex.FolderId;
041    import microsoft.exchange.webservices.data.property.complex.ItemId;
042    import microsoft.exchange.webservices.data.property.complex.UserConfigurationDictionary;
043    import microsoft.exchange.webservices.data.security.XmlNodeType;
044    import org.apache.commons.codec.binary.Base64;
045    import org.apache.commons.logging.Log;
046    import org.apache.commons.logging.LogFactory;
047    
048    import javax.xml.stream.XMLStreamException;
049    
050    import java.util.EnumSet;
051    
052    /**
053     * Represents an object that can be used to store user-defined configuration
054     * settings.
055     */
056    public 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    }