DocumentXmlUtils.java
001 /*
002  *  DocumentXmlUtils.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  *  Ian Roberts, 20/Jul/2006
013  *
014  *  $Id: DocumentXmlUtils.java 17580 2014-03-07 18:58:06Z markagreenwood $
015  */
016 package gate.corpora;
017 
018 import gate.Annotation;
019 import gate.AnnotationSet;
020 import gate.FeatureMap;
021 import gate.TextualDocument;
022 import gate.event.StatusListener;
023 import gate.util.Err;
024 import gate.util.Strings;
025 
026 import java.util.Collection;
027 import java.util.HashMap;
028 import java.util.Iterator;
029 import java.util.Map;
030 import java.util.Set;
031 import java.util.SortedMap;
032 import java.util.SortedSet;
033 import java.util.TreeMap;
034 import java.util.TreeSet;
035 
036 /**
037  * This class is contains useful static methods for working with the GATE XML
038  * format.  Many of the methods in this class were originally in {@link
039  * DocumentImpl} but as they are not specific to any one implementation of the
040  <code>Document</code> interface they have been moved here.
041  */
042 public class DocumentXmlUtils {
043 
044   /**
045    * This field is used when creating StringBuffers for toXml() methods. The
046    * size of the StringBuffer will be docDonctent.size() multiplied by this
047    * value. It is aimed to improve the performance of StringBuffer
048    */
049   public static final int DOC_SIZE_MULTIPLICATION_FACTOR = 40;
050 
051   /**
052    * Returns a GateXml document that is a custom XML format for wich there is a
053    * reader inside GATE called gate.xml.GateFormatXmlHandler. What it does is to
054    * serialize a GATE document in an XML format.
055    
056    @param doc the document to serialize.
057    @return a string representing a Gate Xml document.
058    */
059   public static String toXml(TextualDocument doc) {
060     // Initialize the xmlContent several time the size of the current document.
061     // This is because of the tags size. This measure is made to increase the
062     // performance of StringBuffer.
063     StringBuffer xmlContent = new StringBuffer(
064             DOC_SIZE_MULTIPLICATION_FACTOR
065             (doc.getContent().size().intValue()));
066     // Add xml header
067     xmlContent.append("<?xml version=\"1.0\" encoding=\"");
068     xmlContent.append(doc.getEncoding());
069     xmlContent.append("\" ?>");
070     xmlContent.append(Strings.getNl());
071     // Add the root element
072     xmlContent.append("<GateDocument>\n");
073     xmlContent.append("<!-- The document's features-->\n\n");
074     xmlContent.append("<GateDocumentFeatures>\n");
075     xmlContent.append(featuresToXml(doc.getFeatures(),null));
076     xmlContent.append("</GateDocumentFeatures>\n");
077     xmlContent.append("<!-- The document content area with serialized"
078             " nodes -->\n\n");
079     // Add plain text element
080     xmlContent.append("<TextWithNodes>");
081     xmlContent.append(textWithNodes(doc, doc.getContent().toString()));
082     xmlContent.append("</TextWithNodes>\n");
083     // Serialize as XML all document's annotation sets
084     // Serialize the default AnnotationSet
085     StatusListener sListener = (StatusListener)gate.Gate
086             .getListeners().get("gate.event.StatusListener");
087     if(sListener != null)
088       sListener.statusChanged("Saving the default annotation set ");
089     xmlContent.append("<!-- The default annotation set -->\n\n");
090     annotationSetToXml(doc.getAnnotations(), xmlContent);
091     // Serialize all others AnnotationSets
092     // namedAnnotSets is a Map containing all other named Annotation Sets.
093     Map<String,AnnotationSet> namedAnnotSets = doc.getNamedAnnotationSets();
094     if(namedAnnotSets != null) {
095       Iterator<AnnotationSet> iter = namedAnnotSets.values().iterator();
096       while(iter.hasNext()) {
097         AnnotationSet annotSet = iter.next();
098         xmlContent.append("<!-- Named annotation set -->\n\n");
099         // Serialize it as XML
100         if(sListener != null)
101           sListener.statusChanged("Saving " + annotSet.getName()
102                   " annotation set ");
103         annotationSetToXml(annotSet, xmlContent);
104       }// End while
105     }// End if
106     // Add the end of GateDocument
107     xmlContent.append("</GateDocument>");
108     if(sListener != nullsListener.statusChanged("Done !");
109     // return the XmlGateDocument
110     return xmlContent.toString();
111   }
112 
113 
114   /**
115    * This method saves a FeatureMap as XML elements.
116    
117    @param aFeatureMap
118    *          the feature map that has to be saved as XML.
119    @return a String like this: <Feature><Name>...</Name> <Value>...</Value></Feature><Feature>...</Feature>
120    */
121   public static StringBuffer featuresToXml(FeatureMap aFeatureMap, Map<String,StringBuffer> normalizedFeatureNames) {
122     if(aFeatureMap == nullreturn new StringBuffer();
123     StringBuffer buffer = new StringBuffer(1024);
124     Set<Object> keySet = aFeatureMap.keySet();
125     Iterator<Object> keyIterator = keySet.iterator();
126     while(keyIterator.hasNext()) {
127       Object key = keyIterator.next();
128       Object value = aFeatureMap.get(key);
129       if((key != null&& (value != null)) {
130         String keyClassName = null;
131         String keyItemClassName = null;
132         String valueClassName = null;
133         String valueItemClassName = null;
134         String key2String = key.toString();
135         String value2String = value.toString();
136         Object item = null;
137         // Test key if it is String, Number or Collection
138         if(key instanceof java.lang.String || key instanceof java.lang.Number
139                 || key instanceof java.util.Collection)
140           keyClassName = key.getClass().getName();
141         // Test value if it is String, Number or Collection
142         if(value instanceof java.lang.String
143                 || value instanceof java.lang.Number
144                 || value instanceof java.util.Collection)
145           valueClassName = value.getClass().getName();
146         // Features and values that are not Strings, Numbers or collections
147         // will be discarded.
148         if(keyClassName == null || valueClassName == nullcontinue;
149         // If key is collection serialize the collection in a specific format
150         if(key instanceof java.util.Collection) {
151           StringBuffer keyStrBuff = new StringBuffer();
152           Iterator<?> iter = ((Collection<?>)key).iterator();
153           if(iter.hasNext()) {
154             item = iter.next();
155             if(item instanceof java.lang.Number)
156               keyItemClassName = item.getClass().getName();
157             else keyItemClassName = String.class.getName();
158             keyStrBuff.append(item.toString());
159           }// End if
160           while(iter.hasNext()) {
161             item = iter.next();
162             keyStrBuff.append(";").append(item.toString());
163           }// End while
164           key2String = keyStrBuff.toString();
165         }// End if
166         // If key is collection serialize the colection in a specific format
167         if(value instanceof java.util.Collection) {
168           StringBuffer valueStrBuff = new StringBuffer();
169           Iterator<?> iter = ((Collection<?>)value).iterator();
170           if(iter.hasNext()) {
171             item = iter.next();
172             if(item instanceof java.lang.Number)
173               valueItemClassName = item.getClass().getName();
174             else valueItemClassName = String.class.getName();
175             valueStrBuff.append(item.toString());
176           }// End if
177           while(iter.hasNext()) {
178             item = iter.next();
179             valueStrBuff.append(";").append(item.toString());
180           }// End while
181           value2String = valueStrBuff.toString();
182         }// End if
183         buffer.append("<Feature>\n  <Name");
184         if(keyClassName != null)
185           buffer.append(" className=\"").append(keyClassName).append("\"");
186         if(keyItemClassName != null)
187           buffer.append(" itemClassName=\"").append(keyItemClassName).append(
188                   "\"");
189         buffer.append(">");
190         
191         // use a map of keys already checked for XML validity
192         StringBuffer normalizedKey = new StringBuffer(key2String);
193         if (normalizedFeatureNames!=null){
194           // has this key been already converted ?
195           normalizedKey = normalizedFeatureNames.get(key2String);
196           if (normalizedKey==null){
197             // never seen so far!
198             normalizedKey= combinedNormalisation(key2String);
199             normalizedFeatureNames.put(key2String,normalizedKey);
200           }
201         }
202         else normalizedKey = combinedNormalisation(key2String);
203         
204         buffer.append(normalizedKey);
205         buffer.append("</Name>\n  <Value");
206         if(valueClassName != null)
207           buffer.append(" className=\"").append(valueClassName).append("\"");
208         if(valueItemClassName != null)
209           buffer.append(" itemClassName=\"").append(valueItemClassName).append(
210                   "\"");
211         buffer.append(">");
212         buffer.append(combinedNormalisation(value2String));
213         buffer.append("</Value>\n</Feature>\n");
214       }// End if
215     }// end While
216     return buffer;
217   }// featuresToXml
218 
219 
220   /**
221    * Combines replaceCharsWithEntities and filterNonXmlChars in a single method
222    **/
223   public static StringBuffer combinedNormalisation(String inputString){
224     if(inputString == nullreturn new StringBuffer("");
225     StringBuffer buffer = new StringBuffer(inputString);
226     for (int i=buffer.length()-1; i>=0; i--){
227       char currentchar = buffer.charAt(i);
228       // is the current character an xml char which needs replacing?
229       if(!isXmlChar(currentchar)) buffer.replace(i,i+1," ");
230       // is the current character an xml char which needs replacing?
231       else if(currentchar == '<' || currentchar == '>' || currentchar == '&'|| currentchar == '\''|| currentchar == '\"' || currentchar == 0xA0 || currentchar == 0xA9)
232         buffer.replace(i,i+1,entitiesMap.get(new Character(currentchar)));
233       }
234     return buffer;
235   }
236 
237   /**
238    * This method filters any non XML char see:
239    * http://www.w3c.org/TR/2000/REC-xml-20001006#charsets All non XML chars will
240    * be replaced with 0x20 (space char) This assures that the next time the
241    * document is loaded there won't be any problems.
242    
243    @param aStrBuffer
244    *          represents the input String that is filtred. If the aStrBuffer is
245    *          null then an empty string will be returend
246    @return the "purified" StringBuffer version of the aStrBuffer
247    */
248   public static StringBuffer filterNonXmlChars(StringBuffer aStrBuffer) {
249     if(aStrBuffer == nullreturn new StringBuffer("");
250     // String space = new String(" ");
251     char space = ' ';
252     for(int i = aStrBuffer.length() 1; i >= 0; i--) {
253       if(!isXmlChar(aStrBuffer.charAt(i))) aStrBuffer.setCharAt(i, space);
254     }// End for
255     return aStrBuffer;
256   }// filterNonXmlChars()
257 
258   /**
259    * This method decide if a char is a valid XML one or not
260    
261    @param ch
262    *          the char to be tested
263    @return true if is a valid XML char and fals if is not.
264    */
265   public static boolean isXmlChar(char ch) {
266     if(ch == 0x9 || ch == 0xA || ch == 0xDreturn true;
267     if((0x20 <= ch&& (ch <= 0xD7FF)) return true;
268     if((0xE000 <= ch&& (ch <= 0xFFFD)) return true;
269     if((0x10000 <= ch&& (ch <= 0x10FFFF)) return true;
270     return false;
271   }// End isXmlChar()
272 
273 
274   /** This method replace all chars that appears in the anInputString and also
275     * that are in the entitiesMap with their corresponding entity
276     @param anInputString the string analyzed. If it is null then returns the
277     *  empty string
278     @return a string representing the input string with chars replaced with
279     *  entities
280     */
281   public static StringBuffer replaceCharsWithEntities(String anInputString){
282     if (anInputString == nullreturn new StringBuffer("");
283     StringBuffer strBuff = new StringBuffer(anInputString);
284     for (int i=strBuff.length()-1; i>=0; i--){
285       Character ch = new Character(strBuff.charAt(i));
286       if (entitiesMap.keySet().contains(ch)){
287         strBuff.replace(i,i+1,entitiesMap.get(ch));
288       }// End if
289     }// End for
290     return strBuff;
291   }// replaceCharsWithEntities()
292 
293   /**
294    * Returns the document's text interspersed with &lt;Node&gt; elements at all
295    * points where the document has an annotation beginning or ending.
296    */
297   public static String textWithNodes(TextualDocument doc, String aText) {
298     // filterNonXmlChars
299     // getoffsets for Nodes
300     // getoffsets for XML entities
301     if(aText == nullreturn new String("");
302     StringBuffer textWithNodes = filterNonXmlChars(new StringBuffer(aText));
303     // Construct a map from offsets to Chars ()
304     SortedMap<Long, Character> offsets2CharsMap = new TreeMap<Long, Character>();
305     if(aText.length() != 0) {
306       // Fill the offsets2CharsMap with all the indices where special chars
307       // appear
308       buildEntityMapFromString(aText, offsets2CharsMap);
309     }// End if
310     // Construct the offsetsSet for all nodes belonging to this document
311     SortedSet<Long> offsetsSet = new TreeSet<Long>();
312     Iterator<Annotation> annotSetIter = doc.getAnnotations().iterator();
313     while(annotSetIter.hasNext()) {
314       Annotation annot = annotSetIter.next();
315       offsetsSet.add(annot.getStartNode().getOffset());
316       offsetsSet.add(annot.getEndNode().getOffset());
317     }// end While
318     // Get the nodes from all other named annotation sets.
319     Map<String,AnnotationSet> namedAnnotSets = doc.getNamedAnnotationSets();
320     if(namedAnnotSets != null) {
321       Iterator<AnnotationSet> iter = namedAnnotSets.values().iterator();
322       while(iter.hasNext()) {
323         AnnotationSet annotSet = iter.next();
324         Iterator<Annotation> iter2 = annotSet.iterator();
325         while(iter2.hasNext()) {
326           Annotation annotTmp = iter2.next();
327           offsetsSet.add(annotTmp.getStartNode().getOffset());
328           offsetsSet.add(annotTmp.getEndNode().getOffset());
329         }// End while
330       }// End while
331     }// End if
332     // offsetsSet is ordered in ascending order because the structure
333     // is a TreeSet
334     if(offsetsSet.isEmpty()) { return replaceCharsWithEntities(aText)
335             .toString()}// End if
336     
337     // create a large StringBuffer
338     StringBuffer modifiedBuffer = new StringBuffer(textWithNodes.length() 2);
339     
340     // last character copied from the original String
341     int lastCharactercopied = 0;
342     
343     // append to buffer all text up to next offset
344     // for node or entity
345     // we need to iterate on offsetSet and offsets2CharsMap
346     Set<Long> allOffsets = new TreeSet<Long>();
347     allOffsets.addAll(offsetsSet);
348     allOffsets.addAll(offsets2CharsMap.keySet());
349     Iterator<Long> allOffsetsIterator = allOffsets.iterator();
350     while (allOffsetsIterator.hasNext()){
351       Long nextOffset = allOffsetsIterator.next();
352       int nextOffsetint = nextOffset.intValue();
353       // is there some text to add since last time?
354       if (nextOffsetint>lastCharactercopied){
355         modifiedBuffer.append(textWithNodes.substring(lastCharactercopied,nextOffsetint));
356         lastCharactercopied=nextOffsetint;
357       }
358       // do we need to add a node information here?
359       if (offsetsSet.contains(nextOffset))
360         modifiedBuffer.append("<Node id=\"").append(nextOffsetint).append("\"/>");
361       
362       // do we need to convert an XML entity?
363       if (offsets2CharsMap.containsKey(nextOffset)){
364        String entityString = entitiesMap.get(offsets2CharsMap.get(nextOffset));
365        // skip the character in the original String
366        lastCharactercopied++;
367        // append the corresponding entity
368        modifiedBuffer.append(entityString);
369       }
370     }
371     // copies the remaining text
372     modifiedBuffer.append(textWithNodes.substring(lastCharactercopied,textWithNodes.length()));
373     
374     return modifiedBuffer.toString();
375   }
376 
377   /**
378    * This method takes aScanString and searches for those chars from entitiesMap
379    * that appear in the string. A tree map(offset2Char) is filled using as key
380    * the offsets where those Chars appear and the Char. If one of the params is
381    * null the method simply returns.
382    */
383   public static void buildEntityMapFromString(String aScanString, SortedMap<Long, Character> aMapToFill) {
384     if(aScanString == null || aMapToFill == nullreturn;
385     if(entitiesMap == null || entitiesMap.isEmpty()) {
386       Err.prln("WARNING: Entities map was not initialised !");
387       return;
388     }// End if
389     // Fill the Map with the offsets of the special chars
390     Iterator<Character> entitiesMapIterator = entitiesMap.keySet().iterator();
391     Character c;
392     int fromIndex;
393     while(entitiesMapIterator.hasNext()) {
394       c = entitiesMapIterator.next();
395       fromIndex = 0;
396       while(-!= fromIndex) {
397         fromIndex = aScanString.indexOf(c.charValue(), fromIndex);
398         if(-!= fromIndex) {
399           aMapToFill.put(new Long(fromIndex), c);
400           fromIndex++;
401         }// End if
402       }// End while
403     }// End while
404   }// buildEntityMapFromString();
405 
406   /**
407    * Converts the Annotation set to XML which is appended to the supplied
408    * StringBuffer instance.
409    
410    @param anAnnotationSet
411    *          The annotation set that has to be saved as XML.
412    @param buffer
413    *          the StringBuffer that the XML representation should be appended to
414    */
415   public static void annotationSetToXml(AnnotationSet anAnnotationSet,
416           StringBuffer buffer) {
417     if(anAnnotationSet == null) {
418       buffer.append("<AnnotationSet>\n");
419       buffer.append("</AnnotationSet>\n");
420       return;
421     }// End if
422     if(anAnnotationSet.getName() == null)
423       buffer.append("<AnnotationSet>\n");
424     else {
425       buffer.append("<AnnotationSet Name=\"");
426       buffer.append(anAnnotationSet.getName());
427       buffer.append("\" >\n");
428     }
429     Map<String, StringBuffer> convertedKeys = new HashMap<String, StringBuffer>();
430     // Iterate through AnnotationSet and save each Annotation as XML
431     Iterator<Annotation> iterator = anAnnotationSet.iterator();
432     while(iterator.hasNext()) {
433       Annotation annot = iterator.next();
434       buffer.append("<Annotation Id=\"");
435       buffer.append(annot.getId());
436       buffer.append("\" Type=\"");
437       buffer.append(annot.getType());
438       buffer.append("\" StartNode=\"");
439       buffer.append(annot.getStartNode().getOffset());
440       buffer.append("\" EndNode=\"");
441       buffer.append(annot.getEndNode().getOffset());
442       buffer.append("\">\n");
443       buffer.append(featuresToXml(annot.getFeatures(),convertedKeys));
444       buffer.append("</Annotation>\n");
445     }// End while
446     buffer.append("</AnnotationSet>\n");
447   }// annotationSetToXml
448 
449   /**
450    * Converts the Annotation set to XML which is appended to the supplied
451    * StringBuffer instance. The standard
452    {@link #annotationSetToXml(AnnotationSet, StringBuffer) method} uses the
453    * name that belongs to the provided annotation set, however, this method
454    * allows one to store the provided annotation set under a different
455    * annotation set name.
456    
457    @param anAnnotationSet
458    *          the annotation set that has to be saved as XML.
459    @param annotationSetNameToUse
460    *          the new name for the annotation set being converted to XML
461    @param buffer
462    *          the StringBuffer that the XML representation should be appended to
463    */
464   public static void annotationSetToXml(AnnotationSet anAnnotationSet, String annotationSetNameToUse,
465           StringBuffer buffer) {
466     if(anAnnotationSet == null) {
467       buffer.append("<AnnotationSet>\n");
468       buffer.append("</AnnotationSet>\n");
469       return;
470     }// End if
471     if(annotationSetNameToUse == null || annotationSetNameToUse.trim().length() == 0)
472       buffer.append("<AnnotationSet>\n");
473     else {
474       buffer.append("<AnnotationSet Name=\"");
475       buffer.append(annotationSetNameToUse);
476       buffer.append("\" >\n");
477     }
478     Map<String, StringBuffer> convertedKeys = new HashMap<String, StringBuffer>();
479     // Iterate through AnnotationSet and save each Annotation as XML
480     Iterator<Annotation> iterator = anAnnotationSet.iterator();
481     while(iterator.hasNext()) {
482       Annotation annot = iterator.next();
483       buffer.append("<Annotation Id=\"");
484       buffer.append(annot.getId());
485       buffer.append("\" Type=\"");
486       buffer.append(annot.getType());
487       buffer.append("\" StartNode=\"");
488       buffer.append(annot.getStartNode().getOffset());
489       buffer.append("\" EndNode=\"");
490       buffer.append(annot.getEndNode().getOffset());
491       buffer.append("\">\n");
492       buffer.append(featuresToXml(annot.getFeatures(),convertedKeys));
493       buffer.append("</Annotation>\n");
494     }// End while
495     buffer.append("</AnnotationSet>\n");
496   }// annotationSetToXml
497 
498   /**
499    * A map initialized in init() containing entities that needs to be replaced
500    * in strings
501    */
502   public static final Map<Character,String> entitiesMap = new HashMap<Character,String>();
503   // Initialize the entities map use when saving as xml
504   static {
505     entitiesMap.put(new Character('<')"&lt;");
506     entitiesMap.put(new Character('>')"&gt;");
507     entitiesMap.put(new Character('&')"&amp;");
508     entitiesMap.put(new Character('\'')"&apos;");
509     entitiesMap.put(new Character('"')"&quot;");
510     entitiesMap.put(new Character((char)160)"&#160;");
511     entitiesMap.put(new Character((char)169)"&#169;");
512   }// static
513 }