1   package org.daisy.pipeline.css.calabash.impl;
2   
3   import java.net.URI;
4   import java.net.URISyntaxException;
5   import java.net.URL;
6   import java.util.ArrayList;
7   import java.util.Arrays;
8   import java.util.Collections;
9   import java.util.Iterator;
10  import java.util.LinkedHashMap;
11  import java.util.List;
12  import java.util.Map;
13  import java.util.StringTokenizer;
14  
15  import javax.xml.namespace.QName;
16  import javax.xml.stream.XMLStreamWriter;
17  import javax.xml.transform.dom.DOMSource;
18  import javax.xml.transform.sax.SAXSource;
19  import javax.xml.transform.Source;
20  import javax.xml.transform.stream.StreamSource;
21  import javax.xml.transform.TransformerException;
22  import javax.xml.transform.URIResolver;
23  
24  import static com.google.common.collect.Iterators.forArray;
25  
26  import com.xmlcalabash.core.XProcConstants;
27  import com.xmlcalabash.core.XProcRuntime;
28  import com.xmlcalabash.io.ReadablePipe;
29  import com.xmlcalabash.io.WritablePipe;
30  import com.xmlcalabash.library.DefaultStep;
31  import com.xmlcalabash.model.RuntimeValue;
32  import com.xmlcalabash.runtime.XAtomicStep;
33  
34  import net.sf.saxon.ma.map.MapItem;
35  import net.sf.saxon.s9api.SaxonApiException;
36  
37  import org.daisy.common.file.URLs;
38  import org.daisy.common.saxon.SaxonHelper;
39  import static org.daisy.common.stax.XMLStreamWriterHelper.writeAttribute;
40  import static org.daisy.common.stax.XMLStreamWriterHelper.writeStartElement;
41  import org.daisy.common.xproc.calabash.XMLCalabashInputValue;
42  import org.daisy.common.xproc.calabash.XMLCalabashOutputValue;
43  import org.daisy.common.xproc.calabash.XProcStep;
44  import org.daisy.common.xproc.calabash.XProcStepProvider;
45  import org.daisy.common.xproc.XProcMonitor;
46  import org.daisy.pipeline.css.Medium;
47  import org.daisy.pipeline.css.CssAnalyzer;
48  import org.daisy.pipeline.css.CssAnalyzer.SassVariable;
49  import org.daisy.pipeline.css.UserAgentStylesheetRegistry;
50  
51  import org.osgi.service.component.annotations.Component;
52  import org.osgi.service.component.annotations.Reference;
53  import org.osgi.service.component.annotations.ReferenceCardinality;
54  import org.osgi.service.component.annotations.ReferencePolicy;
55  
56  import org.slf4j.Logger;
57  import org.slf4j.LoggerFactory;
58  
59  import org.w3c.dom.Document;
60  import org.w3c.dom.Node;
61  
62  import org.xml.sax.InputSource;
63  
64  public class CssAnalyzeStep extends DefaultStep implements XProcStep {
65  
66  	private ReadablePipe sourcePipe = null;
67  	private ReadablePipe contextPipe = null;
68  	private WritablePipe resultPipe = null;
69  	private final InMemoryURIResolver inMemoryResolver;
70  	private final URIResolver cssURIResolver;
71  	private final UserAgentStylesheetRegistry userAgentStylesheets;
72  
73  	private final static QName c_param_set = new QName(XProcConstants.NS_XPROC_STEP, "param-set");
74  	private final static QName c_param = new QName(XProcConstants.NS_XPROC_STEP, "param");
75  	private final static QName _name = new QName("name");
76  	private final static QName _value = new QName("value");
77  	private static final net.sf.saxon.s9api.QName _user_stylesheet = new net.sf.saxon.s9api.QName("user-stylesheet");
78  	private static final net.sf.saxon.s9api.QName _include_user_agent_stylesheet
79  		= new net.sf.saxon.s9api.QName("include-user-agent-stylesheet");
80  	private static final net.sf.saxon.s9api.QName _parameters = new net.sf.saxon.s9api.QName("parameters");
81  	private static final net.sf.saxon.s9api.QName _content_type = new net.sf.saxon.s9api.QName("content-type");
82  	private static final net.sf.saxon.s9api.QName _media = new net.sf.saxon.s9api.QName("media");
83  
84  	private static final String DEFAULT_MEDIUM = "embossed";
85  
86  	private CssAnalyzeStep(XProcRuntime runtime, XAtomicStep step,
87  	                       URIResolver resolver, UserAgentStylesheetRegistry userAgentStylesheets) {
88  		super(runtime, step);
89  		inMemoryResolver = new InMemoryURIResolver();
90  		cssURIResolver = fallback(inMemoryResolver, resolver, simpleURIResolver);
91  		this.userAgentStylesheets = userAgentStylesheets;
92  	}
93  
94  	@Override
95  	public void setInput(String port, ReadablePipe pipe) {
96  		if ("source".equals(port))
97  			sourcePipe = pipe;
98  		else
99  			contextPipe = pipe;
100 	}
101 
102 	@Override
103 	public void setOutput(String port, WritablePipe pipe) {
104 		resultPipe = pipe;
105 	}
106 
107 	@Override
108 	public void reset() {
109 		sourcePipe.resetReader();
110 		contextPipe.resetReader();
111 		resultPipe.resetWriter();
112 	}
113 
114 	@Override
115 	public void run() throws SaxonApiException {
116 		super.run();
117 		try {
118 			Map<String,String> params = new LinkedHashMap<>(); // use LinkedHashMap to get same order as insertion order
119 			RuntimeValue paramOption = getOption(_parameters);
120 			if (paramOption != null)
121 				for (Map.Entry<String,Object> e
122 				         : SaxonHelper.mapFromMapItem(
123 				               (MapItem)SaxonHelper.getSingleItem(paramOption.getValue().getUnderlyingValue()),
124 				               Object.class
125 				           ).entrySet())
126 					params.put(e.getKey(), "" + e.getValue());
127 			List<Medium> media = Medium.parseMultiple(getOption(_media, DEFAULT_MEDIUM));
128 			inMemoryResolver.setContext(contextPipe);
129 			Node doc = new XMLCalabashInputValue(sourcePipe).ensureSingleItem().asNodeIterator().next();
130 			if (!(doc instanceof Document))
131 				throw new IllegalArgumentException();
132 			URI baseURI = new URI(doc.getBaseURI());
133 			Source sourceDocument = new DOMSource(doc, baseURI.toASCIIString());
134 			List<Source> stylesheets = new ArrayList<>(); {
135 				if (getOption(_include_user_agent_stylesheet, false))
136 					for (URL u : userAgentStylesheets.get(Collections.singleton("text/x-scss"),
137 					                                      Arrays.asList(getOption(_content_type, "").trim().split("\\s+")),
138 					                                      media))
139 						stylesheets.add(
140 							new SAXSource(new InputSource(u.toString())));
141 				String s = getOption(_user_stylesheet, "");
142 				if (s != null) {
143 					StringTokenizer t = new StringTokenizer(s);
144 					while (t.hasMoreTokens())
145 						stylesheets.add(
146 							new SAXSource(
147 								new InputSource(URLs.resolve(baseURI, URLs.asURI(t.nextToken())).toASCIIString())));
148 				}
149 			}
150 			for (SassVariable v : new CssAnalyzer(media, cssURIResolver, null)
151 			                          .analyze(stylesheets, sourceDocument)
152 			                          .getVariables()) {
153 				if (params.containsKey(v.getName()) && v.isDefault())
154 					continue;
155 				params.put(v.getName(), v.getValue());
156 			}
157 			XMLStreamWriter writer = new XMLCalabashOutputValue(resultPipe, runtime).asXMLStreamWriter();
158 			writer.writeStartDocument();
159 			writeStartElement(writer, c_param_set);
160 			for (String p : params.keySet()) {
161 				writeStartElement(writer, c_param);
162 				writeAttribute(writer, _name, p);
163 				writeAttribute(writer, _value, params.get(p));
164 				writer.writeEndElement();
165 			}
166 			writer.writeEndElement();
167 			writer.writeEndDocument();
168 		} catch (Throwable e) {
169 			throw XProcStep.raiseError(e, step);
170 		}
171 	}
172 
173 	@Component(
174 		name = "pxi:css-analyze",
175 		service = { XProcStepProvider.class },
176 		property = { "type:String={http://www.daisy.org/ns/pipeline/xproc/internal}css-analyze" }
177 	)
178 	public static class Provider implements XProcStepProvider {
179 
180 		private URIResolver resolver;
181 		private UserAgentStylesheetRegistry userAgentStylesheets;
182 
183 		@Override
184 		public XProcStep newStep(XProcRuntime runtime, XAtomicStep step, XProcMonitor monitor, Map<String,String> properties) {
185 			return new CssAnalyzeStep(runtime, step, resolver, userAgentStylesheets);
186 		}
187 
188 		@Reference(
189 			name = "URIResolver",
190 			unbind = "-",
191 			service = URIResolver.class,
192 			cardinality = ReferenceCardinality.MANDATORY,
193 			policy = ReferencePolicy.STATIC
194 		)
195 		public void setUriResolver(URIResolver resolver) {
196 			this.resolver = resolver;
197 		}
198 
199 		@Reference(
200 			name = "UserAgentStylesheetRegistry",
201 			unbind = "-",
202 			service = UserAgentStylesheetRegistry.class,
203 			cardinality = ReferenceCardinality.MANDATORY,
204 			policy = ReferencePolicy.STATIC
205 		)
206 		public void setUserAgentStylesheetRegistry(UserAgentStylesheetRegistry registry) {
207 			this.userAgentStylesheets = registry;
208 		}
209 	}
210 
211 	private static URIResolver simpleURIResolver = new URIResolver() {
212 			@Override
213 			public Source resolve(String href, String base) throws TransformerException {
214 				try {
215 					URI uri; {
216 						if (base != null)
217 							uri = new URI(base).resolve(new URI(href));
218 						else
219 							uri = new URI(href);
220 					}
221 					return new SAXSource(new InputSource(uri.toASCIIString()));
222 				} catch (URISyntaxException e) {
223 					throw new TransformerException(e);
224 				}
225 			}
226 		};
227 
228 	private static URIResolver fallback(URIResolver... resolvers) {
229 		return new URIResolver() {
230 			public Source resolve(String href, String base) throws javax.xml.transform.TransformerException {
231 				Iterator<URIResolver> iterator = forArray(resolvers);
232 				while (iterator.hasNext()) {
233 					Source source = iterator.next().resolve(href, base);
234 					if (source != null)
235 						return source; }
236 				return null;
237 			}
238 		};
239 	}
240 
241 	private static final Logger logger = LoggerFactory.getLogger(CssAnalyzeStep.class);
242 
243 }