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.property.complex;
025
026import microsoft.exchange.webservices.data.attribute.EditorBrowsable;
027import microsoft.exchange.webservices.data.core.EwsUtilities;
028import microsoft.exchange.webservices.data.core.XmlElementNames;
029import microsoft.exchange.webservices.data.core.response.CreateAttachmentResponse;
030import microsoft.exchange.webservices.data.core.response.DeleteAttachmentResponse;
031import microsoft.exchange.webservices.data.core.response.ServiceResponseCollection;
032import microsoft.exchange.webservices.data.core.service.ServiceObject;
033import microsoft.exchange.webservices.data.core.service.item.Item;
034import microsoft.exchange.webservices.data.core.enumeration.attribute.EditorBrowsableState;
035import microsoft.exchange.webservices.data.core.enumeration.misc.ExchangeVersion;
036import microsoft.exchange.webservices.data.core.enumeration.service.ServiceResult;
037import microsoft.exchange.webservices.data.core.exception.service.remote.CreateAttachmentException;
038import microsoft.exchange.webservices.data.core.exception.service.remote.DeleteAttachmentException;
039import microsoft.exchange.webservices.data.core.exception.misc.InvalidOperationException;
040import microsoft.exchange.webservices.data.core.exception.service.local.ServiceLocalException;
041import microsoft.exchange.webservices.data.core.exception.service.local.ServiceValidationException;
042
043import java.io.File;
044import java.io.InputStream;
045import java.util.ArrayList;
046import java.util.Collection;
047import java.util.Enumeration;
048
049/**
050 * Represents an item's attachment collection.
051 */
052@EditorBrowsable(state = EditorBrowsableState.Never)
053public final class AttachmentCollection extends ComplexPropertyCollection<Attachment>
054    implements IOwnedProperty {
055
056  // The item owner that owns this attachment collection
057  /**
058   * The owner.
059   */
060  private Item owner;
061
062  /**
063   * Initializes a new instance of AttachmentCollection.
064   */
065  public AttachmentCollection() {
066    super();
067  }
068
069  /**
070   * The owner of this attachment collection.
071   *
072   * @return the owner
073   */
074  public ServiceObject getOwner() {
075    return this.owner;
076  }
077
078  /**
079   * The owner of this attachment collection.
080   *
081   * @param value accepts ServiceObject
082   */
083  public void setOwner(ServiceObject value) {
084    Item item = (Item) value;
085    EwsUtilities.ewsAssert(item != null, "AttachmentCollection.IOwnedProperty.set_Owner",
086                           "value is not a descendant of ItemBase");
087
088    this.owner = item;
089  }
090
091  /**
092   * Adds a file attachment to the collection.
093   *
094   * @param fileName the file name
095   * @return A FileAttachment instance.
096   */
097  public FileAttachment addFileAttachment(String fileName) {
098    return this.addFileAttachment(new File(fileName).getName(), fileName);
099  }
100
101  /**
102   * Adds a file attachment to the collection.
103   *
104   * @param name     accepts String display name of the new attachment.
105   * @param fileName accepts String name of the file representing the content of
106   *                 the attachment.
107   * @return A FileAttachment instance.
108   */
109  public FileAttachment addFileAttachment(String name, String fileName) {
110    FileAttachment fileAttachment = new FileAttachment(this.owner);
111    fileAttachment.setName(name);
112    fileAttachment.setFileName(fileName);
113
114    this.internalAdd(fileAttachment);
115
116    return fileAttachment;
117  }
118
119  /**
120   * Adds a file attachment to the collection.
121   *
122   * @param name          accepts String display name of the new attachment.
123   * @param contentStream accepts InputStream stream from which to read the content of
124   *                      the attachment.
125   * @return A FileAttachment instance.
126   */
127  public FileAttachment addFileAttachment(String name,
128      InputStream contentStream) {
129    FileAttachment fileAttachment = new FileAttachment(this.owner);
130    fileAttachment.setName(name);
131    fileAttachment.setContentStream(contentStream);
132
133    this.internalAdd(fileAttachment);
134
135    return fileAttachment;
136  }
137
138  /**
139   * Adds a file attachment to the collection.
140   *
141   * @param name    the name
142   * @param content accepts byte byte arrays representing the content of the
143   *                attachment.
144   * @return FileAttachment
145   */
146  public FileAttachment addFileAttachment(String name, byte[] content) {
147    FileAttachment fileAttachment = new FileAttachment(this.owner);
148    fileAttachment.setName(name);
149    fileAttachment.setContent(content);
150
151    this.internalAdd(fileAttachment);
152
153    return fileAttachment;
154  }
155
156  /**
157   * Adds an item attachment to the collection.
158   *
159   * @param <TItem> the generic type
160   * @param cls     the cls
161   * @return An ItemAttachment instance.
162   * @throws Exception the exception
163   */
164  public <TItem extends Item> GenericItemAttachment<TItem> addItemAttachment(
165      Class<TItem> cls) throws Exception {
166    if (cls.getDeclaredFields().length == 0) {
167      throw new InvalidOperationException(String.format(
168          "Items of type %s are not supported as attachments.", cls
169              .getName()));
170    }
171
172    GenericItemAttachment<TItem> itemAttachment =
173        new GenericItemAttachment<TItem>(
174            this.owner);
175    itemAttachment.setTItem((TItem) EwsUtilities.createItemFromItemClass(
176        itemAttachment, cls, true));
177
178    this.internalAdd(itemAttachment);
179
180    return itemAttachment;
181  }
182
183  /**
184   * Removes all attachments from this collection.
185   */
186  public void clear() {
187    this.internalClear();
188  }
189
190  /**
191   * Removes the attachment at the specified index.
192   *
193   * @param index Index of the attachment to remove.
194   */
195  public void removeAt(int index) {
196    if (index < 0 || index >= this.getCount()) {
197      throw new IllegalArgumentException("parameter \'index\' : " + "index is out of range.");
198    }
199
200    this.internalRemoveAt(index);
201  }
202
203  /**
204   * Removes the specified attachment.
205   *
206   * @param attachment The attachment to remove.
207   * @return True if the attachment was successfully removed from the
208   * collection, false otherwise.
209   * @throws Exception the exception
210   */
211  public boolean remove(Attachment attachment) throws Exception {
212    EwsUtilities.validateParam(attachment, "attachment");
213
214    return this.internalRemove(attachment);
215  }
216
217  /**
218   * Instantiate the appropriate attachment type depending on the current XML
219   * element name.
220   *
221   * @param xmlElementName The XML element name from which to determine the type of
222   *                       attachment to create.
223   * @return An Attachment instance.
224   */
225  @Override
226  protected Attachment createComplexProperty(String xmlElementName) {
227    if (xmlElementName.equals(XmlElementNames.FileAttachment)) {
228      return new FileAttachment(this.owner);
229    } else if (xmlElementName.equals(XmlElementNames.ItemAttachment)) {
230      return new ItemAttachment(this.owner);
231    } else {
232      return null;
233    }
234  }
235
236  /**
237   * Determines the name of the XML element associated with the
238   * complexProperty parameter.
239   *
240   * @param complexProperty The attachment object for which to determine the XML element
241   *                        name with.
242   * @return The XML element name associated with the complexProperty
243   * parameter.
244   */
245  @Override
246  protected String getCollectionItemXmlElementName(Attachment
247      complexProperty) {
248    if (complexProperty instanceof FileAttachment) {
249      return XmlElementNames.FileAttachment;
250    } else {
251      return XmlElementNames.ItemAttachment;
252    }
253  }
254
255  /**
256   * Saves this collection by creating new attachment and deleting removed
257   * ones.
258   *
259   * @throws Exception the exception
260   */
261  public void save() throws Exception {
262    ArrayList<Attachment> attachments =
263        new ArrayList<Attachment>();
264
265    for (Attachment attachment : this.getRemovedItems()) {
266      if (!attachment.isNew()) {
267        attachments.add(attachment);
268      }
269    }
270
271    // If any, delete them by calling the DeleteAttachment web method.
272    if (attachments.size() > 0) {
273      this.internalDeleteAttachments(attachments);
274    }
275
276    attachments.clear();
277
278    // Retrieve a list of attachments that have to be created.
279    for (Attachment attachment : this) {
280      if (attachment.isNew()) {
281        attachments.add(attachment);
282      }
283    }
284
285    // If there are any, create them by calling the CreateAttachment web
286    // method.
287    if (attachments.size() > 0) {
288      if (this.owner.isAttachment()) {
289        this.internalCreateAttachments(this.owner.getParentAttachment()
290            .getId(), attachments);
291      } else {
292        this.internalCreateAttachments(
293            this.owner.getId().getUniqueId(), attachments);
294      }
295    }
296
297
298    // Process all of the item attachments in this collection.
299    for (Attachment attachment : this) {
300      ItemAttachment itemAttachment = (ItemAttachment)
301          ((attachment instanceof
302              ItemAttachment) ? attachment :
303              null);
304      if (itemAttachment != null) {
305        // Bug E14:80864: Make sure item was created/loaded before
306        // trying to create/delete sub-attachments
307        if (itemAttachment.getItem() != null) {
308          // Create/delete any sub-attachments
309          itemAttachment.getItem().getAttachments().save();
310
311          // Clear the item's change log
312          itemAttachment.getItem().clearChangeLog();
313        }
314      }
315    }
316
317    super.clearChangeLog();
318  }
319
320  /**
321   * Determines whether there are any unsaved attachment collection changes.
322   *
323   * @return True if attachment adds or deletes haven't been processed yet.
324   * @throws ServiceLocalException
325   */
326  public boolean hasUnprocessedChanges() throws ServiceLocalException {
327    // Any new attachments?
328    for (Attachment attachment : this) {
329      if (attachment.isNew()) {
330        return true;
331      }
332    }
333
334    // Any pending deletions?
335    for (Attachment attachment : this.getRemovedItems()) {
336      if (!attachment.isNew()) {
337        return true;
338      }
339    }
340
341
342    Collection<ItemAttachment> itemAttachments =
343        new ArrayList<ItemAttachment>();
344    for (Object event : this.getItems()) {
345      if (event instanceof ItemAttachment) {
346        itemAttachments.add((ItemAttachment) event);
347      }
348    }
349
350    // Recurse: process item attachments to check
351    // for new or deleted sub-attachments.
352    for (ItemAttachment itemAttachment : itemAttachments) {
353      if (itemAttachment.getItem() != null) {
354        if (itemAttachment.getItem().getAttachments().hasUnprocessedChanges()) {
355          return true;
356        }
357      }
358    }
359
360    return false;
361  }
362
363  /**
364   * Disables the change log clearing mechanism. Attachment collections are
365   * saved separately from the item they belong to.
366   */
367  @Override public void clearChangeLog() {
368    // Do nothing
369  }
370
371  /**
372   * Validates this instance.
373   *
374   * @throws Exception the exception
375   */
376  public void validate() throws Exception {
377    // Validate all added attachments
378    if (this.owner.isNew()
379        && this.owner.getService().getRequestedServerVersion()
380        .ordinal() >= ExchangeVersion.Exchange2010_SP2
381        .ordinal()) {
382      boolean contactPhotoFound = false;
383      for (int attachmentIndex = 0; attachmentIndex < this.getAddedItems()
384          .size(); attachmentIndex++) {
385        final Attachment attachment = this.getAddedItems().get(attachmentIndex);
386        if (attachment != null) {
387          if (attachment.isNew() && attachment instanceof FileAttachment) {
388            // At the server side, only the last attachment with
389            // IsContactPhoto is kept, all other IsContactPhoto
390            // attachments are removed. CreateAttachment will generate
391            // AttachmentId for each of such attachments (although
392            // only the last one is valid).
393            //
394            // With E14 SP2 CreateItemWithAttachment, such request will only
395            // return 1 AttachmentId; but the client
396            // expects to see all, so let us prevent such "invalid" request
397            // in the first place.
398            //
399            // The IsNew check is to still let CreateAttachmentRequest allow
400            // multiple IsContactPhoto attachments.
401            //
402            if (((FileAttachment) attachment).isContactPhoto()) {
403              if (contactPhotoFound) {
404                throw new ServiceValidationException("Multiple contact photos in attachment.");
405              }
406              contactPhotoFound = true;
407            }
408          }
409          attachment.validate(attachmentIndex);
410        }
411      }
412    }
413  }
414
415
416  /**
417   * Calls the DeleteAttachment web method to delete a list of attachments.
418   *
419   * @param attachments the attachments
420   * @throws Exception the exception
421   */
422  private void internalDeleteAttachments(Iterable<Attachment> attachments)
423      throws Exception {
424    ServiceResponseCollection<DeleteAttachmentResponse> responses =
425        this.owner
426            .getService().deleteAttachments(attachments);
427    Enumeration<DeleteAttachmentResponse> enumerator = responses
428        .getEnumerator();
429    while (enumerator.hasMoreElements()) {
430      DeleteAttachmentResponse response = enumerator.nextElement();
431      // We remove all attachments that were successfully deleted from the
432      // change log. We should never
433      // receive a warning from EWS, so we ignore them.
434      if (response.getResult() != ServiceResult.Error) {
435        this.removeFromChangeLog(response.getAttachment());
436      }
437    }
438
439    // TODO : Should we throw for warnings as well?
440    if (responses.getOverallResult() == ServiceResult.Error) {
441      throw new DeleteAttachmentException(responses, "At least one attachment couldn't be deleted.");
442    }
443  }
444
445  /**
446   * Calls the CreateAttachment web method to create a list of attachments.
447   *
448   * @param parentItemId the parent item id
449   * @param attachments  the attachments
450   * @throws Exception the exception
451   */
452  private void internalCreateAttachments(String parentItemId,
453      Iterable<Attachment> attachments) throws Exception {
454    ServiceResponseCollection<CreateAttachmentResponse> responses =
455        this.owner
456            .getService().createAttachments(parentItemId, attachments);
457
458    Enumeration<CreateAttachmentResponse> enumerator = responses
459        .getEnumerator();
460    while (enumerator.hasMoreElements()) {
461      CreateAttachmentResponse response = enumerator.nextElement();
462      // We remove all attachments that were successfully created from the
463      // change log. We should never
464      // receive a warning from EWS, so we ignore them.
465      if (response.getResult() != ServiceResult.Error) {
466        this.removeFromChangeLog(response.getAttachment());
467      }
468    }
469
470    // TODO : Should we throw for warnings as well?
471    if (responses.getOverallResult() == ServiceResult.Error) {
472      throw new CreateAttachmentException(responses, "At least one attachment couldn't be created.");
473    }
474  }
475
476}