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
020 package org.apache.james.mime4j.message;
021
022 import java.util.Collections;
023 import java.util.Date;
024 import java.util.HashMap;
025 import java.util.Map;
026
027 import org.apache.james.mime4j.dom.Body;
028 import org.apache.james.mime4j.dom.Disposable;
029 import org.apache.james.mime4j.dom.Entity;
030 import org.apache.james.mime4j.dom.Header;
031 import org.apache.james.mime4j.dom.Message;
032 import org.apache.james.mime4j.dom.Multipart;
033 import org.apache.james.mime4j.dom.TextBody;
034 import org.apache.james.mime4j.dom.field.ContentDispositionField;
035 import org.apache.james.mime4j.dom.field.ContentTransferEncodingField;
036 import org.apache.james.mime4j.dom.field.ContentTypeField;
037 import org.apache.james.mime4j.dom.field.FieldName;
038 import org.apache.james.mime4j.dom.field.ParsedField;
039
040 /**
041 * Abstract MIME entity.
042 */
043 public 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 }