RegexSentenceSplitter.java
001 /*
002  *  Copyright (c) 1995-2012, The University of Sheffield. See the file
003  *  COPYRIGHT.txt in the software or at http://gate.ac.uk/gate/COPYRIGHT.txt
004  *
005  *  This file is part of GATE (see http://gate.ac.uk/), and is free
006  *  software, licenced under the GNU Library General Public License,
007  *  Version 2, June 1991 (in the distribution as file licence.html,
008  *  and also available at http://gate.ac.uk/gate/licence.html).
009  *
010  *  Valentin Tablan, 04 Sep 2007
011  *
012  *  $Id: RegexSentenceSplitter.java 17595 2014-03-08 13:05:32Z markagreenwood $
013  */
014 package gate.creole.splitter;
015 
016 import java.io.*;
017 import java.net.URL;
018 import java.util.*;
019 import java.util.regex.*;
020 
021 import org.apache.commons.io.IOUtils;
022 
023 import gate.*;
024 import gate.creole.*;
025 import gate.creole.metadata.CreoleParameter;
026 import gate.creole.metadata.CreoleResource;
027 import gate.creole.metadata.Optional;
028 import gate.creole.metadata.RunTime;
029 import gate.util.*;
030 
031 /**
032  * A fast sentence splitter replacement based on regular expressions.
033  */
034 @CreoleResource(name="RegEx Sentence Splitter", icon="sentence-splitter", comment="A sentence splitter based on regular expressions.", helpURL="http://gate.ac.uk/userguide/sec:annie:regex-splitter")
035 public class RegexSentenceSplitter extends AbstractLanguageAnalyser {
036 
037   /**
038    * Parameter name
039    */
040   public static final String SPLIT_DOCUMENT_PARAMETER_NAME = "document";
041 
042   /**
043    * Parameter name
044    */
045   public static final String SPLIT_INPUT_AS_PARAMETER_NAME = "inputASName";
046 
047   /**
048    * Parameter name
049    */
050   public static final String SPLIT_OUTPUT_AS_PARAMETER_NAME = "outputASName";
051 
052   /**
053    * Parameter name
054    */
055   public static final String SPLIT_ENCODING_PARAMETER_NAME = "encoding";
056 
057   /**
058    * Parameter name
059    */
060   public static final String SPLIT_SPLIT_LIST_PARAMETER_NAME = "splitListURL";
061 
062 
063   /**
064    * Parameter name
065    */
066   public static final String SPLIT_NON_SPLIT_LIST_PARAMETER_NAME = "nonSplitListURL";
067 
068   /**
069    * serialisation ID
070    */
071   private static final long serialVersionUID = 1L;
072 
073   /**
074    * Output annotation set name.
075    */
076   protected String outputASName;
077 
078   /**
079    * Encoding used when reading config files
080    */
081   protected String encoding;
082 
083   /**
084    * URL pointing to a file with regex patterns for internal sentence splits.
085    */
086   protected URL internalSplitListURL;
087 
088   /**
089    * URL pointing to a file with regex patterns for external sentence splits.
090    */
091   protected URL externalSplitListURL;
092 
093   /**
094    * URL pointing to a file with regex patterns for non sentence splits.
095    */
096   protected URL nonSplitListURL;
097 
098 
099   protected Pattern internalSplitsPattern;
100 
101   protected Pattern externalSplitsPattern;
102 
103   protected Pattern nonSplitsPattern;
104 
105   protected Pattern compilePattern(URL paternsListUrl, String encoding)
106           throws UnsupportedEncodingException, IOException {
107     BufferedReader reader = null;
108     StringBuffer patternString = new StringBuffer();
109     
110     try {
111       reader =
112               new BomStrippingInputStreamReader(paternsListUrl.openStream(),
113                       encoding);
114       
115       String line = reader.readLine();
116       while(line != null) {
117         line = line.trim();
118 
119         if(line.length() == || line.startsWith("//")) {
120           // ignore empty lines and comments
121         else {
122           if(patternString.length() 0patternString.append("|");
123           patternString.append("(?:" + line + ")");
124         }
125         // move to next line
126         line = reader.readLine();
127       }
128     finally {
129       IOUtils.closeQuietly(reader);
130     }
131     return Pattern.compile(patternString.toString());
132   }
133 
134 
135 //  protected enum StartEnd {START, END};
136 
137   /**
138    * A comparator for MatchResult objects. This is used to find the next match
139    * result in a text. A null value is used to signify that no more matches are
140    * available, hence nulls are the largest value, according to this comparator.
141    @author Valentin Tablan (valyt)
142    */
143   private class MatchResultComparator implements Comparator<MatchResult>{
144 
145     /* (non-Javadoc)
146      * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
147      */
148     @Override
149     public int compare(MatchResult o1, MatchResult o2) {
150       if(o1 == null && o2 == nullreturn 0;
151       if(o1 == nullreturn 1;
152       if(o2 == nullreturn -1;
153       //at this point both match results are not null
154       return o1.start() - o2.start();
155     }
156   }
157 
158   @Override
159   public void execute() throws ExecutionException {
160     interrupted = false;
161     int lastProgress = 0;
162     fireProgressChanged(lastProgress);
163     //get pointers to the annotation sets
164     AnnotationSet outputAS = (outputASName == null ||
165             outputASName.trim().length() == 0?
166                              document.getAnnotations() :
167                              document.getAnnotations(outputASName);
168 
169     String docText = document.getContent().toString();
170 
171     /* If the document's content is empty or contains only whitespace,
172      * we drop out right here, since there's nothing to sentence-split.     */
173     if (docText.trim().length() 1)  {
174       return;
175     }
176 
177     Matcher internalSplitMatcher = internalSplitsPattern.matcher(docText);
178     Matcher externalSplitMatcher = externalSplitsPattern.matcher(docText);
179 
180     Matcher nonSplitMatcher = nonSplitsPattern.matcher(docText);
181     //store all non split locations in a list of pairs
182     List<int[]> nonSplits = new LinkedList<int[]>();
183     while(nonSplitMatcher.find()){
184       nonSplits.add(new int[]{nonSplitMatcher.start(), nonSplitMatcher.end()});
185     }
186     //this lists holds the next matches at each step
187     List<MatchResult> nextSplitMatches = new ArrayList<MatchResult>();
188     //initialise matching process
189     MatchResult internalMatchResult = null;
190     if(internalSplitMatcher.find()){
191       internalMatchResult = internalSplitMatcher.toMatchResult();
192       nextSplitMatches.add(internalMatchResult);
193     }
194     MatchResult externalMatchResult = null;
195     if(externalSplitMatcher.find()){
196       externalMatchResult = externalSplitMatcher.toMatchResult();
197       nextSplitMatches.add(externalMatchResult);
198     }
199     MatchResultComparator comparator = new MatchResultComparator();
200     int lastSentenceEnd = 0;
201 
202     while(!nextSplitMatches.isEmpty()){
203       //see which one matches first
204       Collections.sort(nextSplitMatches, comparator);
205       MatchResult nextMatch = nextSplitMatches.remove(0);
206       if(nextMatch == internalMatchResult){
207         //we have a new internal split; see if it's vetoed or not
208         if(!veto(nextMatch, nonSplits)){
209           //split is not vetoed
210           try {
211             //add the split annotation
212             FeatureMap features = Factory.newFeatureMap();
213             features.put("kind""internal");
214             outputAS.add(new Long(nextMatch.start())new Long(nextMatch.end()),
215                     "Split", features);
216             //generate the sentence annotation
217             int endOffset = nextMatch.end();
218             //find the first non whitespace character starting from where the
219             //last sentence ended
220             while(lastSentenceEnd < endOffset &&
221                   Character.isWhitespace(
222                           Character.codePointAt(docText, lastSentenceEnd))){
223               lastSentenceEnd++;
224             }
225             //if there is any useful text between the two offsets, generate
226             //a new sentence
227             if(lastSentenceEnd < nextMatch.start()){
228               outputAS.add(new Long(lastSentenceEnd)new Long(endOffset),
229                       ANNIEConstants.SENTENCE_ANNOTATION_TYPE,
230                       Factory.newFeatureMap());
231             }
232             //store the new sentence end
233             lastSentenceEnd = endOffset;
234           catch(InvalidOffsetException e) {
235             // this should never happen
236             throw new ExecutionException(e);
237           }
238         }
239         //prepare for next step
240         if(internalSplitMatcher.find()){
241           internalMatchResult = internalSplitMatcher.toMatchResult();
242           nextSplitMatches.add(internalMatchResult);
243         }else{
244           internalMatchResult = null;
245         }
246       }else if(nextMatch == externalMatchResult){
247         //we have a new external split; see if it's vetoed or not
248         if(!veto(nextMatch, nonSplits)){
249           //split is not vetoed
250           try {
251             //generate the split
252             FeatureMap features = Factory.newFeatureMap();
253             features.put("kind""external");
254             outputAS.add(new Long(nextMatch.start())new Long(nextMatch.end()),
255                     "Split", features);
256             //generate the sentence annotation
257             //find the last non whitespace character, going backward from
258             //where the external skip starts
259             int endOffset = nextMatch.start();
260             while(endOffset > lastSentenceEnd &&
261                     Character.isSpaceChar(
262                             Character.codePointAt(docText, endOffset -1))){
263               endOffset--;
264             }
265             //find the first non whitespace character starting from where the
266             //last sentence ended
267             while(lastSentenceEnd < endOffset &&
268                     Character.isSpaceChar(
269                             Character.codePointAt(docText, lastSentenceEnd))){
270               lastSentenceEnd++;
271             }
272             //if there is any useful text between the two offsets, generate
273             //a new sentence
274             if(lastSentenceEnd < endOffset){
275               outputAS.add(new Long(lastSentenceEnd)new Long(endOffset),
276                       ANNIEConstants.SENTENCE_ANNOTATION_TYPE,
277                       Factory.newFeatureMap());
278             }
279             //store the new sentence end
280             lastSentenceEnd = nextMatch.end();
281           catch(InvalidOffsetException e) {
282             // this should never happen
283             throw new ExecutionException(e);
284           }
285         }
286         //prepare for next step
287         if(externalSplitMatcher.find()){
288           externalMatchResult = externalSplitMatcher.toMatchResult();
289           nextSplitMatches.add(externalMatchResult);
290         }else{
291           externalMatchResult = null;
292         }
293       }else{
294         //malfunction
295         throw new ExecutionException("Invalid state - cannot identify match!");
296       }
297       //report progress
298       int newProgress = 100 * lastSentenceEnd / docText.length();
299       if(newProgress - lastProgress > 20){
300         lastProgress = newProgress;
301         fireProgressChanged(lastProgress);
302       }
303     }//while(!nextMatches.isEmpty()){
304     fireProcessFinished();
305   }
306 
307 
308   /**
309    * Checks whether a possible match is being vetoed by a non split match. A
310    * possible match is vetoed if it any nay overlap with a veto region.
311    *
312    @param split the match result representing the split to be tested
313    @param vetoRegions regions where matches are not allowed. For efficiency
314    * reasons, this method assumes these regions to be non overlapping and sorted
315    * in ascending order.
316    * All veto regions that end before the proposed match are also discarded
317    * (again for efficiency reasons). This requires the proposed matches to be
318    * sent to this method in ascending order, so as to avoid malfunctions.
319    @return <tt>true</tt> iff the proposed split should be ignored
320    */
321   private boolean veto(MatchResult split, List<int[]> vetoRegions){
322     //if no more non splits available, accept everything
323     for(Iterator<int[]> vetoRegIter = vetoRegions.iterator();
324         vetoRegIter.hasNext();){
325       int[] aVetoRegion = vetoRegIter.next();
326       if(aVetoRegion[1-< split.start()){
327         //current veto region ends before the proposed split starts
328         //--> discard the veto region
329         vetoRegIter.remove();
330       }else if(split.end() -< aVetoRegion[0]){
331         //veto region starts after the split ends
332         //-> we can return false
333         return false;
334       }else{
335         //we have overlap
336         return true;
337       }
338     }
339     //if we got this far, all veto regions are before the split
340     return false;
341   }
342 
343   @Override
344   public Resource init() throws ResourceInstantiationException {
345     super.init();
346     try {
347       //sanity checks
348       if(internalSplitListURL == null)
349         throw new ResourceInstantiationException("No list of internal splits provided!");
350       if(externalSplitListURL == null)
351         throw new ResourceInstantiationException("No list of external splits provided!");
352       if(nonSplitListURL == null)
353         throw new ResourceInstantiationException("No list of non splits provided!");
354       if(encoding == null)
355         throw new ResourceInstantiationException("No encoding provided!");
356 
357       //load the known abbreviations list
358       internalSplitsPattern = compilePattern(internalSplitListURL, encoding);
359       externalSplitsPattern = compilePattern(externalSplitListURL, encoding);
360       nonSplitsPattern = compilePattern(nonSplitListURL, encoding);
361     catch(UnsupportedEncodingException e) {
362       throw new ResourceInstantiationException(e);
363     catch(IOException e) {
364       throw new ResourceInstantiationException(e);
365     }
366 
367     return this;
368   }
369 
370   /**
371    @return the outputASName
372    */
373   public String getOutputASName() {
374     return outputASName;
375   }
376 
377   /**
378    @param outputASName the outputASName to set
379    */
380   @RunTime
381   @Optional
382   @CreoleParameter(comment="The annotation set to be used as output for 'Sentence' and 'Split' annotations")
383   public void setOutputASName(String outputASName) {
384     this.outputASName = outputASName;
385   }
386 
387   /**
388    @return the encoding
389    */
390   public String getEncoding() {
391     return encoding;
392   }
393 
394   /**
395    @param encoding the encoding to set
396    */
397   @CreoleParameter(comment="The encoding used for reading the definition files", defaultValue="UTF-8")
398   public void setEncoding(String encoding) {
399     this.encoding = encoding;
400   }
401 
402   /**
403    @return the internalSplitListURL
404    */
405   public URL getInternalSplitListURL() {
406     return internalSplitListURL;
407   }
408 
409   /**
410    @param internalSplitListURL the internalSplitListURL to set
411    */
412   @CreoleParameter(defaultValue="resources/regex-splitter/internal-split-patterns.txt", suffixes="txt", comment="The URL to the internal splits pattern list")
413   public void setInternalSplitListURL(URL internalSplitListURL) {
414     this.internalSplitListURL = internalSplitListURL;
415   }
416 
417   /**
418    @return the externalSplitListURL
419    */
420   public URL getExternalSplitListURL() {
421     return externalSplitListURL;
422   }
423 
424   /**
425    @param externalSplitListURL the externalSplitListURL to set
426    */
427   @CreoleParameter(defaultValue="resources/regex-splitter/external-split-patterns.txt", comment="The URL to the external splits pattern list", suffixes="txt")
428   public void setExternalSplitListURL(URL externalSplitListURL) {
429     this.externalSplitListURL = externalSplitListURL;
430   }
431 
432   /**
433    @return the nonSplitListURL
434    */
435   public URL getNonSplitListURL() {
436     return nonSplitListURL;
437   }
438 
439   /**
440    @param nonSplitListURL the nonSplitListURL to set
441    */
442   @CreoleParameter(defaultValue="resources/regex-splitter/non-split-patterns.txt", comment="The URL to the non splits pattern list", suffixes="txt")
443   public void setNonSplitListURL(URL nonSplitListURL) {
444     this.nonSplitListURL = nonSplitListURL;
445   }
446 
447   /**
448    @return the internalSplitsPattern
449    */
450   public Pattern getInternalSplitsPattern() {
451     return internalSplitsPattern;
452   }
453 
454   /**
455    @param internalSplitsPattern the internalSplitsPattern to set
456    */
457   public void setInternalSplitsPattern(Pattern internalSplitsPattern) {
458     this.internalSplitsPattern = internalSplitsPattern;
459   }
460 }