ConllDocumentFormat.java
001 /*
002  *  ConllDocumentFormat.java
003  *
004  *  Copyright (c) 1995-2012, The University of Sheffield. See the file
005  *  COPYRIGHT.txt in the software or at http://gate.ac.uk/gate/COPYRIGHT.txt
006  *
007  *  This file is part of GATE (see http://gate.ac.uk/), and is free
008  *  software, licenced under the GNU Library General Public License,
009  *  Version 2, June 1991 (in the distribution as file licence.html,
010  *  and also available at http://gate.ac.uk/gate/licence.html).
011  *  
012  *  $Id: ConllDocumentFormat.java 17530 2014-03-04 15:57:43Z markagreenwood $
013  */
014 
015 package gate.corpora;
016 
017 import java.util.*;
018 import gate.*;
019 import gate.creole.ANNIEConstants;
020 import gate.creole.ResourceInstantiationException;
021 import gate.creole.metadata.AutoInstance;
022 import gate.creole.metadata.CreoleResource;
023 import gate.util.DocumentFormatException;
024 import gate.util.InvalidOffsetException;
025 
026 
027 /** Document format for handling CoNLL/IOB documents:
028   * He PRP B-NP
029   * accepted VBD B-VP
030   * the DT B-NP
031   * position NN I-NP
032   * ...
033   */
034 @CreoleResource(name = "GATE CoNLL Document Format", isPrivate = true,
035     autoinstances = {@AutoInstance(hidden = true)})
036 public class ConllDocumentFormat extends TextualDocumentFormat {
037   
038   private static final long serialVersionUID = 5756433194230855515L;
039   
040   public static final String ANNOTATION_COLUMN_FEATURE = "column";
041   public static final String ANNOTATION_KIND_FEATURE = "kind";
042   
043   
044   /** Debug flag */
045   private static final boolean DEBUG = false;
046 
047   /** Default construction */
048   public ConllDocumentFormat() { super();}
049 
050 
051   @Override
052   public void unpackMarkup(gate.Document docthrows DocumentFormatException{
053     if ( (doc == null|| (doc.getSourceUrl() == null && doc.getContent() == null) ) {
054       throw new DocumentFormatException("GATE document is null or no content found. Nothing to parse!");
055     }
056 
057     setNewLineProperty(doc);
058 
059     String[] lines = doc.getContent().toString().split("[\\n\\r]+");
060     StringBuilder newContent = new StringBuilder();
061     
062     // Items of data to be turned into Original markups annotations
063     List<Annotandum> annotanda = new ArrayList<Annotandum>();
064     
065     // Currently open tags: created by "B-FOO", extended by "I-FOO", closed
066     // by "O" or end of sentence.
067     Map<String, Annotandum> inProgress = new HashMap<String, Annotandum>();
068 
069     /* Note: I-Foo handling currently has a weak spot.
070      
071      * this    B-Foo
072      * is      B-Bar
073      * strange I-Foo
074      
075      * will result in a Foo annotation spanning "this is strange", because
076      * the I-Foo extends the existing B-Foo.  If the sentence is cut off 
077      * before hitting another I-Foo, however, the Foo annotation will not
078      * have been extended.  But this situation will not occur in carefully
079      * edited input.  
080      */
081 
082     long oldEnd = 0L;
083     long start = 0L;
084     long end = 0L;
085 
086     for (String line : lines) {
087       oldEnd = end;
088       start = newContent.length();
089       String[] items = line.split("\\s+");
090       
091       // blank line: stick a newline in the document content and close
092       // any annotations in progress
093       if (items.length == 0) {
094         newContent.append("\n");
095         end = newContent.length();
096         finishAllTags(inProgress, annotanda, oldEnd);
097       }
098       
099       else {
100         String token = items[0];
101         // We've agreed to put the space after every token.
102         newContent.append(token);
103         end = newContent.length();
104         newContent.append(' ');
105         
106         // Create Token and following SpaceToken annotation.
107         annotanda.add(Annotandum.makeToken(start, end, token));
108         annotanda.add(Annotandum.makeSpaceToken(end));
109 
110         for (int column=; column < items.length ; column++) {
111           // O means close all annotations in progress
112           if (items[column].equals("O")) {
113             finishAllTags(inProgress, annotanda, oldEnd);
114           }
115           
116           // "U-FOO": unigram, single-token "FOO"
117           // annotation, after closing any "FOO" already in progress
118           else if ( (items[column].length() 2&&
119               items[column].startsWith("U-") ) {
120             String type = items[column].substring(2);
121             finishTag(type, inProgress, annotanda, oldEnd);
122             annotanda.add(new Annotandum(type, start, end, column, true));
123           }
124           
125           // "L-FOO": last bit of "FOO": extend and 
126           // close any "FOO" already in progress
127           else if ( (items[column].length() 2&&
128               items[column].startsWith("L-") ) {
129             String type = items[column].substring(2);
130             
131             if (inProgress.containsKey(type)) {
132               // good L-FOO, so update the end offset
133               inProgress.get(type).endOffset = end;
134             }
135             else {
136               // bad data, containing I-FOO without a B-FOO, so treat as if B-FOO
137               inProgress.put(type, new Annotandum(type, start, end, column, true));
138             }
139 
140             finishTag(type, inProgress, annotanda, end);
141           }
142           
143           // "B-FOO": start a new "FOO" annotation
144           // after closing any "FOO" already in progress
145           else if ( (items[column].length() 2&&
146               items[column].startsWith("B-") ) {
147             String type = items[column].substring(2);
148             finishTag(type, inProgress, annotanda, oldEnd);
149             inProgress.put(type, new Annotandum(type, start, end, column, true));
150           }
151           
152           // "I-FOO": extend current "FOO" annotation
153           else if ( (items[column].length() 2&&
154               items[column].startsWith("I-") ) {
155             String type = items[column].substring(2);
156             
157             if (inProgress.containsKey(type)) {
158               // good I-FOO, so update the end offset
159               inProgress.get(type).endOffset = end;
160             }
161             else {
162               // bad data, containing I-FOO without a B-FOO, so treat as if B-FOO
163               inProgress.put(type, new Annotandum(type, start, end, column, true));
164             }
165           }
166           
167           // "FOO": treat as single-token annotation (such as POS tag)
168           else 
169             Annotandum tag = new Annotandum(items[column], start, end, column, false);
170             annotanda.add(tag);
171           }
172         }
173       }
174       
175     }
176     // end of input: close any remaining annotations
177     finishAllTags(inProgress, annotanda, end);
178 
179     
180     // set new content & create Original markups annotations
181     try {
182       DocumentContent newContentImpl = new DocumentContentImpl(newContent.toString());
183       doc.edit(0L, doc.getContent().size(), newContentImpl);
184       long newSize = doc.getContent().size();
185       
186       AnnotationSet originalMarkups = doc.getAnnotations(GateConstants.ORIGINAL_MARKUPS_ANNOT_SET_NAME);
187       for (Annotandum ann : annotanda) {
188         if (DEBUG) {
189           String string = Utils.stringFor(doc, ann.startOffset, 
190               (ann.endOffset <= newSize? ann.endOffset : newSize);
191           System.out.format("%d  %d  %s  %s\n", ann.startOffset, ann.endOffset, ann.type, string);
192         }
193         originalMarkups.add(ann.startOffset, ann.endOffset, ann.type, ann.features);
194       }
195     }
196     catch (InvalidOffsetException e) {
197       throw new DocumentFormatException(e);
198     }
199   }
200 
201   
202   /* Close any open annotations (typically at the end of a sentence).  Leave the existing
203    * end offset on an annotation that has one, but chop it off if it's still unspecified.
204    */
205   private void finishAllTags(Map<String, Annotandum> annsUnderway, List<Annotandum> annsFinished, long cutoff) {
206     for (String key : annsUnderway.keySet()) {
207       Annotandum ann = annsUnderway.get(key);
208       if (ann.endOffset == null) {
209         ann.endOffset = cutoff;
210       }
211       annsFinished.add(ann);
212     }
213     annsUnderway.clear();
214   }
215   
216   
217   /* If there is an annotation in progress of this type, close it;
218    * if not, do nothing.    */
219   private void finishTag(String type, Map<String, Annotandum> annsUnderway, List<Annotandum> annsFinished, long cutoff) {
220     Annotandum ann = annsUnderway.remove(type);
221     if (ann != null) {
222       if (ann.endOffset == null) {
223         ann.endOffset = cutoff;
224       }
225       annsFinished.add(ann);
226     }
227   }
228   
229   
230   
231   /** Initialise this resource, and return it. */
232   @Override
233   public Resource init() throws ResourceInstantiationException{
234     // Register ad hoc MIME-type
235     MimeType mime = new MimeType("text","x-conll");
236     // Register the class handler for this MIME-type
237     mimeString2ClassHandlerMap.put(mime.getType()"/" + mime.getSubtype()this);
238     // Register the mime type with string
239     mimeString2mimeTypeMap.put(mime.getType() "/" + mime.getSubtype(), mime);
240     // Register file suffixes for this mime type
241     suffixes2mimeTypeMap.put("conll",mime);
242     suffixes2mimeTypeMap.put("iob",mime);
243     // Register magic numbers for this mime type
244     //magic2mimeTypeMap.put("Subject:",mime);
245     // Set the mimeType for this language resource
246     setMimeType(mime);
247     return this;
248   }
249   
250 }
251 
252 
253 /** Wrapper around data to be turned into an "Original markups" annotation.
254  */
255 class Annotandum {
256   protected Long startOffset, endOffset;
257   protected String type;
258   protected FeatureMap features;
259   
260   protected Annotandum(String type, Long startOffset, Long endOffset) {
261     this.startOffset = startOffset;
262     this.endOffset = endOffset;
263     this.type = type;
264     this.features = Factory.newFeatureMap();
265   }
266   
267   /* Note that chunkiness is determined by the tag structure.  A "B-Foo"
268    * that spans only one token is chunky.  Tags outside the B/I/L/U system
269    * get the kind==token feature; tags in the system get kind==chunky.   */
270   protected Annotandum(String type, Long startOffset, Long endOffset, int column, boolean chunky) {
271     this.features = Factory.newFeatureMap();
272     this.features.put(ConllDocumentFormat.ANNOTATION_COLUMN_FEATURE, column);
273     this.features.put(ConllDocumentFormat.ANNOTATION_KIND_FEATURE, chunky ? "chunk" "token");
274     this.startOffset = startOffset;
275     this.endOffset = endOffset;
276     this.type = type;
277   }
278 
279   protected Annotandum(String type, Long startOffset, Long endOffset, FeatureMap features) {
280     this.startOffset = startOffset;
281     this.endOffset = endOffset;
282     this.type = type;
283     this.features = features;
284   }
285 
286   protected static Annotandum makeToken(long start, long end, String string) {
287     int length = (int) (end - start);
288     FeatureMap features = Factory.newFeatureMap();
289     features.put(ANNIEConstants.TOKEN_LENGTH_FEATURE_NAME, length);
290     features.put(ANNIEConstants.TOKEN_STRING_FEATURE_NAME, string);
291     return new Annotandum("Token", start, end, features);
292   }
293   
294   protected static Annotandum makeSpaceToken(long start) {
295     long end = start + 1L;
296     FeatureMap features = Factory.newFeatureMap();
297     features.put(ANNIEConstants.TOKEN_LENGTH_FEATURE_NAME, 1);
298     features.put(ANNIEConstants.TOKEN_STRING_FEATURE_NAME, " ");
299     return new Annotandum("SpaceToken", start, end, features);
300   }
301   
302   
303 }