001    /*
002     * Copyright 2009 - 2010 JEuclid, http://jeuclid.sf.net
003     *
004     * Licensed under the Apache License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     *      http://www.apache.org/licenses/LICENSE-2.0
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    
017    /* $Id $ */
018    
019    package net.sourceforge.jeuclid.biparser;
020    
021    import org.w3c.dom.Document;
022    import org.w3c.dom.Element;
023    import org.w3c.dom.Node;
024    import org.xml.sax.Attributes;
025    
026    /**
027     * this class is used to store specific information about a composite xml-node.
028     * the node can have one child, many attributes and can be invalid
029     * 
030     * @version $Revision: 0b66106c7ff7 $
031     */
032    public final class BiNode extends AbstractBiNode {
033    
034        /** child node. */
035        private IBiNode child;
036    
037        /** offset to child from node begin (length of open tag). */
038        private final int childOffset;
039    
040        /** if false, node is a valid xml-node. */
041        private boolean invalid;
042    
043        /** DOM-info: namespaceURI. */
044        private String namespaceURI;
045    
046        /** DOM-info: tag-name. */
047        private String eName;
048    
049        /** DOM-info: attributes of node. */
050        private Attributes attrs;
051    
052        /**
053         * creates a new BiNode, length must be set afterwards. constructor does not
054         * create a DOM-node
055         * 
056         * @param co
057         *            offset to child from node begin (length of open tag)
058         * @param ns
059         *            DOM-info
060         * @param n
061         *            DOM-info
062         * @param a
063         *            DOM-info
064         */
065        public BiNode(final int co, final String ns, final String n,
066                final Attributes a) {
067            this.childOffset = co;
068            this.namespaceURI = ns;
069            this.eName = n;
070            this.attrs = a;
071        }
072    
073        /**
074         * get the name of the node (tagname).
075         * 
076         * @return nodename
077         */
078        public String getNodeName() {
079            final String ret;
080    
081            if (this.eName == null) {
082                if (this.getNode() == null) {
083                    ret = null;
084                } else {
085                    ret = this.getNode().getNodeName();
086                }
087            } else {
088                ret = this.eName;
089            }
090    
091            return ret;
092        }
093    
094        /**
095         * add a child to this node, if node has already a child, forward to child.
096         * 
097         * @param c
098         *            new child for this node
099         */
100        public void addChild(final IBiNode c) {
101            // 1st child
102            if (this.child == null) {
103                this.setChild(c);
104            } else {
105                // 2nd - nth child
106                this.child.addSibling(c);
107            }
108        }
109    
110        /**
111         * get the child of the node.
112         * 
113         * @return child
114         */
115        public IBiNode getChild() {
116            return this.child;
117        }
118    
119        /**
120         * set child for this node.
121         * 
122         * @param c
123         *            new child for this node
124         */
125        public void setChild(final IBiNode c) {
126            if (c != null) {
127                c.setPrevious(this);
128            }
129    
130            this.child = c;
131        }
132    
133        /**
134         * get the type of node.
135         * 
136         * @return NODE
137         */
138        public BiType getType() {
139            return BiType.NODE;
140        }
141    
142        /** {@inheritDoc} */
143        public void insert(final BiTree biTree, final int offset, final int length,
144                final int totalOffset) throws ReparseException,
145                NonIncrementalElementException {
146            // System.out.println("insert " + toString() + " offset=" + offset +
147            // " length=" + length);
148    
149            // ---------------- end of this or SIBLING ----------------
150            if (offset >= this.getLength()) {
151    
152                // reparse if node is invalid and start position is at node-end
153                if (offset == this.getLength() && this.invalid) {
154                    throw new ReparseException();
155                }
156    
157                // forward to sibling
158                this.forwardToSibling(true, biTree, offset - this.getLength(),
159                        length, totalOffset + this.getLength());
160    
161            } else if (this.child != null && !this.invalid
162                    && offset >= this.childOffset
163                    && offset <= this.childOffset + this.getLengthOfChildren()) {
164                // ---------------- CHILDREN ----------------
165                try {
166                    this.child.insert(biTree, offset - this.childOffset, length,
167                            totalOffset + this.childOffset);
168                } catch (final ReparseException ex) {
169                    this.parseAndReplace(
170                            biTree,
171                            biTree.getText().substring(totalOffset,
172                                    totalOffset + this.getLength() + length),
173                            length);
174                }
175            } else if (offset == 0) {
176                // ---------------- before THIS ----------------
177                throw new ReparseException();
178            } else {
179                // ---------------- THIS ----------------
180                this.parseAndReplace(
181                        biTree,
182                        biTree.getText().substring(totalOffset,
183                                totalOffset + this.getLength() + length), length);
184            }
185        }
186    
187        /** {@inheritDoc} */
188        public void remove(final BiTree biTree, final int offset, final int length,
189                final int totalOffset) throws ReparseException,
190                NonIncrementalElementException {
191            // System.out.println("remove " + toString() + " offset=" +
192            // offset + " length=" + length);
193    
194            // ---------------- REMOVE THIS ----------------
195            if (offset == 0 && length >= this.getLength()) {
196                throw new ReparseException();
197    
198            } else if (offset >= this.getLength()) {
199                // ---------------- SIBLING ----------------
200                this.forwardToSibling(false, biTree, offset - this.getLength(),
201                        length, totalOffset + this.getLength());
202    
203            } else if (this.child != null
204                    && !this.invalid
205                    && offset >= this.childOffset
206                    && offset + length <= this.childOffset
207                            + this.getLengthOfChildren()) {
208                // ---------------- CHILDREN ----------------
209                try {
210                    this.child.remove(biTree, offset - this.childOffset, length,
211                            totalOffset + this.childOffset);
212                } catch (final ReparseException ex) {
213                    this.parseAndReplace(
214                            biTree,
215                            biTree.getText().substring(totalOffset,
216                                    totalOffset + this.getLength() - length),
217                            -length);
218                }
219            } else {
220                // ---------------- THIS ----------------
221                this.parseAndReplace(
222                        biTree,
223                        biTree.getText().substring(totalOffset,
224                                totalOffset + this.getLength() - length), -length);
225            }
226        }
227    
228        /**
229         * set the node as invalid, remove all children. replace node in DOM-tree
230         * with a red '#'
231         * 
232         * @param doc
233         *            Document to insert the invalid mark '#'
234         */
235        private void makeInvalidNode(final Document doc) {
236            Element element;
237    
238            // create INVALID-textnode in DOM tree
239            element = doc.createElement("mi");
240            element.setAttribute("mathcolor", "#F00");
241            element.appendChild(doc.createTextNode("#"));
242    
243            if (this.getNode().getParentNode() == null) {
244                doc.replaceChild(element, this.getNode());
245            } else {
246                this.getNode().getParentNode()
247                        .replaceChild(element, this.getNode());
248            }
249    
250            // remove bi-subtree
251            this.setNode(element);
252            this.child = null;
253            this.invalid = true;
254        }
255    
256        /**
257         * try to parse the text, if valid replace this node with parsed tree. else
258         * replace this node with invalid mark '#'
259         * 
260         * @param biTree
261         *            reference to BiTree to which this node contains
262         * @param text
263         *            to parse
264         * @param length
265         *            change length of this node
266         * @throws NonIncrementalElementException
267         *             if the subtree contains an element which cannot be
268         *             incrementally updated.
269         */
270        private void parseAndReplace(final BiTree biTree, final String text,
271                final int length) throws ReparseException,
272                NonIncrementalElementException {
273            BiTree treePart;
274            Node domValid;
275            BiNode parent;
276            final boolean invalidSibling;
277            final boolean invalidPrevious;
278    
279            treePart = SAXBiParser.getInstance().parse(text);
280    
281            // parse unsuccessful
282            if (treePart == null) {
283                invalidSibling = this.getSibling() != null
284                        && this.getSibling().getType() == BiType.NODE
285                        && ((BiNode) this.getSibling()).invalid;
286    
287                invalidPrevious = this.getPrevious() != null
288                        && this.getPrevious().getType() == BiType.NODE
289                        && ((BiNode) this.getPrevious()).invalid;
290    
291                // if node & previous or node & sibling are invalid - reparse
292                // parent
293                if (invalidPrevious || invalidSibling) {
294                    throw new ReparseException();
295                }
296    
297                if (!this.invalid) {
298                    this.makeInvalidNode((Document) biTree.getDocument());
299                }
300    
301                this.changeLengthRec(length);
302    
303            } else {
304    
305                parent = this.getParent();
306                domValid = treePart.getDOMTree((Document) biTree.getDocument());
307                treePart.getRoot().addSibling(this.getSibling());
308    
309                // node is root
310                if (parent == null) {
311    
312                    // no emtpy text
313                    if (this.getPrevious() == null) {
314                        biTree.setRoot(treePart.getRoot());
315                    } else {
316                        // empty text on left side of root
317                        this.getPrevious().setSibling(treePart.getRoot());
318                    }
319    
320                    // replace invalid DOM node
321                    biTree.getDocument().replaceChild(domValid, this.getNode());
322    
323                } else {
324                    if (this.getPrevious() == parent) {
325                        // invalid node is 1st child
326                        this.getParent().setChild(treePart.getRoot());
327                    } else {
328                        // 2nd - nth child
329                        this.getPrevious().setSibling(treePart.getRoot());
330                    }
331    
332                    // replace invalid DOM node
333                    parent.getNode().replaceChild(domValid, this.getNode());
334                    parent.changeLengthRec(length);
335                }
336    
337                this.invalid = false;
338            }
339        }
340    
341        /**
342         * calculate the length of all children.
343         * 
344         * @return length of children
345         */
346        public int getLengthOfChildren() {
347            int length = 0;
348            IBiNode childTmp;
349    
350            if (this.child != null) {
351                // length of first child
352                length += this.child.getLength();
353    
354                childTmp = this.child.getSibling();
355                while (childTmp != null) {
356                    // length of 2nd - nth children
357                    length += childTmp.getLength();
358                    childTmp = childTmp.getSibling();
359                }
360    
361            }
362    
363            return length;
364        }
365    
366        /**
367         * create a DOM-tree from node and all children (recursive).
368         * 
369         * @param doc
370         *            Document to create DOM-tree
371         * @return root of DOM-tree
372         */
373        public Node createDOMSubtree(final Document doc) {
374            int i;
375            String aName;
376            Node childNode;
377            Element element;
378            IBiNode tmp;
379    
380            element = doc.createElementNS(this.namespaceURI, this.eName);
381    
382            // add attributes
383            if (this.attrs != null) {
384                for (i = 0; i < this.attrs.getLength(); i++) {
385                    aName = this.attrs.getLocalName(i);
386    
387                    if ("".equals(aName)) {
388                        aName = this.attrs.getQName(i);
389                    }
390    
391                    element.setAttribute(aName, this.attrs.getValue(i));
392                }
393            }
394    
395            // create DOM-tree of children
396            if (this.child != null) {
397                tmp = this.child;
398    
399                while (tmp != null) {
400                    childNode = tmp.createDOMSubtree(doc);
401    
402                    if (childNode != null) {
403                        element.appendChild(childNode);
404                    }
405    
406                    tmp = tmp.getSibling();
407                }
408            }
409    
410            this.namespaceURI = null;
411            this.eName = null;
412            this.attrs = null;
413    
414            this.setNode(element);
415            return element;
416        }
417    
418        /** {@inheritDoc} */
419        @Override
420        public TextPosition searchNode(final Node node, final int totalOffset) {
421            TextPosition result;
422    
423            // check if node is this
424            result = super.searchNode(node, totalOffset);
425    
426            // forward to child
427            if (result == null && this.child != null) {
428                result = this.child
429                        .searchNode(node, totalOffset + this.childOffset);
430            }
431    
432            // forward to sibling
433            if (result == null && this.getSibling() != null) {
434                result = this.getSibling().searchNode(node,
435                        totalOffset + this.getLength());
436            }
437    
438            return result;
439        }
440    
441        @Override
442        public String toString() {
443            final StringBuffer sb = new StringBuffer(32);
444    
445            sb.append('[');
446            sb.append(this.invalid ? "INVALID " : "");
447            sb.append("NODE length: ");
448            sb.append(this.getLength());
449    
450            if (!this.invalid) {
451                sb.append(" <");
452                sb.append(this.getNodeName());
453                sb.append("> tag: ");
454                sb.append(this.childOffset);
455            }
456    
457            sb.append(']');
458    
459            return sb.toString();
460        }
461    
462        /** {@inheritDoc} */
463        public String toString(final int level) {
464            final StringBuffer sb = new StringBuffer(32);
465            final String nl = System.getProperty("line.separator");
466    
467            sb.append(this.formatLength());
468            sb.append(':');
469            for (int i = 0; i <= level; i++) {
470                sb.append(' ');
471            }
472    
473            if (this.invalid) {
474                sb.append("INVALID ");
475            }
476    
477            if (!this.invalid) {
478                sb.append('<');
479                sb.append(this.getNodeName());
480                sb.append("> tag: ");
481                if (this.childOffset < 100) {
482                    sb.append('0');
483                }
484                if (this.childOffset < 10) {
485                    sb.append('0');
486                }
487                sb.append(this.childOffset);
488            }
489    
490            sb.append(nl);
491    
492            if (this.child != null) {
493                sb.append(this.child.toString(level + 1));
494                if (this.getSibling() != null) {
495                    sb.append(nl);
496                }
497    
498            }
499    
500            if (this.getSibling() != null) {
501                sb.append(this.getSibling().toString(level));
502            }
503    
504            return sb.toString();
505        }
506    }