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 &lt;exclude&gt; 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}