DocumentStaxUtils.java
0001 /*
0002  *  DocumentStaxUtils.java
0003  *
0004  *  Copyright (c) 1995-2012, The University of Sheffield. See the file
0005  *  COPYRIGHT.txt in the software or at http://gate.ac.uk/gate/COPYRIGHT.txt
0006  *
0007  *  This file is part of GATE (see http://gate.ac.uk/), and is free
0008  *  software, licenced under the GNU Library General Public License,
0009  *  Version 2, June 1991 (in the distribution as file licence.html,
0010  *  and also available at http://gate.ac.uk/gate/licence.html).
0011  *
0012  *  Ian Roberts, 20/Jul/2006
0013  *
0014  *  $Id: DocumentStaxUtils.java 20232 2017-06-09 12:09:50Z ian_roberts $
0015  */
0016 package gate.corpora;
0017 
0018 import gate.Annotation;
0019 import gate.AnnotationSet;
0020 import gate.Document;
0021 import gate.DocumentContent;
0022 import gate.Factory;
0023 import gate.FeatureMap;
0024 import gate.Gate;
0025 import gate.TextualDocument;
0026 import gate.event.StatusListener;
0027 import gate.relations.Relation;
0028 import gate.relations.RelationSet;
0029 import gate.relations.SimpleRelation;
0030 import gate.util.GateException;
0031 import gate.util.GateRuntimeException;
0032 import gate.util.InvalidOffsetException;
0033 import gate.util.Out;
0034 
0035 import java.io.BufferedWriter;
0036 import java.io.File;
0037 import java.io.FileOutputStream;
0038 import java.io.IOException;
0039 import java.io.InputStream;
0040 import java.io.OutputStream;
0041 import java.io.OutputStreamWriter;
0042 import java.io.StringWriter;
0043 import java.lang.reflect.Constructor;
0044 import java.util.ArrayList;
0045 import java.util.Collection;
0046 import java.util.Collections;
0047 import java.util.Comparator;
0048 import java.util.HashMap;
0049 import java.util.Iterator;
0050 import java.util.List;
0051 import java.util.Map;
0052 import java.util.Set;
0053 import java.util.SortedSet;
0054 import java.util.StringTokenizer;
0055 import java.util.TreeSet;
0056 import java.util.regex.Matcher;
0057 import java.util.regex.Pattern;
0058 
0059 import javax.xml.stream.XMLInputFactory;
0060 import javax.xml.stream.XMLOutputFactory;
0061 import javax.xml.stream.XMLStreamConstants;
0062 import javax.xml.stream.XMLStreamException;
0063 import javax.xml.stream.XMLStreamReader;
0064 import javax.xml.stream.XMLStreamWriter;
0065 
0066 /**
0067  * This class provides support for reading and writing GATE XML format
0068  * using StAX (the Streaming API for XML).
0069  */
0070 public class DocumentStaxUtils {
0071 
0072   private static XMLInputFactory inputFactory = null;
0073 
0074   /**
0075    * The char used to replace characters in text content that are
0076    * illegal in XML.
0077    */
0078   public static final char INVALID_CHARACTER_REPLACEMENT = ' ';
0079 
0080   public static final String GATE_XML_VERSION = "3";
0081   
0082   /**
0083    * The number of < signs after which we encode a string using CDATA
0084    * rather than writeCharacters.
0085    */
0086   public static final int LT_THRESHOLD = 5;
0087 
0088   /**
0089    * Reads GATE XML format data from the given XMLStreamReader and puts
0090    * the content and annotation sets into the given Document, replacing
0091    * its current content. The reader must be positioned on the opening
0092    * GateDocument tag (i.e. the last event was a START_ELEMENT for which
0093    * getLocalName returns "GateDocument"), and when the method returns
0094    * the reader will be left positioned on the corresponding closing
0095    * tag.
0096    
0097    @param xsr the source of the XML to parse
0098    @param doc the document to update
0099    @throws XMLStreamException
0100    */
0101   public static void readGateXmlDocument(XMLStreamReader xsr, Document doc)
0102           throws XMLStreamException {
0103     readGateXmlDocument(xsr, doc, null);
0104   }
0105 
0106   /**
0107    * Reads GATE XML format data from the given XMLStreamReader and puts
0108    * the content and annotation sets into the given Document, replacing
0109    * its current content. The reader must be positioned on the opening
0110    * GateDocument tag (i.e. the last event was a START_ELEMENT for which
0111    * getLocalName returns "GateDocument"), and when the method returns
0112    * the reader will be left positioned on the corresponding closing
0113    * tag.
0114    
0115    @param xsr the source of the XML to parse
0116    @param doc the document to update
0117    @param statusListener optional status listener to receive status
0118    *          messages
0119    @throws XMLStreamException
0120    */
0121   public static void readGateXmlDocument(XMLStreamReader xsr, Document doc,
0122           StatusListener statusListenerthrows XMLStreamException {
0123     DocumentContent savedContent = null;
0124 
0125     // check the precondition
0126     xsr.require(XMLStreamConstants.START_ELEMENT, null, "GateDocument");
0127 
0128     // process the document features
0129     xsr.nextTag();
0130     xsr.require(XMLStreamConstants.START_ELEMENT, null, "GateDocumentFeatures");
0131 
0132     if(statusListener != null) {
0133       statusListener.statusChanged("Reading document features");
0134     }
0135     FeatureMap documentFeatures = readFeatureMap(xsr);
0136 
0137     // read document text, building the map of node IDs to offsets
0138     xsr.nextTag();
0139     xsr.require(XMLStreamConstants.START_ELEMENT, null, "TextWithNodes");
0140 
0141     Map<Integer, Long> nodeIdToOffsetMap = new HashMap<Integer, Long>();
0142     if(statusListener != null) {
0143       statusListener.statusChanged("Reading document content");
0144     }
0145     String documentText = readTextWithNodes(xsr, nodeIdToOffsetMap);
0146 
0147     // TODO this is almost never needed and will cause the double
0148     // loading so see if we can live without it for now 
0149     // save the content, in case anything goes wrong later
0150     //savedContent = doc.getContent();
0151     
0152     // set the document content to the text with nodes text.
0153     doc.setContent(new DocumentContentImpl(documentText));
0154 
0155     try {
0156       int numAnnots = 0;
0157       // process annotation sets, using the node map built above
0158       Integer maxAnnotId = null;
0159       // initially, we don't know whether annotation IDs are required or
0160       // not
0161       Boolean requireAnnotationIds = null;
0162       int eventType = xsr.nextTag();
0163       while(eventType == XMLStreamConstants.START_ELEMENT && xsr.getLocalName().equals("AnnotationSet")) {
0164         xsr.require(XMLStreamConstants.START_ELEMENT, null, "AnnotationSet");
0165         String annotationSetName = xsr.getAttributeValue(null, "Name");
0166         AnnotationSet annotationSet = null;
0167         if(annotationSetName == null) {
0168           if(statusListener != null) {
0169             statusListener.statusChanged("Reading default annotation set");
0170           }
0171           annotationSet = doc.getAnnotations();
0172         }
0173         else {
0174           if(statusListener != null) {
0175             statusListener.statusChanged("Reading \"" + annotationSetName
0176                     "\" annotation set");
0177           }
0178           annotationSet = doc.getAnnotations(annotationSetName);
0179         }
0180         annotationSet.clear();
0181         SortedSet<Integer> annotIdsInSet = new TreeSet<Integer>();
0182         requireAnnotationIds = readAnnotationSet(xsr, annotationSet,
0183                 nodeIdToOffsetMap, annotIdsInSet, requireAnnotationIds);
0184         if(annotIdsInSet.size() 0
0185                 && (maxAnnotId == null || annotIdsInSet.last().intValue() > maxAnnotId
0186                         .intValue())) {
0187           maxAnnotId = annotIdsInSet.last();
0188         }
0189         numAnnots += annotIdsInSet.size();
0190         // readAnnotationSet leaves reader positioned on the
0191         // </AnnotationSet> tag, so nextTag takes us to either the next
0192         // <AnnotationSet>, a <RelationSet>, or </GateDocument>
0193         eventType = xsr.nextTag();
0194       }
0195 
0196       while(eventType == XMLStreamConstants.START_ELEMENT
0197               && xsr.getLocalName().equals("RelationSet")) {
0198         xsr.require(XMLStreamConstants.START_ELEMENT, null, "RelationSet");
0199         String relationSetName = xsr.getAttributeValue(null, "Name");
0200         RelationSet relations = null;
0201         if(relationSetName == null) {
0202           if(statusListener != null) {
0203             statusListener
0204                     .statusChanged("Reading relation set for default annotation set");
0205           }
0206           relations = doc.getAnnotations().getRelations();
0207         else {
0208           if(statusListener != null) {
0209             statusListener.statusChanged("Reading relation set for \""
0210                     + relationSetName + "\" annotation set");
0211           }
0212           relations = doc.getAnnotations(relationSetName).getRelations();
0213         }
0214 
0215         SortedSet<Integer> relIdsInSet = new TreeSet<Integer>();
0216         readRelationSet(xsr, relations, relIdsInSet);
0217         if(relIdsInSet.size() 0
0218                 && (maxAnnotId == null || relIdsInSet.last().intValue() > maxAnnotId
0219                         .intValue())) {
0220           maxAnnotId = relIdsInSet.last();
0221         }
0222         numAnnots += relIdsInSet.size();
0223         // readAnnotationSet leaves reader positioned on the
0224         // </RelationSet> tag, so nextTag takes us to either the next
0225         // <RelationSet> or to the </GateDocument>
0226         eventType = xsr.nextTag();
0227       }
0228       
0229       // check we are on the end document tag
0230       xsr.require(XMLStreamConstants.END_ELEMENT, null, "GateDocument");
0231 
0232       doc.setFeatures(documentFeatures);
0233 
0234       // set the ID generator, if doc is a DocumentImpl
0235       if(doc instanceof DocumentImpl && maxAnnotId != null) {
0236         ((DocumentImpl)doc).setNextAnnotationId(maxAnnotId.intValue() 1);
0237       }
0238       if(statusListener != null) {
0239         statusListener.statusChanged("Finished.  " + numAnnots
0240                 " annotation(s) processed");
0241       }
0242     }
0243     // in case of exception, reset document content to the unparsed XML
0244     catch(XMLStreamException xse) {
0245       doc.setContent(savedContent);
0246       throw xse;
0247     }
0248     catch(RuntimeException re) {
0249       doc.setContent(savedContent);
0250       throw re;
0251     }
0252   }
0253 
0254   /**
0255    * Processes an AnnotationSet element from the given reader and fills
0256    * the given annotation set with the corresponding annotations. The
0257    * reader must initially be positioned on the starting AnnotationSet
0258    * tag and will be left positioned on the correspnding closing tag.
0259    
0260    @param xsr the reader
0261    @param annotationSet the annotation set to fill.
0262    @param nodeIdToOffsetMap a map mapping node IDs (Integer) to their
0263    *          offsets in the text (Long). If null, we assume that the
0264    *          node ids and offsets are the same (useful if parsing an
0265    *          annotation set in isolation).
0266    @param allAnnotIds a set to contain all annotation IDs specified in
0267    *          the annotation set. It should initially be empty and will
0268    *          be updated if any of the annotations in this set specify
0269    *          an ID.
0270    @param requireAnnotationIds whether annotations are required to
0271    *          specify their IDs. If true, it is an error for an
0272    *          annotation to omit the Id attribute. If false, it is an
0273    *          error for the Id to be present. If null, we have not yet
0274    *          determined what style of XML this is.
0275    @return <code>requireAnnotationIds</code>. If the passed in
0276    *         value was null, and we have since determined what it should
0277    *         be, the updated value is returned.
0278    @throws XMLStreamException
0279    */
0280   public static Boolean readAnnotationSet(XMLStreamReader xsr,
0281           AnnotationSet annotationSet, Map<Integer, Long> nodeIdToOffsetMap,
0282           Set<Integer> allAnnotIds, Boolean requireAnnotationIds)
0283           throws XMLStreamException {
0284     List<AnnotationObject> collectedAnnots = new ArrayList<AnnotationObject>();
0285     while(xsr.nextTag() == XMLStreamConstants.START_ELEMENT) {
0286       xsr.require(XMLStreamConstants.START_ELEMENT, null, "Annotation");
0287       AnnotationObject annObj = new AnnotationObject();
0288       annObj.setElemName(xsr.getAttributeValue(null, "Type"));
0289       try {
0290         int startNodeId = Integer.parseInt(xsr.getAttributeValue(null,
0291                 "StartNode"));
0292         if(nodeIdToOffsetMap != null) {
0293           Long startOffset = nodeIdToOffsetMap.get(new Integer(startNodeId));
0294           if(startOffset != null) {
0295             annObj.setStart(startOffset);
0296           }
0297           else {
0298             throw new XMLStreamException("Invalid start node ID", xsr
0299                     .getLocation());
0300           }
0301         }
0302         else {
0303           // no offset map, so just use the ID as an offset
0304           annObj.setStart(new Long(startNodeId));
0305         }
0306       }
0307       catch(NumberFormatException nfe) {
0308         throw new XMLStreamException("Non-integer value found for StartNode",
0309                 xsr.getLocation());
0310       }
0311 
0312       try {
0313         int endNodeId = Integer
0314                 .parseInt(xsr.getAttributeValue(null, "EndNode"));
0315         if(nodeIdToOffsetMap != null) {
0316           Long endOffset = nodeIdToOffsetMap.get(new Integer(endNodeId));
0317           if(endOffset != null) {
0318             annObj.setEnd(endOffset);
0319           }
0320           else {
0321             throw new XMLStreamException("Invalid end node ID", xsr
0322                     .getLocation());
0323           }
0324         }
0325         else {
0326           // no offset map, so just use the ID as an offset
0327           annObj.setEnd(new Long(endNodeId));
0328         }
0329       }
0330       catch(NumberFormatException nfe) {
0331         throw new XMLStreamException("Non-integer value found for EndNode", xsr
0332                 .getLocation());
0333       }
0334 
0335       String annotIdString = xsr.getAttributeValue(null, "Id");
0336       if(annotIdString == null) {
0337         if(requireAnnotationIds == null) {
0338           // if one annotation doesn't specify Id than all must
0339           requireAnnotationIds = Boolean.FALSE;
0340         }
0341         else {
0342           if(requireAnnotationIds.booleanValue()) {
0343             // if we were expecting an Id but didn't get one...
0344             throw new XMLStreamException(
0345                     "New style GATE XML format requires that every annotation "
0346                             "specify its Id, but an annotation with no Id was found",
0347                     xsr.getLocation());
0348           }
0349         }
0350       }
0351       else {
0352         // we have an ID attribute
0353         if(requireAnnotationIds == null) {
0354           // if one annotation specifies an Id then all must
0355           requireAnnotationIds = Boolean.TRUE;
0356         }
0357         else {
0358           if(!requireAnnotationIds.booleanValue()) {
0359             // if we were expecting not to have an Id but got one...
0360             throw new XMLStreamException(
0361                     "Old style GATE XML format requires that no annotation "
0362                             "specifies its Id, but an annotation with an Id was found",
0363                     xsr.getLocation());
0364           }
0365         }
0366         try {
0367           Integer annotationId = Integer.valueOf(annotIdString);
0368           if(allAnnotIds.contains(annotationId)) {
0369             throw new XMLStreamException("Annotation IDs must be unique "
0370                     "within an annotation set. Found duplicate ID", xsr
0371                     .getLocation());
0372           }
0373           allAnnotIds.add(annotationId);
0374           annObj.setId(annotationId);
0375         }
0376         catch(NumberFormatException nfe) {
0377           throw new XMLStreamException("Non-integer annotation ID found", xsr
0378                   .getLocation());
0379         }
0380       }
0381 
0382       // get the features of this annotation
0383       annObj.setFM(readFeatureMap(xsr));
0384       // readFeatureMap leaves xsr on the </Annotation> tag
0385       collectedAnnots.add(annObj);
0386     }
0387 
0388     // now process all found annotations.to add to the set
0389     Iterator<AnnotationObject> collectedAnnotsIt = collectedAnnots.iterator();
0390     while(collectedAnnotsIt.hasNext()) {
0391       AnnotationObject annObj = collectedAnnotsIt.next();
0392       try {
0393         if(annObj.getId() != null) {
0394           annotationSet.add(annObj.getId(), annObj.getStart(), annObj.getEnd(),
0395                   annObj.getElemName(), annObj.getFM());
0396         }
0397         else {
0398           annotationSet.add(annObj.getStart(), annObj.getEnd(), annObj
0399                   .getElemName(), annObj.getFM());
0400         }
0401       }
0402       catch(InvalidOffsetException ioe) {
0403         // really shouldn't happen, but could if we're not using an id
0404         // to offset map
0405         throw new XMLStreamException("Invalid offset when creating annotation "
0406                 + annObj, ioe);
0407       }
0408     }
0409     return requireAnnotationIds;
0410   }
0411   
0412   public static void readRelationSet(XMLStreamReader xsr,
0413           RelationSet relations, Set<Integer> allAnnotIds)
0414           throws XMLStreamException {
0415     while(xsr.nextTag() == XMLStreamConstants.START_ELEMENT) {
0416       xsr.require(XMLStreamConstants.START_ELEMENT, null, "Relation");
0417       String type = xsr.getAttributeValue(null, "Type");
0418       String idString = xsr.getAttributeValue(null, "Id");
0419       String memberString = xsr.getAttributeValue(null, "Members");
0420       
0421       if(memberString == null)
0422         throw new XMLStreamException("A relation must have members");
0423       if (type == null)
0424         throw new XMLStreamException("A relation must have a type");
0425       if (idString == null)
0426         throw new XMLStreamException("A relation must have an id");
0427       
0428       String[] memberStrings = memberString.split(";");
0429       int[] members = new int[memberStrings.length];
0430       for(int i = 0; i < members.length; ++i) {
0431         members[i= Integer.parseInt(memberStrings[i]);
0432       }
0433 
0434       xsr.nextTag();
0435       xsr.require(XMLStreamConstants.START_ELEMENT, null, "UserData");
0436 
0437       // get the string representation of the user data
0438       StringBuilder stringRep = new StringBuilder(1024);
0439       int eventType;
0440       while((eventType = xsr.next()) != XMLStreamConstants.END_ELEMENT) {
0441         switch(eventType) {
0442           case XMLStreamConstants.CHARACTERS:
0443           case XMLStreamConstants.CDATA:
0444             stringRep.append(xsr.getTextCharacters(), xsr.getTextStart(),
0445                     xsr.getTextLength());
0446             break;
0447 
0448           case XMLStreamConstants.START_ELEMENT:
0449             throw new XMLStreamException("Elements not allowed within "
0450                     "user data.", xsr.getLocation());
0451 
0452           default:
0453             // do nothing - ignore comments, PIs, etc.
0454         }
0455       }
0456 
0457       xsr.require(XMLStreamConstants.END_ELEMENT, null, "UserData");
0458 
0459       FeatureMap features = readFeatureMap(xsr);
0460 
0461       Relation r = new SimpleRelation(Integer.valueOf(idString), type, members);
0462       r.setFeatures(features);
0463 
0464       if(stringRep.length() 0) {
0465         ObjectWrapper wrapper = new ObjectWrapper(stringRep.toString());
0466         r.setUserData(wrapper.getValue());
0467       }
0468 
0469       relations.add(r);
0470     }
0471   }
0472 
0473   /**
0474    * Processes the TextWithNodes element from this XMLStreamReader,
0475    * returning the text content of the document. The supplied map is
0476    * updated with the offset of each Node element encountered. The
0477    * reader must be positioned on the starting TextWithNodes tag and
0478    * will be returned positioned on the corresponding closing tag.
0479    
0480    @param xsr
0481    @param nodeIdToOffsetMap
0482    @return the text content of the document
0483    */
0484   public static String readTextWithNodes(XMLStreamReader xsr,
0485           Map<Integer, Long> nodeIdToOffsetMapthrows XMLStreamException {
0486     StringBuffer textBuf = new StringBuffer(20480);
0487     int eventType;
0488     while((eventType = xsr.next()) != XMLStreamConstants.END_ELEMENT) {
0489       switch(eventType) {
0490         case XMLStreamConstants.CHARACTERS:
0491         case XMLStreamConstants.CDATA:
0492           textBuf.append(xsr.getTextCharacters(), xsr.getTextStart(), xsr
0493                   .getTextLength());
0494           break;
0495 
0496         case XMLStreamConstants.START_ELEMENT:
0497           // only Node elements allowed
0498           xsr.require(XMLStreamConstants.START_ELEMENT, null, "Node");
0499           String idString = xsr.getAttributeValue(null, "id");
0500           if(idString == null) {
0501             throw new XMLStreamException("Node element has no id", xsr
0502                     .getLocation());
0503           }
0504           try {
0505             Integer id = Integer.valueOf(idString);
0506             Long offset = new Long(textBuf.length());
0507             nodeIdToOffsetMap.put(id, offset);
0508           }
0509           catch(NumberFormatException nfe) {
0510             throw new XMLStreamException("Node element must have "
0511                     "integer id", xsr.getLocation());
0512           }
0513 
0514           // Node element must be empty
0515           if(xsr.next() != XMLStreamConstants.END_ELEMENT) {
0516             throw new XMLStreamException("Node element within TextWithNodes "
0517                     "must be empty.", xsr.getLocation());
0518           }
0519           break;
0520 
0521         default:
0522           // do nothing - ignore comments, PIs...
0523       }
0524     }
0525     return textBuf.toString();
0526   }
0527 
0528   /**
0529    * Processes a GateDocumentFeatures or Annotation element to build a
0530    * feature map. The element is expected to contain Feature children,
0531    * each with a Name and Value. The reader will be returned positioned
0532    * on the closing GateDocumentFeatures or Annotation tag.
0533    
0534    @throws XMLStreamException
0535    */
0536   public static FeatureMap readFeatureMap(XMLStreamReader xsr)
0537           throws XMLStreamException {
0538     FeatureMap fm = Factory.newFeatureMap();
0539     while(xsr.nextTag() == XMLStreamConstants.START_ELEMENT) {
0540       xsr.require(XMLStreamConstants.START_ELEMENT, null, "Feature");
0541       Object featureName = null;
0542       Object featureValue = null;
0543       while(xsr.nextTag() == XMLStreamConstants.START_ELEMENT) {
0544         if("Name".equals(xsr.getLocalName())) {
0545           featureName = readFeatureNameOrValue(xsr);
0546         }
0547         else if("Value".equals(xsr.getLocalName())) {
0548           featureValue = readFeatureNameOrValue(xsr);
0549         }
0550         else {
0551           throw new XMLStreamException("Feature element should contain "
0552                   "only Name and Value children", xsr.getLocation());
0553         }
0554       }
0555       fm.put(featureName, featureValue);
0556     }
0557     return fm;
0558   }
0559 
0560   /**
0561    * Read the name or value of a feature. The reader must be initially
0562    * positioned on an element with className and optional itemClassName
0563    * attributes, and text content convertable to this class. It will be
0564    * returned on the corresponding end tag.
0565    
0566    @param xsr the reader
0567    @return the name or value represented by this element.
0568    @throws XMLStreamException
0569    */
0570   @SuppressWarnings({"unchecked""rawtypes"})
0571   static Object readFeatureNameOrValue(XMLStreamReader xsr)
0572           throws XMLStreamException {
0573     String className = xsr.getAttributeValue(null, "className");
0574     if(className == null) {
0575       className = "java.lang.String";
0576     }
0577     String itemClassName = xsr.getAttributeValue(null, "itemClassName");
0578     if(itemClassName == null) {
0579       itemClassName = "java.lang.String";
0580     }
0581     // get the string representation of the name/value
0582     StringBuffer stringRep = new StringBuffer(1024);
0583     int eventType;
0584     while((eventType = xsr.next()) != XMLStreamConstants.END_ELEMENT) {
0585       switch(eventType) {
0586         case XMLStreamConstants.CHARACTERS:
0587         case XMLStreamConstants.CDATA:
0588           stringRep.append(xsr.getTextCharacters(), xsr.getTextStart(), xsr
0589                   .getTextLength());
0590           break;
0591       
0592         case XMLStreamConstants.START_ELEMENT:
0593           throw new XMLStreamException("Elements not allowed within "
0594                   "feature name or value element.", xsr.getLocation());
0595 
0596         default:
0597           // do nothing - ignore comments, PIs, etc.
0598       }
0599     }
0600 
0601     // shortcut - if class name is java.lang.String, just return the
0602     // string representation directly
0603     if("java.lang.String".equals(className)) {
0604       return stringRep.toString();
0605     }
0606 
0607     // otherwise, do some fancy reflection
0608     Class<?> theClass = null;
0609     try {
0610       theClass = Class.forName(className, true, Gate.getClassLoader());
0611     }
0612     catch(ClassNotFoundException cnfe) {
0613       // give up and just return the String
0614       return stringRep.toString();
0615     }
0616 
0617     // backwards compatibility only - newly serialized GATE documents will
0618     // use an ObjectWrapper for Collection-valued features
0619     if(java.util.Collection.class.isAssignableFrom(theClass)) {
0620       Class<?> itemClass = null;
0621       Constructor<?> itemConstructor = null;
0622       Collection featObject = null;
0623 
0624       boolean addItemAsString = false;
0625 
0626       // construct the collection object to use as the feature value
0627       try {
0628         featObject = (Collection)theClass.newInstance();
0629       }
0630       // if we can't instantiate the collection class at all, give up
0631       // and return the value as a string
0632       catch(IllegalAccessException iae) {
0633         return stringRep.toString();
0634       }
0635       catch(InstantiationException ie) {
0636         return stringRep.toString();
0637       }
0638 
0639       // common case - itemClass *is* java.lang.String, so we can
0640       // avoid all the reflection
0641       if("java.lang.String".equals(itemClassName)) {
0642         addItemAsString = true;
0643       }
0644       else {
0645         try {
0646           itemClass = Class.forName(itemClassName, true, Gate.getClassLoader());
0647           // Let's detect if itemClass takes a constructor with a String
0648           // as param
0649           Class<?>[] paramsArray = new Class[1];
0650           paramsArray[0= java.lang.String.class;
0651           itemConstructor = itemClass.getConstructor(paramsArray);
0652         }
0653         catch(ClassNotFoundException cnfex) {
0654           Out.prln("Warning: Item class " + itemClassName + " not found."
0655                   "Adding items as Strings");
0656           addItemAsString = true;
0657         }
0658         catch(NoSuchMethodException nsme) {
0659           addItemAsString = true;
0660         }
0661         catch(SecurityException se) {
0662           addItemAsString = true;
0663         }// End try
0664       }
0665 
0666       StringTokenizer strTok = new StringTokenizer(stringRep.toString()";");
0667       Object[] params = new Object[1];
0668       Object itemObj = null;
0669       while(strTok.hasMoreTokens()) {
0670         String itemStrRep = strTok.nextToken();
0671         if(addItemAsString)
0672           featObject.add(itemStrRep);
0673         else {
0674           params[0= itemStrRep;
0675           try {
0676             itemObj = itemConstructor.newInstance(params);
0677           }
0678           catch(Exception e) {
0679             throw new XMLStreamException("An item(" + itemStrRep
0680                     ")  does not comply with its class" " definition("
0681                     + itemClassName + ")", xsr.getLocation());
0682           }// End try
0683           featObject.add(itemObj);
0684         }// End if
0685       }// End while
0686 
0687       return featObject;
0688     }// End if
0689 
0690     // If currentfeatClass is not a Collection and not String, test to
0691     // see if it has a constructor that takes a String as param
0692     Class<?>[] params = new Class[1];
0693     params[0= java.lang.String.class;
0694     try {
0695       Constructor<?> featConstr = theClass.getConstructor(params);
0696       Object[] featConstrParams = new Object[1];
0697       featConstrParams[0= stringRep.toString();
0698       Object featObject = featConstr.newInstance(featConstrParams);
0699       if(featObject instanceof ObjectWrapper) {
0700         featObject = ((ObjectWrapper)featObject).getValue();
0701       }
0702       return featObject;
0703     }
0704     catch(Exception e) {
0705       return stringRep.toString();
0706     }// End try
0707   }
0708 
0709   // ///// Reading XCES /////
0710 
0711   // constants
0712   /**
0713    * Version of XCES that this class can handle.
0714    */
0715   public static final String XCES_VERSION = "1.0";
0716 
0717   /**
0718    * XCES namespace URI.
0719    */
0720   public static final String XCES_NAMESPACE = "http://www.xces.org/schema/2003";
0721 
0722   /**
0723    * Read XML data in <a href="http://www.xces.org/">XCES</a> format
0724    * from the given stream and add the corresponding annotations to the
0725    * given annotation set. This method does not close the stream, this
0726    * is the responsibility of the caller.
0727    
0728    @param is the input stream to read from, which will <b>not</b> be
0729    *          closed before returning.
0730    @param as the annotation set to read into.
0731    */
0732   public static void readXces(InputStream is, AnnotationSet as)
0733           throws XMLStreamException {
0734     if(inputFactory == null) {
0735       inputFactory = XMLInputFactory.newInstance();
0736     }
0737     XMLStreamReader xsr = inputFactory.createXMLStreamReader(is);
0738     try {
0739       nextTagSkipDTD(xsr);
0740       readXces(xsr, as);
0741     }
0742     finally {
0743       xsr.close();
0744     }
0745   }
0746 
0747   /**
0748    * A copy of the nextTag algorithm from the XMLStreamReader javadocs,
0749    * but which also skips over DTD events as well as whitespace,
0750    * comments and PIs.
0751    
0752    @param xsr the reader to advance
0753    @return {@link XMLStreamConstants#START_ELEMENT} or
0754    *         {@link XMLStreamConstants#END_ELEMENT} for the next tag.
0755    @throws XMLStreamException
0756    */
0757   private static int nextTagSkipDTD(XMLStreamReader xsr)
0758           throws XMLStreamException {
0759     int eventType = xsr.next();
0760     while((eventType == XMLStreamConstants.CHARACTERS && xsr.isWhiteSpace())
0761             || (eventType == XMLStreamConstants.CDATA && xsr.isWhiteSpace())
0762             || eventType == XMLStreamConstants.SPACE
0763             || eventType == XMLStreamConstants.PROCESSING_INSTRUCTION
0764             || eventType == XMLStreamConstants.COMMENT
0765             || eventType == XMLStreamConstants.DTD) {
0766       eventType = xsr.next();
0767     }
0768     if(eventType != XMLStreamConstants.START_ELEMENT
0769             && eventType != XMLStreamConstants.END_ELEMENT) {
0770       throw new XMLStreamException("expected start or end tag", xsr
0771               .getLocation());
0772     }
0773     return eventType;
0774   }
0775 
0776   /**
0777    * Read XML data in <a href="http://www.xces.org/">XCES</a> format
0778    * from the given reader and add the corresponding annotations to the
0779    * given annotation set. The reader must be positioned on the starting
0780    <code>cesAna</code> tag and will be left pointing to the
0781    * corresponding end tag.
0782    
0783    @param xsr the XMLStreamReader to read from.
0784    @param as the annotation set to read into.
0785    @throws XMLStreamException
0786    */
0787   public static void readXces(XMLStreamReader xsr, AnnotationSet as)
0788           throws XMLStreamException {
0789     xsr.require(XMLStreamConstants.START_ELEMENT, XCES_NAMESPACE, "cesAna");
0790 
0791     // Set of all annotation IDs in this set.
0792     Set<Integer> allAnnotIds = new TreeSet<Integer>();
0793     // pre-populate with the IDs of any existing annotations in the set
0794     for(Annotation a : as) {
0795       allAnnotIds.add(a.getId());
0796     }
0797 
0798     // lists to collect the annotations in before adding them to the
0799     // set. We collect the annotations that specify and ID (via
0800     // struct/@n) in one list and those that don't in another, so we can
0801     // add the identified ones first, then the others will take the next
0802     // available ID
0803     List<AnnotationObject> collectedIdentifiedAnnots = new ArrayList<AnnotationObject>();
0804     List<AnnotationObject> collectedNonIdentifiedAnnots = new ArrayList<AnnotationObject>();
0805     while(xsr.nextTag() == XMLStreamConstants.START_ELEMENT) {
0806       xsr.require(XMLStreamConstants.START_ELEMENT, XCES_NAMESPACE, "struct");
0807       AnnotationObject annObj = new AnnotationObject();
0808       annObj.setElemName(xsr.getAttributeValue(null, "type"));
0809       try {
0810         int from = Integer.parseInt(xsr.getAttributeValue(null, "from"));
0811         annObj.setStart(new Long(from));
0812       }
0813       catch(NumberFormatException nfe) {
0814         throw new XMLStreamException(
0815                 "Non-integer value found for struct/@from", xsr.getLocation());
0816       }
0817 
0818       try {
0819         int to = Integer.parseInt(xsr.getAttributeValue(null, "to"));
0820         annObj.setEnd(new Long(to));
0821       }
0822       catch(NumberFormatException nfe) {
0823         throw new XMLStreamException("Non-integer value found for struct/@to",
0824                 xsr.getLocation());
0825       }
0826 
0827       String annotIdString = xsr.getAttributeValue(null, "n");
0828       if(annotIdString != null) {
0829         try {
0830           Integer annotationId = Integer.valueOf(annotIdString);
0831           if(allAnnotIds.contains(annotationId)) {
0832             throw new XMLStreamException("Annotation IDs must be unique "
0833                     "within an annotation set. Found duplicate ID", xsr
0834                     .getLocation());
0835           }
0836           allAnnotIds.add(annotationId);
0837           annObj.setId(annotationId);
0838         }
0839         catch(NumberFormatException nfe) {
0840           throw new XMLStreamException("Non-integer annotation ID found", xsr
0841                   .getLocation());
0842         }
0843       }
0844 
0845       // get the features of this annotation
0846       annObj.setFM(readXcesFeatureMap(xsr));
0847       // readFeatureMap leaves xsr on the </Annotation> tag
0848       if(annObj.getId() != null) {
0849         collectedIdentifiedAnnots.add(annObj);
0850       }
0851       else {
0852         collectedNonIdentifiedAnnots.add(annObj);
0853       }
0854     }
0855 
0856     // finished reading, add the annotations to the set
0857     AnnotationObject a = null;
0858     try {
0859       // first the ones that specify an ID
0860       Iterator<AnnotationObject> it = collectedIdentifiedAnnots.iterator();
0861       while(it.hasNext()) {
0862         a = it.next();
0863         as.add(a.getId(), a.getStart(), a.getEnd(), a.getElemName(), a.getFM());
0864       }
0865       // next the ones that don't
0866       it = collectedNonIdentifiedAnnots.iterator();
0867       while(it.hasNext()) {
0868         a = it.next();
0869         as.add(a.getStart(), a.getEnd(), a.getElemName(), a.getFM());
0870       }
0871     }
0872     catch(InvalidOffsetException ioe) {
0873       throw new XMLStreamException("Invalid offset when creating annotation "
0874               + a, ioe);
0875     }
0876   }
0877 
0878   /**
0879    * Processes a struct element to build a feature map. The element is
0880    * expected to contain feat children, each with name and value
0881    * attributes. The reader will be returned positioned on the closing
0882    * struct tag.
0883    
0884    @throws XMLStreamException
0885    */
0886   public static FeatureMap readXcesFeatureMap(XMLStreamReader xsr)
0887           throws XMLStreamException {
0888     FeatureMap fm = Factory.newFeatureMap();
0889     while(xsr.nextTag() == XMLStreamConstants.START_ELEMENT) {
0890       xsr.require(XMLStreamConstants.START_ELEMENT, XCES_NAMESPACE, "feat");
0891       String featureName = xsr.getAttributeValue(null, "name");
0892       Object featureValue = xsr.getAttributeValue(null, "value");
0893 
0894       fm.put(featureName, featureValue);
0895       // read the (possibly virtual) closing tag of the feat element
0896       xsr.nextTag();
0897       xsr.require(XMLStreamConstants.END_ELEMENT, XCES_NAMESPACE, "feat");
0898     }
0899     return fm;
0900   }
0901 
0902   // ////////// Writing methods ////////////
0903 
0904   private static XMLOutputFactory outputFactory = null;
0905 
0906   /**
0907    * Returns a string containing the specified document in GATE XML
0908    * format.
0909    
0910    @param doc the document
0911    */
0912   public static String toXml(Document doc) {
0913     try {
0914       if(outputFactory == null) {
0915         outputFactory = XMLOutputFactory.newInstance();
0916       }
0917       StringWriter sw = new StringWriter(doc.getContent().size().intValue()
0918               * DocumentXmlUtils.DOC_SIZE_MULTIPLICATION_FACTOR);
0919       XMLStreamWriter xsw = outputFactory.createXMLStreamWriter(sw);
0920 
0921       // start the document
0922       if(doc instanceof TextualDocument) {
0923         xsw.writeStartDocument(((TextualDocument)doc).getEncoding()"1.0");
0924       }
0925       else {
0926         xsw.writeStartDocument("1.0");
0927       }
0928       newLine(xsw);
0929       writeDocument(doc, xsw, "");
0930       xsw.close();
0931 
0932       return sw.toString();
0933     }
0934     catch(XMLStreamException xse) {
0935       throw new GateRuntimeException("Error converting document to XML", xse);
0936     }
0937   }
0938 
0939   /**
0940    * Write the specified GATE document to a File.
0941    
0942    @param doc the document to write
0943    @param file the file to write it to
0944    @throws XMLStreamException
0945    @throws IOException
0946    */
0947   public static void writeDocument(Document doc, File file)
0948           throws XMLStreamException, IOException {
0949     writeDocument(doc, file, "");
0950   }
0951 
0952   /**
0953    * Write the specified GATE document to a File, optionally putting the
0954    * XML in a namespace.
0955    
0956    @param doc the document to write
0957    @param file the file to write it to
0958    @param namespaceURI the namespace URI to use for the XML elements.
0959    *          Must not be null, but can be the empty string if no
0960    *          namespace is desired.
0961    @throws XMLStreamException
0962    @throws IOException
0963    */
0964   public static void writeDocument(Document doc, File file, String namespaceURI)
0965           throws XMLStreamException, IOException {
0966 
0967     OutputStream outputStream = new FileOutputStream(file);
0968     try {
0969       writeDocument(doc,outputStream,namespaceURI);
0970     }
0971     finally {
0972       outputStream.close();
0973     }
0974   }
0975   
0976   public static void writeDocument(Document doc, OutputStream outputStream, String namespaceURIthrows XMLStreamException, IOException {
0977     if(outputFactory == null) {
0978       outputFactory = XMLOutputFactory.newInstance();
0979     }
0980 
0981     XMLStreamWriter xsw = null;
0982     try {
0983       if(doc instanceof TextualDocument) {
0984         xsw = outputFactory.createXMLStreamWriter(outputStream,
0985                 ((TextualDocument)doc).getEncoding());
0986         xsw.writeStartDocument(((TextualDocument)doc).getEncoding()"1.0");
0987       }
0988       else {
0989         xsw = outputFactory.createXMLStreamWriter(outputStream);
0990         xsw.writeStartDocument("1.0");
0991       }
0992       newLine(xsw);
0993 
0994       writeDocument(doc, xsw, namespaceURI);
0995     }
0996     finally {
0997       if(xsw != null) {
0998         xsw.close();
0999       }
1000     }
1001   }
1002 
1003   /**
1004    * Write the specified GATE Document to an XMLStreamWriter. This
1005    * method writes just the GateDocument element - the XML declaration
1006    * must be filled in by the caller if required.
1007    
1008    @param doc the Document to write
1009    @param annotationSets the annotations to include. If the map
1010    *          contains an entry for the key <code>null</code>, this
1011    *          will be treated as the default set. All other entries are
1012    *          treated as named annotation sets.
1013    @param xsw the StAX XMLStreamWriter to use for output
1014    @throws GateException if an error occurs during writing
1015    */
1016   public static void writeDocument(Document doc,
1017           Map<String, Collection<Annotation>> annotationSets,
1018           XMLStreamWriter xsw, String namespaceURIthrows XMLStreamException {
1019     xsw.setDefaultNamespace(namespaceURI);
1020     xsw.writeStartElement(namespaceURI, "GateDocument");
1021     xsw.writeAttribute("version", GATE_XML_VERSION);
1022     if(namespaceURI.length() 0) {
1023       xsw.writeDefaultNamespace(namespaceURI);
1024     }
1025     newLine(xsw);
1026     // features
1027     xsw.writeComment(" The document's features");
1028     newLine(xsw);
1029     newLine(xsw);
1030     xsw.writeStartElement(namespaceURI, "GateDocumentFeatures");
1031     newLine(xsw);
1032     writeFeatures(doc.getFeatures(), xsw, namespaceURI);
1033     xsw.writeEndElement()// GateDocumentFeatures
1034     newLine(xsw);
1035     // text with nodes
1036     xsw.writeComment(" The document content area with serialized nodes ");
1037     newLine(xsw);
1038     newLine(xsw);
1039     writeTextWithNodes(doc, annotationSets.values(), xsw, namespaceURI);
1040     newLine(xsw);
1041     // Serialize as XML all document's annotation sets
1042     // Serialize the default AnnotationSet
1043     StatusListener sListener = (StatusListener)gate.Gate
1044             .getListeners().get("gate.event.StatusListener");
1045     if(annotationSets.containsKey(null)) {
1046       if(sListener != null)
1047         sListener.statusChanged("Saving the default annotation set ");
1048       xsw.writeComment(" The default annotation set ");
1049       newLine(xsw);
1050       newLine(xsw);
1051       writeAnnotationSet(annotationSets.get(null), null, xsw, namespaceURI);
1052       newLine(xsw);
1053     }
1054 
1055     // Serialize all others AnnotationSets
1056     // namedAnnotSets is a Map containing all other named Annotation
1057     // Sets.
1058     Iterator<String> iter = annotationSets.keySet().iterator();
1059     while(iter.hasNext()) {
1060       String annotationSetName = iter.next();
1061       // ignore the null entry, if present - we've already handled that
1062       // above
1063       if(annotationSetName != null) {
1064         Collection<Annotation> annots = annotationSets.get(annotationSetName);
1065         xsw.writeComment(" Named annotation set ");
1066         newLine(xsw);
1067         newLine(xsw);
1068         // Serialize it as XML
1069         if(sListener != null)
1070           sListener.statusChanged("Saving " + annotationSetName
1071                   " annotation set ");
1072         writeAnnotationSet(annots, annotationSetName, xsw, namespaceURI);
1073         newLine(xsw);
1074       }// End if
1075     }// End while
1076     
1077     iter = annotationSets.keySet().iterator();
1078     while(iter.hasNext()) {
1079       
1080       writeRelationSet(doc.getAnnotations(iter.next()).getRelations(), xsw,
1081               namespaceURI);
1082     }
1083 
1084     // close the GateDocument element
1085     xsw.writeEndElement();
1086     newLine(xsw);
1087   }
1088 
1089   /**
1090    * Write the specified GATE Document to an XMLStreamWriter. This
1091    * method writes just the GateDocument element - the XML declaration
1092    * must be filled in by the caller if required. This method writes all
1093    * the annotations in all the annotation sets on the document. To
1094    * write just specific annotations, use
1095    {@link #writeDocument(Document, Map, XMLStreamWriter, String)}.
1096    */
1097   public static void writeDocument(Document doc, XMLStreamWriter xsw,
1098           String namespaceURIthrows XMLStreamException {
1099     Map<String, Collection<Annotation>> asMap = new HashMap<String, Collection<Annotation>>();
1100     asMap.put(null, doc.getAnnotations());
1101     if(doc.getNamedAnnotationSets() != null) {
1102       asMap.putAll(doc.getNamedAnnotationSets());
1103     }
1104     writeDocument(doc, asMap, xsw, namespaceURI);
1105   }
1106 
1107   /**
1108    * Writes the given annotation set to an XMLStreamWriter as GATE XML
1109    * format. The Name attribute of the generated AnnotationSet element
1110    * is set to the default value, i.e. <code>annotations.getName</code>.
1111    
1112    @param annotations the annotation set to write
1113    @param xsw the writer to use for output
1114    @param namespaceURI
1115    @throws XMLStreamException
1116    */
1117   public static void writeAnnotationSet(AnnotationSet annotations,
1118           XMLStreamWriter xsw, String namespaceURIthrows XMLStreamException {
1119     writeAnnotationSet((Collection<Annotation>)annotations, annotations.getName(), xsw,
1120             namespaceURI);
1121   }
1122 
1123   /**
1124    * Writes the given annotation set to an XMLStreamWriter as GATE XML
1125    * format. The value for the Name attribute of the generated
1126    * AnnotationSet element is given by <code>asName</code>.
1127    
1128    @param annotations the annotation set to write
1129    @param asName the name under which to write the annotation set.
1130    *          <code>null</code> means that no name will be used.
1131    @param xsw the writer to use for output
1132    @param namespaceURI
1133    @throws XMLStreamException
1134    */
1135   public static void writeAnnotationSet(Collection<Annotation> annotations,
1136           String asName, XMLStreamWriter xsw, String namespaceURI)
1137           throws XMLStreamException {
1138     xsw.writeStartElement(namespaceURI, "AnnotationSet");
1139     if(asName != null) {
1140       xsw.writeAttribute("Name", asName);
1141     }
1142     newLine(xsw);
1143 
1144     if(annotations != null) {
1145       Iterator<Annotation> iterator = annotations.iterator();
1146       while(iterator.hasNext()) {
1147         Annotation annot = iterator.next();
1148         xsw.writeStartElement(namespaceURI, "Annotation");
1149         xsw.writeAttribute("Id", String.valueOf(annot.getId()));
1150         xsw.writeAttribute("Type", annot.getType());
1151         xsw.writeAttribute("StartNode", String.valueOf(annot.getStartNode()
1152                 .getOffset()));
1153         xsw.writeAttribute("EndNode", String.valueOf(annot.getEndNode()
1154                 .getOffset()));
1155         newLine(xsw);
1156         writeFeatures(annot.getFeatures(), xsw, namespaceURI);
1157         xsw.writeEndElement();
1158         newLine(xsw);
1159       }
1160     }
1161     // end AnnotationSet element
1162     xsw.writeEndElement();
1163     newLine(xsw);
1164   }
1165   
1166   public static void writeRelationSet(RelationSet relations,
1167           XMLStreamWriter xsw, String namespaceURIthrows XMLStreamException {
1168 
1169     // if there are no relations then don't write the set, this means
1170     // that docs without relations will remain compatible with earlier
1171     // versions of GATE
1172     if(relations == null || relations.size() == 0return;
1173 
1174     xsw.writeComment(" Relation Set for "
1175             + relations.getAnnotationSet().getName() " ");
1176     newLine(xsw);
1177     newLine(xsw);
1178 
1179     xsw.writeStartElement(namespaceURI, "RelationSet");
1180 
1181     if(relations.getAnnotationSet().getName() != null) {
1182       xsw.writeAttribute("Name", relations.getAnnotationSet().getName());
1183     }
1184     newLine(xsw);
1185 
1186     for(Relation relation : relations.get()) {
1187 
1188       StringBuilder str = new StringBuilder();
1189       int[] members = relation.getMembers();
1190       for(int i = 0; i < members.length; i++) {
1191         if(i > 0str.append(";");
1192         str.append(members[i]);
1193       }
1194       xsw.writeStartElement(namespaceURI, "Relation");
1195       xsw.writeAttribute("Id", String.valueOf(relation.getId()));
1196       xsw.writeAttribute("Type", relation.getType());
1197       xsw.writeAttribute("Members", str.toString());
1198       newLine(xsw);
1199 
1200       xsw.writeStartElement(namespaceURI, "UserData");
1201       if(relation.getUserData() != null) {
1202         ObjectWrapper userData = new ObjectWrapper(relation.getUserData());
1203         writeCharactersOrCDATA(xsw,
1204                 replaceXMLIllegalCharactersInString(userData.toString()));
1205       }
1206       xsw.writeEndElement();
1207       newLine(xsw);
1208 
1209       writeFeatures(relation.getFeatures(), xsw, namespaceURI);
1210       xsw.writeEndElement();
1211       newLine(xsw);
1212     }
1213 
1214     // end RelationSet element
1215     xsw.writeEndElement();
1216     newLine(xsw);
1217   }
1218 
1219   /**
1220    * Retained for binary compatibility, new code should call the
1221    <code>Collection&lt;Annotation&gt;</code> version instead.
1222    */
1223   public static void writeAnnotationSet(AnnotationSet annotations,
1224           String asName, XMLStreamWriter xsw, String namespaceURI)
1225           throws XMLStreamException {
1226     writeAnnotationSet((Collection<Annotation>)annotations, asName, xsw, namespaceURI);
1227   }
1228 
1229   /**
1230    * Writes the content of the given document to an XMLStreamWriter as a
1231    * mixed content element called "TextWithNodes". At each point where
1232    * there is the start or end of an annotation in any annotation set on
1233    * the document, a "Node" element is written with an "id" feature
1234    * whose value is the offset of that node.
1235    
1236    @param doc the document whose content is to be written
1237    @param annotationSets the annotations for which nodes are required.
1238    *          This is a collection of collections.
1239    @param xsw the {@link XMLStreamWriter} to write to.
1240    @param namespaceURI the namespace URI. May be empty but may not be
1241    *          null.
1242    @throws XMLStreamException
1243    */
1244   public static void writeTextWithNodes(Document doc,
1245           Collection<Collection<Annotation>> annotationSets,
1246           XMLStreamWriter xsw, String namespaceURIthrows XMLStreamException {
1247     String aText = doc.getContent().toString();
1248     // no text, so return an empty element
1249     if(aText == null) {
1250       xsw.writeEmptyElement(namespaceURI, "TextWithNodes");
1251       return;
1252     }
1253 
1254     // build a set of all the offsets where Nodes are required
1255     TreeSet<Long> offsetsSet = new TreeSet<Long>();
1256     if(annotationSets != null) {
1257       for(Collection<Annotation> set : annotationSets) {
1258         if(set != null) {
1259           for(Annotation annot : set) {
1260             offsetsSet.add(annot.getStartNode().getOffset());
1261             offsetsSet.add(annot.getEndNode().getOffset());
1262           }
1263         }
1264       }
1265     }
1266 
1267     // write the TextWithNodes element
1268     char[] textArray = aText.toCharArray();
1269     xsw.writeStartElement(namespaceURI, "TextWithNodes");
1270     int lastNodeOffset = 0;
1271     // offsetsSet iterator is in ascending order of offset, as it is a
1272     // SortedSet
1273     Iterator<Long> offsetsIterator = offsetsSet.iterator();
1274     while(offsetsIterator.hasNext()) {
1275       int offset = offsetsIterator.next().intValue();
1276       // write characters since the last node output
1277       // replace XML-illegal characters in this slice of text - we
1278       // have to do this here rather than on the text as a whole in
1279       // case the node falls between the two halves of a surrogate
1280       // pair (in which case both halves are illegal and must be
1281       // replaced).
1282       replaceXMLIllegalCharacters(textArray, lastNodeOffset, offset - lastNodeOffset);
1283       writeCharactersOrCDATA(xsw, new String(textArray, lastNodeOffset, offset
1284               - lastNodeOffset));
1285       xsw.writeEmptyElement(namespaceURI, "Node");
1286       xsw.writeAttribute("id", String.valueOf(offset));
1287       lastNodeOffset = offset;
1288     }
1289     // write any remaining text after the last node
1290     replaceXMLIllegalCharacters(textArray, lastNodeOffset, textArray.length - lastNodeOffset);
1291     writeCharactersOrCDATA(xsw, new String(textArray, lastNodeOffset,
1292             textArray.length - lastNodeOffset));
1293     // and the closing TextWithNodes
1294     xsw.writeEndElement();
1295   }
1296 
1297   /**
1298    * Write a TextWithNodes section containing nodes for all annotations
1299    * in the given document.
1300    
1301    @see #writeTextWithNodes(Document, Collection, XMLStreamWriter,
1302    *      String)
1303    */
1304   public static void writeTextWithNodes(Document doc, XMLStreamWriter xsw,
1305           String namespaceURIthrows XMLStreamException {
1306     Collection<Collection<Annotation>> annotationSets = new ArrayList<Collection<Annotation>>();
1307     annotationSets.add(doc.getAnnotations());
1308     if(doc.getNamedAnnotationSets() != null) {
1309       annotationSets.addAll(doc.getNamedAnnotationSets().values());
1310     }
1311     writeTextWithNodes(doc, annotationSets, xsw, namespaceURI);
1312   }
1313 
1314   /**
1315    * Replace any characters in the given buffer that are illegal in XML
1316    * with spaces. Characters that are illegal in XML are:
1317    <ul>
1318    <li>Control characters U+0000 to U+001F, <i>except</i> U+0009,
1319    * U+000A and U+000D, which are permitted.</li>
1320    <li><i>Unpaired</i> surrogates U+D800 to U+D8FF (valid surrogate
1321    * pairs are OK).</li>
1322    <li>U+FFFE and U+FFFF (only allowed as part of the Unicode byte
1323    * order mark).</li>
1324    </ul>
1325    
1326    @param buf the buffer to process
1327    */
1328   static void replaceXMLIllegalCharacters(char[] buf) {
1329     replaceXMLIllegalCharacters(buf, 0, buf.length);
1330   }
1331 
1332   /**
1333    * Replace any characters in the given buffer that are illegal in XML
1334    * with spaces. Characters that are illegal in XML are:
1335    <ul>
1336    <li>Control characters U+0000 to U+001F, <i>except</i> U+0009,
1337    * U+000A and U+000D, which are permitted.</li>
1338    <li><i>Unpaired</i> surrogates U+D800 to U+D8FF (valid surrogate
1339    * pairs are OK).</li>
1340    <li>U+FFFE and U+FFFF (only allowed as part of the Unicode byte
1341    * order mark).</li>
1342    </ul>
1343    
1344    @param buf the buffer to process
1345    */
1346   static void replaceXMLIllegalCharacters(char[] buf, int start, int len) {
1347     ArrayCharSequence bufSequence = new ArrayCharSequence(buf, start, len);
1348     for(int i = 0; i < len; i++) {
1349       if(isInvalidXmlChar(bufSequence, i)) {
1350         buf[start + i= INVALID_CHARACTER_REPLACEMENT;
1351       }
1352     }
1353   }
1354 
1355   /**
1356    * Return a string containing the same characters as the supplied
1357    * string, except that any characters that are illegal in XML will be
1358    * replaced with spaces. Characters that are illegal in XML are:
1359    <ul>
1360    <li>Control characters U+0000 to U+001F, <i>except</i> U+0009,
1361    * U+000A and U+000D, which are permitted.</li>
1362    <li><i>Unpaired</i> surrogates U+D800 to U+D8FF (valid surrogate
1363    * pairs are OK).</li>
1364    <li>U+FFFE and U+FFFF (only allowed as part of the Unicode byte
1365    * order mark).</li>
1366    </ul>
1367    
1368    * A new string is only created if required - if the supplied string
1369    * contains no illegal characters then the same object is returned.
1370    
1371    @param str the string to process
1372    @return <code>str</code>, unless it contains illegal characters
1373    *         in which case a new string the same as str but with the
1374    *         illegal characters replaced by spaces.
1375    */
1376   static String replaceXMLIllegalCharactersInString(String str) {
1377     StringBuilder builder = null;
1378     for(int i = 0; i < str.length(); i++) {
1379       if(isInvalidXmlChar(str, i)) {
1380         // lazily create the StringBuilder
1381         if(builder == null) {
1382           builder = new StringBuilder(str.substring(0, i));
1383         }
1384         builder.append(INVALID_CHARACTER_REPLACEMENT);
1385       }
1386       else if(builder != null) {
1387         builder.append(str.charAt(i));
1388       }
1389     }
1390 
1391     if(builder == null) {
1392       // no illegal characters were found
1393       return str;
1394     }
1395     else {
1396       return builder.toString();
1397     }
1398   }
1399 
1400   /**
1401    * Check whether a character is illegal in XML.
1402    
1403    @param buf the character sequence in which to look (must not be
1404    *          null)
1405    @param i the index of the character to check (must be within the
1406    *          valid range of characters in <code>buf</code>)
1407    */
1408   static final boolean isInvalidXmlChar(CharSequence buf, int i) {
1409     // illegal control character
1410     if(buf.charAt(i<= 0x0008 || buf.charAt(i== 0x000B
1411             || buf.charAt(i== 0x000C
1412             || (buf.charAt(i>= 0x000E && buf.charAt(i<= 0x001F)) {
1413       return true;
1414     }
1415 
1416     // buf.charAt(i) is a high surrogate...
1417     if(buf.charAt(i>= 0xD800 && buf.charAt(i<= 0xDBFF) {
1418       // if we're not at the end of the buffer we can look ahead
1419       if(i < buf.length() 1) {
1420         // followed by a low surrogate is OK
1421         if(buf.charAt(i + 1>= 0xDC00 && buf.charAt(i + 1<= 0xDFFF) {
1422           return false;
1423         }
1424       }
1425 
1426       // at the end of the buffer, or not followed by a low surrogate is
1427       // not OK.
1428       return true;
1429     }
1430 
1431     // buf.charAt(i) is a low surrogate...
1432     if(buf.charAt(i>= 0xDC00 && buf.charAt(i<= 0xDFFF) {
1433       // if we're not at the start of the buffer we can look behind
1434       if(i > 0) {
1435         // preceded by a high surrogate is OK
1436         if(buf.charAt(i - 1>= 0xD800 && buf.charAt(i - 1<= 0xDBFF) {
1437           return false;
1438         }
1439       }
1440 
1441       // at the start of the buffer, or not preceded by a high surrogate
1442       // is not OK
1443       return true;
1444     }
1445 
1446     // buf.charAt(i) is a BOM character
1447     if(buf.charAt(i== 0xFFFE || buf.charAt(i== 0xFFFF) {
1448       return true;
1449     }
1450 
1451     // anything else is OK
1452     return false;
1453   }
1454 
1455   /**
1456    * Write a feature map to the given XMLStreamWriter. The map is output
1457    * as a sequence of "Feature" elements, each having "Name" and "Value"
1458    * children. Note that there is no enclosing element - the caller must
1459    * write the enclosing "GateDocumentFeatures" or "Annotation" element.
1460    * Characters in feature values that are illegal in XML are replaced
1461    * by {@link #INVALID_CHARACTER_REPLACEMENT} (a space). Feature
1462    <i>names</i> are not modified - an illegal character in a feature
1463    * name will cause the serialization to fail.
1464    
1465    @param features
1466    @param xsw
1467    @param namespaceURI
1468    @throws XMLStreamException
1469    */
1470   public static void writeFeatures(FeatureMap features, XMLStreamWriter xsw,
1471           String namespaceURIthrows XMLStreamException {
1472     if(features == null) {
1473       return;
1474     }
1475 
1476     Set<Object> keySet = features.keySet();
1477     Iterator<Object> keySetIterator = keySet.iterator();
1478     FEATURES:while(keySetIterator.hasNext()) {
1479       Object key = keySetIterator.next();
1480       Object value = features.get(key);
1481       if(key != null && value != null) {
1482         String keyClassName = null;
1483         String keyItemClassName = null;
1484         String valueClassName = null;
1485         String valueItemClassName = null;
1486         String key2String = key.toString();
1487         String value2String = value.toString();
1488         Object item = null;
1489         // Test key if it is String or Number
1490         if(key instanceof java.lang.String || 
1491            key instanceof java.lang.Number) {
1492           keyClassName = key.getClass().getName();
1493         else {
1494           keyClassName = ObjectWrapper.class.getName();
1495           key2String = new ObjectWrapper(key).toString();
1496         }
1497           
1498         // Test value if it is String, Number or Boolean
1499         if(value instanceof java.lang.String
1500                 || value instanceof java.lang.Number
1501                 || value instanceof java.lang.Boolean){
1502           valueClassName = value.getClass().getName();
1503         else {
1504           valueClassName = ObjectWrapper.class.getName();
1505           value2String = new ObjectWrapper(value).toString();
1506         }
1507           
1508         xsw.writeStartElement(namespaceURI, "Feature");
1509         xsw.writeCharacters("\n  ");
1510 
1511         // write the Name
1512         xsw.writeStartElement(namespaceURI, "Name");
1513         if(keyClassName != null) {
1514           xsw.writeAttribute("className", keyClassName);
1515         }
1516         if(keyItemClassName != null) {
1517           xsw.writeAttribute("itemClassName", keyItemClassName);
1518         }
1519         xsw.writeCharacters(key2String);
1520         xsw.writeEndElement();
1521         xsw.writeCharacters("\n  ");
1522 
1523         // write the Value
1524         xsw.writeStartElement(namespaceURI, "Value");
1525         if(valueClassName != null) {
1526           xsw.writeAttribute("className", valueClassName);
1527         }
1528         if(valueItemClassName != null) {
1529           xsw.writeAttribute("itemClassName", valueItemClassName);
1530         }
1531         writeCharactersOrCDATA(xsw,
1532                 replaceXMLIllegalCharactersInString(value2String));
1533         xsw.writeEndElement();
1534         newLine(xsw);
1535 
1536         // close the Feature element
1537         xsw.writeEndElement();
1538         newLine(xsw);
1539       }
1540     }
1541   }
1542 
1543   /**
1544    * Convenience method to write a single new line to the given writer.
1545    
1546    @param xsw the XMLStreamWriter to write to.
1547    @throws XMLStreamException
1548    */
1549   static void newLine(XMLStreamWriter xswthrows XMLStreamException {
1550     xsw.writeCharacters("\n");
1551   }
1552 
1553   /**
1554    * The regular expression pattern that will match the end of a CDATA
1555    * section.
1556    */
1557   private static Pattern CDATA_END_PATTERN = Pattern.compile("\\]\\]>");
1558 
1559   /**
1560    * Write the given string to the given writer, using either
1561    * writeCharacters or, if there are more than a few less than signs in
1562    * the string (e.g. if it is an XML fragment itself), write it with
1563    * writeCData. This method properly handles the case where the string
1564    * contains other CDATA sections - as a CDATA section cannot contain
1565    * the CDATA end marker <code>]]></code>, we split the output CDATA
1566    * at any occurrences of this marker and write the marker using a
1567    * normal writeCharacters call in between.
1568    
1569    @param xsw the writer to write to
1570    @param string the string to write
1571    @throws XMLStreamException
1572    */
1573   static void writeCharactersOrCDATA(XMLStreamWriter xsw, String string)
1574           throws XMLStreamException {
1575     if(containsEnoughLTs(string)) {
1576       Matcher m = CDATA_END_PATTERN.matcher(string);
1577       int startFrom = 0;
1578       while(m.find()) {
1579         // we found a CDATA end marker, so write everything up to the
1580         // marker as CDATA...
1581         xsw.writeCData(string.substring(startFrom, m.start()));
1582         // then write the marker as characters
1583         xsw.writeCharacters("]]>");
1584         startFrom = m.end();
1585       }
1586 
1587       if(startFrom == 0) {
1588         // no "]]>" in the string, the normal case
1589         xsw.writeCData(string);
1590       }
1591       else if(startFrom < string.length()) {
1592         // there is some trailing text after the last ]]>
1593         xsw.writeCData(string.substring(startFrom));
1594       }
1595       // else the last ]]> was the end of the string, so nothing more to
1596       // do.
1597     }
1598     else {
1599       // if fewer '<' characters, just writeCharacters as normal
1600       xsw.writeCharacters(string);
1601     }
1602   }
1603 
1604   /**
1605    * Checks whether the given string contains at least
1606    <code>LT_THRESHOLD</code> &lt; characters.
1607    */
1608   private static boolean containsEnoughLTs(String string) {
1609     int numLTs = 0;
1610     int index = -1;
1611     while((index = string.indexOf('<', index + 1)) >= 0) {
1612       numLTs++;
1613       if(numLTs >= LT_THRESHOLD) {
1614         return true;
1615       }
1616     }
1617 
1618     return false;
1619   }
1620 
1621   // ///// Writing XCES /////
1622 
1623   /**
1624    * Comparator that compares annotations based on their offsets; when
1625    * two annotations start at the same location, the longer one is
1626    * considered to come first in the ordering.
1627    */
1628   public static final Comparator<Annotation> LONGEST_FIRST_OFFSET_COMPARATOR = new Comparator<Annotation>() {
1629     @Override
1630     public int compare(Annotation left, Annotation right) {
1631       long loffset = left.getStartNode().getOffset().longValue();
1632       long roffset = right.getStartNode().getOffset().longValue();
1633       if(loffset == roffset) {
1634         // if the start offsets are the same compare end
1635         // offsets.
1636         // the largest offset should come first
1637         loffset = left.getEndNode().getOffset().longValue();
1638         roffset = right.getEndNode().getOffset().longValue();
1639         if(loffset == roffset) {
1640           return left.getId() - right.getId();
1641         }
1642         else {
1643           return (int)(roffset - loffset);
1644         }
1645       }
1646       return (int)(loffset - roffset);
1647     }
1648   };
1649 
1650   /**
1651    * Save the content of a document to the given output stream. Since
1652    * XCES content files are plain text (not XML), XML-illegal characters
1653    * are not replaced when writing. The stream is <i>not</i> closed by
1654    * this method, that is left to the caller.
1655    
1656    @param doc the document to save
1657    @param out the stream to write to
1658    @param encoding the character encoding to use. If null, defaults to
1659    *          UTF-8
1660    */
1661   public static void writeXcesContent(Document doc, OutputStream out,
1662           String encodingthrows IOException {
1663     if(encoding == null) {
1664       encoding = "UTF-8";
1665     }
1666 
1667     String documentContent = doc.getContent().toString();
1668 
1669     OutputStreamWriter osw = new OutputStreamWriter(out, encoding);
1670     BufferedWriter writer = new BufferedWriter(osw);
1671     writer.write(documentContent);
1672     writer.flush();
1673     // do not close the writer, this would close the underlying stream,
1674     // which is something we want to leave to the caller
1675   }
1676 
1677   /**
1678    * Save annotations to the given output stream in XCES format, with
1679    * their IDs included as the "n" attribute of each <code>struct</code>.
1680    * The stream is <i>not</i> closed by this method, that is left to
1681    * the caller.
1682    
1683    @param annotations the annotations to save, typically an
1684    *          AnnotationSet
1685    @param os the output stream to write to
1686    @param encoding the character encoding to use.
1687    */
1688   public static void writeXcesAnnotations(Collection<Annotation> annotations,
1689           OutputStream os, String encodingthrows XMLStreamException {
1690     XMLStreamWriter xsw = null;
1691     try {
1692       if(outputFactory == null) {
1693         outputFactory = XMLOutputFactory.newInstance();
1694       }      
1695       if(encoding == null) {
1696         xsw = outputFactory.createXMLStreamWriter(os);
1697         xsw.writeStartDocument();
1698       }
1699       else {
1700         xsw = outputFactory.createXMLStreamWriter(os, encoding);
1701         xsw.writeStartDocument(encoding, "1.0");
1702       }
1703       newLine(xsw);
1704       writeXcesAnnotations(annotations, xsw);
1705     }
1706     finally {
1707       if(xsw != null) {
1708         xsw.close();
1709       }
1710     }
1711   }
1712 
1713   /**
1714    * Save annotations to the given XMLStreamWriter in XCES format, with
1715    * their IDs included as the "n" attribute of each <code>struct</code>.
1716    * The writer is <i>not</i> closed by this method, that is left to
1717    * the caller. This method writes just the cesAna element - the XML
1718    * declaration must be filled in by the caller if required.
1719    
1720    @param annotations the annotations to save, typically an
1721    *          AnnotationSet
1722    @param xsw the XMLStreamWriter to write to
1723    */
1724   public static void writeXcesAnnotations(Collection<Annotation> annotations,
1725           XMLStreamWriter xswthrows XMLStreamException {
1726     writeXcesAnnotations(annotations, xsw, true);
1727   }
1728 
1729   /**
1730    * Save annotations to the given XMLStreamWriter in XCES format. The
1731    * writer is <i>not</i> closed by this method, that is left to the
1732    * caller. This method writes just the cesAna element - the XML
1733    * declaration must be filled in by the caller if required. Characters
1734    * in feature values that are illegal in XML are replaced by
1735    {@link #INVALID_CHARACTER_REPLACEMENT} (a space). Feature <i>names</i>
1736    * are not modified, nor are annotation types - an illegal character
1737    * in one of these will cause the serialization to fail.
1738    
1739    @param annotations the annotations to save, typically an
1740    *          AnnotationSet
1741    @param xsw the XMLStreamWriter to write to
1742    @param includeId should we include the annotation IDs (as the "n"
1743    *          attribute on each <code>struct</code>)?
1744    @throws XMLStreamException
1745    */
1746   public static void writeXcesAnnotations(Collection<Annotation> annotations,
1747           XMLStreamWriter xsw, boolean includeIdthrows XMLStreamException {
1748     List<Annotation> annotsToDump = new ArrayList<Annotation>(annotations);
1749     Collections.sort(annotsToDump, LONGEST_FIRST_OFFSET_COMPARATOR);
1750 
1751     xsw.setDefaultNamespace(XCES_NAMESPACE);
1752     xsw.writeStartElement(XCES_NAMESPACE, "cesAna");
1753     xsw.writeDefaultNamespace(XCES_NAMESPACE);
1754     xsw.writeAttribute("version", XCES_VERSION);
1755     newLine(xsw);
1756 
1757     String indent = "   ";
1758     String indentMore = indent + indent;
1759 
1760     for(Annotation a : annotsToDump) {
1761       long start = a.getStartNode().getOffset().longValue();
1762       long end = a.getEndNode().getOffset().longValue();
1763       FeatureMap fm = a.getFeatures();
1764       xsw.writeCharacters(indent);
1765       if(fm == null || fm.size() == 0) {
1766         xsw.writeEmptyElement(XCES_NAMESPACE, "struct");
1767       }
1768       else {
1769         xsw.writeStartElement(XCES_NAMESPACE, "struct");
1770       }
1771       xsw.writeAttribute("type", a.getType());
1772       xsw.writeAttribute("from", String.valueOf(start));
1773       xsw.writeAttribute("to", String.valueOf(end));
1774       // include the annotation ID as the "n" attribute if requested
1775       if(includeId) {
1776         xsw.writeAttribute("n", String.valueOf(a.getId()));
1777       }
1778       newLine(xsw);
1779 
1780       if(fm != null && fm.size() != 0) {
1781         for(Map.Entry<Object,Object> att : fm.entrySet()) {
1782           if(!"isEmptyAndSpan".equals(att.getKey())) {
1783             xsw.writeCharacters(indentMore);
1784             xsw.writeEmptyElement(XCES_NAMESPACE, "feat");
1785             xsw.writeAttribute("name", String.valueOf(att.getKey()));
1786             xsw.writeAttribute("value",
1787                     replaceXMLIllegalCharactersInString(String.valueOf(att
1788                             .getValue())));
1789             newLine(xsw);
1790           }
1791         }
1792         xsw.writeCharacters(indent);
1793         xsw.writeEndElement();
1794         newLine(xsw);
1795       }
1796     }
1797 
1798     xsw.writeEndElement();
1799     newLine(xsw);
1800   }
1801 
1802   /** An inner class modeling the information contained by an annotation. */
1803   static class AnnotationObject {
1804     /** Constructor */
1805     public AnnotationObject() {
1806     }// AnnotationObject
1807 
1808     /** Accesor for the annotation type modeled here as ElemName */
1809     public String getElemName() {
1810       return elemName;
1811     }// getElemName
1812 
1813     /** Accesor for the feature map */
1814     public FeatureMap getFM() {
1815       return fm;
1816     }// getFM()
1817 
1818     /** Accesor for the start ofset */
1819     public Long getStart() {
1820       return start;
1821     }// getStart()
1822 
1823     /** Accesor for the end offset */
1824     public Long getEnd() {
1825       return end;
1826     }// getEnd()
1827 
1828     /** Mutator for the annotation type */
1829     public void setElemName(String anElemName) {
1830       elemName = anElemName;
1831     }// setElemName();
1832 
1833     /** Mutator for the feature map */
1834     public void setFM(FeatureMap aFm) {
1835       fm = aFm;
1836     }// setFM();
1837 
1838     /** Mutator for the start offset */
1839     public void setStart(Long aStart) {
1840       start = aStart;
1841     }// setStart();
1842 
1843     /** Mutator for the end offset */
1844     public void setEnd(Long anEnd) {
1845       end = anEnd;
1846     }// setEnd();
1847 
1848     /** Accesor for the id */
1849     public Integer getId() {
1850       return id;
1851     }// End of getId()
1852 
1853     /** Mutator for the id */
1854     public void setId(Integer anId) {
1855       id = anId;
1856     }// End of setId()
1857 
1858     @Override
1859     public String toString() {
1860       return " [id =" + id + " type=" + elemName + " startNode=" + start
1861               " endNode=" + end + " features=" + fm + "] ";
1862     }
1863 
1864     // Data fields
1865     private String elemName = null;
1866 
1867     private FeatureMap fm = null;
1868 
1869     private Long start = null;
1870 
1871     private Long end = null;
1872 
1873     private Integer id = null;
1874   // AnnotationObject
1875 
1876   /**
1877    * Thin wrapper class to use a char[] as a CharSequence. The array is
1878    * not copied - changes to the array are reflected by the CharSequence
1879    * methods.
1880    */
1881   static class ArrayCharSequence implements CharSequence {
1882     char[] array;
1883     int offset;
1884     int len;
1885 
1886     ArrayCharSequence(char[] array) {
1887       this(array, 0, array.length);
1888     }
1889 
1890     ArrayCharSequence(char[] array, int offset, int len) {
1891       this.array = array;
1892       this.offset = offset;
1893       this.len = len;
1894     }
1895 
1896     @Override
1897     public final char charAt(int i) {
1898       return array[offset + i];
1899     }
1900 
1901     @Override
1902     public final int length() {
1903       return len;
1904     }
1905 
1906     @Override
1907     public CharSequence subSequence(int start, int end) {
1908       return new ArrayCharSequence(array, offset + start, offset + end);
1909     }
1910 
1911     @Override
1912     public String toString() {
1913       return String.valueOf(array, offset, len);
1914     }
1915   // ArrayCharSequence
1916 }