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.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}