Java tutorial
/* Copyright 2011-2012 The Apache Software Foundation. * * Licensed 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 cn.dreampie; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintStream; import java.io.SequenceInputStream; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.List; import cn.dreampie.logging.LessLogger; import cn.dreampie.logging.LessLoggerFactory; import cn.dreampie.resource.LessSource; import org.apache.commons.io.FileUtils; import org.mozilla.javascript.Context; import org.mozilla.javascript.Function; import org.mozilla.javascript.JavaScriptException; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; import org.mozilla.javascript.tools.shell.Global; /** * The LESS compiler to compile LESS sources to CSS stylesheets. * <p> * The compiler uses Rhino (JavaScript implementation written in Java), Envjs * (simulated browser environment written in JavaScript), and the official LESS * JavaScript compiler.<br /> * Note that the compiler is not a Java implementation of LESS itself, but rather * integrates the LESS JavaScript compiler within a Java/JavaScript browser * environment provided by Rhino and Envjs. * </p> * <p> * The compiler comes bundled with the Envjs and LESS JavaScript, so there is * no need to include them yourself. But if needed they can be overridden. * </p> * <h4>Basic code example:</h4> * <pre> * LessCompiler lessCompiler = new LessCompiler(); * String css = lessCompiler.compile("@color: #4D926F; #header { color: @color; }"); * </pre> * * @author Marcel Overdijk * @see <a href="http://lesscss.org/">LESS - The Dynamic Stylesheet language</a> * @see <a href="http://www.mozilla.org/rhino/">Rhino - JavaScript for Java</a> * @see <a href="http://www.envjs.com/">Envjs - Bringing the Browser</a> */ public class LessCompiler { private static final LessLogger logger = LessLoggerFactory.getLogger(LessCompiler.class); private URL lessJs = LessCompiler.class.getClassLoader().getResource("lib/less-rhino-1.7.0.js"); private URL lesscJs = LessCompiler.class.getClassLoader().getResource("lib/lessc-rhino-1.7.0.js"); private List<URL> customJs = Collections.emptyList(); private List<String> options = Collections.emptyList(); private Boolean compress = null; private String encoding = null; private Scriptable scope; private ByteArrayOutputStream out; private Function compiler; /** * Constructs a new <code>LessCompiler</code>. */ public LessCompiler() { } /** * Constructs a new <code>LessCompiler</code>. */ public LessCompiler(List<String> options) { this.options = new ArrayList<String>(options); } public List<String> getOptions() { return Collections.unmodifiableList(options); } public void setOptions(List<String> options) { if (scope != null) { throw new IllegalStateException("This method can only be called before init()"); } this.options = new ArrayList<String>(options); } /** * Returns the Envjs JavaScript file used by the compiler. * * @return The Envjs JavaScript file used by the compiler. */ public URL getEnvJs() { throw new IllegalArgumentException( "EnvJs is no longer supported. You don't need this if you use a less-rhino-<version>.js build like the default."); } /** * Sets the Envjs JavaScript file used by the compiler. * Must be set before {@link #init()} is called. * * @param envJs The Envjs JavaScript file used by the compiler. */ public synchronized void setEnvJs(URL envJs) { throw new IllegalArgumentException( "EnvJs is no longer supported. You don't need this if you use a less-rhino-<version>.js build like the default."); } /** * Returns the LESS JavaScript file used by the compiler. * COMPILE_STRING * * @return The LESS JavaScript file used by the compiler. */ public URL getLessJs() { return lessJs; } /** * Sets the LESS JavaScript file used by the compiler. * Must be set before {@link #init()} is called. * * @param lessJs LESS JavaScript file used by the compiler. */ public synchronized void setLessJs(URL lessJs) { if (scope != null) { throw new IllegalStateException("This method can only be called before init()"); } this.lessJs = lessJs; } /** * Returns the LESSC JavaScript file used by the compiler. * COMPILE_STRING * * @return The LESSC JavaScript file used by the compiler. */ public URL getLesscJs() { return lesscJs; } /** * Sets the LESSC JavaScript file used by the compiler. * Must be set before {@link #init()} is called. * * @param lesscJs LESSC JavaScript file used by the compiler. */ public synchronized void setLesscJs(URL lesscJs) { if (scope != null) { throw new IllegalStateException("This method can only be called before init()"); } this.lesscJs = lesscJs; } /** * Returns the custom JavaScript files used by the compiler. * * @return The custom JavaScript files used by the compiler. */ public List<URL> getCustomJs() { return Collections.unmodifiableList(customJs); } /** * Sets a single custom JavaScript file used by the compiler. * Must be set before {@link #init()} is called. * * @param customJs A single custom JavaScript file used by the compiler. */ public synchronized void setCustomJs(URL customJs) { if (scope != null) { throw new IllegalStateException("This method can only be called before init()"); } this.customJs = Collections.singletonList(customJs); } /** * Sets the custom JavaScript files used by the compiler. * Must be set before {@link #init()} is called. * * @param customJs The custom JavaScript files used by the compiler. */ public synchronized void setCustomJs(List<URL> customJs) { if (scope != null) { throw new IllegalStateException("This method can only be called before init()"); } // copy the list so there's no way for anyone else who holds a reference to the list to modify it this.customJs = new ArrayList<URL>(customJs); } /** * Returns whether the compiler will compress the CSS. * * @return Whether the compiler will compress the CSS. */ public boolean isCompress() { return (compress != null && compress.booleanValue()) || options.contains("compress") || options.contains("x"); } /** * Sets the compiler to compress the CSS. * Must be set before {@link #init()} is called. * * @param compress If <code>true</code>, sets the compiler to compress the CSS. */ public synchronized void setCompress(boolean compress) { if (scope != null) { throw new IllegalStateException("This method can only be called before init()"); } this.compress = compress; } /** * Returns the character encoding used by the compiler when writing the output <code>File</code>. * * @return The character encoding used by the compiler when writing the output <code>File</code>. */ public String getEncoding() { return encoding; } /** * Sets the character encoding used by the compiler when writing the output <code>File</code>. * If not set the platform default will be used. * Must be set before {@link #init()} is called. * * @param encoding character encoding used by the compiler when writing the output <code>File</code>. */ public synchronized void setEncoding(String encoding) { if (scope != null) { throw new IllegalStateException("This method can only be called before init()"); } this.encoding = encoding; } /** * Initializes this <code>LessCompiler</code>. * <p> * It is not needed to call this method manually, as it is called implicitly by the compile methods if needed. * </p> */ public synchronized void init() { long start = System.currentTimeMillis(); try { Context cx = Context.enter(); //cx.setOptimizationLevel(-1); cx.setLanguageVersion(Context.VERSION_1_7); Global global = new Global(); global.init(cx); scope = cx.initStandardObjects(global); scope.put("logger", scope, Context.toObject(logger, scope)); out = new ByteArrayOutputStream(); global.setOut(new PrintStream(out)); // Combine all of the streams (less, custom, lessc) into one big stream List<InputStream> streams = new ArrayList<InputStream>(); // less should be first streams.add(lessJs.openConnection().getInputStream()); // then the custom js so it has a chance to add any hooks for (URL url : customJs) { streams.add(url.openConnection().getInputStream()); } // then the lessc so we can do the compile streams.add(lesscJs.openConnection().getInputStream()); InputStreamReader reader = new InputStreamReader( new SequenceInputStream(Collections.enumeration(streams))); // Load the streams into a function we can run compiler = (Function) cx.compileReader(reader, lessJs.toString(), 1, null); } catch (Exception e) { String message = "Failed to initialize LESS compiler."; logger.error(message, e); throw new IllegalStateException(message, e); } finally { Context.exit(); } if (logger.isDebugEnabled()) { logger.debug("Finished initialization of LESS compiler in %,d ms.%n", System.currentTimeMillis() - start); } } /** * Compiles the LESS input <code>String</code> to CSS. * * @param input The LESS input <code>String</code> to compile. * @return The CSS. */ public String compile(String input) throws LessException { return compile(input, "<inline>"); } /** * Compiles the LESS input <code>String</code> to CSS, but specifies the source name <code>String</code>. * * @param input The LESS input <code>String</code> to compile * @param name The source's name <code>String</code> to provide better error messages. * @return the CSS. * @throws LessException any error encountered by the compiler */ public String compile(String input, String name) throws LessException { File tempFile = null; try { tempFile = File.createTempFile("tmp", "less.tmp"); FileUtils.writeStringToFile(tempFile, input, this.encoding); return compile(tempFile, name); } catch (IOException e) { throw new LessException(e); } finally { tempFile.delete(); } } /** * Compiles the LESS input <code>String</code> to CSS, but specifies the source name <code>String</code>. The entire * method is synchronized so that two threads don't read the output at the same time. * * @param input The LESS input <code>String</code> to compile * @param name The source's name <code>String</code> to provide better error messages. * @return the CSS. * @throws LessException any error encountered by the compiler */ public synchronized String compile(File input, String name) throws LessException { if (scope == null) { init(); } long start = System.currentTimeMillis(); try { Context cx = Context.enter(); // The scope for compiling <input> ScriptableObject compileScope = (ScriptableObject) cx.newObject(scope); // give it a reference to the parent scope compileScope.setPrototype(scope); compileScope.setParentScope(null); // Copy the default options List<String> options = new ArrayList<String>(this.options); // Set up the arguments for <input> options.add(input.getAbsolutePath()); // Add compress if the value is set for backward compatibility if (this.compress != null && this.compress.booleanValue()) { options.add("-x"); } Scriptable argsObj = cx.newArray(compileScope, options.toArray(new Object[options.size()])); //Scriptable argsObj = cx.newArray(compileScope, new Object[] {"-ru", "c.less"}); compileScope.defineProperty("arguments", argsObj, ScriptableObject.DONTENUM); // invoke the compiler - we don't pass arguments here because its a script not a real function // and we don't care about the result because its written to the output stream (out) compiler.call(cx, compileScope, null, new Object[] {}); if (logger.isDebugEnabled()) { logger.debug("Finished compilation of LESS source in %,d ms.", System.currentTimeMillis() - start); } return this.encoding != null && !this.encoding.equals("") ? out.toString(encoding) : out.toString(); } catch (Exception e) { if (e instanceof JavaScriptException) { Scriptable value = (Scriptable) ((JavaScriptException) e).getValue(); if (value != null) { StringBuilder message = new StringBuilder(); if (ScriptableObject.hasProperty(value, "filename")) { message.append(ScriptableObject.getProperty(value, "filename").toString()); } if (ScriptableObject.hasProperty(value, "line")) { message.append("@("); message.append(ScriptableObject.getProperty(value, "line").toString()); message.append(","); message.append(ScriptableObject.getProperty(value, "column").toString()); message.append(")"); } if (ScriptableObject.hasProperty(value, "message")) { if (message.length() > 0) message.append(": "); message.append(ScriptableObject.getProperty(value, "message").toString()); } if (ScriptableObject.hasProperty(value, "extract")) { List<String> lines = (List<String>) ScriptableObject.getProperty(value, "extract"); for (String line : lines) { if (line != null) { message.append("\n"); message.append(line); } } } throw new LessException(message.toString()); } } throw new LessException(e.getMessage()); } finally { // reset our ouput stream so we don't copy data on the next invocation out.reset(); // we're done with this invocation Context.exit(); } } /** * Compiles the LESS input <code>File</code> to CSS. * * @param input The LESS input <code>File</code> to compile. * @return The CSS. * @throws IOException If the LESS file cannot be read. */ public String compile(File input) throws IOException, LessException { return compile(input, input.getName()); } /** * Compiles the LESS input <code>File</code> to CSS and writes it to the specified output <code>File</code>. * * @param input The LESS input <code>File</code> to compile. * @param output The output <code>File</code> to write the CSS to. * @throws IOException If the LESS file cannot be read or the output file cannot be written. */ public void compile(File input, File output) throws IOException, LessException { this.compile(input, output, true); } /** * Compiles the LESS input <code>File</code> to CSS and writes it to the specified output <code>File</code>. * * @param input The LESS input <code>File</code> to compile. * @param output The output <code>File</code> to write the CSS to. * @param force 'false' to only compile the LESS input file in case the LESS source has been modified (including imports) or the output file does not exists. * @throws IOException If the LESS file cannot be read or the output file cannot be written. */ public void compile(File input, File output, boolean force) throws IOException, LessException { if (force || !output.exists() || output.lastModified() < input.lastModified()) { String data = compile(input); FileUtils.writeStringToFile(output, data, encoding); } } public String compile(LessSource input) throws LessException { return compile(input.getNormalizedContent(), input.getName()); } /** * Compiles the input <code>LessSource</code> to CSS and writes it to the specified output <code>File</code>. * * @param input The input <code>LessSource</code> to compile. * @param output The output <code>File</code> to write the CSS to. * @throws IOException If the LESS file cannot be read or the output file cannot be written. */ public void compile(LessSource input, File output) throws IOException, LessException { compile(input, output, true); } /** * Compiles the input <code>LessSource</code> to CSS and writes it to the specified output <code>File</code>. * * @param input The input <code>LessSource</code> to compile. * @param output The output <code>File</code> to write the CSS to. * @param force 'false' to only compile the input <code>LessSource</code> in case the LESS source has been modified (including imports) or the output file does not exists. * @throws IOException If the LESS file cannot be read or the output file cannot be written. */ public void compile(LessSource input, File output, boolean force) throws IOException, LessException { if (force || !output.exists() || output.lastModified() < input.getLastModifiedIncludingImports()) { String data = compile(input); FileUtils.writeStringToFile(output, data, encoding); } } }