/**
* (The MIT License)
*
* Copyright (c) 2008 - 2011:
*
* * {Aaron Patterson}[http://tenderlovemaking.com]
* * {Mike Dalessio}[http://mike.daless.io]
* * {Charles Nutter}[http://blog.headius.com]
* * {Sergio Arbeo}[http://www.serabe.com]
* * {Patrick Mahoney}[http://polycrystal.org]
* * {Yoko Harada}[http://yokolet.blogspot.com]
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* 'Software'), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package nokogiri.internals;
import static nokogiri.internals.NokogiriHelpers.getNokogiriClass;
import static nokogiri.internals.NokogiriHelpers.isNamespace;
import static nokogiri.internals.NokogiriHelpers.isXmlBase;
import static nokogiri.internals.NokogiriHelpers.rubyStringToString;
import static nokogiri.internals.NokogiriHelpers.stringOrBlank;
import static nokogiri.internals.NokogiriHelpers.stringOrNil;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import nokogiri.NokogiriService;
import nokogiri.XmlAttr;
import nokogiri.XmlDocument;
import nokogiri.XmlSyntaxError;
import org.apache.xerces.xni.XMLAttributes;
import org.jruby.Ruby;
import org.jruby.RubyArray;
import org.jruby.RubyBoolean;
import org.jruby.RubyHash;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
/**
* Abstract class of Node for XmlReader.
*
* @author Yoko Harada <yokolet@gmail.com>
*
*/
public abstract class ReaderNode {
Ruby ruby;
public ReaderAttributeList attributeList;
public Map<String, String> namespaces;
public int depth, nodeType;
public String lang, localName, xmlBase, prefix, name, uri, value, xmlVersion = "1.0";
public int startOffset, endOffset;
public boolean hasChildren = false;
public abstract String getString();
private Document document = null;
private static ElementNode elementNode = null;
private static ClosingNode closingNode = null;
private static TextNode textNode = null;
public IRubyObject getAttributeByIndex(IRubyObject index){
if(index.isNil()) return index;
long i = index.convertToInteger().getLongValue();
if(i > Integer.MAX_VALUE) {
throw ruby.newArgumentError("value too long to be an array index");
}
if (attributeList == null) return ruby.getNil();
if (i<0 || attributeList.length <= i) return ruby.getNil();
return stringOrBlank(ruby, attributeList.values.get(((Long)i).intValue()));
}
public IRubyObject getAttributeByName(IRubyObject name){
if(attributeList == null) return ruby.getNil();
String value = attributeList.getByName(rubyStringToString(name));
return stringOrNil(ruby, value);
}
public IRubyObject getAttributeByName(String name){
if(attributeList == null) return ruby.getNil();
String value = attributeList.getByName(name);
return stringOrNil(ruby, value);
}
public IRubyObject getAttributeCount(){
if(attributeList == null) return ruby.newFixnum(0);
return ruby.newFixnum(attributeList.length);
}
public IRubyObject getAttributesNodes() {
RubyArray array = RubyArray.newArray(ruby);
if (attributeList != null && attributeList.length > 0) {
if (document == null) {
XmlDocument doc = (XmlDocument) XmlDocument.rbNew(ruby.getCurrentContext(), getNokogiriClass(ruby, "Nokogiri::XML::Document"), new IRubyObject[0]);
document = doc.getDocument();
}
for (int i=0; i<attributeList.length; i++) {
if (!isNamespace(attributeList.names.get(i))) {
Attr attr = document.createAttributeNS(attributeList.namespaces.get(i), attributeList.names.get(i));
attr.setValue(attributeList.values.get(i));
XmlAttr xmlAttr = (XmlAttr) NokogiriService.XML_ATTR_ALLOCATOR.allocate(ruby, getNokogiriClass(ruby, "Nokogiri::XML::Attr"));
xmlAttr.setNode(ruby.getCurrentContext(), attr);
array.append(xmlAttr);
}
}
}
return array;
}
public IRubyObject getAttributes(ThreadContext context) {
if(attributeList == null) return context.getRuntime().getNil();
RubyHash hash = RubyHash.newHash(context.getRuntime());
for (int i=0; i<attributeList.length; i++) {
IRubyObject k = stringOrBlank(context.getRuntime(), attributeList.names.get(i));
IRubyObject v = stringOrBlank(context.getRuntime(), attributeList.values.get(i));
if (context.getRuntime().is1_9()) hash.op_aset19(context, k, v);
else hash.op_aset(context, k, v);
}
return hash;
}
public IRubyObject getDepth() {
return ruby.newFixnum(depth);
}
public IRubyObject getLang() {
return stringOrNil(ruby, lang);
}
public IRubyObject getLocalName() {
return stringOrNil(ruby, localName);
}
public IRubyObject getName() {
return stringOrNil(ruby, name);
}
public IRubyObject getNamespaces(ThreadContext context) {
if(namespaces == null) return ruby.getNil();
RubyHash hash = RubyHash.newHash(ruby);
Set<String> keys = namespaces.keySet();
for (String key : keys) {
String stringValue = namespaces.get(key);
IRubyObject k = stringOrBlank(context.getRuntime(), key);
IRubyObject v = stringOrBlank(context.getRuntime(), stringValue);
if (context.getRuntime().is1_9()) hash.op_aset19(context, k, v);
else hash.op_aset(context, k, v);
}
return hash;
}
public IRubyObject getXmlBase() {
return stringOrNil(ruby, xmlBase);
}
public IRubyObject getPrefix() {
return stringOrNil(ruby, prefix);
}
public IRubyObject getUri() {
return stringOrNil(ruby, uri);
}
public IRubyObject getValue() {
return stringOrNil(ruby, value);
}
public IRubyObject getXmlVersion() {
return ruby.newString(xmlVersion);
}
public RubyBoolean hasAttributes() {
if (attributeList == null || attributeList.length == 0) return ruby.getFalse();
return ruby.getTrue();
}
public abstract RubyBoolean hasValue();
public RubyBoolean isDefault(){
// TODO Implement.
return ruby.getFalse();
}
public boolean isError() { return false; }
protected void parsePrefix(String qName) {
int index = qName.indexOf(':');
if(index != -1) prefix = qName.substring(0, index);
}
public void setLang(String lang) {
lang = (lang != null) ? lang : null;
}
public IRubyObject toSyntaxError() { return ruby.getNil(); }
public IRubyObject getNodeType() { return ruby.newFixnum(nodeType); }
public static enum ReaderNodeType {
NODE(0),
ELEMENT(1),
ATTRIBUTE(2),
TEXT(3),
CDATA(4),
ENTITY_REFERENCE(5),
ENTITY(6),
PROCESSING_INSTRUCTION(7),
COMMENT(8),
DOCUMENT(9),
DOCUMENT_TYPE(10),
DOCUMENTFRAGMENT(11),
NOTATION(12),
WHITESPACE(13),
SIGNIFICANT_WHITESPACE(14),
END_ELEMENT(15),
END_ENTITY(16),
XML_DECLARATION(17);
private final int value;
ReaderNodeType(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
public static ClosingNode createClosingNode(Ruby ruby, String uri, String localName, String qName, int depth, Stack<String> langStack, Stack<String> xmlBaseStack) {
if (closingNode == null) closingNode = new ClosingNode();
ClosingNode clone;
try {
clone = (ClosingNode) closingNode.clone();
} catch (CloneNotSupportedException e) {
clone = new ClosingNode();
}
clone.init(ruby, uri, localName, qName, depth, langStack, xmlBaseStack);
return clone;
}
public static class ClosingNode extends ReaderNode {
public ClosingNode() {}
public ClosingNode(Ruby ruby, String uri, String localName, String qName, int depth, Stack<String> langStack, Stack<String> xmlBaseStack) {
init(ruby, uri, localName, qName, depth, langStack, xmlBaseStack);
}
public void init(Ruby ruby, String uri, String localName, String qName, int depth, Stack<String> langStack, Stack<String> xmlBaseStack) {
this.ruby = ruby;
nodeType = ReaderNodeType.END_ELEMENT.getValue();
this.uri = "".equals(uri) ? null : uri;
this.localName = localName.trim().length() > 0 ? localName : qName;
this.name = qName;
parsePrefix(qName);
this.depth = depth;
if (!langStack.isEmpty()) this.lang = langStack.peek();
if (!xmlBaseStack.isEmpty()) this.xmlBase = xmlBaseStack.peek();
}
@Override
public IRubyObject getAttributeCount() {
return ruby.newFixnum(0);
}
@Override
public RubyBoolean hasValue() {
return ruby.getFalse();
}
@Override
public String getString() {
StringBuffer sb = new StringBuffer();
sb.append("</").append(name).append(">");
return new String(sb);
}
}
public static ElementNode createElementNode(Ruby ruby, String uri, String localName, String qName, XMLAttributes attrs, int depth, Stack<String> langStack, Stack<String> xmlBaseStack) {
if (elementNode == null) elementNode = new ElementNode();
ElementNode clone;
try {
clone = (ElementNode) elementNode.clone();
} catch (CloneNotSupportedException e) {
clone = new ElementNode();
}
clone.init(ruby, uri, localName, qName, attrs, depth, langStack, xmlBaseStack);
return clone;
}
public static class ElementNode extends ReaderNode {
private final List<String> attributeStrings = new ArrayList<String>();
public ElementNode() {}
public ElementNode(Ruby ruby, String uri, String localName, String qName, XMLAttributes attrs, int depth, Stack<String> langStack, Stack<String> xmlBaseStack) {
init(ruby, uri, localName, qName, attrs, depth, langStack, xmlBaseStack);
}
public void init(Ruby ruby, String uri, String localName, String qName, XMLAttributes attrs, int depth, Stack<String> langStack, Stack<String> xmlBaseStack) {
this.ruby = ruby;
this.nodeType = ReaderNodeType.ELEMENT.getValue();
this.uri = "".equals(uri) ? null : uri;
this.localName = localName.trim().length() > 0 ? localName : qName;
this.name = qName;
parsePrefix(qName);
this.depth = depth;
parseAttributes(attrs, langStack, xmlBaseStack);
}
@Override
public RubyBoolean hasValue() {
return ruby.getFalse();
}
private void parseAttributes(XMLAttributes attrs, Stack<String> langStack, Stack<String> xmlBaseStack) {
if (attrs.getLength() > 0) attributeList = new ReaderAttributeList();
String u, n, v;
for (int i = 0; i < attrs.getLength(); i++) {
u = attrs.getURI(i);
n = attrs.getQName(i);
v = attrs.getValue(i);
if (isNamespace(n)) {
if (namespaces == null) namespaces = new HashMap<String, String>();
namespaces.put(n, v);
} else {
if (lang == null) lang = resolveLang(n, v, langStack);
if (xmlBase == null) xmlBase = resolveXmlBase(n, v, xmlBaseStack);
}
attributeList.add(u, n, v);
attributeStrings.add(n + "=\"" + v + "\"");
}
}
private String resolveLang(String n, String v, Stack<String> langStack) {
if ("xml:lang".equals(n)) {
return v;
} else if (!langStack.isEmpty()) {
return langStack.peek();
} else {
return null;
}
}
private String resolveXmlBase(String n, String v, Stack<String> xmlBaseStack) {
if (isXmlBase(n)) {
return getXmlBaseUri(n, v, xmlBaseStack);
} else if (!xmlBaseStack.isEmpty()) {
return xmlBaseStack.peek();
} else {
return null;
}
}
private String getXmlBaseUri(String n, String v, Stack<String> xmlBaseStack) {
if ("xml:base".equals(n)) {
if (v.startsWith("http://")) {
return v;
} else if (v.startsWith("/") && v.endsWith("/")) {
String sub = v.substring(1, v.length() - 2);
String base = xmlBaseStack.peek();
if (base.endsWith("/")) {
base = base.substring(0, base.length() - 1);
}
int pos = base.lastIndexOf("/");
return base.substring(0, pos).concat(sub);
} else {
String base = xmlBaseStack.peek();
if (base.endsWith("/")) return base.concat(v);
else return base.concat("/").concat(v);
}
} else if ("xlink:href".equals(n)) {
if (v.startsWith("http://")) {
return v;
} else if (!xmlBaseStack.isEmpty()) {
String base = xmlBaseStack.peek();
return base;
}
}
return null;
}
@Override
public String getString() {
StringBuffer sb = new StringBuffer();
sb.append("<").append(name);
if (attributeList != null) {
for (int i=0; i<attributeList.length; i++) {
sb.append(" ").append(attributeStrings.get(i));
}
}
if (hasChildren) sb.append(">");
else sb.append("/>");
return new String(sb);
}
}
public static class ReaderAttributeList {
List<String> namespaces = new ArrayList<String>();
List<String> names = new ArrayList<String>();
List<String> values = new ArrayList<String>();
int length = 0;
void add(String namespace, String name, String value) {
namespace = namespace != null ? namespace : "";
namespaces.add(namespace);
name = name != null ? name : "";
names.add(name);
value = value != null ? value : "";
values.add(value);
length++;
}
String getByName(String name) {
for (int i=0; i<names.size(); i++) {
if (name.equals(names.get(i))) {
return values.get(i);
}
}
return null;
}
}
public static class EmptyNode extends ReaderNode {
public EmptyNode(Ruby ruby) {
this.ruby = ruby;
this.nodeType = ReaderNodeType.NODE.getValue();
}
@Override
public IRubyObject getXmlVersion() {
return this.ruby.getNil();
}
@Override
public RubyBoolean hasValue() {
return ruby.getFalse();
}
@Override
public String getString() {
return null;
}
}
public static class ExceptionNode extends EmptyNode {
private final XmlSyntaxError exception;
// Still don't know what to do with ex.
public ExceptionNode(Ruby runtime, Exception ex) {
super(runtime);
exception = (XmlSyntaxError) NokogiriService.XML_SYNTAXERROR_ALLOCATOR.allocate(runtime, getNokogiriClass(ruby, "Nokogiri::XML::SyntaxError"));
}
@Override
public boolean isError() {
return true;
}
@Override
public IRubyObject toSyntaxError() {
return this.exception;
}
}
public static TextNode createTextNode(Ruby ruby, String content, int depth, Stack<String> langStack, Stack<String> xmlBaseStack) {
if (textNode == null) textNode = new TextNode();
TextNode clone;
try {
clone = (TextNode) textNode.clone();
} catch (CloneNotSupportedException e) {
clone = new TextNode();
}
clone.init(ruby, content, depth, langStack, xmlBaseStack);
return clone;
}
public static class TextNode extends ReaderNode {
public TextNode() {}
public TextNode(Ruby ruby, String content, int depth, Stack<String> langStack, Stack<String> xmlBaseStack) {
init(ruby, content, depth, langStack, xmlBaseStack);
}
public void init(Ruby ruby, String content, int depth, Stack<String> langStack, Stack<String> xmlBaseStack) {
this.ruby = ruby;
this.value = content;
this.localName = "#text";
this.name = "#text";
this.depth = depth;
if (content.trim().length() > 0) nodeType = ReaderNodeType.TEXT.getValue();
else nodeType = ReaderNodeType.SIGNIFICANT_WHITESPACE.getValue();
if (!langStack.isEmpty()) this.lang = langStack.peek();
if (!xmlBaseStack.isEmpty()) this.xmlBase = xmlBaseStack.peek();
}
@Override
public RubyBoolean hasValue() {
return ruby.getTrue();
}
@Override
public String getString() {
return value;
}
}
}