NekoHtmlDocumentFormat.java
001 /*
002  *  NekoHtmlDocumentFormat.java
003  *
004  *  Copyright (c) 2006, The University of Sheffield.
005  *
006  *  This file is part of GATE (see http://gate.ac.uk/), and is free
007  *  software, licenced under the GNU Library General Public License,
008  *  Version 2, June 1991 (in the distribution as file licence.html,
009  *  and also available at http://gate.ac.uk/gate/licence.html).
010  *
011  *  Ian Roberts, 17/Dec/2006
012  *
013  *  $Id: NekoHtmlDocumentFormat.java 17864 2014-04-18 07:12:27Z markagreenwood $
014  */
015 
016 package gate.corpora;
017 
018 import gate.Document;
019 import gate.GateConstants;
020 import gate.Resource;
021 import gate.TextualDocument;
022 import gate.creole.ResourceInstantiationException;
023 import gate.creole.metadata.AutoInstance;
024 import gate.creole.metadata.CreoleParameter;
025 import gate.creole.metadata.CreoleResource;
026 import gate.event.StatusListener;
027 import gate.html.NekoHtmlDocumentHandler;
028 import gate.util.DocumentFormatException;
029 import gate.util.Out;
030 
031 import java.io.IOException;
032 import java.io.InputStream;
033 import java.io.InputStreamReader;
034 import java.io.Reader;
035 import java.io.StringReader;
036 import java.net.URLConnection;
037 import java.util.Set;
038 import java.util.regex.Matcher;
039 import java.util.regex.Pattern;
040 import java.util.zip.GZIPInputStream;
041 
042 import org.apache.xerces.xni.parser.XMLInputSource;
043 import org.cyberneko.html.HTMLConfiguration;
044 
045 /**
046  <p>
047  * DocumentFormat that uses Andy Clark's <a
048  * href="http://people.apache.org/~andyc/neko/doc/html/">NekoHTML</a>
049  * parser to parse HTML documents. It tries to render HTML in a similar
050  * way to a web browser, i.e. whitespace is normalized, paragraphs are
051  * separated by a blank line, etc. By default the text content of style
052  * and script tags is ignored completely, though the set of tags treated
053  * in this way is configurable via a CREOLE parameter.
054  </p>
055  */
056 @CreoleResource(name = "GATE HTML Document Format", isPrivate = true,
057     autoinstances = {@AutoInstance(hidden = true)})
058 public class NekoHtmlDocumentFormat extends TextualDocumentFormat {
059  
060   private static final long serialVersionUID = -3163147687966075651L;
061  
062   /** Debug flag */
063   private static final boolean DEBUG = false;
064 
065   /** Default construction */
066   public NekoHtmlDocumentFormat() {
067     super();
068   }
069 
070   /**
071    * The set of tags whose text content is to be ignored when parsing.
072    */
073   private Set<String> ignorableTags = null;
074 
075   @CreoleParameter(comment = "HTML tags whose text content should be ignored",
076       defaultValue = "script;style")
077   public void setIgnorableTags(Set<String> newTags) {
078     this.ignorableTags = newTags;
079   }
080 
081   public Set<String> getIgnorableTags() {
082     return ignorableTags;
083   }
084 
085   /**
086    * We support repositioning info for HTML files.
087    */
088   @Override
089   public Boolean supportsRepositioning() {
090     return Boolean.TRUE;
091   }
092 
093   /**
094    * Old-style unpackMarkup, without repositioning info.
095    */
096   @Override
097   public void unpackMarkup(Document docthrows DocumentFormatException {
098     unpackMarkup(doc, null, null);
099   }
100 
101   /**
102    * Unpack the markup in the document. This converts markup from the
103    * native format into annotations in GATE format. If the document was
104    * created from a String, then is recomandable to set the doc's
105    * sourceUrl to <b>null</b>. So, if the document has a valid URL,
106    * then the parser will try to parse the XML document pointed by the
107    * URL.If the URL is not valid, or is null, then the doc's content
108    * will be parsed. If the doc's content is not a valid XML then the
109    * parser might crash.
110    *
111    @param doc The gate document you want to parse. If
112    *          <code>doc.getSourceUrl()</code> returns <b>null</b>
113    *          then the content of doc will be parsed. Using a URL is
114    *          recomended because the parser will report errors corectlly
115    *          if the document is not well formed.
116    */
117   @Override
118   public void unpackMarkup(Document doc, RepositioningInfo repInfo,
119           RepositioningInfo ampCodingInfothrows DocumentFormatException {
120     if((doc == null)
121             || (doc.getSourceUrl() == null && doc.getContent() == null)) {
122 
123       throw new DocumentFormatException(
124               "GATE document is null or no content found. Nothing to parse!");
125     }// End if
126 
127     // Create a status listener
128     StatusListener statusListener = new StatusListener() {
129       @Override
130       public void statusChanged(String text) {
131         // This is implemented in DocumentFormat.java and inherited here
132         fireStatusChanged(text);
133       }
134     };
135 
136     boolean docHasContentButNoValidURL = hasContentButNoValidUrl(doc);
137 
138     NekoHtmlDocumentHandler handler = null;
139     try {
140       org.cyberneko.html.HTMLConfiguration parser = new HTMLConfiguration();
141 
142       // convert element and attribute names to lower case
143       parser.setProperty("http://cyberneko.org/html/properties/names/elems",
144               "lower");
145       parser.setProperty("http://cyberneko.org/html/properties/names/attrs",
146               "lower");
147       // make parser augment infoset with location information
148       parser.setFeature(NekoHtmlDocumentHandler.AUGMENTATIONS, true);
149 
150       // Create a new Xml document handler
151       handler = new NekoHtmlDocumentHandler(doc, null, ignorableTags);
152       // Register a status listener with it
153       handler.addStatusListener(statusListener);
154       // set repositioning object
155       handler.setRepositioningInfo(repInfo);
156       // set the object with ampersand coding positions
157       handler.setAmpCodingInfo(ampCodingInfo);
158       // construct the list of offsets for each line of the document
159       int[] lineOffsets = buildLineOffsets(doc.getContent().toString());
160       handler.setLineOffsets(lineOffsets);
161 
162       // set the handlers
163       parser.setDocumentHandler(handler);
164       parser.setErrorHandler(handler);
165 
166       // Parse the XML Document with the appropriate encoding
167       XMLInputSource is;
168 
169       if(docHasContentButNoValidURL) {
170         // no URL, so parse from string
171         is =
172                 new XMLInputSource(null, null, null, new StringReader(doc
173                         .getContent().toString())null);
174       }
175       else if(doc instanceof TextualDocument) {
176         // textual document - load with user specified encoding
177         String docEncoding = ((TextualDocument)doc).getEncoding();
178         // XML, so no BOM stripping.
179         
180         URLConnection conn = doc.getSourceUrl().openConnection();
181         InputStream uStream = conn.getInputStream();
182                 
183         if ("gzip".equals(conn.getContentEncoding())) {
184           uStream = new GZIPInputStream(uStream);
185         }
186         
187         Reader docReader =
188                 new InputStreamReader(uStream,
189                         docEncoding);
190         is =
191                 new XMLInputSource(null, doc.getSourceUrl().toString(), doc
192                         .getSourceUrl().toString(), docReader, docEncoding);
193 
194         // since we control the encoding, tell the parser to ignore any
195         // meta http-equiv hints
196         parser
197                 .setFeature(
198                         "http://cyberneko.org/html/features/scanner/ignore-specified-charset",
199                         true);
200       }
201       else {
202         // let the parser decide the encoding
203         is =
204                 new XMLInputSource(null, doc.getSourceUrl().toString(), doc
205                         .getSourceUrl().toString());
206       }
207 
208       /* The following line can forward an
209        * ArrayIndexOutOfBoundsException from
210        * org.cyberneko.html.HTMLConfiguration.parse and crash GATE.    */
211       parser.parse(is);
212       // Angel - end
213       ((DocumentImpl)doc).setNextAnnotationId(handler.getCustomObjectsId());
214     }
215 
216     /* Handle IOException specially.      */
217     catch(IOException e) {
218       throw new DocumentFormatException("I/O exception for "
219               + doc.getSourceUrl().toString(), e);
220     }
221 
222     /* Handle XNIException and ArrayIndexOutOfBoundsException:
223      * flag the parsing error and keep going.     */
224     catch(Exception e) {
225       doc.getFeatures().put("parsingError", Boolean.TRUE);
226 
227       Boolean bThrow =
228               (Boolean)doc.getFeatures().get(
229                       GateConstants.THROWEX_FORMAT_PROPERTY_NAME);
230 
231       if(bThrow != null && bThrow.booleanValue()) {
232         // the next line is commented to avoid Document creation fail on
233         // error
234         throw new DocumentFormatException(e);
235       }
236       else {
237         Out.println("Warning: Document remains unparsed. \n"
238                 "\n  Stack Dump: ");
239         e.printStackTrace(Out.getPrintWriter());
240       // if
241 
242     }
243     finally {
244       if(handler != nullhandler.removeStatusListener(statusListener);
245     }// End if else try
246 
247   }
248 
249   /**
250    * Pattern that matches the beginning of every line in a multi-line
251    * string. The regular expression engine handles the different types
252    * of newline characters (\n, \r\n or \r) automatically.
253    */
254   private static Pattern afterNewlinePattern =
255           Pattern.compile("^", Pattern.MULTILINE);
256 
257   /**
258    * Build an array giving the starting character offset of each line in
259    * the document. The HTML parser only reports event positions as line
260    * and column numbers, so we need this information to be able to
261    * correctly infer the repositioning information.
262    */
263   private int[] buildLineOffsets(String docContent) {
264     Matcher m = afterNewlinePattern.matcher(docContent);
265     // we have to scan the text twice, first to determine how many lines
266     // there are (i.e. how long the array needs to be)...
267     int numMatches = 0;
268     while(m.find()) {
269       if(DEBUG) {
270         System.out.println("found line starting at offset " + m.start());
271       }
272       numMatches++;
273     }
274 
275     int[] lineOffsets = new int[numMatches];
276 
277     // ... and then again to populate the array with values.
278     m.reset();
279     for(int i = 0; i < lineOffsets.length; i++) {
280       m.find();
281       lineOffsets[i= m.start();
282     }
283 
284     return lineOffsets;
285   }
286 
287   /** Initialise this resource, and return it. */
288   @Override
289   public Resource init() throws ResourceInstantiationException {
290     // Register HTML mime type
291     MimeType mime = new MimeType("text""html");
292     // Register the class handler for this mime type
293     mimeString2ClassHandlerMap.put(mime.getType() "/" + mime.getSubtype(),
294             this);
295     // Register the mime type with mine string
296     mimeString2mimeTypeMap.put(mime.getType() "/" + mime.getSubtype(), mime);
297     // sometimes XHTML file appear as application/xhtml+xml
298     mimeString2mimeTypeMap.put("application/xhtml+xml", mime);
299     // Register file sufixes for this mime type
300     suffixes2mimeTypeMap.put("html", mime);
301     suffixes2mimeTypeMap.put("htm", mime);
302     // Register magic numbers for this mime type
303     magic2mimeTypeMap.put("<html", mime);
304     // Set the mimeType for this language resource
305     setMimeType(mime);
306     return this;
307   }// init()
308 
309 }// class XmlDocumentFormat