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