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   * @since 1.0
53   */
54  public class LessCompilerImpl implements LessCompiler {
55  
56      private static final Pattern IMPORT_ERROR_PATTERN = Pattern.compile("^FileError:\\s+'(.+)'\\s+wasn't\\s+found\\s+(?s).*");
57      private static final int IMPORT_ERROR_FILE_NAME_GROUP_INDEX = 1;
58  
59      private static final String CHARSET = "UTF-8";
60  
61      private final Object mutex = new Object();
62  
63      private Scriptable scope;
64      private ByteArrayOutputStream console;
65      private Function compiler;
66  
67      /**
68       * Constructs a new instance.
69       * @since 1.0
70       */
71      public LessCompilerImpl() {
72          // do nothing
73      }
74  
75      /**
76       * {@inheritDoc}
77       * @throws InitializationException if an error occurred during compiler initialization.
78       * @throws ResolveImportException if an error occurred during resolve imports.
79       * @throws SyntaxException if a syntax error occurred during source file compilation.
80       * @since 1.0
81       */
82      public String compile(final File source) throws CompilerException {
83          return compile(source, new CompilerOptions(Collections.emptyList()));
84      }
85  
86      /**
87       * {@inheritDoc}
88       * @throws InitializationException if an error occurred during compiler initialization.
89       * @throws ResolveImportException if an error occurred during resolve imports.
90       * @throws SyntaxException if a syntax error occurred during source file compilation.
91       * @since 1.0
92       */
93      public String compile(final File input, final CompilerOptions options) throws CompilerException {
94          synchronized (mutex) {
95              if (compiler == null) {
96                  initialize();
97              }
98              try {
99                  final Context context = Context.enter();
100 
101                 final ScriptableObject compileScope = (ScriptableObject) context.newObject(scope);
102                 compileScope.setParentScope(null);
103                 compileScope.setPrototype(scope);
104 
105                 final Scriptable arguments = context.newArray(compileScope, prepareCompilerArguments(input, options));
106                 compileScope.defineProperty("arguments", arguments, ScriptableObject.DONTENUM);
107 
108                 compiler.call(context, compileScope, null, new Object[0]);
109                 return console.toString(CHARSET);
110 
111             } catch (final JavaScriptException e) {
112                 throw parseException(e);
113             } catch (final Exception e) {
114                 throw new CompilerException(e);
115             } finally {
116                 console.reset();
117                 Context.exit();
118             }
119         }
120     }
121 
122     private void initialize() throws InitializationException {
123         try {
124             final Context context = Context.enter();
125             context.setLanguageVersion(Context.VERSION_1_8);
126 
127             final Global global = new Global();
128             global.init(context);
129             scope = context.initStandardObjects(global);
130 
131             console = new ByteArrayOutputStream();
132             global.setOut(new PrintStream(console, false, CHARSET));
133 
134             final URL lessFile = LessCompilerImpl.class.getResource("/less/less-rhino-1.7.5.js");
135             final URL lesscFile = LessCompilerImpl.class.getResource("/less/lessc-rhino-1.7.5.js");
136 
137             final Collection<InputStream> streams = new ArrayList<InputStream>();
138             streams.add(lessFile.openConnection().getInputStream());
139             streams.add(lesscFile.openConnection().getInputStream());
140 
141             final InputStreamReader reader = new InputStreamReader(new SequenceInputStream(Collections.enumeration(streams)), CHARSET);
142             compiler = (Function) context.compileReader(reader, lessFile.toString(), 1, null);
143 
144         } catch (final Exception e) {
145             throw new InitializationException("Failed to initialize Less compiler", e);
146 
147         } finally {
148             Context.exit();
149         }
150     }
151 
152     private static Object[] prepareCompilerArguments(final File sourceFile, final CompilerOptions options) {
153         final Collection<Object> arguments = new ArrayList<Object>();
154         arguments.addAll(options.getArguments());
155         arguments.add(sourceFile.getAbsolutePath());
156         return arguments.toArray();
157     }
158 
159     private CompilerException parseException(final JavaScriptException e) {
160         final Scriptable value = (Scriptable) e.getValue();
161         if (value != null && ScriptableObject.hasProperty(value, "message")) {
162             final String message = ScriptableObject.getProperty(value, "message").toString();
163             final Matcher matcher = IMPORT_ERROR_PATTERN.matcher(message);
164             if (matcher.find()) {
165                 return new ResolveImportException(message, matcher.group(IMPORT_ERROR_FILE_NAME_GROUP_INDEX), e);
166             }
167             return new SyntaxException(message, e);
168         }
169         return new SyntaxException(e);
170     }
171 }