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.property.complex;
025
026 import microsoft.exchange.webservices.data.attribute.EditorBrowsable;
027 import microsoft.exchange.webservices.data.core.EwsUtilities;
028 import microsoft.exchange.webservices.data.core.XmlElementNames;
029 import microsoft.exchange.webservices.data.core.response.CreateAttachmentResponse;
030 import microsoft.exchange.webservices.data.core.response.DeleteAttachmentResponse;
031 import microsoft.exchange.webservices.data.core.response.ServiceResponseCollection;
032 import microsoft.exchange.webservices.data.core.service.ServiceObject;
033 import microsoft.exchange.webservices.data.core.service.item.Item;
034 import microsoft.exchange.webservices.data.core.enumeration.attribute.EditorBrowsableState;
035 import microsoft.exchange.webservices.data.core.enumeration.misc.ExchangeVersion;
036 import microsoft.exchange.webservices.data.core.enumeration.service.ServiceResult;
037 import microsoft.exchange.webservices.data.core.exception.service.remote.CreateAttachmentException;
038 import microsoft.exchange.webservices.data.core.exception.service.remote.DeleteAttachmentException;
039 import microsoft.exchange.webservices.data.core.exception.misc.InvalidOperationException;
040 import microsoft.exchange.webservices.data.core.exception.service.local.ServiceLocalException;
041 import microsoft.exchange.webservices.data.core.exception.service.local.ServiceValidationException;
042
043 import java.io.File;
044 import java.io.InputStream;
045 import java.util.ArrayList;
046 import java.util.Collection;
047 import java.util.Enumeration;
048
049 /**
050 * Represents an item's attachment collection.
051 */
052 @EditorBrowsable(state = EditorBrowsableState.Never)
053 public 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 }