View Javadoc
1   /**
2    * Copyright 2005-2015 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.theme;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.apache.log4j.Logger;
20  import org.kuali.common.util.Assert;
21  import org.kuali.common.util.execute.Executable;
22  import org.kuali.rice.krad.theme.postprocessor.ThemeCssFilesProcessor;
23  import org.kuali.rice.krad.theme.postprocessor.ThemeFilesProcessor;
24  import org.kuali.rice.krad.theme.postprocessor.ThemeJsFilesProcessor;
25  import org.kuali.rice.krad.theme.preprocessor.ThemePreProcessor;
26  import org.kuali.rice.krad.theme.util.NonHiddenDirectoryFilter;
27  import org.kuali.rice.krad.theme.util.ThemeBuilderConstants;
28  import org.kuali.rice.krad.theme.util.ThemeBuilderUtils;
29  
30  import java.io.File;
31  import java.io.IOException;
32  import java.util.ArrayList;
33  import java.util.Arrays;
34  import java.util.HashMap;
35  import java.util.List;
36  import java.util.Map;
37  import java.util.Properties;
38  
39  /**
40   * Class that gets executed from the Spring context to build out view themes.
41   *
42   * <p>A view theme is a collection of assets that provides the base css and js for one or more views (see
43   * {@link org.kuali.rice.krad.uif.view.ViewTheme}. The theme builder provides utilities for creating and
44   * configuring themes that follow a standard directory convention.</p>
45   *
46   * <p>By default, the theme builder processes any directories under '/themes' as theme directories. Other
47   * theme directories can be added through the property {@link #getAdditionalThemeDirectories()}
48   *
49   * The basic functions provided by the theme builder are:
50   *
51   * <ul>
52   * <li>Overlay assets from a parent theme directory (if a parent is configured). Only assets that exist in
53   * the parent directory but not in the child will be overlaid</li>
54   * <li>Applies one or more configured {@link ThemePreProcessor} instances to the theme files. For example, Less
55   * files are compiled to CSS files here by the {@link org.kuali.rice.krad.theme.preprocessor.LessThemePreProcessor}</li>
56   * <li>Collects JS and CSS resources for the theme. This includes bringing in plugin resources and base KRAD script.
57   * Resources can be filtered and ordered as needed</li>
58   * <li>Perform merging and minification for each file type. During this the file types can perform additional
59   * processing (for example, URL rewriting is done for CSS files)</li>
60   * </ul>
61   *
62   * To just perform the first step (overlay parent assets), the property {@link #isSkipThemeProcessing()} can be set to
63   * true. This is useful in development where an update to a parent file just needs pushed to the output directory.</p>
64   *
65   * @author Kuali Rice Team (rice.collab@kuali.org)
66   * @see org.kuali.rice.krad.theme.ThemeBuilderOverlays
67   * @see org.kuali.rice.krad.theme.preprocessor.ThemePreProcessor
68   * @see org.kuali.rice.krad.theme.postprocessor.ThemeFilesProcessor
69   */
70  public class ThemeBuilder implements Executable {
71      private static final Logger LOG = Logger.getLogger(ThemeBuilder.class);
72  
73      private String webappSourceDir;
74      private String themeBuilderOutputDir;
75  
76      private List<String> themeExcludes;
77  
78      private List<String> additionalThemeDirectories;
79      private List<String> additionalPluginDirectories;
80  
81      private String projectVersion;
82  
83      private List<ThemePreProcessor> themePreProcessors;
84  
85      private Map<String, String> themeNamePathMapping;
86      private Map<String, Properties> themeNamePropertiesMapping;
87  
88      private Map<String, String> pluginNamePathMapping;
89  
90      private boolean skipThemeProcessing;
91  
92      /**
93       * Invoked from the spring context to execute the theme builder.
94       *
95       * <p>
96       * Invokes processing of the main theme builder functions, this includes:
97       *
98       * <ul>
99       * <li>Copying assets from web source directory to the output directory</li>
100      * <li>Retrieving all theme and plugin directories, then setting up convenience maps for acquiring paths</li>
101      * <li>Iterating through each theme that should be built (those not excluded with {@link #getThemeExcludes()})</li>
102      * <li>For each theme, invoking parent and additional directory overlays, then finally calling a helper method
103      * to process the theme assets</li>
104      * </ul>
105      * </p>
106      *
107      * <p>
108      * To just perform copying of the web assets, and parent/additional directory overlays, set the property
109      * {@link #isSkipThemeProcessing()} to true
110      * </p>
111      */
112     @Override
113     public void execute() {
114         Assert.hasText(this.webappSourceDir, "Webapp source directory not set");
115 
116         LOG.info("View builder executed on " + this.webappSourceDir);
117 
118         try {
119             ThemeBuilderOverlays.copyAssetsToWorkingDir(this.webappSourceDir, this.themeBuilderOutputDir,
120                     this.additionalThemeDirectories, this.additionalPluginDirectories);
121         } catch (IOException e) {
122             throw new RuntimeException("Unable to copy assets to working directory", e);
123         }
124 
125         List<File> themeDirectories = getThemeDirectories();
126         List<File> pluginDirectories = getPluginDirectories();
127 
128         // build mappings for convenient access
129         try {
130             buildMappings(themeDirectories, pluginDirectories);
131         } catch (IOException e) {
132             throw new RuntimeException("Unable to build theme mappings", e);
133         }
134 
135         // themes must be ordered so that we build the parents first, and therefore they have all their files
136         // for overlaying to a child theme
137         List<String> orderedThemes = orderThemesForBuilding();
138 
139         if (this.themeExcludes != null) {
140             for (String themeToExclude : themeExcludes) {
141                 themeToExclude = themeToExclude.toLowerCase();
142 
143                 if (orderedThemes.contains(themeToExclude)) {
144                     orderedThemes.remove(themeToExclude);
145                 }
146 
147                 if (LOG.isDebugEnabled()) {
148                     LOG.debug("Skipping build for theme " + themeToExclude);
149                 }
150             }
151         }
152 
153         // note important that two iterations be done over the themes and not one, all the parent
154         // and plugin assets need to be overlaid before processing is done on a theme
155         for (String themeName : orderedThemes) {
156             copyParentThemeConfig(themeName);
157 
158             Properties themeProperties = this.themeNamePropertiesMapping.get(themeName);
159 
160             String themePath = this.themeNamePathMapping.get(themeName);
161             File themeDirectory = new File(themePath);
162 
163             ThemeBuilderOverlays.overlayParentAssets(themeName, themeDirectory, themeProperties,
164                     this.themeNamePathMapping);
165 
166             ThemeBuilderOverlays.overlayAdditionalDirs(themeDirectory, themeProperties, this.webappSourceDir,
167                     this.themeBuilderOutputDir);
168         }
169 
170         if (this.skipThemeProcessing) {
171             LOG.info("Skipping theme processing");
172 
173             return;
174         }
175 
176         for (String themeName : orderedThemes) {
177             processThemeAssets(themeName);
178         }
179     }
180 
181     /**
182      * Retrieves the directories that should be processed as themes.
183      *
184      * <p>
185      * By default all directories in '/themes' are included as theme directories. Additional directories can
186      * be included by setting {@link #getAdditionalThemeDirectories()}
187      * </p>
188      *
189      * @return list of file objects pointing to the theme directories
190      */
191     protected List<File> getThemeDirectories() {
192         List<File> themeDirectories = new ArrayList<File>();
193 
194         String defaultThemesDirectoryPath =
195                 this.themeBuilderOutputDir + ThemeBuilderConstants.DEFAULT_THEMES_DIRECTORY;
196 
197         File defaultThemesDirectory = new File(defaultThemesDirectoryPath);
198         File[] defaultThemeDirectories = defaultThemesDirectory.listFiles(new NonHiddenDirectoryFilter());
199 
200         if (defaultThemeDirectories != null) {
201             themeDirectories = Arrays.asList(defaultThemeDirectories);
202         }
203 
204         if (this.additionalThemeDirectories != null) {
205             List<File> additionalThemeDirs = ThemeBuilderUtils.getSubDirectories(new File(this.themeBuilderOutputDir),
206                     this.additionalThemeDirectories);
207             themeDirectories.addAll(additionalThemeDirs);
208         }
209 
210         ThemeBuilderUtils.validateFileExistence(themeDirectories, "Invalid theme directory.");
211 
212         if (LOG.isDebugEnabled()) {
213             LOG.debug("Found theme directories: " + StringUtils.join(themeDirectories, ","));
214         }
215 
216         return themeDirectories;
217     }
218 
219     /**
220      * Retrieves the directories that should be processed as plugins.
221      *
222      * <p>
223      * By default all directories in '/plugins' are included as plugins. Additional directories can
224      * be included by setting {@link #getAdditionalPluginDirectories()}
225      * </p>
226      *
227      * @return list of file objects pointing to the plugin directories
228      */
229     protected List<File> getPluginDirectories() {
230         List<File> pluginDirectories = new ArrayList<File>();
231 
232         String defaultPluginsDirectoryPath =
233                 this.themeBuilderOutputDir + ThemeBuilderConstants.DEFAULT_PLUGINS_DIRECTORY;
234         File defaultPluginsDirectory = new File(defaultPluginsDirectoryPath);
235 
236         File[] pluginDirs = defaultPluginsDirectory.listFiles(new NonHiddenDirectoryFilter());
237 
238         if (pluginDirs != null) {
239             pluginDirectories = Arrays.asList(pluginDirs);
240         }
241 
242         if (this.additionalPluginDirectories != null) {
243             List<File> additionalPluginDirs = ThemeBuilderUtils.getSubDirectories(new File(this.themeBuilderOutputDir),
244                     this.additionalPluginDirectories);
245             pluginDirectories.addAll(additionalPluginDirs);
246         }
247 
248         ThemeBuilderUtils.validateFileExistence(pluginDirectories, "Invalid plugin directory.");
249 
250         return pluginDirectories;
251     }
252 
253     /**
254      * Builds convenience maps (theme name to path map, theme name to properties mapping, and plugin
255      * name to path mapping) for the given theme and plugin directories.
256      *
257      * @param themeDirectories list of theme directories to build mappings for
258      * @param pluginDirectories list of file directories to build mappings for
259      * @throws IOException
260      */
261     protected void buildMappings(List<File> themeDirectories, List<File> pluginDirectories) throws IOException {
262         if (LOG.isDebugEnabled()) {
263             LOG.debug("Building mappings");
264         }
265 
266         this.themeNamePathMapping = new HashMap<String, String>();
267         this.themeNamePropertiesMapping = new HashMap<String, Properties>();
268 
269         for (File themeDirectory : themeDirectories) {
270             String themeName = themeDirectory.getName().toLowerCase();
271 
272             this.themeNamePathMapping.put(themeName, themeDirectory.getPath());
273 
274             Properties themeProperties = ThemeBuilderUtils.retrieveThemeProperties(themeDirectory.getPath());
275             if (themeProperties == null) {
276                 themeProperties = new Properties();
277             }
278 
279             this.themeNamePropertiesMapping.put(themeName, themeProperties);
280         }
281 
282         this.pluginNamePathMapping = new HashMap<String, String>();
283 
284         for (File pluginDirectory : pluginDirectories) {
285             String pluginName = pluginDirectory.getName().toLowerCase();
286 
287             this.pluginNamePathMapping.put(pluginName, pluginDirectory.getPath());
288         }
289     }
290 
291     /**
292      * Builds a list containing theme names in the order for which they should be processed.
293      *
294      * <p>
295      * For the parent overlays to work correctly, the parent must be processed before the child. There can
296      * be multiple parents in the hierarchy, so here we go through and figure out the correct order
297      * </p>
298      *
299      * @return list of ordered theme names
300      */
301     protected List<String> orderThemesForBuilding() {
302         if (LOG.isDebugEnabled()) {
303             LOG.debug("Ordering themes for building");
304         }
305 
306         List<String> orderedThemes = new ArrayList<String>();
307 
308         for (String themeName : this.themeNamePathMapping.keySet()) {
309             String themePath = this.themeNamePathMapping.get(themeName);
310 
311             if (orderedThemes.contains(themeName)) {
312                 continue;
313             }
314 
315             List<String> themeParents = getAllThemeParents(themeName, new ArrayList<String>());
316             for (String themeParent : themeParents) {
317                 if (!orderedThemes.contains(themeParent)) {
318                     orderedThemes.add(themeParent);
319                 }
320             }
321 
322             orderedThemes.add(themeName);
323         }
324 
325         return orderedThemes;
326     }
327 
328     /**
329      * Gets all parents (ancestors) for the given theme name.
330      *
331      * <p>
332      * The parent for a theme is determined by retrieving the theme's properties file, then pulling the
333      * property with key 'parent'. Then the properties file for the parent theme is pulled and check to see if
334      * it has a parent. So on until a theme is reached that does not have a parent
335      * </p>
336      *
337      * @param themeName name of theme to retrieve parents for
338      * @param themeParents list of parents that have been previously found (used to find circular references)
339      * @return list of theme names that are parents to the given theme
340      */
341     protected List<String> getAllThemeParents(String themeName, List<String> themeParents) {
342         Properties themeProperties = this.themeNamePropertiesMapping.get(themeName);
343         if (themeProperties.containsKey(ThemeBuilderConstants.ThemeConfiguration.PARENT)) {
344             String parentThemeName = themeProperties.getProperty(ThemeBuilderConstants.ThemeConfiguration.PARENT);
345 
346             if (StringUtils.isBlank(parentThemeName)) {
347                 return themeParents;
348             }
349 
350             if (!this.themeNamePropertiesMapping.containsKey(parentThemeName)) {
351                 throw new RuntimeException("Invalid theme name for parent property: " + parentThemeName);
352             }
353 
354             if (themeParents.contains(parentThemeName)) {
355                 throw new RuntimeException("Circular reference found for parent: " + parentThemeName);
356             }
357 
358             themeParents.addAll(getAllThemeParents(parentThemeName, themeParents));
359 
360             themeParents.add(parentThemeName);
361         }
362 
363         return themeParents;
364     }
365 
366     /**
367      * If the given theme has a parent, retrieve the theme properties (if exists) for the parent,
368      * then for each config property copy the parent value to the child theme properties if missing
369      *
370      * @param themeName name of the theme to pull parent config for and copy
371      */
372     protected void copyParentThemeConfig(String themeName) {
373         Properties themeProperties = this.themeNamePropertiesMapping.get(themeName);
374 
375         if (!themeProperties.containsKey(ThemeBuilderConstants.ThemeConfiguration.PARENT)) {
376             return;
377         }
378 
379         String parentThemeName = themeProperties.getProperty(ThemeBuilderConstants.ThemeConfiguration.PARENT);
380         Properties parentThemeProperties = this.themeNamePropertiesMapping.get(parentThemeName);
381 
382         String[] propertiesToCopy = new String[] {ThemeBuilderConstants.ThemeConfiguration.LESS_INCLUDES,
383                 ThemeBuilderConstants.ThemeConfiguration.LESS_EXCLUDES,
384                 ThemeBuilderConstants.ThemeConfiguration.PLUGIN_INCLUDES,
385                 ThemeBuilderConstants.ThemeConfiguration.PLUGIN_EXCLUDES,
386                 ThemeBuilderConstants.ThemeConfiguration.PLUGIN_FILE_EXCLUDES,
387                 ThemeBuilderConstants.ThemeConfiguration.ADDITIONAL_OVERLAYS,
388                 ThemeBuilderConstants.ThemeConfiguration.CSS_LOAD_FIRST,
389                 ThemeBuilderConstants.ThemeConfiguration.CSS_LOAD_LAST,
390                 ThemeBuilderConstants.ThemeConfiguration.PLUGIN_JS_LOAD_ORDER,
391                 ThemeBuilderConstants.ThemeConfiguration.PLUGIN_CSS_LOAD_ORDER,
392                 ThemeBuilderConstants.ThemeConfiguration.THEME_JS_LOAD_ORDER,
393                 ThemeBuilderConstants.ThemeConfiguration.THEME_CSS_LOAD_ORDER,
394                 ThemeBuilderConstants.ThemeConfiguration.JS_LOAD_FIRST,
395                 ThemeBuilderConstants.ThemeConfiguration.JS_LOAD_LAST,
396                 ThemeBuilderConstants.ThemeConfiguration.DEV_JS_INCLUDES};
397 
398         for (String propertyKey : propertiesToCopy) {
399             ThemeBuilderUtils.copyProperty(propertyKey, parentThemeProperties, themeProperties);
400         }
401     }
402 
403     /**
404      * Performs the various steps to process the given theme
405      *
406      * <p>
407      * The theme is processed first by applying any configured {@link org.kuali.rice.krad.theme.preprocessor.ThemePreProcessor}
408      * instances (such as less processing). Once the pre processors are applied, the CSS and JS post processors are
409      * then invoked to do the final processing
410      *
411      * After processing is complete the 'theme-derived.properties' file gets written to the theme directory, which
412      * contains all the properties for the theme (set, inherited, derived)
413      * </p>
414      *
415      * @param themeName name of the theme to process
416      */
417     protected void processThemeAssets(String themeName) {
418         Properties themeProperties = this.themeNamePropertiesMapping.get(themeName);
419 
420         String themePath = this.themeNamePathMapping.get(themeName);
421         File themeDirectory = new File(themePath);
422 
423         LOG.info("Processing assets for theme: " + themeName);
424 
425         // apply pre-processors which can modify the theme assets before they are collected
426         if (this.themePreProcessors != null) {
427             for (ThemePreProcessor preProcessor : this.themePreProcessors) {
428                 preProcessor.processTheme(themeName, themeDirectory, themeProperties);
429             }
430         }
431 
432         // apply processors for CSS and JS files to do final processing
433         File workingDir = new File(this.themeBuilderOutputDir);
434 
435         Map<String, File> themePluginDirsMap = collectThemePluginDirs(themeProperties);
436 
437         ThemeFilesProcessor filesProcessor = new ThemeCssFilesProcessor(themeName, themeDirectory, themeProperties,
438                 themePluginDirsMap, workingDir, this.projectVersion);
439         filesProcessor.process();
440 
441         filesProcessor = new ThemeJsFilesProcessor(themeName, themeDirectory, themeProperties,
442                 themePluginDirsMap, workingDir, this.projectVersion);
443         filesProcessor.process();
444 
445         try {
446             ThemeBuilderUtils.storeThemeProperties(themePath, themeProperties);
447         } catch (IOException e) {
448             throw new RuntimeException("Unable to update theme.properties file", e);
449         }
450     }
451 
452     /**
453      * Helper method that filters the list of all plugins and returns those that should be used
454      * with the theme
455      *
456      * <p>
457      * Which plugins to include for a theme can be configured using the pluginIncludes and pluginExlcudes
458      * property keys
459      * </p>
460      *
461      * @param themeProperties properties file for the theme, used to retrieve the plugin configuration
462      * @return map containing the plugins for the theme, map key is the plugin name and map value gives
463      *         the plugin directory
464      */
465     protected Map<String, File> collectThemePluginDirs(Properties themeProperties) {
466         Map<String, File> themePluginDirs = new HashMap<String, File>();
467 
468         String[] pluginIncludes = ThemeBuilderUtils.getPropertyValueAsArray(
469                 ThemeBuilderConstants.ThemeConfiguration.PLUGIN_INCLUDES, themeProperties);
470 
471         String[] pluginExcludes = ThemeBuilderUtils.getPropertyValueAsArray(
472                 ThemeBuilderConstants.ThemeConfiguration.PLUGIN_EXCLUDES, themeProperties);
473 
474         for (Map.Entry<String, String> pluginMapping : this.pluginNamePathMapping.entrySet()) {
475             String pluginName = pluginMapping.getKey();
476 
477             if (ThemeBuilderUtils.inExcludeList(pluginName, pluginExcludes)) {
478                 continue;
479             }
480 
481             if (ThemeBuilderUtils.inIncludeList(pluginName, pluginIncludes)) {
482                 themePluginDirs.put(pluginName, new File(pluginMapping.getValue()));
483             }
484         }
485 
486         themeProperties.put(ThemeBuilderConstants.DerivedConfiguration.THEME_PLUGIN_NAMES,
487                 StringUtils.join(themePluginDirs.keySet(), ","));
488 
489         return themePluginDirs;
490     }
491 
492     /**
493      * Map that associates theme names with their path, provided here for subclasses
494      *
495      * @return map of theme name/paths, map key is the theme name, map value is the theme path
496      */
497     protected Map<String, String> getThemeNamePathMapping() {
498         return themeNamePathMapping;
499     }
500 
501     /**
502      * Map that associates theme names with their properties, provided here for subclasses
503      *
504      * @return map of theme name/properties, map key is the theme name, map value is the properties object
505      */
506     protected Map<String, Properties> getThemeNamePropertiesMapping() {
507         return themeNamePropertiesMapping;
508     }
509 
510     /**
511      * Absolute path to the directory that contains the web application source
512      *
513      * <p>
514      * Generally this is the base directory for the application/module, then /src/main/webapp
515      * </p>
516      *
517      * <p>
518      * If you are using the maven plugin this can be set by the maven property <code>webapp.source.dir</code>
519      * </p>
520      *
521      * @return path to webapp source directory
522      */
523     public String getWebappSourceDir() {
524         return webappSourceDir;
525     }
526 
527     /**
528      * Setter for the path to the webapp source
529      *
530      * @param webappSourceDir
531      */
532     public void setWebappSourceDir(String webappSourceDir) {
533         if (StringUtils.isNotBlank(webappSourceDir)) {
534             // trim off any trailing path separators
535             if (webappSourceDir.endsWith(File.separator) || webappSourceDir.endsWith("/")) {
536                 webappSourceDir = webappSourceDir.substring(0, webappSourceDir.length() - 1);
537             }
538         }
539 
540         this.webappSourceDir = webappSourceDir;
541     }
542 
543     /**
544      * Absolute path to the directory the theme builder should output content to
545      *
546      * <p>
547      * Generally this will be the output directory for the exploded war being created. However you can also
548      * choose to output to a temporary directory, then copy the assets over at a later phase
549      * </p>
550      *
551      * <p>
552      * If you are using the maven plugin this can be set by the maven property <code>theme.builder.output.dir</code>
553      * </p>
554      *
555      * @return path to the output directory
556      */
557     public String getThemeBuilderOutputDir() {
558         return themeBuilderOutputDir;
559     }
560 
561     /**
562      * Setter for the path to the output directory
563      *
564      * @param themeBuilderOutputDir
565      */
566     public void setThemeBuilderOutputDir(String themeBuilderOutputDir) {
567         if (StringUtils.isNotBlank(themeBuilderOutputDir)) {
568             // trim off any trailing path separators
569             if (themeBuilderOutputDir.endsWith(File.separator) || themeBuilderOutputDir.endsWith("/")) {
570                 themeBuilderOutputDir = themeBuilderOutputDir.substring(0, themeBuilderOutputDir.length() - 1);
571             }
572         }
573 
574         this.themeBuilderOutputDir = themeBuilderOutputDir;
575     }
576 
577     /**
578      * List of theme names that should be excluded from theme processing
579      *
580      * <p>
581      * Directories for themes that are excluded will be copied to the output directory but no further
582      * processing will occur on that theme.
583      * </p>
584      *
585      * <p>
586      * If your web application receives web overlays which include themes, they will already be processed.
587      * Processing them again will result in duplicate content. Therefore you should exclude these themes using
588      * this property
589      * </p>
590      *
591      * <p>
592      * If you are using the maven plugin this can be set by the maven property <code>theme.builder.excludes</code>
593      * </p>
594      *
595      * @return list of excluded theme names
596      */
597     public List<String> getThemeExcludes() {
598         return themeExcludes;
599     }
600 
601     /**
602      * Setter for the list of theme names to exclude from processing
603      *
604      * @param themeExcludes
605      */
606     public void setThemeExcludes(List<String> themeExcludes) {
607         this.themeExcludes = themeExcludes;
608     }
609 
610     /**
611      * Convenience setter that takes a string and parses to populate the theme excludes list
612      *
613      * @param themeExcludes string containing theme names to exclude which are delimited using a comma
614      */
615     public void setThemeExcludesStr(String themeExcludes) {
616         if (StringUtils.isNotBlank(themeExcludes)) {
617             String[] themeExcludesArray = themeExcludes.split(",");
618             this.themeExcludes = Arrays.asList(themeExcludesArray);
619         }
620     }
621 
622     /**
623      * List of absolute paths to include as additional theme directories
624      *
625      * <p>
626      * By default all directories under the web root folder <code>themes</code> are included. Other web
627      * directories can be processed as themes by including their path in this list
628      * </p>
629      *
630      * <p>
631      * If you are using the maven plugin this can be set by the maven property <code>theme.builder.theme.adddirs</code>
632      * </p>
633      *
634      * @return list of paths for additional themes
635      */
636     public List<String> getAdditionalThemeDirectories() {
637         return additionalThemeDirectories;
638     }
639 
640     /**
641      * Setter for the list of additional theme directory paths
642      *
643      * @param additionalThemeDirectories
644      */
645     public void setAdditionalThemeDirectories(List<String> additionalThemeDirectories) {
646         this.additionalThemeDirectories = additionalThemeDirectories;
647     }
648 
649     /**
650      * Convenience setter that takes a string and parses to populate the additional theme directories list
651      *
652      * @param additionalThemeDirectories string containing additional theme directories which are
653      * delimited using a comma
654      */
655     public void setAdditionalThemeDirectoriesStr(String additionalThemeDirectories) {
656         if (StringUtils.isNotBlank(additionalThemeDirectories)) {
657             String[] additionalThemeDirectoriesArray = additionalThemeDirectories.split(",");
658             this.additionalThemeDirectories = Arrays.asList(additionalThemeDirectoriesArray);
659         }
660     }
661 
662     /**
663      * List of absolute paths to include as additional plugin directories
664      *
665      * <p>
666      * By default all directories under the web root folder <code>plugins</code> are included. Other web
667      * directories can be processed as plugins by including their path in this list
668      * </p>
669      *
670      * <p>
671      * If you are using the maven plugin this can be set by the maven property <code>theme.builder.plugin.adddirs</code>
672      * </p>
673      *
674      * @return list of paths for additional plugins
675      */
676     public List<String> getAdditionalPluginDirectories() {
677         return additionalPluginDirectories;
678     }
679 
680     /**
681      * Setter for the list of additional plugin directory paths
682      *
683      * @param additionalPluginDirectories
684      */
685     public void setAdditionalPluginDirectories(List<String> additionalPluginDirectories) {
686         this.additionalPluginDirectories = additionalPluginDirectories;
687     }
688 
689     /**
690      * Convenience setter that takes a string and parses to populate the additional plugin directories list
691      *
692      * @param additionalPluginDirectories string containing additional plugin directories which are
693      * delimited using a comma
694      */
695     public void setAdditionalPluginDirectoriesStr(String additionalPluginDirectories) {
696         if (StringUtils.isNotBlank(additionalPluginDirectories)) {
697             String[] additionalPluginDirectoriesArray = additionalPluginDirectories.split(",");
698             this.additionalPluginDirectories = Arrays.asList(additionalPluginDirectoriesArray);
699         }
700     }
701 
702     /**
703      * Version for the project that will be used to stamp the minified file
704      *
705      * <p>
706      * In order to facilitate automatic downloads between project releases, the minified files are stamped with
707      * the version number.
708      * </p>
709      *
710      * <p>
711      * If you are using the maven plugin this can be set by the maven property <code>project.version</code>
712      * </p>
713      *
714      * @return version string for project
715      */
716     public String getProjectVersion() {
717         return projectVersion;
718     }
719 
720     /**
721      * Setter for the project version
722      *
723      * @param projectVersion
724      */
725     public void setProjectVersion(String projectVersion) {
726         this.projectVersion = projectVersion;
727     }
728 
729     /**
730      * List of {@link ThemePreProcessor} instances that should be applied to the themes
731      *
732      * @return list of pre processors to apply
733      */
734     public List<ThemePreProcessor> getThemePreProcessors() {
735         return themePreProcessors;
736     }
737 
738     /**
739      * Setter for the list of theme pre processors
740      *
741      * @param themePreProcessors
742      */
743     public void setThemePreProcessors(List<ThemePreProcessor> themePreProcessors) {
744         this.themePreProcessors = themePreProcessors;
745     }
746 
747     /**
748      * Indicates whether processing of the themes should be skipped
749      *
750      * <p>
751      * In development it can be useful to just update the output directory with the theme assets, and skip
752      * processing such as Less and minification (which can be time consuming). Setting this flag to true will
753      * skip processing of pre and post processors, just doing the overlay. By default this is false
754      * </p>
755      *
756      * @return true if theme processing should be skipped, false if not
757      */
758     public boolean isSkipThemeProcessing() {
759         return skipThemeProcessing;
760     }
761 
762     /**
763      * Setter to skip theme processing
764      *
765      * @param skipThemeProcessing
766      */
767     public void setSkipThemeProcessing(boolean skipThemeProcessing) {
768         this.skipThemeProcessing = skipThemeProcessing;
769     }
770 
771 }