FlexibleGazetteer.java
001 /*
002  * FlexibleGazetteer.java
003  
004  * Copyright (c) 2004-2012, The University of Sheffield.
005  
006  * This file is part of GATE (see http://gate.ac.uk/), and is free software,
007  * licenced under the GNU Library General Public License, Version 2, June1991.
008  
009  * A copy of this licence is included in the distribution in the file
010  * licence.html, and is also available at http://gate.ac.uk/gate/licence.html.
011  
012  * Niraj Aswani 02/2002
013  * $Id: FlexibleGazetteer.java 17530 2014-03-04 15:57:43Z markagreenwood $
014  */
015 package gate.creole.gazetteer;
016 
017 import gate.Annotation;
018 import gate.AnnotationSet;
019 import gate.Document;
020 import gate.Factory;
021 import gate.FeatureMap;
022 import gate.Gate;
023 import gate.ProcessingResource;
024 import gate.Resource;
025 import gate.Utils;
026 import gate.corpora.DocumentImpl;
027 import gate.creole.AbstractLanguageAnalyser;
028 import gate.creole.ExecutionException;
029 import gate.creole.ResourceInstantiationException;
030 import gate.util.InvalidOffsetException;
031 import java.util.List;
032 
033 
034 /**
035  <p>
036  * Title: Flexible Gazetteer
037  </p>
038  <p>
039  * The Flexible Gazetteer provides users with the flexibility to choose
040  * their own customised input and an external Gazetteer. For example,
041  * the user might want to replace words in the text with their base
042  * forms (which is an output of the Morphological Analyser).
043  </p>
044  <p>
045  * The Flexible Gazetteer performs lookup over a document based on the
046  * values of an arbitrary feature of an arbitrary annotation type, by
047  * using an externally provided gazetteer. It is important to use an
048  * external gazetteer as this allows the use of any type of gazetteer
049  * (e.g. an Ontological gazetteer).
050  </p>
051  
052  @author niraj aswani
053  @version 1.0
054  */
055 public class FlexibleGazetteer extends AbstractLanguageAnalyser 
056   implements ProcessingResource {
057   
058   private static final long serialVersionUID = -1023682327651886920L;
059   private static final String wrappedOutputASName = "Output";
060   private static final String wrappedInputASName = "Input";
061   
062   // SET TO false BEFORE CHECKING IN
063   private static final boolean DEBUG = false;
064 
065   /**
066    * Does the actual loading and parsing of the lists. This method must be
067    * called before the gazetteer can be used
068    */
069   @Override
070   public Resource init() throws ResourceInstantiationException {
071     if(gazetteerInst == null) { throw new ResourceInstantiationException(
072         "No Gazetteer Provided!")}
073     return this;
074   }
075 
076   /**
077    * This method runs the gazetteer. It assumes that all the needed parameters
078    * are set. If they are not, an exception will be fired.
079    */
080   @Override
081   public void execute() throws ExecutionException {
082     fireProgressChanged(0);
083     fireStatusChanged("Checking Document...");
084     if(document == null) { throw new ExecutionException(
085         "No document to process!")}
086     // obtain the inputAS
087     AnnotationSet inputAS = document.getAnnotations(inputASName);
088     // anything in the inputFeatureNames?
089     if(inputFeatureNames == null || inputFeatureNames.size() == 0) { throw new ExecutionException(
090         "No input feature names provided!")}
091     // for each input feature, create a temporary document and run the
092     // gazetteer
093     for(String aFeature : inputFeatureNames) {
094       // find out the feature name user wants us to use
095       String[] keyVal = aFeature.split("\\.");
096       // if invalid feature name
097       if(keyVal.length != 2) {
098         System.err.println("Invalid input feature name:" + aFeature);
099         continue;
100       }
101       // keyVal[0] = annotation type
102       // keyVal[1] = feature name
103       // holds mapping for newly created annotations
104       FlexGazMappingTable mappingTable = new FlexGazMappingTable();
105       fireStatusChanged("Creating temporary Document for feature " + aFeature);
106       StringBuilder newdocString =
107           new StringBuilder(document.getContent().toString());
108       // sort annotations
109       List<Annotation> annotations =
110           Utils.inDocumentOrder(inputAS.get(keyVal[0]));
111 
112       // remove duplicate annotations
113       // (this makes the reverse mapping much easier)
114       removeOverlappingAnnotations(annotations);
115       // initially no space is deducted
116       int totalDeductedSpaces = 0;
117       // now replace the document content with the value of the feature that
118       // user has provided
119       for(Annotation currentAnnotation : annotations) {
120         // if there's no such feature, continue
121         if(!currentAnnotation.getFeatures().containsKey(keyVal[1])) continue;
122         String newTokenValue =
123             currentAnnotation.getFeatures().get(keyVal[1]).toString();
124         // if no value found for this feature
125         if(newTokenValue == nullcontinue;
126         // feature value found so we need to replace it
127         // find the start and end offsets for this token
128         long startOffset = Utils.start(currentAnnotation);
129         long endOffset = Utils.end(currentAnnotation);
130         // let us find the difference between the lengths of the
131         // actual string and the newTokenValue
132         long actualLength = endOffset - startOffset;
133         long lengthDifference = actualLength - newTokenValue.length();
134         // so lets find out the new startOffset and endOffset
135         long newStartOffset = startOffset - totalDeductedSpaces;
136         long newEndOffset = newStartOffset + newTokenValue.length();
137         totalDeductedSpaces += lengthDifference;
138 
139         mappingTable.add(startOffset, endOffset, newStartOffset, newEndOffset);
140         
141         // and finally replace the actual string in the document
142         // with the new document
143         newdocString.replace((int)newStartOffset, (int)newStartOffset
144             (int)actualLength, newTokenValue);
145       }
146 
147       // proceed only if there was any replacement Map
148       if(mappingTable.isEmpty()) continue;
149       
150       /* All the binary search stuff is done inside FlexGazMappingTable
151        * now, so it's guaranteed to return valid original annotation start
152        * and end offsets.       */
153       
154       // otherwise create a temporary document for the new text
155       Document tempDoc = null;
156       // update the status
157       fireStatusChanged("Processing document with Gazetteer...");
158       try {
159         FeatureMap params = Factory.newFeatureMap();
160         params.put("stringContent", newdocString.toString());
161         // set the appropriate encoding
162         if(document instanceof DocumentImpl) {
163           params.put("encoding"((DocumentImpl)document).getEncoding());
164           params.put("markupAware"((DocumentImpl)document).getMarkupAware());
165         }
166         FeatureMap features = Factory.newFeatureMap();
167         Gate.setHiddenAttribute(features, true);
168         tempDoc =
169             (Document)Factory.createResource("gate.corpora.DocumentImpl",
170                 params, features);
171 
172         /* Mark the temp document with the locations of the input annotations so
173          * that we can later eliminate Lookups that are out of scope.       */
174         for (NodePosition mapping : mappingTable.getMappings()) {
175           tempDoc.getAnnotations(wrappedInputASName).add(mapping.getTempStartOffset()
176               mapping.getTempEndOffset()"Input", Factory.newFeatureMap());
177         }
178       
179       catch(ResourceInstantiationException rie) {
180         throw new ExecutionException("Temporary document cannot be created", rie);
181       
182       catch(InvalidOffsetException e) {
183         throw new ExecutionException("Error duplicating Input annotations", e);
184       }
185       try {
186         // lets create the gazetteer based on the provided gazetteer name
187         gazetteerInst.setDocument(tempDoc);
188         gazetteerInst.setAnnotationSetName(wrappedOutputASName);
189         fireStatusChanged("Executing Gazetteer...");
190         gazetteerInst.execute();
191         // now the tempDoc has been looked up, we need to shift the annotations
192         // from this temp document to the original document
193         fireStatusChanged("Transfering new annotations to the original one...");
194         AnnotationSet originalDocOutput = document.getAnnotations(outputASName);
195         
196         if (DEBUG) {
197           mappingTable.dump();
198         }
199         
200         // Now iterate over the new annotations and transfer them from the 
201         // temp document back to the real one
202         for(Annotation currentLookup : tempDoc.getAnnotations(wrappedOutputASName)) {
203           long tempStartOffset = Utils.start(currentLookup);
204           long tempEndOffset = Utils.end(currentLookup);
205 
206           /* Ignore annotations that fall entirely outside the input annotations,
207            * so that we don't get dodgy Lookups outside the area covered by
208            * Tokens copied into a restricted working set by the AST PR
209            * (for example)           */
210           if (coveredByInput(tempStartOffset, tempEndOffset, tempDoc.getAnnotations(wrappedInputASName)))  {
211             long destinationStart = mappingTable.getBestOriginalStart(tempStartOffset);
212             long destinationEnd = mappingTable.getBestOriginalEnd(tempEndOffset);
213 
214             boolean valid = (destinationStart >= 0&& (destinationEnd >= 0);  
215 
216             if (valid) {
217               // Now make sure there is no other annotation like this
218               AnnotationSet testSet = originalDocOutput.getContained(destinationStart, destinationEnd).get(
219                   currentLookup.getType(), currentLookup.getFeatures());
220               for(Annotation annot : testSet) {
221                 if(Utils.start(annot== destinationStart
222                     && Utils.end(annot== destinationEnd
223                     && annot.getFeatures().size() == currentLookup.getFeatures().size()) {
224                   valid = false;
225                   break;
226                 }
227               }
228             }
229             
230             if(valid) {
231               addToOriginal(originalDocOutput, destinationStart, destinationEnd, 
232                   tempStartOffset, tempEndOffset, currentLookup, tempDoc);
233             }
234           // END if coveredByInput(...)
235         // END for OVER ALL THE Lookups
236       
237       finally {
238         gazetteerInst.setDocument(null);
239         if(tempDoc != null) {
240           // now remove the newDoc
241           Factory.deleteResource(tempDoc);
242         }
243       }
244     // for
245     fireProcessFinished();
246   // END execute METHOD
247 
248   
249   /**
250    * Removes the overlapping annotations. preserves the one that appears first
251    * in the list.  This assumes the list has been sorted already.
252    
253    @param annotations
254    */
255   private void removeOverlappingAnnotations(List<Annotation> annotations) {
256     for(int i = 0; i < annotations.size() 1; i++) {
257       Annotation annot1 = annotations.get(i);
258       Annotation annot2 = annotations.get(i + 1);
259       long annot2Start = Utils.start(annot2);
260       if(annot2Start >= Utils.start(annot1&& annot2Start < Utils.end(annot1)) {
261         annotations.remove(annot2);
262         i--;
263         continue;
264       }
265     }
266   }
267 
268   
269   /* We try hard not to cause InvalidOffsetExceptions, but let's have
270    * some better debugging info in case they happen.
271    */
272   private void addToOriginal(AnnotationSet original, long originalStart, long originalEnd, 
273       long tempStart, long tempEnd, Annotation tempLookup, Document tempDocthrows ExecutionException {
274     try {
275       original.add(originalStart, originalEnd, tempLookup.getType(), tempLookup.getFeatures());
276     }
277     catch(InvalidOffsetException ioe) {
278       String errorDetails = String.format("temp %d, %d [%s]-> original %d, %d  ", tempStart, tempEnd, Utils.stringFor(tempDoc, tempLookup)
279           originalStart, originalEnd);
280       throw new ExecutionException(errorDetails, ioe);
281     }
282   }
283 
284   
285   
286   /* Is this Lookup within the scope of the input annotations?  It might not be, if Token annotations
287    * have been copied by AST only over the significant sections of the document.
288    */
289   private boolean coveredByInput(long tempStart, long tempEnd, AnnotationSet tempInputAS) {
290     if (tempInputAS.getCovering(wrappedInputASName, tempStart, tempStart).isEmpty()) {
291       return false;
292     }
293     // implied else
294     if (tempInputAS.getCovering(wrappedInputASName, tempEnd, tempEnd).isEmpty()) {
295       return false;
296     }
297     // implied else
298     return true;
299   }
300 
301   
302   /**
303    * Sets the document to work on
304    
305    @param doc
306    */
307   @Override
308   public void setDocument(gate.Document doc) {
309     this.document = doc;
310   }
311 
312   /**
313    * Returns the document set up by user to work on
314    
315    @return {@link Document}
316    */
317   @Override
318   public gate.Document getDocument() {
319     return this.document;
320   }
321 
322   /**
323    * Sets the name of annotation set that should be used for storing new
324    * annotations
325    
326    @param outputASName
327    */
328   public void setOutputASName(String outputASName) {
329     this.outputASName = outputASName;
330   }
331 
332   /**
333    * Returns the outputAnnotationSetName
334    
335    @return {@link String} value.
336    */
337   public String getOutputASName() {
338     return this.outputASName;
339   }
340 
341   /**
342    * sets the input AnnotationSet Name
343    
344    @param inputASName
345    */
346   public void setInputASName(String inputASName) {
347     this.inputASName = inputASName;
348   }
349 
350   /**
351    * Returns the inputAnnotationSetName
352    
353    @return {@link String} value.
354    */
355   public String getInputASName() {
356     return this.inputASName;
357   }
358 
359   /**
360    * Feature names for example: Token.string, Token.root etc... Values of these
361    * features should be used to replace the actual string of these features.
362    * This method allows a user to set the name of such features
363    
364    @param inputs
365    */
366   public void setInputFeatureNames(java.util.List<String> inputs) {
367     this.inputFeatureNames = inputs;
368   }
369 
370   /**
371    * Returns the feature names that are provided by the user to use their values
372    * to replace their actual strings in the document
373    
374    @return {@link List} value.
375    */
376   public java.util.List<String> getInputFeatureNames() {
377     return this.inputFeatureNames;
378   }
379 
380   public Gazetteer getGazetteerInst() {
381     return this.gazetteerInst;
382   }
383 
384   public void setGazetteerInst(gate.creole.gazetteer.Gazetteer gazetteerInst) {
385     this.gazetteerInst = gazetteerInst;
386   }
387 
388   // Gazetteer Runtime parameters
389   private gate.Document document;
390 
391   private java.lang.String outputASName;
392 
393   private java.lang.String inputASName;
394 
395   // Flexible Gazetteer parameter
396   private Gazetteer gazetteerInst;
397 
398   private java.util.List<String> inputFeatureNames;
399 }