View Javadoc
1   /**
2    * Copyright 2005-2015 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.google.javascript.jscomp.CompilationLevel;
19  import com.google.javascript.jscomp.Compiler;
20  import com.google.javascript.jscomp.CompilerOptions;
21  import com.google.javascript.jscomp.SourceFile;
22  import org.apache.commons.lang.StringUtils;
23  import org.apache.log4j.Logger;
24  import org.kuali.rice.krad.theme.util.ThemeBuilderConstants;
25  import org.kuali.rice.krad.theme.util.ThemeBuilderUtils;
26  
27  import java.io.File;
28  import java.io.FileInputStream;
29  import java.io.FileOutputStream;
30  import java.io.IOException;
31  import java.io.InputStream;
32  import java.io.InputStreamReader;
33  import java.io.OutputStream;
34  import java.io.OutputStreamWriter;
35  import java.util.ArrayList;
36  import java.util.Arrays;
37  import java.util.Collections;
38  import java.util.List;
39  import java.util.Map;
40  import java.util.Properties;
41  import java.util.Set;
42  import java.util.HashSet;
43  
44  /**
45   * Theme files processor for JavaScript files
46   *
47   * <p>
48   * Merge contents are checked for a trailing semi-colon, and altered if not found to contain one. For
49   * minification, the Google Closure compiler is used: <a link="https://developers.google.com/closure/">Google
50   * Closure</a>
51   * </p>
52   *
53   * @author Kuali Rice Team (rice.collab@kuali.org)
54   * @see ThemeFilesProcessor
55   * @see com.google.javascript.jscomp.Compiler
56   */
57  public class ThemeJsFilesProcessor extends ThemeFilesProcessor {
58      private static final Logger LOG = Logger.getLogger(ThemeJsFilesProcessor.class);
59  
60      public ThemeJsFilesProcessor(String themeName, File themeDirectory, Properties themeProperties,
61              Map<String, File> themePluginDirsMap, File workingDir, String projectVersion) {
62          super(themeName, themeDirectory, themeProperties, themePluginDirsMap, workingDir, projectVersion);
63      }
64  
65      /**
66       * @see ThemeFilesProcessor#getFileTypeExtension()
67       */
68      @Override
69      protected String getFileTypeExtension() {
70          return ThemeBuilderConstants.FileExtensions.JS;
71      }
72  
73      /**
74       * @see ThemeFilesProcessor#getExcludesConfigKey()
75       */
76      @Override
77      protected String getExcludesConfigKey() {
78          return ThemeBuilderConstants.ThemeConfiguration.JS_EXCLUDES;
79      }
80  
81      /**
82       * @see ThemeFilesProcessor#getFileTypeDirectoryName()
83       */
84      @Override
85      protected String getFileTypeDirectoryName() {
86          return ThemeBuilderConstants.ThemeDirectories.SCRIPTS;
87      }
88  
89      /**
90       * @see ThemeFilesProcessor#getFileListingConfigKey()
91       */
92      @Override
93      protected String getFileListingConfigKey() {
94          return ThemeBuilderConstants.DerivedConfiguration.THEME_JS_FILES;
95      }
96  
97      /**
98       * Adds JS files from the krad scripts directory to the theme file list
99       *
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 }