001/**
002 * Copyright 2005-2018 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.preprocessor;
017
018import org.apache.commons.lang.StringUtils;
019import org.apache.log4j.Logger;
020import org.kuali.rice.krad.theme.util.ThemeBuilderConstants;
021import org.kuali.rice.krad.theme.util.ThemeBuilderUtils;
022import org.lesscss.LessCompiler;
023import org.lesscss.LessException;
024import org.lesscss.LessSource;
025import org.springframework.core.io.ClassPathResource;
026import org.springframework.core.io.Resource;
027
028import java.io.File;
029import java.io.IOException;
030import java.net.URL;
031import java.util.List;
032import java.util.Properties;
033
034/**
035 * Pre processor that picks up Less files in the theme directory and compiles to CSS
036 *
037 * <p>
038 * Less files are compiled using the Apache lesscss compiler
039 * </p>
040 *
041 * @author Kuali Rice Team (rice.collab@kuali.org)
042 * @see org.lesscss.LessCompiler
043 */
044public class LessThemePreProcessor implements ThemePreProcessor {
045    private static final Logger LOG = Logger.getLogger(LessThemePreProcessor.class);
046
047    private static final String LESS_JS_FILE = "less-1.7.0.min.js";
048
049    /**
050     * Processes Less files that should be included for the given theme
051     *
052     * <p>
053     * The list of Less files for the theme is collected by a helper method then iterated over and compiled
054     * using the less compiler. The list of Less files that were processed is written as a property in the theme
055     * properties for direct Less support in development mode
056     * </p>
057     *
058     * @param themeName name of the theme to process
059     * @param themeDirectory directory containing the theme assets
060     * @param themeProperties properties for the theme containing its configuration
061     * @see #getLessFileNamesForTheme(java.lang.String, java.io.File, java.util.Properties, java.io.File)
062     */
063    public void processTheme(String themeName, File themeDirectory, Properties themeProperties) {
064        if (LOG.isDebugEnabled()) {
065            LOG.debug("Performing Less compilation for theme " + themeName);
066        }
067
068        File stylesheetsDirectory = new File(themeDirectory, ThemeBuilderConstants.ThemeDirectories.STYLESHEETS);
069        if (!stylesheetsDirectory.exists()) {
070            throw new RuntimeException("Stylesheets directory does not exist for theme: " + themeName);
071        }
072
073        List<String> lessFileNames = getLessFileNamesForTheme(themeName, themeDirectory, themeProperties,
074                stylesheetsDirectory);
075
076        LessCompiler lessCompiler = new LessCompiler();
077
078        URL lessJS = this.getClass().getClassLoader().getResource(LESS_JS_FILE);
079        lessCompiler.setLessJs(lessJS);
080
081        // not compressing here since that will be done later in the build process
082        lessCompiler.setCompress(false);
083
084        for (String lessFileName : lessFileNames) {
085            LOG.info("compiling less file: " + lessFileName);
086
087            File sourceLessFile = new File(stylesheetsDirectory, lessFileName);
088            File compiledLessFile = new File(stylesheetsDirectory, lessFileName.replace(
089                    ThemeBuilderConstants.FileExtensions.LESS, ThemeBuilderConstants.FileExtensions.CSS));
090
091            try {
092                LessSource lessSource = new LessSource(sourceLessFile);
093
094                lessCompiler.compile(lessSource, compiledLessFile, true);
095            } catch (IOException e) {
096                throw new RuntimeException("Error while compiling LESS source: " + lessFileName, e);
097            } catch (LessException e) {
098                throw new RuntimeException("Error while compiling LESS source: " + lessFileName, e);
099            }
100        }
101
102        // add list of less files to theme properties so it can be picked up by the view theme
103        // runtime in development mode
104        themeProperties.put(ThemeBuilderConstants.DerivedConfiguration.THEME_LESS_FILES,
105                StringUtils.join(lessFileNames, ","));
106    }
107
108    /**
109     * Builds the list of Less files names that should be processed for the given theme
110     *
111     * <p>
112     * All files with the <code>.less</code> extension that are in the theme's <code>stylesheets</code>
113     * directory are picked up as part of the theme (this includes files that are overlaid from a parent).
114     *
115     * All subdirectories of stylesheets are also picked up, with the exception of the <code>include</code>
116     * subdirectory. Other exclusions can be configured using the <code>lessExcludes</code> property in the
117     * theme's properties file
118     * </p>
119     *
120     * @param themeName name of the theme to pull less files for
121     * @param themeDirectory directory containing the theme's assets
122     * @param themeProperties config properties for the theme
123     * @param stylesheetsDirectory theme directory which contains the stylesheets, less files will be
124     * picked up here
125     * @return list of less file names (any path is relative to the stylesheets directory)
126     */
127    protected List<String> getLessFileNamesForTheme(String themeName, File themeDirectory, Properties themeProperties,
128            File stylesheetsDirectory) {
129        String[] lessIncludes = ThemeBuilderUtils.getPropertyValueAsArray(
130                ThemeBuilderConstants.ThemeConfiguration.LESS_INCLUDES, themeProperties);
131
132        if ((lessIncludes == null) || (lessIncludes.length == 0)) {
133            lessIncludes = new String[1];
134
135            lessIncludes[0] = ThemeBuilderConstants.Patterns.ANT_MATCH_ALL + ThemeBuilderConstants.FileExtensions.LESS;
136        }
137
138        String[] lessExcludes = ThemeBuilderUtils.getPropertyValueAsArray(
139                ThemeBuilderConstants.ThemeConfiguration.LESS_EXCLUDES, themeProperties);
140
141        lessExcludes = ThemeBuilderUtils.addToArray(lessExcludes,
142                ThemeBuilderConstants.ThemeDirectories.INCLUDES + ThemeBuilderConstants.Patterns.ANT_MATCH_DIR);
143
144        return ThemeBuilderUtils.getDirectoryContents(stylesheetsDirectory, lessIncludes, lessExcludes);
145    }
146}