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.google.javascript.jscomp.CompilationLevel; 019import com.google.javascript.jscomp.Compiler; 020import com.google.javascript.jscomp.CompilerOptions; 021import com.google.javascript.jscomp.SourceFile; 022import org.apache.commons.lang.StringUtils; 023import org.apache.log4j.Logger; 024import org.kuali.rice.krad.theme.util.ThemeBuilderConstants; 025import org.kuali.rice.krad.theme.util.ThemeBuilderUtils; 026 027import java.io.File; 028import java.io.FileInputStream; 029import java.io.FileOutputStream; 030import java.io.IOException; 031import java.io.InputStream; 032import java.io.InputStreamReader; 033import java.io.OutputStream; 034import java.io.OutputStreamWriter; 035import java.util.ArrayList; 036import java.util.Arrays; 037import java.util.Collections; 038import java.util.List; 039import java.util.Map; 040import java.util.Properties; 041import java.util.Set; 042import java.util.HashSet; 043 044/** 045 * Theme files processor for JavaScript files 046 * 047 * <p> 048 * Merge contents are checked for a trailing semi-colon, and altered if not found to contain one. For 049 * minification, the Google Closure compiler is used: <a link="https://developers.google.com/closure/">Google 050 * Closure</a> 051 * </p> 052 * 053 * @author Kuali Rice Team (rice.collab@kuali.org) 054 * @see ThemeFilesProcessor 055 * @see com.google.javascript.jscomp.Compiler 056 */ 057public class ThemeJsFilesProcessor extends ThemeFilesProcessor { 058 private static final Logger LOG = Logger.getLogger(ThemeJsFilesProcessor.class); 059 060 public ThemeJsFilesProcessor(String themeName, File themeDirectory, Properties themeProperties, 061 Map<String, File> themePluginDirsMap, File workingDir, String projectVersion) { 062 super(themeName, themeDirectory, themeProperties, themePluginDirsMap, workingDir, projectVersion); 063 } 064 065 /** 066 * @see ThemeFilesProcessor#getFileTypeExtension() 067 */ 068 @Override 069 protected String getFileTypeExtension() { 070 return ThemeBuilderConstants.FileExtensions.JS; 071 } 072 073 /** 074 * @see ThemeFilesProcessor#getExcludesConfigKey() 075 */ 076 @Override 077 protected String getExcludesConfigKey() { 078 return ThemeBuilderConstants.ThemeConfiguration.JS_EXCLUDES; 079 } 080 081 /** 082 * @see ThemeFilesProcessor#getFileTypeDirectoryName() 083 */ 084 @Override 085 protected String getFileTypeDirectoryName() { 086 return ThemeBuilderConstants.ThemeDirectories.SCRIPTS; 087 } 088 089 /** 090 * @see ThemeFilesProcessor#getFileListingConfigKey() 091 */ 092 @Override 093 protected String getFileListingConfigKey() { 094 return ThemeBuilderConstants.DerivedConfiguration.THEME_JS_FILES; 095 } 096 097 /** 098 * Adds JS files from the krad scripts directory to the theme file list 099 * 100 * @see ThemeFilesProcessor#addAdditionalFiles(java.util.List<java.io.File>) 101 */ 102 @Override 103 protected void addAdditionalFiles(List<File> themeFiles) { 104 File kradScriptDir = new File(this.workingDir, ThemeBuilderConstants.KRAD_SCRIPTS_DIRECTORY); 105 106 themeFiles.addAll(ThemeBuilderUtils.getDirectoryFiles(kradScriptDir, getFileIncludes(), null)); 107 } 108 109 /** 110 * Sorts the list of JS files from the plugin and sub directories 111 * 112 * <p> 113 * The sorting algorithm is as follows: 114 * 115 * <ol> 116 * <li>Any files which match patterns configured by the property <code>jsLoadFirst</code></li> 117 * <li>JS files from plugin directories, first ordered by any files that match patterns configured with 118 * <code>pluginJsLoadOrder</code>, followed by all remaining plugin files</li> 119 * <li>KRAD script files, in the order retrieved from {@link #retrieveKradScriptLoadOrder()}</li> 120 * <li>JS files from the theme subdirectory, first ordered by any files that match patterns configured 121 * with <code>themeJsLoadOrder</code>, then any remaining theme files</li> 122 * <li>Files that match patterns configured by the property <code>jsLoadLast</code>. Note any files that 123 * match here will be excluded from any of the previous steps</li> 124 * </ol> 125 * </p> 126 * 127 * @see ThemeFilesProcessor#sortThemeFiles(java.util.List<java.io.File>, java.util.List<java.io.File>) 128 * @see #retrieveKradScriptLoadOrder() 129 */ 130 @Override 131 protected List<File> sortThemeFiles(List<File> pluginFiles, List<File> subDirFiles) { 132 List<String> loadJsFirst = getThemePropertyValue(ThemeBuilderConstants.ThemeConfiguration.JS_LOAD_FIRST); 133 List<String> loadJsLast = getThemePropertyValue(ThemeBuilderConstants.ThemeConfiguration.JS_LOAD_LAST); 134 135 List<String> pluginJsLoadOrder = getThemePropertyValue( 136 ThemeBuilderConstants.ThemeConfiguration.PLUGIN_JS_LOAD_ORDER); 137 List<String> jsLoadOrder = getThemePropertyValue(ThemeBuilderConstants.ThemeConfiguration.THEME_JS_LOAD_ORDER); 138 139 // krad scripts should go before theme js files, order for these is configured in a load.properties file 140 List<String> kradScriptOrder = null; 141 try { 142 kradScriptOrder = retrieveKradScriptLoadOrder(); 143 } catch (IOException e) { 144 throw new RuntimeException("Unable to pull KRAD load order property key", e); 145 } 146 147 if (kradScriptOrder != null) { 148 if (jsLoadOrder == null) { 149 jsLoadOrder = new ArrayList<String>(); 150 } 151 152 jsLoadOrder.addAll(0, kradScriptOrder); 153 } 154 155 return ThemeBuilderUtils.orderFiles(pluginFiles, subDirFiles, loadJsFirst, loadJsLast, pluginJsLoadOrder, 156 jsLoadOrder); 157 } 158 159 /** 160 * Builds a list of KRAD script file names that indicates the order they should be loaded in 161 * 162 * <p> 163 * Populates a properties object from the file {@link org.kuali.rice.krad.theme.util.ThemeBuilderConstants#KRAD_SCRIPT_LOAD_PROPERTIES_FILE} 164 * located in the KRAD script directory. Then pulls the value for the property org.kuali.rice.krad.theme.util.ThemeBuilderConstants#LOAD_ORDER_PROPERTY_KEY 165 * to get the configured file load order. 166 * 167 * The KRAD scripts directory is then listed to get the remaining files names which are added at the end 168 * of the file list 169 * </p> 170 * 171 * @return list of KRAD file names (not including path or file extension) 172 * @throws IOException 173 */ 174 protected List<String> retrieveKradScriptLoadOrder() throws IOException { 175 List<String> scriptLoadOrder = new ArrayList<String>(); 176 177 File kradScriptsDir = new File(this.workingDir, ThemeBuilderConstants.KRAD_SCRIPTS_DIRECTORY); 178 179 File loadPropertiesFile = new File(kradScriptsDir, ThemeBuilderConstants.KRAD_SCRIPT_LOAD_PROPERTIES_FILE); 180 if (!loadPropertiesFile.exists()) { 181 throw new RuntimeException("load.properties file not found in KRAD scripts directory"); 182 } 183 184 Properties loadProperties = null; 185 186 FileInputStream fileInputStream = null; 187 try { 188 fileInputStream = new FileInputStream(loadPropertiesFile); 189 190 loadProperties = new Properties(); 191 loadProperties.load(fileInputStream); 192 } finally { 193 if (fileInputStream != null) { 194 fileInputStream.close(); 195 } 196 } 197 198 // pull the load order property from properties file 199 if (loadProperties.containsKey(ThemeBuilderConstants.LOAD_ORDER_PROPERTY_KEY)) { 200 scriptLoadOrder = ThemeBuilderUtils.getPropertyValueAsList(ThemeBuilderConstants.LOAD_ORDER_PROPERTY_KEY, 201 loadProperties); 202 } 203 204 // get remaining files from the directory 205 List<String> scriptFileNames = ThemeBuilderUtils.getDirectoryFileNames(kradScriptsDir, null, null); 206 if (scriptFileNames != null) { 207 for (String scriptFileName : scriptFileNames) { 208 // remove file extension 209 String baseScriptFileName = StringUtils.substringBeforeLast(scriptFileName, "."); 210 211 if (!scriptLoadOrder.contains(baseScriptFileName)) { 212 scriptLoadOrder.add(baseScriptFileName); 213 } 214 } 215 } 216 217 return scriptLoadOrder; 218 } 219 220 /** 221 * Checks the given file contents to determine if the last character is a semicolon, if not the contents 222 * are appended with a semicolon to prevent problems when other content is appended 223 * 224 * @see ThemeFilesProcessor#processMergeFileContents(java.lang.String, java.io.File, java.io.File) 225 */ 226 @Override 227 protected String processMergeFileContents(String fileContents, File fileToMerge, File mergedFile) 228 throws IOException { 229 if ((fileContents != null) && !fileContents.matches( 230 ThemeBuilderConstants.Patterns.JS_SEMICOLON_PATTERN)) { 231 fileContents += ";"; 232 } 233 234 return fileContents; 235 } 236 237 /** 238 * Minifies the JS contents from the given merged file into the minified file 239 * 240 * <p> 241 * Minification is performed using the Google Closure compiler, using 242 * com.google.javascript.jscomp.CompilationLevel#WHITESPACE_ONLY and EcmaScript5 language level 243 * </p> 244 * 245 * @see ThemeFilesProcessor#minify(java.io.File, java.io.File) 246 * @see com.google.javascript.jscomp.Compiler 247 */ 248 @Override 249 protected void minify(File mergedFile, File minifiedFile) throws IOException { 250 InputStream in = null; 251 OutputStream out = null; 252 OutputStreamWriter writer = null; 253 InputStreamReader reader = null; 254 255 LOG.info("Populating minified JS file: " + minifiedFile.getPath()); 256 257 try { 258 out = new FileOutputStream(minifiedFile); 259 writer = new OutputStreamWriter(out); 260 261 in = new FileInputStream(mergedFile); 262 reader = new InputStreamReader(in); 263 264 CompilerOptions options = new CompilerOptions(); 265 CompilationLevel.WHITESPACE_ONLY.setOptionsForCompilationLevel(options); 266 options.setLanguageIn(CompilerOptions.LanguageMode.ECMASCRIPT6); 267 options.setExtraAnnotationNames(ignoredAnnotations()); 268 269 SourceFile input = SourceFile.fromInputStream(mergedFile.getName(), in); 270 List<SourceFile> externs = Collections.emptyList(); 271 272 Compiler compiler = new Compiler(); 273 compiler.compile(externs, Arrays.asList(input), options); 274 275 writer.append(compiler.toSource()); 276 writer.flush(); 277 } finally { 278 if (in != null) { 279 in.close(); 280 } 281 282 if (out != null) { 283 out.close(); 284 } 285 } 286 } 287 288 /** 289 * Build a Set of annotations for the compiler to ignore in jsdoc blocks 290 * 291 * @return Iterable<String> 292 */ 293 protected Set<String> ignoredAnnotations() { 294 Set<String> annotations = new HashSet<String>(); 295 annotations.add("dtopt"); 296 annotations.add("result"); 297 annotations.add("cat"); 298 annotations.add("parm"); 299 300 return annotations; 301 } 302 303}