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 }