package freenet.support; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; public class HTMLNode implements XMLCharacterClasses, Cloneable { private static final Pattern namePattern = Pattern.compile("^[" + NAME + "]*$"); private static final Pattern simpleNamePattern = Pattern.compile("^[A-Za-z][A-Za-z0-9]*$"); public static HTMLNode STRONG = new HTMLNode("strong").setReadOnly(); protected final String name; private boolean readOnly; public HTMLNode setReadOnly() { readOnly = true; return this; } /** Text to be inserted between tags, or possibly raw HTML. Only non-null if name * is "#" (= text) or "%" (= raw HTML). Otherwise the constructor will allocate a * separate child node to contain it. */ private String content; private final Map<String, String> attributes = new HashMap<String, String>(); protected final List<HTMLNode> children = new ArrayList<HTMLNode>(); public HTMLNode(String name) { this(name, null); } private static final ArrayList<String> EmptyTag = new ArrayList<String>(10); private static final ArrayList<String> OpenTags = new ArrayList<String>(12); private static final ArrayList<String> CloseTags = new ArrayList<String>(12); static { /* HTML elements which are allowed to be empty */ EmptyTag.add("area"); EmptyTag.add("base"); EmptyTag.add("br"); EmptyTag.add("col"); EmptyTag.add("hr"); EmptyTag.add("img"); EmptyTag.add("input"); EmptyTag.add("link"); EmptyTag.add("meta"); EmptyTag.add("param"); /* HTML elements for which we should add a newline following the open tag. */ OpenTags.add("body"); OpenTags.add("div"); OpenTags.add("form"); OpenTags.add("head"); OpenTags.add("html"); OpenTags.add("input"); OpenTags.add("ol"); OpenTags.add("script"); OpenTags.add("table"); OpenTags.add("td"); OpenTags.add("tr"); OpenTags.add("ul"); /* HTML elements for which we should add a newline following the close tag. */ CloseTags.add("h1"); CloseTags.add("h2"); CloseTags.add("h3"); CloseTags.add("h4"); CloseTags.add("h5"); CloseTags.add("h6"); CloseTags.add("li"); CloseTags.add("link"); CloseTags.add("meta"); CloseTags.add("noscript"); CloseTags.add("option"); CloseTags.add("title"); } /** Tests an HTML element name to determine if it is one of the elements permitted * to be empty in the XHTML spec ( http://www.w3.org/TR/xhtml1/ ) * @param name The name of the html element * @return True if the element is allowed to be empty */ private Boolean isEmptyElement(String name) { return EmptyTag.contains(name); } /** Tests an HTML element to determine if we should add a newline after the opening tag * for readability * @param name The name of the html element * @return True if we should add a newline after the opening tag */ Boolean newlineOpen(String name) { return OpenTags.contains(name); } /** Tests an HTML element to determine if we should add a newline after the closing tag * for readability. All tags with newlines after the opening tag also get newlines after * the closing tag. * @param name The name of the html element * @return True if we should add a newline after the opening tag */ private Boolean newlineClose(String name) { return (newlineOpen(name) || CloseTags.contains(name)); } /** Returns a properly formatted closing angle bracket to complete an open tag of a * named html element * @param name the name of the element * @return the proper string of characters to complete the open tag */ private String OpenSuffix(String name) { if (isEmptyElement(name)) { return " />"; } else { return ">"; } } /** Returns a closing tag for a named html elemen * @param name the name of the element * @return the complete closing tag for the element */ private String CloseTag(String name) { if (isEmptyElement(name)) { return ""; } else { return "</" + name + ">"; } } private String indentString(int indentDepth) { StringBuffer indentLine = new StringBuffer(); for (int indentIndex = 0, indentCount = indentDepth+1; indentIndex < indentCount; indentIndex++) { indentLine.append('\t'); } return indentLine.toString(); } public HTMLNode(String name, String content) { this(name, (String[]) null, (String[]) null, content); } public HTMLNode(String name, String attributeName, String attributeValue) { this(name, attributeName, attributeValue, null); } public HTMLNode(String name, String attributeName, String attributeValue, String content) { this(name, new String[] { attributeName }, new String[] { attributeValue }, content); } public HTMLNode(String name, String[] attributeNames, String[] attributeValues) { this(name, attributeNames, attributeValues, null); } protected HTMLNode(HTMLNode node, boolean clearReadOnly) { attributes.putAll(node.attributes); children.addAll(node.children); content = node.content; name = node.name; if(clearReadOnly) readOnly = false; else readOnly = node.readOnly; } @Override public HTMLNode clone() { // Implement Cloneable to shut up findbugs. We need a deep copy. // FIXME is clearing read only an abuse of the clone() API? Should we rename the method? return new HTMLNode(this, true); } protected boolean checkNamePattern(String str) { // Workaround buggy java regexes, also probably slightly faster. if(str.length() < 1) return false; char c; c = str.charAt(0); if((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) { boolean simpleMatch = true; for(int i=1;i<str.length();i++) { c = str.charAt(i); if(!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'))) { simpleMatch = false; break; } } if(simpleMatch) return true; } // Regex-based match. Probably more expensive, and problems (infinite recursion in Pattern$6.isSatisfiedBy) have been seen in practice. // Oddly these problems were seen where the answer is almost certainly in the first matcher, because the tag name was "html"... return simpleNamePattern.matcher(str).matches() || namePattern.matcher(str).matches(); } public HTMLNode(String name, String[] attributeNames, String[] attributeValues, String content) { if ((name == null) || (!"#".equals(name) && !"%".equals(name) && !checkNamePattern(name))) { throw new IllegalArgumentException("element name is not legal"); } if ((attributeNames != null) && (attributeValues != null)) { if (attributeNames.length != attributeValues.length) { throw new IllegalArgumentException("attribute names and values differ in length"); } for (int attributeIndex = 0, attributeCount = attributeNames.length; attributeIndex < attributeCount; attributeIndex++) { if ((attributeNames[attributeIndex] == null) || !checkNamePattern(attributeNames[attributeIndex])) { throw new IllegalArgumentException("attributeName is not legal"); } addAttribute(attributeNames[attributeIndex], attributeValues[attributeIndex]); } } this.name = name.toLowerCase(Locale.ENGLISH); if (content != null && !("#").equals(name)&& !("%").equals(name)) { addChild(new HTMLNode("#", content)); this.content = null; } else this.content = content; } /** * @return the content */ public String getContent() { return content; } public void addAttribute(String attributeName, String attributeValue) { if(readOnly) throw new IllegalArgumentException("Read only"); if (attributeName == null) throw new IllegalArgumentException("Cannot add an attribute with a null name"); if (attributeValue == null) throw new IllegalArgumentException("Cannot add an attribute with a null value"); attributes.put(attributeName, attributeValue); } public Map<String, String> getAttributes() { return Collections.unmodifiableMap(attributes); } public String getAttribute(String attributeName) { return attributes.get(attributeName); } public HTMLNode addChild(HTMLNode childNode) { if(readOnly) throw new IllegalArgumentException("Read only"); if (childNode == null) throw new NullPointerException(); //since an efficient algorithm to check the loop presence //is not present, at least it checks if we are trying to //addChild the node itself as a child if (childNode == this) throw new IllegalArgumentException("A HTMLNode cannot be child of himself"); if (children.contains(childNode)) throw new IllegalArgumentException("Cannot add twice the same HTMLNode as child"); children.add(childNode); return childNode; } public void addChildren(HTMLNode[] childNodes) { addChildren(Arrays.asList(childNodes)); } public void addChildren(List<HTMLNode> childNodes) { if(readOnly) throw new IllegalArgumentException("Read only"); for (HTMLNode childNode: childNodes) { addChild(childNode); } } public HTMLNode addChild(String nodeName) { return addChild(nodeName, null); } /** Add a tag with content inside/under this node. * @param nodeName The tag name e.g. "div". "#" means add content only, no tag. * @param content The content (to be added as body text). * @return The added node. You can add more tags inside it with addChild(), or add attributes * with addAttribute() etc. If you render the parent tag with generate(), it will include this * tag in its output. */ public HTMLNode addChild(String nodeName, String content) { return addChild(nodeName, (String[]) null, (String[]) null, content); } /** Add a tag with one attribute inside/under this node. * @param nodeName The tag name e.g. "div". * @param attributeName The name of the attribute, e.g. "class" * @param attributeValue The value of the attribute. * @return The added node. You can add more tags inside it with addChild(), or add attributes * with addAttribute() etc. If you render the parent tag with generate(), it will include this * tag in its output. */ public HTMLNode addChild(String nodeName, String attributeName, String attributeValue) { return addChild(nodeName, attributeName, attributeValue, null); } /** * Add a tag with one attribute and body text inside/under this node. * @param nodeName The tag name e.g. "div". * @param attributeName The name of the attribute, e.g. "class" * @param attributeValue The value of the attribute. * @param content The content (to be added as body text). * @return The added node. You can add more tags inside it with addChild(), or add attributes * with addAttribute() etc. If you render the parent tag with generate(), it will include this * tag in its output. */ public HTMLNode addChild(String nodeName, String attributeName, String attributeValue, String content) { return addChild(nodeName, new String[] { attributeName }, new String[] { attributeValue }, content); } /** * Add a tag with several attributes inside/under this node. * @param nodeName The tag name e.g. "div". * @param attributeName The name of the attribute, e.g. "class" * @param attributeValue The value of the attribute. * @return The added node. You can add more tags inside it with addChild(), or add attributes * with addAttribute() etc. If you render the parent tag with generate(), it will include this * tag in its output. */ public HTMLNode addChild(String nodeName, String[] attributeNames, String[] attributeValues) { return addChild(nodeName, attributeNames, attributeValues, null); } /** * Add a tag with several attributes and body text inside/under this node. * @param nodeName The tag name e.g. "div". * @param attributeName The name of the attribute, e.g. "class" * @param attributeValue The value of the attribute. * @param content The content (to be added as body text). * @return The added node. You can add more tags inside it with addChild(), or add attributes * with addAttribute() etc. If you render the parent tag with generate(), it will include this * tag in its output. */ public HTMLNode addChild(String nodeName, String[] attributeNames, String[] attributeValues, String content) { return addChild(new HTMLNode(nodeName, attributeNames, attributeValues, content)); } /** * Returns the name of the first "real" tag found in the hierarchy below * this node. * * @return The name of the first "real" tag, or <code>null</code> if no * "real" tag could be found */ public String getFirstTag() { if (!"#".equals(name)) { return name; } for (int childIndex = 0, childCount = children.size(); childIndex < childCount; childIndex++) { HTMLNode childNode = children.get(childIndex); String tag = childNode.getFirstTag(); if (tag != null) { return tag; } } return null; } public String generate() { StringBuilder tagBuffer = new StringBuilder(); return generate(tagBuffer).toString(); } public StringBuilder generate(StringBuilder tagBuffer) { return generate(tagBuffer,0); } public StringBuilder generate(StringBuilder tagBuffer, int indentDepth ) { if("#".equals(name)) { if(content != null) { HTMLEncoder.encodeToBuffer(content, tagBuffer); return tagBuffer; } for(int childIndex = 0, childCount = children.size(); childIndex < childCount; childIndex++) { HTMLNode childNode = children.get(childIndex); childNode.generate(tagBuffer); } return tagBuffer; } // Perhaps this should be something else, but since I don't know if '#' was not just arbitrary chosen, I'll just pick '%' // This allows non-encoded text to be appended to the tag buffer if ("%".equals(name)) { tagBuffer.append(content); return tagBuffer; } /* start the open tag */ tagBuffer.append('<').append(name); /* add attributes*/ Set<Map.Entry<String, String>> attributeSet = attributes.entrySet(); for (Map.Entry<String, String> attributeEntry : attributeSet) { String attributeName = attributeEntry.getKey(); String attributeValue = attributeEntry.getValue(); tagBuffer.append(' '); HTMLEncoder.encodeToBuffer(attributeName, tagBuffer); tagBuffer.append("=\""); HTMLEncoder.encodeToBuffer(attributeValue, tagBuffer); tagBuffer.append('"'); } /* complete the open tag*/ tagBuffer.append(OpenSuffix(name)); /*insert the contents*/ if (children.size() == 0) { if(content==null) { } else { HTMLEncoder.encodeToBuffer(content, tagBuffer); } } else { if (newlineOpen(name)) { tagBuffer.append('\n'); tagBuffer.append(indentString(indentDepth+1)); } for (int childIndex = 0, childCount = children.size(); childIndex < childCount; childIndex++) { HTMLNode childNode = children.get(childIndex); childNode.generate(tagBuffer,indentDepth+1); } } /* add a closing tag */ if (newlineOpen(name)) { tagBuffer.append('\n'); tagBuffer.append(indentString(indentDepth)); } tagBuffer.append(CloseTag(name)); if (newlineClose(name)) { tagBuffer.append('\n'); tagBuffer.append(indentString(indentDepth)); } return tagBuffer; } public String generateChildren(){ if(content!=null){ return content; } StringBuilder tagBuffer=new StringBuilder(); for(int childIndex = 0, childCount = children.size(); childIndex < childCount; childIndex++) { HTMLNode childNode = children.get(childIndex); childNode.generate(tagBuffer); } return tagBuffer.toString(); } public void setContent(String newContent){ if(readOnly) throw new IllegalArgumentException("Read only"); content=newContent; } public List<HTMLNode> getChildren(){ return children; } /** * Special HTML node for the DOCTYPE declaration. This node differs from a * normal HTML node in that it's child (and it should only have exactly one * child, the "html" node) is rendered <em>after</em> this node. * * @author David 'Bombe' Roden <bombe@freenetproject.org> * @version $Id$ */ public static class HTMLDoctype extends HTMLNode { private final String systemUri; /** * */ public HTMLDoctype(String doctype, String systemUri) { super(doctype); this.systemUri = systemUri; } /** * @see freenet.support.HTMLNode#generate(java.lang.StringBuilder) */ @Override public StringBuilder generate(StringBuilder tagBuffer) { tagBuffer.append("<!DOCTYPE ").append(name).append(" PUBLIC \"").append(systemUri).append("\">\n"); //TODO A meaningful exception should be raised // when trying to call the method for a HTMLDoctype // with number of child != 1 return children.get(0).generate(tagBuffer); } } public static HTMLNode link(String path) { return new HTMLNode("a", "href", path); } public static HTMLNode linkInNewWindow(String path) { return new HTMLNode("a", new String[] { "href", "target", "rel" }, new String[] { path, "_blank", "noreferrer noopener" }); } public static HTMLNode text(String text) { return new HTMLNode("#", text); } public static HTMLNode text(int count) { return new HTMLNode("#", Integer.toString(count)); } public static HTMLNode text(long count) { return new HTMLNode("#", Long.toString(count)); } public static HTMLNode text(short count) { return new HTMLNode("#", Short.toString(count)); } public void removeChildren() { if(readOnly) throw new IllegalArgumentException("Read only"); children.clear(); } }