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 }