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 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_.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 }