View Javadoc

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