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}