DefaultGazetteer.java
001 /*
002  * DefaultGazeteer.java
003  *
004  * Copyright (c) 1998-2005, 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, June1991.
009  *
010  * A copy of this licence is included in the distribution in the file
011  * licence.html, and is also available at http://gate.ac.uk/gate/licence.html.
012  *
013  * Valentin Tablan, 03/07/2000
014  * borislav popov 24/03/2002
015  *
016  * $Id: DefaultGazetteer.java 18829 2015-07-26 08:31:03Z markagreenwood $
017  */
018 package gate.creole.gazetteer;
019 
020 import gate.AnnotationSet;
021 import gate.Factory;
022 import gate.FeatureMap;
023 import gate.Resource;
024 import gate.Utils;
025 import gate.creole.CustomDuplication;
026 import gate.creole.ExecutionException;
027 import gate.creole.ExecutionInterruptedException;
028 import gate.creole.ResourceInstantiationException;
029 import gate.creole.metadata.CreoleParameter;
030 import gate.creole.metadata.CreoleResource;
031 import gate.creole.metadata.Optional;
032 import gate.util.GateRuntimeException;
033 import gate.util.InvalidOffsetException;
034 import gate.util.Strings;
035 
036 import java.io.Serializable;
037 import java.util.Arrays;
038 import java.util.HashSet;
039 import java.util.Iterator;
040 import java.util.Map;
041 import java.util.Set;
042 
043 /** This component is responsible for doing lists lookup. The implementation is
044  * based on finite state machines.
045  * The phrases to be recognised should be listed in a set of files, one for
046  * each type of occurrences.
047  * The gazetteer is build with the information from a file that contains the set
048  * of lists (which are files as well) and the associated type for each list.
049  * The file defining the set of lists should have the following syntax:
050  * each list definition should be written on its own line and should contain:
051  <ol>
052  <li>the file name (required) </li>
053  <li>the major type (required) </li>
054  <li>the minor type (optional)</li>
055  <li>the language(s) (optional) </li>
056  </ol>
057  * The elements of each definition are separated by &quot;:&quot;.
058  * The following is an example of a valid definition: <br>
059  <code>personmale.lst:person:male:english</code>
060  * Each list file named in the lists definition file is just a list containing
061  * one entry per line.
062  * When this gazetteer will be run over some input text (a Gate document) it
063  * will generate annotations of type Lookup having the attributes specified in
064  * the definition file.
065  */
066 @CreoleResource(name="ANNIE Gazetteer", comment="A list lookup component.", icon="gazetteer", helpURL="http://gate.ac.uk/userguide/sec:annie:gazetteer")
067 public class DefaultGazetteer extends AbstractGazetteer
068                               implements CustomDuplication {
069 
070   private static final long serialVersionUID = -8976141132455436099L;
071 
072   public static final String
073     DEF_GAZ_DOCUMENT_PARAMETER_NAME = "document";
074 
075   public static final String
076     DEF_GAZ_ANNOT_SET_PARAMETER_NAME = "annotationSetName";
077 
078   public static final String
079     DEF_GAZ_LISTS_URL_PARAMETER_NAME = "listsURL";
080 
081   public static final String
082     DEF_GAZ_ENCODING_PARAMETER_NAME = "encoding";
083 
084   public static final String
085     DEF_GAZ_CASE_SENSITIVE_PARAMETER_NAME = "caseSensitive";
086   
087   public static final String
088     DEF_GAZ_LONGEST_MATCH_ONLY_PARAMETER_NAME = "longestMatchOnly";
089   
090   public static final String
091     DEF_GAZ_FEATURE_SEPARATOR_PARAMETER_NAME = "gazetteerFeatureSeparator";
092 
093   /** The separator used for gazetteer entry features */
094   protected String gazetteerFeatureSeparator;
095   
096   /** a map of nodes vs gaz lists */
097   protected Map<LinearNode,GazetteerList> listsByNode;
098 
099   /** 
100    * Build a gazetteer using the default lists from the gate resources
101    */
102   public DefaultGazetteer(){
103   }
104   
105   /** Does the actual loading and parsing of the lists. This method must be
106    * called before the gazetteer can be used
107    */
108   @Override
109   public Resource init()throws ResourceInstantiationException{
110     fsmStates = new HashSet<FSMState>();
111     initialState = new FSMState(this);
112     if(listsURL == null){
113       throw new ResourceInstantiationException (
114             "No URL provided for gazetteer creation!");
115     }
116     definition = new LinearDefinition();
117     definition.setSeparator(Strings.unescape(gazetteerFeatureSeparator));
118     definition.setURL(listsURL);
119     definition.load();
120     int linesCnt = definition.size();
121     listsByNode = definition.loadLists();
122     Iterator<LinearNode> inodes = definition.iterator();
123 
124     int nodeIdx = 0;
125     LinearNode node;
126     while (inodes.hasNext()) {
127       node = inodes.next();
128       fireStatusChanged("Reading " + node.toString());
129       fireProgressChanged(++nodeIdx * 100 / linesCnt);
130       readList(node,true);
131     // while iline
132     fireProcessFinished();
133     return this;
134   }
135 
136 
137   /** Reads one lists (one file) of phrases
138    *
139    @param node the node
140    @param add if <b>true</b> will add the phrases found in the list to the ones
141    *     recognised by this gazetteer, if <b>false</b> the phrases found in the
142    *     list will be removed from the list of phrases recognised by this
143    *     gazetteer.
144    */
145    protected void readList(LinearNode node, boolean add
146        throws ResourceInstantiationException{
147     String listName, majorType, minorType, languages,annotationType;
148     if null == node ) {
149       throw new ResourceInstantiationException(" LinearNode node is null ");
150     }
151 
152     listName = node.getList();
153     majorType = node.getMajorType();
154     minorType = node.getMinorType();
155     languages = node.getLanguage();
156     annotationType = node.getAnnotationType();
157     GazetteerList gazList = listsByNode.get(node);
158     if (null == gazList) {
159       throw new ResourceInstantiationException("gazetteer list not found by node");
160     }
161 
162     Iterator<GazetteerNode> iline = gazList.iterator();
163     
164     // create default lookup for entries with no arbitrary features
165     Lookup defaultLookup = new Lookup(listName,majorType, minorType, languages,annotationType);
166     defaultLookup.list = node.getList();
167     if null != mappingDefinition){
168       MappingNode mnode = mappingDefinition.getNodeByList(defaultLookup.list);
169       if (null!=mnode){
170         defaultLookup.oClass = mnode.getClassID();
171         defaultLookup.ontology = mnode.getOntologyID();
172       }
173     }//if mapping def
174     
175     Lookup lookup;
176     String entry; // the actual gazetteer entry text
177     while(iline.hasNext()){
178       GazetteerNode gazNode = iline.next();
179       entry = gazNode.getEntry();
180       
181       Map<String,Object> features = gazNode.getFeatureMap();
182       if (features == null) {
183         lookup = defaultLookup;
184       else {
185         // create a new Lookup object with features
186         lookup = new Lookup(listName, majorType, minorType, languages,annotationType);
187         lookup.list = node.getList();
188         if(null != mappingDefinition) {
189           MappingNode mnode = mappingDefinition.getNodeByList(lookup.list);
190           if(null != mnode) {
191             lookup.oClass = mnode.getClassID();
192             lookup.ontology = mnode.getOntologyID();
193           }
194         }// if mapping def
195         lookup.features = features;
196       
197       
198       if(add)addLookup(entry, lookup);
199       else removeLookup(entry, lookup);
200     }
201   // void readList(String listDesc)
202 
203   /** Adds one phrase to the list of phrases recognised by this gazetteer
204    *
205    @param text the phrase to be added
206    @param lookup the description of the annotation to be added when this
207    *     phrase is recognised
208    */
209   public void addLookup(String text, Lookup lookup) {
210     char currentChar;
211     FSMState currentState = initialState;
212     FSMState nextState;
213     boolean isSpace;
214 
215     for(int i = 0; i< text.length(); i++) {
216         currentChar = text.charAt(i);
217         isSpace = Character.isSpaceChar(currentChar|| Character.isWhitespace(currentChar);
218         if(isSpacecurrentChar = ' ';
219         else currentChar = (caseSensitive.booleanValue()) ?
220                           currentChar :
221                           Character.toUpperCase(currentChar;
222       nextState = currentState.next(currentChar);
223       if(nextState == null){
224         nextState = new FSMState(this);
225         currentState.put(currentChar, nextState);
226         if(isSpacenextState.put(' ',nextState);
227       }
228       currentState = nextState;
229     //for(int i = 0; i< text.length(); i++)
230 
231     currentState.addLookup(lookup);
232     //Out.println(text + "|" + lookup.majorType + "|" + lookup.minorType);
233 
234   // addLookup
235 
236   /** Removes one phrase to the list of phrases recognised by this gazetteer
237    *
238    @param text the phrase to be removed
239    @param lookup the description of the annotation associated to this phrase
240    */
241   public void removeLookup(String text, Lookup lookup) {
242     char currentChar;
243     FSMState currentState = initialState;
244     FSMState nextState;
245 
246     for(int i = 0; i< text.length(); i++) {
247         currentChar = text.charAt(i);
248         if Character.isSpaceChar(currentChar|| Character.isWhitespace(currentChar) ) currentChar = ' ';
249         if (!caseSensitivecurrentChar = Character.toUpperCase(currentChar);
250         nextState = currentState.next(currentChar);
251         if(nextState == nullreturn;//nothing to remove
252         currentState = nextState;
253     //for(int i = 0; i< text.length(); i++)
254     currentState.removeLookup(lookup);
255   // removeLookup
256 
257   /** Returns a string representation of the deterministic FSM graph using
258    * GML.
259    */
260   public String getFSMgml() {
261     String res = "graph[ \ndirected 1\n";
262     StringBuffer nodes = new StringBuffer(gate.Gate.STRINGBUFFER_SIZE),
263                 edges = new StringBuffer(gate.Gate.STRINGBUFFER_SIZE);
264     Iterator<FSMState> fsmStatesIter = fsmStates.iterator();
265     while (fsmStatesIter.hasNext()){
266       FSMState currentState = fsmStatesIter.next();
267       int stateIndex = currentState.getIndex();
268       nodes.append("node[ id ");
269       nodes.append(stateIndex);
270       nodes.append(" label \"");
271       nodes.append(stateIndex);
272 
273              if(currentState.isFinal()){
274               nodes.append(",F\\n");
275               nodes.append(currentState.getLookupSet());
276              }
277              nodes.append("\"  ]\n");
278       edges.append(currentState.getEdgesGML());
279     }
280     res += nodes.toString() + edges.toString() "]\n";
281     return res;
282   // getFSMgml
283 
284 
285   /**
286    * Tests whether a character is internal to a word (i.e. if it's a letter or
287    * a combining mark (spacing or not)).
288    @param ch the character to be tested
289    @return a boolean value
290    */
291   public static boolean isWordInternal(char ch){
292     return Character.isLetter(ch||
293            Character.getType(ch== Character.COMBINING_SPACING_MARK ||
294            Character.getType(ch== Character.NON_SPACING_MARK;
295   }
296 
297   /**
298    * This method runs the gazetteer. It assumes that all the needed parameters
299    * are set. If they are not, an exception will be fired.
300    */
301   @Override
302   public void execute() throws ExecutionException{
303     interrupted = false;
304     AnnotationSet annotationSet;
305     //check the input
306     if(document == null) {
307       throw new ExecutionException(
308         "No document to process!"
309       );
310     }
311 
312     if(annotationSetName == null ||
313        annotationSetName.equals("")) annotationSet = document.getAnnotations();
314     else annotationSet = document.getAnnotations(annotationSetName);
315 
316     fireStatusChanged("Performing look-up in " + document.getName() "...");
317     String content = document.getContent().toString();
318     int length = content.length();
319     char currentChar;
320     FSMState currentState = initialState;
321     FSMState nextState;
322     FSMState lastMatchingState = null;
323     int matchedRegionEnd = 0;
324     int matchedRegionStart = 0;
325     int charIdx = 0;
326     int oldCharIdx = 0;
327 
328     while(charIdx < length) {
329       currentChar = content.charAt(charIdx);
330       ifCharacter.isSpaceChar(currentChar|| Character.isWhitespace(currentChar) ) currentChar = ' ';
331       else currentChar = caseSensitive.booleanValue() ?
332                           currentChar :
333                           Character.toUpperCase(currentChar);
334       nextState = currentState.next(currentChar);
335       if(nextState == null) {
336         //the matching stopped
337         //if we had a successful match then act on it;
338         if(lastMatchingState != null){
339           createLookups(lastMatchingState, matchedRegionStart, matchedRegionEnd, 
340                   annotationSet);
341           lastMatchingState = null;
342         }
343         //reset the FSM
344         charIdx = matchedRegionStart + 1;
345         matchedRegionStart = charIdx;
346         currentState = initialState;
347       else{//go on with the matching
348         currentState = nextState;
349         //if we have a successful state then store it
350         if(currentState.isFinal() &&
351            (
352             (!wholeWordsOnly.booleanValue())
353              ||
354             ((matchedRegionStart == ||
355              !isWordInternal(content.charAt(matchedRegionStart - 1)))
356              &&
357              (charIdx + >= content.length()   ||
358              !isWordInternal(content.charAt(charIdx + 1)))
359             )
360            )
361           ){
362           //we have a new match
363           //if we had an existing match and we need to annotate prefixes, then 
364           //apply it
365           if(!longestMatchOnly && lastMatchingState != null){
366             createLookups(lastMatchingState, matchedRegionStart, 
367                     matchedRegionEnd, annotationSet);
368           }
369           matchedRegionEnd = charIdx;
370           lastMatchingState = currentState;
371         }
372         charIdx ++;
373         if(charIdx == content.length()){
374           //we can't go on, use the last matching state and restart matching
375           //from the next char
376           if(lastMatchingState != null){
377             //let's add the new annotation(s)
378             createLookups(lastMatchingState, matchedRegionStart, 
379                     matchedRegionEnd, annotationSet);
380             lastMatchingState = null;
381           }
382           //reset the FSM
383           charIdx = matchedRegionStart + 1;
384           matchedRegionStart = charIdx;
385           currentState = initialState;
386         }
387       }
388       //fire the progress event
389       if(charIdx - oldCharIdx > 256) {
390         fireProgressChanged((100 * charIdx )/ length );
391         oldCharIdx = charIdx;
392         if(isInterrupted()) throw new ExecutionInterruptedException(
393             "The execution of the " + getName() +
394             " gazetteer has been abruptly interrupted!");
395       }
396     // while(charIdx < length)
397     //we've finished. If we had a stored match, then apply it.
398     if(lastMatchingState != null) {
399       createLookups(lastMatchingState, matchedRegionStart, 
400               matchedRegionEnd, annotationSet);
401     }
402     fireProcessFinished();
403     fireStatusChanged("Look-up complete!");
404   // execute
405 
406 
407   /**
408    * Creates the Lookup annotations according to a gazetteer match.
409    @param matchingState the final FSMState that was reached while matching. 
410    @param matchedRegionStart the start of the matched text region.
411    @param matchedRegionEnd the end of the matched text region.
412    @param annotationSet the annotation set where the new annotations should 
413    * be added.
414    */
415   protected void createLookups(FSMState matchingState, long matchedRegionStart, 
416           long matchedRegionEnd, AnnotationSet annotationSet){
417     Iterator<Lookup> lookupIter = matchingState.getLookupSet().iterator();
418     while(lookupIter.hasNext()) {
419       Lookup currentLookup = lookupIter.next();
420       FeatureMap fm = Factory.newFeatureMap();
421       fm.put(LOOKUP_MAJOR_TYPE_FEATURE_NAME, currentLookup.majorType);
422       if (null!= currentLookup.oClass && null!=currentLookup.ontology){
423         fm.put(LOOKUP_CLASS_FEATURE_NAME,currentLookup.oClass);
424         fm.put(LOOKUP_ONTOLOGY_FEATURE_NAME,currentLookup.ontology);
425       }
426 
427       if(null != currentLookup.minorType)
428         fm.put(LOOKUP_MINOR_TYPE_FEATURE_NAME, currentLookup.minorType);
429       if(null != currentLookup.languages)
430         fm.put(LOOKUP_LANGUAGE_FEATURE_NAME, currentLookup.languages);      
431       if(null != currentLookup.features) {
432         fm.putAll(currentLookup.features);
433       }
434       try{
435 //        if(currentLookup.annotationType==null || "".equals(currentLookup.annotationType)){
436 //          annotationSet.add(new Long(matchedRegionStart),
437 //                          new Long(matchedRegionEnd + 1),
438 //                          LOOKUP_ANNOTATION_TYPE,
439 //                          fm);
440 //        }else{
441           annotationSet.add(new Long(matchedRegionStart),
442                           new Long(matchedRegionEnd + 1),
443                           currentLookup.annotationType, //this pojo attribute will have Lookup as a default tag.
444                           fm);
445        // }
446       catch(InvalidOffsetException ioe) {
447         throw new GateRuntimeException(ioe.toString());
448       }
449     }//while(lookupIter.hasNext())
450   }
451   
452   /** The initial state of the FSM that backs this gazetteer
453    */
454   protected FSMState initialState;
455 
456   /** A set containing all the states of the FSM backing the gazetteer
457    */
458   protected Set<FSMState> fsmStates;
459 
460   /**lookup <br>
461    @param singleItem a single string to be looked up by the gazetteer
462    @return set of the Lookups associated with the parameter*/
463   @Override
464   public Set<Lookup> lookup(String singleItem) {
465     char currentChar;
466     Set<Lookup> set = new HashSet<Lookup>();
467     FSMState currentState = initialState;
468     FSMState nextState;
469 
470     for(int i = 0; i< singleItem.length(); i++) {
471         currentChar = singleItem.charAt(i);
472         if Character.isSpaceChar(currentChar|| Character.isWhitespace(currentChar) ) currentChar = ' ';
473         nextState = currentState.next(currentChar);
474         if(nextState == null) {
475           return set;
476         }
477         currentState = nextState;
478     //for(int i = 0; i< text.length(); i++)
479     set = currentState.getLookupSet();
480     return set;
481   }
482 
483   @Override
484   public boolean remove(String singleItem) {
485     char currentChar;
486     FSMState currentState = initialState;
487     FSMState nextState;
488 
489     for(int i = 0; i< singleItem.length(); i++) {
490         currentChar = singleItem.charAt(i);
491         if Character.isSpaceChar(currentChar|| Character.isWhitespace(currentChar) ) currentChar = ' ';
492         if (!caseSensitivecurrentChar = Character.toUpperCase(currentChar);
493         nextState = currentState.next(currentChar);
494         if(nextState == null) {
495           return false;
496         }//nothing to remove
497         currentState = nextState;
498     //for(int i = 0; i< text.length(); i++)
499     currentState.lookupSet = new HashSet<Lookup>();
500     return true;
501   }
502 
503   @Override
504   public boolean add(String singleItem, Lookup lookup) {
505     addLookup(singleItem,lookup);
506     return true;
507   }
508   
509   /**
510    * Use a {@link SharedDefaultGazetteer} to duplicate this gazetteer
511    * by sharing the internal FSM rather than re-loading the lists.
512    */
513   @Override
514   public Resource duplicate(Factory.DuplicationContext ctx)
515           throws ResourceInstantiationException {
516     return Factory.createResource(SharedDefaultGazetteer.class.getName(),
517             Utils.featureMap(
518                     SharedDefaultGazetteer.SDEF_GAZ_BOOTSTRAP_GAZETTEER_PROPERTY_NAME,
519                     this),
520             Factory.duplicate(this.getFeatures(), ctx),
521             this.getName());
522   }
523 
524 
525   public static interface Iter
526   {
527       public boolean hasNext();
528       public char next();
529   // iter class
530 
531   /**
532    * class implementing the map using binary search by char as key
533    * to retrieve the corresponding object.
534    */
535   public static class CharMap implements Serializable
536   {
537     private static final long serialVersionUID = 4192829422957074447L;
538 
539     char[] itemsKeys = null;
540       Object[] itemsObjs = null;
541 
542       /**
543        * resize the containers by one, leaving empty element at position 'index'
544        */
545       void resize(int index)
546       {
547           int newsz = itemsKeys.length + 1;
548           char[] tempKeys = new char[newsz];
549           Object[] tempObjs = new Object[newsz];
550           System.arraycopy(itemsKeys, 0, tempKeys, 0, index);
551           System.arraycopy(itemsObjs, 0, tempObjs, 0, index);
552           System.arraycopy(itemsKeys, index, tempKeys, index + 1, newsz - index - 1);
553           System.arraycopy(itemsObjs, index, tempObjs, index + 1, newsz - index - 1);
554 
555           itemsKeys = tempKeys;
556           itemsObjs = tempObjs;
557       // resize
558 
559   /**
560    * get the object from the map using the char key
561    */
562       Object get(char key)
563       {
564           if (itemsKeys == nullreturn null;
565           int index = Arrays.binarySearch(itemsKeys, key);
566           if (index<0)
567               return null;
568           return itemsObjs[index];
569       }
570   /**
571    * put the object into the char map using the char as the key
572    */
573       Object put(char key, Object value)
574       {
575           if (itemsKeys == null)
576           {
577               itemsKeys = new char[1];
578               itemsKeys[0= key;
579               itemsObjs = new Object[1];
580               itemsObjs[0= value;
581               return value;
582           }// if first time
583           int index = Arrays.binarySearch(itemsKeys, key);
584           if (index<0)
585           {
586               index = ~index;
587               resize(index);
588               itemsKeys[index= key;
589               itemsObjs[index= value;
590           }
591           return itemsObjs[index];
592       // put
593 
594   }// class CharMap
595 
596   /**
597    @return the gazetteerFeatureSeparator
598    */
599   public String getGazetteerFeatureSeparator() {
600     return gazetteerFeatureSeparator;
601   }
602 
603   /**
604    @param gazetteerFeatureSeparator the gazetteerFeatureSeparator to set
605    */
606   @Optional
607   @CreoleParameter(comment="The character used to separate features for entries in gazetteer lists. Accepts strings like &quot;\t&quot; and will unescape it to the relevant character. If not specified, this gazetteer does not support extra features.",defaultValue=":")
608   public void setGazetteerFeatureSeparator(String gazetteerFeatureSeparator) {
609     this.gazetteerFeatureSeparator = gazetteerFeatureSeparator;
610   }
611    
612 // DefaultGazetteer