blob: 9a35807113d167859f1a4c29f99f5f6a01299437 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.any23.extractor.html;
import org.apache.any23.extractor.ExtractionException;
import org.apache.any23.rdf.Any23ValueFactoryWrapper;
import org.apache.any23.rdf.RDFUtils;
import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
/**
* A wrapper around the DOM representation of an HTML document. Provides convenience access to various parts of the
* document.
*
* @author Gabriele Renzi
* @author Michele Mostarda
*/
public class HTMLDocument {
private final static XPath xPathEngine = XPathFactory.newInstance().newXPath();
private final static Logger log = LoggerFactory.getLogger(HTMLDocument.class);
private Node document;
private java.net.URI baseIRI;
private final Any23ValueFactoryWrapper valueFactory = new Any23ValueFactoryWrapper(
SimpleValueFactory.getInstance());
/**
* Reads a text field from the given node adding the content to the given <i>res</i> list.
*
* @param node
* the node from which read the content.
*
* @return a valid TextField
*/
public static TextField readTextField(Node node) {
TextField result;
final String name = node.getNodeName();
final NamedNodeMap attributes = node.getAttributes();
// excess of safety check, should be impossible
if (attributes == null) {
return new TextField(node.getTextContent(), node);
}
// first check if there are values inside
List<Node> values = DomUtils.findAllByClassName(node, "value");
if (!values.isEmpty()) {
StringBuilder val = new StringBuilder();
for (Node n : values)
val.append(n.getTextContent());
return new TextField(val.toString().trim(), node);
}
if ("ABBR".equals(name) && (null != attributes.getNamedItem("title"))) {
result = new TextField(attributes.getNamedItem("title").getNodeValue(), node);
} else if ("A".equals(name)) {
if (DomUtils.hasAttribute(node, "rel", "tag")) {
String href = extractRelTag(attributes);
result = new TextField(href, node);
} else
result = new TextField(node.getTextContent(), node);
} else if (("IMG".equals(name) || "AREA".equals(name)) && (null != attributes.getNamedItem("alt"))) {
result = new TextField(attributes.getNamedItem("alt").getNodeValue(), node);
} else {
result = new TextField(node.getTextContent(), node);
}
return result;
}
/**
* Reads an URL field from the given node adding the content to the given <i>res</i> list.
*
* @param res
* {@link java.util.List} of {@link org.apache.any23.extractor.html.HTMLDocument.TextField}
* @param node
* the node to read
*/
public static void readUrlField(List<TextField> res, Node node) {
String name = node.getNodeName();
NamedNodeMap attributes = node.getAttributes();
if (null == attributes) {
res.add(new TextField(node.getTextContent(), node));
return;
}
if ("A".equals(name) || "AREA".equals(name)) {
Node n = attributes.getNamedItem("href");
if (n != null) {
res.add(new TextField(n.getNodeValue(), n));
}
} else if ("ABBR".equals(name)) {
Node n = attributes.getNamedItem("title");
if (n != null) {
res.add(new TextField(n.getNodeValue(), n));
}
} else if ("IMG".equals(name)) {
Node n = attributes.getNamedItem("src");
if (n != null) {
res.add(new TextField(n.getNodeValue(), n));
} else {
n = attributes.getNamedItem("srcset");
if (n != null) {
res.add(new TextField(n.getNodeValue().split("[\\s,]+")[0], n));
}
}
} else if ("OBJECT".equals(name)) {
Node n = attributes.getNamedItem("data");
if (n != null) {
res.add(new TextField(n.getNodeValue(), n));
}
} else {
res.add(new TextField(extractHCardTextContent(node), node));
}
}
private static String extractHCardTextContent(Node node) {
StringBuilder sb = new StringBuilder();
NodeList nodes = node.getChildNodes();
// if at least one element with 'value' class, concatenate all text in value
if (extractTextInValue(nodes, sb) == 0) {
// otherwise, concatenate all text not in elements with 'type' class
extractTextNotInType(nodes, sb);
}
return sb.toString();
}
private static int extractTextInValue(NodeList nodes, StringBuilder b) {
int count = 0;
for (int i = 0, len = nodes.getLength(); i < len; i++) {
Node n = nodes.item(i);
if (DomUtils.hasClassName(n, "value")) {
count++;
b.append(n.getTextContent().trim());
} else {
count += extractTextInValue(n.getChildNodes(), b);
}
}
return count;
}
private static void extractTextNotInType(NodeList nodes, StringBuilder b) {
for (int i = 0, len = nodes.getLength(); i < len; i++) {
Node n = nodes.item(i);
if (n.getNodeType() == Node.TEXT_NODE) {
b.append(n.getNodeValue().trim());
} else if (!DomUtils.hasClassName(n, "type")) {
extractTextNotInType(n.getChildNodes(), b);
}
}
}
/**
* Extracts the href specific rel-tag string. See the <a href="http://microformats.org/wiki/rel-tag">rel-tag</a>
* specification.
*
* @param hrefAttributeContent
* the content of the <i>href</i> attribute.
*
* @return the rel-tag specification.
*/
public static String extractRelTag(String hrefAttributeContent) {
String[] all = hrefAttributeContent.split("[#?]");
// Cleanup spurious segments.
String path = all[0];
int pathLenghtMin1 = path.length() - 1;
if ('/' == path.charAt(pathLenghtMin1)) {
path = path.substring(0, pathLenghtMin1);
}
return path;
}
/**
* Extracts the href specific rel-tag string. See the <a href="http://microformats.org/wiki/rel-tag">rel-tag</a>
* specification.
*
* @param attributes
* the list of attributes of a node.
*
* @return the rel-tag specification.
*/
public static String extractRelTag(NamedNodeMap attributes) {
return extractRelTag(attributes.getNamedItem("href").getNodeValue());
}
/**
* Reads the text content of the given node and returns it. If the <code>prettify</code> flag is <code>true</code>
* the text is cleaned up.
*
* @param node
* node to read content.
* @param prettify
* if <code>true</code> blank chars will be removed.
*
* @return the read text.
*/
public static String readNodeContent(Node node, boolean prettify) {
final String content = node.getTextContent();
return prettify ? content.trim().replaceAll("\\n", " ").replaceAll(" +", " ") : content;
}
/**
* Constructor accepting the root node.
*
* @param document
* a {@link org.w3c.dom.Node}
*/
public HTMLDocument(Node document) {
if (null == document)
throw new IllegalArgumentException("node cannot be null when constructing an HTMLDocument");
this.document = document;
}
/**
* @param uri
* string to resolve to {@link org.eclipse.rdf4j.model.IRI}
*
* @return An absolute IRI, or null if the IRI is not fixable
*
* @throws org.apache.any23.extractor.ExtractionException
* If the base IRI is invalid
*/
public IRI resolveIRI(String uri) throws ExtractionException {
return valueFactory.resolveIRI(uri, getBaseIRI());
}
public String find(String xpath) {
return DomUtils.find(getDocument(), xpath);
}
public Node findNodeById(String id) {
return DomUtils.findNodeById(getDocument(), id);
}
public List<Node> findAll(String xpath) {
return DomUtils.findAll(getDocument(), xpath);
}
public String findMicroformattedValue(String objectTag, String object, String fieldTag, String field, String key) {
Node node = findMicroformattedObjectNode(objectTag, object);
if (null == node)
return "";
// try to check if it is inline
if (DomUtils.hasClassName(node, field))
return node.getTextContent();
// failed, try to find it in a child
try {
String xpath = ".//" + fieldTag + "[contains(@class, '" + field + "')]/" + key;
String value = (String) xPathEngine.evaluate(xpath, node, XPathConstants.STRING);
if (null == value) {
return "";
}
return value;
} catch (XPathExpressionException ex) {
throw new RuntimeException("Should not happen, XPath expression is built locally", ex);
}
}
public Node getDocument() {
return document;
}
/**
* Returns a singular text field.
*
* @param className
* name of class containing text.
*
* @return if multiple values are found just the first is returned, if we want to check that there are no n-ary
* values use plural finder
*/
public TextField getSingularTextField(String className) {
TextField[] res = getPluralTextField(className);
if (res.length == 0)
return new TextField("", null);
return res[0];
}
/**
* Returns a plural text field.
*
* @param className
* name of class node containing text.
*
* @return list of fields.
*/
public TextField[] getPluralTextField(String className) {
List<TextField> res = new ArrayList<TextField>();
List<Node> nodes = DomUtils.findAllByClassName(getDocument(), className);
for (Node node : nodes) {
res.add(readTextField(node));
}
return res.toArray(new TextField[res.size()]);
}
/**
* Returns the URL associated to the field marked with class <i>className</i>.
*
* @param className
* name of node class containing the URL field.
*
* @return if multiple values are found just the first is returned, if we want to check that there are no n-ary
* values use plural finder
*/
public TextField getSingularUrlField(String className) {
TextField[] res = getPluralUrlField(className);
if (res.length < 1)
return new TextField("", null);
return res[0];
}
/**
* Returns the list of URLs associated to the fields marked with class <i>className</i>.
*
* @param className
* name of node class containing the URL field.
*
* @return the list of {@link HTMLDocument.TextField} found.
*/
public TextField[] getPluralUrlField(String className) {
List<TextField> res = new ArrayList<TextField>();
List<Node> nodes = DomUtils.findAllByClassName(getDocument(), className);
for (Node node : nodes)
readUrlField(res, node);
return res.toArray(new TextField[res.size()]);
}
public Node findMicroformattedObjectNode(String objectTag, String name) {
List<Node> nodes = DomUtils.findAllByTagAndClassName(getDocument(), objectTag, name);
if (nodes.isEmpty())
return null;
return nodes.get(0);
}
/**
* Read an attribute avoiding NullPointerExceptions, if the attr is missing it just returns an empty string.
*
* @param attribute
* the attribute name.
*
* @return the string representing the attribute.
*/
public String readAttribute(String attribute) {
return DomUtils.readAttribute(getDocument(), attribute);
}
/**
* Finds all the nodes by class name.
*
* @param clazz
* the class name.
*
* @return list of matching nodes.
*/
public List<Node> findAllByClassName(String clazz) {
return DomUtils.findAllByClassName(getDocument(), clazz);
}
/**
* Returns the text contained inside a node if leaf, <code>null</code> otherwise.
*
* @return the text of a leaf node.
*/
public String getText() {
NodeList children = getDocument().getChildNodes();
if (children.getLength() == 1 && children.item(0) instanceof Text) {
return children.item(0).getTextContent();
}
return null;
}
/**
* Returns the document default language.
*
* @return default language if any, <code>null</code> otherwise.
*/
public String getDefaultLanguage() {
final String xpathLanguageSelector = "/HTML";
Node html;
try {
html = (Node) xPathEngine.evaluate(xpathLanguageSelector, document, XPathConstants.NODE);
} catch (XPathExpressionException xpeee) {
throw new IllegalStateException();
}
if (html == null) {
return null;
}
Node langAttribute = html.getAttributes().getNamedItem("xml:lang");
return langAttribute == null ? null : langAttribute.getTextContent();
}
/**
* Returns the sequence of ancestors from the document root to the local root (document).
*
* @return a sequence of node names.
*/
public String[] getPathToLocalRoot() {
return DomUtils.getXPathListForNode(document);
}
/**
* Extracts all the <code>rel</code> tag nodes.
*
* @return list of rel tag nodes.
*/
public TextField[] extractRelTagNodes() {
final List<Node> relTagNodes = DomUtils.findAllByAttributeName(getDocument(), "rel");
final List<TextField> result = new ArrayList<TextField>();
for (Node relTagNode : relTagNodes) {
readUrlField(result, relTagNode);
}
return result.toArray(new TextField[result.size()]);
}
private java.net.URI getBaseIRI() throws ExtractionException {
if (baseIRI == null) {
// document.getBaseURI() returns null for document URIs with
// special characters, e.g., http://semanticweb.org/wiki/Knud_Möller
// It also does *not* take html "base" elements into account.
// (But it does take into account urls specified by the attribute "xml:base".)
// So, for now, let's use getDocumentURI() instead.
// TODO: Make this approach better.
Document doc = document instanceof Document ? (Document) document : document.getOwnerDocument();
if (doc == null) {
throw new ExtractionException(
"Node " + document.getNodeName() + " was not associated with a document.");
}
String uri = doc.getDocumentURI();
if (uri == null) {
throw new ExtractionException("document URI is null, this should not happen");
}
try {
baseIRI = new java.net.URI(RDFUtils.fixAbsoluteIRI(uri));
} catch (IllegalArgumentException ex) {
throw new ExtractionException("Error in base IRI: " + uri, ex);
} catch (URISyntaxException ex) {
throw new ExtractionException("Error in base IRI: " + uri, ex);
}
}
return baseIRI;
}
/**
* This class represents a text extracted from the <i>HTML</i> DOM related to the node from which such test has been
* retrieved.
*/
public static class TextField {
private String value;
private Node source;
public TextField(String value, Node source) {
this.value = value;
this.source = source;
}
public String value() {
return value;
}
public Node source() {
return source;
}
}
}