All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.unit8.moshas.dom.Node Maven / Gradle / Ivy

package net.unit8.moshas.dom;

import net.unit8.moshas.helper.StringUtil;
import net.unit8.moshas.helper.Validate;
import net.unit8.moshas.select.NodeTraversor;
import net.unit8.moshas.select.NodeVisitor;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 *
 * @author kawasima
 */
public abstract class Node implements Serializable, Cloneable {
    private static final List EMPTY_NODES = Collections.emptyList();
    Node parentNode;
    Map> childNodes = new ConcurrentHashMap<>();
    Map attributes  = new ConcurrentHashMap<>();
    String baseUri;
    int siblingIndex;

    protected String outerHtmlHead;
    protected String outerHtmlTail;
    protected String renderedHtml;
    protected boolean needsRerender;

    protected Node(String baseUri, Attributes attributes) {
        Validate.notNull(baseUri);
        Validate.notNull(attributes);
        childNodes.put(0, EMPTY_NODES);
        baseUri = baseUri.trim();
        this.attributes.put(0, attributes);
        needsRerender = false;
    }

    protected Node(String baseUri) {
        this(baseUri, new Attributes());
    }

    protected Node() {
        this("");
    }

    public String baseUri() {
        return baseUri;
    }

    /**
     Get the node name of this node. Use for debugging purposes and not logic switching (for that, use instanceof).
     @return node name
     */
    public abstract String nodeName();


