1 /****************************************************************
2 * Licensed to the Apache Software Foundation (ASF) under one *
3 * or more contributor license agreements. See the NOTICE file *
4 * distributed with this work for additional information *
5 * regarding copyright ownership. The ASF licenses this file *
6 * to you under the Apache License, Version 2.0 (the *
7 * "License"); you may not use this file except in compliance *
8 * with the License. You may obtain a copy of the License at *
9 * *
10 * http://www.apache.org/licenses/LICENSE-2.0 *
11 * *
12 * Unless required by applicable law or agreed to in writing, *
13 * software distributed under the License is distributed on an *
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
15 * KIND, either express or implied. See the License for the *
16 * specific language governing permissions and limitations *
17 * under the License. *
18 ****************************************************************/
19
20 package org.apache.james.mime4j.message;
21
22 import java.util.Collections;
23 import java.util.Date;
24 import java.util.HashMap;
25 import java.util.Map;
26
27 import org.apache.james.mime4j.field.ContentDispositionField;
28 import org.apache.james.mime4j.field.ContentTransferEncodingField;
29 import org.apache.james.mime4j.field.ContentTypeField;
30 import org.apache.james.mime4j.field.FieldName;
31 import org.apache.james.mime4j.field.Fields;
32 import org.apache.james.mime4j.parser.Field;
33 import org.apache.james.mime4j.util.MimeUtil;
34
35 /**
36 * MIME entity. An entity has a header and a body (see RFC 2045).
37 */
38 public abstract class Entity implements Disposable {
39 private Header header = null;
40 private Body body = null;
41 private Entity parent = null;
42
43 /**
44 * Creates a new <code>Entity</code>. Typically invoked implicitly by a
45 * subclass constructor.
46 */
47 protected Entity() {
48 }
49
50 /**
51 * Creates a new <code>Entity</code> from the specified
52 * <code>Entity</code>. The <code>Entity</code> instance is initialized
53 * with copies of header and body of the specified <code>Entity</code>.
54 * The parent entity of the new entity is <code>null</code>.
55 *
56 * @param other
57 * entity to copy.
58 * @throws UnsupportedOperationException
59 * if <code>other</code> contains a {@link SingleBody} that
60 * does not support the {@link SingleBody#copy() copy()}
61 * operation.
62 * @throws IllegalArgumentException
63 * if <code>other</code> contains a <code>Body</code> that
64 * is neither a {@link Message}, {@link Multipart} or
65 * {@link SingleBody}.
66 */
67 protected Entity(Entity other) {
68 if (other.header != null) {
69 header = new Header(other.header);
70 }
71
72 if (other.body != null) {
73 Body bodyCopy = BodyCopier.copy(other.body);
74 setBody(bodyCopy);
75 }
76 }
77
78 /**
79 * Gets the parent entity of this entity.
80 * Returns <code>null</code> if this is the root entity.
81 *
82 * @return the parent or <code>null</code>.
83 */
84 public Entity getParent() {
85 return parent;
86 }
87
88 /**
89 * Sets the parent entity of this entity.
90 *
91 * @param parent the parent entity or <code>null</code> if
92 * this will be the root entity.
93 */
94 public void setParent(Entity parent) {
95 this.parent = parent;
96 }
97
98 /**
99 * Gets the entity header.
100 *
101 * @return the header.
102 */
103 public Header getHeader() {
104 return header;
105 }
106
107 /**
108 * Sets the entity header.
109 *
110 * @param header the header.
111 */
112 public void setHeader(Header header) {
113 this.header = header;
114 }
115
116 /**
117 * Gets the body of this entity.
118 *
119 * @return the body,
120 */
121 public Body getBody() {
122 return body;
123 }
124
125 /**
126 * Sets the body of this entity.
127 *
128 * @param body the body.
129 * @throws IllegalStateException if the body has already been set.
130 */
131 public void setBody(Body body) {
132 if (this.body != null)
133 throw new IllegalStateException("body already set");
134
135 this.body = body;
136 body.setParent(this);
137 }
138
139 /**
140 * Removes and returns the body of this entity. The removed body may be
141 * attached to another entity. If it is no longer needed it should be
142 * {@link Disposable#dispose() disposed} of.
143 *
144 * @return the removed body or <code>null</code> if no body was set.
145 */
146 public Body removeBody() {
147 if (body == null)
148 return null;
149
150 Body body = this.body;
151 this.body = null;
152 body.setParent(null);
153
154 return body;
155 }
156
157 /**
158 * Sets the specified message as body of this entity and the content type to
159 * "message/rfc822". A <code>Header</code> is created if this
160 * entity does not already have one.
161 *
162 * @param message
163 * the message to set as body.
164 */
165 public void setMessage(Message message) {
166 setBody(message, "message/rfc822", null);
167 }
168
169 /**
170 * Sets the specified multipart as body of this entity. Also sets the
171 * content type accordingly and creates a message boundary string. A
172 * <code>Header</code> is created if this entity does not already have
173 * one.
174 *
175 * @param multipart
176 * the multipart to set as body.
177 */
178 public void setMultipart(Multipart multipart) {
179 String mimeType = "multipart/" + multipart.getSubType();
180 Map<String, String> parameters = Collections.singletonMap("boundary",
181 MimeUtil.createUniqueBoundary());
182
183 setBody(multipart, mimeType, parameters);
184 }
185
186 /**
187 * Sets the specified multipart as body of this entity. Also sets the
188 * content type accordingly and creates a message boundary string. A
189 * <code>Header</code> is created if this entity does not already have
190 * one.
191 *
192 * @param multipart
193 * the multipart to set as body.
194 * @param parameters
195 * additional parameters for the Content-Type header field.
196 */
197 public void setMultipart(Multipart multipart, Map<String, String> parameters) {
198 String mimeType = "multipart/" + multipart.getSubType();
199 if (!parameters.containsKey("boundary")) {
200 parameters = new HashMap<String, String>(parameters);
201 parameters.put("boundary", MimeUtil.createUniqueBoundary());
202 }
203
204 setBody(multipart, mimeType, parameters);
205 }
206
207 /**
208 * Sets the specified <code>TextBody</code> as body of this entity and the
209 * content type to "text/plain". A <code>Header</code> is
210 * created if this entity does not already have one.
211 *
212 * @param textBody
213 * the <code>TextBody</code> to set as body.
214 * @see BodyFactory#textBody(String)
215 */
216 public void setText(TextBody textBody) {
217 setText(textBody, "plain");
218 }
219
220 /**
221 * Sets the specified <code>TextBody</code> as body of this entity. Also
222 * sets the content type according to the specified sub-type. A
223 * <code>Header</code> is created if this entity does not already have
224 * one.
225 *
226 * @param textBody
227 * the <code>TextBody</code> to set as body.
228 * @param subtype
229 * the text subtype (e.g. "plain", "html" or
230 * "xml").
231 * @see BodyFactory#textBody(String)
232 */
233 public void setText(TextBody textBody, String subtype) {
234 String mimeType = "text/" + subtype;
235
236 Map<String, String> parameters = null;
237 String mimeCharset = textBody.getMimeCharset();
238 if (mimeCharset != null && !mimeCharset.equalsIgnoreCase("us-ascii")) {
239 parameters = Collections.singletonMap("charset", mimeCharset);
240 }
241
242 setBody(textBody, mimeType, parameters);
243 }
244
245 /**
246 * Sets the body of this entity and sets the content-type to the specified
247 * value. A <code>Header</code> is created if this entity does not already
248 * have one.
249 *
250 * @param body
251 * the body.
252 * @param mimeType
253 * the MIME media type of the specified body
254 * ("type/subtype").
255 */
256 public void setBody(Body body, String mimeType) {
257 setBody(body, mimeType, null);
258 }
259
260 /**
261 * Sets the body of this entity and sets the content-type to the specified
262 * value. A <code>Header</code> is created if this entity does not already
263 * have one.
264 *
265 * @param body
266 * the body.
267 * @param mimeType
268 * the MIME media type of the specified body
269 * ("type/subtype").
270 * @param parameters
271 * additional parameters for the Content-Type header field.
272 */
273 public void setBody(Body body, String mimeType,
274 Map<String, String> parameters) {
275 setBody(body);
276
277 Header header = obtainHeader();
278 header.setField(Fields.contentType(mimeType, parameters));
279 }
280
281 /**
282 * Determines the MIME type of this <code>Entity</code>. The MIME type
283 * is derived by looking at the parent's Content-Type field if no
284 * Content-Type field is set for this <code>Entity</code>.
285 *
286 * @return the MIME type.
287 */
288 public String getMimeType() {
289 ContentTypeField child =
290 (ContentTypeField) getHeader().getField(FieldName.CONTENT_TYPE);
291 ContentTypeField parent = getParent() != null
292 ? (ContentTypeField) getParent().getHeader().
293 getField(FieldName.CONTENT_TYPE)
294 : null;
295
296 return ContentTypeField.getMimeType(child, parent);
297 }
298
299 /**
300 * Determines the MIME character set encoding of this <code>Entity</code>.
301 *
302 * @return the MIME character set encoding.
303 */
304 public String getCharset() {
305 return ContentTypeField.getCharset(
306 (ContentTypeField) getHeader().getField(FieldName.CONTENT_TYPE));
307 }
308
309 /**
310 * Determines the transfer encoding of this <code>Entity</code>.
311 *
312 * @return the transfer encoding.
313 */
314 public String getContentTransferEncoding() {
315 ContentTransferEncodingField f = (ContentTransferEncodingField)
316 getHeader().getField(FieldName.CONTENT_TRANSFER_ENCODING);
317
318 return ContentTransferEncodingField.getEncoding(f);
319 }
320
321 /**
322 * Sets the transfer encoding of this <code>Entity</code> to the specified
323 * value.
324 *
325 * @param contentTransferEncoding
326 * transfer encoding to use.
327 */
328 public void setContentTransferEncoding(String contentTransferEncoding) {
329 Header header = obtainHeader();
330 header.setField(Fields.contentTransferEncoding(contentTransferEncoding));
331 }
332
333 /**
334 * Return the disposition type of the content disposition of this
335 * <code>Entity</code>.
336 *
337 * @return the disposition type or <code>null</code> if no disposition
338 * type has been set.
339 */
340 public String getDispositionType() {
341 ContentDispositionField field = obtainField(FieldName.CONTENT_DISPOSITION);
342 if (field == null)
343 return null;
344
345 return field.getDispositionType();
346 }
347
348 /**
349 * Sets the content disposition of this <code>Entity</code> to the
350 * specified disposition type. No filename, size or date parameters
351 * are included in the content disposition.
352 *
353 * @param dispositionType
354 * disposition type value (usually <code>inline</code> or
355 * <code>attachment</code>).
356 */
357 public void setContentDisposition(String dispositionType) {
358 Header header = obtainHeader();
359 header.setField(Fields.contentDisposition(dispositionType, null, -1,
360 null, null, null));
361 }
362
363 /**
364 * Sets the content disposition of this <code>Entity</code> to the
365 * specified disposition type and filename. No size or date parameters are
366 * included in the content disposition.
367 *
368 * @param dispositionType
369 * disposition type value (usually <code>inline</code> or
370 * <code>attachment</code>).
371 * @param filename
372 * filename parameter value or <code>null</code> if the
373 * parameter should not be included.
374 */
375 public void setContentDisposition(String dispositionType, String filename) {
376 Header header = obtainHeader();
377 header.setField(Fields.contentDisposition(dispositionType, filename,
378 -1, null, null, null));
379 }
380
381 /**
382 * Sets the content disposition of this <code>Entity</code> to the
383 * specified values. No date parameters are included in the content
384 * disposition.
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 */
396 public void setContentDisposition(String dispositionType, String filename,
397 long size) {
398 Header header = obtainHeader();
399 header.setField(Fields.contentDisposition(dispositionType, filename,
400 size, null, null, null));
401 }
402
403 /**
404 * Sets the content disposition of this <code>Entity</code> to the
405 * specified values.
406 *
407 * @param dispositionType
408 * disposition type value (usually <code>inline</code> or
409 * <code>attachment</code>).
410 * @param filename
411 * filename parameter value or <code>null</code> if the
412 * parameter should not be included.
413 * @param size
414 * size parameter value or <code>-1</code> if the parameter
415 * should not be included.
416 * @param creationDate
417 * creation-date parameter value or <code>null</code> if the
418 * parameter should not be included.
419 * @param modificationDate
420 * modification-date parameter value or <code>null</code> if
421 * the parameter should not be included.
422 * @param readDate
423 * read-date parameter value or <code>null</code> if the
424 * parameter should not be included.
425 */
426 public void setContentDisposition(String dispositionType, String filename,
427 long size, Date creationDate, Date modificationDate, Date readDate) {
428 Header header = obtainHeader();
429 header.setField(Fields.contentDisposition(dispositionType, filename,
430 size, creationDate, modificationDate, readDate));
431 }
432
433 /**
434 * Returns the filename parameter of the content disposition of this
435 * <code>Entity</code>.
436 *
437 * @return the filename parameter of the content disposition or
438 * <code>null</code> if the filename has not been set.
439 */
440 public String getFilename() {
441 ContentDispositionField field = obtainField(FieldName.CONTENT_DISPOSITION);
442 if (field == null)
443 return null;
444
445 return field.getFilename();
446 }
447
448 /**
449 * Sets the filename parameter of the content disposition of this
450 * <code>Entity</code> to the specified value. If this entity does not
451 * have a content disposition header field a new one with disposition type
452 * <code>attachment</code> is created.
453 *
454 * @param filename
455 * filename parameter value or <code>null</code> if the
456 * parameter should be removed.
457 */
458 public void setFilename(String filename) {
459 Header header = obtainHeader();
460 ContentDispositionField field = (ContentDispositionField) header
461 .getField(FieldName.CONTENT_DISPOSITION);
462 if (field == null) {
463 if (filename != null) {
464 header.setField(Fields.contentDisposition(
465 ContentDispositionField.DISPOSITION_TYPE_ATTACHMENT,
466 filename, -1, null, null, null));
467 }
468 } else {
469 String dispositionType = field.getDispositionType();
470 Map<String, String> parameters = new HashMap<String, String>(field
471 .getParameters());
472 if (filename == null) {
473 parameters.remove(ContentDispositionField.PARAM_FILENAME);
474 } else {
475 parameters
476 .put(ContentDispositionField.PARAM_FILENAME, filename);
477 }
478 header.setField(Fields.contentDisposition(dispositionType,
479 parameters));
480 }
481 }
482
483 /**
484 * Determines if the MIME type of this <code>Entity</code> matches the
485 * given one. MIME types are case-insensitive.
486 *
487 * @param type the MIME type to match against.
488 * @return <code>true</code> on match, <code>false</code> otherwise.
489 */
490 public boolean isMimeType(String type) {
491 return getMimeType().equalsIgnoreCase(type);
492 }
493
494 /**
495 * Determines if the MIME type of this <code>Entity</code> is
496 * <code>multipart/*</code>. Since multipart-entities must have
497 * a boundary parameter in the <code>Content-Type</code> field this
498 * method returns <code>false</code> if no boundary exists.
499 *
500 * @return <code>true</code> on match, <code>false</code> otherwise.
501 */
502 public boolean isMultipart() {
503 ContentTypeField f =
504 (ContentTypeField) getHeader().getField(FieldName.CONTENT_TYPE);
505 return f != null && f.getBoundary() != null
506 && getMimeType().startsWith(ContentTypeField.TYPE_MULTIPART_PREFIX);
507 }
508
509 /**
510 * Disposes of the body of this entity. Note that the dispose call does not
511 * get forwarded to the parent entity of this Entity.
512 *
513 * Subclasses that need to free resources should override this method and
514 * invoke super.dispose().
515 *
516 * @see org.apache.james.mime4j.message.Disposable#dispose()
517 */
518 public void dispose() {
519 if (body != null) {
520 body.dispose();
521 }
522 }
523
524 /**
525 * Obtains the header of this entity. Creates and sets a new header if this
526 * entity's header is currently <code>null</code>.
527 *
528 * @return the header of this entity; never <code>null</code>.
529 */
530 Header obtainHeader() {
531 if (header == null) {
532 header = new Header();
533 }
534 return header;
535 }
536
537 /**
538 * Obtains the header field with the specified name.
539 *
540 * @param <F>
541 * concrete field type.
542 * @param fieldName
543 * name of the field to retrieve.
544 * @return the header field or <code>null</code> if this entity has no
545 * header or the header contains no such field.
546 */
547 <F extends Field> F obtainField(String fieldName) {
548 Header header = getHeader();
549 if (header == null)
550 return null;
551
552 @SuppressWarnings("unchecked")
553 F field = (F) header.getField(fieldName);
554 return field;
555 }
556
557 }