1   package org.daisy.pipeline.css;
2   
3   import java.io.InputStreamReader;
4   import java.io.IOException;
5   import java.io.Reader;
6   import java.io.StringReader;
7   import java.net.URL;
8   import java.net.URI;
9   import java.nio.charset.Charset;
10  import java.util.ArrayList;
11  import java.util.HashMap;
12  import java.util.List;
13  import java.util.Map;
14  import java.util.Optional;
15  import java.util.StringTokenizer;
16  import java.util.function.Function;
17  
18  import javax.xml.namespace.QName;
19  import javax.xml.stream.XMLStreamException;
20  import javax.xml.transform.Source;
21  import javax.xml.transform.stream.StreamSource;
22  import javax.xml.transform.URIResolver;
23  
24  import com.google.common.collect.Iterables;
25  import com.google.common.io.CharStreams;
26  
27  import cz.vutbr.web.css.CombinedSelector;
28  import cz.vutbr.web.css.CSSFactory;
29  import cz.vutbr.web.css.CSSProperty;
30  import cz.vutbr.web.css.Declaration;
31  import cz.vutbr.web.css.MediaSpec;
32  import cz.vutbr.web.css.NetworkProcessor;
33  import cz.vutbr.web.css.NodeData;
34  import cz.vutbr.web.css.RuleFactory;
35  import cz.vutbr.web.css.Selector;
36  import cz.vutbr.web.css.Selector.Combinator;
37  import cz.vutbr.web.css.Selector.PseudoElement;
38  import cz.vutbr.web.css.SourceLocator;
39  import cz.vutbr.web.css.StyleSheet;
40  import cz.vutbr.web.css.SupportedCSS;
41  import cz.vutbr.web.css.Term;
42  import cz.vutbr.web.css.TermIdent;
43  import cz.vutbr.web.css.TermInteger;
44  import cz.vutbr.web.css.TermString;
45  import cz.vutbr.web.csskit.antlr.CSSParserFactory;
46  import cz.vutbr.web.csskit.antlr.CSSSource;
47  import cz.vutbr.web.csskit.antlr.CSSSourceReader;
48  import cz.vutbr.web.csskit.antlr.DefaultCSSSourceReader;
49  import cz.vutbr.web.csskit.antlr.SourceMap;
50  import cz.vutbr.web.csskit.DefaultNetworkProcessor;
51  import cz.vutbr.web.csskit.RuleXslt;
52  import cz.vutbr.web.domassign.Analyzer;
53  import cz.vutbr.web.domassign.DeclarationTransformer;
54  import cz.vutbr.web.domassign.StyleMap;
55  import net.sf.saxon.dom.NodeOverNodeInfo;
56  import net.sf.saxon.om.NodeInfo;
57  
58  import org.daisy.common.file.URLs;
59  import org.daisy.common.stax.BaseURIAwareXMLStreamWriter;
60  import static org.daisy.common.stax.XMLStreamWriterHelper.writeAttribute;
61  import static org.daisy.common.stax.XMLStreamWriterHelper.writeAttributes;
62  import static org.daisy.common.stax.XMLStreamWriterHelper.writeCharacters;
63  import static org.daisy.common.stax.XMLStreamWriterHelper.writeComment;
64  import static org.daisy.common.stax.XMLStreamWriterHelper.writeProcessingInstruction;
65  import static org.daisy.common.stax.XMLStreamWriterHelper.writeStartElement;
66  import org.daisy.common.transform.InputValue;
67  import org.daisy.common.transform.Mult;
68  import org.daisy.common.transform.SingleInSingleOutXMLTransformer;
69  import org.daisy.common.transform.TransformerException;
70  import org.daisy.common.transform.XMLInputValue;
71  import org.daisy.common.transform.XMLOutputValue;
72  
73  import org.slf4j.Logger;
74  import org.slf4j.LoggerFactory;
75  
76  import org.w3c.dom.Document;
77  import org.w3c.dom.Element;
78  import org.w3c.dom.NamedNodeMap;
79  import org.w3c.dom.Node;
80  import org.w3c.dom.ProcessingInstruction;
81  
82  public abstract class JStyleParserCssCascader extends SingleInSingleOutXMLTransformer {
83  
84  	private final String userAndUserAgentStylesheets;
85  	private final MediaSpec medium;
86  	private final QName removeInlineStyleAttribute;
87  	private final CSSParserFactory parserFactory;
88  	private final RuleFactory ruleFactory;
89  	private final SupportedCSS supportedCSS;
90  	private final DeclarationTransformer declarationTransformer;
91  	private final CSSSourceReader cssReader;
92  	private final XsltProcessor xsltProcessor;
93  
94  	private static final Logger logger = LoggerFactory.getLogger(JStyleParserCssCascader.class);
95  
96  	/**
97  	 * @param ruleFactory to create StyleSheet objects
98  	 */
99  	public JStyleParserCssCascader(URIResolver uriResolver,
100 	                               CssPreProcessor preProcessor,
101 	                               XsltProcessor xsltProcessor,
102 	                               String userAndUserAgentStylesheets,
103 	                               Medium medium,
104 	                               QName removeInlineStyleAttribute,
105 	                               CSSParserFactory parserFactory,
106 	                               RuleFactory ruleFactory,
107 	                               SupportedCSS supportedCSS,
108 	                               DeclarationTransformer declarationTransformer) {
109 		this.userAndUserAgentStylesheets = userAndUserAgentStylesheets;
110 		this.medium = medium.asMediaSpec();
111 		this.removeInlineStyleAttribute = removeInlineStyleAttribute;
112 		this.parserFactory = parserFactory;
113 		this.ruleFactory = ruleFactory;
114 		this.supportedCSS = supportedCSS;
115 		this.declarationTransformer = declarationTransformer;
116 		this.xsltProcessor = xsltProcessor;
117 		NetworkProcessor network = new DefaultNetworkProcessor() {
118 				@Override
119 				public Reader fetch(URL url, Charset encoding, boolean forceEncoding, boolean assertEncoding) throws IOException {
120 					logger.debug("Fetching style sheet: " + url);
121 					if (uriResolver != null) {
122 						Source resolved; {
123 							try {
124 								resolved = uriResolver.resolve(URLs.asURI(url).toASCIIString(), ""); }
125 							catch (javax.xml.transform.TransformerException e) {
126 								throw new IOException(e); }}
127 						if (resolved != null) {
128 							if (resolved instanceof StreamSource) {
129 								InputStreamReader r = detectEncodingAndSkipBOM(
130 									((StreamSource)resolved).getInputStream(), null, encoding, forceEncoding);
131 								if (assertEncoding) {
132 									if (encoding == null)
133 										throw new IllegalArgumentException("encoding must not be null");
134 									if (!encoding.equals(getEncoding(r)))
135 										throw new IOException("Failed to read URL as " + encoding + ": " + url);
136 								}
137 								return r;
138 							} else {
139 								url = new URL(resolved.getSystemId());
140 							}
141 						}
142 					}
143 					return super.fetch(url, encoding, forceEncoding, assertEncoding);
144 				}
145 			};
146 		/*
147 		 * CSSSourceReader that handles media types supported by preProcessor. Throws a
148 		 * IOException if something goes wrong when resolving the source or if the
149 		 * pre-processing fails.
150 		 */
151 		this.cssReader = new DefaultCSSSourceReader(network) {
152 				@Override
153 				public boolean supportsMediaType(String mediaType, URL url) {
154 					if ("text/css".equals(mediaType))
155 						return true;
156 					else if (mediaType == null && (url == null || url.toString().endsWith(".css")))
157 						return true;
158 					else if (preProcessor == null)
159 						return false;
160 					else
161 						return preProcessor.supportsMediaType(mediaType, url);
162 				}
163 				@Override
164 				public CSSInputStream read(CSSSource source) throws IOException {
165 					if (source.type == CSSSource.SourceType.URL && uriResolver != null) {
166 						try {
167 							// NetworkProcessor.fetch() also does resolve() but we need an additional resolve() to give
168 							// CSSInputStream the correct base URL
169 							Source resolved = uriResolver.resolve(URLs.asURI((URL)source.source).toASCIIString(), "");
170 							if (resolved != null)
171 								source = new CSSSource(new URL(resolved.getSystemId()), source.encoding, source.mediaType);
172 						} catch (javax.xml.transform.TransformerException e) {
173 							throw new IOException(e);
174 						}
175 					}
176 					CSSInputStream cssStream = super.read(source);
177 					if (!("text/css".equals(source.mediaType)
178 					      || source.mediaType == null && (source.type != CSSSource.SourceType.URL
179 					                                      || ((URL)source.source).toString().endsWith(".css")))) {
180 						// preProcessor must be non-null
181 						try {
182 							CssPreProcessor.PreProcessingResult result = preProcessor.compile(
183 								new CssPreProcessor.PreProcessingSource(cssStream.stream, URLs.asURI(cssStream.base)) {
184 									@Override
185 									public Reader reread(Charset encoding) throws IOException {
186 										Reader r = cssStream.reread(encoding);
187 										stream.close();
188 										return r;
189 									}
190 								}
191 							);
192 							SourceMap sourceMap; {
193 								if (result.sourceMap != null) {
194 									SourceMap m = SourceMapReader.read(result.sourceMap, result.base);
195 									if (cssStream.sourceMap != null) {
196 										sourceMap = new SourceMap() {
197 											public SourceLocator get(int line, int column) {
198 												SourceLocator loc = m.get(line, column);
199 												if (loc != null && loc.getURL().equals(cssStream.base))
200 													loc = cssStream.sourceMap.get(loc.getLineNumber(), loc.getColumnNumber());
201 												return loc;
202 											}
203 											public SourceLocator floor(int line, int column) {
204 												SourceLocator loc = m.floor(line, column);
205 												if (loc != null && loc.getURL().equals(cssStream.base))
206 													loc = cssStream.sourceMap.floor(loc.getLineNumber(), loc.getColumnNumber());
207 												return loc;
208 											}
209 											public SourceLocator ceiling(int line, int column) {
210 												SourceLocator loc = m.ceiling(line, column);
211 												if (loc != null && loc.getURL().equals(cssStream.base))
212 													loc = cssStream.sourceMap.ceiling(loc.getLineNumber(), loc.getColumnNumber());
213 												return loc;
214 											}
215 										};
216 									} else
217 										sourceMap = m;
218 								} else
219 									sourceMap = cssStream.sourceMap;
220 							}
221 							String resultString = CharStreams.toString(result.stream);
222 							return new CSSInputStream(new StringReader(resultString), cssStream.base, sourceMap) {
223 								@Override
224 								public Reader reread(Charset encoding) throws IOException {
225 									// assuming that the preprocessor has already handled @charset rules
226 									// simply return the remainder of the stream
227 									result.stream.close();
228 									return new StringReader(resultString);
229 								}
230 							};
231 						} catch (RuntimeException e) {
232 							throw new IOException(
233 								(source.mediaType != null ? (source.mediaType + " p") : "P")
234 								+ "re-processing failed: " + e.getMessage(), e);
235 						}
236 					} else
237 						return cssStream;
238 				}
239 			};
240 	}
241 
242 	private StyleSheet styleSheet = null;
243 
244 	public Runnable transform(XMLInputValue<?> source, XMLOutputValue<?> result, InputValue<?> params) throws TransformerException {
245 		if (source == null || result == null)
246 			throw new TransformerException(new IllegalArgumentException());
247 		return () -> transform(source.ensureSingleItem().mult(2), result.asXMLStreamWriter());
248 	}
249 
250 	private void transform(Mult<? extends XMLInputValue<?>> source, BaseURIAwareXMLStreamWriter output) throws TransformerException {
251 		Node node = source.get().asNodeIterator().next();
252 		if (!(node instanceof Document))
253 			throw new TransformerException(new IllegalArgumentException());
254 		Document document = (Document)node;
255 		BaseURIAwareXMLStreamWriter writer = output;
256 		try {
257 			URI baseURI = new URI(document.getBaseURI());
258 			Function<Node,SourceLocator> nodeLocator = n -> {
259 				if (n instanceof NodeOverNodeInfo) {
260 					NodeInfo info = ((NodeOverNodeInfo)n).getUnderlyingNodeInfo();
261 					return new SourceLocator() {
262 						public URL getURL() {
263 							return URLs.asURL(URI.create(info.getBaseURI()));
264 						}
265 						public int getLineNumber() {
266 							int line = info.getLineNumber();
267 							if (line > 0)
268 								return line - 1; // line numbers are 1-based in Saxon
269 							else
270 								return -1;
271 						}
272 						public int getColumnNumber() {
273 							return info.getColumnNumber(); // -1
274 						}
275 					};
276 				} else {
277 					// should not happen
278 					return new SourceLocator() {
279 						public URL getURL() {
280 							return URLs.asURL(URI.create(n.getBaseURI()));
281 						}
282 						public int getLineNumber() {
283 							return -1;
284 						}
285 						public int getColumnNumber() {
286 							return -1;
287 						}
288 					};
289 				}
290 			};
291 			StyleMap styleMap;
292 			StyleSheet userAndUserAgentStyleSheet; {
293 				StyleSheet s = (StyleSheet)ruleFactory.createStyleSheet().unlock();
294 				if (userAndUserAgentStylesheets != null) {
295 					StringTokenizer t = new StringTokenizer(userAndUserAgentStylesheets);
296 					while (t.hasMoreTokens()) {
297 						URL u = URLs.asURL(URLs.resolve(baseURI, URLs.asURI(t.nextToken())));
298 						if (!cssReader.supportsMediaType(null, u))
299 							logger.warn("Style sheet type not supported: " + u);
300 						else
301 							try {
302 								s = parserFactory.append(new CSSSource(u, (Charset)null, (String)null), cssReader, s);
303 							} catch (IOException e) {
304 								logger.warn("Style sheet could not be parsed: " + u, e);
305 							}
306 					}
307 				}
308 				userAndUserAgentStyleSheet = s;
309 			}
310 			styleSheet = (StyleSheet)ruleFactory.createStyleSheet().unlock();
311 			styleSheet.addAll(userAndUserAgentStyleSheet);
312 			synchronized(JStyleParserCssCascader.class) {
313 				// FIXME: CSSParserFactory injected in CSSAssignTraversal.<init> in CSSFactory.getUsedStyles
314 				CSSFactory.registerCSSParserFactory(parserFactory);
315 				styleSheet = CSSFactory.getUsedStyles(document, nodeLocator, medium, cssReader, styleSheet);
316 			}
317 
318 			// FIXME: use a dedicated parser to parse @xslt rules (and ignore all the rest)
319 			XMLInputValue<?> transformed = null;
320 			for (RuleXslt r : Iterables.filter(styleSheet, RuleXslt.class)) {
321 				Map<QName,InputValue<?>> params = new HashMap<>();
322 				for (Declaration d : r) {
323 					List<Object> val = new ArrayList<>();
324 					boolean invalid = false;
325 					for (Term<?> t : d) {
326 						if (t instanceof TermIdent || t instanceof TermString) {
327 							String v = ((Term<String>)t).getValue();
328 							if (t instanceof TermIdent && ("true".equals(v) || "false".equals(v)))
329 								val.add(Boolean.valueOf(v));
330 							else
331 								val.add(v);
332 						} else if (t instanceof TermInteger)
333 							val.add(((TermInteger)t).getIntValue());
334 						else {
335 							logger.warn("@xslt parameter value must be a sequence of string, ident or integer, but got " + d);
336 							invalid = true;
337 							break;
338 						}
339 					}
340 					if (!invalid)
341 						params.put(new QName(d.getProperty()), new InputValue<>(val));
342 				}
343 				Document doc; {
344 					if (transformed != null) {
345 						Mult<? extends XMLInputValue<?>> m = transformed.mult(2);
346 						doc = (Document)m.get().ensureSingleItem().asNodeIterator().next();
347 						transformed = m.get();
348 					} else
349 						doc = document;
350 				}
351 				params.put(
352 					new QName("style"),
353 					new InputValue<>(
354 						new StyleAccessor() {
355 							StyleMap style = null;
356 							public Optional<String> get(Element element, String property) {
357 								if (style == null) {
358 									StyleSheet s = (StyleSheet)ruleFactory.createStyleSheet().unlock();
359 									s.addAll(userAndUserAgentStyleSheet);
360 									synchronized(JStyleParserCssCascader.class) {
361 										// FIXME: CSSParserFactory injected in CSSAssignTraversal.<init> in CSSFactory.getUsedStyles
362 										CSSFactory.registerCSSParserFactory(parserFactory);
363 										// not using element.getOwnerDocument() because base URI is null/empty in some cases
364 										s = CSSFactory.getUsedStyles(doc, nodeLocator, medium, cssReader, s);
365 									}
366 									style = new Analyzer(s, declarationTransformer, supportedCSS)
367 									            .evaluateDOM(doc, medium, true); }
368 								NodeData data = style.get(element);
369 								if (data != null) {
370 									Term<?> value = data.getValue(property, true);
371 									if (value != null)
372 										return Optional.of(serializeValue(value));
373 									else {
374 										CSSProperty p = data.getProperty(property);
375 										if (p != null)
376 											return Optional.of(p.toString()); }}
377 								return Optional.empty();
378 							}
379 
380 							Map<String,List<CombinedSelector>> selectorCache = null; // cache of compiled selectors
381 							public boolean matches(Element element, String selector) {
382 								List<CombinedSelector> compiledSelector;
383 								if (selectorCache == null)
384 									selectorCache = new HashMap<>();
385 								if (selectorCache.containsKey(selector))
386 									compiledSelector = selectorCache.get(selector);
387 								else {
388 									compiledSelector = parserFactory.parseSelector(selector, r.namespaces);
389 									if (compiledSelector != null)
390 										selectorCache.put(selector, compiledSelector); }
391 								if (compiledSelector == null)
392 									return false;
393 								for (CombinedSelector sel : compiledSelector) {
394 									boolean match = false;
395 									Combinator combinator = null;
396 									for (int i = sel.size() - 1; i >= 0; i--) {
397 										Selector s = sel.get(i);
398 										if (combinator == null)
399 											match = s.matches(element);
400 										else if (combinator == Combinator.ADJACENT) {
401 											Node adjacent = element.getPreviousSibling();
402 											while (adjacent != null && !(adjacent instanceof Element))
403 												adjacent = adjacent.getPreviousSibling();
404 											match = adjacent != null && s.matches((Element)adjacent); }
405 										else if (combinator == Combinator.PRECEDING) {
406 											match = false;
407 											Node preceding = element.getPreviousSibling();
408 											while (!match) {
409 												while (preceding != null && !(preceding instanceof Element))
410 													preceding = preceding.getPreviousSibling();
411 												if (preceding == null)
412 													break;
413 												match = s.matches((Element)preceding);
414 												preceding = preceding.getPreviousSibling(); }}
415 										else if (combinator == Combinator.DESCENDANT) {
416 											match = false;
417 											Node ancestor = element.getParentNode();
418 											while (!match && ancestor != null) {
419 												match = s.matches((Element)ancestor);
420 												ancestor = ancestor.getParentNode(); }}
421 										else if (combinator == Combinator.CHILD) {
422 											Element parent = (Element)element.getParentNode();
423 											match = parent != null && s.matches(parent); }
424 										combinator = s.getCombinator();
425 										if (!match)
426 											return false;
427 									}
428 									if (match == true)
429 										return true;
430 								}
431 								return false;
432 							}
433 						}
434 					)
435 				);
436 				transformed = xsltProcessor.transform(
437 					URLs.resolve(URLs.asURI(r.base), URLs.asURI(r.uri)),
438 					transformed != null ? transformed : source.get(),
439 					params);
440 			}
441 			if (transformed != null) {
442 				node = transformed.ensureSingleItem().asNodeIterator().next();
443 				if (!(node instanceof Document))
444 					throw new TransformerException(
445 						new RuntimeException("XsltProcessor must return a (single) document"));
446 				document = (Document)node;
447 				// We assume that base URI did not change.
448 				// We need to recompute the stylesheet because of any possible inline styles, which
449 				// are attached to an element in the original document.
450 				styleSheet = (StyleSheet)ruleFactory.createStyleSheet().unlock();
451 				styleSheet.addAll(userAndUserAgentStyleSheet);
452 				synchronized(JStyleParserCssCascader.class) {
453 					// FIXME: CSSParserFactory injected in CSSAssignTraversal.<init> in CSSFactory.getUsedStyles
454 					CSSFactory.registerCSSParserFactory(parserFactory);
455 					styleSheet = CSSFactory.getUsedStyles(document, nodeLocator, medium, cssReader, styleSheet);
456 				}
457 			}
458 			styleMap = new Analyzer(styleSheet, declarationTransformer, supportedCSS).evaluateDOM(document, medium, true);
459 			writer.setBaseURI(baseURI);
460 			traverse(document, styleMap, writer);
461 		} catch (TransformerException e) {
462 			throw e;
463 		} catch (Exception e) {
464 			throw new TransformerException(e);
465 		} finally {
466 			styleSheet = null;
467 		}
468 	}
469 
470 	protected abstract Map<QName,String> serializeStyle(NodeData mainStyle, Map<PseudoElement,NodeData> pseudoStyles, Element context);
471 
472 	protected abstract String serializeValue(Term<?> value);
473 
474 	protected StyleSheet getParsedStyleSheet() {
475 		if (styleSheet == null)
476 			throw new UnsupportedOperationException();
477 		return styleSheet;
478 	}
479 
480 	protected void traverse(Node node, StyleMap styleMap, BaseURIAwareXMLStreamWriter writer) throws XMLStreamException {
481 		if (node.getNodeType() == Node.DOCUMENT_NODE) {
482 			writer.writeStartDocument();
483 			for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling())
484 				traverse(child, styleMap, writer);
485 			writer.writeEndDocument(); }
486 		else if (node.getNodeType() == Node.ELEMENT_NODE)
487 			processElement((Element)node, styleMap, writer);
488 		else if (node.getNodeType() == Node.COMMENT_NODE)
489 			writeComment(writer, node);
490 		else if (node.getNodeType() == Node.TEXT_NODE)
491 			writeCharacters(writer, node);
492 		else if (node.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE)
493 			// FIXME: broken: writeProcessingInstruction(writer, node);
494 			writer.writeProcessingInstruction(((ProcessingInstruction)node).getTarget(), node.getNodeValue());
495 		else
496 			throw new UnsupportedOperationException("Unexpected node type");
497 	}
498 
499 	protected void processElement(Element element, StyleMap styleMap, BaseURIAwareXMLStreamWriter writer) throws XMLStreamException {
500 		writeStartElement(writer, element);
501 		copyAttributes(element, writer);
502 		Map<PseudoElement,NodeData> pseudoStyles = new HashMap<>(); {
503 			for (PseudoElement pseudo : styleMap.pseudoSet(element))
504 				pseudoStyles.put(pseudo, styleMap.get(element, pseudo)); }
505 		Map<QName,String> style = serializeStyle(
506 			styleMap.get(element),
507 			pseudoStyles,
508 			element);
509 		if (style != null)
510 			writeAttributes(writer, style);
511 		for (Node child = element.getFirstChild(); child != null; child = child.getNextSibling())
512 			traverse(child, styleMap, writer);
513 		writer.writeEndElement();
514 	}
515 
516 	protected void copyAttributes(Element element, BaseURIAwareXMLStreamWriter writer) throws XMLStreamException {
517 		NamedNodeMap attributes = element.getAttributes();
518 		for (int i = 0; i < attributes.getLength(); i++) {
519 			Node attr = attributes.item(i);
520 			if (removeInlineStyleAttribute == null || !nodeNameEquals(attr, removeInlineStyleAttribute))
521 				writeAttribute(writer, attr); }
522 	}
523 
524 	private static boolean nodeNameEquals(Node node, QName name) {
525 		if (name == null)
526 			return false;
527 		return new QName(node.getNamespaceURI(), node.getLocalName()).equals(name);
528 	}
529 }