1
2
3
4
5
6
7
8
9
10 package org.dom4j.io;
11
12 import java.lang.reflect.Method;
13 import java.util.ArrayList;
14 import java.util.HashMap;
15 import java.util.List;
16 import java.util.Map;
17
18 import org.dom4j.Branch;
19 import org.dom4j.Document;
20 import org.dom4j.DocumentFactory;
21 import org.dom4j.DocumentType;
22 import org.dom4j.Element;
23 import org.dom4j.ElementHandler;
24 import org.dom4j.Namespace;
25 import org.dom4j.QName;
26 import org.dom4j.dtd.AttributeDecl;
27 import org.dom4j.dtd.ElementDecl;
28 import org.dom4j.dtd.ExternalEntityDecl;
29 import org.dom4j.dtd.InternalEntityDecl;
30 import org.dom4j.tree.AbstractElement;
31 import org.dom4j.tree.NamespaceStack;
32 import org.xml.sax.Attributes;
33 import org.xml.sax.DTDHandler;
34 import org.xml.sax.EntityResolver;
35 import org.xml.sax.InputSource;
36 import org.xml.sax.Locator;
37 import org.xml.sax.SAXException;
38 import org.xml.sax.SAXParseException;
39 import org.xml.sax.ext.DeclHandler;
40 import org.xml.sax.ext.LexicalHandler;
41 import org.xml.sax.helpers.DefaultHandler;
42
43 /*** <p><code>SAXContentHandler</code> builds a dom4j tree via SAX events.</p>
44 *
45 * @author <a href="mailto:jstrachan@apache.org">James Strachan</a>
46 * @version $Revision: 1.59 $
47 */
48 public class SAXContentHandler extends DefaultHandler implements LexicalHandler, DeclHandler, DTDHandler {
49
50 /*** The factory used to create new <code>Document</code> instances */
51 private DocumentFactory documentFactory;
52
53 /*** The document that is being built */
54 private Document document;
55
56 /*** stack of <code>Element</code> objects */
57 private ElementStack elementStack;
58
59 /*** stack of <code>Namespace</code> and <code>QName</code> objects */
60 private NamespaceStack namespaceStack;
61
62 /*** the <code>ElementHandler</code> called as the elements are complete */
63 private ElementHandler elementHandler;
64
65 /*** the Locator */
66 private Locator locator;
67
68 /*** The name of the current entity */
69 private String entity;
70
71 /*** Flag used to indicate that we are inside a DTD section */
72 private boolean insideDTDSection;
73
74 /*** Flag used to indicate that we are inside a CDATA section */
75 private boolean insideCDATASection;
76
77 /*** buffer to hold contents of cdata section across multiple characters events */
78 private StringBuffer cdataText;
79
80 /*** namespaces that are available for use */
81 private Map availableNamespaceMap = new HashMap();
82
83 /*** declared namespaces that are not yet available for use */
84 private List declaredNamespaceList = new ArrayList();
85
86 /*** internal DTD declarations */
87 private List internalDTDDeclarations;
88
89 /*** external DTD declarations */
90 private List externalDTDDeclarations;
91
92 /*** The number of namespaces that are declared in the current scope */
93 private int declaredNamespaceIndex;
94
95 /*** The entity resolver */
96 private EntityResolver entityResolver;
97
98 private InputSource inputSource;
99
100 /*** The current element we are on */
101 private Element currentElement;
102
103 /*** Should internal DTD declarations be expanded into a List in the DTD */
104 private boolean includeInternalDTDDeclarations = false;
105
106 /*** Should external DTD declarations be expanded into a List in the DTD */
107 private boolean includeExternalDTDDeclarations = false;
108
109 /*** The number of levels deep we are inside a startEntity / endEntity call */
110 private int entityLevel;
111
112 /*** Are we in an internal DTD subset? */
113 private boolean internalDTDsubset = false;
114
115 /*** Whether adjacent text nodes should be merged */
116 private boolean mergeAdjacentText = false;
117
118 /*** Have we added text to the buffer */
119 private boolean textInTextBuffer = false;
120
121 /*** Should we ignore comments */
122 private boolean ignoreComments = false;
123
124 /*** Buffer used to concatenate text together */
125 private StringBuffer textBuffer;
126
127 /*** Holds value of property stripWhitespaceText. */
128 private boolean stripWhitespaceText = false;
129
130
131 public SAXContentHandler() {
132 this(DocumentFactory.getInstance());
133 }
134
135 public SAXContentHandler(DocumentFactory documentFactory) {
136 this(documentFactory, null);
137 }
138
139 public SAXContentHandler(DocumentFactory documentFactory, ElementHandler elementHandler) {
140 this(documentFactory, elementHandler, null);
141 this.elementStack = createElementStack();
142 }
143
144 public SAXContentHandler(DocumentFactory documentFactory, ElementHandler elementHandler, ElementStack elementStack) {
145 this.documentFactory = documentFactory;
146 this.elementHandler = elementHandler;
147 this.elementStack = elementStack;
148 this.namespaceStack = new NamespaceStack(documentFactory);
149 }
150
151 /*** @return the document that has been or is being built
152 */
153 public Document getDocument() {
154 if ( document == null ) {
155 document = createDocument();
156 }
157 return document;
158 }
159
160
161
162
163 public void setDocumentLocator(Locator locator) {
164 this.locator = locator;
165 }
166
167 public void processingInstruction(String target, String data) throws SAXException {
168 if ( mergeAdjacentText && textInTextBuffer ) {
169 completeCurrentTextNode();
170 }
171 if ( currentElement != null ) {
172 currentElement.addProcessingInstruction(target, data);
173 }
174 else {
175 getDocument().addProcessingInstruction(target, data);
176 }
177 }
178
179 public void startPrefixMapping(String prefix, String uri) throws SAXException {
180 namespaceStack.push( prefix, uri );
181 }
182
183 public void endPrefixMapping(String prefix) throws SAXException {
184 namespaceStack.pop( prefix );
185 declaredNamespaceIndex = namespaceStack.size();
186 }
187
188 public void startDocument() throws SAXException {
189
190 document = null;
191 currentElement = null;
192
193 elementStack.clear();
194
195 if ( (elementHandler != null) &&
196 (elementHandler instanceof DispatchHandler) ) {
197 elementStack.setDispatchHandler((DispatchHandler)elementHandler);
198 }
199
200 namespaceStack.clear();
201 declaredNamespaceIndex = 0;
202
203 if ( mergeAdjacentText && textBuffer == null ) {
204 textBuffer = new StringBuffer();
205 }
206 textInTextBuffer = false;
207 }
208
209 public void endDocument() throws SAXException {
210 namespaceStack.clear();
211 elementStack.clear();
212 currentElement = null;
213 textBuffer = null;
214 }
215
216 public void startElement(String namespaceURI, String localName, String qualifiedName, Attributes attributes) throws SAXException {
217 if ( mergeAdjacentText && textInTextBuffer ) {
218 completeCurrentTextNode();
219 }
220
221 QName qName = namespaceStack.getQName(
222 namespaceURI, localName, qualifiedName
223 );
224
225 Branch branch = currentElement;
226 if ( branch == null ) {
227 branch = getDocument();
228 }
229 Element element = branch.addElement(qName);
230
231
232 addDeclaredNamespaces(element);
233
234
235 addAttributes( element, attributes );
236
237 elementStack.pushElement(element);
238 currentElement = element;
239
240 entity = null;
241
242 if ( elementHandler != null ) {
243 elementHandler.onStart(elementStack);
244 }
245 }
246
247 public void endElement(String namespaceURI, String localName, String qName) throws SAXException {
248 if ( mergeAdjacentText && textInTextBuffer ) {
249 completeCurrentTextNode();
250 }
251
252 if ( elementHandler != null && currentElement != null ) {
253 elementHandler.onEnd(elementStack);
254 }
255 elementStack.popElement();
256 currentElement = elementStack.peekElement();
257 }
258
259 public void characters(char[] ch, int start, int end) throws SAXException {
260 if ( end == 0 ) {
261 return;
262 }
263
264 if ( currentElement != null ) {
265 if (entity != null) {
266 if ( mergeAdjacentText && textInTextBuffer ) {
267 completeCurrentTextNode();
268 }
269 currentElement.addEntity(entity, new String(ch, start, end));
270 entity = null;
271 }
272 else if (insideCDATASection) {
273 if ( mergeAdjacentText && textInTextBuffer ) {
274 completeCurrentTextNode();
275 }
276 cdataText.append(new String(ch, start, end));
277 }
278 else {
279 if ( mergeAdjacentText ) {
280 textBuffer.append(ch, start, end);
281 textInTextBuffer = true;
282 }
283 else {
284 currentElement.addText(new String(ch, start, end));
285 }
286 }
287 }
288 }
289
290
291
292
293 /*** This method is called when a warning occurs during the parsing
294 * of the document.
295 * This method does nothing.
296 */
297 public void warning(SAXParseException exception) throws SAXException {
298
299 }
300
301 /*** This method is called when an error is detected during parsing
302 * such as a validation error.
303 * This method rethrows the exception
304 */
305 public void error(SAXParseException exception) throws SAXException {
306 throw exception;
307 }
308
309 /*** This method is called when a fatal error occurs during parsing.
310 * This method rethrows the exception
311 */
312 public void fatalError(SAXParseException exception) throws SAXException {
313 throw exception;
314 }
315
316
317
318
319 public void startDTD(String name, String publicId, String systemId) throws SAXException {
320 getDocument().addDocType(name, publicId, systemId);
321 insideDTDSection = true;
322 internalDTDsubset = true;
323 }
324
325 public void endDTD() throws SAXException {
326 insideDTDSection = false;
327
328 DocumentType docType = getDocument().getDocType();
329 if ( docType != null ) {
330 if ( internalDTDDeclarations != null ) {
331 docType.setInternalDeclarations( internalDTDDeclarations );
332 }
333 if ( externalDTDDeclarations != null ) {
334 docType.setExternalDeclarations( externalDTDDeclarations );
335 }
336 }
337
338 internalDTDDeclarations = null;
339 externalDTDDeclarations = null;
340 }
341
342 public void startEntity(String name) throws SAXException {
343 ++entityLevel;
344
345
346 entity = null;
347 if (! insideDTDSection ) {
348 if ( ! isIgnorableEntity(name) ) {
349 entity = name;
350 }
351 }
352
353
354
355
356
357
358
359 internalDTDsubset = false;
360 }
361
362 public void endEntity(String name) throws SAXException {
363 --entityLevel;
364 entity = null;
365 if ( entityLevel == 0 ) {
366 internalDTDsubset = true;
367 }
368
369 }
370
371 public void startCDATA() throws SAXException {
372 insideCDATASection = true;
373 cdataText = new StringBuffer();
374 }
375
376 public void endCDATA() throws SAXException {
377 insideCDATASection = false;
378 currentElement.addCDATA(cdataText.toString());
379 }
380
381 public void comment(char[] ch, int start, int end) throws SAXException {
382 if (!ignoreComments) {
383 if ( mergeAdjacentText && textInTextBuffer ) {
384 completeCurrentTextNode();
385 }
386 String text = new String(ch, start, end);
387 if (!insideDTDSection && text.length() > 0) {
388 if ( currentElement != null ) {
389 currentElement.addComment(text);
390 }
391 else {
392 getDocument().addComment(text);
393 }
394 }
395 }
396 }
397
398
399
400
401 /***
402 * Report an element type declaration.
403 *
404 * <p>The content model will consist of the string "EMPTY", the
405 * string "ANY", or a parenthesised group, optionally followed
406 * by an occurrence indicator. The model will be normalized so
407 * that all parameter entities are fully resolved and all whitespace
408 * is removed,and will include the enclosing parentheses. Other
409 * normalization (such as removing redundant parentheses or
410 * simplifying occurrence indicators) is at the discretion of the
411 * parser.</p>
412 *
413 * @param name The element type name.
414 * @param model The content model as a normalized string.
415 * @exception SAXException The application may raise an exception.
416 */
417 public void elementDecl(String name, String model) throws SAXException {
418 if ( internalDTDsubset ) {
419 if ( includeInternalDTDDeclarations ) {
420 addDTDDeclaration( new ElementDecl( name, model ) );
421 }
422 }
423 else {
424 if ( includeExternalDTDDeclarations ) {
425 addExternalDTDDeclaration( new ElementDecl( name, model ) );
426 }
427 }
428 }
429
430 /***
431 * Report an attribute type declaration.
432 *
433 * <p>Only the effective (first) declaration for an attribute will
434 * be reported. The type will be one of the strings "CDATA",
435 * "ID", "IDREF", "IDREFS", "NMTOKEN", "NMTOKENS", "ENTITY",
436 * "ENTITIES", a parenthesized token group with
437 * the separator "|" and all whitespace removed, or the word
438 * "NOTATION" followed by a space followed by a parenthesized
439 * token group with all whitespace removed.</p>
440 *
441 * <p>Any parameter entities in the attribute value will be
442 * expanded, but general entities will not.</p>
443 *
444 * @param eName The name of the associated element.
445 * @param aName The name of the attribute.
446 * @param type A string representing the attribute type.
447 * @param valueDefault A string representing the attribute default
448 * ("#IMPLIED", "#REQUIRED", or "#FIXED") or null if
449 * none of these applies.
450 * @param value A string representing the attribute's default value,
451 * or null if there is none.
452 * @exception SAXException The application may raise an exception.
453 */
454 public void attributeDecl(String eName,String aName,String type,String valueDefault,String value) throws SAXException {
455 if ( internalDTDsubset ) {
456 if ( includeInternalDTDDeclarations ) {
457 addDTDDeclaration( new AttributeDecl( eName, aName, type, valueDefault, value) );
458 }
459 }
460 else {
461 if ( includeExternalDTDDeclarations ) {
462 addExternalDTDDeclaration( new AttributeDecl( eName, aName, type, valueDefault, value) );
463 }
464 }
465 }
466
467 /***
468 * Report an internal entity declaration.
469 *
470 * <p>Only the effective (first) declaration for each entity
471 * will be reported. All parameter entities in the value
472 * will be expanded, but general entities will not.</p>
473 *
474 * @param name The name of the entity. If it is a parameter
475 * entity, the name will begin with '%'.
476 * @param value The replacement text of the entity.
477 * @exception SAXException The application may raise an exception.
478 * @see #externalEntityDecl
479 * @see org.xml.sax.DTDHandler#unparsedEntityDecl
480 */
481 public void internalEntityDecl(String name, String value) throws SAXException {
482 if ( internalDTDsubset ) {
483 if ( includeInternalDTDDeclarations ) {
484 addDTDDeclaration( new InternalEntityDecl( name, value ) );
485 }
486 }
487 else {
488 if ( includeExternalDTDDeclarations ) {
489 addExternalDTDDeclaration( new InternalEntityDecl( name, value ) );
490 }
491 }
492 }
493
494 /***
495 * Report a parsed external entity declaration.
496 *
497 * <p>Only the effective (first) declaration for each entity
498 * will be reported.</p>
499 *
500 * @param name The name of the entity. If it is a parameter
501 * entity, the name will begin with '%'.
502 * @param publicId The declared public identifier of the entity, or
503 * null if none was declared.
504 * @param systemId The declared system identifier of the entity.
505 * @exception SAXException The application may raise an exception.
506 * @see #internalEntityDecl
507 * @see org.xml.sax.DTDHandler#unparsedEntityDecl
508 */
509 public void externalEntityDecl(String name, String publicId, String systemId) throws SAXException {
510 if ( internalDTDsubset ) {
511 if ( includeInternalDTDDeclarations ) {
512 addDTDDeclaration( new ExternalEntityDecl( name, publicId, systemId ) );
513 }
514 }
515 else {
516 if ( includeExternalDTDDeclarations ) {
517 addExternalDTDDeclaration( new ExternalEntityDecl( name, publicId, systemId ) );
518 }
519 }
520 }
521
522
523
524
525
526 /***
527 * Receive notification of a notation declaration event.
528 *
529 * <p>It is up to the application to record the notation for later
530 * reference, if necessary.</p>
531 *
532 * <p>At least one of publicId and systemId must be non-null.
533 * If a system identifier is present, and it is a URL, the SAX
534 * parser must resolve it fully before passing it to the
535 * application through this event.</p>
536 *
537 * <p>There is no guarantee that the notation declaration will be
538 * reported before any unparsed entities that use it.</p>
539 *
540 * @param name The notation name.
541 * @param publicId The notation's public identifier, or null if
542 * none was given.
543 * @param systemId The notation's system identifier, or null if
544 * none was given.
545 * @exception org.xml.sax.SAXException Any SAX exception, possibly
546 * wrapping another exception.
547 * @see #unparsedEntityDecl
548 * @see org.xml.sax.AttributeList
549 */
550 public void notationDecl(String name,String publicId,String systemId) throws SAXException {
551
552 }
553
554 /***
555 * Receive notification of an unparsed entity declaration event.
556 *
557 * <p>Note that the notation name corresponds to a notation
558 * reported by the {@link #notationDecl notationDecl} event.
559 * It is up to the application to record the entity for later
560 * reference, if necessary.</p>
561 *
562 * <p>If the system identifier is a URL, the parser must resolve it
563 * fully before passing it to the application.</p>
564 *
565 * @exception org.xml.sax.SAXException Any SAX exception, possibly
566 * wrapping another exception.
567 * @param name The unparsed entity's name.
568 * @param publicId The entity's public identifier, or null if none
569 * was given.
570 * @param systemId The entity's system identifier.
571 * @param notationName The name of the associated notation.
572 * @see #notationDecl
573 * @see org.xml.sax.AttributeList
574 */
575 public void unparsedEntityDecl(String name,String publicId,String systemId,String notationName) throws SAXException {
576
577 }
578
579
580
581
582 public ElementStack getElementStack() {
583 return elementStack;
584 }
585
586 public void setElementStack(ElementStack elementStack) {
587 this.elementStack = elementStack;
588 }
589
590 public EntityResolver getEntityResolver() {
591 return entityResolver;
592 }
593
594 public void setEntityResolver(EntityResolver entityResolver) {
595 this.entityResolver = entityResolver;
596 }
597
598 public InputSource getInputSource() {
599 return inputSource;
600 }
601
602 public void setInputSource(InputSource inputSource) {
603 this.inputSource = inputSource;
604 }
605
606 /*** @return whether internal DTD declarations should be expanded into the DocumentType
607 * object or not.
608 */
609 public boolean isIncludeInternalDTDDeclarations() {
610 return includeInternalDTDDeclarations;
611 }
612
613 /*** Sets whether internal DTD declarations should be expanded into the DocumentType
614 * object or not.
615 *
616 * @param includeInternalDTDDeclarations whether or not DTD declarations should be expanded
617 * and included into the DocumentType object.
618 */
619 public void setIncludeInternalDTDDeclarations(boolean includeInternalDTDDeclarations) {
620 this.includeInternalDTDDeclarations = includeInternalDTDDeclarations;
621 }
622
623 /*** @return whether external DTD declarations should be expanded into the DocumentType
624 * object or not.
625 */
626 public boolean isIncludeExternalDTDDeclarations() {
627 return includeExternalDTDDeclarations;
628 }
629
630 /*** Sets whether DTD external declarations should be expanded into the DocumentType
631 * object or not.
632 *
633 * @param includeExternalDTDDeclarations whether or not DTD declarations should be expanded
634 * and included into the DocumentType object.
635 */
636 public void setIncludeExternalDTDDeclarations(boolean includeExternalDTDDeclarations) {
637 this.includeExternalDTDDeclarations = includeExternalDTDDeclarations;
638 }
639
640 /*** Returns whether adjacent text nodes should be merged together.
641 * @return Value of property mergeAdjacentText.
642 */
643 public boolean isMergeAdjacentText() {
644 return mergeAdjacentText;
645 }
646
647 /*** Sets whether or not adjacent text nodes should be merged
648 * together when parsing.
649 * @param mergeAdjacentText New value of property mergeAdjacentText.
650 */
651 public void setMergeAdjacentText(boolean mergeAdjacentText) {
652 this.mergeAdjacentText = mergeAdjacentText;
653 }
654
655
656 /*** Sets whether whitespace between element start and end tags should be ignored
657 *
658 * @return Value of property stripWhitespaceText.
659 */
660 public boolean isStripWhitespaceText() {
661 return stripWhitespaceText;
662 }
663
664 /*** Sets whether whitespace between element start and end tags should be ignored.
665 *
666 * @param stripWhitespaceText New value of property stripWhitespaceText.
667 */
668 public void setStripWhitespaceText(boolean stripWhitespaceText) {
669 this.stripWhitespaceText = stripWhitespaceText;
670 }
671
672 /***
673 * Returns whether we should ignore comments or not.
674 * @return boolean
675 */
676 public boolean isIgnoreComments() {
677 return ignoreComments;
678 }
679
680 /***
681 * Sets whether we should ignore comments or not.
682 * @param ignoreComments whether we should ignore comments or not.
683 */
684 public void setIgnoreComments(boolean ignoreComments) {
685 this.ignoreComments = ignoreComments;
686 }
687
688
689
690
691
692 /*** If the current text buffer contains any text then create a new
693 * text node with it and add it to the current element
694 */
695 protected void completeCurrentTextNode() {
696 if ( stripWhitespaceText ) {
697 boolean whitespace = true;
698 for ( int i = 0, size = textBuffer.length(); i < size; i++ ) {
699 if ( ! Character.isWhitespace( textBuffer.charAt(i) ) ) {
700 whitespace = false;
701 break;
702 }
703 }
704 if ( ! whitespace ) {
705 currentElement.addText( textBuffer.toString() );
706 }
707 }
708 else {
709 currentElement.addText( textBuffer.toString() );
710 }
711 textBuffer.setLength(0);
712 textInTextBuffer = false;
713 }
714
715 /*** @return the current document
716 */
717 protected Document createDocument() {
718 String encoding = getEncoding();
719 Document document = documentFactory.createDocument(encoding);
720
721
722 document.setEntityResolver(entityResolver);
723 if (inputSource != null) {
724 document.setName(inputSource.getSystemId());
725 }
726
727 return document;
728 }
729
730 private String getEncoding() {
731 if (locator == null) {
732 return null;
733 }
734
735
736
737 try {
738 Method m = locator.getClass().getMethod("getEncoding", new Class[]{});
739 if (m != null) {
740 return (String) m.invoke(locator, null);
741 }
742 } catch (Exception e) {
743
744 }
745
746
747 return null;
748 }
749
750 /*** a Strategy Method to determine if a given entity name is ignorable
751 */
752 protected boolean isIgnorableEntity(String name) {
753 return "amp".equals( name )
754 || "apos".equals( name )
755 || "gt".equals( name )
756 || "lt".equals( name )
757 || "quot".equals( name );
758 }
759
760
761 /*** Add all namespaces declared before the startElement() SAX event
762 * to the current element so that they are available to child elements
763 * and attributes
764 */
765 protected void addDeclaredNamespaces(Element element) {
766 Namespace elementNamespace = element.getNamespace();
767 for ( int size = namespaceStack.size(); declaredNamespaceIndex < size; declaredNamespaceIndex++ ) {
768 Namespace namespace = namespaceStack.getNamespace(declaredNamespaceIndex);
769
770 element.add( namespace );
771
772 }
773 }
774
775 /*** Add all the attributes to the given elements
776 */
777 protected void addAttributes( Element element, Attributes attributes ) {
778
779
780
781 boolean noNamespaceAttributes = false;
782 if ( element instanceof AbstractElement ) {
783
784 AbstractElement baseElement = (AbstractElement) element;
785 baseElement.setAttributes( attributes, namespaceStack, noNamespaceAttributes );
786 }
787 else {
788 int size = attributes.getLength();
789 for ( int i = 0; i < size; i++ ) {
790 String attributeQualifiedName = attributes.getQName(i);
791 if ( noNamespaceAttributes || ! attributeQualifiedName.startsWith( "xmlns" ) ) {
792 String attributeURI = attributes.getURI(i);
793 String attributeLocalName = attributes.getLocalName(i);
794 String attributeValue = attributes.getValue(i);
795
796 QName attributeQName = namespaceStack.getAttributeQName(
797 attributeURI, attributeLocalName, attributeQualifiedName
798 );
799 element.addAttribute(attributeQName, attributeValue);
800 }
801 }
802 }
803 }
804
805
806 /*** Adds an internal DTD declaration to the list of declarations */
807 protected void addDTDDeclaration(Object declaration) {
808 if ( internalDTDDeclarations == null ) {
809 internalDTDDeclarations = new ArrayList();
810 }
811 internalDTDDeclarations.add( declaration );
812 }
813
814 /*** Adds an external DTD declaration to the list of declarations */
815 protected void addExternalDTDDeclaration(Object declaration) {
816 if ( externalDTDDeclarations == null ) {
817 externalDTDDeclarations = new ArrayList();
818 }
819 externalDTDDeclarations.add( declaration );
820 }
821
822 protected ElementStack createElementStack() {
823 return new ElementStack();
824 }
825 }
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873