HtmlDocumentHandler.java
001 /*
002  *  HtmlDocumentHandler.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,  12/June/2000
013  *
014  *  $Id: HtmlDocumentHandler.java 17638 2014-03-12 09:36:47Z markagreenwood $
015  */
016 
017 package gate.html;
018 
019 import gate.Factory;
020 import gate.FeatureMap;
021 import gate.GateConstants;
022 import gate.corpora.DocumentContentImpl;
023 import gate.corpora.RepositioningInfo;
024 import gate.event.StatusListener;
025 import gate.util.Err;
026 import gate.util.InvalidOffsetException;
027 
028 import java.util.Collections;
029 import java.util.Enumeration;
030 import java.util.Iterator;
031 import java.util.LinkedList;
032 import java.util.List;
033 import java.util.Map;
034 import java.util.Stack;
035 
036 import javax.swing.text.BadLocationException;
037 import javax.swing.text.MutableAttributeSet;
038 import javax.swing.text.html.HTML;
039 import javax.swing.text.html.HTMLEditorKit.ParserCallback;
040 
041 
042 /** Implements the behaviour of the HTML reader.
043   * Methods of an object of this class are called by the HTML parser when
044   * events will appear.
045   * The idea is to parse the HTML document and construct Gate annotations
046   * objects.
047   * This class also will replace the content of the Gate document with a
048   * new one containing anly text from the HTML document.
049   */
050 public class HtmlDocumentHandler extends ParserCallback {
051 
052   /** Constructor initialises all the private memeber data.
053     * This will use the default annotation set taken from the gate document.
054     @param aDocument The gate document that will be processed
055     @param aMarkupElementsMap The map containing the elements that will
056     * transform into annotations
057     */
058   public HtmlDocumentHandler(gate.Document aDocument, Map<String,String> aMarkupElementsMap) {
059     this(aDocument,aMarkupElementsMap,null);
060   }
061 
062   /** Constructor initialises all the private memeber data
063     @param aDocument The gate document that will be processed
064     @param aMarkupElementsMap The map containing the elements that will
065     * transform into annotations
066     @param anAnnotationSet The annotation set that will contain annotations
067     * resulted from the processing of the gate document
068     */
069   public HtmlDocumentHandler(gate.Document       aDocument,
070                              Map<String,String>  aMarkupElementsMap,
071                              gate.AnnotationSet  anAnnotationSet) {
072     // init stack
073     stack = new Stack<CustomObject>();
074 
075     // this string contains the plain text (the text without markup)
076     tmpDocContent = new StringBuffer(aDocument.getContent().size().intValue());
077 
078     // colector is used later to transform all custom objects into
079     // annotation objects
080     colector = new LinkedList<CustomObject>();
081 
082     // the Gate document
083     doc = aDocument;
084 
085     // this map contains the elements name that we want to create
086     // if it's null all the elements from the XML documents will be transformed
087     // into Gate annotation objects
088     markupElementsMap = aMarkupElementsMap;
089 
090     // init an annotation set for this gate document
091     basicAS = anAnnotationSet;
092 
093     customObjectsId = 0;
094   }//HtmlDocumentHandler
095 
096   /** Keep the refference to this structure */
097   private RepositioningInfo reposInfo = null;
098 
099   /** Keep the refference to this structure */
100   private RepositioningInfo ampCodingInfo = null;
101 
102   /** Set repositioning information structure refference. If you set this
103    *  refference to <B>null</B> information wouldn't be collected.
104    */
105   public void setRepositioningInfo(RepositioningInfo info) {
106     reposInfo = info;
107   // setRepositioningInfo
108 
109   /** Return current RepositioningInfo object */
110   public RepositioningInfo getRepositioningInfo() {
111     return reposInfo;
112   // getRepositioningInfo
113 
114   /** Set repositioning information structure refference for ampersand coding.
115    *  If you set this refference to <B>null</B> information wouldn't be used.
116    */
117   public void setAmpCodingInfo(RepositioningInfo info) {
118     ampCodingInfo = info;
119   // setRepositioningInfo
120 
121   /** Return current RepositioningInfo object for ampersand coding. */
122   public RepositioningInfo getAmpCodingInfo() {
123     return ampCodingInfo;
124   // getRepositioningInfo
125 
126   /** The text inside the STYLE tag is processed with <code>handleText()</code>.
127    *  We should skip inserting of this text in the document. */
128   private boolean isInsideStyleTag = false;
129 
130   /** This method is called when the HTML parser encounts the beginning
131     * of a tag that means that the tag is paired by an end tag and it's
132     * not an empty one.
133     */
134   @Override
135   public void handleStartTag(HTML.Tag t, MutableAttributeSet a, int pos) {
136     // Fire the status listener if the elements processed exceded the rate
137     if (== (++elements % ELEMENTS_RATE))
138       fireStatusChangedEvent("Processed elements : " + elements);
139 
140     // Start of STYLE tag
141     if(HTML.Tag.STYLE.equals(t)) {
142       isInsideStyleTag = true;
143     // if
144 
145     // Construct a feature map from the attributes list
146     FeatureMap fm = Factory.newFeatureMap();
147 
148     // Take all the attributes an put them into the feature map
149     if (!= a.getAttributeCount()){
150       Enumeration<?> enumeration = a.getAttributeNames();
151       while (enumeration.hasMoreElements()){
152         Object attribute = enumeration.nextElement();
153         fm.put(attribute.toString(),(a.getAttribute(attribute)).toString());
154       }// while
155     }// if
156 
157     // Just analize the tag t and add some\n chars and spaces to the
158     // tmpDocContent.The reason behind is that we need to have a readable form
159     // for the final document.
160     customizeAppearanceOfDocumentWithStartTag(t);
161 
162     // If until here the "tmpDocContent" ends with a NON whitespace char,
163     // then we add a space char before calculating the START index of this
164     // tag.
165     // This is done in order not to concatenate the content of two separate tags
166     // and obtain a different NEW word.
167     int tmpDocContentSize = tmpDocContent.length();
168     if tmpDocContentSize != &&
169          !Character.isWhitespace(tmpDocContent.charAt(tmpDocContentSize - 1))
170        tmpDocContent.append(" ");
171 
172     // create the start index of the annotation
173     Long startIndex = new Long(tmpDocContent.length());
174 
175     // initialy the start index is equal with the End index
176     CustomObject obj = new CustomObject(t.toString(),fm,startIndex,startIndex);
177 
178     // put it into the stack
179     stack.push (obj);
180 
181   }//handleStartTag
182 
183    /** This method is called when the HTML parser encounts the end of a tag
184      * that means that the tag is paired by a beginning tag
185      */
186   @Override
187   public void handleEndTag(HTML.Tag t, int pos){
188     // obj is for internal use
189     CustomObject obj = null;
190 
191     // end of STYLE tag
192     if(HTML.Tag.STYLE.equals(t)) {
193       isInsideStyleTag = false;
194     // if
195 
196     // If the stack is not empty then we get the object from the stack
197     if (!stack.isEmpty()){
198       obj = stack.pop();
199       // Before adding it to the colector, we need to check if is an
200       // emptyAndSpan one. See CustomObject's isEmptyAndSpan field.
201       if (obj.getStart().equals(obj.getEnd())){
202         // The element had an end tag and its start was equal to its end. Hence
203         // it is anEmptyAndSpan one.
204         obj.getFM().put("isEmptyAndSpan","true");
205       }// End iff
206       // we add it to the colector
207       colector.add(obj);
208     }// End if
209 
210     // If element has text between, then customize its apearance
211     if obj != null &&
212          obj.getStart().longValue() != obj.getEnd().longValue()
213        )
214       // Customize the appearance of the document
215       customizeAppearanceOfDocumentWithEndTag(t);
216 
217     // if t is the </HTML> tag then we reached the end of theHTMLdocument
218     if (t == HTML.Tag.HTML){
219       // replace the old content with the new one
220       doc.setContent (new DocumentContentImpl(tmpDocContent.toString()));
221 
222       // If basicAs is null then get the default annotation
223       // set from this gate document
224       if (basicAS == null)
225         basicAS = doc.getAnnotations(
226                                 GateConstants.ORIGINAL_MARKUPS_ANNOT_SET_NAME);
227 
228       // sort colector ascending on its id
229       Collections.sort(colector);
230       // iterate through colector and construct annotations
231       while (!colector.isEmpty()){
232         obj = colector.getFirst();
233         colector.remove(obj);
234           // Construct an annotation from this obj
235           try{
236             if (markupElementsMap == null){
237                basicAS.addobj.getStart(),
238                             obj.getEnd(),
239                             obj.getElemName(),
240                             obj.getFM()
241                            );
242             }else{
243               String annotationType =
244                      markupElementsMap.get(obj.getElemName());
245               if (annotationType != null)
246                  basicAS.addobj.getStart(),
247                               obj.getEnd(),
248                               annotationType,
249                               obj.getFM()
250                              );
251             }
252           }catch (InvalidOffsetException e){
253               Err.prln("Error creating an annot :" + obj + " Discarded...");
254           }// end try
255 //        }// end if
256       }//while
257 
258       // notify the listener about the total amount of elements that
259       // has been processed
260       fireStatusChangedEvent("Total elements : " + elements);
261 
262     }//else
263 
264   }//handleEndTag
265 
266   /** This method is called when the HTML parser encounts an empty tag
267     */
268   @Override
269   public void handleSimpleTag(HTML.Tag t, MutableAttributeSet a, int pos){
270     // fire the status listener if the elements processed exceded the rate
271     if ((++elements % ELEMENTS_RATE== 0)
272       fireStatusChangedEvent("Processed elements : " + elements);
273 
274     // construct a feature map from the attributes list
275     // these are empty elements
276     FeatureMap fm = Factory.newFeatureMap();
277 
278     // take all the attributes an put them into the feature map
279     if (!= a.getAttributeCount ()){
280 
281        // Out.println("HAS  attributes = " + a.getAttributeCount ());
282         Enumeration<?> enumeration = a.getAttributeNames ();
283         while (enumeration.hasMoreElements ()){
284           Object attribute = enumeration.nextElement ();
285           fm.put attribute.toString(),(a.getAttribute(attribute)).toString());
286 
287         }//while
288 
289     }//if
290 
291     // create the start index of the annotation
292     Long startIndex = new Long(tmpDocContent.length());
293 
294     // initialy the start index is equal with the End index
295     CustomObject obj = new CustomObject(t.toString(),fm,startIndex,startIndex);
296 
297     // we add the object directly into the colector
298     // we don't add it to the stack because this is an empty tag
299     colector.add(obj);
300 
301     // Just analize the tag t and add some\n chars and spaces to the
302     // tmpDocContent.The reason behind is that we need to have a readable form
303     // for the final document.
304     customizeAppearanceOfDocumentWithSimpleTag(t);
305 
306   // handleSimpleTag
307 
308   /** This method is called when the HTML parser encounts text (PCDATA)
309     */
310   @Override
311   public void handleText(char[] text, int pos){
312 
313     // Skip the STYLE tag content
314     if(isInsideStyleTagreturn;
315 
316     // create a string object based on the reported text
317     String content = new String(text);
318 
319     // remove the difference between JDK 1.3 and JDK 1.4
320     String trimContent = content.trim();
321     if(trimContent.length() == 0) {
322       return;
323     // if
324 
325     int trimCorrection = content.indexOf(trimContent.charAt(0));
326     content = trimContent;
327 
328     StringBuffer contentBuffer = new StringBuffer("");
329     int tmpDocContentSize = tmpDocContent.length();
330     boolean incrementStartIndex = false;
331     // If the first char of the text just read "text[0]" is NOT whitespace AND
332     // the last char of the tmpDocContent[SIZE-1] is NOT whitespace then
333     // concatenation "tmpDocContent + content" will result into a new different
334     // word... and we want to avoid that...
335     if tmpDocContentSize != &&
336          content.length() != &&
337          !Character.isWhitespace(content.charAt(0)) &&
338          !Character.isWhitespace(tmpDocContent.charAt(tmpDocContentSize - 1))){
339 
340             contentBuffer.append(" ");
341             incrementStartIndex = true;
342     }// End if
343     // update the document content
344 
345     // put the repositioning information
346     if(reposInfo != null) {
347       int extractedPos = tmpDocContent.length() + contentBuffer.length();
348       addRepositioningInfo(content, pos + trimCorrection, extractedPos);
349     // if
350 
351     contentBuffer.append(content);
352     // calculate the End index for all the elements of the stack
353     // the expression is : End index = Current doc length + text length
354     Long end = new Long(tmpDocContent.length() + contentBuffer.length());
355 
356     CustomObject obj = null;
357     // Iterate through stack to modify the End index of the existing elements
358 
359     Iterator<CustomObject> anIterator = stack.iterator();
360     while (anIterator.hasNext ()){
361       // get the object and move to the next one
362       obj = anIterator.next ();
363       if (incrementStartIndex && obj.getStart().equals(obj.getEnd())){
364         obj.setStart(new Long(obj.getStart().longValue() 1));
365       }// End if
366       // sets its End index
367       obj.setEnd(end);
368     }// End while
369 
370     tmpDocContent.append(contentBuffer.toString());
371   }// end handleText();
372 
373   /** For given content the list with shrink position information is searched
374    *  and on the corresponding positions the correct repositioning information
375    *  is calculated and generated.
376    */
377   public void addRepositioningInfo(String content, int pos, int extractedPos) {
378     int contentLength = content.length();
379 
380     // wrong way (without correction and analysing)
381    //reposInfo.addPositionInfo(pos, contentLength, extractedPos, contentLength);
382 
383     RepositioningInfo.PositionInfo pi = null;
384     long startPos = pos;
385     long correction = 0;
386     long substituteStart;
387     long remainingLen;
388     long offsetInExtracted;
389 
390     for(int i = 0; i < ampCodingInfo.size(); ++i) {
391       pi = ampCodingInfo.get(i);
392       substituteStart = pi.getOriginalPosition();
393 
394       if(substituteStart >= startPos) {
395         if(substituteStart > pos + contentLength + correction) {
396           break// outside the current text
397         // if
398 
399         // should create two repositioning information records
400         remainingLen = substituteStart - (startPos + correction);
401         offsetInExtracted = startPos - pos;
402         if(remainingLen > 0) {
403           reposInfo.addPositionInfo(startPos + correction, remainingLen,
404                             extractedPos + offsetInExtracted, remainingLen);
405         // if
406         // record for shrank text
407         reposInfo.addPositionInfo(substituteStart, pi.getOriginalLength(),
408                           extractedPos + offsetInExtracted + remainingLen,
409                           pi.getCurrentLength());
410         startPos = startPos + remainingLen + pi.getCurrentLength();
411         correction += pi.getOriginalLength() - pi.getCurrentLength();
412       // if
413     // for
414 
415     // there is some text remaining for repositioning
416     offsetInExtracted = startPos - pos;
417     remainingLen = contentLength - offsetInExtracted;
418     if(remainingLen > 0) {
419       reposInfo.addPositionInfo(startPos + correction, remainingLen,
420                         extractedPos + offsetInExtracted, remainingLen);
421     // if
422   // addRepositioningInfo
423 
424   /** This method analizes the tag t and adds some \n chars and spaces to the
425     * tmpDocContent.The reason behind is that we need to have a readable form
426     * for the final document. This method modifies the content of tmpDocContent.
427     @param t the Html tag encounted by the HTML parser
428     */
429   protected void customizeAppearanceOfDocumentWithSimpleTag(HTML.Tag t){
430     boolean modification = false;
431     // if the HTML tag is BR then we add a new line character to the document
432     if (HTML.Tag.BR == t){
433       tmpDocContent.append("\n");
434       modification = true;
435     }// End if
436     if (modification == true){
437       Long end = new Long (tmpDocContent.length());
438       Iterator<CustomObject> anIterator = stack.iterator();
439       while (anIterator.hasNext ()){
440         // get the object and move to the next one
441         CustomObject obj = anIterator.next();
442         // sets its End index
443         obj.setEnd(end);
444       }// End while
445     }//End if
446   }// customizeAppearanceOfDocumentWithSimpleTag
447 
448   /** This method analizes the tag t and adds some \n chars and spaces to the
449     * tmpDocContent.The reason behind is that we need to have a readable form
450     * for the final document. This method modifies the content of tmpDocContent.
451     @param t the Html tag encounted by the HTML parser
452     */
453   protected void customizeAppearanceOfDocumentWithStartTag(HTML.Tag t){
454     boolean modification = false;
455     if (HTML.Tag.P == t){
456       int tmpDocContentSize = tmpDocContent.length();
457       if tmpDocContentSize >= &&
458            '\n' != tmpDocContent.charAt(tmpDocContentSize - 2)
459          ) { tmpDocContent.append("\n"); modification = true;}
460     }// End if
461     if (modification == true){
462       Long end = new Long (tmpDocContent.length());
463       Iterator<CustomObject> anIterator = stack.iterator();
464       while (anIterator.hasNext ()){
465         // get the object and move to the next one
466         CustomObject obj = anIterator.next();
467         // sets its End index
468         obj.setEnd(end);
469       }// End while
470     }//End if
471   }// customizeAppearanceOfDocumentWithStartTag
472 
473   /** This method analizes the tag t and adds some \n chars and spaces to the
474     * tmpDocContent.The reason behind is that we need to have a readable form
475     * for the final document. This method modifies the content of tmpDocContent.
476     @param t the Html tag encounted by the HTML parser
477     */
478   protected void customizeAppearanceOfDocumentWithEndTag(HTML.Tag t){
479     boolean modification = false;
480     // if the HTML tag is BR then we add a new line character to the document
481     if ( (HTML.Tag.P == t||
482 
483          (HTML.Tag.H1 == t||
484          (HTML.Tag.H2 == t||
485          (HTML.Tag.H3 == t||
486          (HTML.Tag.H4 == t||
487          (HTML.Tag.H5 == t||
488          (HTML.Tag.H6 == t||
489          (HTML.Tag.TR == t||
490          (HTML.Tag.CENTER == t||
491          (HTML.Tag.LI == t)
492        ){ tmpDocContent.append("\n"); modification = true;}
493 
494     if (HTML.Tag.TITLE == t){
495       tmpDocContent.append("\n\n");
496       modification = true;
497     }// End if
498 
499     if (modification == true){
500       Long end = new Long (tmpDocContent.length());
501       Iterator<CustomObject> anIterator = stack.iterator();
502       while (anIterator.hasNext ()){
503         // get the object and move to the next one
504         CustomObject obj = anIterator.next();
505         // sets its End index
506         obj.setEnd(end);
507       }// End while
508     }//End if
509   }// customizeAppearanceOfDocumentWithEndTag
510 
511   /**
512     * This method is called when the HTML parser encounts an error
513     * it depends on the programmer if he wants to deal with that error
514     */
515   @Override
516   public void handleError(String errorMsg, int pos) {
517     //Out.println ("ERROR CALLED : " + errorMsg);
518   }
519 
520   /** This method is called once, when the HTML parser reaches the end
521     * of its input streamin order to notify the parserCallback that there
522     * is nothing more to parse.
523     */
524   @Override
525   public void flush() throws BadLocationException{
526   }// flush
527 
528   /** This method is called when the HTML parser encounts a comment
529     */
530   @Override
531   public void handleComment(char[] text, int pos) {
532   }
533 
534   //StatusReporter Implementation
535 
536   public void addStatusListener(StatusListener listener) {
537     myStatusListeners.add(listener);
538   }
539 
540   public void removeStatusListener(StatusListener listener) {
541     myStatusListeners.remove(listener);
542   }
543 
544   protected void fireStatusChangedEvent(String text) {
545     Iterator<StatusListener> listenersIter = myStatusListeners.iterator();
546     while(listenersIter.hasNext())
547       listenersIter.next().statusChanged(text);
548   }
549 
550   /**
551     * This method verifies if data contained by the CustomObject can be used
552     * to create a GATE annotation.
553     */
554 /*  private boolean canCreateAnnotation(CustomObject aCustomObject){
555     long start            = aCustomObject.getStart().longValue();
556     long end              = aCustomObject.getEnd().longValue();
557     long gateDocumentSize = doc.getContent().size().longValue();
558 
559     if (start < 0 || end < 0 ) return false;
560     if (start > end ) return false;
561     if ((start > gateDocumentSize) || (end > gateDocumentSize)) return false;
562     return true;
563   }// canCreateAnnotation
564 */
565 
566   // HtmlDocumentHandler member data
567 
568   // this constant indicates when to fire the status listener
569   // this listener will add an overhead and we don't want a big overhead
570   // this listener will be callled from ELEMENTS_RATE to ELEMENTS_RATE
571   final static  int ELEMENTS_RATE = 128;
572 
573   // this map contains the elements name that we want to create
574   // if it's null all the elements from the HTML documents will be transformed
575   // into Gate annotation objects otherwise only the elements it contains will
576   // be transformed
577   private Map<String,String> markupElementsMap = null;
578 
579   // the content of the HTML document, without any tag
580   // for internal use
581   private StringBuffer tmpDocContent = null;
582 
583   // a stack used to remember elements and to keep the order
584   private Stack<CustomObject> stack = null;
585 
586   // a gate document
587   private gate.Document doc = null;
588 
589   // an annotation set used for creating annotation reffering the doc
590   private gate.AnnotationSet basicAS;
591 
592   // listeners for status report
593   protected List<StatusListener> myStatusListeners = new LinkedList<StatusListener>();
594 
595   // this reports the the number of elements that have beed processed so far
596   private int elements = 0;
597 
598   protected  long customObjectsId = 0;
599   // we need a colection to retain all the CustomObjects that will be
600   // transformed into annotation over the gate document...
601   // the transformation will take place inside onDocumentEnd() method
602   private LinkedList<CustomObject> colector = null;
603 
604   // Inner class
605   /**
606     * The objects belonging to this class are used inside the stack.
607     * This class is for internal needs
608     */
609   class  CustomObject implements Comparable<CustomObject> {
610 
611     // constructor
612     public CustomObject(String anElemName, FeatureMap aFm,
613                            Long aStart, Long anEnd) {
614       elemName = anElemName;
615       fm = aFm;
616       start = aStart;
617       end = anEnd;
618       id = new Long(customObjectsId ++);
619     }// End CustomObject()
620 
621     // Methos implemented as required by Comparable interface
622     @Override
623     public int compareTo(CustomObject obj){
624       return this.id.compareTo(obj.getId());
625     }// compareTo();
626 
627     // accesor
628     public String getElemName() {
629       return elemName;
630     }// getElemName()
631 
632     public FeatureMap getFM() {
633       return fm;
634     }// getFM()
635 
636     public Long getStart() {
637       return start;
638     }// getStart()
639 
640     public Long getEnd() {
641       return end;
642     }// getEnd()
643 
644     public Long getId(){ return id;}
645 
646     // mutator
647     public void setElemName(String anElemName) {
648       elemName = anElemName;
649     }// getElemName()
650 
651     public void setFM(FeatureMap aFm) {
652       fm = aFm;
653     }// setFM();
654 
655     public void setStart(Long aStart) {
656       start = aStart;
657     }// setStart();
658 
659     public void setEnd(Long anEnd) {
660       end = anEnd;
661     }// setEnd();
662 
663     // data fields
664     private String elemName = null;
665     private FeatureMap fm = null;
666     private Long start = null;
667     private Long end  = null;
668     private Long id = null;
669 
670   // End inner class CustomObject
671 
672 }//End class HtmlDocumentHandler
673 
674