View Javadoc
1   /*
2    * LessCSS Compiler
3    * http://lesscss-compiler.projects.gabrys.biz/
4    *
5    * Copyright (c) 2015 Adam Gabryƛ
6    *
7    * This file is licensed under the BSD 3-Clause (the "License").
8    * You may not use this file except in compliance with the License.
9    * You may obtain:
10   *  - a copy of the License at project page
11   *  - a template of the License at https://opensource.org/licenses/BSD-3-Clause
12   */
13  package biz.gabrys.lesscss.compiler;
14  
15  import java.io.ByteArrayOutputStream;
16  import java.io.File;
17  import java.io.InputStream;
18  import java.io.InputStreamReader;
19  import java.io.PrintStream;
20  import java.io.SequenceInputStream;
21  import java.net.URL;
22  import java.util.ArrayList;
23  import java.util.Collection;
24  import java.util.Collections;
25  import java.util.regex.Matcher;
26  import java.util.regex.Pattern;
27  
28  import org.mozilla.javascript.Context;
29  import org.mozilla.javascript.Function;
30  import org.mozilla.javascript.JavaScriptException;
31  import org.mozilla.javascript.Scriptable;
32  import org.mozilla.javascript.ScriptableObject;
33  import org.mozilla.javascript.tools.shell.Global;
34  
35  /**
36   * <p>
37   * Default implementation of the {@link LessCompiler}. Compiler is compatible with version
38   * <a href="https://github.com/less/less.js/releases/tag/v1.7.5">1.7.5</a> with certain restrictions:
39   * </p>
40   * <ul>
41   * <li>cannot fetch sources from Internet, e.g. {@code @import (less) "http://example.org/style.less"}</li>
42   * </ul>
43   * <p>
44   * The library is based on the official <a href="http://lesscss.org/">Less</a> JavaScript compiler adapted to the
45   * <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Rhino">Rhino</a> engine.
46   * </p>
47   * <p>
48   * The idea for this class implementation was based on the
49   * <a href="https://github.com/marceloverdijk/lesscss-java">lesscss-java</a> library by
50   * <a href="https://github.com/marceloverdijk">Marcel Overdijk</a>.
51   * </p>
52   * <p>
53   * How to use:
54   * </p>
55   * 
56   * <pre>
57   * // create compiler &amp; source file
58   * LessCompiler compiler = new LessCompilerImpl();
59   * File source = new File("/less/file.less");
60   * 
61   * // compile file with default options
62   * String cssCode = compiler.compile(source);
63   * 
64   * // set custom option: minify CSS code
65   * CompilerOptions options = new CompilerOptionsBuilder().setMinified(true).create();
66   * String cssMinifiedCode = compiler.compile(source, options);
67   * </pre>
68   * 
69   * @since 1.0
70   */
71  public class LessCompilerImpl implements LessCompiler {
72  
73      private static final Pattern IMPORT_ERROR_PATTERN = Pattern.compile("^FileError:\\s+'(.+)'\\s+wasn't\\s+found\\s+(?s).*");
74      private static final int IMPORT_ERROR_FILE_NAME_GROUP_INDEX = 1;
75  
76      private static final String CHARSET = "UTF-8";
77  
78      private final Object mutex = new Object();
79  
80      private Scriptable scope;
81      private ByteArrayOutputStream console;
82      private Function compiler;
83  
84      /**
85       * Constructs a new instance.
86       * @since 1.0
87       */
88      public LessCompilerImpl() {
89          // do nothing
90      }
91  
92      /**
93       * {@inheritDoc}
94       * @throws InitializationException if an error occurred during compiler initialization.
95       * @throws ResolveImportException if an error occurred during resolve imports.
96       * @throws SyntaxException if a syntax error occurred during source file compilation.
97       * @since 1.0
98       */
99      public String compile(final File source) throws CompilerException {
100         return compile(source, new CompilerOptions());
101     }
102 
103     /**
104      * {@inheritDoc}
105      * @throws InitializationException if an error occurred during compiler initialization.
106      * @throws ResolveImportException if an error occurred during resolve imports.
107      * @throws SyntaxException if a syntax error occurred during source file compilation.
108      * @since 1.0
109      */
110     public String compile(final File input, final CompilerOptions options) throws CompilerException {
111         synchronized (mutex) {
112             if (compiler == null) {
113                 initialize();
114             }
115             try {
116                 final Context context = Context.enter();
117 
118                 final ScriptableObject compileScope = (ScriptableObject) context.newObject(scope);
119                 compileScope.setParentScope(null);
120                 compileScope.setPrototype(scope);
121 
122                 final Scriptable arguments = context.newArray(compileScope, prepareCompilerArguments(input, options));
123                 compileScope.defineProperty("arguments", arguments, ScriptableObject.DONTENUM);
124 
125                 compiler.call(context, compileScope, null, new Object[0]);
126                 return console.toString(CHARSET);
127 
128             } catch (final JavaScriptException e) {
129                 throw parseException(e);
130             } catch (final Exception e) {
131                 throw new CompilerException(e);
132             } finally {
133                 console.reset();
134                 Context.exit();
135             }
136         }
137     }
138 
139     private void initialize() throws InitializationException {
140         InputStreamReader reader = null;
141         try {
142             final Context context = Context.enter();
143             context.setLanguageVersion(Context.VERSION_1_8);
144 
145             final Global global = new Global();
146             global.init(context);
147             scope = context.initStandardObjects(global);
148 
149             console = new ByteArrayOutputStream();
150             global.setOut(new PrintStream(console, false, CHARSET));
151 
152             final URL lessFile = LessCompilerImpl.class.getResource("/less/less-rhino-1.7.5.js");
153             final URL lesscFile = LessCompilerImpl.class.getResource("/less/lessc-rhino-1.7.5.js");
154 
155             final Collection<InputStream> streams = new ArrayList<InputStream>();
156             streams.add(lessFile.openConnection().getInputStream());
157             streams.add(lesscFile.openConnection().getInputStream());
158 
159             reader = new InputStreamReader(new SequenceInputStream(Collections.enumeration(streams)), CHARSET);
160             compiler = (Function) context.compileReader(reader, lessFile.toString(), 1, null);
161 
162         } catch (final Exception e) {
163             throw new InitializationException("Failed to initialize Less compiler", e);
164 
165         } finally {
166             IOUtils.closeQuietly(reader);
167             Context.exit();
168         }
169     }
170 
171     private static Object[] prepareCompilerArguments(final File sourceFile, final CompilerOptions options) {
172         final Collection<Object> arguments = new ArrayList<Object>();
173         arguments.addAll(options.getArguments());
174         arguments.add(sourceFile.getAbsolutePath());
175         return arguments.toArray();
176     }
177 
178     private static CompilerException parseException(final JavaScriptException exception) {
179         final Scriptable value = (Scriptable) exception.getValue();
180         if (value != null && ScriptableObject.hasProperty(value, "message")) {
181             final String message = ScriptableObject.getProperty(value, "message").toString();
182             final Matcher matcher = IMPORT_ERROR_PATTERN.matcher(message);
183             if (matcher.find()) {
184                 return new ResolveImportException(message, matcher.group(IMPORT_ERROR_FILE_NAME_GROUP_INDEX), exception);
185             }
186             return new SyntaxException(message, exception);
187         }
188         return new SyntaxException(exception);
189     }
190 }