View Javadoc
1   /*
2    * Copyright (c) 2002-2026 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 Christian Sell
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_ == null ? 0 : 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         if (newData != null) {
103             setData(data_ == null ? newData : data_ + newData);
104         }
105     }
106 
107     /**
108      * Deletes characters from character data.
109      * @param offset the position of the first character to be deleted (can't be
110      *        less than zero)
111      * @param count the number of characters to be deleted, if less than zero
112      *        leaves the first offset chars
113      */
114     @Override
115     public void deleteData(final int offset, final int count) {
116         if (offset < 0) {
117             throw new IllegalArgumentException("Provided offset: " + offset + " is less than zero.");
118         }
119         if (data_ == null) {
120             return;
121         }
122 
123         final String data = data_.substring(0, Math.min(offset, data_.length()));
124         if (count >= 0) {
125             final int fromLeft = offset + count;
126             if (fromLeft < data_.length()) {
127                 setData(data + data_.substring(fromLeft));
128                 return;
129             }
130         }
131         setData(data);
132     }
133 
134     /**
135      * Inserts a string into character data.
136      * @param offset the position within the first character at which the string is to be inserted
137      * @param arg the string to insert
138      */
139     @Override
140     public void insertData(final int offset, final String arg) {
141         if (data_ == null) {
142             setData(arg);
143             return;
144         }
145 
146         setData(new StringBuilder(data_).insert(offset, arg).toString());
147     }
148 
149     /**
150      * Replaces characters of character data with a string.
151      * @param offset the position within the first character at which the string is to be replaced
152      * @param count the number of characters to be replaced
153      * @param arg the string that replaces the count characters beginning at the character at offset
154      */
155     @Override
156     public void replaceData(final int offset, final int count, final String arg) {
157         if (offset < 0 || count < 0) {
158             throw new IllegalArgumentException("offset: " + offset + " count: " + count);
159         }
160         if (data_ == null) {
161             setData(arg);
162             return;
163         }
164 
165         final int end = Math.min(offset + count, data_.length());
166         final StringBuilder sb = new StringBuilder(data_);
167         sb.replace(offset, end, arg != null ? arg : "");
168         setData(sb.toString());
169     }
170 
171     /**
172      * Extracts a substring from character data.
173      * @param offset the position of the first character to be extracted
174      * @param count the number of characters to be extracted
175      * @return a string that consists of the count characters of the character data starting
176      *         from the character at position offset
177      */
178     @Override
179     public String substringData(final int offset, final int count) {
180         if (data_ == null) {
181             throw new IllegalArgumentException("offset: " + offset + " count: " + count);
182         }
183         final int length = data_.length();
184         if (count < 0 || offset < 0 || offset > length - 1) {
185             throw new IllegalArgumentException("offset: " + offset + " count: " + count);
186         }
187 
188         final int tailIndex = Math.min(offset + count, length);
189         return data_.substring(offset, tailIndex);
190     }
191 
192     /**
193      * {@inheritDoc}
194      * @return the string data held by this node
195      */
196     @Override
197     public String getNodeValue() {
198         return data_;
199     }
200 
201     /**
202      * {@inheritDoc}
203      */
204     @Override
205     public String getCanonicalXPath() {
206         final DomNode parent = getParentNode();
207         if (parent == null) {
208             return "/" + getXPathToken();
209         }
210         return parent.getCanonicalXPath() + '/' + getXPathToken();
211     }
212 
213     /**
214      * Returns the XPath token for this node only.
215      */
216     private String getXPathToken() {
217         final DomNode parent = getParentNode();
218         if (parent == null) {
219             return getNodeName().substring(1) + "()";
220         }
221 
222         // If there are other siblings of the same node type, we have to provide
223         // the node's index.
224         int siblingsOfSameType = 0;
225         int nodeIndex = 0;
226         for (final DomNode child : parent.getChildren()) {
227             if (child == this) {
228                 nodeIndex = ++siblingsOfSameType;
229                 if (nodeIndex > 1) {
230                     // Optimization: if the node index is greater than 1, there
231                     // are at least two nodes of the same type.
232                     break;
233                 }
234             }
235             else if (child.getNodeType() == getNodeType()) {
236                 siblingsOfSameType++;
237                 if (nodeIndex > 0) {
238                     // Optimization: if the node index is greater than 0, there
239                     // are at least two nodes of the same type.
240                     break;
241                 }
242             }
243         }
244 
245         final String nodeName = getNodeName().substring(1) + "()";
246         if (siblingsOfSameType == 1) {
247             return nodeName;
248         }
249         return nodeName + '[' + nodeIndex + ']';
250     }
251 }