001/*
002 * The MIT License
003 * Copyright (c) 2012 Microsoft Corporation
004 *
005 * Permission is hereby granted, free of charge, to any person obtaining a copy
006 * of this software and associated documentation files (the "Software"), to deal
007 * in the Software without restriction, including without limitation the rights
008 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
009 * copies of the Software, and to permit persons to whom the Software is
010 * furnished to do so, subject to the following conditions:
011 *
012 * The above copyright notice and this permission notice shall be included in
013 * all copies or substantial portions of the Software.
014 *
015 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
016 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
017 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
018 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
019 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
020 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
021 * THE SOFTWARE.
022 */
023
024package microsoft.exchange.webservices.data.core;
025
026import microsoft.exchange.webservices.data.ISelfValidate;
027import microsoft.exchange.webservices.data.core.request.ServiceRequestBase;
028import microsoft.exchange.webservices.data.core.enumeration.property.BasePropertySet;
029import microsoft.exchange.webservices.data.core.enumeration.property.BodyType;
030import microsoft.exchange.webservices.data.core.enumeration.misc.ExchangeVersion;
031import microsoft.exchange.webservices.data.core.enumeration.property.PropertyDefinitionFlags;
032import microsoft.exchange.webservices.data.core.enumeration.service.ServiceObjectType;
033import microsoft.exchange.webservices.data.core.enumeration.misc.XmlNamespace;
034import microsoft.exchange.webservices.data.core.exception.service.local.ServiceValidationException;
035import microsoft.exchange.webservices.data.core.exception.service.local.ServiceVersionException;
036import microsoft.exchange.webservices.data.core.exception.service.local.ServiceXmlSerializationException;
037import microsoft.exchange.webservices.data.property.definition.PropertyDefinition;
038import microsoft.exchange.webservices.data.property.definition.PropertyDefinitionBase;
039
040import javax.xml.stream.XMLStreamException;
041
042import java.util.ArrayList;
043import java.util.Arrays;
044import java.util.HashMap;
045import java.util.Iterator;
046import java.util.List;
047import java.util.Map;
048
049/**
050 * Represents a set of item or folder property. Property sets are used to
051 * indicate what property of an item or folder should be loaded when binding
052 * to an existing item or folder or when loading an item or folder's property.
053 */
054public final class PropertySet implements ISelfValidate,
055    Iterable<PropertyDefinitionBase> {
056
057  /**
058   * The Constant IdOnly.
059   */
060  public static final PropertySet IdOnly = PropertySet.
061      createReadonlyPropertySet(BasePropertySet.IdOnly);
062
063  /**
064   * Returns a predefined property set that only includes the Id property.
065   *
066   * @return Returns a predefined property set that only includes the Id
067   * property.
068   */
069  public static PropertySet getIdOnly() {
070    return IdOnly;
071  }
072
073  /**
074   * The Constant FirstClassProperties.
075   */
076  public static final PropertySet FirstClassProperties = PropertySet.
077      createReadonlyPropertySet(BasePropertySet.FirstClassProperties);
078
079  /**
080   * Returns a predefined property set that includes the first class
081   * property of an item or folder.
082   *
083   * @return A predefined property set that includes the first class
084   * property of an item or folder.
085   */
086  public static PropertySet getFirstClassProperties() {
087    return FirstClassProperties;
088  }
089
090  /**
091   * Maps BasePropertySet values to EWS's BaseShape values.
092   */
093  private static LazyMember<Map<BasePropertySet, String>> defaultPropertySetMap =
094      new LazyMember<Map<BasePropertySet, String>>(new
095                                                       ILazyMember<Map<BasePropertySet, String>>() {
096                                                         @Override
097                                                         public Map<BasePropertySet, String> createInstance() {
098                                                           Map<BasePropertySet, String> result = new
099                                                               HashMap<BasePropertySet, String>();
100                                                           result.put(BasePropertySet.IdOnly,
101                                                               BasePropertySet.IdOnly
102                                                                   .getBaseShapeValue());
103                                                           result.put(BasePropertySet.FirstClassProperties,
104                                                               BasePropertySet.FirstClassProperties
105                                                                   .getBaseShapeValue());
106                                                           return result;
107                                                         }
108                                                       });
109  /**
110   * The base property set this property set is based upon.
111   */
112  private BasePropertySet basePropertySet;
113
114  /**
115   * The list of additional property included in this property set.
116   */
117  private List<PropertyDefinitionBase> additionalProperties = new
118      ArrayList<PropertyDefinitionBase>();
119
120  /**
121   * The requested body type for get and find operations. If null, the
122   * "best body" is returned.
123   */
124  private BodyType requestedBodyType;
125
126  /**
127   * Value indicating whether or not the server should filter HTML content.
128   */
129  private Boolean filterHtml;
130
131  /**
132   * Value indicating whether or not the server
133   * should convert HTML code page to UTF8.
134   */
135  private Boolean convertHtmlCodePageToUTF8;
136
137  /**
138   * Value indicating whether or not this PropertySet can be modified.
139   */
140  private boolean isReadOnly;
141
142  /**
143   * Initializes a new instance of PropertySet.
144   *
145   * @param basePropertySet      The base property set to base the property set upon.
146   * @param additionalProperties Additional property to include in the property set. Property
147   *                             definitions are available as static members from schema
148   *                             classes (for example, EmailMessageSchema.Subject,
149   *                             AppointmentSchema.Start, ContactSchema.GivenName, etc.)
150   */
151  public PropertySet(BasePropertySet basePropertySet,
152      PropertyDefinitionBase... additionalProperties) {
153    this.basePropertySet = basePropertySet;
154    if (null != additionalProperties) {
155        this.additionalProperties.addAll(Arrays.asList(additionalProperties));
156    }
157  }
158
159  /**
160   * Initializes a new instance of PropertySet.
161   *
162   * @param basePropertySet      The base property set to base the property set upon.
163   * @param additionalProperties Additional property to include in the property set. Property
164   *                             definitions are available as static members from schema
165   *                             classes (for example, EmailMessageSchema.Subject,
166   *                             AppointmentSchema.Start, ContactSchema.GivenName, etc.)
167   */
168  public PropertySet(BasePropertySet basePropertySet,
169      Iterator<PropertyDefinitionBase> additionalProperties) {
170    this.basePropertySet = basePropertySet;
171    if (null != additionalProperties) {
172      while (additionalProperties.hasNext()) {
173        this.additionalProperties.add(additionalProperties.next());
174      }
175    }
176  }
177
178  /**
179   * Initializes a new instance of PropertySet based upon
180   * BasePropertySet.IdOnly.
181   */
182  public PropertySet() {
183    this.basePropertySet = BasePropertySet.IdOnly;
184  }
185
186  /**
187   * Initializes a new instance of PropertySet.
188   *
189   * @param basePropertySet The base property set to base the property set upon.
190   */
191  public PropertySet(BasePropertySet basePropertySet) {
192    this.basePropertySet = basePropertySet;
193  }
194
195  /**
196   * Initializes a new instance of PropertySet based upon
197   * BasePropertySet.IdOnly.
198   *
199   * @param additionalProperties Additional property to include in the property set. Property
200   *                             definitions are available as static members from schema
201   *                             classes (for example, EmailMessageSchema.Subject,
202   *                             AppointmentSchema.Start, ContactSchema.GivenName, etc.)
203   */
204  public PropertySet(PropertyDefinitionBase... additionalProperties) {
205    this(BasePropertySet.IdOnly, additionalProperties);
206  }
207
208  /**
209   * Initializes a new instance of PropertySet based upon
210   * BasePropertySet.IdOnly.
211   *
212   * @param additionalProperties Additional property to include in the property set. Property
213   *                             definitions are available as static members from schema
214   *                             classes (for example, EmailMessageSchema.Subject,
215   *                             AppointmentSchema.Start, ContactSchema.GivenName, etc.)
216   */
217  public PropertySet(Iterator<PropertyDefinitionBase> additionalProperties) {
218    this(BasePropertySet.IdOnly, additionalProperties);
219  }
220
221  /**
222   * Implements an implicit conversion between
223   * PropertySet and BasePropertySet.
224   *
225   * @param basePropertySet The BasePropertySet value to convert from.
226   * @return A PropertySet instance based on the specified base property set.
227   */
228  public static PropertySet getPropertySetFromBasePropertySet(BasePropertySet
229      basePropertySet) {
230    return new PropertySet(basePropertySet);
231  }
232
233
234  /**
235   * Adds the specified property to the property set.
236   *
237   * @param property The property to add.
238   * @throws Exception the exception
239   */
240  public void add(PropertyDefinitionBase property) throws Exception {
241    this.throwIfReadonly();
242    EwsUtilities.validateParam(property, "property");
243
244    if (!this.additionalProperties.contains(property)) {
245      this.additionalProperties.add(property);
246    }
247  }
248
249  /**
250   * Adds the specified property to the property set.
251   *
252   * @param properties The property to add.
253   * @throws Exception the exception
254   */
255  public void addRange(Iterable<PropertyDefinitionBase> properties)
256      throws Exception {
257    this.throwIfReadonly();
258    Iterator<PropertyDefinitionBase> property = properties.iterator();
259    EwsUtilities.validateParamCollection(property, "property");
260
261    for (Iterator<PropertyDefinitionBase> it = properties.iterator(); it
262        .hasNext(); ) {
263      this.add(it.next());
264    }
265  }
266
267  /**
268   * Remove all explicitly added property from the property set.
269   */
270  public void clear() {
271    this.throwIfReadonly();
272    this.additionalProperties.clear();
273  }
274
275  /**
276   * Creates a read-only PropertySet.
277   *
278   * @param basePropertySet The base property set.
279   * @return PropertySet
280   */
281  private static PropertySet createReadonlyPropertySet(
282      BasePropertySet basePropertySet) {
283    PropertySet propertySet = new PropertySet(basePropertySet);
284    propertySet.isReadOnly = true;
285    return propertySet;
286  }
287
288  /**
289   * Throws if readonly property set.
290   */
291  private void throwIfReadonly() {
292    if (this.isReadOnly) {
293      throw new UnsupportedOperationException("This PropertySet is read-only and can't be modified.");
294    }
295  }
296
297  /**
298   * Determines whether the specified property has been explicitly added to
299   * this property set using the Add or AddRange methods.
300   *
301   * @param property The property.
302   * @return true if this property set contains the specified property
303   * otherwise, false
304   */
305  public boolean contains(PropertyDefinitionBase property) {
306    return this.additionalProperties.contains(property);
307  }
308
309  /**
310   * Removes the specified property from the set.
311   *
312   * @param property The property to remove.
313   * @return true if the property was successfully removed, false otherwise.
314   */
315  public boolean remove(PropertyDefinitionBase property) {
316    this.throwIfReadonly();
317    return this.additionalProperties.remove(property);
318  }
319
320  /**
321   * Gets the base property set, the property set is based upon.
322   *
323   * @return the base property set
324   */
325  public BasePropertySet getBasePropertySet() {
326    return this.basePropertySet;
327  }
328
329  /**
330   * Maps BasePropertySet values to EWS's BaseShape values.
331   *
332   * @return the base property set
333   */
334  public static LazyMember<Map<BasePropertySet, String>> getDefaultPropertySetMap() {
335    return PropertySet.defaultPropertySetMap;
336
337  }
338
339  /**
340   * Sets the base property set, the property set is based upon.
341   *
342   * @param basePropertySet Base property set.
343   */
344  public void setBasePropertySet(BasePropertySet basePropertySet) {
345    this.throwIfReadonly();
346    this.basePropertySet = basePropertySet;
347  }
348
349  /**
350   * Gets type of body that should be loaded on item. If RequestedBodyType
351   * is null, body is returned as HTML if available, plain text otherwise.
352   *
353   * @return the requested body type
354   */
355  public BodyType getRequestedBodyType() {
356    return this.requestedBodyType;
357  }
358
359  /**
360   * Sets type of body that should be loaded on item. If RequestedBodyType is
361   * null, body is returned as HTML if available, plain text otherwise.
362   *
363   * @param requestedBodyType Type of body that should be loaded on item.
364   */
365  public void setRequestedBodyType(BodyType requestedBodyType) {
366    this.throwIfReadonly();
367    this.requestedBodyType = requestedBodyType;
368  }
369
370  /**
371   * Gets the number of explicitly added property in this set.
372   *
373   * @return the count
374   */
375  public int getCount() {
376    return this.additionalProperties.size();
377  }
378
379  /**
380   * Gets value indicating whether or not to filter potentially unsafe HTML
381   * content from message bodies.
382   *
383   * @return the filter html content
384   */
385  public Boolean getFilterHtmlContent() {
386    return this.filterHtml;
387  }
388
389  /**
390   * Sets value indicating whether or not to filter potentially unsafe HTML
391   * content from message bodies.
392   *
393   * @param filterHtml true to filter otherwise false.
394   */
395  public void setFilterHtmlContent(Boolean filterHtml) {
396    this.throwIfReadonly();
397    this.filterHtml = filterHtml;
398  }
399
400
401
402  /**
403   * Gets value indicating whether or not to convert
404   * HTML code page to UTF8 encoding.
405   */
406  public Boolean getConvertHtmlCodePageToUTF8() {
407    return this.convertHtmlCodePageToUTF8;
408
409  }
410
411  /**
412   * Sets value indicating whether or not to
413   * convert HTML code page to UTF8 encoding.
414   */
415  public void setConvertHtmlCodePageToUTF8(Boolean value) {
416    this.throwIfReadonly();
417    this.convertHtmlCodePageToUTF8 = value;
418
419  }
420
421
422  /**
423   * Gets the PropertyDefinitionBase at the specified index.
424   *
425   * @param index Index.
426   * @return the property definition base at
427   */
428  public PropertyDefinitionBase getPropertyDefinitionBaseAt(int index) {
429    return this.additionalProperties.get(index);
430  }
431
432
433  /**
434   * Validate.
435   *
436   * @throws ServiceValidationException the service validation exception
437   */
438  @Override
439  public void validate() throws ServiceValidationException {
440    this.internalValidate();
441  }
442
443  /**
444   * Writes additional property to XML.
445   *
446   * @param writer              The writer to write to
447   * @param propertyDefinitions The property definitions to write
448   * @throws XMLStreamException the XML stream exception
449   * @throws ServiceXmlSerializationException the service xml serialization exception
450   */
451  public static void writeAdditionalPropertiesToXml(EwsServiceXmlWriter writer,
452      Iterator<PropertyDefinitionBase> propertyDefinitions)
453      throws XMLStreamException, ServiceXmlSerializationException {
454    writer.writeStartElement(XmlNamespace.Types,
455        XmlElementNames.AdditionalProperties);
456
457    while (propertyDefinitions.hasNext()) {
458      PropertyDefinitionBase propertyDefinition = propertyDefinitions
459          .next();
460      propertyDefinition.writeToXml(writer);
461    }
462
463    writer.writeEndElement();
464  }
465
466  /**
467   * Validates this property set.
468   *
469   * @throws ServiceValidationException the service validation exception
470   */
471  public void internalValidate() throws ServiceValidationException {
472    for (int i = 0; i < this.additionalProperties.size(); i++) {
473      if (this.additionalProperties.get(i) == null) {
474        throw new ServiceValidationException(String.format("The additional property at index %d is null.", i));
475      }
476    }
477  }
478
479  /**
480   * Validates this property set instance for request to ensure that: 1.
481   * Properties are valid for the request server version 2. If only summary
482   * property are legal for this request (e.g. FindItem) then only summary
483   * property were specified.
484   *
485   * @param request               The request.
486   * @param summaryPropertiesOnly if set to true then only summary property are allowed.
487   * @throws ServiceVersionException    the service version exception
488   * @throws ServiceValidationException the service validation exception
489   */
490  public void validateForRequest(ServiceRequestBase request, boolean summaryPropertiesOnly) throws ServiceVersionException,
491      ServiceValidationException {
492    for (PropertyDefinitionBase propDefBase : this.additionalProperties) {
493      if (propDefBase instanceof PropertyDefinition) {
494        PropertyDefinition propertyDefinition =
495            (PropertyDefinition) propDefBase;
496        if (propertyDefinition.getVersion().ordinal() > request
497            .getService().getRequestedServerVersion().ordinal()) {
498          throw new ServiceVersionException(String.format(
499              "The property %s is valid only for Exchange %s or later versions.",
500              propertyDefinition.getName(), propertyDefinition
501                  .getVersion()));
502        }
503
504        if (summaryPropertiesOnly &&
505            !propertyDefinition.hasFlag(
506                PropertyDefinitionFlags.CanFind, request.
507                    getService().getRequestedServerVersion())) {
508          throw new ServiceValidationException(String.format("The property %s can't be used in %s request.",
509              propertyDefinition.getName(), request
510                  .getXmlElementName()));
511        }
512      }
513    }
514    if (this.getFilterHtmlContent() != null) {
515      if (request.getService().getRequestedServerVersion().compareTo(ExchangeVersion.Exchange2010) < 0) {
516        throw new ServiceVersionException(
517            String.format("The property %s is valid only for Exchange %s or later versions.",
518                "FilterHtmlContent",
519                ExchangeVersion.Exchange2010));
520      }
521    }
522
523    if (this.getConvertHtmlCodePageToUTF8() != null) {
524      if (request.getService().getRequestedServerVersion().compareTo(ExchangeVersion.Exchange2010_SP1) < 0) {
525        throw new ServiceVersionException(
526            String.format("The property %s is valid only for Exchange %s or later versions.",
527                "ConvertHtmlCodePageToUTF8",
528                ExchangeVersion.Exchange2010_SP1));
529      }
530    }
531  }
532
533  /**
534   * Writes the property set to XML.
535   *
536   * @param writer            The writer to write to
537   * @param serviceObjectType The type of service object the property set is emitted for
538   * @throws XMLStreamException the XML stream exception
539   * @throws ServiceXmlSerializationException the service xml serialization exception
540   */
541  public void writeToXml(EwsServiceXmlWriter writer, ServiceObjectType serviceObjectType) throws XMLStreamException, ServiceXmlSerializationException {
542    writer
543        .writeStartElement(
544            XmlNamespace.Messages,
545            serviceObjectType == ServiceObjectType.Item ?
546                XmlElementNames.ItemShape
547                : XmlElementNames.FolderShape);
548
549    writer.writeElementValue(XmlNamespace.Types, XmlElementNames.BaseShape,
550        this.getBasePropertySet().getBaseShapeValue());
551
552    if (serviceObjectType == ServiceObjectType.Item) {
553      if (this.getRequestedBodyType() != null) {
554        writer.writeElementValue(XmlNamespace.Types,
555            XmlElementNames.BodyType, this.getRequestedBodyType());
556      }
557
558      if (this.getFilterHtmlContent() != null) {
559        writer.writeElementValue(XmlNamespace.Types,
560            XmlElementNames.FilterHtmlContent, this
561                .getFilterHtmlContent());
562      }
563      if ((this.getConvertHtmlCodePageToUTF8() != null) &&
564          writer.getService().getRequestedServerVersion().
565              compareTo(ExchangeVersion.Exchange2010_SP1) >= 0) {
566        writer.writeElementValue(
567            XmlNamespace.Types,
568            XmlElementNames.ConvertHtmlCodePageToUTF8,
569            this.getConvertHtmlCodePageToUTF8());
570      }
571    }
572
573    if (this.additionalProperties.size() > 0) {
574      writeAdditionalPropertiesToXml(writer, this.additionalProperties
575          .iterator());
576    }
577
578    writer.writeEndElement(); // Item/FolderShape
579  }
580
581  /*
582   * (non-Javadoc)
583   *
584   * @see java.lang.Iterable#iterator()
585   */
586  @Override
587  public Iterator<PropertyDefinitionBase> iterator() {
588    return this.additionalProperties.iterator();
589  }
590
591}