        /**
     * Get an attribute's value by its key.
     * 

* To get an absolute URL from an attribute that may be a relative URL, prefix the key with abs, * which is a shortcut to the absUrl method. *

* E.g.: *
String url = a.attr("abs:href");
* * @param attributeKey The attribute key. * @return The attribute, or empty string if not present (to avoid nulls). * @see #attributes() * @see #hasAttr(String) */ public String attr(String attributeKey) { Validate.notNull(attributeKey); Attributes attrs = attributes(); if (attrs.hasKey(attributeKey)) return attrs.get(attributeKey); else return ""; } /** * Get all of the element's attributes. * @return attributes (which implements iterable, in same order as presented in original HTML). */ public Attributes attributes() { return attributes.computeIfAbsent(RenderingId.get(), id -> attributes.get(0).clone()); } /** * Set an attribute (key=value). If the attribute already exists, it is replaced. * @param attributeKey The attribute key. * @param attributeValue The attribute value. * @return this (for chaining) */ public Node attr(String attributeKey, String attributeValue) { needsRerender = true; if (attributes.containsKey(RenderingId.get())) { attributes.get(RenderingId.get()).put(attributeKey, attributeValue); } else { Attributes newAttrs = attributes.get(0).clone(); newAttrs.put(attributeKey, attributeValue); attributes.put(RenderingId.get(), newAttrs); } return this; } /** * Test if this element has an attribute. * @param attributeKey The attribute key to check. * @return true if the attribute exists, false if not. */ public boolean hasAttr(String attributeKey) { return attributes().hasKey(attributeKey); } /** * Remove an attribute from this element. * @param attributeKey The attribute to remove. * @return this (for chaining) */ public Node removeAttr(String attributeKey) { Validate.notNull(attributeKey); attributes().remove(attributeKey); return this; } /** Get a child node by its 0-based index. @param index index of child node @return the child node at this index. Throws a {@code IndexOutOfBoundsException} if the index is out of bounds. */ public Node childNode(int index) { List cs = childNodes.get(RenderingId.get()); return (cs == null) ? childNodes.get(0).get(index) : cs.get(index); } /** Get this node's children. Presented as an unmodifiable list: new children can not be added, but the child nodes themselves can be manipulated. @return list of children. If no children, returns an empty list. */ public List childNodes() { List cs = childNodes.get(RenderingId.get()); return (cs == null) ? childNodes.get(0) : cs; } /** * Get the number of child nodes that this node holds. * @return the number of child nodes that this node holds. */ public int childNodeSize() { List cs = childNodes.get(RenderingId.get()); return (cs == null) ? childNodes.get(0).size() : cs.size(); } /** Gets this node's parent node. @return parent node; or null if no parent. */ public Node parent() { return parentNode; } public Node parentNode() { return parentNode; } protected void setParentNode(Node parentNode) { /* if (this.parentNode != null) this.parentNode.removeChild(this); */ this.parentNode = parentNode; } /** * Gets the Document associated with this Node. * @return the Document associated with this Node, or null if there is no such Document. */ public Document ownerDocument() { if (this instanceof Document) return (Document) this; else if (parentNode == null) return null; else return parentNode.ownerDocument(); } /** * Remove (delete) this node from the DOM tree. If this node has children, they are also removed. */ public void remove() { Validate.notNull(parentNode); parentNode.removeChild(this); } /** * Insert the specified node into the DOM before this node (i.e. as a preceding sibling). * @param node to add before this node * @return this node, for chaining */ public Node before(Node node) { Validate.notNull(node); Validate.notNull(parentNode); parentNode.addChildren(siblingIndex, node); return this; } protected void addChildren(int index, Node... children) { Validate.noNullElements(children); for (int i = children.length - 1; i >= 0; i--) { Node in = children[i]; reparentChild(in); ensureChildNodes(); childNodes().add(index, in); } reindexChildren(index); } protected void removeChild(Node out) { Validate.isTrue(out.parentNode == this); ensureChildNodes(); final int index = out.siblingIndex; childNodes().remove(index); reindexChildren(index); } protected void ensureChildNodes() { Integer id = RenderingId.get(); List cns = childNodes.get(id); if (cns == EMPTY_NODES || cns == null) { childNodes.put(id, new ArrayList<>(childNodes.get(0))); } } protected void reparentChild(Node child) { /* if (child.parentNode != null) child.parentNode.removeChild(child); */ child.setParentNode(this); } private void reindexChildren(int start) { for (int i = start; i < childNodes().size(); i++) { childNodes().get(i).setSiblingIndex(i); } } /** Get this node's next sibling. @return next sibling, or null if this is the last sibling */ public Node nextSibling() { if (parentNode == null) return null; // root final List siblings = parentNode.childNodes(); final int index = siblingIndex+1; if (siblings.size() > index) return siblings.get(index); else return null; /* List siblings = parentNode.childNodes(); for (int i=0; i < siblings.size(); i++) { if (siblings.get(i) == this) { if (i < siblings.size() - 1) { return siblings.get(i+1); } else { return null; } } } return null; */ } /** * Get the list index of this node in its node sibling list. I.e. if this is the first node * sibling, returns 0. * @return position in node sibling list */ public int siblingIndex() { return siblingIndex; } protected void setSiblingIndex(int siblingIndex) { this.siblingIndex = siblingIndex; } /** * Perform a depth-first traversal through this node and its descendants. * @param nodeVisitor the visitor callbacks to perform on each node * @return this node, for chaining */ public Node traverse(NodeVisitor nodeVisitor) { Validate.notNull(nodeVisitor); NodeTraversor traversor = new NodeTraversor(nodeVisitor); traversor.traverse(this); return this; } /** Get the outer HTML of this node. @return HTML */ public String outerHtml() { StringBuilder accum = new StringBuilder(128); outerHtml(accum); return accum.toString(); } protected void outerHtml(StringBuilder accum) { new NodeTraversor(new OuterHtmlVisitor(accum, getOutputSettings())).traverse(this); } // if this node has no document (or parent), retrieve the default output settings Document.OutputSettings getOutputSettings() { return ownerDocument() != null ? ownerDocument().outputSettings() : (new Document("")).outputSettings(); } @Override public String toString() { return outerHtml(); } public abstract void outerHtmlHead(StringBuilder accum, int depth, Document.OutputSettings out); public abstract void outerHtmlTail(StringBuilder accum, int depth, Document.OutputSettings out); protected void indent(StringBuilder accum, int depth, Document.OutputSettings out) { accum.append("\n").append(StringUtil.padding(depth * out.indentAmount())); } public void cleanThreadCache(int renderingId) { attributes.remove(renderingId); childNodes.remove(renderingId); } private static class OuterHtmlVisitor implements NodeVisitor { private final StringBuilder accum; private final Document.OutputSettings out; OuterHtmlVisitor(StringBuilder accum, Document.OutputSettings out) { this.accum = accum; this.out = out; } @Override public void head(Node node, int depth) { node.outerHtmlHead(accum, depth, out); } @Override public void tail(Node node, int depth) { if (!node.nodeName().equals("#text")) // saves a void hit. node.outerHtmlTail(accum, depth, out); } } @Override public Node clone() { Node clone; try { clone = (Node) super.clone(); clone.renderedHtml = null; } catch (CloneNotSupportedException e) { throw new RuntimeException(e); } return clone; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy