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 a file attachment to the collection.
158   *
159   * @param name    the name
160   * @param content accepts byte byte arrays representing the content of the
161   *                attachment.
162   * @return FileAttachment
163   */
164  public Attachment addAttachment(Attachment attachment) {
165      this.internalAdd(attachment);
166      return attachment;
167  }
168
169  /**
170   * Adds an item attachment to the collection.
171   *
172   * @param <TItem> the generic type
173   * @param cls     the cls
174   * @return An ItemAttachment instance.
175   * @throws Exception the exception
176   */
177  public <TItem extends Item> GenericItemAttachment<TItem> addItemAttachment(
178      Class<TItem> cls) throws Exception {
179    if (cls.getDeclaredFields().length == 0) {
180      throw new InvalidOperationException(String.format(
181          "Items of type %s are not supported as attachments.", cls
182              .getName()));
183    }
184
185    GenericItemAttachment<TItem> itemAttachment =
186        new GenericItemAttachment<TItem>(
187            this.owner);
188    itemAttachment.setTItem((TItem) EwsUtilities.createItemFromItemClass(
189        itemAttachment, cls, true));
190
191    this.internalAdd(itemAttachment);
192
193    return itemAttachment;
194  }
195
196  /**
197   * Removes all attachments from this collection.
198   */
199  public void clear() {
200    this.internalClear();
201  }
202
203  /**
204   * Removes the attachment at the specified index.
205   *
206   * @param index Index of the attachment to remove.
207   */
208  public void removeAt(int index) {
209    if (index < 0 || index >= this.getCount()) {
210      throw new IllegalArgumentException("parameter \'index\' : " + "index is out of range.");
211    }
212
213    this.internalRemoveAt(index);
214  }
215
216  /**
217   * Removes the specified attachment.
218   *
219   * @param attachment The attachment to remove.
220   * @return True if the attachment was successfully removed from the
221   * collection, false otherwise.
222   * @throws Exception the exception
223   */
224  public boolean remove(Attachment attachment) throws Exception {
225    EwsUtilities.validateParam(attachment, "attachment");
226
227    return this.internalRemove(attachment);
228  }
229
230  /**
231   * Instantiate the appropriate attachment type depending on the current XML
232   * element name.
233   *
234   * @param xmlElementName The XML element name from which to determine the type of
235   *                       attachment to create.
236   * @return An Attachment instance.
237   */
238  @Override
239  protected Attachment createComplexProperty(String xmlElementName) {
240    if (xmlElementName.equals(XmlElementNames.FileAttachment)) {
241      return new FileAttachment(this.owner);
242    } else if (xmlElementName.equals(XmlElementNames.ItemAttachment)) {
243      return new ItemAttachment(this.owner);
244    } else {
245      return null;
246    }
247  }
248
249  /**
250   * Determines the name of the XML element associated with the
251   * complexProperty parameter.
252   *
253   * @param complexProperty The attachment object for which to determine the XML element
254   *                        name with.
255   * @return The XML element name associated with the complexProperty
256   * parameter.
257   */
258  @Override
259  protected String getCollectionItemXmlElementName(Attachment
260      complexProperty) {
261    if (complexProperty instanceof FileAttachment) {
262      return XmlElementNames.FileAttachment;
263    } else {
264      return XmlElementNames.ItemAttachment;
265    }
266  }
267
268  /**
269   * Saves this collection by creating new attachment and deleting removed
270   * ones.
271   *
272   * @throws Exception the exception
273   */
274  public void save() throws Exception {
275    ArrayList<Attachment> attachments =
276        new ArrayList<Attachment>();
277
278    for (Attachment attachment : this.getRemovedItems()) {
279      if (!attachment.isNew()) {
280        attachments.add(attachment);
281      }
282    }
283
284    // If any, delete them by calling the DeleteAttachment web method.
285    if (attachments.size() > 0) {
286      this.internalDeleteAttachments(attachments);
287    }
288
289    attachments.clear();
290
291    // Retrieve a list of attachments that have to be created.
292    for (Attachment attachment : this) {
293      if (attachment.isNew()) {
294        attachments.add(attachment);
295      }
296    }
297
298    // If there are any, create them by calling the CreateAttachment web
299    // method.
300    if (attachments.size() > 0) {
301      if (this.owner.isAttachment()) {
302        this.internalCreateAttachments(this.owner.getParentAttachment()
303            .getId(), attachments);
304      } else {
305        this.internalCreateAttachments(
306            this.owner.getId().getUniqueId(), attachments);
307      }
308    }
309
310
311    // Process all of the item attachments in this collection.
312    for (Attachment attachment : this) {
313      ItemAttachment itemAttachment = (ItemAttachment)
314          ((attachment instanceof
315              ItemAttachment) ? attachment :
316              null);
317      if (itemAttachment != null) {
318        // Bug E14:80864: Make sure item was created/loaded before
319        // trying to create/delete sub-attachments
320        if (itemAttachment.getItem() != null) {
321          // Create/delete any sub-attachments
322          itemAttachment.getItem().getAttachments().save();
323
324          // Clear the item's change log
325          itemAttachment.getItem().clearChangeLog();
326        }
327      }
328    }
329
330    super.clearChangeLog();
331  }
332
333  /**
334   * Determines whether there are any unsaved attachment collection changes.
335   *
336   * @return True if attachment adds or deletes haven't been processed yet.
337   * @throws ServiceLocalException
338   */
339  public boolean hasUnprocessedChanges() throws ServiceLocalException {
340    // Any new attachments?
341    for (Attachment attachment : this) {
342      if (attachment.isNew()) {
343        return true;
344      }
345    }
346
347    // Any pending deletions?
348    for (Attachment attachment : this.getRemovedItems()) {
349      if (!attachment.isNew()) {
350        return true;
351      }
352    }
353
354
355    Collection<ItemAttachment> itemAttachments =
356        new ArrayList<ItemAttachment>();
357    for (Object event : this.getItems()) {
358      if (event instanceof ItemAttachment) {
359        itemAttachments.add((ItemAttachment) event);
360      }
361    }
362
363    // Recurse: process item attachments to check
364    // for new or deleted sub-attachments.
365    for (ItemAttachment itemAttachment : itemAttachments) {
366      if (itemAttachment.getItem() != null) {
367        if (itemAttachment.getItem().getAttachments().hasUnprocessedChanges()) {
368          return true;
369        }
370      }
371    }
372
373    return false;
374  }
375
376  /**
377   * Disables the change log clearing mechanism. Attachment collections are
378   * saved separately from the item they belong to.
379   */
380  @Override public void clearChangeLog() {
381    // Do nothing
382  }
383
384  /**
385   * Validates this instance.
386   *
387   * @throws Exception the exception
388   */
389  public void validate() throws Exception {
390    // Validate all added attachments
391    if (this.owner.isNew()
392        && this.owner.getService().getRequestedServerVersion()
393        .ordinal() >= ExchangeVersion.Exchange2010_SP2
394        .ordinal()) {
395      boolean contactPhotoFound = false;
396      for (int attachmentIndex = 0; attachmentIndex < this.getAddedItems()
397          .size(); attachmentIndex++) {
398        final Attachment attachment = this.getAddedItems().get(attachmentIndex);
399        if (attachment != null) {
400          if (attachment.isNew() && attachment instanceof FileAttachment) {
401            // At the server side, only the last attachment with
402            // IsContactPhoto is kept, all other IsContactPhoto
403            // attachments are removed. CreateAttachment will generate
404            // AttachmentId for each of such attachments (although
405            // only the last one is valid).
406            //
407            // With E14 SP2 CreateItemWithAttachment, such request will only
408            // return 1 AttachmentId; but the client
409            // expects to see all, so let us prevent such "invalid" request
410            // in the first place.
411            //
412            // The IsNew check is to still let CreateAttachmentRequest allow
413            // multiple IsContactPhoto attachments.
414            //
415            if (((FileAttachment) attachment).isContactPhoto()) {
416              if (contactPhotoFound) {
417                throw new ServiceValidationException("Multiple contact photos in attachment.");
418              }
419              contactPhotoFound = true;
420            }
421          }
422          attachment.validate(attachmentIndex);
423        }
424      }
425    }
426  }
427
428
429  /**
430   * Calls the DeleteAttachment web method to delete a list of attachments.
431   *
432   * @param attachments the attachments
433   * @throws Exception the exception
434   */
435  private void internalDeleteAttachments(Iterable<Attachment> attachments)
436      throws Exception {
437    ServiceResponseCollection<DeleteAttachmentResponse> responses =
438        this.owner
439            .getService().deleteAttachments(attachments);
440    Enumeration<DeleteAttachmentResponse> enumerator = responses
441        .getEnumerator();
442    while (enumerator.hasMoreElements()) {
443      DeleteAttachmentResponse response = enumerator.nextElement();
444      // We remove all attachments that were successfully deleted from the
445      // change log. We should never
446      // receive a warning from EWS, so we ignore them.
447      if (response.getResult() != ServiceResult.Error) {
448        this.removeFromChangeLog(response.getAttachment());
449      }
450    }
451
452    // TODO : Should we throw for warnings as well?
453    if (responses.getOverallResult() == ServiceResult.Error) {
454      throw new DeleteAttachmentException(responses, "At least one attachment couldn't be deleted.");
455    }
456  }
457
458  /**
459   * Calls the CreateAttachment web method to create a list of attachments.
460   *
461   * @param parentItemId the parent item id
462   * @param attachments  the attachments
463   * @throws Exception the exception
464   */
465  private void internalCreateAttachments(String parentItemId,
466      Iterable<Attachment> attachments) throws Exception {
467    ServiceResponseCollection<CreateAttachmentResponse> responses =
468        this.owner
469            .getService().createAttachments(parentItemId, attachments);
470
471    Enumeration<CreateAttachmentResponse> enumerator = responses
472        .getEnumerator();
473    while (enumerator.hasMoreElements()) {
474      CreateAttachmentResponse response = enumerator.nextElement();
475      // We remove all attachments that were successfully created from the
476      // change log. We should never
477      // receive a warning from EWS, so we ignore them.
478      if (response.getResult() != ServiceResult.Error) {
479        this.removeFromChangeLog(response.getAttachment());
480      }
481    }
482
483    // TODO : Should we throw for warnings as well?
484    if (responses.getOverallResult() == ServiceResult.Error) {
485      throw new CreateAttachmentException(responses, "At least one attachment couldn't be created.");
486    }
487  }
488
489}