001/**
002 * Copyright 2005-2016 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.krad.theme;
017
018import org.apache.commons.lang.StringUtils;
019import org.apache.log4j.Logger;
020import org.kuali.common.util.Assert;
021import org.kuali.common.util.execute.Executable;
022import org.kuali.rice.krad.theme.postprocessor.ThemeCssFilesProcessor;
023import org.kuali.rice.krad.theme.postprocessor.ThemeFilesProcessor;
024import org.kuali.rice.krad.theme.postprocessor.ThemeJsFilesProcessor;
025import org.kuali.rice.krad.theme.preprocessor.ThemePreProcessor;
026import org.kuali.rice.krad.theme.util.NonHiddenDirectoryFilter;
027import org.kuali.rice.krad.theme.util.ThemeBuilderConstants;
028import org.kuali.rice.krad.theme.util.ThemeBuilderUtils;
029
030import java.io.File;
031import java.io.IOException;
032import java.util.ArrayList;
033import java.util.Arrays;
034import java.util.HashMap;
035import java.util.List;
036import java.util.Map;
037import java.util.Properties;
038
039/**
040 * Class that gets executed from the Spring context to build out view themes.
041 *
042 * <p>A view theme is a collection of assets that provides the base css and js for one or more views (see
043 * {@link org.kuali.rice.krad.uif.view.ViewTheme}. The theme builder provides utilities for creating and
044 * configuring themes that follow a standard directory convention.</p>
045 *
046 * <p>By default, the theme builder processes any directories under '/themes' as theme directories. Other
047 * theme directories can be added through the property {@link #getAdditionalThemeDirectories()}
048 *
049 * The basic functions provided by the theme builder are:
050 *
051 * <ul>
052 * <li>Overlay assets from a parent theme directory (if a parent is configured). Only assets that exist in
053 * the parent directory but not in the child will be overlaid</li>
054 * <li>Applies one or more configured {@link ThemePreProcessor} instances to the theme files. For example, Less
055 * files are compiled to CSS files here by the {@link org.kuali.rice.krad.theme.preprocessor.LessThemePreProcessor}</li>
056 * <li>Collects JS and CSS resources for the theme. This includes bringing in plugin resources and base KRAD script.
057 * Resources can be filtered and ordered as needed</li>
058 * <li>Perform merging and minification for each file type. During this the file types can perform additional
059 * processing (for example, URL rewriting is done for CSS files)</li>
060 * </ul>
061 *
062 * To just perform the first step (overlay parent assets), the property {@link #isSkipThemeProcessing()} can be set to
063 * true. This is useful in development where an update to a parent file just needs pushed to the output directory.</p>
064 *
065 * @author Kuali Rice Team (rice.collab@kuali.org)
066 * @see org.kuali.rice.krad.theme.ThemeBuilderOverlays
067 * @see org.kuali.rice.krad.theme.preprocessor.ThemePreProcessor
068 * @see org.kuali.rice.krad.theme.postprocessor.ThemeFilesProcessor
069 */
070public class ThemeBuilder implements Executable {
071    private static final Logger LOG = Logger.getLogger(ThemeBuilder.class);
072
073    private String webappSourceDir;
074    private String themeBuilderOutputDir;
075
076    private List<String> themeExcludes;
077
078    private List<String> additionalThemeDirectories;
079    private List<String> additionalPluginDirectories;
080
081    private String projectVersion;
082
083    private List<ThemePreProcessor> themePreProcessors;
084
085    private Map<String, String> themeNamePathMapping;
086    private Map<String, Properties> themeNamePropertiesMapping;
087
088    private Map<String, String> pluginNamePathMapping;
089
090    private boolean skipThemeProcessing;
091
092    /**
093     * Invoked from the spring context to execute the theme builder.
094     *
095     * <p>
096     * Invokes processing of the main theme builder functions, this includes:
097     *
098     * <ul>
099     * <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}