| XmlDocumentHandler.java |
1 /*
2 * XmlDocumentHandler.java
3 *
4 * Copyright (c) 1998-2004, 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.48 2004/07/21 17:10:10 akshay 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