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}