1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package nl.geodienstencentrum.maven.plugin.sass;
21
22 import java.io.File;
23 import java.io.FileOutputStream;
24 import java.io.IOException;
25 import java.net.URI;
26 import java.net.URISyntaxException;
27 import java.net.URL;
28 import java.util.ArrayList;
29 import java.util.Arrays;
30 import java.util.Collections;
31 import java.util.Enumeration;
32 import java.util.HashMap;
33 import java.util.Iterator;
34 import java.util.List;
35 import java.util.Map;
36 import java.util.Map.Entry;
37 import java.util.jar.JarEntry;
38 import java.util.jar.JarFile;
39
40 import org.apache.commons.io.FilenameUtils;
41 import org.apache.maven.model.FileSet;
42 import org.apache.maven.plugin.AbstractMojo;
43 import org.apache.maven.plugin.MojoExecutionException;
44 import org.apache.maven.plugin.MojoFailureException;
45 import org.apache.maven.plugin.logging.Log;
46 import org.apache.maven.plugins.annotations.Parameter;
47 import org.apache.maven.shared.utils.io.IOUtil;
48 import org.jruby.embed.LocalContextScope;
49 import org.jruby.embed.ScriptingContainer;
50
51 import com.google.common.collect.ImmutableList;
52 import com.google.common.collect.ImmutableMap;
53
54 import nl.geodienstencentrum.maven.plugin.sass.compiler.CompilerCallback;
55
56
57
58
59
60 public abstract class AbstractSassMojo extends AbstractMojo {
61
62 private static final String BOURBON_GEM_PATH = "core";
63 private static final String BOURBON_DEST_PATH = "bourbon";
64
65
66
67
68
69
70 @Parameter(defaultValue = "${project.build.directory}")
71 protected File buildDirectory;
72
73
74
75
76
77
78 @Parameter(defaultValue = "${project.build.directory}/${project.build.finalName}/css")
79 protected File destination;
80
81
82
83
84
85
86 @Parameter(defaultValue = "true")
87 protected boolean failOnError;
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107 @Parameter
108 private Map<String, String> sassOptions = new HashMap<String, String>(
109 ImmutableMap.of("unix_newlines", "true", "cache", "true",
110 "always_update", "true", "style", ":expanded"));
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138 @Parameter
139 private List<Resource> resources = Collections.emptyList();
140
141
142
143
144
145
146
147
148
149 @Parameter(defaultValue = "${project.build.directory}/rubygems")
150 private String[] gemPaths = new String[0];
151
152
153
154
155
156
157
158
159 @Parameter
160 private String[] gems = new String[0];
161
162
163
164
165
166
167 @Parameter(defaultValue = "false")
168 private boolean useBourbon;
169
170
171
172
173
174
175
176 @Parameter(defaultValue = "${basedir}/src/main/sass", property = "sassSourceDirectory")
177 private File sassSourceDirectory;
178
179
180
181
182
183
184
185
186 @Parameter
187 private String[] includes = new String[] { "**/*.scss" };
188
189
190
191
192
193
194
195 @Parameter
196 private String[] excludes;
197
198
199
200
201
202
203
204
205
206
207 @Parameter(defaultValue = "..")
208 private String relativeOutputDirectory;
209
210
211
212
213
214
215 @Parameter(defaultValue = "false")
216 private boolean skip;
217
218
219
220
221
222
223
224
225
226
227
228 protected void executeSassScript(final String sassScript)
229 throws MojoExecutionException, MojoFailureException {
230 if (this.skip) {
231 return;
232 }
233
234 final Log log = this.getLog();
235 log.debug("Execute Sass Ruby script:\n\n" + sassScript + "\n\n");
236
237 final ScriptingContainer scriptingContainer = new ScriptingContainer(LocalContextScope.SINGLETHREAD);
238 final CompilerCallbackler/CompilerCallback.html#CompilerCallback">CompilerCallback compilerCallback = new CompilerCallback(log);
239
240 scriptingContainer.setHomeDirectory("uri:classloader://META-INF/jruby.home");
241 scriptingContainer.put("$compiler_callback", compilerCallback);
242 Object o = scriptingContainer.runScriptlet(sassScript);
243
244 if (this.failOnError && compilerCallback.hadError()) {
245 throw new MojoFailureException(
246 "Sass compilation encountered errors (see above for details).");
247 }
248 log.debug("\n");
249 scriptingContainer.terminate();
250 }
251
252
253
254
255
256
257
258
259
260 protected void buildBasicSassScript(final StringBuilder sassScript)
261 throws MojoExecutionException {
262 final Log log = this.getLog();
263
264 sassScript.append("require 'rubygems'\n");
265 if (this.gemPaths.length > 0) {
266 sassScript.append("env = { 'GEM_PATH' => [\n");
267 for (final String gemPath : this.gemPaths) {
268 sassScript.append(" '").append(gemPath).append("',\n");
269 }
270
271 final String gemPath = System.getenv("GEM_PATH");
272 if (gemPath != null) {
273 for (final String p : gemPath.split(File.pathSeparator)) {
274 sassScript.append(" '").append(p).append("',\n");
275 }
276 }
277
278 sassScript.setLength(sassScript.length() - 2);
279
280
281
282
283 sassScript.append("\n].uniq.join(File::PATH_SEPARATOR) }\n");
284 sassScript.append("Gem.paths = env\n");
285 }
286
287 for (final String gem : this.gems) {
288 sassScript.append("require '").append(gem).append("'\n");
289 }
290
291 sassScript.append("require 'sass/plugin'\n");
292 sassScript.append("require 'java'\n");
293
294
295
296
297
298
299 final Iterator<Entry<String, String>> templateLocations = this
300 .getTemplateLocations();
301 if (templateLocations.hasNext()) {
302 final Entry<String, String> location = templateLocations.next();
303 this.sassOptions.put("template_location",
304 "'" + location.getKey() + "'");
305 this.sassOptions.put("css_location",
306 "'" + location.getValue() + "'");
307 }
308
309
310 if (!this.sassOptions.containsKey("cache_location")) {
311 final File sassCacheDir = new File(this.buildDirectory,
312 "sass_cache");
313 final String sassCacheDirStr = sassCacheDir.toString();
314 this.sassOptions.put("cache_location",
315 "'" + FilenameUtils.separatorsToUnix(sassCacheDirStr) + "'");
316 }
317
318
319 sassScript.append("Sass::Plugin.options.merge!(\n");
320 for (final Iterator<Entry<String, String>> entryItr = this.sassOptions
321 .entrySet().iterator(); entryItr.hasNext();) {
322 final Entry<String, String> optEntry = entryItr.next();
323 final String opt = optEntry.getKey();
324 final String value = optEntry.getValue();
325 sassScript.append(" :").append(opt).append(" => ").append(value);
326 if (entryItr.hasNext()) {
327 sassScript.append(",");
328 }
329 sassScript.append('\n');
330 }
331 sassScript.append(")\n");
332
333
334
335 while (templateLocations.hasNext()) {
336 final Entry<String, String> location = templateLocations.next();
337 sassScript.append("Sass::Plugin.add_template_location('")
338 .append(location.getKey()).append("', '")
339 .append(location.getValue()).append("')\n");
340 }
341
342 if (this.useBourbon) {
343 log.info("Running with Bourbon enabled.");
344 final String bDest = this.buildDirectory + "/bourbon-lib";
345 this.extractBourbonResources(bDest);
346 sassScript.append("Sass::Plugin.add_template_location('")
347 .append(bDest)
348 .append("', '")
349 .append(destination).append("')\n");
350 }
351
352
353 sassScript
354 .append("Sass::Plugin.on_compilation_error {|error, template, css| $compiler_callback.compilationError(error.message, template, css) }\n");
355 sassScript
356 .append("Sass::Plugin.on_updated_stylesheet {|template, css| $compiler_callback.updatedStylesheeet(template, css) }\n");
357 sassScript
358 .append("Sass::Plugin.on_template_modified {|template| $compiler_callback.templateModified(template) }\n");
359 sassScript
360 .append("Sass::Plugin.on_template_created {|template| $compiler_callback.templateCreated(template) }\n");
361 sassScript
362 .append("Sass::Plugin.on_template_deleted {|template| $compiler_callback.templateDeleted(template) }\n");
363
364
365 if (log.isDebugEnabled()) {
366 sassScript.append("require 'pp'\n");
367 sassScript.append("pp Sass::Plugin.options\n");
368 }
369 }
370
371
372
373
374
375
376 private Iterator<Entry<String, String>> getTemplateLocations() {
377 final Log log = this.getLog();
378 List<Resource> resList = this.resources;
379
380 if (resList.isEmpty()) {
381 log.info("No resource element was specified, using short configuration.");
382
383
384 final Resourcelugin/sass/Resource.html#Resource">Resource resource = new Resource();
385 resource.source = new FileSet();
386
387 if (this.sassSourceDirectory != null) {
388 log.debug("Setting source directory: "
389 + this.sassSourceDirectory.toString());
390 resource.source.setDirectory(this.sassSourceDirectory
391 .toString());
392 } else {
393 log.error("\"" + this.sassSourceDirectory
394 + "\" is not a directory.");
395 resource.source.setDirectory("./src/main/sass");
396 }
397 if (this.includes != null) {
398 log.debug("Setting includes: " + Arrays.toString(this.includes));
399 resource.source.setIncludes(Arrays.asList(this.includes));
400 }
401 if (this.excludes != null) {
402 log.debug("Setting excludes: " + Arrays.toString(this.excludes));
403 resource.source.setExcludes(Arrays.asList(this.excludes));
404 }
405 resource.relativeOutputDirectory = this.relativeOutputDirectory;
406 resource.destination = this.destination;
407 resList = ImmutableList.of(resource);
408 }
409
410 final List<Entry<String, String>> locations = new ArrayList<Entry<String, String>>();
411 for (final Resource source : resList) {
412 for (final Entry<String, String> entry : source
413 .getDirectoriesAndDestinations(log).entrySet()) {
414 log.info("Queueing Sass template for compile: "
415 + entry.getKey() + " => " + entry.getValue());
416 locations.add(entry);
417 }
418 }
419 return locations.iterator();
420 }
421
422
423
424
425
426 private synchronized void extractBourbonResources(String destinationDir) {
427 final Log log = this.getLog();
428 try {
429 File destDir = new File(destinationDir);
430 if (destDir.isDirectory()) {
431
432 log.info("Bourbon resources seems to have been extracted before.");
433 return;
434 }
435 log.info("Extracting Bourbon resources to: " + destinationDir);
436 destDir.mkdirs();
437
438 URL urlJar = this.getClass().getClassLoader().getResource("scss-report.xsl");
439 String resourceFilePath = urlJar.getFile();
440 int index = resourceFilePath.indexOf("!");
441 String jarFileURI = resourceFilePath.substring(0, index);
442 File jarFile = new File(new URI(jarFileURI));
443 JarFile jar = new JarFile(jarFile);
444
445
446 for (Enumeration<JarEntry> enums = jar.entries(); enums.hasMoreElements();) {
447 JarEntry entry = enums.nextElement();
448
449 if (entry.getName().contains(BOURBON_GEM_PATH) || entry.getName().contains("_bourbon.scss")) {
450
451 index = entry.getName().indexOf(BOURBON_GEM_PATH);
452 String fileName = destinationDir + File.separator + entry.getName().substring(index);
453
454 File f = new File(fileName.replace(BOURBON_GEM_PATH,BOURBON_DEST_PATH));
455 if (fileName.endsWith("/")) {
456 log.debug("create Bourbon directory: " + f);
457 f.mkdirs();
458 } else {
459 FileOutputStream fos = new FileOutputStream(f);
460 try {
461
462 IOUtil.copy(jar.getInputStream(entry), fos);
463 } finally {
464 IOUtil.close(fos);
465 }
466 }
467 }
468 }
469 } catch (IOException | URISyntaxException ex) {
470 log.error("Error extracting Bourbon resources.", ex);
471 }
472 }
473
474
475
476
477
478 protected List<Resource> getResources() {
479 return this.resources;
480 }
481
482
483
484
485
486 protected boolean isSkip() {
487 return this.skip;
488 }
489
490
491
492
493
494 protected File getSassSourceDirectory() {
495 return this.sassSourceDirectory;
496 }
497 }