001/* 002 * Copyright 2011-2012 Stephen Connolly. 003 * 004 * Licensed under the Apache 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.apache.org/licenses/LICENSE-2.0 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 */ 016 017package org.jszip.maven; 018 019import org.apache.maven.plugin.MojoExecutionException; 020import org.apache.maven.plugin.MojoFailureException; 021import org.apache.maven.plugins.annotations.LifecyclePhase; 022import org.apache.maven.plugins.annotations.Mojo; 023import org.apache.maven.plugins.annotations.Parameter; 024import org.apache.maven.plugins.annotations.ResolutionScope; 025import org.codehaus.plexus.util.DirectoryScanner; 026import org.codehaus.plexus.util.IOUtil; 027import org.jszip.pseudo.io.PseudoFileSystem; 028import org.jszip.rhino.JavaScriptTerminationException; 029import org.jszip.rhino.OptimizeContextAction; 030import org.mozilla.javascript.Context; 031import org.mozilla.javascript.ContextFactory; 032import org.mozilla.javascript.JavaScriptException; 033import org.mozilla.javascript.tools.shell.Global; 034import org.mozilla.javascript.tools.shell.QuitAction; 035import org.mozilla.javascript.tools.shell.ShellContextFactory; 036 037import java.io.File; 038import java.io.FileInputStream; 039import java.io.IOException; 040import java.io.InputStream; 041import java.io.InputStreamReader; 042import java.util.List; 043import java.util.regex.Matcher; 044import java.util.regex.Pattern; 045 046/** 047 * Runs the r.js optimizer over the source 048 */ 049@Mojo(name = "optimize", defaultPhase = LifecyclePhase.PROCESS_CLASSES, 050 requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME) 051public class OptimizeMojo extends AbstractPseudoFileSystemProcessorMojo { 052 053 /** 054 * Regex for a quoted string which may include escapes. 055 */ 056 public static final String QUOTED_STRING_WITH_ESCAPES = "'([^\\\\']+|\\\\([btnfr\"'\\\\]|[0-3]?[0-7]{1," 057 + "2}|u[0-9a-fA-F]{4}))*'|\"([^\\\\\"]+|\\\\([btnfr\"'\\\\]|[0-3]?[0-7]{1,2}|u[0-9a-fA-F]{4}))*\""; 058 059 /** 060 * Regex for sniffing the version of r.js being used. 061 */ 062 public static final String R_JS_VERSION_REGEX = "\\s+version\\s*=\\s*(" + QUOTED_STRING_WITH_ESCAPES + ")"; 063 064 /** 065 * Directory containing the build profiles. 066 */ 067 @Parameter(defaultValue = "src/build/js", required = true) 068 private File contentDirectory; 069 070 /** 071 * Directory containing the build profiles. 072 */ 073 @Parameter(defaultValue = "src/build/js/r.js") 074 private File customRScript; 075 076 /** 077 * Skip optimization. 078 */ 079 @Parameter(property = "jszip.optimize.skip", defaultValue = "false") 080 private boolean skip; 081 082 /** 083 * A list of <include> elements specifying the build profiles (by pattern) that should be included in 084 * optimization. 085 */ 086 @Parameter(property = "includes") 087 private List<String> includes; 088 089 /** 090 * A list of <exclude> elements specifying the build profiles (by pattern) that should be excluded from 091 * optimization. 092 */ 093 @Parameter(property = "excludes") 094 private List<String> excludes; 095 096 /** 097 * @see org.apache.maven.plugin.Mojo#execute() 098 */ 099 public void execute() throws MojoExecutionException, MojoFailureException { 100 if (skip) { 101 getLog().info("Optimization skipped."); 102 return; 103 } 104 if (!contentDirectory.exists()) { 105 getLog().info("Nothing to do, no r.js build profiles in " + contentDirectory); 106 return; 107 } 108 if (!contentDirectory.isDirectory()) { 109 throw new MojoExecutionException("Build profile directory '" + contentDirectory + "' is not a directory"); 110 } 111 if (webappDirectory.isFile()) { 112 throw new MojoExecutionException("Webapp directory '" + webappDirectory + "' is not a directory"); 113 } 114 if (!webappDirectory.isDirectory() && !webappDirectory.mkdirs()) { 115 throw new MojoExecutionException("Could not create Webapp directory '" + webappDirectory + "'"); 116 } 117 String source; 118 int lineNo = 0; 119 InputStream inputStream = null; 120 InputStreamReader reader = null; 121 try { 122 if (customRScript.isFile()) { 123 getLog().debug("Using custom r.js from: " + customRScript); 124 inputStream = new FileInputStream(customRScript); 125 } else { 126 getLog().debug("Using bundled r.js"); 127 inputStream = getClass().getResourceAsStream("/org/jszip/maven/r.js"); 128 } 129 source = IOUtil.toString(inputStream, "UTF-8"); 130 if (source.startsWith("#!")) { 131 int i1 = source.indexOf('\n'); 132 int i2 = source.indexOf('\r'); 133 int index = (i1 == -1 || i2 == -1) ? Math.max(i1, i2) : Math.min(i1, i2); 134 if (index > 0) { 135 source = source.substring(index); 136 lineNo++; 137 } 138 } 139 } catch (IOException e) { 140 throw new MojoExecutionException(e.getMessage(), e); 141 } finally { 142 IOUtil.close(reader); 143 IOUtil.close(inputStream); 144 } 145 146 String sourceVersion = "unknown"; 147 Pattern rJsVersionPattern = Pattern.compile(R_JS_VERSION_REGEX); 148 Matcher rJsVersionMatcher = rJsVersionPattern.matcher(source); 149 if (rJsVersionMatcher.find()) { 150 sourceVersion = rJsVersionMatcher.group(1); 151 } 152 153 getLog().info("Using r.js version " + sourceVersion); 154 155 List<PseudoFileSystem.Layer> layers = buildVirtualFileSystemLayers(); 156 157 final ContextFactory contextFactory = new ShellContextFactory(); 158 final Global global = new Global(); 159 global.initQuitAction(new QuitAction() { 160 public void quit(Context context, int exitCode) { 161 if (exitCode != 0) { 162 throw new JavaScriptTerminationException("Script exited with exit code of " + exitCode, exitCode); 163 } 164 } 165 }); 166 if (!global.isInitialized()) { 167 global.init(contextFactory); 168 } 169 DirectoryScanner scanner = new DirectoryScanner(); 170 171 scanner.setBasedir(contentDirectory); 172 173 if (includes != null && !includes.isEmpty()) { 174 scanner.setIncludes(processIncludesExcludes(includes)); 175 } else { 176 scanner.setIncludes(new String[]{"**/*.js"}); 177 } 178 179 if (excludes != null && !excludes.isEmpty()) { 180 scanner.setExcludes(processIncludesExcludes(excludes)); 181 } else { 182 scanner.setExcludes(new String[]{"r.js"}); 183 } 184 185 scanner.scan(); 186 187 for (String path : scanner.getIncludedFiles()) { 188 File profileJs = new File(contentDirectory, path); 189 PseudoFileSystem.Layer[] layersArray = layers.toArray(new PseudoFileSystem.Layer[layers.size() + 1]); 190 layersArray[layers.size()] = new PseudoFileSystem.FileLayer("build", profileJs.getParentFile()); 191 try { 192 Object rv = contextFactory 193 .call(new OptimizeContextAction(getLog(), global, profileJs, source, lineNo, layersArray)); 194 if (rv instanceof Number) { 195 if (((Number) rv).intValue() != 0) { 196 throw new MojoExecutionException( 197 "Non-zero exit code of " + ((Number) rv).intValue() 198 + " when trying to optimize profile " + profileJs); 199 } 200 } 201 } catch (JavaScriptException e) { 202 throw new MojoExecutionException( 203 "Uncaught exception when trying to optimize profile " + profileJs, e); 204 } catch (JavaScriptTerminationException e) { 205 throw new MojoExecutionException( 206 "Non-zero exit code of " + e.getExitCode() + " when trying to optimize profile " + profileJs); 207 } 208 } 209 } 210 211}