001/**************************************************************** 002 * Licensed to the Apache Software Foundation (ASF) under one * 003 * or more contributor license agreements. See the NOTICE file * 004 * distributed with this work for additional information * 005 * regarding copyright ownership. The ASF licenses this file * 006 * to you under the Apache License, Version 2.0 (the * 007 * "License"); you may not use this file except in compliance * 008 * with the License. You may obtain a copy of the License at * 009 * * 010 * http://www.apache.org/licenses/LICENSE-2.0 * 011 * * 012 * Unless required by applicable law or agreed to in writing, * 013 * software distributed under the License is distributed on an * 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * 015 * KIND, either express or implied. See the License for the * 016 * specific language governing permissions and limitations * 017 * under the License. * 018 ****************************************************************/ 019 020package org.apache.james.mime4j.message; 021 022import java.util.Collections; 023import java.util.Date; 024import java.util.HashMap; 025import java.util.Map; 026 027import org.apache.james.mime4j.dom.Body; 028import org.apache.james.mime4j.dom.Disposable; 029import org.apache.james.mime4j.dom.Entity; 030import org.apache.james.mime4j.dom.Header; 031import org.apache.james.mime4j.dom.Message; 032import org.apache.james.mime4j.dom.Multipart; 033import org.apache.james.mime4j.dom.TextBody; 034import org.apache.james.mime4j.dom.field.ContentDispositionField; 035import org.apache.james.mime4j.dom.field.ContentTransferEncodingField; 036import org.apache.james.mime4j.dom.field.ContentTypeField; 037import org.apache.james.mime4j.dom.field.FieldName; 038import org.apache.james.mime4j.dom.field.ParsedField; 039 040/** 041 * Abstract MIME entity. 042 */ 043public abstract class AbstractEntity implements Entity { 044 private Header header = null; 045 private Body body = null; 046 private Entity parent = null; 047 048 /** 049 * Creates a new <code>Entity</code>. Typically invoked implicitly by a 050 * subclass constructor. 051 */ 052 protected AbstractEntity() { 053 } 054 055 /** 056 * Gets the parent entity of this entity. 057 * Returns <code>null</code> if this is the root entity. 058 * 059 * @return the parent or <code>null</code>. 060 */ 061 public Entity getParent() { 062 return parent; 063 } 064 065 /** 066 * Sets the parent entity of this entity. 067 * 068 * @param parent the parent entity or <code>null</code> if 069 * this will be the root entity. 070 */ 071 public void setParent(Entity parent) { 072 this.parent = parent; 073 } 074 075 /** 076 * Gets the entity header. 077 * 078 * @return the header. 079 */ 080 public Header getHeader() { 081 return header; 082 } 083 084 /** 085 * Sets the entity header. 086 * 087 * @param header the header. 088 */ 089 public void setHeader(Header header) { 090 this.header = header; 091 } 092 093 /** 094 * Gets the body of this entity. 095 * 096 * @return the body, 097 */ 098 public Body getBody() { 099 return body; 100 } 101 102 /** 103 * Sets the body of this entity. 104 * 105 * @param body the body. 106 * @throws IllegalStateException if the body has already been set. 107 */ 108 public void setBody(Body body) { 109 if (this.body != null) 110 throw new IllegalStateException("body already set"); 111 112 this.body = body; 113 body.setParent(this); 114 } 115 116 /** 117 * Removes and returns the body of this entity. The removed body may be 118 * attached to another entity. If it is no longer needed it should be 119 * {@link Disposable#dispose() disposed} of. 120 * 121 * @return the removed body or <code>null</code> if no body was set. 122 */ 123 public Body removeBody() { 124 if (body == null) 125 return null; 126 127 Body body = this.body; 128 this.body = null; 129 body.setParent(null); 130 131 return body; 132 } 133 134 /** 135 * Sets the specified message as body of this entity and the content type to 136 * "message/rfc822". A <code>Header</code> is created if this 137 * entity does not already have one. 138 * 139 * @param message 140 * the message to set as body. 141 */ 142 public void setMessage(Message message) { 143 setBody(message, "message/rfc822", null); 144 } 145 146 /** 147 * Sets the specified multipart as body of this entity. Also sets the 148 * content type accordingly and creates a message boundary string. A 149 * <code>Header</code> is created if this entity does not already have 150 * one. 151 * 152 * @param multipart 153 * the multipart to set as body. 154 */ 155 public void setMultipart(Multipart multipart) { 156 String mimeType = "multipart/" + multipart.getSubType(); 157 Map<String, String> parameters = Collections.singletonMap("boundary", 158 newUniqueBoundary()); 159 160 setBody(multipart, mimeType, parameters); 161 } 162 163 /** 164 * Sets the specified multipart as body of this entity. Also sets the 165 * content type accordingly and creates a message boundary string. A 166 * <code>Header</code> is created if this entity does not already have 167 * one. 168 * 169 * @param multipart 170 * the multipart to set as body. 171 * @param parameters 172 * additional parameters for the Content-Type header field. 173 */ 174 public void setMultipart(Multipart multipart, Map<String, String> parameters) { 175 String mimeType = "multipart/" + multipart.getSubType(); 176 if (!parameters.containsKey("boundary")) { 177 parameters = new HashMap<String, String>(parameters); 178 parameters.put("boundary", newUniqueBoundary()); 179 } 180 181 setBody(multipart, mimeType, parameters); 182 } 183 184 /** 185 * Sets the specified <code>TextBody</code> as body of this entity and the 186 * content type to "text/plain". A <code>Header</code> is 187 * created if this entity does not already have one. 188 * 189 * @param textBody 190 * the <code>TextBody</code> to set as body. 191 * @see org.apache.james.mime4j.message.BodyFactory#textBody(java.io.InputStream, String) 192 */ 193 public void setText(TextBody textBody) { 194 setText(textBody, "plain"); 195 } 196 197 /** 198 * Sets the specified <code>TextBody</code> as body of this entity. Also 199 * sets the content type according to the specified sub-type. A 200 * <code>Header</code> is created if this entity does not already have 201 * one. 202 * 203 * @param textBody 204 * the <code>TextBody</code> to set as body. 205 * @param subtype 206 * the text subtype (e.g. "plain", "html" or 207 * "xml"). 208 */ 209 public void setText(TextBody textBody, String subtype) { 210 String mimeType = "text/" + subtype; 211 212 Map<String, String> parameters = null; 213 String mimeCharset = textBody.getMimeCharset(); 214 if (mimeCharset != null && !mimeCharset.equalsIgnoreCase("us-ascii")) { 215 parameters = Collections.singletonMap("charset", mimeCharset); 216 } 217 218 setBody(textBody, mimeType, parameters); 219 } 220 221 /** 222 * Sets the body of this entity and sets the content-type to the specified 223 * value. A <code>Header</code> is created if this entity does not already 224 * have one. 225 * 226 * @param body 227 * the body. 228 * @param mimeType 229 * the MIME media type of the specified body 230 * ("type/subtype"). 231 */ 232 public void setBody(Body body, String mimeType) { 233 setBody(body, mimeType, null); 234 } 235 236 /** 237 * Sets the body of this entity and sets the content-type to the specified 238 * value. A <code>Header</code> is created if this entity does not already 239 * have one. 240 * 241 * @param body 242 * the body. 243 * @param mimeType 244 * the MIME media type of the specified body 245 * ("type/subtype"). 246 * @param parameters 247 * additional parameters for the Content-Type header field. 248 */ 249 public void setBody(Body body, String mimeType, 250 Map<String, String> parameters) { 251 setBody(body); 252 253 Header header = obtainHeader(); 254 header.setField(newContentType(mimeType, parameters)); 255 } 256 257 /** 258 * Determines the MIME type of this <code>Entity</code>. The MIME type 259 * is derived by looking at the parent's Content-Type field if no 260 * Content-Type field is set for this <code>Entity</code>. 261 * 262 * @return the MIME type. 263 */ 264 public String getMimeType() { 265 ContentTypeField child = 266 getContentTypeField(); 267 ContentTypeField parent = getParent() != null 268 ? (ContentTypeField) getParent().getHeader(). 269 getField(FieldName.CONTENT_TYPE) 270 : null; 271 272 return calcMimeType(child, parent); 273 } 274 275 private ContentTypeField getContentTypeField() { 276 return (ContentTypeField) getHeader().getField(FieldName.CONTENT_TYPE); 277 } 278 279 /** 280 * Determines the MIME character set encoding of this <code>Entity</code>. 281 * 282 * @return the MIME character set encoding. 283 */ 284 public String getCharset() { 285 return calcCharset((ContentTypeField) getHeader().getField(FieldName.CONTENT_TYPE)); 286 } 287 288 /** 289 * Determines the transfer encoding of this <code>Entity</code>. 290 * 291 * @return the transfer encoding. 292 */ 293 public String getContentTransferEncoding() { 294 ContentTransferEncodingField f = (ContentTransferEncodingField) 295 getHeader().getField(FieldName.CONTENT_TRANSFER_ENCODING); 296 297 return calcTransferEncoding(f); 298 } 299 300 /** 301 * Sets the transfer encoding of this <code>Entity</code> to the specified 302 * value. 303 * 304 * @param contentTransferEncoding 305 * transfer encoding to use. 306 */ 307 public void setContentTransferEncoding(String contentTransferEncoding) { 308 Header header = obtainHeader(); 309 header.setField(newContentTransferEncoding(contentTransferEncoding)); 310 } 311 312 /** 313 * Return the disposition type of the content disposition of this 314 * <code>Entity</code>. 315 * 316 * @return the disposition type or <code>null</code> if no disposition 317 * type has been set. 318 */ 319 public String getDispositionType() { 320 ContentDispositionField field = obtainField(FieldName.CONTENT_DISPOSITION); 321 if (field == null) 322 return null; 323 324 return field.getDispositionType(); 325 } 326 327 /** 328 * Sets the content disposition of this <code>Entity</code> to the 329 * specified disposition type. No filename, size or date parameters 330 * are included in the content disposition. 331 * 332 * @param dispositionType 333 * disposition type value (usually <code>inline</code> or 334 * <code>attachment</code>). 335 */ 336 public void setContentDisposition(String dispositionType) { 337 Header header = obtainHeader(); 338 header.setField(newContentDisposition(dispositionType, null, -1, null, 339 null, null)); 340 } 341 342 /** 343 * Sets the content disposition of this <code>Entity</code> to the 344 * specified disposition type and filename. No size or date parameters are 345 * included in the content disposition. 346 * 347 * @param dispositionType 348 * disposition type value (usually <code>inline</code> or 349 * <code>attachment</code>). 350 * @param filename 351 * filename parameter value or <code>null</code> if the 352 * parameter should not be included. 353 */ 354 public void setContentDisposition(String dispositionType, String filename) { 355 Header header = obtainHeader(); 356 header.setField(newContentDisposition(dispositionType, filename, -1, 357 null, null, null)); 358 } 359 360 /** 361 * Sets the content disposition of this <code>Entity</code> to the 362 * specified values. No date parameters are included in the content 363 * disposition. 364 * 365 * @param dispositionType 366 * disposition type value (usually <code>inline</code> or 367 * <code>attachment</code>). 368 * @param filename 369 * filename parameter value or <code>null</code> if the 370 * parameter should not be included. 371 * @param size 372 * size parameter value or <code>-1</code> if the parameter 373 * should not be included. 374 */ 375 public void setContentDisposition(String dispositionType, String filename, 376 long size) { 377 Header header = obtainHeader(); 378 header.setField(newContentDisposition(dispositionType, filename, size, 379 null, null, null)); 380 } 381 382 /** 383 * Sets the content disposition of this <code>Entity</code> to the 384 * specified values. 385 * 386 * @param dispositionType 387 * disposition type value (usually <code>inline</code> or 388 * <code>attachment</code>). 389 * @param filename 390 * filename parameter value or <code>null</code> if the 391 * parameter should not be included. 392 * @param size 393 * size parameter value or <code>-1</code> if the parameter 394 * should not be included. 395 * @param creationDate 396 * creation-date parameter value or <code>null</code> if the 397 * parameter should not be included. 398 * @param modificationDate 399 * modification-date parameter value or <code>null</code> if 400 * the parameter should not be included. 401 * @param readDate 402 * read-date parameter value or <code>null</code> if the 403 * parameter should not be included. 404 */ 405 public void setContentDisposition(String dispositionType, String filename, 406 long size, Date creationDate, Date modificationDate, Date readDate) { 407 Header header = obtainHeader(); 408 header.setField(newContentDisposition(dispositionType, filename, size, 409 creationDate, modificationDate, readDate)); 410 } 411 412 /** 413 * Returns the filename parameter of the content disposition of this 414 * <code>Entity</code>. 415 * 416 * @return the filename parameter of the content disposition or 417 * <code>null</code> if the filename has not been set. 418 */ 419 public String getFilename() { 420 ContentDispositionField field = obtainField(FieldName.CONTENT_DISPOSITION); 421 if (field == null) 422 return null; 423 424 return field.getFilename(); 425 } 426 427 /** 428 * Sets the filename parameter of the content disposition of this 429 * <code>Entity</code> to the specified value. If this entity does not 430 * have a content disposition header field a new one with disposition type 431 * <code>attachment</code> is created. 432 * 433 * @param filename 434 * filename parameter value or <code>null</code> if the 435 * parameter should be removed. 436 */ 437 public void setFilename(String filename) { 438 Header header = obtainHeader(); 439 ContentDispositionField field = (ContentDispositionField) header 440 .getField(FieldName.CONTENT_DISPOSITION); 441 if (field == null) { 442 if (filename != null) { 443 header.setField(newContentDisposition( 444 ContentDispositionField.DISPOSITION_TYPE_ATTACHMENT, 445 filename, -1, null, null, null)); 446 } 447 } else { 448 String dispositionType = field.getDispositionType(); 449 Map<String, String> parameters = new HashMap<String, String>(field 450 .getParameters()); 451 if (filename == null) { 452 parameters.remove(ContentDispositionField.PARAM_FILENAME); 453 } else { 454 parameters 455 .put(ContentDispositionField.PARAM_FILENAME, filename); 456 } 457 header.setField(newContentDisposition(dispositionType, parameters)); 458 } 459 } 460 461 /** 462 * Determines if the MIME type of this <code>Entity</code> matches the 463 * given one. MIME types are case-insensitive. 464 * 465 * @param type the MIME type to match against. 466 * @return <code>true</code> on match, <code>false</code> otherwise. 467 */ 468 public boolean isMimeType(String type) { 469 return getMimeType().equalsIgnoreCase(type); 470 } 471 472 /** 473 * Determines if the MIME type of this <code>Entity</code> is 474 * <code>multipart/*</code>. Since multipart-entities must have 475 * a boundary parameter in the <code>Content-Type</code> field this 476 * method returns <code>false</code> if no boundary exists. 477 * 478 * @return <code>true</code> on match, <code>false</code> otherwise. 479 */ 480 public boolean isMultipart() { 481 ContentTypeField f = getContentTypeField(); 482 return f != null 483 && f.getBoundary() != null 484 && getMimeType().startsWith( 485 ContentTypeField.TYPE_MULTIPART_PREFIX); 486 } 487 488 /** 489 * Disposes of the body of this entity. Note that the dispose call does not 490 * get forwarded to the parent entity of this Entity. 491 * 492 * Subclasses that need to free resources should override this method and 493 * invoke super.dispose(). 494 * 495 * @see org.apache.james.mime4j.dom.Disposable#dispose() 496 */ 497 public void dispose() { 498 if (body != null) { 499 body.dispose(); 500 } 501 } 502 503 /** 504 * Obtains the header of this entity. Creates and sets a new header if this 505 * entity's header is currently <code>null</code>. 506 * 507 * @return the header of this entity; never <code>null</code>. 508 */ 509 Header obtainHeader() { 510 if (header == null) { 511 header = new HeaderImpl(); 512 } 513 return header; 514 } 515 516 /** 517 * Obtains the header field with the specified name. 518 * 519 * @param <F> 520 * concrete field type. 521 * @param fieldName 522 * name of the field to retrieve. 523 * @return the header field or <code>null</code> if this entity has no 524 * header or the header contains no such field. 525 */ 526 <F extends ParsedField> F obtainField(String fieldName) { 527 Header header = getHeader(); 528 if (header == null) 529 return null; 530 531 @SuppressWarnings("unchecked") 532 F field = (F) header.getField(fieldName); 533 return field; 534 } 535 536 protected abstract String newUniqueBoundary(); 537 538 protected abstract ContentDispositionField newContentDisposition( 539 String dispositionType, String filename, long size, 540 Date creationDate, Date modificationDate, Date readDate); 541 542 protected abstract ContentDispositionField newContentDisposition( 543 String dispositionType, Map<String, String> parameters); 544 545 protected abstract ContentTypeField newContentType(String mimeType, 546 Map<String, String> parameters); 547 548 protected abstract ContentTransferEncodingField newContentTransferEncoding( 549 String contentTransferEncoding); 550 551 protected abstract String calcMimeType(ContentTypeField child, ContentTypeField parent); 552 553 protected abstract String calcTransferEncoding(ContentTransferEncodingField f); 554 555 protected abstract String calcCharset(ContentTypeField contentType); 556}