1 package org.daisy.pipeline.html.impl;
2
3 import java.io.InputStream;
4 import java.io.InputStreamReader;
5 import java.io.IOException;
6 import java.io.Reader;
7 import java.io.StringReader;
8 import java.net.MalformedURLException;
9 import java.net.URI;
10 import java.net.URL;
11 import java.nio.charset.Charset;
12 import java.nio.charset.IllegalCharsetNameException;
13 import java.nio.charset.StandardCharsets;
14 import java.nio.charset.UnsupportedCharsetException;
15 import java.util.Iterator;
16 import java.util.NoSuchElementException;
17 import java.util.regex.Pattern;
18
19 import javax.xml.transform.dom.DOMSource;
20 import javax.xml.transform.sax.SAXSource;
21 import javax.xml.transform.Source;
22 import javax.xml.transform.TransformerException;
23 import javax.xml.transform.URIResolver;
24
25 import com.google.common.io.ByteStreams;
26 import com.google.common.io.CharStreams;
27
28 import com.xmlcalabash.util.TreeWriter;
29
30 import net.sf.saxon.dom.DocumentOverNodeInfo;
31 import net.sf.saxon.s9api.Processor;
32 import net.sf.saxon.s9api.SaxonApiException;
33 import net.sf.saxon.s9api.XdmNode;
34
35 import nu.validator.htmlparser.common.XmlViolationPolicy;
36
37 import org.daisy.common.file.URLs;
38
39 import org.daisy.common.saxon.xslt.XslTransformCompiler;
40 import org.daisy.common.xml.DocumentBuilder;
41
42 import org.osgi.service.component.annotations.Component;
43 import org.osgi.service.component.annotations.Reference;
44 import org.osgi.service.component.annotations.ReferenceCardinality;
45 import org.osgi.service.component.annotations.ReferencePolicy;
46
47 import org.w3c.dom.Attr;
48 import org.w3c.dom.Document;
49 import org.w3c.dom.Element;
50 import org.w3c.dom.Node;
51 import org.w3c.dom.NodeList;
52
53 import org.xml.sax.EntityResolver;
54 import org.xml.sax.InputSource;
55 import org.xml.sax.SAXException;
56
57 @Component(
58 name = "html-document-builder",
59 service = { DocumentBuilder.class }
60 )
61 public class HtmlDocumentBuilder implements DocumentBuilder {
62
63 private final static String MIME_HTML = "text/html";
64 private final static String MIME_XHTML = "application/xhtml+xml";
65
66
67
68
69 @Override
70 public boolean supportsContentType(String type) {
71 if (MIME_HTML.equals(type))
72 return true;
73 else if (xmlParser != null && MIME_XHTML.equals(type))
74 return true;
75 else
76 return false;
77 }
78
79
80
81
82 @Override
83 public Document parse(InputSource input) throws IOException, SAXException {
84 if (xmlParser != null)
85 try {
86 return xmlParser.parse(input);
87 } catch (SAXException|IllegalArgumentException e) {
88
89 }
90 URI baseURI; {
91 try {
92 baseURI = getBaseURI(input);
93 } catch (IllegalArgumentException e) {
94 if (input.getCharacterStream() == null && input.getByteStream() == null)
95 throw new IOException("Error reading input: invalid URI", e);
96 else
97 throw e;
98 }
99 }
100 if (input.getCharacterStream() == null && input.getByteStream() == null) {
101
102 if (baseURI == null)
103 throw new IOException(
104 "Error reading input: no character or byte stream available and no base URI");
105 Source s; {
106 try {
107 s = uriResolver.resolve(baseURI.toASCIIString(), null);
108 } catch (TransformerException e) {
109 throw new IOException("Error reading input: " + baseURI, e);
110 }
111 }
112 if (s != null) {
113 input = SAXSource.sourceToInputSource(s);
114 if (input == null)
115 throw new IOException("Error reading input: " + baseURI);
116 }
117 }
118 Reader reader = input.getCharacterStream();
119 String text;
120 byte[] bytes = null;
121 boolean checkEncoding = false;;
122 if (reader != null)
123 text = CharStreams.toString(reader);
124 else {
125 checkEncoding = true;
126 bytes = readBytes(input);
127 text = new String(bytes, StandardCharsets.UTF_8);
128 }
129 text = removeDoctype(text);
130 Document doc = readHtml(text);
131 if (checkEncoding) {
132 for (Node n : iterateNodeList(doc.getDocumentElement().getElementsByTagName("meta"))) {
133 Element elem = (Element)n;
134 Attr httpEquiv = elem.getAttributeNode("http-equiv");
135 if (httpEquiv != null && "Content-Type".equals(httpEquiv.getValue())) {
136 Attr contentType = elem.getAttributeNode("content");
137 if (contentType != null) {
138 String charset = contentType.getValue()
139 .replaceAll(".*?charset\\s*=\\s*('(.*?)'|\"(.*?)\"|([^'\"\\s][^\\s;]*)|.).*",
140 "$2$3$4");
141 if (!charset.equals(contentType.getValue())) {
142 Charset encoding; {
143 try {
144 encoding = Charset.forName(charset.toLowerCase());
145 } catch (IllegalCharsetNameException e) {
146 throw new IOException("Invalid charset: " + charset, e);
147 } catch (UnsupportedCharsetException e) {
148 throw new IOException("Unsupported charset: " + charset, e);
149 }
150 }
151 if (!StandardCharsets.UTF_8.equals(encoding)) {
152 text = new String(bytes, encoding);
153 text = removeDoctype(text);
154 doc = readHtml(text);
155 }
156 }
157 break;
158 }
159 }
160 }
161 }
162
163 try {
164 XdmNode saxonDoc = saxonProcessor.newDocumentBuilder().build(new DOMSource(doc));
165 saxonDoc = new XslTransformCompiler(saxonProcessor.getUnderlyingConfiguration())
166 .compileStylesheet(URLs.getResourceFromJAR("/xml/xslt/namespace-fixup.xsl", HtmlDocumentBuilder.class)
167 .openStream())
168 .newTransformer()
169 .transform(saxonDoc);
170 if (baseURI != null) {
171
172
173 TreeWriter fixbase = new TreeWriter(saxonProcessor);
174 fixbase.startDocument(baseURI);
175 fixbase.addSubtree(saxonDoc);
176 fixbase.endDocument();
177 saxonDoc = fixbase.getResult();
178 }
179 doc = (Document)DocumentOverNodeInfo.wrap(saxonDoc.getUnderlyingNode());
180 } catch (IOException|SaxonApiException e) {
181 throw new SAXException("Error parsing input", e);
182 }
183 return doc;
184 }
185
186
187
188
189
190 private static URI getBaseURI(InputSource input) throws IllegalArgumentException {
191 String sysId = input.getSystemId();
192 if (sysId == null || "".equals(sysId))
193 return null;
194 try {
195 return URI.create(sysId);
196 } catch (IllegalArgumentException e) {
197 throw new IllegalArgumentException("Invalid base URI: " + sysId, e);
198 }
199 }
200
201 private static byte[] readBytes(InputSource input) throws IOException {
202 if (input.getCharacterStream() != null)
203 throw new IllegalArgumentException();
204 InputStream s = input.getByteStream();
205 if (s == null) {
206 URI uri; {
207 try {
208 uri = getBaseURI(input);
209 if (uri == null)
210 throw new IOException(
211 "Error reading input: no character or byte stream available and no base URI");
212 } catch (IllegalArgumentException e) {
213 throw new IOException("Error reading input: invalid URI", e);
214 }
215 }
216 URL url; {
217 try {
218 url = uri.toURL();
219 } catch (IllegalArgumentException e) {
220
221 throw new IOException( "Error reading input: not an absolute URI: " + uri, e);
222 } catch (MalformedURLException e) {
223 throw new IOException( "Error reading input: malformed URL: " + uri, e);
224 }
225 }
226 s = url.openStream();
227 }
228 return ByteStreams.toByteArray(s);
229 }
230
231 private static String removeDoctype(String input) {
232 return Pattern.compile("^<[!\\?].*?(<[^!\\?])", Pattern.DOTALL).matcher(input).replaceFirst("$1");
233 }
234
235 private Document readHtml(String input) throws SAXException, IOException {
236 nu.validator.htmlparser.dom.HtmlDocumentBuilder builder
237 = new nu.validator.htmlparser.dom.HtmlDocumentBuilder(XmlViolationPolicy.ALTER_INFOSET);
238 builder.setEntityResolver(entityResolver);
239 try {
240 return builder.parse(new InputSource(new StringReader(input)));
241 } catch (IOException e) {
242 throw e;
243 } catch (SAXException e) {
244 throw e;
245 }
246 }
247
248 private static Iterable<Node> iterateNodeList(NodeList nodeList) {
249 int len = nodeList.getLength();
250 return new Iterable<Node>() {
251 @Override
252 public Iterator<Node> iterator() {
253 return new Iterator() {
254 int i = 0;
255 @Override
256 public boolean hasNext() {
257 return i < len;
258 }
259 @Override
260 public Node next() throws NoSuchElementException {
261 if (!hasNext())
262 throw new NoSuchElementException();
263 return nodeList.item(i++);
264 }
265 };
266 }
267 };
268 }
269
270 private DocumentBuilder xmlParser = null;
271 private URIResolver uriResolver = null;
272 private EntityResolver entityResolver = null;
273 private Processor saxonProcessor = null;
274
275
276
277 @Reference(
278 name = "xml-parser",
279 unbind = "-",
280 service = DocumentBuilder.class,
281 cardinality = ReferenceCardinality.MULTIPLE,
282 policy = ReferencePolicy.STATIC
283 )
284 public void setXmlParser(DocumentBuilder parser) {
285 if (xmlParser == null && parser.supportsContentType(MIME_XHTML))
286 xmlParser = parser;
287 }
288
289 @Reference(
290 name = "uri-resolver",
291 unbind = "-",
292 service = URIResolver.class,
293 cardinality = ReferenceCardinality.MANDATORY,
294 policy = ReferencePolicy.STATIC
295 )
296 public void setURIResolver(URIResolver resolver) {
297 uriResolver = resolver;
298 }
299
300 @Reference(
301 name = "entity-resolver",
302 unbind = "-",
303 service = EntityResolver.class,
304 cardinality = ReferenceCardinality.MANDATORY,
305 policy = ReferencePolicy.STATIC
306 )
307 public void setEntityResolver(EntityResolver resolver) {
308 entityResolver = resolver;
309 }
310
311 @Reference(
312 name = "saxon-processor",
313 unbind = "-",
314 service = Processor.class,
315 cardinality = ReferenceCardinality.MANDATORY,
316 policy = ReferencePolicy.STATIC
317 )
318 public void setSaxonProcessor(Processor processor) {
319 saxonProcessor = processor;
320 }
321 }