View Javadoc
1   /*
2    * Copyright (c) 2002-2025 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.html;
16  
17  import org.htmlunit.SgmlPage;
18  import org.w3c.dom.CharacterData;
19  
20  /**
21   * Wrapper for the DOM node CharacterData.
22   *
23   * @author David K. Taylor
24   * @author <a href="mailto:cse@dynabean.de">Christian Sell</a>
25   * @author Ahmed Ashour
26   * @author Philip Graf
27   * @author Ronald Brill
28   * @author Frank Danek
29   */
30  public abstract class DomCharacterData extends DomNode implements CharacterData {
31  
32      /** The data string. */
33      private String data_;
34  
35      /**
36       * Creates an instance of DomCharacterData.
37       *
38       * @param page the Page that contains this element
39       * @param data the data string wrapped by this node
40       */
41      public DomCharacterData(final SgmlPage page, final String data) {
42          super(page);
43          setData(data);
44      }
45  
46      /**
47       * Gets the data character string for this character data node.
48       * @return the data character string
49       */
50      @Override
51      public String getData() {
52          return data_;
53      }
54  
55      /**
56       * Sets the data character string for this character data node.
57       * @param data the new data character string
58       */
59      @Override
60      public void setData(final String data) {
61          final String oldData = data_;
62          data_ = data;
63  
64          final SgmlPage page = getPage();
65          if (page == null || page.isCharacterDataChangeListenerInUse()) {
66              fireCharacterDataChanged(this, oldData);
67          }
68      }
69  
70      /**
71       * Sets the data character string to the new string.
72       * @param newValue the new string of data
73       */
74      @Override
75      public void setNodeValue(final String newValue) {
76          setData(newValue);
77      }
78  
79      /**
80       * {@inheritDoc}
81       */
82      @Override
83      public void setTextContent(final String textContent) {
84          setData(textContent);
85      }
86  
87      /**
88       * Returns the number of characters in the character data.
89       * @return the number of characters
90       */
91      @Override
92      public int getLength() {
93          return data_.length();
94      }
95  
96      /**
97       * Appends a string to character data.
98       * @param newData the string to be appended to the character data
99       */
100     @Override
101     public void appendData(final String newData) {
102         data_ += newData;
103     }
104 
105     /**
106      * Deletes characters from character data.
107      * @param offset the position of the first character to be deleted (can't be
108      *        less than zero)
109      * @param count the number of characters to be deleted, if less than zero
110      *        leaves the first offset chars
111      */
112     @Override
113     public void deleteData(final int offset, final int count) {
114         if (offset < 0) {
115             throw new IllegalArgumentException("Provided offset: " + offset + " is less than zero.");
116         }
117 
118         final String data = data_.substring(0, offset);
119         if (count >= 0) {
120             final int fromLeft = offset + count;
121             if (fromLeft < data_.length()) {
122                 setData(data + data_.substring(fromLeft));
123                 return;
124             }
125         }
126         setData(data);
127     }
128 
129     /**
130      * Inserts a string into character data.
131      * @param offset the position within the first character at which the string is to be inserted
132      * @param arg the string to insert
133      */
134     @Override
135     public void insertData(final int offset, final String arg) {
136         setData(new StringBuilder(data_).insert(offset, arg).toString());
137     }
138 
139     /**
140      * Replaces characters of character data with a string.
141      * @param offset the position within the first character at which the string is to be replaced
142      * @param count the number of characters to be replaced
143      * @param arg the string that replaces the count characters beginning at the character at offset
144      */
145     @Override
146     public void replaceData(final int offset, final int count, final String arg) {
147         deleteData(offset, count);
148         insertData(offset, arg);
149     }
150 
151     /**
152      * Extracts a substring from character data.
153      * @param offset the position of the first character to be extracted
154      * @param count the number of characters to be extracted
155      * @return a string that consists of the count characters of the character data starting
156      *         from the character at position offset
157      */
158     @Override
159     public String substringData(final int offset, final int count) {
160         final int length = data_.length();
161         if (count < 0 || offset < 0 || offset > length - 1) {
162             throw new IllegalArgumentException("offset: " + offset + " count: " + count);
163         }
164 
165         final int tailIndex = Math.min(offset + count, length);
166         return data_.substring(offset, tailIndex);
167     }
168 
169     /**
170      * {@inheritDoc}
171      * @return the string data held by this node
172      */
173     @Override
174     public String getNodeValue() {
175         return data_;
176     }
177 
178     /**
179      * {@inheritDoc}
180      */
181     @Override
182     public String getCanonicalXPath() {
183         return getParentNode().getCanonicalXPath() + '/' + getXPathToken();
184     }
185 
186     /**
187      * Returns the XPath token for this node only.
188      */
189     private String getXPathToken() {
190         final DomNode parent = getParentNode();
191 
192         // If there are other siblings of the same node type, we have to provide
193         // the node's index.
194         int siblingsOfSameType = 0;
195         int nodeIndex = 0;
196         for (final DomNode child : parent.getChildren()) {
197             if (child == this) {
198                 nodeIndex = ++siblingsOfSameType;
199                 if (nodeIndex > 1) {
200                     // Optimization: if the node index is greater than 1, there
201                     // are at least two nodes of the same type.
202                     break;
203                 }
204             }
205             else if (child.getNodeType() == getNodeType()) {
206                 siblingsOfSameType++;
207                 if (nodeIndex > 0) {
208                     // Optimization: if the node index is greater than 0, there
209                     // are at least two nodes of the same type.
210                     break;
211                 }
212             }
213         }
214 
215         final String nodeName = getNodeName().substring(1) + "()";
216         if (siblingsOfSameType == 1) {
217             return nodeName;
218         }
219         return nodeName + '[' + nodeIndex + ']';
220     }
221 }