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.postprocessor;
017
018import com.yahoo.platform.yui.compressor.CssCompressor;
019import org.apache.commons.lang.StringUtils;
020import org.apache.log4j.Logger;
021import org.kuali.rice.krad.theme.util.ThemeBuilderConstants;
022import org.kuali.rice.krad.theme.util.ThemeBuilderUtils;
023
024import java.io.File;
025import java.io.FileInputStream;
026import java.io.FileOutputStream;
027import java.io.IOException;
028import java.io.InputStream;
029import java.io.InputStreamReader;
030import java.io.OutputStream;
031import java.io.OutputStreamWriter;
032import java.util.List;
033import java.util.Map;
034import java.util.Properties;
035import java.util.regex.Matcher;
036import java.util.regex.Pattern;
037
038/**
039 * Theme files processor for CSS files
040 *
041 * <p>
042 * Merge contents are processed to rewrite any URLs (to images) for the changed path. CSS includes are not
043 * rewritten and will not work correctly in the merged file. For minification, the YUI compressor is
044 * used: <a href="http://yui.github.io/yuicompressor/">YUI Compressor</a>
045 * </p>
046 *
047 * @author Kuali Rice Team (rice.collab@kuali.org)
048 *
049 * @see ThemeFilesProcessor
050 * @see com.yahoo.platform.yui.compressor.CssCompressor
051 */
052public class ThemeCssFilesProcessor extends ThemeFilesProcessor {
053    private static final Logger LOG = Logger.getLogger(ThemeCssFilesProcessor.class);
054
055    protected int linebreak = -1;
056
057    public ThemeCssFilesProcessor(String themeName, File themeDirectory, Properties themeProperties,
058            Map<String, File> themePluginDirsMap, File workingDir, String projectVersion) {
059        super(themeName, themeDirectory, themeProperties, themePluginDirsMap, workingDir, projectVersion);
060    }
061
062    /**
063     * @see ThemeFilesProcessor#getFileTypeExtension()
064     */
065    @Override
066    protected String getFileTypeExtension() {
067        return ThemeBuilderConstants.FileExtensions.CSS;
068    }
069
070    /**
071     * @see ThemeFilesProcessor#getExcludesConfigKey()
072     */
073    @Override
074    protected String getExcludesConfigKey() {
075        return ThemeBuilderConstants.ThemeConfiguration.CSS_EXCLUDES;
076    }
077
078    /**
079     * @see ThemeFilesProcessor#getFileTypeDirectoryName()
080     */
081    @Override
082    protected String getFileTypeDirectoryName() {
083        return ThemeBuilderConstants.ThemeDirectories.STYLESHEETS;
084    }
085
086    /**
087     * @see ThemeFilesProcessor#getFileListingConfigKey()
088     */
089    @Override
090    protected String getFileListingConfigKey() {
091        return ThemeBuilderConstants.DerivedConfiguration.THEME_CSS_FILES;
092    }
093
094    /**
095     * @see ThemeFilesProcessor#addAdditionalFiles(java.util.List<java.io.File>)
096     */
097    @Override
098    protected void addAdditionalFiles(List<File> themeFiles) {
099        // no additional files
100    }
101
102    /**
103     * Sorts the list of CSS files from the plugin and sub directories
104     *
105     * <p>
106     * The sorting algorithm is as follows:
107     *
108     * <ol>
109     * <li>Any files which match patterns configured by the property <code>cssLoadFirst</code></li>
110     * <li>CSS files from plugin directories, first ordered by any files that match patterns configured with
111     * <code>pluginCssLoadOrder</code>, followed by all remaining plugin files</li>
112     * <li>CSS files from the theme subdirectory, first ordered by any files that match patterns configured
113     * with <code>themeCssLoadOrder</code>, then any remaining theme files</li>
114     * <li>Files that match patterns configured by the property <code>cssLoadLast</code>. Note any files that
115     * match here will be excluded from any of the previous steps</li>
116     * </ol>
117     * </p>
118     *
119     * @see ThemeFilesProcessor#sortThemeFiles(java.util.List<java.io.File>, java.util.List<java.io.File>)
120     */
121    @Override
122    protected List<File> sortThemeFiles(List<File> pluginFiles, List<File> subDirFiles) {
123        List<String> loadCssFirst = getThemePropertyValue(ThemeBuilderConstants.ThemeConfiguration.CSS_LOAD_FIRST);
124        List<String> loadCssLast = getThemePropertyValue(ThemeBuilderConstants.ThemeConfiguration.CSS_LOAD_LAST);
125
126        List<String> pluginCssLoadOrder = getThemePropertyValue(
127                ThemeBuilderConstants.ThemeConfiguration.PLUGIN_CSS_LOAD_ORDER);
128        List<String> cssLoadOrder = getThemePropertyValue(
129                ThemeBuilderConstants.ThemeConfiguration.THEME_CSS_LOAD_ORDER);
130
131        return ThemeBuilderUtils.orderFiles(pluginFiles, subDirFiles, loadCssFirst, loadCssLast, pluginCssLoadOrder,
132                cssLoadOrder);
133    }
134
135    /**
136     * Processes the merge contents to rewrite any URLs necessary for the directory change
137     *
138     * @see ThemeFilesProcessor#processMergeFileContents(java.lang.String, java.io.File, java.io.File)
139     */
140    @Override
141    protected String processMergeFileContents(String fileContents, File fileToMerge, File mergedFile)
142            throws IOException {
143        return rewriteCssUrls(fileContents, fileToMerge, mergedFile);
144    }
145
146    /**
147     * Performs URL rewriting within the given CSS contents
148     *
149     * <p>
150     * The given merge file (where the merge contents come from) and the merged file (where they are going to)
151     * is used to determine the path difference. Once that path difference is found, the contents are then matched
152     * to find any URLs. For each relative URL (absolute URLs are not modified), the path is adjusted and
153     * replaced into the contents.
154     *
155     * ex. suppose the merged file is /plugins/foo/plugin.css, and the merged file is
156     * /themes/mytheme/stylesheets/merged.css, the path difference will then be '../../../plugins/foo/'. So a URL
157     * in the CSS contents of 'images/image.png' will get rewritten to '../../../plugins/foo/images/image.png'
158     * </p>
159     *
160     * @param css contents to adjust URLs for
161     * @param mergeFile file that provided the merge contents
162     * @param mergedFile file the contents will be going to
163     * @return css contents, with possible adjusted URLs
164     * @throws IOException
165     */
166    protected String rewriteCssUrls(String css, File mergeFile, File mergedFile) throws IOException {
167        String urlAdjustment = ThemeBuilderUtils.calculatePathToFile(mergedFile, mergeFile);
168
169        if (StringUtils.isBlank(urlAdjustment)) {
170            // no adjustment needed
171            return css;
172        }
173
174        // match all URLs in css string and then adjust each one
175        Pattern urlPattern = Pattern.compile(ThemeBuilderConstants.Patterns.CSS_URL_PATTERN);
176
177        Matcher matcher = urlPattern.matcher(css);
178
179        StringBuffer sb = new StringBuffer();
180        while (matcher.find()) {
181            String cssStatement = matcher.group();
182
183            String cssUrl = null;
184            if (matcher.group(1) != null) {
185                cssUrl = matcher.group(1);
186            } else {
187                cssUrl = matcher.group(2);
188            }
189
190            if (cssUrl != null) {
191                // only adjust URL if it is relative
192                String modifiedUrl = cssUrl;
193
194                if (!cssUrl.startsWith("/")) {
195                    modifiedUrl = urlAdjustment + cssUrl;
196                }
197
198                String modifiedStatement = Matcher.quoteReplacement(cssStatement.replace(cssUrl, modifiedUrl));
199
200                matcher.appendReplacement(sb, modifiedStatement);
201            }
202        }
203
204        matcher.appendTail(sb);
205
206        return sb.toString();
207    }
208
209    /**
210     * Minifies the CSS contents from the given merged file into the minified file
211     *
212     * <p>
213     * Minification is performed using the YUI Compressor compiler with no line break
214     * </p>
215     *
216     * @see ThemeFilesProcessor#minify(java.io.File, java.io.File)
217     * @see com.yahoo.platform.yui.compressor.CssCompressor
218     */
219    @Override
220    protected void minify(File mergedFile, File minifiedFile) throws IOException {
221        InputStream in = null;
222        OutputStream out = null;
223        OutputStreamWriter writer = null;
224        InputStreamReader reader = null;
225
226        LOG.info("Populating minified CSS file: " + minifiedFile.getPath());
227
228        try {
229            out = new FileOutputStream(minifiedFile);
230            writer = new OutputStreamWriter(out);
231
232            in = new FileInputStream(mergedFile);
233            reader = new InputStreamReader(in);
234
235            CssCompressor compressor = new CssCompressor(reader);
236            compressor.compress(writer, this.linebreak);
237
238            writer.flush();
239        } finally {
240            if (in != null) {
241                in.close();
242            }
243
244            if (out != null) {
245                out.close();
246            }
247        }
248    }
249}