001    /*
002     * Copyright (C) 2006-2007 the original author or authors.
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    
017    package org.codehaus.gmaven.plugin.execute;
018    
019    import org.apache.maven.artifact.Artifact;
020    import org.apache.maven.artifact.DependencyResolutionRequiredException;
021    import org.apache.maven.execution.MavenSession;
022    import org.apache.maven.plugin.MojoExecutionException;
023    import org.apache.maven.project.MavenProject;
024    import org.apache.maven.settings.Settings;
025    import org.codehaus.gmaven.common.ArtifactItem;
026    import org.codehaus.gmaven.feature.Component;
027    import org.codehaus.gmaven.feature.Configuration;
028    import org.codehaus.gmaven.plugin.ComponentMojoSupport;
029    import org.codehaus.gmaven.runtime.ScriptExecutor;
030    import org.codehaus.gmaven.runtime.loader.realm.RealmManager;
031    import org.codehaus.gmaven.runtime.support.util.ResourceLoaderImpl;
032    import org.codehaus.gmaven.runtime.util.Callable;
033    import org.codehaus.gmaven.runtime.util.ClassSource;
034    import org.codehaus.gmaven.runtime.util.MagicAttribute;
035    import org.codehaus.gmaven.runtime.util.ResourceLoader;
036    import org.codehaus.plexus.classworlds.realm.ClassRealm;
037    import org.slf4j.Logger;
038    import org.slf4j.LoggerFactory;
039    
040    import java.io.File;
041    import java.io.IOException;
042    import java.net.MalformedURLException;
043    import java.net.URL;
044    import java.net.URLClassLoader;
045    import java.util.ArrayList;
046    import java.util.Iterator;
047    import java.util.LinkedHashSet;
048    import java.util.List;
049    import java.util.Map;
050    import java.util.Set;
051    
052    /**
053     * Executes a Groovy script.
054     *
055     * @goal execute
056     * @requiresDependencyResolution test
057     * @configurator override
058     * @since 1.0-alpha-1
059     *
060     * @version $Id: ExecuteMojo.java 88 2009-12-11 11:04:25Z user57 $
061     * @author <a href="mailto:jason@planet57.com">Jason Dillon</a>
062     */
063    public class ExecuteMojo
064        extends ComponentMojoSupport
065    {
066        /** 
067         * The plugin dependencies.
068         *
069         * @parameter expression="${plugin.artifacts}" 
070         * @noinspection UnusedDeclaration
071         * @readonly
072         */
073        private List pluginArtifacts;
074    
075        /**
076         * The source of the script to execute.  This can be a URL, File or script body.
077         *
078         * @parameter
079         * @required
080         *
081         * @noinspection UnusedDeclaration
082         */
083        private Source source;
084    
085        /**
086         * Additional artifacts to add to the scripts classpath.
087         *
088         * @parameter
089         *
090         * @noinspection UnusedDeclaration,MismatchedReadAndWriteOfArray
091         */
092        private ArtifactItem[] classpath;
093    
094        /**
095         * Path to search for imported scripts.
096         *
097         * @parameter
098         *
099         * @noinspection UnusedDeclaration,MismatchedReadAndWriteOfArray
100         */
101        private File[] scriptpath;
102    
103        /**
104         * A set of default project properties, which the values will be used only if
105         * the project or system does not override.
106         *
107         * @parameter
108         *
109         * @noinspection UnusedDeclaration
110         */
111        private Map defaults;
112    
113        /**
114         * A set of additional project properties.
115         * 
116         * @parameter
117         *
118         * @noinspection UnusedDeclaration
119         */
120        private Map properties;
121        
122        /**
123         * Trap assertion errors and rethrow them as execution failures.
124         *
125         * @parameter default-value="true"
126         *
127         * @noinspection UnusedDeclaration
128         */
129        private boolean trapAssertionErrors;
130    
131        /**
132         * Sanatize errors, stripping out Groovy internals.
133         *
134         * @parameter default-value="true"
135         * @since 1.0-beta-3
136         *
137         * @noinspection UnusedDeclaration
138         */
139        private boolean sanitizeErrors;
140    
141        /**
142         * @parameter expression="${session}"
143         * @readonly
144         * @required
145         *
146         * @noinspection UnusedDeclaration
147         */
148        private MavenSession session;
149    
150        /**
151         * @parameter expression="${settings}"
152         * @readonly
153         * @required
154         *
155         * @noinspection UnusedDeclaration
156         */
157        private Settings settings;
158    
159        /**
160         * @component
161         *
162         * @noinspection UnusedDeclaration
163         */
164        private RealmManager realmManager;
165        
166        public ExecuteMojo() {
167            super(ScriptExecutor.KEY);
168        }
169    
170        /**
171         * Allow the script to work with every JAR dependency of both the project and plugin, including
172         * optional and provided dependencies. Runtime classpath elements are loaded first, so that 
173         * legacy behavior is not modified.  Additional elements are added first in the order of 
174         * project artifacts, then in the order of plugin artifacts.
175         */
176        protected List getProjectClasspathElements() throws DependencyResolutionRequiredException {
177            Set results = new LinkedHashSet();
178    
179            for (Iterator i = project.getRuntimeClasspathElements().iterator(); i.hasNext();) {
180                String cpe = (String) i.next();
181                try {
182                    results.add(new File(cpe).getCanonicalPath());
183                }
184                catch (IOException e) {
185                    throw new RuntimeException("Classpath element not found: " + cpe, e);
186                }
187            }
188    
189            for (Iterator i = project.getArtifacts().iterator(); i.hasNext();) {
190                Artifact artifact = (Artifact) i.next();
191                if (artifact.getType().equals("jar") && artifact.getClassifier() == null) {
192                    try {
193                        results.add(artifact.getFile().getCanonicalPath());
194                    }
195                    catch (IOException e) {
196                        throw new RuntimeException("Maven artifact file not found: " + artifact.toString(), e);
197                    }
198                }
199            }
200    
201            for (Iterator i = pluginArtifacts.iterator(); i.hasNext();) {
202                Artifact artifact = (Artifact) i.next();
203                if (artifact.getType().equals("jar") && artifact.getClassifier() == null) {
204                    try {
205                        results.add(artifact.getFile().getCanonicalPath());
206                    }
207                    catch (IOException e) {
208                        throw new RuntimeException("Maven plugin-artifact file not found: " + artifact.toString(), e);
209                    }
210                }
211            }
212            return new ArrayList(results);
213        }
214    
215        protected ArtifactItem[] getUserClassspathElements() {
216            return classpath;
217        }
218    
219        protected void process(final Component component) throws Exception {
220            assert component != null;
221    
222            ScriptExecutor executor = (ScriptExecutor)component;
223    
224            if (source.configuration.getChildCount() != 0) {
225                throw new MojoExecutionException("Invalid value for 'source' parameter; contains nested elements");
226            }
227            
228            ClassSource classSource = ClassSource.forValue(source.configuration.getValue());
229            log.debug("Class source: {}", classSource);
230    
231            ClassRealm realm = realmManager.createComponentRealm(provider(), createClassPath());
232    
233            ResourceLoader resourceLoader = new MojoResourceLoader(realm, classSource);
234    
235            Configuration context = createContext();
236    
237            log.debug("Executing '''{}''' w/context: {}", source, context);
238    
239            Object result = executor.execute(classSource, realm, resourceLoader, context);
240    
241            log.debug("Result: {}", result);
242    
243            realmManager.releaseComponentRealm(realm);
244        }
245    
246        private Configuration createContext() {
247            Configuration context = new Configuration();
248    
249            // Expose logging, give it a new logger that has better chances of being configured if needed
250            // would be nice to get the execution id in here...
251            Logger logger = LoggerFactory.getLogger(project.getGroupId() + "." + project.getArtifactId() + ".ExecuteMojo");
252            context.set("log", logger);
253    
254            // Add a custom project to resolve properties
255            MavenProject projectAdapter = new GroovyMavenProjectAdapter(project, session, properties, defaults);
256            context.set("project", projectAdapter);
257            context.set("pom", projectAdapter);
258    
259            // Stuff in some other Maven bits
260            context.set("session", session);
261            context.set("settings", settings);
262    
263            // Stuff in an Ant helper
264            context.set("ant", MagicAttribute.ANT_BUILDER);
265    
266            // Stuff on a fail helper
267            context.set("fail", new FailClosure());
268    
269            return context;
270        }
271    
272        //
273        // MojoResourceLoader
274        //
275    
276        private class MojoResourceLoader
277            extends ResourceLoaderImpl
278        {
279            private final ClassSource classSource;
280    
281            public MojoResourceLoader(final URLClassLoader classLoader, final ClassSource classSource) {
282                super(classLoader);
283    
284                assert classSource != null;
285    
286                this.classSource = classSource;
287            }
288    
289            protected URL resolve(final String className, final ClassLoader classLoader) throws MalformedURLException {
290                assert className != null;
291                assert classLoader != null;
292    
293                String resource = toResourceName(className);
294    
295                URL url;
296    
297                // First check the scriptpath
298                if (scriptpath != null) {
299                    for (int i=0; i<scriptpath.length; i++) {
300                        assert scriptpath[i] != null;
301    
302                        File file = new File(scriptpath[i], resource);
303    
304                        if (file.exists()) {
305                            return file.toURI().toURL();
306                        }
307                    }
308                }
309    
310                // Then look for a resource in the classpath
311                url = classLoader.getResource(resource);
312    
313                // Try w/o leading '/'... ???  Seems that when loading resources the '/' prefix messes things up?
314                if (url == null) {
315                    if (resource.startsWith("/")) {
316                        String tmp = resource.substring(1, resource.length());
317    
318                        url = classLoader.getResource(tmp);
319                    }
320                }
321    
322                if (url == null) {
323                    // And finally check for a class defined in a file next to the main script file
324                    File script = classSource.file;
325    
326                    if (script != null) {
327                        File file = new File(script.getParentFile(), resource);
328    
329                        if (file.exists()) {
330                            return file.toURI().toURL();
331                        }
332                    }
333                }
334                else {
335                    return url;
336                }
337    
338                return super.resolve(className, classLoader);
339            }
340        }
341    
342        //
343        // FailClosure
344        //
345    
346        private class FailClosure
347            implements Callable
348        {
349            public Object call(final Object[] args) throws Exception {
350                if (args == null || args.length == 0) {
351                    throw new MojoExecutionException("Failed");
352                }
353                else if (args.length == 1) {
354                    if (args[0] instanceof Throwable) {
355                        Throwable cause = (Throwable)args[0];
356                        throw new MojoExecutionException(cause.getMessage(), cause);
357                    }
358                    else {
359                        throw new MojoExecutionException(String.valueOf(args[0]));
360                    }
361                }
362                else if (args.length == 2) {
363                    if (args[1] instanceof Throwable) {
364                        throw new MojoExecutionException(String.valueOf(args[0]), (Throwable)args[1]);
365                    }
366                    else {
367                        throw new Error("Invalid arguments to fail(Object, Throwable), second argument must be a Throwable");
368                    }
369                }
370                else {
371                    throw new Error("Too many arguments for fail(), expected one of: fail(), fail(Object) or fail(Object, Throwable)");
372                }
373            }
374        }
375    }