View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.james.jcr;
18  
19  import java.io.ByteArrayInputStream;
20  import java.io.IOException;
21  import java.io.InputStream;
22  import java.text.SimpleDateFormat;
23  import java.util.Calendar;
24  import java.util.Date;
25  
26  import javax.jcr.Node;
27  import javax.jcr.PathNotFoundException;
28  import javax.jcr.RepositoryException;
29  import javax.mail.Address;
30  import javax.mail.BodyPart;
31  import javax.mail.Message;
32  import javax.mail.MessagingException;
33  import javax.mail.Multipart;
34  import javax.mail.Part;
35  import javax.mail.Message.RecipientType;
36  import javax.mail.internet.ContentType;
37  import javax.mail.internet.MimeMessage;
38  
39  import org.apache.jackrabbit.util.Text;
40  
41  /**
42   * JavaBean that stores messages to a JCR content repository.
43   * <p>
44   * After instantiating this bean you should use the
45   * {@link #setParentNode(Node)} method to specify the root node under
46   * which all messages should be stored. Then you can call
47   * {@link #storeMessage(Message)} to store messages in the repository.
48   * <p>
49   * The created content structure below the given parent node consists
50   * of a date based .../year/month/day tree structure, below which the actual
51   * messages are stored. A stored message consists of an nt:file node whose
52   * name is based on the subject of the message. The jcr:content child of the
53   * nt:file node contains the MIME structure and all relevant headers of the
54   * message. Note that the original message source is <em>not</em> stored,
55   * which means that some of the message information will be lost.
56   * <p>
57   * The messages are stored using the session associated with the specified
58   * parent node. No locking or synchronization is performed, and it is expected
59   * that only one thread writing to the message subtree at any given moment.
60   * You should use JCR locking or some other explicit synchronization mechanism
61   * if you want to have concurrent writes to the message subtree.
62   */
63  public class JCRStoreBean {
64  
65      /**
66       * Parent node where the messages are stored.
67       */
68      private Node parent;
69  
70      public void setParentNode(Node parent) {
71          this.parent = parent;
72      }
73  
74      /**
75       * Stores the given mail message to the content repository.
76       *
77       * @param message mail message
78       * @throws MessagingException if the message could not be read
79       * @throws RepositoryException if the message could not be saved
80       */
81      public void storeMessage(Message message)
82              throws MessagingException, RepositoryException {
83          try {
84              Date date = message.getSentDate();
85              Node year = getOrAddNode(parent, format("yyyy", date), "nt:folder");
86              Node month = getOrAddNode(year, format("mm", date), "nt:folder");
87              Node day = getOrAddNode(month, format("dd", date), "nt:folder");
88              Node node = createNode(day, getMessageName(message), "nt:file");
89              importEntity(message, node);
90              parent.save();
91          } catch (IOException e) {
92              throw new MessagingException("Could not read message", e);
93          }
94      }
95  
96      /**
97       * Import the given entity to the given JCR node.
98       *
99       * @param entity the source entity
100      * @param parent the target node
101      * @throws MessagingException if the message could not be read
102      * @throws RepositoryException if the message could not be written
103      * @throws IOException if the message could not be read
104      */
105     private void importEntity(Part entity, Node parent)
106             throws MessagingException, RepositoryException, IOException {
107         Node node = parent.addNode("jcr:content", "nt:unstructured");
108 
109         setProperty(node, "description", entity.getDescription());
110         setProperty(node, "disposition", entity.getDisposition());
111         setProperty(node, "filename", entity.getFileName());
112 
113         if (entity instanceof MimeMessage) {
114             MimeMessage mime = (MimeMessage) entity;
115             setProperty(node, "subject", mime.getSubject());
116             setProperty(node, "message-id", mime.getMessageID());
117             setProperty(node, "content-id", mime.getContentID());
118             setProperty(node, "content-md5", mime.getContentMD5());
119             setProperty(node, "language", mime.getContentLanguage());
120             setProperty(node, "sent", mime.getSentDate());
121             setProperty(node, "received", mime.getReceivedDate());
122             setProperty(node, "from", mime.getFrom());
123             setProperty(node, "to", mime.getRecipients(RecipientType.TO));
124             setProperty(node, "cc", mime.getRecipients(RecipientType.CC));
125             setProperty(node, "bcc", mime.getRecipients(RecipientType.BCC));
126             setProperty(node, "reply-to", mime.getReplyTo());
127             setProperty(node, "sender", mime.getSender());
128         }
129 
130         Object content = entity.getContent();
131         ContentType type = getContentType(entity);
132         node.setProperty("jcr:mimeType", type.getBaseType());
133         if (content instanceof Multipart) {
134             Multipart multipart = (Multipart) content;
135             for (int i = 0; i < multipart.getCount(); i++) {
136                 BodyPart part = multipart.getBodyPart(i);
137                 Node child;
138                 if (part.getFileName() != null) {
139                     child = createNode(node, part.getFileName(), "nt:file");
140                 } else {
141                     child = createNode(node, "part", "nt:unstructured");
142                 }
143                 importEntity(part, child);
144             }
145         } else if (content instanceof String) {
146             byte[] bytes = ((String) content).getBytes("UTF-8");
147             node.setProperty("jcr:encoding", "UTF-8");
148             node.setProperty("jcr:data", new ByteArrayInputStream(bytes));
149         } else if (content instanceof InputStream) {
150             setProperty(
151                     node, "jcr:encoding", type.getParameter("encoding"));
152             node.setProperty("jcr:data", (InputStream) content);
153         } else {
154             node.setProperty("jcr:data", entity.getInputStream());
155         }
156     }
157 
158     /**
159      * Formats the given date using the given {@link SimpleDateFormat}
160      * format string.
161      *
162      * @param format format string
163      * @param date date to be formatted
164      * @return formatted date
165      */
166     private String format(String format, Date date) {
167         return new SimpleDateFormat(format).format(date);
168     }
169 
170     /**
171      * Suggests a name for the node where the given message will be stored.
172      *
173      * @param message mail message
174      * @return suggested name
175      * @throws MessagingException if an error occurs
176      */
177     private String getMessageName(Message message)
178             throws MessagingException {
179         String name = message.getSubject();
180         if (name == null) {
181             name = "unnamed";
182         } else {
183             name = name.replaceAll("[^A-Za-z0-9 ]", "").trim();
184             if (name.length() == 0) {
185                 name = "unnamed";
186             }
187         }
188         return name;
189     }
190 
191     /**
192      * Returns the named child node of the given parent. If the child node
193      * does not exist, it is automatically created with the given node type.
194      * The created node is not saved by this method.
195      *
196      * @param parent parent node
197      * @param name name of the child node
198      * @param type type of the child node
199      * @return child node
200      * @throws RepositoryException if the child node could not be accessed
201      */
202     private Node getOrAddNode(Node parent, String name, String type)
203             throws RepositoryException {
204         try {
205             return parent.getNode(name);
206         } catch (PathNotFoundException e) {
207             return parent.addNode(name, type);
208         }
209     }
210 
211     /**
212      * Creates a new node with a name that resembles the given suggestion.
213      * The created node is not saved by this method.
214      *
215      * @param parent parent node
216      * @param name suggested name
217      * @param type node type
218      * @return created node
219      * @throws RepositoryException if an error occurs
220      */
221     private Node createNode(Node parent, String name, String type)
222             throws RepositoryException {
223         String original = name;
224         name = Text.escapeIllegalJcrChars(name);
225         for (int i = 2; parent.hasNode(name); i++) {
226             name = Text.escapeIllegalJcrChars(original + i);
227         }
228         return parent.addNode(name, type);
229     }
230 
231     /**
232      * Returns the content type of the given message entity. Returns
233      * the default "text/plain" content type if a content type is not
234      * available. Returns "application/octet-stream" if an error occurs.
235      *
236      * @param entity the message entity
237      * @return content type, or <code>text/plain</code> if not available
238      */
239     private static ContentType getContentType(Part entity) {
240         try {
241             String type = entity.getContentType();
242             if (type != null) {
243                 return new ContentType(type);
244             } else {
245                 return new ContentType("text/plain");
246             }
247         } catch (MessagingException e) {
248             ContentType type = new ContentType();
249             type.setPrimaryType("application");
250             type.setSubType("octet-stream");
251             return type;
252         }
253     }
254 
255     /**
256      * Sets the named property if the given value is not null.
257      *
258      * @param node target node
259      * @param name property name
260      * @param value property value
261      * @throws RepositoryException if an error occurs
262      */
263     private void setProperty(Node node, String name, String value)
264             throws RepositoryException {
265         if (value != null) {
266             node.setProperty(name, value);
267         }
268     }
269 
270     /**
271      * Sets the named property if the given array of values is
272      * not null or empty.
273      *
274      * @param node target node
275      * @param name property name
276      * @param values property values
277      * @throws RepositoryException if an error occurs
278      */
279     private void setProperty(Node node, String name, String[] values)
280             throws RepositoryException {
281         if (values != null && values.length > 0) {
282             node.setProperty(name, values);
283         }
284     }
285 
286     /**
287      * Sets the named property if the given value is not null.
288      *
289      * @param node target node
290      * @param name property name
291      * @param value property value
292      * @throws RepositoryException if an error occurs
293      */
294     private void setProperty(Node node, String name, Date value)
295             throws RepositoryException {
296         if (value != null) {
297             Calendar calendar = Calendar.getInstance();
298             calendar.setTime(value);
299             node.setProperty(name, calendar);
300         }
301     }
302 
303     /**
304      * Sets the named property if the given value is not null.
305      *
306      * @param node target node
307      * @param name property name
308      * @param value property value
309      * @throws RepositoryException if an error occurs
310      */
311     private void setProperty(Node node, String name, Address value)
312             throws RepositoryException {
313         if (value != null) {
314             node.setProperty(name, value.toString());
315         }
316     }
317 
318     /**
319      * Sets the named property if the given array of values is
320      * not null or empty.
321      *
322      * @param node target node
323      * @param name property name
324      * @param values property values
325      * @throws RepositoryException if an error occurs
326      */
327     private void setProperty(Node node, String name, Address[] values)
328             throws RepositoryException {
329         if (values != null && values.length > 0) {
330             String[] strings = new String[values.length];
331             for (int i = 0; i < values.length; i++) {
332                 strings[i] = values[i].toString();
333             }
334             node.setProperty(name, strings);
335         }
336     }
337 
338 }