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     * &quot;message/rfc822&quot;. 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 &quot;text/plain&quot;. 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. &quot;plain&quot;, &quot;html&quot; or
207     *            &quot;xml&quot;).
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     *            (&quot;type/subtype&quot;).
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     *            (&quot;type/subtype&quot;).
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}