1   /*
2    *  XmlDocumentHandler.java
3    *
4    *  Copyright (c) 1998-2005, The University of Sheffield.
5    *
6    *  This file is part of GATE (see http://gate.ac.uk/), and is free
7    *  software, licenced under the GNU Library General Public License,
8    *  Version 2, June 1991 (in the distribution as file licence.html,
9    *  and also available at http://gate.ac.uk/gate/licence.html).
10   *
11   *  Cristian URSU,  9 May 2000
12   *
13   *  $Id: XmlDocumentHandler.java,v 1.49 2005/01/11 13:51:38 ian Exp $
14   */
15  
16  package gate.xml;
17  
18  import java.util.*;
19  
20  import org.xml.sax.*;
21  
22  import gate.*;
23  import gate.corpora.DocumentContentImpl;
24  import gate.corpora.RepositioningInfo;
25  import gate.event.StatusListener;
26  import gate.util.*;
27  
28  
29  /**
30    * Implements the behaviour of the XML reader
31    * Methods of an object of this class are called by the SAX parser when
32    * events will appear.
33    * The idea is to parse the XML document and construct Gate annotations
34    * objects.
35    * This class also will replace the content of the Gate document with a
36    * new one containing only text from the XML document.
37    */
38  public class XmlDocumentHandler extends XmlPositionCorrectionHandler {
39    /** Debug flag */
40    private static final boolean DEBUG = false;
41  
42    /** Keep the refference to this structure */
43    private RepositioningInfo reposInfo = null;
44  
45    /** Keep the refference to this structure */
46    private RepositioningInfo ampCodingInfo = null;
47  
48  
49    /** This is used to capture all data within two tags before calling the actual characters method */
50    private StringBuffer contentBuffer = new StringBuffer("");
51  
52    /** This is a variable that shows if characters have been read */
53    private boolean readCharacterStatus = false;
54  
55  
56    /** Set repositioning information structure refference. If you set this
57     *  refference to <B>null</B> information wouldn't be collected.
58     */
59    public void setRepositioningInfo(RepositioningInfo info) {
60      reposInfo = info;
61    } // setRepositioningInfo
62  
63    /** Return current RepositioningInfo object */
64    public RepositioningInfo getRepositioningInfo() {
65      return reposInfo;
66    } // getRepositioningInfo
67  
68    /** Set repositioning information structure refference for ampersand coding.
69     *  If you set this refference to <B>null</B> information wouldn't be used.
70     */
71    public void setAmpCodingInfo(RepositioningInfo info) {
72      ampCodingInfo = info;
73    } // setRepositioningInfo
74  
75    /** Return current RepositioningInfo object for ampersand coding. */
76    public RepositioningInfo getAmpCodingInfo() {
77      return ampCodingInfo;
78    } // getRepositioningInfo
79  
80  
81    /**
82      * Constructs a XmlDocumentHandler object. The annotationSet set will be the
83      * default one taken from the gate document.
84      * @param aDocument the Gate document that will be processed.
85      * @param aMarkupElementsMap this map contains the elements name that we
86      * want to create.
87      * @param anElement2StringMap this map contains the strings that will be
88      * added to the text contained by the key element.
89      */
90    public XmlDocumentHandler(gate.Document aDocument, Map  aMarkupElementsMap,
91                              Map anElement2StringMap){
92      this(aDocument,aMarkupElementsMap,anElement2StringMap,null);
93    } // XmlDocumentHandler
94  
95    /**
96      * Constructs a XmlDocumentHandler object.
97      * @param aDocument the Gate document that will be processed.
98      * @param aMarkupElementsMap this map contains the elements name that we
99      * want to create.
100     * @param anElement2StringMap this map contains the strings that will be
101     * added to the text contained by the key element.
102     * @param anAnnotationSet is the annotation set that will be filled when the
103     * document was processed
104     */
105   public XmlDocumentHandler(gate.Document       aDocument,
106                             Map                 aMarkupElementsMap,
107                             Map                 anElement2StringMap,
108                             gate.AnnotationSet  anAnnotationSet){
109     // init parent
110     super();
111     // init stack
112     stack = new java.util.Stack();
113 
114     // this string contains the plain text (the text without markup)
115     tmpDocContent = new StringBuffer(aDocument.getContent().size().intValue());
116 
117     // colector is used later to transform all custom objects into annotation
118     // objects
119     colector = new LinkedList();
120 
121     // the Gate document
122     doc = aDocument;
123 
124     // this map contains the elements name that we want to create
125     // if it's null all the elements from the XML documents will be transformed
126     // into Gate annotation objects
127     markupElementsMap = aMarkupElementsMap;
128 
129     // this map contains the string that we want to insert iside the document
130     // content, when a certain element is found
131     // if the map is null then no string is added
132     element2StringMap = anElement2StringMap;
133 
134     basicAS = anAnnotationSet;
135     customObjectsId = 0;
136   }// XmlDocumentHandler()/
137 
138   /**
139     * This method is called when the SAX parser encounts the beginning of the
140     * XML document.
141     */
142   public void startDocument() throws org.xml.sax.SAXException {
143     // init of variables in the parent
144     super.startDocument();
145   }
146 
147   /**
148     * This method is called when the SAX parser encounts the end of the
149     * XML document.
150     * Here we set the content of the gate Document to be the one generated
151     * inside this class (tmpDocContent).
152     * After that we use the colector to generate all the annotation reffering
153     * this new gate document.
154     */
155   public void endDocument() throws org.xml.sax.SAXException {
156 
157     // replace the document content with the one without markups
158     doc.setContent(new DocumentContentImpl(tmpDocContent.toString()));
159 
160     // fire the status listener
161     fireStatusChangedEvent("Total elements: " + elements);
162 
163     // If basicAs is null then get the default AnnotationSet,
164     // based on the gate document.
165     if (basicAS == null)
166       basicAS=doc.getAnnotations(GateConstants.ORIGINAL_MARKUPS_ANNOT_SET_NAME);
167 
168     // sort colector ascending on its id
169     Collections.sort(colector);
170     Set testIdsSet = new HashSet();
171     // create all the annotations (on this new document) from the collector
172     while (!colector.isEmpty()){
173       CustomObject obj = (CustomObject) colector.getFirst();
174       // Test to see if there are two annotation objects with the same id.
175       if (testIdsSet.contains(obj.getId())){
176         throw new GateSaxException("Found two annotations with the same Id("+
177         obj.getId()+
178         ").The document is inconsistent.");
179       }else{
180         testIdsSet.add(obj.getId());
181       }// End iff
182       // create a new annotation and add it to the annotation set
183       try{
184         // the annotation type will be conforming with markupElementsMap
185         //add the annotation to the Annotation Set
186         if (markupElementsMap == null)
187           basicAS.add(  obj.getId(),
188                         obj.getStart(),
189                         obj.getEnd(),
190                         obj.getElemName(),
191                         obj.getFM ());
192         else {
193           // get the type of the annotation from Map
194           String annotationType = (String)
195                                 markupElementsMap.get(obj.getElemName());
196           if (annotationType != null)
197             basicAS.add( obj.getId(),
198                          obj.getStart(),
199                          obj.getEnd(),
200                          annotationType,
201                          obj.getFM());
202         }// End if
203       }catch (gate.util.InvalidOffsetException e){
204         Err.prln("InvalidOffsetException for annot :" + obj.getElemName() +
205          " with Id =" + obj.getId() + ". Discarded...");
206       }// End try
207       colector.remove(obj);
208     }// End while
209   }// endDocument();
210 
211   /**
212     * This method is called when the SAX parser encounts the beginning of an
213     * XML element.
214     */
215   public void startElement (String uri, String qName, String elemName,
216                                                              Attributes atts) throws SAXException {
217 
218      // call characterActions
219      if(readCharacterStatus) {
220        readCharacterStatus = false;
221        charactersAction(new String(contentBuffer).toCharArray(),0,contentBuffer.length());
222      }
223 
224     // Inform the progress listener to fire only if no of elements processed
225     // so far is a multiple of ELEMENTS_RATE
226     if ((++elements % ELEMENTS_RATE) == 0)
227         fireStatusChangedEvent("Processed elements : " + elements);
228 
229     Integer customObjectId = null;
230     // Construct a SimpleFeatureMapImpl from the list of attributes
231     FeatureMap fm = Factory.newFeatureMap();
232     //Get the name and the value of the attributes and add them to a FeaturesMAP
233     for (int i = 0; i < atts.getLength(); i++) {
234       String attName  = atts.getLocalName(i);
235       String attValue = atts.getValue(i);
236       String attUri =   atts.getURI(i);
237       if (attUri != null && Gate.URI.equals(attUri)){
238         if ("gateId".equals(attName)){
239           customObjectId = new Integer(attValue);
240         }// End if
241         if ("annotMaxId".equals(attName)){
242           customObjectsId = new Integer(attValue).intValue();
243         }// End if
244         if ("matches".equals(attName)){
245           StringTokenizer strTokenizer = new StringTokenizer(attValue,";");
246           List list = new ArrayList();
247           // Take all tokens,create Integers and add them to the list
248           while (strTokenizer.hasMoreTokens()){
249             String token = strTokenizer.nextToken();
250             list.add(new Integer(token));
251           }// End while
252           fm.put(attName,list);
253         }// End if
254       }else{
255         fm.put(atts.getQName(i), attValue);
256       }// End if
257     }// End for
258 
259     // create the START index of the annotation
260     Long startIndex = new Long(tmpDocContent.length());
261 
262     // initialy the Start index is equal with End index
263     CustomObject obj = new CustomObject(customObjectId,elemName,fm,
264                                                  startIndex, startIndex);
265 
266     // put this object into the stack
267     stack.push(obj);
268   }// startElement();
269 
270   /**
271     * This method is called when the SAX parser encounts the end of an
272     * XML element.
273     * Here we extract
274     */
275   public void endElement (String uri, String qName, String elemName )
276                                                          throws SAXException{
277      // call characterActions
278      if(readCharacterStatus) {
279        readCharacterStatus = false;
280        charactersAction(new String(contentBuffer).toCharArray(),0,contentBuffer.length());
281      }
282 
283     // obj is for internal use
284     CustomObject obj = null;
285 
286     // if the stack is not empty, we extract the custom object and delete it
287     if (!stack.isEmpty ()){
288       obj = (CustomObject) stack.pop();
289     }// End if
290 
291     // Before adding it to the colector, we need to check if is an
292     // emptyAndSpan one. See CustomObject's isEmptyAndSpan field.
293     if (obj.getStart().equals(obj.getEnd())){
294       // The element had an end tag and its start was equal to its end. Hence
295       // it is anEmptyAndSpan one.
296       obj.getFM().put("isEmptyAndSpan","true");
297     }// End iff
298 
299     // Put the object into colector
300     // Later, when the document ends we will use colector to create all the
301     // annotations
302     colector.add(obj);
303 
304     // if element is found on Element2String map, then add the string to the
305     // end of the document content
306     if (element2StringMap != null){
307       String stringFromMap = null;
308 
309       // test to see if element is inside the map
310       // if it is then get the string value and add it to the document content
311       stringFromMap = (String) element2StringMap.get(elemName);
312       if (stringFromMap != null)
313           tmpDocContent.append(stringFromMap);
314     }// End if
315   }// endElement();
316 
317   /**
318     * This method is called when the SAX parser encounts text in the XML doc.
319     * Here we calculate the end indices for all the elements present inside the
320     * stack and update with the new values. For entities, this method is called
321     * separatley regardless of the text sourinding the entity.
322     */
323    public void characters(char [] text,int start,int length) throws SAXException {
324      if(!readCharacterStatus) {
325        contentBuffer = new StringBuffer(new String(text,start,length));
326      } else {
327        contentBuffer.append(new String(text,start,length));
328      }
329      readCharacterStatus = true;
330    }
331 
332    /**
333      * This method is called when all characters between specific tags have been read completely
334      */
335   public void charactersAction( char[] text,int start,int length) throws SAXException{
336     // correction of real offset. Didn't affect on other data.
337     super.characters(text, start, length);
338     // create a string object based on the reported text
339     String content = new String(text, start, length);
340     StringBuffer contentBuffer = new StringBuffer("");
341     int tmpDocContentSize = tmpDocContent.length();
342     boolean incrementStartIndex = false;
343     boolean addExtraSpace = true;
344     if ( Gate.getUserConfig().get(
345           GateConstants.DOCUMENT_ADD_SPACE_ON_UNPACK_FEATURE_NAME)!= null)
346       addExtraSpace =
347         Gate.getUserConfig().getBoolean(
348           GateConstants.DOCUMENT_ADD_SPACE_ON_UNPACK_FEATURE_NAME
349         ).booleanValue();
350     // If the first char of the text just read "text[0]" is NOT whitespace AND
351     // the last char of the tmpDocContent[SIZE-1] is NOT whitespace then
352     // concatenation "tmpDocContent + content" will result into a new different
353     // word... and we want to avoid that, because the tokenizer, gazetter and
354     // Jape work on the raw text and concatenating tokens might be not good.
355     if ( tmpDocContentSize != 0 &&
356          content.length() != 0 &&
357          !Character.isWhitespace(content.charAt(0)) &&
358          !Character.isWhitespace(tmpDocContent.charAt(tmpDocContentSize - 1))){
359 
360          // If we are here it means that a concatenation between the last
361          // token in the tmpDocContent and the content(which doesn't start
362          // with a white space) will be performed. In order to prevent this,
363          // we will add a " " space char in order to assure that the 2 tokens
364          // stay apart. Howerver we will except from this rule the most known
365          // internal entities like &, <, >, etc
366          if (
367               (
368                  // Testing the length against 1 makes it more likely that
369                  // an internal entity was called. characters() gets called for
370                  // each entity separately.
371                  (content.length() == 1)
372                   &&
373                  (content.charAt(0) == '&' ||
374                   content.charAt(0) == '<' ||
375                   content.charAt(0) == '>' ||
376                   content.charAt(0) == '"' ||
377                   content.charAt(0) == '\''
378                   )
379                ) ||
380                (tmpDocContent.charAt(tmpDocContentSize - 1) == '&' ||
381                 tmpDocContent.charAt(tmpDocContentSize - 1) == '<' ||
382                 tmpDocContent.charAt(tmpDocContentSize - 1) == '>' ||
383                 tmpDocContent.charAt(tmpDocContentSize - 1) == '"' ||
384                 tmpDocContent.charAt(tmpDocContentSize - 1) == '\''
385                )){// do nothing. The content will be appended
386          }else if (!addExtraSpace) {
387          }else
388            {
389             // In all other cases append " "
390             contentBuffer.append(" ");
391             incrementStartIndex = true;
392         }// End if
393     }// End if
394 
395     // put the repositioning information
396     if(reposInfo != null) {
397       if(! (start == 0 && length == 1 && text.length <= 2)) {
398         // normal piece of text
399         reposInfo.addPositionInfo(getRealOffset(), content.length(),
400                       tmpDocContent.length()+contentBuffer.length(),
401                       content.length());
402         if(DEBUG) {
403           Out.println("Info: "+getRealOffset()+", "+content.length());
404           Out.println("Start: "+start+" len"+length);
405         } // DEBUG
406       }
407       else {
408         // unicode char or &xxx; coding
409         // Reported from the parser offset is 0
410         // The real offset should be found in the ampCodingInfo structure.
411 
412         long lastPosition = 0;
413         RepositioningInfo.PositionInfo pi;
414 
415         if(reposInfo.size() > 0) {
416           pi =
417             (RepositioningInfo.PositionInfo) reposInfo.get(reposInfo.size()-1);
418           lastPosition = pi.getOriginalPosition();
419         } // if
420 
421         for(int i = 0; i < ampCodingInfo.size(); ++i) {
422           pi = (RepositioningInfo.PositionInfo) ampCodingInfo.get(i);
423           if(pi.getOriginalPosition() > lastPosition) {
424             // found
425             reposInfo.addPositionInfo(pi.getOriginalPosition(),
426                           pi.getOriginalLength(),
427                           tmpDocContent.length()+contentBuffer.length(),
428                           content.length());
429             break;
430           } // if
431         } // for
432       } // if
433     } // if
434 
435     // update the document content
436     contentBuffer.append(content);
437     // calculate the End index for all the elements of the stack
438     // the expression is : End index = Current doc length + text length
439     Long end = new Long(tmpDocContent.length() + contentBuffer.length());
440 
441     CustomObject obj = null;
442     // Iterate through stack to modify the End index of the existing elements
443 
444     java.util.Iterator anIterator = stack.iterator();
445     while (anIterator.hasNext ()){
446       // get the object and move to the next one
447       obj = (CustomObject) anIterator.next ();
448       if (incrementStartIndex && obj.getStart().equals(obj.getEnd())){
449         obj.setStart(new Long(obj.getStart().longValue() + 1));
450       }// End if
451       // sets its End index
452       obj.setEnd(end);
453     }// End while
454 
455     tmpDocContent.append(contentBuffer.toString());
456   }// characters();
457 
458   /**
459     * This method is called when the SAX parser encounts white spaces
460     */
461   public void ignorableWhitespace(char ch[],int start,int length) throws
462                                                                    SAXException{
463 
464     // internal String object
465     String  text = new String(ch, start, length);
466     // if the last character in tmpDocContent is \n and the read whitespace is
467     // \n then don't add it to tmpDocContent...
468 
469     if (tmpDocContent.length () != 0)
470       if (tmpDocContent.charAt (tmpDocContent.length () - 1) != '\n' ||
471         !text.equalsIgnoreCase("\n")
472       )
473          tmpDocContent.append(text);
474   }
475 
476   /**
477     * Error method.We deal with this exception inside SimpleErrorHandler class
478     */
479   public void error(SAXParseException ex) throws SAXException {
480     // deal with a SAXParseException
481     // see SimpleErrorhandler class
482     _seh.error(ex);
483   }
484 
485   /**
486     * FatalError method.
487     */
488   public void fatalError(SAXParseException ex) throws SAXException {
489     // deal with a SAXParseException
490     // see SimpleErrorhandler class
491     _seh.fatalError(ex);
492   }
493 
494   /**
495     * Warning method comment.
496     */
497   public void warning(SAXParseException ex) throws SAXException {
498     // deal with a SAXParseException
499     // see SimpleErrorhandler class
500     _seh.warning(ex);
501   }
502 
503   /**
504     * This method is called when the SAX parser encounts a comment
505     * It works only if the XmlDocumentHandler implements a
506     * com.sun.parser.LexicalEventListener
507     */
508   public void comment(String text) throws SAXException {
509     // create a FeatureMap and then add the comment to the annotation set.
510     /*
511     gate.util.SimpleFeatureMapImpl fm = new gate.util.SimpleFeatureMapImpl();
512     fm.put ("text_comment",text);
513     Long node = new Long (tmpDocContent.length());
514     CustomObject anObject = new CustomObject("Comment",fm,node,node);
515     colector.add(anObject);
516     */
517   }
518 
519   /**
520     * This method is called when the SAX parser encounts a start of a CDATA
521     * section
522     * It works only if the XmlDocumentHandler implements a
523     * com.sun.parser.LexicalEventListener
524     */
525   public void startCDATA()throws SAXException {
526   }
527 
528   /**
529     * This method is called when the SAX parser encounts the end of a CDATA
530     * section.
531     * It works only if the XmlDocumentHandler implements a
532     * com.sun.parser.LexicalEventListener
533     */
534   public void endCDATA() throws SAXException {
535   }
536 
537   /**
538     * This method is called when the SAX parser encounts a parsed Entity
539     * It works only if the XmlDocumentHandler implements a
540     * com.sun.parser.LexicalEventListener
541     */
542   public void startParsedEntity(String name) throws SAXException {
543   }
544 
545   /**
546     * This method is called when the SAX parser encounts a parsed entity and
547     * informs the application if that entity was parsed or not
548     * It's working only if the CustomDocumentHandler implements a
549     *  com.sun.parser.LexicalEventListener
550     */
551   public void endParsedEntity(String name, boolean included)throws SAXException{
552   }
553 
554   //StatusReporter Implementation
555 
556   /**
557     * This methos is called when a listener is registered with this class
558     */
559   public void addStatusListener(StatusListener listener){
560     myStatusListeners.add(listener);
561   }
562   /**
563     * This methos is called when a listener is removed
564     */
565   public void removeStatusListener(StatusListener listener){
566     myStatusListeners.remove(listener);
567   }
568   /**
569     * This methos is called whenever we need to inform the listener about an
570     * event.
571   */
572   protected void fireStatusChangedEvent(String text){
573     Iterator listenersIter = myStatusListeners.iterator();
574     while(listenersIter.hasNext())
575       ((StatusListener)listenersIter.next()).statusChanged(text);
576   }
577 
578   /** This method is a workaround of the java 4 non namespace supporting parser
579     * It receives a qualified name and returns its local name.
580     * For eg. if it receives gate:gateId it will return gateId
581     */
582   private String getMyLocalName(String aQName){
583     if (aQName == null) return "";
584     StringTokenizer strToken = new StringTokenizer(aQName,":");
585     if (strToken.countTokens()<= 1) return aQName;
586     // The nr of tokens is >= than 2
587     // Skip the first token which is the QName
588     strToken.nextToken();
589     return strToken.nextToken();
590   }//getMyLocalName()
591 
592   /** Also a workaround for URI identifier. If the QName is gate it will return
593     *  GATE's. Otherwhise it will return the empty string
594     */
595   private String getMyURI(String aQName){
596     if (aQName == null) return "";
597     StringTokenizer strToken = new StringTokenizer(aQName,":");
598     if (strToken.countTokens()<= 1) return "";
599     // If first token is "gate" then return GATE's URI
600     if ("gate".equalsIgnoreCase(strToken.nextToken()))
601       return Gate.URI;
602     return "";
603   }// getMyURI()
604 
605   // XmlDocumentHandler member data
606 
607   // this constant indicates when to fire the status listener
608   // this listener will add an overhead and we don't want a big overhead
609   // this listener will be callled from ELEMENTS_RATE to ELEMENTS_RATE
610   final static  int ELEMENTS_RATE = 128;
611 
612   // this map contains the elements name that we want to create
613   // if it's null all the elements from the XML documents will be transformed
614   // into Gate annotation objects otherwise only the elements it contains will
615   // be transformed
616   private Map markupElementsMap = null;
617 
618   // this map contains the string that we want to insert iside the document
619   // content, when a certain element is found
620   // if the map is null then no string is added
621   private Map element2StringMap = null;
622 
623   /**This object inducates what to do when the parser encounts an error*/
624   private SimpleErrorHandler _seh = new SimpleErrorHandler();
625 
626   /**The content of the XML document, without any tag for internal use*/
627   private StringBuffer tmpDocContent = null;
628 
629   /**A stack used to remember elements and to keep the order */
630   private java.util.Stack stack = null;
631 
632   /**A gate document */
633   private gate.Document doc = null;
634 
635   /**An annotation set used for creating annotation reffering the doc */
636   private gate.AnnotationSet basicAS = null;
637 
638   /**Listeners for status report */
639   protected List myStatusListeners = new LinkedList();
640 
641   /**This reports the the number of elements that have beed processed so far*/
642   private int elements = 0;
643 
644   /** We need a colection to retain all the CustomObjects that will be
645     * transformed into annotation over the gate document...
646     * the transformation will take place inside onDocumentEnd() method
647     */
648   private LinkedList colector = null;
649 
650   /** This is used to generate unique Ids for the CustomObjects read*/
651   protected  int customObjectsId = 0;
652 
653   /** Accesor method for the customObjectsId field*/
654   public int getCustomObjectsId(){ return customObjectsId;}
655 
656   //////// INNER CLASS
657   /**
658     * The objects belonging to this class are used inside the stack.
659     * This class is for internal needs
660     */
661   class  CustomObject implements Comparable {
662 
663     // constructor
664     public CustomObject(Integer anId,String anElemName, FeatureMap aFm,
665                            Long aStart, Long anEnd) {
666       elemName = anElemName;
667       fm = aFm;
668       start = aStart;
669       end = anEnd;
670       if (anId == null){
671         id = new Integer(customObjectsId ++);
672       }else{
673         id = anId;
674         if (customObjectsId <= anId.intValue())
675           customObjectsId = anId.intValue() + 1 ;
676       }// End if
677     }// End CustomObject()
678 
679     // Methos implemented as required by Comparable interface
680     public int compareTo(Object o){
681       CustomObject obj = (CustomObject) o;
682       return this.id.compareTo(obj.getId());
683     }// compareTo();
684 
685     // accesor
686     public String getElemName() {
687       return elemName;
688     }// getElemName()
689 
690     public FeatureMap getFM() {
691       return fm;
692     }// getFM()
693 
694     public Long getStart() {
695       return start;
696     }// getStart()
697 
698     public Long getEnd() {
699       return end;
700     }// getEnd()
701 
702     public Integer getId(){ return id;}
703 
704     // mutator
705     public void setElemName(String anElemName) {
706       elemName = anElemName;
707     }// getElemName()
708 
709     public void setFM(FeatureMap aFm) {
710       fm = aFm;
711     }// setFM();
712 
713     public void setStart(Long aStart) {
714       start = aStart;
715     }// setStart();
716 
717     public void setEnd(Long anEnd) {
718       end = anEnd;
719     }// setEnd();
720 
721     // data fields
722     private String elemName = null;
723     private FeatureMap fm = null;
724     private Long start = null;
725     private Long end  = null;
726     private Integer id = null;
727 
728   } // End inner class CustomObject
729 
730 } //XmlDocumentHandler
731 
732 
733 
734