View Javadoc

1   package org.apache.maven.plugin;
2   
3   /* ====================================================================
4    *   Licensed to the Apache Software Foundation (ASF) under one or more
5    *   contributor license agreements.  See the NOTICE file distributed with
6    *   this work for additional information regarding copyright ownership.
7    *   The ASF licenses this file to You under the Apache License, Version 2.0
8    *   (the "License"); you may not use this file except in compliance with
9    *   the License.  You may obtain a copy of the License at
10   *
11   *       http://www.apache.org/licenses/LICENSE-2.0
12   *
13   *   Unless required by applicable law or agreed to in writing, software
14   *   distributed under the License is distributed on an "AS IS" BASIS,
15   *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   *   See the License for the specific language governing permissions and
17   *   limitations under the License.
18   * ====================================================================
19   */
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.net.MalformedURLException;
25  import java.net.URL;
26  import java.util.ArrayList;
27  import java.util.Collection;
28  import java.util.Collections;
29  import java.util.HashMap;
30  import java.util.HashSet;
31  import java.util.Iterator;
32  import java.util.List;
33  import java.util.Map;
34  import java.util.Set;
35  
36  import org.apache.commons.io.FileUtils;
37  import org.apache.commons.jelly.Script;
38  import org.apache.commons.jelly.expression.Expression;
39  import org.apache.commons.logging.Log;
40  import org.apache.commons.logging.LogFactory;
41  import org.apache.maven.AbstractMavenComponent;
42  import org.apache.maven.AntProjectBuilder;
43  import org.apache.maven.MavenConstants;
44  import org.apache.maven.MavenException;
45  import org.apache.maven.MavenSession;
46  import org.apache.maven.MavenUtils;
47  import org.apache.maven.UnknownGoalException;
48  import org.apache.maven.jelly.JellyUtils;
49  import org.apache.maven.jelly.MavenJellyContext;
50  import org.apache.maven.project.Dependency;
51  import org.apache.maven.project.Project;
52  import org.apache.maven.repository.Artifact;
53  import org.apache.maven.util.Expand;
54  import org.apache.maven.werkz.Goal;
55  import org.apache.maven.werkz.NoSuchGoalException;
56  import org.apache.maven.werkz.Session;
57  import org.apache.maven.werkz.WerkzProject;
58  import org.apache.maven.werkz.jelly.JellySession;
59  
60  import com.werken.forehead.Forehead;
61  import com.werken.forehead.ForeheadClassLoader;
62  
63  /*
64  
65   NOTES:
66  
67   We initialize the plugin with an empty context. When we are finished initializing the
68   plugins we will have a context with a werkz project object that has been created
69   that will contain all the goals present within the plugins.
70  
71   The result of this initialization is the creation of all the cache files which
72   give us an outline of what is required to attain a particular goal.
73  
74   When a particular project needs to attain one of these goals we need to
75   load the plugin and in doing so we run the plugin.jelly script for the required
76   plugins against the projects context.
77  
78   Possibly eventually we could make this more efficient storing the plugin.jelly
79   in an accessible templated form which when required for a particular project
80   can be executed against the project's context.
81  
82   IMPLEMENTATION NOTES
83  
84   - We need to know the goals to attain
85   - We need to keep track of plugins that have been loaded for a particular project.
86  
87   */
88  
89  /**
90   * Plugin manager for MavenSession.
91   * <p/>
92   * The <code>PluginManager</code> deals with all aspects of a plugins lifecycle.
93   * <p/>
94   * This is <b>not</b> thread safe.
95   *
96   * @author <a href="mailto:jason@zenplex.com">Jason van Zyl</a>
97   * @author <a href="mailto:bob@eng.werken.com">bob mcwhirter</a>
98   * @author <a href="mailto:brett@apache.org">Brett Porter</a>
99   * @version $Id: PluginManager.java 517014 2007-03-11 21:15:50Z ltheussl $
100  */
101 public class PluginManager
102     extends AbstractMavenComponent
103 {
104     /**
105      * Logger
106      */
107     private static final Log LOGGER = LogFactory.getLog( PluginManager.class );
108 
109     /**
110      * The variable key that holds an implementation of the
111      * <code>com.werken.werkz.Session</code> in the parent scope context.
112      */
113     public static final String GLOBAL_SESSION_KEY = "maven.session.global";
114 
115     public static final String PLUGIN_MANAGER = "maven.plugin.manager";
116 
117     public static final String PLUGIN_HOUSING = "maven.plugin.script.housing";
118 
119     public static final String GOAL_MAPPER = "maven.plugin.mapper";
120 
121     /** */
122     public static final String BASE_CONTEXT = "maven.goalAttainmentContext";
123 
124     /**
125      * The directory where plugin jars reside under Maven's home.
126      */
127     private File pluginsDir;
128 
129     /**
130      * The directory where the plugin jars are unpacked to.
131      */
132     private File unpackedPluginsDir;
133 
134     /**
135      * The directory where the user's plugin jars are installed.
136      */
137     private File userPluginsDir;
138 
139     /**
140      * This contains a map of plugins, keyed by id.
141      */
142     private final Map pluginHousings = new HashMap();
143 
144     /**
145      * This contains a map of plugins, keyed by artifact id.
146      */
147     private final Map artifactIdToHousingMap = new HashMap();
148 
149     /**
150      * Maven session reference.
151      */
152     private MavenSession mavenSession;
153 
154     /**
155      * Plugin cache manager.
156      */
157     private final PluginCacheManager cacheManager = new PluginCacheManager();
158 
159     /**
160      * Goal to Plugins mapper.
161      */
162     private GoalToJellyScriptHousingMapper mapper = new GoalToJellyScriptHousingMapper();
163 
164     /**
165      * Current plugins mapper (transient - includes maven.xml, etc). *
166      */
167     private GoalToJellyScriptHousingMapper transientMapper = mapper;
168 
169     /**
170      * Plugins to be popped afterwards.
171      */
172     private Set delayedPops = new HashSet();
173 
174     /**
175      * The current goal attainment base context.
176      */
177     private MavenJellyContext baseContext;
178 
179     private static final String PLUGIN_TEMP_MAP = "PluginManager.PLUGIN_TEMP_MAP";
180 
181     /**
182      * Default constructor.
183      *
184      * @param session The MavenSession this plugin manager will use
185      *                until Maven shuts down.
186      */
187     public PluginManager( MavenSession session )
188     {
189         mavenSession = session;
190     }
191 
192     /**
193      * Get the list of plugin files.
194      */
195     private Map getPluginFiles( File directory, boolean acceptDirectories )
196     {
197         File[] files = directory.listFiles();
198         if ( files == null )
199         {
200             return Collections.EMPTY_MAP;
201         }
202 
203         Map pluginFiles = new HashMap();
204         for ( int i = 0; i < files.length; i++ )
205         {
206             String plugin = files[i].getName();
207             if ( files[i].isDirectory() && acceptDirectories )
208             {
209                 pluginFiles.put( plugin, files[i] );
210             }
211             else
212             {
213                 int index = plugin.indexOf( ".jar" );
214                 if ( index >= 0 )
215                 {
216                     String name = plugin.substring( 0, index );
217                     pluginFiles.put( name, files[i] );
218                 }
219             }
220         }
221         return pluginFiles;
222     }
223 
224     /**
225      * Load plugins.
226      *
227      * @throws MavenException when the plugin jars can't be expanded
228      */
229     private void loadUncachedPlugins( Map pluginFiles )
230         throws IOException, MavenException
231     {
232         LOGGER.debug( "Now loading uncached plugins" );
233 
234         for ( Iterator i = pluginFiles.keySet().iterator(); i.hasNext(); )
235         {
236             String name = (String) i.next();
237             File pluginDir = (File) pluginFiles.get( name );
238 
239             if ( !isLoaded( name ) )
240             {
241                 JellyScriptHousing housing = createPluginHousing( pluginDir );
242                 if ( housing != null )
243                 {
244                     cacheManager.registerPlugin( name, housing );
245                     housing.parse( cacheManager );
246                     housing.parse( mapper );
247                 }
248             }
249         }
250     }
251 
252     /**
253      * Initialize all plugins.
254      */
255     public void initialize()
256         throws IOException, MavenException
257     {
258         if ( LOGGER.isDebugEnabled() )
259         {
260             LOGGER.debug( "Initializing Plugins!" );
261         }
262 
263         setPluginsDir( new File( mavenSession.getRootContext().getPluginsDir() ) );
264         setUnpackedPluginsDir( new File( mavenSession.getRootContext().getUnpackedPluginsDir() ) );
265         setUserPluginsDir( new File( mavenSession.getRootContext().getUserPluginsDir() ) );
266 
267         if ( !getPluginsDir().isDirectory()
268             || ( ( getPluginsDir().listFiles() != null ) && ( getPluginsDir().listFiles().length == 0 ) ) )
269         {
270             throw new MavenException( "Maven was badly installed. Please reinstall it." );
271         }
272 
273         if ( LOGGER.isDebugEnabled() )
274         {
275             LOGGER.debug( "Set plugin source directory to " + getPluginsDir().getAbsolutePath() );
276             LOGGER.debug( "Set unpacked plugin directory to " + getUnpackedPluginsDir().getAbsolutePath() );
277             LOGGER.debug( "Set user plugin directory to " + getUserPluginsDir().getAbsolutePath() );
278         }
279 
280         verifyUnpackedPluginsDir();
281 
282         // plugin profile at this point is all of the JAR files in the plugins directory and the unpacked plugins dir
283         // future ideas: have installation, user (outside of the cache), and project (via deps) plugins
284         // allow further customisation via a profile descriptor.
285 
286         Map pluginFiles = getPluginFiles( pluginsDir, true );
287         Map userPluginFiles = getPluginFiles( userPluginsDir, false );
288 
289         if ( !userPluginFiles.isEmpty() )
290         {
291             pluginFiles.putAll( userPluginFiles );
292         }
293 
294         cacheManager.loadCache( unpackedPluginsDir );
295 
296         Map pluginDirs = expandPluginFiles( pluginFiles );
297 
298         LOGGER.debug( "Now mapping cached plugins" );
299         if ( !cacheManager.mapPlugins( mapper, this, pluginDirs ) )
300         {
301             LOGGER.info( "Cache invalidated due to out of date plugins" );
302             // The following housings are considered loaded - so go through and cache them manually
303             for ( Iterator i = pluginHousings.values().iterator(); i.hasNext(); )
304             {
305                 JellyScriptHousing housing = (JellyScriptHousing) i.next();
306                 cacheManager.registerPlugin( housing.getName(), housing );
307                 housing.parse( cacheManager );
308                 housing.parse( mapper );
309             }
310         }
311 
312         loadUncachedPlugins( pluginDirs );
313 
314         cacheManager.saveCache( unpackedPluginsDir );
315 
316         LOGGER.debug( "Finished initializing Plugins!" );
317     }
318 
319     private Map expandPluginFiles( Map pluginFiles )
320         throws MavenException
321     {
322         Map pluginDirs = new HashMap();
323         for ( Iterator i = pluginFiles.keySet().iterator(); i.hasNext(); )
324         {
325             String name = (String) i.next();
326             File jarFile = (File) pluginFiles.get( name );
327             File dir = jarFile;
328             if ( !dir.isDirectory() )
329             {
330                 dir = unpackPlugin( name, jarFile, true );
331             }
332             pluginDirs.put( name, dir );
333         }
334         return pluginDirs;
335     }
336 
337     JellyScriptHousing loadPluginHousing( String name, File pluginDir )
338         throws IOException
339     {
340         JellyScriptHousing housing = (JellyScriptHousing) pluginHousings.get( name );
341         return ( housing == null ? createLazyPluginHousing( pluginDir ) : housing );
342     }
343 
344     private JellyScriptHousing createPluginHousing( File pluginDir )
345         throws MavenException, IOException
346     {
347         JellyScriptHousing housing = createLazyPluginHousing( pluginDir );
348         if ( housing != null )
349         {
350             String artifactId = housing.getProject().getArtifactId();
351             mapArtifactIdToPluginHousing( artifactId, housing );
352         }
353         return housing;
354     }
355 
356     void mapArtifactIdToPluginHousing( String artifactId, JellyScriptHousing housing )
357     {
358         if ( artifactIdToHousingMap.containsKey( artifactId ) )
359         {
360             JellyScriptHousing h = (JellyScriptHousing) artifactIdToHousingMap.get( artifactId );
361             LOGGER.warn( "WARNING: Plugin '" + artifactId + "' is already loaded from " + h.getName()
362                 + "; attempting to load " + housing.getName() );
363         }
364         artifactIdToHousingMap.put( artifactId, housing );
365     }
366 
367     private JellyScriptHousing createLazyPluginHousing( File pluginDir )
368         throws IOException
369     {
370         if ( !pluginDir.isDirectory() || !new File( pluginDir, "project.xml" ).exists() )
371         {
372             LOGGER.debug( "Not a plugin directory: " + pluginDir );
373             return null;
374         }
375 
376         String pluginName = pluginDir.getName();
377 
378         LOGGER.debug( "Loading plugin '" + pluginName + "'" );
379 
380         JellyScriptHousing jellyScriptHousing = new JellyScriptHousing( pluginDir, mavenSession.getRootContext() );
381 
382         pluginHousings.put( pluginName, jellyScriptHousing );
383 
384         return jellyScriptHousing;
385     }
386 
387     private boolean isLoaded( String name )
388     {
389         return pluginHousings.containsKey( name );
390     }
391 
392     /**
393      * @param project
394      * @param jelly
395      * @param systemId the system identifier to help resolve relative URLs
396      * @return JellyScriptHousing
397      * @throws Exception
398      * @deprecated get rid of this - it duplicates functionality in the housing
399      * @todo refactor into housing
400      * @todo don't throw Exception
401      * @todo get rid of this - it duplicates functionality in the housing
402      */
403     private JellyScriptHousing createJellyScriptHousing( Project project, String systemId, InputStream jelly )
404         throws Exception
405     {
406         JellyScriptHousing jellyScriptHousing = new JellyScriptHousing();
407 
408         try
409         {
410             // Now lets compile the script once so we can use it repeatedly.
411             Script script = JellyUtils.compileScript( jelly, systemId, project.getContext() );
412 
413             jellyScriptHousing.setProject( project );
414             jellyScriptHousing.setScript( script );
415         }
416         catch ( Exception e )
417         {
418             // FIXME: exception? Yuck!
419             throw new MavenException( "Error parsing driver.jelly", e );
420         }
421 
422         return jellyScriptHousing;
423     }
424 
425     /**
426      * @param project
427      * @param jelly
428      * @return JellyScriptHousing
429      * @todo [1.0] into the housing?
430      */
431     private JellyScriptHousing createJellyScriptHousing( Project project, File jelly )
432     {
433         JellyScriptHousing jellyScriptHousing = new JellyScriptHousing();
434 
435         jellyScriptHousing.setProject( project );
436         jellyScriptHousing.setSource( jelly );
437 
438         return jellyScriptHousing;
439     }
440 
441     /**
442      * Process the dependencies of the project, adding dependencies to the
443      * appropriate classloader etc
444      *
445      * @throws MalformedURLException if a file can't be converted to a URL.
446      * @throws Exception             for any other issue. FIXME
447      */
448     public void processDependencies( Project project )
449         throws MalformedURLException, Exception
450     {
451         if ( ( project.getArtifacts() == null ) || project.getArtifacts().isEmpty() )
452         {
453             LOGGER.debug( "No dependencies to process for project " + project.getName() );
454             return;
455         }
456 
457         ClassLoader cl = project.getContext().getClassLoader();
458         if ( !( cl instanceof ForeheadClassLoader ) )
459         {
460             return;
461         }
462 
463         ForeheadClassLoader projectClassLoader = (ForeheadClassLoader) cl;
464         LOGGER.debug( "Processing dependencies for project " + project.getName() + "; classloader " + projectClassLoader );
465 
466         // add the dependencies to the classpath
467         for ( Iterator i = project.getArtifacts().iterator(); i.hasNext(); )
468         {
469             Artifact artifact = (Artifact) i.next();
470             Dependency dependency = artifact.getDependency();
471             if ( dependency.isPlugin() )
472             {
473                 // TODO: is this the best place to call this?
474                 installPlugin( artifact.getFile(), project );
475             }
476 
477             // get correct classloader
478             String dependencyClassLoader = dependency.getProperty( "classloader" );
479 
480             // add to classloader
481             if ( artifact.exists() )
482             {
483                 // Only add compile type dependencies to classloader
484                 // what about ejbs etc
485                 if ( dependency.isAddedToClasspath() )
486                 {
487                     if ( dependencyClassLoader != null )
488                     {
489                         LOGGER.debug( "DEPRECATION: " + dependency.getId() + " in project " + project.getId()
490                             + " forces the classloader '" + dependencyClassLoader + "'" );
491                         LOGGER.debug( "             This behaviour is deprecated. Please refer to the FAQ" );
492                         ForeheadClassLoader loader = Forehead.getInstance().getClassLoader( dependencyClassLoader );
493                         if ( loader == null )
494                         {
495                             LOGGER.warn( "classloader '" + dependencyClassLoader
496                                 + "' not found. Adding dependencies to the project classloader instead" );
497                             loader = projectClassLoader;
498                         }
499                         else
500                         {
501                             LOGGER.debug( "poking dependency " + artifact.getFile() + " into classloader "
502                                 + dependencyClassLoader );
503                         }
504                         loader.addURL( artifact.getFile().toURL() );
505                     }
506                     else
507                     {
508                         LOGGER.debug( "adding dependency " + artifact.getFile() + " into project classloader" );
509                         projectClassLoader.addURL( artifact.getFile().toURL() );
510                     }
511                 }
512                 else
513                 {
514                     LOGGER.debug( "Non classpath dependency: '" + artifact.getFile() + "' not added to classpath" );
515                 }
516             }
517             else
518             {
519                 LOGGER.info( "Artifact '" + artifact.getFile() + "' not found to add to classpath" );
520             }
521         }
522         // Explicity set the classloader used to find resources. As we just
523         // poked all the dependencies into the classloader.
524         project.getContext().setClassLoader( projectClassLoader );
525     }
526 
527     List readMavenXml( Project project, GoalToJellyScriptHousingMapper mapper )
528         throws MavenException
529     {
530         Project p = project;
531         List projectHousings = new ArrayList();
532 
533         // Project's Jelly script
534         while ( p != null )
535         {
536             if ( p.hasMavenXml() )
537             {
538                 File mavenXml = p.getMavenXml();
539 
540                 JellyScriptHousing jellyScriptHousing = createJellyScriptHousing( project, mavenXml );
541                 jellyScriptHousing.parse( mapper );
542                 projectHousings.add( jellyScriptHousing );
543             }
544             p = p.getParent();
545         }
546         return projectHousings;
547     }
548 
549     MavenJellyContext setupBaseContext( Project project )
550     {
551         MavenJellyContext prevBaseContext = baseContext;
552         baseContext = new MavenJellyContext( mavenSession.getRootContext() );
553         baseContext.setInherit( true );
554         JellyUtils.populateVariables( baseContext, project.getContext() );
555         project.pushContext( baseContext );
556         baseContext.setProject( project );
557 
558         return prevBaseContext;
559     }
560 
561     /**
562      * Attain the goals.
563      *
564      * @throws Exception If one of the specified
565      *                   goals refers to an non-existent goal.
566      * @throws Exception If an exception occurs while running a goal.
567      * @todo stop throwing Exception
568      */
569     public void attainGoals( Project project, List goals )
570         throws Exception
571     {
572         MavenJellyContext prevBaseContext = setupBaseContext( project );
573 
574         // Set up the ant project.
575         AntProjectBuilder.build( project, baseContext );
576 
577         // TODO: shouldn't this be a stack too? Then session attribute not needed
578         transientMapper = new GoalToJellyScriptHousingMapper();
579 
580         // Create the Jelly session
581         Session session = new JellySession( baseContext.getXMLOutput() );
582         session.setAttribute( BASE_CONTEXT, baseContext );
583         session.setAttribute( PLUGIN_MANAGER, this );
584         session.setAttribute( GOAL_MAPPER, transientMapper );
585 
586         // add the global session to the pluginContext so that it can be used by tags
587         baseContext.setVariable( GLOBAL_SESSION_KEY, session );
588 
589         // Execution of the Jelly scripts:
590         //
591         // We run the Jelly scripts in the following order:
592         //
593         // 1) driver.jelly - should be removed
594         // 2) project's maven.xml
595         // 3) parent's maven.xml (if it exists)
596         // 4) plugin.jelly
597         //
598         // The Maven version of the <goal/> Werkz tag has been constructed so that the first
599         // definition of a goal wins.
600         //
601         // We run them in this order because we do not know before hand which plugin goals a project
602         // may wish to override so we guarantee precedence of the goals by running the jelly scripts
603         // in the above mentioned order.
604 
605         // driver.jelly
606         InputStream driver = getClass().getResourceAsStream( "/driver.jelly" );
607         JellyScriptHousing driverHousing = createJellyScriptHousing( project, getClass().getResource( "/driver.jelly" )
608             .toString(), driver );
609         // TODO: stop reading all scripts 2 times
610         driver.close();
611         driver = getClass().getResourceAsStream( "/driver.jelly" );
612         driverHousing.parse( transientMapper, null, driver );
613         driver.close();
614 
615         List projectHousings = readMavenXml( project, transientMapper );
616 
617         if ( goals != null )
618         {
619             for ( Iterator i = goals.iterator(); i.hasNext(); )
620             {
621                 String goal = (String) i.next();
622                 if ( goal.trim().length() == 0 )
623                 {
624                     i.remove();
625                 }
626             }
627         }
628 
629         // Default goal handling - if goals are null, don't even process the default
630         String defaultGoalName = transientMapper.getDefaultGoalName();
631 
632         if ( ( project.getBuild() != null ) && ( project.getBuild().getDefaultGoal() != null ) )
633         {
634             defaultGoalName = project.getBuild().getDefaultGoal();
635         }
636 
637         if ( defaultGoalName != null )
638         {
639             // By evaluating expression now it has the same scope as the POM.
640             Expression e = JellyUtils.decomposeExpression( defaultGoalName, baseContext );
641             defaultGoalName = e.evaluateAsString( baseContext );
642             baseContext.setVariable( MavenConstants.DEFAULT_GOAL, defaultGoalName );
643 
644             if ( ( goals != null ) && ( goals.size() == 0 ) )
645             {
646                 LOGGER.debug( "Using default goal: " + defaultGoalName );
647                 goals.add( defaultGoalName );
648             }
649         }
650         if ( goals == null )
651         {
652             // So the reactor can process projects but not run any goals
653             goals = Collections.EMPTY_LIST;
654         }
655         else
656         {
657             // Always run build:start and build:end
658             goals.add( 0, "build:start" );
659             goals.add( "build:end" );
660         }
661 
662         transientMapper.merge( mapper );
663 
664         WerkzProject werkzProject = new WerkzProject();
665         baseContext.setWerkzProject( werkzProject );
666 
667         Set pluginSet = new HashSet();
668         // poor mans stack - only pop when you finish the same frame that the plugin was lazily init in
669         Set oldDelayedPops = new HashSet( delayedPops );
670         delayedPops.clear();
671 
672         Thread.currentThread().setContextClassLoader( null );
673 
674         try
675         {
676             runScript( driverHousing, baseContext );
677             transientMapper.addResolvedPlugins( Collections.singletonList( driverHousing ) );
678 
679             // Dependencies must be processed after the driver is run for compatibility
680             // FIXME: From attainGoals angle, how does it know the project needs to
681             //        to be verified, or that the project object hasn't been used before
682             project.verifyDependencies();
683             processDependencies( project );
684 
685             for ( Iterator j = projectHousings.iterator(); j.hasNext(); )
686             {
687                 JellyScriptHousing housing = (JellyScriptHousing) j.next();
688                 runScript( housing, baseContext );
689             }
690             transientMapper.addResolvedPlugins( projectHousings );
691 
692             // Plugin Jelly scripts
693             for ( Iterator i = goals.iterator(); i.hasNext(); )
694             {
695                 String goalName = (String) i.next();
696 
697                 pluginSet.addAll( prepAttainGoal( goalName, baseContext, transientMapper ) );
698             }
699 
700             // Plugin Jelly scripts
701             for ( Iterator i = goals.iterator(); i.hasNext(); )
702             {
703                 String goalName = (String) i.next();
704                 LOGGER.debug( "attaining goal " + goalName );
705                 try
706                 {
707                     Goal goal = werkzProject.getGoal( goalName );
708                     if ( ( goal == null ) || ( goal.getAction() == null ) )
709                     {
710                         throw new NoSuchGoalException( goalName );
711                     }
712                     goal.attain( session );
713                 }
714                 catch ( NoSuchGoalException e )
715                 {
716                     throw new UnknownGoalException( goalName );
717                 }
718             }
719         }
720         finally
721         {
722             cleanupAttainGoal( pluginSet );
723             delayedPops = oldDelayedPops;
724             reinstallPlugins( project.getContext() );
725             project.popContext();
726             baseContext = prevBaseContext;
727         }
728     }
729 
730     /**
731      * @todo don't throw Exception
732      */
733     public void cleanupAttainGoal( Set pluginSet )
734         throws Exception
735     {
736         delayedPops.addAll( pluginSet );
737 
738         for ( Iterator j = delayedPops.iterator(); j.hasNext(); )
739         {
740             JellyScriptHousing housing = (JellyScriptHousing) j.next();
741 
742             reinstallPlugins( housing.getProject().getContext() );
743 
744             housing.getProject().popContext();
745         }
746         delayedPops.clear();
747     }
748 
749     /**
750      * Use the name of a goal to lookup all the plugins (that are stored in the plugin housings) that need to be
751      * executed in order to satisfy all the required preconditions for successful goal attainment.
752      *
753      * @param goalName    the goal
754      * @param baseContext the base context to attain in
755      * @return a set of plugins required to attain the goal
756      * @throws Exception
757      * @todo don't throw Exception
758      */
759     public Set prepAttainGoal( String goalName, MavenJellyContext baseContext, GoalToJellyScriptHousingMapper goalMapper )
760         throws Exception
761     {
762         Set pluginSet = goalMapper.resolveJellyScriptHousings( goalName );
763 
764         for ( Iterator j = pluginSet.iterator(); j.hasNext(); )
765         {
766             JellyScriptHousing housing = (JellyScriptHousing) j.next();
767             initialiseHousingPluginContext( housing, baseContext );
768         }
769         return pluginSet;
770     }
771 
772     private MavenJellyContext initialiseHousingPluginContext( JellyScriptHousing housing, MavenJellyContext baseContext )
773         throws Exception
774     {
775         Project project = housing.getProject();
776 
777         // TODO: I think this should merge into pluginContext, but that seems to break existing jelly as it depends on
778         // that kind of warped scoping. Fix this in 1.1
779         MavenUtils.integrateMapInContext( housing.getPluginProperties(), baseContext );
780 
781         /*
782          // Instead, we must go through each property and merge from previous plugin contexts, or the original property
783          // first integrate new ones
784          MavenUtils.integrateMapInContext( project.getContext().getVariables(), baseContext );
785          // now integrate properties that have a new value
786          Properties p = new Properties();
787          for ( Iterator i = housing.getPluginProperties().keySet().iterator(); i.hasNext(); )
788          {
789          String key = (String) i.next();
790          Object value = project.getContext().getVariable( key );
791          if ( value != null )
792          {
793          baseContext.setVariable( key, value );
794          }
795          else
796          {
797          p.setProperty( key, (String) housing.getPluginProperties().get( key ) );
798          }
799          }
800          MavenUtils.integrateMapInContext( p, baseContext );
801          // but leave alone anything in there that is not a plugin property, and already exists in baseContext
802          */
803         // TODO necessary to create a new one every time?
804         MavenJellyContext pluginContext = new MavenJellyContext( baseContext );
805         project.pushContext( pluginContext );
806         pluginContext.setInherit( true );
807         pluginContext.setVariable( "context", pluginContext );
808         pluginContext.setVariable( "plugin", project );
809         pluginContext.setVariable( "plugin.dir", housing.getPluginDirectory() );
810         pluginContext.setVariable( "plugin.resources", new File( housing.getPluginDirectory(), "plugin-resources" ) );
811 
812         LOGGER.debug( "initialising plugin housing: " + project );
813         runScript( housing, pluginContext );
814 
815         return pluginContext;
816     }
817 
818     /**
819      * Sets the pluginsDir attribute of the PluginManager object
820      *
821      * @param dir The maven plugin directory.
822      */
823     private void setPluginsDir( File dir )
824     {
825         pluginsDir = dir;
826     }
827 
828     /**
829      * Retrieve the directory containing all plugins.
830      *
831      * @return The directory containing all plugins.
832      */
833     private File getPluginsDir()
834     {
835         return pluginsDir;
836     }
837 
838     /**
839      * Sets the directory where the users plugins are located.
840      *
841      * @param dir The directory where the users plugins are located.
842      */
843     private void setUserPluginsDir( File dir )
844     {
845         userPluginsDir = dir;
846     }
847 
848     /**
849      * Gets the directory where the user plugins are located.
850      *
851      * @return the directory where the user plugins are located.
852      */
853     private File getUserPluginsDir()
854     {
855         return userPluginsDir;
856     }
857 
858     /**
859      * Sets the directory where the unpacked plugins are located.
860      *
861      * @param dir The directory where the unpacked plugins are located.
862      */
863     private void setUnpackedPluginsDir( File dir )
864     {
865         unpackedPluginsDir = dir;
866     }
867 
868     /**
869      * Sets the directory where the unpacked plugins are located.
870      *
871      * @return the directory where the unpacked plugins are located.
872      */
873     private File getUnpackedPluginsDir()
874     {
875         return unpackedPluginsDir;
876     }
877 
878     /**
879      * @return Set
880      */
881     public Set getGoalNames()
882     {
883         return mapper.getGoalNames();
884     }
885 
886     /**
887      * Warning - this completely scrogs the default mapper. Only use this before System.exit!
888      * (currently used by maven -u).
889      *
890      * @return Set
891      * @todo refactor to return mapper instead and use that, or perhaps instantiate a new plugin manager
892      */
893     public Set getGoalNames( Project project )
894         throws MavenException
895     {
896         mapper = new GoalToJellyScriptHousingMapper();
897         readMavenXml( project, mapper );
898         return mapper.getGoalNames();
899     }
900 
901     /**
902      */
903     public void installPlugin( File file, Project parentProject )
904         throws MavenException
905     {
906         // By default, don't copy to the unpacked plugins directory - only use this dependency for this project
907         installPlugin( file, parentProject, false );
908     }
909 
910     /**
911      * Load and install a plugin.
912      *
913      * @param file          the file to install. Must be a plugin jar
914      * @param parentProject the project to load the installed plugin into
915      */
916     public void installPlugin( File file, Project parentProject, boolean cache )
917         throws MavenException
918     {
919         LOGGER.debug( "Using plugin file: " + file );
920         try
921         {
922             String pluginName = file.getCanonicalFile().getName();
923             pluginName = pluginName.substring( 0, pluginName.indexOf( ".jar" ) );
924 
925             if ( isLoaded( pluginName ) )
926             {
927                 // already installed this version
928                 return;
929             }
930 
931             // expand it
932             File unpackedPluginDir = unpackPlugin( pluginName, file, cache );
933             if ( unpackedPluginDir != null )
934             {
935                 JellyScriptHousing housing = createLazyPluginHousing( unpackedPluginDir );
936                 if ( housing != null )
937                 {
938                     String artifactId = housing.getProject().getArtifactId();
939                     if ( artifactIdToHousingMap.containsKey( artifactId ) )
940                     {
941                         // old version
942                         JellyScriptHousing oldHousing = (JellyScriptHousing) artifactIdToHousingMap.get( artifactId );
943                         LOGGER.debug( "Temporarily uninstalling: " + oldHousing );
944                         addPluginToReinstall( parentProject.getContext(), artifactId, oldHousing );
945                         pluginHousings.remove( oldHousing.getName() );
946                         mapper.invalidatePlugin( oldHousing );
947                         transientMapper.invalidatePlugin( oldHousing );
948                         artifactIdToHousingMap.remove( artifactId );
949                     }
950 
951                     mapArtifactIdToPluginHousing( artifactId, housing );
952                 }
953                 else
954                 {
955                     throw new MavenException( "Not a valid plugin file: " + file );
956                 }
957 
958                 LOGGER.debug( "Installing plugin: " + housing );
959                 // By default, not caching the plugin - its a per execution installation
960                 housing.parse( transientMapper );
961                 // Should only be putting in the transientMapper - but it is not consistent with isLoaded
962                 housing.parse( mapper );
963                 if ( cache )
964                 {
965                     FileUtils.copyFileToDirectory( file, userPluginsDir );
966                     cacheManager.registerPlugin( pluginName, housing );
967                     housing.parse( cacheManager );
968                     cacheManager.saveCache( unpackedPluginsDir );
969                 }
970             }
971             else
972             {
973                 throw new MavenException( "Not a valid JAR file: " + file );
974             }
975         }
976         catch ( IOException e )
977         {
978             throw new MavenException( "Error installing plugin", e );
979         }
980     }
981 
982     /**
983      * @todo can this be removed and simplified if transient mapper contains the plugin mapping only?
984      */
985     private void addPluginToReinstall( MavenJellyContext context, String artifactId, JellyScriptHousing housing )
986     {
987         Map m = (Map) context.getVariables().get( PLUGIN_TEMP_MAP );
988         if ( m == null )
989         {
990             m = new HashMap();
991             context.setVariable( PLUGIN_TEMP_MAP, m );
992         }
993         m.put( artifactId, housing );
994     }
995 
996     /**
997      * @todo can this be removed and simplified if transient mapper contains the plugin mapping only?
998      */
999     private void reinstallPlugins( MavenJellyContext context )
1000         throws MavenException
1001     {
1002         Map m = (Map) context.getVariables().get( PLUGIN_TEMP_MAP );
1003         if ( m != null )
1004         {
1005             for ( Iterator i = m.keySet().iterator(); i.hasNext(); )
1006             {
1007                 String artifactId = (String) i.next();
1008                 JellyScriptHousing housing = (JellyScriptHousing) m.get( artifactId );
1009                 pluginHousings.remove( housing.getName() );
1010                 mapper.invalidatePlugin( housing );
1011                 transientMapper.invalidatePlugin( housing );
1012                 housing = (JellyScriptHousing) m.get( artifactId );
1013                 LOGGER.debug( "Reinstalling: " + housing );
1014                 housing.parse( transientMapper );
1015                 housing.parse( mapper );
1016             }
1017         }
1018     }
1019 
1020     public void uninstallPlugin( String artifactId )
1021         throws IOException
1022     {
1023         LOGGER.debug( "Uninstalling plugin: " + artifactId );
1024 
1025         JellyScriptHousing housing = (JellyScriptHousing) artifactIdToHousingMap.get( artifactId );
1026         if ( housing == null )
1027         {
1028             LOGGER.warn( "Plugin not found when attempting to uninstall '" + artifactId + "'" );
1029             return;
1030         }
1031 
1032         String name = housing.getName();
1033         pluginHousings.remove( name );
1034         cacheManager.invalidateCache( name );
1035         mapper.invalidatePlugin( housing );
1036         transientMapper.invalidatePlugin( housing );
1037         artifactIdToHousingMap.remove( artifactId );
1038         cacheManager.saveCache( unpackedPluginsDir );
1039     }
1040 
1041     /**
1042      * @param id
1043      * @return MavenJellyContext
1044      * @throws UnknownPluginException
1045      * @todo [1.0] refactor out, or make more appropriate structure
1046      * @todo remove throws Exception
1047      */
1048     public MavenJellyContext getPluginContext( String id )
1049         throws MavenException, UnknownPluginException
1050     {
1051         JellyScriptHousing housing = (JellyScriptHousing) artifactIdToHousingMap.get( id );
1052         if ( housing != null )
1053         {
1054             Project project = housing.getProject();
1055             if ( baseContext.equals( project.getContext().getParent() ) )
1056             {
1057                 LOGGER.debug( "Plugin context for " + id + " already initialised for this base context" );
1058                 return project.getContext();
1059             }
1060             else
1061             {
1062                 LOGGER.debug( "Plugin context for " + id
1063                     + " not initialised for this base context: initialising inside getPluginContext" );
1064                 try
1065                 {
1066                     return initialiseHousingPluginContext( housing, baseContext );
1067                 }
1068                 catch ( Exception e )
1069                 {
1070                     throw new MavenException( "Error initialising plugin context", e );
1071                 }
1072             }
1073         }
1074         throw new UnknownPluginException( id );
1075     }
1076 
1077     public String getGoalDescription( String goalName )
1078     {
1079         return mapper.getGoalDescription( goalName );
1080     }
1081 
1082     public void addDelayedPops( Set set )
1083     {
1084         delayedPops.addAll( set );
1085     }
1086 
1087     /**
1088      * Unpack the plugin.
1089      *
1090      * @throws MavenException if there was a problem unpacking
1091      */
1092     File unpackPlugin( String pluginName, File jarFile, boolean cache )
1093         throws MavenException
1094     {
1095         File unzipDir = new File( unpackedPluginsDir, pluginName );
1096 
1097         // if there's no directory, or the jar is newer, expand the jar
1098         boolean exists = unzipDir.exists();
1099         if ( !exists || ( jarFile.lastModified() > unzipDir.lastModified() ) )
1100         {
1101             if ( LOGGER.isDebugEnabled() )
1102             {
1103                 LOGGER.debug( "Unpacking " + jarFile.getName() + " to directory --> " + unzipDir.getAbsolutePath() );
1104             }
1105 
1106             if ( cache )
1107             {
1108                 cacheManager.invalidateCache( pluginName );
1109             }
1110 
1111             try
1112             {
1113                 if ( exists )
1114                 {
1115                     FileUtils.deleteDirectory( unzipDir );
1116                 }
1117 
1118                 Expand unzipper = new Expand();
1119                 unzipper.setSrc( jarFile );
1120                 unzipper.setDest( unzipDir );
1121                 unzipper.execute();
1122             }
1123             catch ( IOException e )
1124             {
1125                 throw new MavenException( "Unable to extract plugin: " + jarFile, e );
1126             }
1127         }
1128         return unzipDir;
1129     }
1130 
1131     /**
1132      * @return Script
1133      * @todo get rid of throws Exception
1134      */
1135     private Script loadScript( JellyScriptHousing jellyScriptHousing )
1136         throws Exception
1137     {
1138         // TODO [1.0]: this currently duplicates createJellyScriptHousing for others - it is the lazy version
1139 
1140         // We will add the plugin classes to the plugin class loader.
1141         if ( jellyScriptHousing.getPluginDirectory() != null )
1142         {
1143             // not needed for maven.xml
1144             // TODO: should differentiate between plugins and script housings better
1145             jellyScriptHousing.getProject().verifyDependencies();
1146             processDependencies( jellyScriptHousing.getProject() );
1147             ClassLoader cl = jellyScriptHousing.getProject().getContext().getClassLoader();
1148             if ( cl instanceof ForeheadClassLoader )
1149             {
1150                 ForeheadClassLoader pluginClassLoader = (ForeheadClassLoader) cl;
1151                 pluginClassLoader.addURL( jellyScriptHousing.getPluginDirectory().toURL() );
1152             }
1153         }
1154 
1155         MavenJellyContext context = jellyScriptHousing.getProject().getContext();
1156         URL oldRoot = context.getRootURL();
1157         URL oldCurrent = context.getCurrentURL();
1158 
1159         context.setRootURL( jellyScriptHousing.getSource().toURL() );
1160         context.setCurrentURL( jellyScriptHousing.getSource().toURL() );
1161 
1162         try
1163         {
1164             if ( LOGGER.isDebugEnabled() )
1165             {
1166                 LOGGER.debug( "Jelly script to parse : " + jellyScriptHousing.getSource().toURL() );
1167             }
1168             Script script = JellyUtils.compileScript( jellyScriptHousing.getSource(), context );
1169 
1170             context.setRootURL( oldRoot );
1171             context.setCurrentURL( oldCurrent );
1172 
1173             return script;
1174         }
1175         catch ( Exception e )
1176         {
1177             // FIXME: exception? Yuck!
1178             throw new MavenException( "Error parsing: " + jellyScriptHousing.getSource(), e );
1179         }
1180     }
1181 
1182     /**
1183      * @param context
1184      * @throws Exception
1185      * @todo get rid of throws Exception
1186      */
1187     void runScript( JellyScriptHousing jellyScriptHousing, MavenJellyContext context )
1188         throws Exception
1189     {
1190         LOGGER.debug( "running script " + jellyScriptHousing.getSource() );
1191 
1192         Script s = jellyScriptHousing.getScript();
1193         if ( s == null )
1194         {
1195             s = loadScript( jellyScriptHousing );
1196             jellyScriptHousing.setScript( s );
1197         }
1198         if ( context.getVariable( PLUGIN_HOUSING ) != null )
1199         {
1200             throw new IllegalStateException( "nested plugin housings" );
1201         }
1202         context.setVariable( PLUGIN_HOUSING, jellyScriptHousing );
1203         s.run( context, context.getXMLOutput() );
1204         context.removeVariable( PLUGIN_HOUSING );
1205     }
1206 
1207     public Project getPluginProjectFromGoal( String goal )
1208         throws MavenException
1209     {
1210         JellyScriptHousing housing = mapper.getPluginHousing( goal );
1211         return housing != null ? housing.getProject() : null;
1212     }
1213 
1214     public Collection getPluginList()
1215     {
1216         ArrayList list = new ArrayList();
1217         for ( Iterator i = pluginHousings.values().iterator(); i.hasNext(); )
1218         {
1219             JellyScriptHousing housing = (JellyScriptHousing) i.next();
1220             list.add( housing.getName() );
1221         }
1222         Collections.sort( list );
1223         return list;
1224     }
1225 
1226     private void verifyUnpackedPluginsDir()
1227         throws MavenException
1228     {
1229         if ( !unpackedPluginsDir.exists() )
1230         {
1231             LOGGER.warn( getMessage( "directory.nonexistant.warning", unpackedPluginsDir ) );
1232 
1233             if ( !unpackedPluginsDir.mkdirs() )
1234             {
1235                 throw new MavenException( getMessage( "cannot.create.directory.warning", unpackedPluginsDir ) );
1236             }
1237         }
1238 
1239         if ( !unpackedPluginsDir.isDirectory() )
1240         {
1241             throw new MavenException( getMessage( "not.directory.warning", unpackedPluginsDir ) );
1242         }
1243 
1244         if ( !unpackedPluginsDir.canWrite() )
1245         {
1246             throw new MavenException( getMessage( "not.writable.warning", unpackedPluginsDir ) );
1247         }
1248     }
1249 
1250 }