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