NekoHtmlDocumentHandler.java
0001 /*
0002  *  NekoHtmlDocumentHandler.java
0003  *
0004  *  Copyright (c) 2006, The University of Sheffield.
0005  *
0006  *  This file is part of GATE (see http://gate.ac.uk/), and is free
0007  *  software, licenced under the GNU Library General Public License,
0008  *  Version 2, June 1991 (in the distribution as file licence.html,
0009  *  and also available at http://gate.ac.uk/gate/licence.html).
0010  *
0011  *  Ian Roberts, 17/Dec/2006
0012  *
0013  *  $Id: NekoHtmlDocumentHandler.java 17597 2014-03-08 15:19:43Z markagreenwood $
0014  */
0015 
0016 package gate.html;
0017 
0018 import gate.Factory;
0019 import gate.FeatureMap;
0020 import gate.Gate;
0021 import gate.GateConstants;
0022 import gate.corpora.DocumentContentImpl;
0023 import gate.corpora.RepositioningInfo;
0024 import gate.event.StatusListener;
0025 import gate.util.Err;
0026 import gate.util.InvalidOffsetException;
0027 import gate.util.Out;
0028 
0029 import java.util.Collections;
0030 import java.util.Comparator;
0031 import java.util.HashSet;
0032 import java.util.Iterator;
0033 import java.util.LinkedList;
0034 import java.util.List;
0035 import java.util.Set;
0036 
0037 import org.apache.xerces.xni.Augmentations;
0038 import org.apache.xerces.xni.NamespaceContext;
0039 import org.apache.xerces.xni.QName;
0040 import org.apache.xerces.xni.XMLAttributes;
0041 import org.apache.xerces.xni.XMLLocator;
0042 import org.apache.xerces.xni.XMLResourceIdentifier;
0043 import org.apache.xerces.xni.XMLString;
0044 import org.apache.xerces.xni.XNIException;
0045 import org.apache.xerces.xni.parser.XMLDocumentSource;
0046 import org.apache.xerces.xni.parser.XMLParseException;
0047 import org.cyberneko.html.HTMLEventInfo;
0048 
0049 /**
0050  * The XNI document handler used with NekoHTML to parse HTML documents.
0051  * We use XNI rather than SAX as XNI can distinguish between empty
0052  * elements (<element/>) and elements with an empty span
0053  * (<element></element>), whereas SAX just treats both cases
0054  * the same.
0055  */
0056 public class NekoHtmlDocumentHandler
0057                                     implements
0058                                     org.apache.xerces.xni.XMLDocumentHandler,
0059                                     org.apache.xerces.xni.parser.XMLErrorHandler {
0060   private static final boolean DEBUG = false;
0061 
0062   private static final boolean DEBUG_GENERAL = DEBUG;
0063 
0064   private static final boolean DEBUG_ELEMENTS = DEBUG;
0065 
0066   private static final boolean DEBUG_CHARACTERS = DEBUG;
0067 
0068   private static final boolean DEBUG_UNUSED = DEBUG;
0069 
0070   public static final String AUGMENTATIONS = "http://cyberneko.org/html/features/augmentations";
0071 
0072   /**
0073    * Constructor initialises all the private memeber data
0074    
0075    @param aDocument The gate document that will be processed
0076    @param anAnnotationSet The annotation set that will contain
0077    *          annotations resulted from the processing of the gate
0078    *          document
0079    @param ignorableTags HTML tag names (lower case) whose text content
0080    *          should be ignored by this handler.
0081    */
0082   public NekoHtmlDocumentHandler(gate.Document aDocument,
0083           gate.AnnotationSet anAnnotationSet, Set<String> ignorableTags) {
0084     if(ignorableTags == null) {
0085       ignorableTags = new HashSet<String>();
0086     }
0087     if(DEBUG_GENERAL) {
0088       Out.println("Created NekoHtmlDocumentHandler.  ignorableTags = "
0089               + ignorableTags);
0090     }
0091     // init stack
0092     stack = new java.util.Stack<CustomObject>();
0093 
0094     // this string contains the plain text (the text without markup)
0095     tmpDocContent = new StringBuilder(aDocument.getContent().size().intValue());
0096 
0097     // colector is used later to transform all custom objects into
0098     // annotation objects
0099     colector = new LinkedList<CustomObject>();
0100 
0101     // the Gate document
0102     doc = aDocument;
0103 
0104     // init an annotation set for this gate document
0105     basicAS = anAnnotationSet;
0106 
0107     // first annotation ID to use
0108     customObjectsId = 0;
0109 
0110     this.ignorableTags = ignorableTags;
0111     
0112     if Gate.getUserConfig().get(
0113             GateConstants.DOCUMENT_ADD_SPACE_ON_UNPACK_FEATURE_NAME)!= null) {
0114       addSpaceOnUnpack =
0115         Gate.getUserConfig().getBoolean(
0116           GateConstants.DOCUMENT_ADD_SPACE_ON_UNPACK_FEATURE_NAME
0117         ).booleanValue();
0118     }
0119   }// HtmlDocumentHandler
0120 
0121   /**
0122    * Set the array of line offsets. This array holds the starting
0123    * character offset in the document of the beginning of each line of
0124    * text, to allow us to convert the NekoHTML location information
0125    * (line and column number) into offsets from the beginning of the
0126    * document for repositioning info.
0127    */
0128   public void setLineOffsets(int[] lineOffsets) {
0129     this.lineOffsets = lineOffsets;
0130   }
0131 
0132   /**
0133    * Called when the parser encounters the start of an HTML element.
0134    * Empty elements also trigger this method, followed immediately by an
0135    {@link #endElement}.
0136    */
0137   @Override
0138   public void startElement(QName element, XMLAttributes attributes,
0139           Augmentations augsthrows XNIException {
0140     // deal with any outstanding character content
0141     charactersAction();
0142 
0143     if(DEBUG_ELEMENTS) {
0144       Out.println("startElement: " + element.localpart);
0145     }
0146     // Fire the status listener if the elements processed exceded the
0147     // rate
0148     if(== (++elements % ELEMENTS_RATE))
0149       fireStatusChangedEvent("Processed elements : " + elements);
0150 
0151     // Start of ignorable tag
0152     if(ignorableTags.contains(element.localpart)) {
0153       ignorableTagLevels++;
0154       if(DEBUG_ELEMENTS) {
0155         Out.println("  ignorable tag: levels = " + ignorableTagLevels);
0156       }
0157     // if
0158 
0159     // Construct a feature map from the attributes list
0160     FeatureMap fm = Factory.newFeatureMap();
0161 
0162     // Take all the attributes an put them into the feature map
0163     for(int i = 0; i < attributes.getLength(); i++) {
0164       if(DEBUG_ELEMENTS) {
0165         Out.println("  attribute: " + attributes.getLocalName(i" = "
0166                 + attributes.getValue(i));
0167       }
0168       fm.put(attributes.getLocalName(i), attributes.getValue(i));
0169     }
0170 
0171     // Just analize the tag and add some\n chars and spaces to the
0172     // tmpDocContent.The reason behind is that we need to have a
0173     // readable form
0174     // for the final document.
0175     customizeAppearanceOfDocumentWithStartTag(element.localpart);
0176 
0177     // create the start index of the annotation
0178     Long startIndex = new Long(tmpDocContent.length());
0179 
0180     // initialy the start index is equal with the End index
0181     CustomObject obj = new CustomObject(element.localpart, fm, startIndex,
0182             startIndex);
0183 
0184     // put it into the stack
0185     stack.push(obj);
0186 
0187   }
0188 
0189   /**
0190    * Called when the parser encounters character or CDATA content.
0191    * Characters may be reported in more than one chunk, so we gather all
0192    * contiguous chunks together and process them in one block.
0193    */
0194   @Override
0195   public void characters(XMLString text, Augmentations augs)
0196           throws XNIException {
0197     if(!readCharacterStatus) {
0198       if(reposInfo != null) {
0199         HTMLEventInfo evInfo = (augs == nullnull (HTMLEventInfo)augs
0200                 .getItem(AUGMENTATIONS);
0201         if(evInfo == null) {
0202           Err.println("Warning: could not determine proper repositioning "
0203                   "info for character chunk \""
0204                   new String(text.ch, text.offset, text.length)
0205                   "\" near offset " + charactersStartOffset
0206                   ".  Save preserving format may give incorret results.");
0207         }
0208         else {
0209           // NekoHTML numbers lines and columns from 1, not 0
0210           int line = evInfo.getBeginLineNumber() 1;
0211           int col = evInfo.getBeginColumnNumber() 1;
0212           charactersStartOffset = lineOffsets[line+ col;
0213           if(DEBUG_CHARACTERS) {
0214             Out.println("characters: line = " + line + " (offset " +
0215                 lineOffsets[line"), col = " + col + " : file offset = " +
0216                 charactersStartOffset);
0217           }
0218         }
0219       }
0220 
0221       contentBuffer = new StringBuilder();
0222     }
0223     readCharacterStatus = true;
0224 
0225     boolean canAppendWS = (contentBuffer.length() == || !Character
0226             .isWhitespace(contentBuffer.charAt(contentBuffer.length() 1)));
0227     // we must collapse
0228     // whitespace down to a single space, to mirror the normal
0229     // HtmlDocumentFormat.
0230     for(int i = text.offset; i < text.offset + text.length; ++i) {
0231       if(!Character.isWhitespace(text.ch[i])) {
0232         contentBuffer.append(text.ch[i]);
0233         canAppendWS = true;
0234       }
0235       else {
0236         if(canAppendWS) {
0237           contentBuffer.append(' ');
0238           canAppendWS = false;
0239         }
0240       }
0241     }
0242   }
0243 
0244   /**
0245    * Called when all text between two tags has been processed.
0246    */
0247   public void charactersAction() throws XNIException {
0248     // check whether there are actually any characters to process
0249     if(!readCharacterStatus) {
0250       return;
0251     }
0252     readCharacterStatus = false;
0253 
0254     if(DEBUG_CHARACTERS) {
0255       Out.println("charactersAction: offset = " + charactersStartOffset);
0256     }
0257 
0258     if(contentBuffer.length() == 0return;
0259 
0260     // Skip ignorable tag content
0261     if(ignorableTagLevels > 0) {
0262       if(DEBUG_CHARACTERS) {
0263         Out.println("  inside ignorable tag, skipping");
0264       }
0265       return;
0266     }
0267 
0268     // the number of whitespace characters trimmed off the front of this
0269     // chunk of characters
0270     boolean thisChunkStartsWithWS = Character.isWhitespace(contentBuffer.charAt(0));
0271 
0272     // trim leading whitespace
0273     if(thisChunkStartsWithWS) {
0274       contentBuffer.deleteCharAt(0);
0275     }
0276 
0277     if(contentBuffer.length() == 0) {
0278       if(DEBUG_CHARACTERS) {
0279         Out.println("  whitespace only: ignoring");
0280       }
0281       // if this chunk starts with whitespace and is whitespace only, then
0282       // it ended with whitespace too
0283       previousChunkEndedWithWS = thisChunkStartsWithWS;
0284       return;
0285     // if
0286 
0287     // trim trailing whitespace
0288     boolean trailingWhitespace = Character.isWhitespace(contentBuffer.charAt(contentBuffer.length() 1));
0289     if(trailingWhitespace) {
0290       contentBuffer.setLength(contentBuffer.length() 1);
0291     }
0292 
0293     if(DEBUG_CHARACTERS) {
0294       Out.println("  content = \"" + contentBuffer + "\"");
0295     }
0296 
0297     int tmpDocContentSize = tmpDocContent.length();
0298     boolean incrementStartIndex = false;
0299     // correct for whitespace.  Since charactersAction never leaves
0300     // tmpDocContent with a trailing whitespace character, we may
0301     // need to add space before we append the current chunk to prevent
0302     // two chunks either side of a tag from running into one.  We need
0303     // to do this if there is whitespace in the original content on
0304     // one side or other of the tag (i.e. the previous chunk ended
0305     // with space or the current chunk starts with space).  Also, if
0306     // the user's "add space on markup unpack" option is true, we add
0307     // space anyway so as not to run things like
0308     // "...foo</td><td>bar..." together into "foobar".
0309     if(tmpDocContentSize != 0
0310             && !Character.isWhitespace(tmpDocContent
0311                     .charAt(tmpDocContentSize - 1))
0312             && (previousChunkEndedWithWS || thisChunkStartsWithWS || addSpaceOnUnpack)) {
0313       if(DEBUG_CHARACTERS) {
0314         Out
0315                 .println(String
0316                         .format(
0317                                 "  non-whitespace character %1$x (%1$c) found at end of content, adding space",
0318                                 (int)tmpDocContent
0319                                         .charAt(tmpDocContentSize - 1)));
0320       }
0321       tmpDocContent.append(' ');
0322       incrementStartIndex = true;
0323     }// End if
0324     // update the document content
0325 
0326     tmpDocContent.append(contentBuffer);
0327 
0328     // put the repositioning information
0329     if(reposInfo != null) {
0330       long actualStartOffset = charactersStartOffset;
0331       if(thisChunkStartsWithWS) {
0332         actualStartOffset = fixStartOffsetForWhitespace(actualStartOffset);
0333       }
0334       int extractedPos = tmpDocContentSize;
0335       if(incrementStartIndexextractedPos++;
0336       addRepositioningInfo(contentBuffer.length()(int)actualStartOffset,
0337               extractedPos);
0338     // if
0339 
0340     // calculate the End index for all the elements of the stack
0341     // the expression is : End index = Current doc length + text length
0342     Long end = new Long(tmpDocContent.length());
0343 
0344     CustomObject obj = null;
0345     // Iterate through stack to modify the End index of the existing
0346     // elements
0347 
0348     java.util.Iterator<CustomObject> anIterator = stack.iterator();
0349     while(anIterator.hasNext()) {
0350       // get the object and move to the next one
0351       obj = anIterator.next();
0352       if(incrementStartIndex && obj.getStart().equals(obj.getEnd())) {
0353         obj.setStart(new Long(obj.getStart().longValue() 1));
0354       }// End if
0355       // sets its End index
0356       obj.setEnd(end);
0357     }// End while
0358     
0359     // remember whether this chunk ended with whitespace for next time
0360     previousChunkEndedWithWS = trailingWhitespace;
0361   }
0362 
0363   /**
0364    * Called when the parser encounters the end of an element.
0365    */
0366   @Override
0367   public void endElement(QName element, Augmentations augsthrows XNIException {
0368     endElement(element, augs, false);
0369   }
0370 
0371   /**
0372    * Called to signal an empty element. This simply synthesizes a
0373    * startElement followed by an endElement event.
0374    */
0375   @Override
0376   public void emptyElement(QName element, XMLAttributes attributes,
0377           Augmentations augsthrows XNIException {
0378     this.startElement(element, attributes, augs);
0379     this.endElement(element, augs, true);
0380   }
0381 
0382   /**
0383    * Called when the parser encounters the end of an HTML element.
0384    */
0385   public void endElement(QName element, Augmentations augs,
0386           boolean wasEmptyElementthrows XNIException {
0387     charactersAction();
0388 
0389     // localName = localName.toLowerCase();
0390     if(DEBUG_ELEMENTS) {
0391       Out.println("endElement: " + element.localpart + " (was "
0392               (wasEmptyElement ? "" "not ""empty)");
0393     }
0394 
0395     // obj is for internal use
0396     CustomObject obj = null;
0397 
0398     // end of ignorable tag
0399     if(ignorableTags.contains(element.localpart)) {
0400       ignorableTagLevels--;
0401       if(DEBUG_ELEMENTS) {
0402         Out.println("  end of ignorable tag.  levels = " + ignorableTagLevels);
0403       }
0404     // if
0405 
0406     // If the stack is not empty then we get the object from the stack
0407     if(!stack.isEmpty()) {
0408       obj = stack.pop();
0409       // Before adding it to the colector, we need to check if is an
0410       // emptyAndSpan one. See CustomObject's isEmptyAndSpan field.
0411       // We only set isEmptyAndSpan if this endElement was NOT generated
0412       // from an empty element in the HTML.
0413       if(obj.getStart().equals(obj.getEnd()) && !wasEmptyElement) {
0414         // The element had an end tag and its start was equal to its
0415         // end. Hence it is anEmptyAndSpan one.
0416         obj.getFM().put("isEmptyAndSpan""true");
0417       }// End iff
0418       // we add it to the colector
0419       colector.add(obj);
0420     }// End if
0421 
0422     // If element has text between, then customize its apearance
0423     if(obj != null && obj.getStart().longValue() != obj.getEnd().longValue())
0424     // Customize the appearance of the document
0425       customizeAppearanceOfDocumentWithEndTag(element.localpart);
0426   }
0427 
0428   /**
0429    * Called when the parser reaches the end of the document. Here we
0430    * store the new content and construct the Original markups
0431    * annotations.
0432    */
0433   @Override
0434   public void endDocument(Augmentations augsthrows XNIException {
0435     if(DEBUG_GENERAL) {
0436       Out.println("endDocument");
0437     }
0438     CustomObject obj = null;
0439     // replace the old content with the new one
0440     doc.setContent(new DocumentContentImpl(tmpDocContent.toString()));
0441 
0442     // If basicAs is null then get the default annotation
0443     // set from this gate document
0444     if(basicAS == null)
0445       basicAS = doc
0446               .getAnnotations(GateConstants.ORIGINAL_MARKUPS_ANNOT_SET_NAME);
0447 
0448     // sort colector ascending on its id
0449     Collections.sort(colector);
0450     // iterate through colector and construct annotations
0451     while(!colector.isEmpty()) {
0452       obj = colector.getFirst();
0453       colector.remove(obj);
0454       // Construct an annotation from this obj
0455       try {
0456         basicAS.add(obj.getStart(), obj.getEnd(), obj.getElemName(), obj
0457                 .getFM());
0458       }
0459       catch(InvalidOffsetException e) {
0460         Err.prln("Error creating an annot :" + obj + " Discarded...");
0461       }// end try
0462       // }// end if
0463     }// while
0464 
0465     // notify the listener about the total amount of elements that
0466     // has been processed
0467     fireStatusChangedEvent("Total elements : " + elements);
0468   }
0469 
0470   /**
0471    * Non-fatal error, print the stack trace but continue processing.
0472    */
0473   @Override
0474   public void error(String domain, String key, XMLParseException e) {
0475     e.printStackTrace(Err.getPrintWriter());
0476   }
0477 
0478   @Override
0479   public void fatalError(String domain, String key, XMLParseException e)
0480           throws XNIException {
0481     throw e;
0482   }
0483 
0484   // we don't do anything with processing instructions, comments or CDATA
0485   // markers, but if we encounter them they interrupt the flow of text.  Thus
0486   // we must call charactersAction so the repositioning info is correctly
0487   // generated.
0488 
0489   @Override
0490   public void processingInstruction(String target, XMLString data,
0491           Augmentations augsthrows XNIException {
0492     charactersAction();
0493   }
0494 
0495   @Override
0496   public void comment(XMLString content,
0497           Augmentations augsthrows XNIException {
0498     charactersAction();
0499   }
0500 
0501   @Override
0502   public void startCDATA(Augmentations augsthrows XNIException {
0503     charactersAction();
0504   }
0505 
0506   @Override
0507   public void endCDATA(Augmentations augsthrows XNIException {
0508     charactersAction();
0509   }
0510 
0511 
0512   /**
0513    * A comparator that compares two RepositioningInfo.PositionInfo
0514    * records by their originalPosition values. It also supports either
0515    * or both argument being a Long, in which case the Long value is used
0516    * directly. This allows you to binarySearch for an offset rather than
0517    * having to construct a PositionInfo record with the target value.
0518    */
0519   private static final Comparator<Object> POSITION_INFO_COMPARATOR = new Comparator<Object>() {
0520     @Override
0521     public int compare(Object a, Object b) {
0522       Long offA = null;
0523       if(instanceof Long) {
0524         offA = (Long)a;
0525       }
0526       else if(instanceof RepositioningInfo.PositionInfo) {
0527         offA = ((RepositioningInfo.PositionInfo)a).getOriginalPosition();
0528       }
0529 
0530       Long offB = null;
0531       if(instanceof Long) {
0532         offB = (Long)b;
0533       }
0534       else if(instanceof RepositioningInfo.PositionInfo) {
0535         offB = ((RepositioningInfo.PositionInfo)a).getOriginalPosition();
0536       }
0537 
0538       return offA.compareTo(offB);
0539     }
0540   };
0541 
0542   /**
0543    * Correct for whitespace. Given the offset of the start of a block of
0544    * whitespace in the original content, this method calculates the
0545    * offset of the first following non-whitespace character. If wsOffset
0546    * points to the start of a run of whitespace then there will be a
0547    * PositionInfo record in the ampCodingInfo that represents this run
0548    * of whitespace, from which we can find the end of the run. If there
0549    * is no PositionInfo record for this offset then it must point to a
0550    * single whitespace character, so we simply return wsOffset+1.
0551    */
0552   private long fixStartOffsetForWhitespace(long wsOffset) {
0553     // see whether we have a repositioning record in ampCodingInfo for
0554     // the whitespace starting at wsOffset
0555     int wsPosInfoIndex = Collections.binarySearch(ampCodingInfo, wsOffset,
0556             POSITION_INFO_COMPARATOR);
0557 
0558     // if we don't find a repos record it means that the whitespace
0559     // really is a single space in the original content
0560     if(wsPosInfoIndex < 0) {
0561       return wsOffset + 1;
0562     }
0563     // if there is a repos record we move by the record's originalLength
0564     else {
0565       return wsOffset
0566               + ampCodingInfo.get(wsPosInfoIndex).getOriginalLength();
0567     }
0568   }
0569 
0570   /**
0571    * For given content the list with shrink position information is
0572    * searched and on the corresponding positions the correct
0573    * repositioning information is calculated and generated.
0574    */
0575   public void addRepositioningInfo(int contentLength, int pos, int extractedPos) {
0576     // wrong way (without correction and analysing)
0577     // reposInfo.addPositionInfo(pos, contentLength, extractedPos,
0578     // contentLength);
0579 
0580     RepositioningInfo.PositionInfo pi = null;
0581     long startPos = pos;
0582     long correction = 0;
0583     long substituteStart;
0584     long remainingLen;
0585     long offsetInExtracted;
0586 
0587     for(int i = 0; i < ampCodingInfo.size(); ++i) {
0588       pi = ampCodingInfo.get(i);
0589       substituteStart = pi.getOriginalPosition();
0590 
0591       if(substituteStart >= startPos) {
0592         if(substituteStart > pos + contentLength + correction) {
0593           break// outside the current text
0594         // if
0595 
0596         // should create two repositioning information records
0597         remainingLen = substituteStart - (startPos + correction);
0598         offsetInExtracted = startPos - pos;
0599         if(remainingLen > 0) {
0600           reposInfo.addPositionInfo(startPos + correction, remainingLen,
0601                   extractedPos + offsetInExtracted, remainingLen);
0602         // if
0603         // record for shrank text
0604         reposInfo.addPositionInfo(substituteStart, pi.getOriginalLength(),
0605                 extractedPos + offsetInExtracted + remainingLen, pi
0606                         .getCurrentLength());
0607         startPos = startPos + remainingLen + pi.getCurrentLength();
0608         correction += pi.getOriginalLength() - pi.getCurrentLength();
0609       // if
0610     // for
0611 
0612     // there is some text remaining for repositioning
0613     offsetInExtracted = startPos - pos;
0614     remainingLen = contentLength - offsetInExtracted;
0615     if(remainingLen > 0) {
0616       reposInfo.addPositionInfo(startPos + correction, remainingLen,
0617               extractedPos + offsetInExtracted, remainingLen);
0618     // if
0619   // addRepositioningInfo
0620 
0621   /**
0622    * This method analizes the tag t and adds some \n chars and spaces to
0623    * the tmpDocContent.The reason behind is that we need to have a
0624    * readable form for the final document. This method modifies the
0625    * content of tmpDocContent.
0626    
0627    @param tagName the Html tag encounted by the HTML parser
0628    */
0629   protected void customizeAppearanceOfDocumentWithStartTag(String tagName) {
0630     boolean modification = false;
0631     int tmpDocContentSize = tmpDocContent.length();
0632     if("p".equals(tagName)) {
0633       if(tmpDocContentSize >= 2
0634               && '\n' != tmpDocContent.charAt(tmpDocContentSize - 2)) {
0635         tmpDocContent.append("\n");
0636         modification = true;
0637       }
0638     }// End if
0639     // if the HTML tag is BR then we add a new line character to the
0640     // document
0641     if("br".equals(tagName)) {
0642       tmpDocContent.append("\n");
0643       modification = true;
0644     }// End if
0645 
0646     // only add a newline at the start of a div if there isn't already a
0647     // newline induced by something else
0648     if("div".equals(tagName&& tmpDocContentSize > 0
0649             && tmpDocContent.charAt(tmpDocContentSize - 1!= '\n') {
0650       tmpDocContent.append("\n");
0651       modification = true;
0652     }
0653 
0654     if(modification == true) {
0655       Long end = new Long(tmpDocContent.length());
0656       java.util.Iterator<CustomObject> anIterator = stack.iterator();
0657       while(anIterator.hasNext()) {
0658         // get the object and move to the next one, and set its end
0659         // index
0660         anIterator.next().setEnd(end);
0661       }// End while
0662     }// End if
0663   }// customizeAppearanceOfDocumentWithStartTag
0664 
0665   /**
0666    * This method analizes the tag t and adds some \n chars and spaces to
0667    * the tmpDocContent.The reason behind is that we need to have a
0668    * readable form for the final document. This method modifies the
0669    * content of tmpDocContent.
0670    
0671    @param tagName the Html tag encounted by the HTML parser
0672    */
0673   protected void customizeAppearanceOfDocumentWithEndTag(String tagName) {
0674     boolean modification = false;
0675     // if the HTML tag is BR then we add a new line character to the
0676     // document
0677     if(("p".equals(tagName)) || ("h1".equals(tagName))
0678             || ("h2".equals(tagName)) || ("h3".equals(tagName))
0679             || ("h4".equals(tagName)) || ("h5".equals(tagName))
0680             || ("h6".equals(tagName)) || ("tr".equals(tagName))
0681             || ("center".equals(tagName)) || ("li".equals(tagName))) {
0682       tmpDocContent.append("\n");
0683       modification = true;
0684     }
0685     // only add a newline at the end of a div if there isn't already a
0686     // newline induced by something else
0687     if("div".equals(tagName&& tmpDocContent.length() 0
0688             && tmpDocContent.charAt(tmpDocContent.length() 1!= '\n') {
0689       tmpDocContent.append("\n");
0690       modification = true;
0691     }
0692 
0693     if("title".equals(tagName)) {
0694       tmpDocContent.append("\n\n");
0695       modification = true;
0696     }// End if
0697 
0698     if(modification == true) {
0699       Long end = new Long(tmpDocContent.length());
0700       Iterator<CustomObject> anIterator = stack.iterator();
0701       while(anIterator.hasNext()) {
0702         // get the object and move to the next one
0703         CustomObject obj = anIterator.next();
0704         // sets its End index
0705         obj.setEnd(end);
0706       }// End while
0707     }// End if
0708   }// customizeAppearanceOfDocumentWithEndTag
0709 
0710   /** Keep the refference to this structure */
0711   private RepositioningInfo reposInfo = null;
0712 
0713   /** Keep the refference to this structure */
0714   private RepositioningInfo ampCodingInfo = null;
0715 
0716   /**
0717    * Set repositioning information structure refference. If you set this
0718    * refference to <B>null</B> information wouldn't be collected.
0719    */
0720   public void setRepositioningInfo(RepositioningInfo info) {
0721     reposInfo = info;
0722   // setRepositioningInfo
0723 
0724   /** Return current RepositioningInfo object */
0725   public RepositioningInfo getRepositioningInfo() {
0726     return reposInfo;
0727   // getRepositioningInfo
0728 
0729   /**
0730    * Set repositioning information structure refference for ampersand
0731    * coding. If you set this refference to <B>null</B> information
0732    * wouldn't be used.
0733    */
0734   public void setAmpCodingInfo(RepositioningInfo info) {
0735     ampCodingInfo = info;
0736   // setRepositioningInfo
0737 
0738   /** Return current RepositioningInfo object for ampersand coding. */
0739   public RepositioningInfo getAmpCodingInfo() {
0740     return ampCodingInfo;
0741   // getRepositioningInfo
0742 
0743   /**
0744    * The HTML tag names (lower case) whose text content should be
0745    * ignored completely by this handler. Typically this is just script
0746    * and style tags.
0747    */
0748   private Set<String> ignorableTags = null;
0749 
0750   /**
0751    * Set the set of tag names whose text content will be ignored.
0752    
0753    @param newTags a set of lower-case tag names
0754    */
0755   public void setIgnorableTags(Set<String> newTags) {
0756     ignorableTags = newTags;
0757   }
0758 
0759   /**
0760    * Get the set of tag names whose content is ignored by this handler.
0761    */
0762   public Set<String> getIgnorableTags() {
0763     return ignorableTags;
0764   }
0765 
0766   // HtmlDocumentHandler member data
0767 
0768   // counter for the number of levels of ignorable tag we are inside.
0769   // For example, if we configured "ul" as an ignorable tag name then
0770   // this variable would have the following values:
0771   //
0772   // 0: <p>
0773   // 0: This is some text
0774   // 1: <ul>
0775   // 1: <li>
0776   // 1: some more text
0777   // 2: <ul> ...
0778   // 1: </ul>
0779   // 1: </li>
0780   // 0: </ul>
0781   //
0782   // this allows us to support nested ignorables
0783   int ignorableTagLevels = 0;
0784 
0785   // this constant indicates when to fire the status listener
0786   // this listener will add an overhead and we don't want a big overhead
0787   // this listener will be callled from ELEMENTS_RATE to ELEMENTS_RATE
0788   final static int ELEMENTS_RATE = 128;
0789 
0790   /**
0791    * Array holding the character offset of the start of each line in the
0792    * document.
0793    */
0794   private int[] lineOffsets;
0795 
0796   // the content of the HTML document, without any tag
0797   // for internal use
0798   private StringBuilder tmpDocContent = null;
0799 
0800   /**
0801    * This is used to capture all data within two tags before calling the
0802    * actual characters method
0803    */
0804   private StringBuilder contentBuffer = new StringBuilder("");
0805 
0806   /** This is a variable that shows if characters have been read */
0807   private boolean readCharacterStatus = false;
0808 
0809   /**
0810    * The start offset of the current block of character content.
0811    */
0812   private int charactersStartOffset;
0813 
0814   // a stack used to remember elements and to keep the order
0815   private java.util.Stack<CustomObject> stack = null;
0816 
0817   // a gate document
0818   private gate.Document doc = null;
0819 
0820   // an annotation set used for creating annotation reffering the doc
0821   private gate.AnnotationSet basicAS;
0822 
0823   // listeners for status report
0824   protected List<StatusListener> myStatusListeners = new LinkedList<StatusListener>();
0825 
0826   // this reports the the number of elements that have beed processed so
0827   // far
0828   private int elements = 0;
0829 
0830   protected int customObjectsId = 0;
0831 
0832   public int getCustomObjectsId() {
0833     return customObjectsId;
0834   }
0835 
0836   // we need a colection to retain all the CustomObjects that will be
0837   // transformed into annotation over the gate document...
0838   // the transformation will take place inside onDocumentEnd() method
0839   private LinkedList<CustomObject> colector = null;
0840   
0841   /**
0842    * Initialised from the user config, stores whether to add extra space
0843    * characters to separate words that would otherwise be run together,
0844    * e.g. "...foo&lt;/td&gt;&lt;td&gt;bar...".  If true, this becomes
0845    * "foo bar", if false it is "foobar".
0846    */
0847   protected boolean addSpaceOnUnpack = true;
0848   
0849   /**
0850    * During parsing, keeps track of whether the previous chunk of
0851    * character data ended with a whitespace character.
0852    */
0853   protected boolean previousChunkEndedWithWS = false;
0854 
0855   // Inner class
0856   /**
0857    * The objects belonging to this class are used inside the stack. This
0858    * class is for internal needs
0859    */
0860   class CustomObject implements Comparable<CustomObject> {
0861 
0862     // constructor
0863     public CustomObject(String anElemName, FeatureMap aFm, Long aStart,
0864             Long anEnd) {
0865       elemName = anElemName;
0866       fm = aFm;
0867       start = aStart;
0868       end = anEnd;
0869       id = new Long(customObjectsId++);
0870     }// End CustomObject()
0871 
0872     // Methos implemented as required by Comparable interface
0873     @Override
0874     public int compareTo(CustomObject obj) {
0875       return this.id.compareTo(obj.getId());
0876     }// compareTo();
0877 
0878     // accesor
0879     public String getElemName() {
0880       return elemName;
0881     }// getElemName()
0882 
0883     public FeatureMap getFM() {
0884       return fm;
0885     }// getFM()
0886 
0887     public Long getStart() {
0888       return start;
0889     }// getStart()
0890 
0891     public Long getEnd() {
0892       return end;
0893     }// getEnd()
0894 
0895     public Long getId() {
0896       return id;
0897     }
0898 
0899     // mutator
0900     public void setElemName(String anElemName) {
0901       elemName = anElemName;
0902     }// getElemName()
0903 
0904     public void setFM(FeatureMap aFm) {
0905       fm = aFm;
0906     }// setFM();
0907 
0908     public void setStart(Long aStart) {
0909       start = aStart;
0910     }// setStart();
0911 
0912     public void setEnd(Long anEnd) {
0913       end = anEnd;
0914     }// setEnd();
0915 
0916     // data fields
0917     private String elemName = null;
0918 
0919     private FeatureMap fm = null;
0920 
0921     private Long start = null;
0922 
0923     private Long end = null;
0924 
0925     private Long id = null;
0926 
0927   // End inner class CustomObject
0928 
0929   // StatusReporter Implementation
0930 
0931   public void addStatusListener(StatusListener listener) {
0932     myStatusListeners.add(listener);
0933   }
0934 
0935   public void removeStatusListener(StatusListener listener) {
0936     myStatusListeners.remove(listener);
0937   }
0938 
0939   protected void fireStatusChangedEvent(String text) {
0940     Iterator<StatusListener> listenersIter = myStatusListeners.iterator();
0941     while(listenersIter.hasNext())
0942       listenersIter.next().statusChanged(text);
0943   }
0944 
0945   // //// Unused methods from XNI interfaces //////
0946 
0947   @Override
0948   public void doctypeDecl(String arg0, String arg1, String arg2,
0949           Augmentations arg3throws XNIException {
0950     if(DEBUG_UNUSED) {
0951       Out.println("doctypeDecl");
0952     }
0953   }
0954 
0955   @Override
0956   public void endGeneralEntity(String arg0, Augmentations arg1)
0957           throws XNIException {
0958     if(DEBUG_UNUSED) {
0959       Out.println("endGeneralEntity");
0960     }
0961   }
0962 
0963   @Override
0964   public XMLDocumentSource getDocumentSource() {
0965     if(DEBUG_UNUSED) {
0966       Out.println("getDocumentSource");
0967     }
0968     return null;
0969   }
0970 
0971   @Override
0972   public void ignorableWhitespace(XMLString arg0, Augmentations arg1)
0973           throws XNIException {
0974     if(DEBUG_UNUSED) {
0975       Out.println("ignorableWhitespace: " + arg0);
0976     }
0977   }
0978 
0979   @Override
0980   public void setDocumentSource(XMLDocumentSource arg0) {
0981     if(DEBUG_UNUSED) {
0982       Out.println("setDocumentSource");
0983     }
0984   }
0985 
0986   @Override
0987   public void startDocument(XMLLocator arg0, String arg1,
0988           NamespaceContext arg2, Augmentations arg3throws XNIException {
0989     if(DEBUG_UNUSED) {
0990       Out.println("startDocument");
0991     }
0992   }
0993 
0994   @Override
0995   public void startGeneralEntity(String arg0, XMLResourceIdentifier arg1,
0996           String arg2, Augmentations arg3throws XNIException {
0997     if(DEBUG_UNUSED) {
0998       Out.println("startGeneralEntity");
0999     }
1000   }
1001 
1002   @Override
1003   public void textDecl(String arg0, String arg1, Augmentations arg2)
1004           throws XNIException {
1005     if(DEBUG_UNUSED) {
1006       Out.println("textDecl");
1007     }
1008   }
1009 
1010   @Override
1011   public void xmlDecl(String arg0, String arg1, String arg2, Augmentations arg3)
1012           throws XNIException {
1013     if(DEBUG_UNUSED) {
1014       Out.println("xmlDecl");
1015     }
1016   }
1017 
1018   @Override
1019   public void warning(String arg0, String arg1, XMLParseException arg2)
1020           throws XNIException {
1021     if(DEBUG_GENERAL) {
1022       Out.println("warning:");
1023       arg2.printStackTrace(Err.getPrintWriter());
1024     }
1025   }
1026 
1027 }