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.postprocessor;
17  
18  import com.yahoo.platform.yui.compressor.CssCompressor;
19  import org.apache.commons.lang.StringUtils;
20  import org.apache.log4j.Logger;
21  import org.kuali.rice.krad.theme.util.ThemeBuilderConstants;
22  import org.kuali.rice.krad.theme.util.ThemeBuilderUtils;
23  
24  import java.io.File;
25  import java.io.FileInputStream;
26  import java.io.FileOutputStream;
27  import java.io.IOException;
28  import java.io.InputStream;
29  import java.io.InputStreamReader;
30  import java.io.OutputStream;
31  import java.io.OutputStreamWriter;
32  import java.util.List;
33  import java.util.Map;
34  import java.util.Properties;
35  import java.util.regex.Matcher;
36  import java.util.regex.Pattern;
37  
38  /**
39   * Theme files processor for CSS files
40   *
41   * <p>
42   * Merge contents are processed to rewrite any URLs (to images) for the changed path. CSS includes are not
43   * rewritten and will not work correctly in the merged file. For minification, the YUI compressor is
44   * used: <a href="http://yui.github.io/yuicompressor/">YUI Compressor</a>
45   * </p>
46   *
47   * @author Kuali Rice Team (rice.collab@kuali.org)
48   *
49   * @see ThemeFilesProcessor
50   * @see com.yahoo.platform.yui.compressor.CssCompressor
51   */
52  public class ThemeCssFilesProcessor extends ThemeFilesProcessor {
53      private static final Logger LOG = Logger.getLogger(ThemeCssFilesProcessor.class);
54  
55      protected int linebreak = -1;
56  
57      public ThemeCssFilesProcessor(String themeName, File themeDirectory, Properties themeProperties,
58              Map<String, File> themePluginDirsMap, File workingDir, String projectVersion) {
59          super(themeName, themeDirectory, themeProperties, themePluginDirsMap, workingDir, projectVersion);
60      }
61  
62      /**
63       * @see ThemeFilesProcessor#getFileTypeExtension()
64       */
65      @Override
66      protected String getFileTypeExtension() {
67          return ThemeBuilderConstants.FileExtensions.CSS;
68      }
69  
70      /**
71       * @see ThemeFilesProcessor#getExcludesConfigKey()
72       */
73      @Override
74      protected String getExcludesConfigKey() {
75          return ThemeBuilderConstants.ThemeConfiguration.CSS_EXCLUDES;
76      }
77  
78      /**
79       * @see ThemeFilesProcessor#getFileTypeDirectoryName()
80       */
81      @Override
82      protected String getFileTypeDirectoryName() {
83          return ThemeBuilderConstants.ThemeDirectories.STYLESHEETS;
84      }
85  
86      /**
87       * @see ThemeFilesProcessor#getFileListingConfigKey()
88       */
89      @Override
90      protected String getFileListingConfigKey() {
91          return ThemeBuilderConstants.DerivedConfiguration.THEME_CSS_FILES;
92      }
93  
94      /**
95       * @see ThemeFilesProcessor#addAdditionalFiles(java.util.List<java.io.File>)
96       */
97      @Override
98      protected void addAdditionalFiles(List<File> themeFiles) {
99          // 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 }