View Javadoc

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.mailboxmanager.torque;
21  
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.io.Reader;
25  import java.io.StringReader;
26  import java.nio.CharBuffer;
27  import java.nio.charset.IllegalCharsetNameException;
28  import java.nio.charset.UnsupportedCharsetException;
29  
30  import org.apache.commons.logging.Log;
31  import org.apache.commons.logging.LogFactory;
32  import org.apache.james.mime4j.MimeException;
33  import org.apache.james.mime4j.parser.MimeTokenStream;
34  
35  /**
36   * Searches an email for content. This class should be safe for use by
37   * concurrent threads.
38   */
39  class MessageSearcher {
40  
41      private Log logger;
42  
43      private CharSequence searchContent = null;
44  
45      private boolean isCaseInsensitive = false;
46  
47      private boolean includeHeaders = false;
48  
49      public MessageSearcher() {
50      }
51  
52      public MessageSearcher(CharSequence searchContent,
53              boolean isCaseInsensitive, boolean includeHeaders) {
54          super();
55          this.searchContent = searchContent;
56          this.isCaseInsensitive = isCaseInsensitive;
57          this.includeHeaders = includeHeaders;
58      }
59  
60      /**
61       * Is the search to include headers?
62       * 
63       * @return true if header values are included, false otherwise
64       */
65      public boolean isIncludeHeaders() {
66          return includeHeaders;
67      }
68  
69      /**
70       * Sets whether the search should include headers.
71       * 
72       * @param true
73       *            if header values are included, false otherwise
74       */
75      public synchronized void setIncludeHeaders(boolean includesHeaders) {
76          this.includeHeaders = includesHeaders;
77      }
78  
79      /**
80       * Is this search case insensitive?
81       * 
82       * @return true if the search should be case insensitive, false otherwise
83       */
84      public boolean isCaseInsensitive() {
85          return isCaseInsensitive;
86      }
87  
88      /**
89       * Sets whether the search should be case insensitive.
90       * 
91       * @param isCaseInsensitive
92       *            true for case insensitive searches, false otherwise
93       */
94      public synchronized void setCaseInsensitive(boolean isCaseInsensitive) {
95          this.isCaseInsensitive = isCaseInsensitive;
96      }
97  
98      /**
99       * Gets the content to be searched for.
100      * 
101      * @return search content, initially null
102      */
103     public CharSequence getSearchContent() {
104         return searchContent;
105     }
106 
107     /**
108      * Sets the content sought.
109      * 
110      * @param searchContent
111      *            content sought
112      */
113     public synchronized void setSearchContent(CharSequence searchContent) {
114         this.searchContent = searchContent;
115     }
116 
117     /**
118      * Is {@link #getSearchContent()} found in the given input?
119      * 
120      * @param input
121      *            <code>InputStream</code> containing an email
122      * @return true if the content exists and the stream contains the content,
123      *         false otherwise
124      * @throws IOException
125      * @throws MimeException
126      */
127     public boolean isFoundIn(final InputStream input) throws IOException,
128             MimeException {
129         final boolean includeHeaders;
130         final CharSequence searchContent;
131         final boolean isCaseInsensitive;
132         synchronized (this) {
133             includeHeaders = this.includeHeaders;
134             searchContent = this.searchContent;
135             isCaseInsensitive = this.isCaseInsensitive;
136         }
137         final boolean result;
138         if (searchContent == null || "".equals(searchContent)) {
139             final Log logger = getLogger();
140             logger.debug("Nothing to search for. ");
141             result = false;
142         } else {
143             final CharBuffer buffer = createBuffer(searchContent,
144                     isCaseInsensitive);
145             result = parse(input, isCaseInsensitive, includeHeaders, buffer);
146         }
147         return result;
148     }
149 
150     private boolean parse(final InputStream input,
151             final boolean isCaseInsensitive, final boolean includeHeaders,
152             final CharBuffer buffer) throws IOException, MimeException {
153         try {
154             boolean result = false;
155             MimeTokenStream parser = new MimeTokenStream();
156             parser.parse(input);
157             while (!result && parser.next() != MimeTokenStream.T_END_OF_STREAM) {
158                 final int state = parser.getState();
159                 switch (state) {
160                     case MimeTokenStream.T_BODY:
161                     case MimeTokenStream.T_PREAMBLE:
162                     case MimeTokenStream.T_EPILOGUE:
163                         result = checkBody(isCaseInsensitive, buffer, result,
164                                 parser);
165                         break;
166                     case MimeTokenStream.T_FIELD:
167                         if (includeHeaders) {
168                             result = checkHeader(isCaseInsensitive, buffer,
169                                     result, parser);
170                         }
171                         break;
172                 }
173             }
174             return result;
175         } catch (IllegalCharsetNameException e) {
176             handle(e);
177         } catch (UnsupportedCharsetException e) {
178             handle(e);
179         } catch (IllegalStateException e) {
180             handle(e);
181         }
182         return false;
183     }
184 
185     private boolean checkHeader(final boolean isCaseInsensitive,
186             final CharBuffer buffer, boolean result, MimeTokenStream parser)
187             throws IOException {
188         final String value = parser.getFieldValue();
189         final StringReader reader = new StringReader(value);
190         if (isFoundIn(reader, buffer, isCaseInsensitive)) {
191             result = true;
192         }
193         return result;
194     }
195 
196     private boolean checkBody(final boolean isCaseInsensitive,
197             final CharBuffer buffer, boolean result, MimeTokenStream parser)
198             throws IOException {
199         final Reader reader = parser.getReader();
200         if (isFoundIn(reader, buffer, isCaseInsensitive)) {
201             result = true;
202         }
203         return result;
204     }
205 
206     private CharBuffer createBuffer(final CharSequence searchContent,
207             final boolean isCaseInsensitive) {
208         final CharBuffer buffer;
209         if (isCaseInsensitive) {
210             final int length = searchContent.length();
211             buffer = CharBuffer.allocate(length);
212             for (int i = 0; i < length; i++) {
213                 final char next = searchContent.charAt(i);
214                 final char upperCase = Character.toUpperCase(next);
215                 buffer.put(upperCase);
216             }
217             buffer.flip();
218         } else {
219             buffer = CharBuffer.wrap(searchContent);
220         }
221         return buffer;
222     }
223 
224     protected void handle(Exception e) throws IOException, MimeException {
225         final Log logger = getLogger();
226         logger.warn("Cannot read MIME body.");
227         logger.debug("Failed to read body.", e);
228     }
229 
230     private boolean isFoundIn(final Reader reader, final CharBuffer buffer,
231             final boolean isCaseInsensitive) throws IOException {
232         boolean result = false;
233         int read;
234         while (!result && (read = reader.read()) != -1) {
235             final char next;
236             if (isCaseInsensitive) {
237                 next = Character.toUpperCase((char) read);
238             } else {
239                 next = (char) read;
240             }
241             result = matches(buffer, next);
242         }
243         return result;
244     }
245 
246     private boolean matches(final CharBuffer buffer, final char next) {
247         boolean result = false;
248         if (buffer.hasRemaining()) {
249             final boolean partialMatch = (buffer.position() > 0);
250             final char matching = buffer.get();
251             if (next != matching) {
252                 buffer.rewind();
253                 if (partialMatch) {
254                     result = matches(buffer, next);
255                 }
256             }
257         } else {
258             result = true;
259         }
260         return result;
261     }
262 
263     public final Log getLogger() {
264         if (logger == null) {
265             logger = LogFactory.getLog(MessageSearcher.class);
266         }
267         return logger;
268     }
269 
270     public final void setLogger(Log logger) {
271         this.logger = logger;
272     }
273 }