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         * &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    }