View Javadoc
1   /*
2    * reserved comment block
3    * DO NOT REMOVE OR ALTER!
4    */
5   /*
6    * Copyright 1999-2002,2004 The Apache Software Foundation.
7    *
8    * Licensed under the Apache License, Version 2.0 (the "License");
9    * you may not use this file except in compliance with the License.
10   * You may obtain a copy of the License at
11   *
12   *      http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS,
16   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17   * See the License for the specific language governing permissions and
18   * limitations under the License.
19   */
20  
21  package com.sun.org.apache.xerces.internal.dom;
22  
23  import org.w3c.dom.CharacterData;
24  import org.w3c.dom.DOMException;
25  import org.w3c.dom.Node;
26  import org.w3c.dom.Text;
27  
28  /**
29   * Text nodes hold the non-markup, non-Entity content of
30   * an Element or Attribute.
31   * <P>
32   * When a document is first made available to the DOM, there is only
33   * one Text object for each block of adjacent plain-text. Users (ie,
34   * applications) may create multiple adjacent Texts during editing --
35   * see {@link org.w3c.dom.Element#normalize} for discussion.
36   * <P>
37   * Note that CDATASection is a subclass of Text. This is conceptually
38   * valid, since they're really just two different ways of quoting
39   * characters when they're written out as part of an XML stream.
40   *
41   * @xerces.internal
42   *
43   * @since  PR-DOM-Level-1-19980818.
44   */
45  public class TextImpl
46      extends CharacterDataImpl
47      implements CharacterData, Text {
48  
49      //
50      // Private Data members
51      //
52  
53  
54      //
55      // Constants
56      //
57  
58      /** Serialization version. */
59      static final long serialVersionUID = -5294980852957403469L;
60  
61      //
62      // Constructors
63      //
64  
65      /** Default constructor */
66      public TextImpl(){}
67  
68      /** Factory constructor. */
69      public TextImpl(CoreDocumentImpl ownerDoc, String data) {
70          super(ownerDoc, data);
71      }
72  
73      /**
74       * NON-DOM: resets node and sets specified values for the current node
75       *
76       * @param ownerDoc
77       * @param data
78       */
79      public void setValues(CoreDocumentImpl ownerDoc, String data){
80  
81          flags=0;
82          nextSibling = null;
83          previousSibling=null;
84          setOwnerDocument(ownerDoc);
85          super.data = data;
86      }
87      //
88      // Node methods
89      //
90  
91      /**
92       * A short integer indicating what type of node this is. The named
93       * constants for this value are defined in the org.w3c.dom.Node interface.
94       */
95      public short getNodeType() {
96          return Node.TEXT_NODE;
97      }
98  
99      /** Returns the node name. */
100     public String getNodeName() {
101         return "#text";
102     }
103 
104     /**
105      * NON-DOM: Set whether this Text is ignorable whitespace.
106      */
107     public void setIgnorableWhitespace(boolean ignore) {
108 
109         if (needsSyncData()) {
110             synchronizeData();
111         }
112         isIgnorableWhitespace(ignore);
113 
114     } // setIgnorableWhitespace(boolean)
115 
116 
117     /**
118      * DOM L3 Core CR - Experimental
119      *
120      * Returns whether this text node contains
121      * element content whitespace</a>, often abusively called "ignorable whitespace".
122      * The text node is determined to contain whitespace in element content
123      * during the load of the document or if validation occurs while using
124      * <code>Document.normalizeDocument()</code>.
125      * @since DOM Level 3
126      */
127     public boolean isElementContentWhitespace() {
128         // REVISIT: is this implemenation correct?
129         if (needsSyncData()) {
130             synchronizeData();
131         }
132         return internalIsIgnorableWhitespace();
133     }
134 
135 
136     /**
137      * DOM Level 3 WD - Experimental.
138      * Returns all text of <code>Text</code> nodes logically-adjacent text
139      * nodes to this node, concatenated in document order.
140      * @since DOM Level 3
141      */
142     public String getWholeText(){
143 
144         if (needsSyncData()) {
145             synchronizeData();
146         }
147 
148         if (fBufferStr == null){
149             fBufferStr = new StringBuffer();
150         }
151         else {
152             fBufferStr.setLength(0);
153         }
154         if (data != null && data.length() != 0) {
155             fBufferStr.append(data);
156         }
157 
158         //concatenate text of logically adjacent text nodes to the left of this node in the tree
159         getWholeTextBackward(this.getPreviousSibling(), fBufferStr, this.getParentNode());
160         String temp = fBufferStr.toString();
161 
162         //clear buffer
163         fBufferStr.setLength(0);
164 
165         //concatenate text of logically adjacent text nodes to the right of this node in the tree
166         getWholeTextForward(this.getNextSibling(), fBufferStr, this.getParentNode());
167 
168         return temp + fBufferStr.toString();
169 
170     }
171 
172     /**
173      * internal method taking a StringBuffer in parameter and inserts the
174      * text content at the start of the buffer
175      *
176      * @param buf
177      */
178     protected void insertTextContent(StringBuffer buf) throws DOMException {
179          String content = getNodeValue();
180          if (content != null) {
181              buf.insert(0, content);
182          }
183      }
184 
185     /**
186      * Concatenates the text of all logically-adjacent text nodes to the
187      * right of this node
188      * @param node
189      * @param buffer
190      * @param parent
191      * @return true - if execution was stopped because the type of node
192      *         other than EntityRef, Text, CDATA is encountered, otherwise
193      *         return false
194      */
195     private boolean getWholeTextForward(Node node, StringBuffer buffer, Node parent){
196         // boolean to indicate whether node is a child of an entity reference
197         boolean inEntRef = false;
198 
199         if (parent!=null) {
200                 inEntRef = parent.getNodeType()==Node.ENTITY_REFERENCE_NODE;
201         }
202 
203         while (node != null) {
204             short type = node.getNodeType();
205             if (type == Node.ENTITY_REFERENCE_NODE) {
206                 if (getWholeTextForward(node.getFirstChild(), buffer, node)){
207                     return true;
208                 }
209             }
210             else if (type == Node.TEXT_NODE ||
211                      type == Node.CDATA_SECTION_NODE) {
212                 ((NodeImpl)node).getTextContent(buffer);
213             }
214             else {
215                 return true;
216             }
217 
218             node = node.getNextSibling();
219         }
220 
221         // if the parent node is an entity reference node, must
222         // check nodes to the right of the parent entity reference node for logically adjacent
223         // text nodes
224         if (inEntRef) {
225             getWholeTextForward(parent.getNextSibling(), buffer, parent.getParentNode());
226                         return true;
227         }
228 
229         return false;
230     }
231 
232     /**
233      * Concatenates the text of all logically-adjacent text nodes to the left of
234      * the node
235      * @param node
236      * @param buffer
237      * @param parent
238      * @return true - if execution was stopped because the type of node
239      *         other than EntityRef, Text, CDATA is encountered, otherwise
240      *         return false
241      */
242     private boolean getWholeTextBackward(Node node, StringBuffer buffer, Node parent){
243 
244         // boolean to indicate whether node is a child of an entity reference
245         boolean inEntRef = false;
246         if (parent!=null) {
247                 inEntRef = parent.getNodeType()==Node.ENTITY_REFERENCE_NODE;
248         }
249 
250         while (node != null) {
251             short type = node.getNodeType();
252             if (type == Node.ENTITY_REFERENCE_NODE) {
253                 if (getWholeTextBackward(node.getLastChild(), buffer, node)){
254                     return true;
255                 }
256             }
257             else if (type == Node.TEXT_NODE ||
258                      type == Node.CDATA_SECTION_NODE) {
259                 ((TextImpl)node).insertTextContent(buffer);
260             }
261             else {
262                 return true;
263             }
264 
265             node = node.getPreviousSibling();
266         }
267 
268         // if the parent node is an entity reference node, must
269         // check nodes to the left of the parent entity reference node for logically adjacent
270         // text nodes
271         if (inEntRef) {
272                 getWholeTextBackward(parent.getPreviousSibling(), buffer, parent.getParentNode());
273             return true;
274         }
275 
276         return false;
277     }
278 
279     /**
280      * Replaces the text of the current node and all logically-adjacent text
281      * nodes with the specified text. All logically-adjacent text nodes are
282      * removed including the current node unless it was the recipient of the
283      * replacement text.
284      *
285      * @param content
286      *            The content of the replacing Text node.
287      * @return text - The Text node created with the specified content.
288      * @since DOM Level 3
289      */
290     public Text replaceWholeText(String content) throws DOMException {
291 
292         if (needsSyncData()) {
293             synchronizeData();
294         }
295 
296         //if the content is null
297         Node parent = this.getParentNode();
298         if (content == null || content.length() == 0) {
299             // remove current node
300             if (parent != null) { // check if node in the tree
301                 parent.removeChild(this);
302             }
303             return null;
304         }
305 
306         // make sure we can make the replacement
307         if (ownerDocument().errorChecking) {
308             if (!canModifyPrev(this)) {
309                 throw new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR,
310                         DOMMessageFormatter.formatMessage(
311                                 DOMMessageFormatter.DOM_DOMAIN,
312                                 "NO_MODIFICATION_ALLOWED_ERR", null));
313             }
314 
315             // make sure we can make the replacement
316             if (!canModifyNext(this)) {
317                 throw new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR,
318                         DOMMessageFormatter.formatMessage(
319                                 DOMMessageFormatter.DOM_DOMAIN,
320                                 "NO_MODIFICATION_ALLOWED_ERR", null));
321             }
322         }
323 
324         //replace the text node
325         Text currentNode = null;
326         if (isReadOnly()) {
327             Text newNode = this.ownerDocument().createTextNode(content);
328             if (parent != null) { // check if node in the tree
329                 parent.insertBefore(newNode, this);
330                 parent.removeChild(this);
331                 currentNode = newNode;
332             } else {
333                 return newNode;
334             }
335         } else {
336             this.setData(content);
337             currentNode = this;
338         }
339 
340         //check logically-adjacent text nodes
341         Node prev = currentNode.getPreviousSibling();
342         while (prev != null) {
343             //If the logically-adjacent next node can be removed
344             //remove it. A logically adjacent node can be removed if
345             //it is a Text or CDATASection node or an EntityReference with
346             //Text and CDATA only children.
347             if ((prev.getNodeType() == Node.TEXT_NODE)
348                     || (prev.getNodeType() == Node.CDATA_SECTION_NODE)
349                     || (prev.getNodeType() == Node.ENTITY_REFERENCE_NODE && hasTextOnlyChildren(prev))) {
350                 parent.removeChild(prev);
351                 prev = currentNode;
352             } else {
353                 break;
354             }
355             prev = prev.getPreviousSibling();
356         }
357 
358         //check logically-adjacent text nodes
359         Node next = currentNode.getNextSibling();
360         while (next != null) {
361             //If the logically-adjacent next node can be removed
362             //remove it. A logically adjacent node can be removed if
363             //it is a Text or CDATASection node or an EntityReference with
364             //Text and CDATA only children.
365             if ((next.getNodeType() == Node.TEXT_NODE)
366                     || (next.getNodeType() == Node.CDATA_SECTION_NODE)
367                     || (next.getNodeType() == Node.ENTITY_REFERENCE_NODE && hasTextOnlyChildren(next))) {
368                 parent.removeChild(next);
369                 next = currentNode;
370             } else {
371                 break;
372             }
373             next = next.getNextSibling();
374         }
375 
376         return currentNode;
377     }
378 
379     /**
380      * If any EntityReference to be removed has descendants that are not
381      * EntityReference, Text, or CDATASection nodes, the replaceWholeText method
382      * must fail before performing any modification of the document, raising a
383      * DOMException with the code NO_MODIFICATION_ALLOWED_ERR. Traverse previous
384      * siblings of the node to be replaced. If a previous sibling is an
385      * EntityReference node, get it's last child. If the last child was a Text
386      * or CDATASection node and its previous siblings are neither a replaceable
387      * EntityReference or Text or CDATASection nodes, return false. IF the last
388      * child was neither Text nor CDATASection nor a replaceable EntityReference
389      * Node, then return true. If the last child was a Text or CDATASection node
390      * any its previous sibling was not or was an EntityReference that did not
391      * contain only Text or CDATASection nodes, return false. Check this
392      * recursively for EntityReference nodes.
393      *
394      * @param node
395      * @return true - can replace text false - can't replace exception must be
396      *         raised
397      */
398     private boolean canModifyPrev(Node node) {
399         boolean textLastChild = false;
400 
401         Node prev = node.getPreviousSibling();
402 
403         while (prev != null) {
404 
405             short type = prev.getNodeType();
406 
407             if (type == Node.ENTITY_REFERENCE_NODE) {
408                 //If the previous sibling was entityreference
409                 //check if its content is replaceable
410                 Node lastChild = prev.getLastChild();
411 
412                 //if the entity reference has no children
413                 //return false
414                 if (lastChild == null) {
415                     return false;
416                 }
417 
418                 //The replacement text of the entity reference should
419                 //be either only text,cadatsections or replaceable entity
420                 //reference nodes or the last child should be neither of these
421                 while (lastChild != null) {
422                     short lType = lastChild.getNodeType();
423 
424                     if (lType == Node.TEXT_NODE
425                             || lType == Node.CDATA_SECTION_NODE) {
426                         textLastChild = true;
427                     } else if (lType == Node.ENTITY_REFERENCE_NODE) {
428                         if (!canModifyPrev(lastChild)) {
429                             return false;
430                         } else {
431                             //If the EntityReference child contains
432                             //only text, or non-text or ends with a
433                             //non-text node.
434                             textLastChild = true;
435                         }
436                     } else {
437                         //If the last child was replaceable and others are not
438                         //Text or CDataSection or replaceable EntityRef nodes
439                         //return false.
440                         if (textLastChild) {
441                             return false;
442                         } else {
443                             return true;
444                         }
445                     }
446                     lastChild = lastChild.getPreviousSibling();
447                 }
448             } else if (type == Node.TEXT_NODE
449                     || type == Node.CDATA_SECTION_NODE) {
450                 //If the previous sibling was text or cdatasection move to next
451             } else {
452                 //If the previous sibling was anything but text or
453                 //cdatasection or an entity reference, stop search and
454                 //return true
455                 return true;
456             }
457 
458             prev = prev.getPreviousSibling();
459         }
460 
461         return true;
462     }
463 
464     /**
465      * If any EntityReference to be removed has descendants that are not
466      * EntityReference, Text, or CDATASection nodes, the replaceWholeText method
467      * must fail before performing any modification of the document, raising a
468      * DOMException with the code NO_MODIFICATION_ALLOWED_ERR. Traverse previous
469      * siblings of the node to be replaced. If a previous sibling is an
470      * EntityReference node, get it's last child. If the first child was a Text
471      * or CDATASection node and its next siblings are neither a replaceable
472      * EntityReference or Text or CDATASection nodes, return false. IF the first
473      * child was neither Text nor CDATASection nor a replaceable EntityReference
474      * Node, then return true. If the first child was a Text or CDATASection
475      * node any its next sibling was not or was an EntityReference that did not
476      * contain only Text or CDATASection nodes, return false. Check this
477      * recursively for EntityReference nodes.
478      *
479      * @param node
480      * @return true - can replace text false - can't replace exception must be
481      *         raised
482      */
483     private boolean canModifyNext(Node node) {
484         boolean textFirstChild = false;
485 
486         Node next = node.getNextSibling();
487         while (next != null) {
488 
489             short type = next.getNodeType();
490 
491             if (type == Node.ENTITY_REFERENCE_NODE) {
492                 //If the previous sibling was entityreference
493                 //check if its content is replaceable
494                 Node firstChild = next.getFirstChild();
495 
496                 //if the entity reference has no children
497                 //return false
498                 if (firstChild == null) {
499                     return false;
500                 }
501 
502                 //The replacement text of the entity reference should
503                 //be either only text,cadatsections or replaceable entity
504                 //reference nodes or the last child should be neither of these
505                 while (firstChild != null) {
506                     short lType = firstChild.getNodeType();
507 
508                     if (lType == Node.TEXT_NODE
509                             || lType == Node.CDATA_SECTION_NODE) {
510                         textFirstChild = true;
511                     } else if (lType == Node.ENTITY_REFERENCE_NODE) {
512                         if (!canModifyNext(firstChild)) {
513                             return false;
514                         } else {
515                             //If the EntityReference child contains
516                             //only text, or non-text or ends with a
517                             //non-text node.
518                             textFirstChild = true;
519                         }
520                     } else {
521                         //If the first child was replaceable text and next
522                         //children are not, then return false
523                         if (textFirstChild) {
524                             return false;
525                         } else {
526                             return true;
527                         }
528                     }
529                     firstChild = firstChild.getNextSibling();
530                 }
531             } else if (type == Node.TEXT_NODE
532                     || type == Node.CDATA_SECTION_NODE) {
533                 //If the previous sibling was text or cdatasection move to next
534             } else {
535                 //If the next sibling was anything but text or
536                 //cdatasection or an entity reference, stop search and
537                 //return true
538                 return true;
539             }
540 
541             next = next.getNextSibling();
542         }
543 
544         return true;
545     }
546 
547     /**
548      * Check if an EntityReference node has Text Only child nodes
549      *
550      * @param node
551      * @return true - Contains text only children
552      */
553     private boolean hasTextOnlyChildren(Node node) {
554 
555         Node child = node;
556 
557         if (child == null) {
558             return false;
559         }
560 
561         child = child.getFirstChild();
562         while (child != null) {
563             int type = child.getNodeType();
564 
565             if (type == Node.ENTITY_REFERENCE_NODE) {
566                 return hasTextOnlyChildren(child);
567             }
568             else if (type != Node.TEXT_NODE
569                     && type != Node.CDATA_SECTION_NODE
570                     && type != Node.ENTITY_REFERENCE_NODE) {
571                 return false;
572             }
573             child = child.getNextSibling();
574         }
575         return true;
576     }
577 
578 
579     /**
580      * NON-DOM: Returns whether this Text is ignorable whitespace.
581      */
582     public boolean isIgnorableWhitespace() {
583 
584         if (needsSyncData()) {
585             synchronizeData();
586         }
587         return internalIsIgnorableWhitespace();
588 
589     } // isIgnorableWhitespace():boolean
590 
591 
592     //
593     // Text methods
594     //
595 
596     /**
597      * Break a text node into two sibling nodes. (Note that if the current node
598      * has no parent, they won't wind up as "siblings" -- they'll both be
599      * orphans.)
600      *
601      * @param offset
602      *            The offset at which to split. If offset is at the end of the
603      *            available data, the second node will be empty.
604      *
605      * @return A reference to the new node (containing data after the offset
606      *         point). The original node will contain data up to that point.
607      *
608      * @throws DOMException(INDEX_SIZE_ERR)
609      *             if offset is <0 or >length.
610      *
611      * @throws DOMException(NO_MODIFICATION_ALLOWED_ERR)
612      *             if node is read-only.
613      */
614     public Text splitText(int offset)
615         throws DOMException {
616 
617         if (isReadOnly()) {
618             throw new DOMException(
619             DOMException.NO_MODIFICATION_ALLOWED_ERR,
620                 DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN, "NO_MODIFICATION_ALLOWED_ERR", null));
621         }
622 
623         if (needsSyncData()) {
624             synchronizeData();
625         }
626         if (offset < 0 || offset > data.length() ) {
627             throw new DOMException(DOMException.INDEX_SIZE_ERR,
628                 DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN, "INDEX_SIZE_ERR", null));
629         }
630 
631         // split text into two separate nodes
632         Text newText =
633             getOwnerDocument().createTextNode(data.substring(offset));
634         setNodeValue(data.substring(0, offset));
635 
636         // insert new text node
637         Node parentNode = getParentNode();
638         if (parentNode != null) {
639             parentNode.insertBefore(newText, nextSibling);
640         }
641 
642         return newText;
643 
644     } // splitText(int):Text
645 
646 
647     /**
648      * NON-DOM (used by DOMParser): Reset data for the node.
649      */
650     public void replaceData (String value){
651         data = value;
652     }
653 
654 
655     /**
656      * NON-DOM (used by DOMParser: Sets data to empty string.
657      *  Returns the value the data was set to.
658      */
659     public String removeData (){
660         String olddata=data;
661         data = "";
662         return olddata;
663     }
664 
665 } // class TextImpl