1 | // Copyright (C) 2006 Google Inc. |
2 | // |
3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
4 | // you may not use this file except in compliance with the License. |
5 | // You may obtain a copy of the License at |
6 | // |
7 | // http://www.apache.org/licenses/LICENSE-2.0 |
8 | // |
9 | // Unless required by applicable law or agreed to in writing, software |
10 | // distributed under the License is distributed on an "AS IS" BASIS, |
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 | // See the License for the specific language governing permissions and |
13 | // limitations under the License. |
14 | |
15 | package com.google.caja.plugin; |
16 | |
17 | import com.google.caja.SomethingWidgyHappenedError; |
18 | import com.google.caja.lexer.CharProducer; |
19 | import com.google.caja.lexer.CssTokenType; |
20 | import com.google.caja.lexer.ExternalReference; |
21 | import com.google.caja.lexer.FetchedData; |
22 | import com.google.caja.lexer.HtmlLexer; |
23 | import com.google.caja.lexer.InputSource; |
24 | import com.google.caja.lexer.JsLexer; |
25 | import com.google.caja.lexer.JsTokenQueue; |
26 | import com.google.caja.lexer.ParseException; |
27 | import com.google.caja.lexer.TokenConsumer; |
28 | import com.google.caja.lexer.TokenQueue; |
29 | import com.google.caja.parser.ParseTreeNode; |
30 | import com.google.caja.parser.css.CssParser; |
31 | import com.google.caja.parser.html.Dom; |
32 | import com.google.caja.parser.html.DomParser; |
33 | import com.google.caja.parser.html.Nodes; |
34 | import com.google.caja.parser.js.CajoledModule; |
35 | import com.google.caja.parser.js.Parser; |
36 | import com.google.caja.plugin.UriFetcher.ChainingUriFetcher; |
37 | import com.google.caja.reporting.Message; |
38 | import com.google.caja.reporting.MessageContext; |
39 | import com.google.caja.reporting.MessageLevel; |
40 | import com.google.caja.reporting.MessagePart; |
41 | import com.google.caja.reporting.MessageQueue; |
42 | import com.google.caja.reporting.MessageType; |
43 | import com.google.caja.reporting.RenderContext; |
44 | import com.google.caja.reporting.SimpleMessageQueue; |
45 | import com.google.caja.reporting.BuildInfo; |
46 | import com.google.caja.util.Callback; |
47 | import com.google.caja.util.CapturingReader; |
48 | import com.google.caja.util.Charsets; |
49 | import com.google.caja.util.Maps; |
50 | import com.google.caja.render.Concatenator; |
51 | import com.google.caja.render.JsMinimalPrinter; |
52 | import com.google.caja.render.SourceSnippetRenderer; |
53 | |
54 | import java.io.File; |
55 | import java.io.FileOutputStream; |
56 | import java.io.IOException; |
57 | import java.io.InputStream; |
58 | import java.io.InputStreamReader; |
59 | import java.io.OutputStreamWriter; |
60 | import java.io.Writer; |
61 | import java.io.Reader; |
62 | import java.io.FileNotFoundException; |
63 | import java.io.FileInputStream; |
64 | import java.net.URI; |
65 | import java.net.URL; |
66 | import java.util.Collection; |
67 | import java.util.Map; |
68 | import java.util.Set; |
69 | |
70 | import org.w3c.dom.Node; |
71 | |
72 | /** |
73 | * An executable that invokes the {@link PluginCompiler}. |
74 | * |
75 | * @author mikesamuel@gmail.com |
76 | */ |
77 | public final class PluginCompilerMain { |
78 | private final MessageQueue mq; |
79 | private final MessageContext mc; |
80 | private final Map<InputSource, CapturingReader> originalInputs |
81 | = Maps.newHashMap(); |
82 | private final Config config = new Config( |
83 | getClass(), System.err, "Cajoles HTML, CSS, and JS files to JS."); |
84 | private final Callback<IOException> exHandler = new Callback<IOException>() { |
85 | public void handle(IOException ex) { |
86 | mq.addMessage( |
87 | MessageType.IO_ERROR, MessagePart.Factory.valueOf(ex.toString())); |
88 | } |
89 | }; |
90 | |
91 | private class CachingUriFetcher extends FileSystemUriFetcher { |
92 | public CachingUriFetcher(UriToFile u2f) { super(u2f); } |
93 | |
94 | @Override |
95 | protected Reader newReader(File f) throws FileNotFoundException { |
96 | return createReader(new InputSource(f), new FileInputStream(f)); |
97 | } |
98 | |
99 | @Override |
100 | protected InputStream newInputStream(File f) throws FileNotFoundException { |
101 | return new FileInputStream(f); |
102 | } |
103 | } |
104 | |
105 | private PluginCompilerMain() { |
106 | mq = new SimpleMessageQueue(); |
107 | mc = new MessageContext(); |
108 | } |
109 | |
110 | private int run(String[] argv) { |
111 | if (!config.processArguments(argv)) { |
112 | return -1; |
113 | } |
114 | |
115 | boolean success = false; |
116 | MessageContext mc = null; |
117 | CajoledModule compiledJsOutput = null; |
118 | Node compiledDomOutput = null; |
119 | String compiledHtmlOutput = null; |
120 | File fileLimitAncestor = config.getFetcherBase(); |
121 | |
122 | File jsOutputDest = config.getOutputJsFile(); |
123 | File htmlOutputDest = config.getOutputHtmlFile(); |
124 | |
125 | try { |
126 | UriFetcher fetcher; |
127 | UriPolicy policy; |
128 | try { |
129 | if (fileLimitAncestor != null) { |
130 | UriToFile u2f = new UriToFile(fileLimitAncestor); |
131 | fetcher = ChainingUriFetcher.make( |
132 | new DataUriFetcher(), |
133 | new CachingUriFetcher(u2f)); |
134 | policy = new FileSystemUriPolicy(u2f); |
135 | } else { |
136 | fetcher = new DataUriFetcher(); |
137 | policy = UriPolicy.DENY_ALL; |
138 | } |
139 | } catch (IOException e) { // Could not resolve file name |
140 | fetcher = new DataUriFetcher(); |
141 | policy = UriPolicy.DENY_ALL; |
142 | } |
143 | final Set<String> lUrls = config.getLinkableUris(); |
144 | if (!lUrls.isEmpty()) { |
145 | final UriPolicy prePolicy = policy; |
146 | policy = new UriPolicy() { |
147 | public String rewriteUri( |
148 | ExternalReference u, UriEffect effect, |
149 | LoaderType loader, Map<String, ?> hints) { |
150 | String uri = u.getUri().toString(); |
151 | if (lUrls.contains(uri)) { return uri; } |
152 | return prePolicy.rewriteUri(u, effect, loader, hints); |
153 | } |
154 | }; |
155 | } |
156 | final Set<String> fUrls = config.getFetchableUris(); |
157 | if (!fUrls.isEmpty()) { |
158 | fetcher = ChainingUriFetcher.make( |
159 | fetcher, |
160 | new UriFetcher() { |
161 | public FetchedData fetch(ExternalReference ref, String mimeType) |
162 | throws UriFetchException { |
163 | String uri = ref.getUri().toString(); |
164 | if (!fUrls.contains(uri)) { |
165 | throw new UriFetchException(ref, mimeType); |
166 | } |
167 | try { |
168 | return FetchedData.fromConnection( |
169 | new URL(uri).openConnection()); |
170 | } catch (IOException ex) { |
171 | throw new UriFetchException(ref, mimeType, ex); |
172 | } |
173 | } |
174 | }); |
175 | } |
176 | |
177 | PluginMeta meta = new PluginMeta(fetcher, policy); |
178 | meta.setIdClass(config.getIdClass()); |
179 | meta.setEnableES53(config.getES53()); |
180 | PluginCompiler compiler = new PluginCompiler( |
181 | BuildInfo.getInstance(), meta, mq); |
182 | compiler.setPreconditions( |
183 | config.preconditions(compiler.getPreconditions())); |
184 | compiler.setGoals(config.goals(compiler.getGoals())); |
185 | |
186 | mc = compiler.getMessageContext(); |
187 | compiler.setCssSchema(config.getCssSchema(mq)); |
188 | compiler.setHtmlSchema(config.getHtmlSchema(mq)); |
189 | |
190 | success = parseInputs(config.getInputUris(), compiler) && compiler.run(); |
191 | if (success) { |
192 | compiledJsOutput = compiler.getJavascript(); |
193 | compiledDomOutput = compiler.getStaticHtml(); |
194 | compiledHtmlOutput = compiledDomOutput != null ? |
195 | Nodes.render(compiledDomOutput) : ""; |
196 | } |
197 | } finally { |
198 | if (mc == null) { mc = new MessageContext(); } |
199 | MessageLevel maxMessageLevel = dumpMessages(mq, mc, System.err); |
200 | success &= MessageLevel.ERROR.compareTo(maxMessageLevel) > 0; |
201 | } |
202 | |
203 | if (success) { |
204 | if (jsOutputDest != null) { |
205 | writeFile(jsOutputDest, compiledJsOutput); |
206 | } else { |
207 | StringBuilder compiledJsOutputBuf = new StringBuilder(); |
208 | compiledJsOutputBuf.append("<script>"); |
209 | try { |
210 | writeFile(compiledJsOutputBuf, compiledJsOutput); |
211 | } catch (IOException ex) { |
212 | throw new SomethingWidgyHappenedError(ex); |
213 | } |
214 | compiledJsOutputBuf.append("</script>"); |
215 | compiledHtmlOutput += compiledJsOutputBuf; |
216 | } |
217 | if (htmlOutputDest != null) { |
218 | writeFile(htmlOutputDest, compiledHtmlOutput); |
219 | } |
220 | } else { |
221 | // Make sure there is no previous output file from a failed run. |
222 | if (jsOutputDest != null) { jsOutputDest.delete(); } |
223 | if (htmlOutputDest != null) { htmlOutputDest.delete(); } |
224 | // If it wasn't there in the first place, or is not writable, that's OK, |
225 | // so ignore the return value. |
226 | } |
227 | |
228 | return success ? 0 : -1; |
229 | } |
230 | |
231 | private boolean parseInputs(Collection<URI> inputs, PluginCompiler pluginc) { |
232 | boolean parsePassed = true; |
233 | for (URI input : inputs) { |
234 | try { |
235 | ParseTreeNode parseTree = parseInput(input); |
236 | if (null != parseTree) { pluginc.addInput(parseTree, input); } |
237 | } catch (ParseException ex) { |
238 | ex.toMessageQueue(mq); |
239 | parsePassed = false; |
240 | } catch (IOException ex) { |
241 | mq.addMessage(MessageType.IO_ERROR, |
242 | MessagePart.Factory.valueOf(ex.toString())); |
243 | parsePassed = false; |
244 | } |
245 | } |
246 | return parsePassed; |
247 | } |
248 | |
249 | /** Parse one input from a URI. */ |
250 | private ParseTreeNode parseInput(URI input) |
251 | throws IOException, ParseException { |
252 | InputSource is = new InputSource(input); |
253 | mc.addInputSource(is); |
254 | |
255 | CharProducer cp = CharProducer.Factory.create( |
256 | createReader(is, input.toURL().openStream()), is); |
257 | return parseInput(is, cp, mq); |
258 | } |
259 | |
260 | /** Classify an input by extension and use the appropriate parser. */ |
261 | static ParseTreeNode parseInput( |
262 | InputSource is, CharProducer cp, MessageQueue mq) |
263 | throws ParseException { |
264 | |
265 | String path = is.getUri().getPath(); |
266 | |
267 | ParseTreeNode input; |
268 | if (path.endsWith(".js")) { |
269 | JsLexer lexer = new JsLexer(cp); |
270 | JsTokenQueue tq = new JsTokenQueue(lexer, is); |
271 | if (tq.isEmpty()) { return null; } |
272 | Parser p = new Parser(tq, mq); |
273 | input = p.parse(); |
274 | tq.expectEmpty(); |
275 | } else if (path.endsWith(".css")) { |
276 | TokenQueue<CssTokenType> tq = CssParser.makeTokenQueue(cp, mq, false); |
277 | if (tq.isEmpty()) { return null; } |
278 | |
279 | CssParser p = new CssParser(tq, mq, MessageLevel.WARNING); |
280 | input = p.parseStyleSheet(); |
281 | tq.expectEmpty(); |
282 | } else if (path.endsWith(".html") || path.endsWith(".xhtml") |
283 | || (!cp.isEmpty() && cp.getBuffer()[cp.getOffset()] == '<')) { |
284 | DomParser p = new DomParser(new HtmlLexer(cp), false, is, mq); |
285 | if (p.getTokenQueue().isEmpty()) { return null; } |
286 | input = new Dom(p.parseFragment()); |
287 | p.getTokenQueue().expectEmpty(); |
288 | } else { |
289 | throw new SomethingWidgyHappenedError("Can't classify input " + is); |
290 | } |
291 | return input; |
292 | } |
293 | |
294 | /** Write the given HTML to the given file. */ |
295 | private void writeFile(File outputHtmlFile, String compiledHtmlOutput) { |
296 | try { |
297 | OutputStreamWriter out = new OutputStreamWriter( |
298 | new FileOutputStream(outputHtmlFile), Charsets.UTF_8); |
299 | try { |
300 | out.append(compiledHtmlOutput); |
301 | } finally { |
302 | try { out.close(); } catch (IOException e) { /* close quietly */ } |
303 | } |
304 | } catch (IOException ex) { |
305 | exHandler.handle(ex); |
306 | } |
307 | } |
308 | |
309 | /** Write the given parse tree to the given file. */ |
310 | private void writeFile(File f, CajoledModule module) { |
311 | if (module == null) { return; } |
312 | |
313 | Writer out = null; |
314 | |
315 | try { |
316 | out = new OutputStreamWriter(new FileOutputStream(f), Charsets.UTF_8); |
317 | writeFile(out, module); |
318 | } catch (IOException ex) { |
319 | ex.printStackTrace(); |
320 | } finally { |
321 | if (out != null) { |
322 | try { |
323 | out.close(); |
324 | } catch (IOException e) { |
325 | /* no zero-argument ctor */ |
326 | } |
327 | } |
328 | } |
329 | } |
330 | |
331 | private void writeFile(Appendable out, CajoledModule module) |
332 | throws IOException { |
333 | if (config.renderer() == Config.SourceRenderMode.DEBUGGER) { |
334 | // Debugger rendering is weird enough to warrant its own method |
335 | writeFileWithDebug(out, module); |
336 | } else { |
337 | writeFileNonDebug(out, module); |
338 | } |
339 | } |
340 | |
341 | private void writeFileNonDebug(Appendable out, CajoledModule module) |
342 | throws IOException { |
343 | TokenConsumer tc; |
344 | switch (config.renderer()) { |
345 | case PRETTY: |
346 | tc = module.makeRenderer(out, exHandler); |
347 | break; |
348 | case MINIFY: |
349 | tc = new JsMinimalPrinter(new Concatenator(out, exHandler)); |
350 | break; |
351 | case SIDEBYSIDE: |
352 | tc = new SourceSnippetRenderer( |
353 | buildOriginalInputCharSequences(), mc, |
354 | makeRenderContext(new Concatenator(out, exHandler))); |
355 | break; |
356 | default: |
357 | throw new SomethingWidgyHappenedError( |
358 | "Unrecognized renderer: " + config.renderer()); |
359 | } |
360 | RenderContext rc = makeRenderContext(tc); |
361 | module.render(rc); |
362 | tc.noMoreTokens(); |
363 | out.append('\n'); |
364 | } |
365 | |
366 | private void writeFileWithDebug(Appendable out, CajoledModule module) |
367 | throws IOException { |
368 | module.renderWithDebugSymbols( |
369 | buildOriginalInputCharSequences(), |
370 | makeRenderContext(new Concatenator(out, exHandler))); |
371 | } |
372 | |
373 | private static RenderContext makeRenderContext(TokenConsumer tc) { |
374 | return new RenderContext(tc).withAsciiOnly(true).withEmbeddable(true); |
375 | } |
376 | |
377 | /** |
378 | * Dumps messages to the given output stream, returning the highest message |
379 | * level seen. |
380 | */ |
381 | static MessageLevel dumpMessages( |
382 | MessageQueue mq, MessageContext mc, Appendable out) { |
383 | MessageLevel maxLevel = MessageLevel.values()[0]; |
384 | for (Message m : mq.getMessages()) { |
385 | MessageLevel level = m.getMessageLevel(); |
386 | if (maxLevel.compareTo(level) < 0) { maxLevel = level; } |
387 | } |
388 | MessageLevel ignoreLevel = null; |
389 | if (maxLevel.compareTo(MessageLevel.LINT) < 0) { |
390 | // If there's only checkpoints, be quiet. |
391 | ignoreLevel = MessageLevel.LOG; |
392 | } |
393 | try { |
394 | for (Message m : mq.getMessages()) { |
395 | MessageLevel level = m.getMessageLevel(); |
396 | if (ignoreLevel != null && level.compareTo(ignoreLevel) <= 0) { |
397 | continue; |
398 | } |
399 | String levelName = level.name(); |
400 | out.append(levelName); |
401 | if (levelName.length() < 7) { |
402 | out.append(" ".substring(levelName.length())); |
403 | } |
404 | out.append(": "); |
405 | m.format(mc, out); |
406 | out.append("\n"); |
407 | |
408 | if (maxLevel.compareTo(level) < 0) { maxLevel = level; } |
409 | } |
410 | } catch (IOException ex) { |
411 | ex.printStackTrace(); |
412 | } |
413 | return maxLevel; |
414 | } |
415 | |
416 | private Reader createReader(InputSource is, InputStream stream) { |
417 | InputStreamReader isr = new InputStreamReader(stream, Charsets.UTF_8); |
418 | |
419 | if (config.renderer() == Config.SourceRenderMode.SIDEBYSIDE || |
420 | config.renderer() == Config.SourceRenderMode.DEBUGGER) { |
421 | CapturingReader cr = new CapturingReader(isr); |
422 | originalInputs.put(is, cr); |
423 | return cr; |
424 | } else { |
425 | return isr; |
426 | } |
427 | } |
428 | |
429 | private Map<InputSource, CharSequence> buildOriginalInputCharSequences() |
430 | throws IOException { |
431 | Map<InputSource, CharSequence> results = Maps.newHashMap(); |
432 | for (InputSource is : originalInputs.keySet()) { |
433 | results.put(is, originalInputs.get(is).getCapture()); |
434 | } |
435 | return results; |
436 | } |
437 | |
438 | public static void main(String[] args) { |
439 | int exitCode; |
440 | try { |
441 | PluginCompilerMain main = new PluginCompilerMain(); |
442 | exitCode = main.run(args); |
443 | } catch (Exception ex) { |
444 | ex.printStackTrace(); |
445 | exitCode = -1; |
446 | } |
447 | try { |
448 | System.exit(exitCode); |
449 | } catch (SecurityException ex) { |
450 | // This method may be invoked under a SecurityManager, e.g. by Ant, |
451 | // so just suppress the security exception and return normally. |
452 | } |
453 | } |
454 | } |