EmailDocumentHandler.java
001 /*
002  *  EmailDocumentHandler.java
003  *
004  *  Copyright (c) 1995-2012, The University of Sheffield. See the file
005  *  COPYRIGHT.txt in the software or at http://gate.ac.uk/gate/COPYRIGHT.txt
006  *
007  *  This file is part of GATE (see http://gate.ac.uk/), and is free
008  *  software, licenced under the GNU Library General Public License,
009  *  Version 2, June 1991 (in the distribution as file licence.html,
010  *  and also available at http://gate.ac.uk/gate/licence.html).
011  *
012  *  Cristian URSU,  3/Aug/2000
013  *
014  *  $Id: EmailDocumentHandler.java 17854 2014-04-17 13:44:42Z markagreenwood $
015  */
016 
017 package gate.email;
018 
019 import gate.Factory;
020 import gate.FeatureMap;
021 import gate.GateConstants;
022 import gate.event.StatusListener;
023 
024 import java.io.BufferedReader;
025 import java.io.IOException;
026 import java.io.StringReader;
027 import java.util.Collection;
028 import java.util.HashSet;
029 import java.util.Iterator;
030 import java.util.LinkedList;
031 import java.util.List;
032 import java.util.Map;
033 import java.util.StringTokenizer;
034 
035 /**
036   * This class implements the behaviour of the Email reader
037   * It takes the Gate Document representing a list with e-mails and
038   * creates Gate annotations on it.
039   */
040 public class EmailDocumentHandler {
041 
042   private String content = null;
043   private long documentSize = 0;
044 
045   /**
046     * Constructor used in tests mostly
047     */
048   public EmailDocumentHandler() {
049     setUp();
050   }//EmailDocumentHandler
051 
052   /**
053     * Constructor initialises some private fields
054     */
055   public EmailDocumentHandlergate.Document aGateDocument,
056                                Map<String,String>  aMarkupElementsMap,
057                                Map<String,String>  anElement2StringMap
058                               ) {
059 
060     gateDocument = aGateDocument;
061 
062     // gets AnnotationSet based on the new gate document
063     if (basicAS == null)
064       basicAS = gateDocument.getAnnotations(
065                                 GateConstants.ORIGINAL_MARKUPS_ANNOT_SET_NAME);
066 
067     markupElementsMap = aMarkupElementsMap;
068     element2StringMap = anElement2StringMap;
069     setUp();
070   }// EmailDocumentHandler
071 
072   /**
073     * Reads the Gate Document line by line and does the folowing things:
074     <ul>
075     <li> Each line is analized in order to detect where an e-mail starts.
076     <li> If the line belongs to an e-mail header then creates the
077     *      annotation if the markupElementsMap allows that.
078     <li> Lines belonging to the e-mail body are placed under a Gate
079     *      annotation called messageBody.
080     </ul>
081     */
082   public void annotateMessages() throws IOException,
083                                         gate.util.InvalidOffsetException {
084     // obtain a BufferedReader form the Gate document...
085     BufferedReader gateDocumentReader = null;
086     // Get the string representing the content of the document
087     // It is used inside CreateAnnotation method
088     content = gateDocument.getContent().toString();
089     // Get the sieze of the Gate Document. For the same purpose.
090     documentSize = gateDocument.getContent().size().longValue();
091 
092 //    gateDocumentReader = new BufferedReader(new InputStreamReader(
093 //              gateDocument.getSourceUrl().openConnection().getInputStream()));
094     gateDocumentReader = new BufferedReader(new StringReader(content));
095 
096     // for each line read from the gateDocumentReader do
097     // if the line begins an e-mail message then fire a status listener, mark
098     // that we are processing an e-mail, update the cursor and go to the next
099     // line.
100 
101     // if we are inside an e-mail, test if the line belongs to the message
102     // header
103     // if so, create a header field annotation.
104 
105     // if we are inside a a body and this is the first line from the body,
106     // create the message body annotation.
107     // Otherwise just update the cursor and go to the next line
108 
109     // if the line doesn't belong to an e-mail message then just update the
110     // cursor.
111     // next line
112 
113     String line = null;
114     String aFieldName = null;
115 
116     long cursor = 0;
117     long endEmail = 0;
118     long startEmail = 0;
119     long endHeader = 0;
120     long startHeader = 0;
121     long endBody = 0;
122     long startBody = 0;
123     long endField = 0;
124     long startField = 0;
125 
126     boolean insideAnEmail   = false;
127     boolean insideHeader    = false;
128     boolean emailReadBefore = false;
129     boolean fieldReadBefore = false;
130 
131     long nlSize = detectNLSize();
132 
133     //Out.println("NL SIZE = " + nlSize);
134 
135     // read each line from the reader
136     while ((line = gateDocumentReader.readLine()) != null){
137       // Here we test if the line delimitates two e-mail messages.
138       // Each e-mail message begins with a line like this:
139       // From P.Fairhurst Thu Apr 18 12:22:23 1996
140       // Method lineBeginsMessage() detects such lines.
141       if (lineBeginsMessage(line)){
142             // Inform the status listener to fire only
143             // if no. of elements processed.
144             // So far is a multiple of ELEMENTS_RATE
145           if ((++ emails % EMAILS_RATE== 0)
146             fireStatusChangedEvent("Reading emails : " + emails);
147           // if there are e-mails read before, then the previous e-mail
148           // ends here.
149           if (true == emailReadBefore){
150             // Cursor points at the beggining of the line
151             // E-mail and Body ends before the \n char
152             // Email ends as cursor value indicates
153             endEmail = cursor - nlSize ;
154             // also the e-mail body ends when an e-mail ends
155             endBody = cursor - nlSize;
156             //Annotate an E-mail body (startBody, endEmail)
157             createAnnotation("Body",startBody,endBody,null);
158             //Annotate an E-mail message(startEmail, endEmail) Email starts
159             createAnnotation("Message",startEmail,endEmail,null);
160           }
161           // if no e-mail was read before, now there is at list one message
162           // read
163           emailReadBefore = true;
164           // E-mail starts imediately from the beginning of this line which
165           // sepatates 2 messages.
166           startEmail = cursor;
167           // E-mail header starts also from here
168           startHeader = cursor;
169           // The cursor is updated with the length of the line + the
170           // new line char
171           cursor += line.length() + nlSize;
172           // We are inside an e-mail
173           insideAnEmail = true;
174           // Next is the E-mail header
175           insideHeader = true;
176           // No field inside header has been read before
177           fieldReadBefore = false;
178           // Read the next line
179           continue;
180       }//if (lineBeginsMessage(line))
181       if (false == insideAnEmail){
182         // the cursor is update with the length of the line +
183         // the new line char
184         cursor += line.length() + nlSize;
185         // read the next line
186         continue;
187       }//if
188       // here we are inside an e-mail message (inside Header or Body)
189       if (true == insideHeader){
190         // E-mail spec sais that E-mail header is separated by E-mail body
191         // by a \n char
192         if (line.equals("")){
193           // this \n sepatates the header of an e-mail form its body
194           // If we are here it means that the header has ended.
195           insideHeader  = false;
196           // e-mail header ends here
197           endHeader = cursor - nlSize;
198           // update the cursor with the length of \n
199           cursor += line.length() + nlSize;
200           // E-mail body starts from here
201           startBody = cursor;
202           // if fields were read before, it means that the e-mail has a header
203           if (true == fieldReadBefore){
204             endField = endHeader;
205             //Create a field annotation (fieldName, startField, endField)
206             createAnnotation(aFieldName, startField, endField, null);
207             //Create an e-mail header annotation
208             createAnnotation("Header",startHeader,endHeader,null);
209           }//if
210           // read the next line
211           continue;
212         }//if (line.equals(""))
213         // if line begins with a field then prepare to create an
214         // annotation with the name of the field
215         if (lineBeginsWithField(line)){
216           // if a field was read before, it means that the previous field ends
217           // here
218           if (true == fieldReadBefore){
219             // the previous field end here
220             endField = cursor - nlSize;
221             //Create a field annotation (fieldName, startField, endField)
222             createAnnotation(aFieldName, startField, endField, null);
223           }//if
224           fieldReadBefore = true;
225           aFieldName = getFieldName();
226           startField = cursor + aFieldName.length() ":".length();
227         }//if
228         // in both cases the cursor is updated and read the next line
229         // the cursor is update with the length of the line +
230         // the new line char
231         cursor += line.length() + nlSize;
232         // read the next line
233         continue;
234       }//if (true == insideHeader)
235       // here we are inside the E-mail body
236       // the body will end when the e-mail will end.
237       // here we just update the cursor
238       cursor += line.length() + nlSize;
239     }//while
240     // it might be possible that the file to contain only one e-mail and
241     // if the file contains only one e-mail message then the variable
242     // emailReadBefore must be set on true value
243     if (true == emailReadBefore){
244       endBody  = cursor - nlSize;
245       endEmail = cursor - nlSize;
246       //Annotate an E-mail body (startBody, endEmail)
247       createAnnotation("Body",startBody,endBody,null);
248       //Annotate an E-mail message(startEmail, endEmail) Email starts
249       createAnnotation("Message",startEmail,endEmail,null);
250     }
251     // if emailReadBefore is not set on true, that means that we didn't
252     // encounter any line like this:
253     // From P.Fairhurst Thu Apr 18 12:22:23 1996
254   }//annotateMessages
255 
256   /**
257     * This method detects if the text file which contains e-mail messages
258     * is under MSDOS or UNIX format.
259     * Under MSDOS the size of NL is 2 (\n \r) and under UNIX (\n) the size is 1
260     @return the size of the NL (1,2 or 0 = if no \n is found)
261     */
262   private int detectNLSize() {
263 
264     // get a char array
265     char[] document = null;
266 
267     // transform the gate Document into a char array
268     document = gateDocument.getContent().toString().toCharArray();
269 
270     // search for the \n char
271     // when it is found test if is followed by the \r char
272     for (int i=0; i<document.length; i++){
273       if (document[i== '\n'){
274 
275         // we just found a \n char.
276         // here we test if is followed by a \r char or preceded by a \r char
277         if (
278             (((i+1< document.length&& (document[i+1== '\r'))
279             ||
280             (((i-1>= 0)              && (document[i-1== '\r'))
281            return 2;
282         else return 1;
283       }
284     }
285     //if no \n char is found then the document is contained into a single text
286     // line.
287     return 0;
288 
289   // detectNLSize
290 
291   /**
292     * This method creates a gate annotation given its name, start, end and
293     * feature map.
294     */
295   private void createAnnotation(String anAnnotationName, long anAnnotationStart,
296                                  long anAnnotationEnd, FeatureMap aFeatureMap)
297                                        throws gate.util.InvalidOffsetException{
298 
299 /*
300     while (Character.isWhitespace(content.charAt((int) anAnnotationStart)))
301       anAnnotationStart ++;
302 
303 //    System.out.println(content.charAt((int) anAnnotationEnd));
304     while (Character.isWhitespace(content.charAt((int) anAnnotationEnd)))
305       anAnnotationEnd --;
306 
307     anAnnotationEnd ++;
308 */
309    if (canCreateAnnotation(anAnnotationStart,anAnnotationEnd,documentSize)){
310       if (aFeatureMap == null)
311           aFeatureMap = Factory.newFeatureMap();
312       basicAS.addnew Long(anAnnotationStart),
313                    new Long(anAnnotationEnd),
314                    anAnnotationName.toLowerCase(),
315                    aFeatureMap);
316    }// End if
317   }//createAnnotation
318   /**
319     * This method verifies if an Annotation can be created.
320     */
321   private boolean canCreateAnnotation(long start,
322                                       long end,
323                                       long gateDocumentSize){
324 
325     if (start < || end < return false;
326     if (start > end return false;
327     if ((start > gateDocumentSize|| (end > gateDocumentSize)) return false;
328     return true;
329   }// canCreateAnnotation
330 
331   /**
332     * Tests if the line begins an e-mail message
333     @param aTextLine a line from the file containing the e-mail messages
334     @return true if the line begins an e-mail message
335     @return false if is doesn't
336     */
337   protected boolean lineBeginsMessage(String aTextLine){
338     int score = 0;
339 
340     // if first token is "From" and the rest contains Day, Zone, etc
341     // then this line begins a message
342     // create a new String Tokenizer with " " as separator
343     StringTokenizer tokenizer = new StringTokenizer(aTextLine," ");
344 
345     // get the first token
346     String firstToken = null;
347     if (tokenizer.hasMoreTokens())
348         firstToken = tokenizer.nextToken();
349     else return false;
350 
351     // trim it
352     firstToken = firstToken.trim();
353 
354     // check against "From" word
355     // if the first token is not From then the entire line can not begin
356     // a message.
357     if (!firstToken.equals("From"))
358         return false;
359 
360     // else continue the analize
361     while (tokenizer.hasMoreTokens()){
362 
363       // get the next token
364       String token = tokenizer.nextToken();
365       token = token.trim();
366 
367       // see if it has a meaning(analize if is a Day, Month,Zone, Time, Year )
368       if (hasAMeaning(token))
369           score += 1;
370     }
371 
372     // a score greather or equql with 5 means that this line begins a message
373     if (score >= 5return true;
374     else return false;
375 
376   // lineBeginsMessage
377 
378   /**
379     * Tests if the line begins with a field from the e-mail header
380     * If the answer is true then it also sets the member fieldName with the
381     * value of this e-mail header field.
382     @param aTextLine a line from the file containing the e-mail text
383     @return true if the line begins with a field from the e-mail header
384     @return false if is doesn't
385     */
386   protected boolean lineBeginsWithField(String aTextLine){
387     if (containsSemicolon(aTextLine)){
388       StringTokenizer tokenizer = new StringTokenizer(aTextLine,":");
389 
390       // get the first token
391       String firstToken = null;
392 
393       if (tokenizer.hasMoreTokens())
394         firstToken = tokenizer.nextToken();
395       else return false;
396 
397       if (firstToken != null){
398         // trim it
399         firstToken = firstToken.trim();
400         if (containsWhiteSpaces(firstToken)) return false;
401 
402         // set the member field
403         fieldName = firstToken;
404       }
405       return true;
406     else return false;
407 
408   // lineBeginsWithField
409 
410   /**
411     * This method checks if a String contains white spaces.
412     */
413   protected boolean containsWhiteSpaces(String aString) {
414     for (int i = 0; i<aString.length(); i++)
415       if (Character.isWhitespace(aString.charAt(i))) return true;
416     return false;
417   // containsWhiteSpaces
418 
419   /**
420     * This method checks if a String contains a semicolon char
421     */
422   protected boolean containsSemicolon(String aString) {
423     for (int i = 0; i<aString.length(); i++)
424       if (aString.charAt(i== ':'return true;
425     return false;
426   // containsSemicolon
427 
428   /**
429     * This method tests a token if is Day, Month, Zone, Time, Year
430     */
431   protected boolean hasAMeaning(String aToken) {
432     // if token is a Day return true
433     if (day.contains(aToken)) return true;
434 
435     // if token is a Month return true
436     if (month.contains(aToken)) return true;
437 
438     // if token is a Zone then return true
439     if (zone.contains(aToken)) return true;
440 
441     // test if is a day number or a year
442     Integer dayNumberOrYear = null;
443     try{
444       dayNumberOrYear = new Integer(aToken);
445     catch (NumberFormatException e){
446       dayNumberOrYear = null;
447     }
448 
449     // if the creation succeded, then test if is day or year
450     if (dayNumberOrYear != null) {
451       int number = dayNumberOrYear.intValue();
452 
453       // if is a number between 1 and 31 then is a day
454       if ((number > 0&& (number < 32)) return true;
455 
456       // if is a number between 1900 si 3000 then is a year ;))
457       if ((number > 1900&& (number < 3000)) return true;
458 
459       // it might be the last two digits of 19xx
460       if ((number >= 0&& (number <= 99)) return true;
461     }
462     // test if is time: hh:mm:ss
463     if (isTime(aToken)) return true;
464 
465    return false;
466   // hasAMeaning
467 
468   /**
469     * Tests a token if is in time format HH:MM:SS
470     */
471   protected boolean isTime(String aToken) {
472     StringTokenizer st = new StringTokenizer(aToken,":");
473 
474     // test each token if is hour, minute or second
475     String hourString = null;
476     if (st.hasMoreTokens())
477         hourString = st.nextToken();
478 
479     // if there are no more tokens, it means that is not a time
480     if (hourString == nullreturn false;
481 
482     // test if is a number between 0 and 23
483     Integer hourInteger = null;
484     try{
485       hourInteger = new Integer(hourString);
486     catch (NumberFormatException e){
487       hourInteger = null;
488     }
489     if (hourInteger == nullreturn false;
490 
491     // if is not null then it means is a number
492     // test if is in 0 - 23 range
493     // if is not in this range then is not an hour
494     int hour = hourInteger.intValue();
495     if ( (hour < 0|| (hour > 23) ) return false;
496 
497     // we have the hour
498     // now repeat the test for minute and seconds
499 
500     // minutes
501     String minutesString = null;
502     if (st.hasMoreTokens())
503         minutesString = st.nextToken();
504 
505     // if there are no more tokens (minutesString == null) then return false
506     if (minutesString == nullreturn false;
507 
508     // test if is a number between 0 and 59
509     Integer minutesInteger = null;
510     try {
511       minutesInteger = new Integer (minutesString);
512     catch (NumberFormatException e){
513       minutesInteger = null;
514     }
515 
516     if (minutesInteger == nullreturn false;
517 
518     // if is not null then it means is a number
519     // test if is in 0 - 59 range
520     // if is not in this range then is not a minute
521     int minutes = minutesInteger.intValue();
522     if ( (minutes < 0|| (minutes > 59) ) return false;
523 
524     // seconds
525     String secondsString = null;
526     if (st.hasMoreTokens())
527         secondsString = st.nextToken();
528 
529     // if there are no more tokens (secondsString == null) then return false
530     if (secondsString == nullreturn false;
531 
532     // test if is a number between 0 and 59
533     Integer secondsInteger = null;
534     try {
535       secondsInteger = new Integer (secondsString);
536     catch (NumberFormatException e){
537       secondsInteger = null;
538     }
539     if (secondsInteger == nullreturn false;
540 
541     // if is not null then it means is a number
542     // test if is in 0 - 59 range
543     // if is not in this range then is not a minute
544     int seconds = secondsInteger.intValue();
545     if ( (seconds < 0|| (seconds > 59) ) return false;
546 
547     // if there are more tokens in st it means that we don't have this format:
548     // HH:MM:SS
549     if (st.hasMoreTokens()) return false;
550 
551     // if we are here it means we have a time
552     return true;
553   }// isTime
554 
555   /**
556     * Initialises the collections with data used by method lineBeginsMessage()
557     */
558   private void setUp(){
559     day = new HashSet<String>();
560     day.add("Mon");
561     day.add("Tue");
562     day.add("Wed");
563     day.add("Thu");
564     day.add("Fri");
565     day.add("Sat");
566     day.add("Sun");
567 
568     month = new HashSet<String>();
569     month.add("Jan");
570     month.add("Feb");
571     month.add("Mar");
572     month.add("Apr");
573     month.add("May");
574     month.add("Jun");
575     month.add("Jul");
576     month.add("Aug");
577     month.add("Sep");
578     month.add("Oct");
579     month.add("Nov");
580     month.add("Dec");
581 
582     zone = new HashSet<String>();
583     zone.add("UT");
584     zone.add("GMT");
585     zone.add("EST");
586     zone.add("EDT");
587     zone.add("CST");
588     zone.add("CDT");
589     zone.add("MST");
590     zone.add("MDT");
591     zone.add("PST");
592     zone.add("PDT");
593   }//setUp
594 
595   /**
596     * This method returns the value of the member fieldName.
597     * fieldName is set by the method lineBeginsWithField(String line).
598     * Each time the the line begins with a field name, that fiels will be stored
599     * in this member.
600     */
601   private String getFieldName() {
602     if (fieldName == nullreturn new String("");
603     else return fieldName;
604   // getFieldName
605 
606   // StatusReporter Implementation
607 
608   /**
609     * This methos is called when a listener is registered with this class
610     */
611   public void addStatusListener(StatusListener listener){
612     myStatusListeners.add(listener);
613   }
614   /**
615     * This methos is called when a listener is removed
616     */
617   public void removeStatusListener(StatusListener listener){
618     myStatusListeners.remove(listener);
619   }
620 
621   /**
622     * This methos is called whenever we need to inform the listener
623     * about an event.
624     */
625   protected void fireStatusChangedEvent(String text){
626     Iterator<StatusListener> listenersIter = myStatusListeners.iterator();
627     while(listenersIter.hasNext())
628       listenersIter.next().statusChanged(text);
629   }
630 
631   private static final int EMAILS_RATE = 16;
632 
633   // a gate document
634   private gate.Document gateDocument = null;
635 
636   // an annotation set used for creating annotation reffering the doc
637   private gate.AnnotationSet basicAS = null;
638 
639   // this map marks the elements that we don't want to create annotations
640   @SuppressWarnings("unused")
641   private Map<String,String>  markupElementsMap = null;
642 
643   // this map marks the elements after we want to insert some strings
644   @SuppressWarnings("unused")
645   private Map<String,String> element2StringMap = null;
646 
647   // listeners for status report
648   protected List<StatusListener> myStatusListeners = new LinkedList<StatusListener>();
649 
650   // this reports the the number of emails that have beed processed so far
651   private int emails = 0;
652 
653   // this is set by the method lineBeginsWithField(String line)
654   // each time the the line begins with a field name, that fiels will be stored
655   // in this member.
656   private String fieldName = null;
657 
658   private Collection<String> day = null;
659   private Collection<String> month = null;
660   private Collection<String> zone = null;
661 
662 
663 //EmailDocumentHandler