001package org.andromda.maven.plugin.andromdapp;
002
003import java.io.File;
004import java.util.ArrayList;
005import java.util.Arrays;
006import java.util.Collection;
007import java.util.LinkedHashMap;
008import java.util.LinkedHashSet;
009import java.util.List;
010import java.util.Map;
011import java.util.Set;
012import org.andromda.core.common.ResourceUtils;
013import org.andromda.maven.plugin.andromdapp.eclipse.ClasspathWriter;
014import org.andromda.maven.plugin.andromdapp.eclipse.ProjectWriter;
015import org.andromda.maven.plugin.andromdapp.eclipse.Variable;
016import org.andromda.maven.plugin.andromdapp.utils.ProjectUtils;
017import org.apache.commons.lang.ObjectUtils;
018import org.apache.commons.lang.StringUtils;
019import org.apache.maven.artifact.factory.ArtifactFactory;
020import org.apache.maven.artifact.metadata.ArtifactMetadataSource;
021import org.apache.maven.artifact.repository.ArtifactRepository;
022import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
023import org.apache.maven.artifact.resolver.ArtifactResolutionException;
024import org.apache.maven.artifact.resolver.ArtifactResolver;
025import org.apache.maven.execution.MavenSession;
026import org.apache.maven.model.Build;
027import org.apache.maven.model.Plugin;
028import org.apache.maven.model.PluginExecution;
029import org.apache.maven.model.PluginManagement;
030import org.apache.maven.plugin.AbstractMojo;
031import org.apache.maven.plugin.MojoExecutionException;
032import org.apache.maven.project.MavenProject;
033import org.apache.maven.project.MavenProjectBuilder;
034import org.apache.maven.project.ProjectBuildingException;
035import org.codehaus.plexus.util.DirectoryScanner;
036import org.codehaus.plexus.util.xml.Xpp3Dom;
037
038/**
039 * Writes the necessary .classpath and .project files
040 * for a new eclipse application.
041 *
042 * @goal eclipse
043 * @phase generate-sources
044 * @author Chad Brandon
045 */
046public class EclipseMojo
047    extends AbstractMojo
048{
049    /**
050     * @parameter expression="${session}"
051     */
052    private MavenSession session;
053
054    /**
055     * @parameter expression="${project}"
056     * @required
057     * @readonly
058     */
059    private MavenProject project;
060
061    private static final String POM_FILE_NAME = "pom.xml";
062
063    /**
064     * Defines the POMs to include when generating the eclipse files.
065     *
066     * @parameter
067     */
068    private String[] includes = new String[] {"*/**/" + POM_FILE_NAME};
069
070    /**
071     * Defines the POMs to exclude when generating the eclipse files.
072     *
073     * @parameter expression="${exclude.poms}"
074     */
075    private String excludePoms;
076
077    /**
078     * Defines the POMs to exclude when generating the eclipse files.
079     *
080     * @parameter
081     */
082    private String[] excludes = new String[0];
083
084    /**
085     * Used to construct Maven project instances from POMs.
086     *
087     * @component
088     */
089    private MavenProjectBuilder projectBuilder;
090
091    /**
092     * The name of the variable that will store the maven repository location.
093     *
094     * @parameter expression="${repository.variable.name}
095     */
096    private String repositoryVariableName = "M2_REPO";
097
098    /**
099     * Artifact factory, needed to download source jars for inclusion in classpath.
100     *
101     * @component role="org.apache.maven.artifact.factory.ArtifactFactory"
102     * @required
103     * @readonly
104     */
105    private ArtifactFactory artifactFactory;
106
107    /**
108     * Artifact resolver, needed to download source jars for inclusion in classpath.
109     *
110     * @component role="org.apache.maven.artifact.resolver.ArtifactResolver"
111     * @required
112     * @readonly
113     */
114    private ArtifactResolver artifactResolver;
115
116    /**
117     * @parameter expression="${localRepository}"
118     * @required
119     * @readonly
120     */
121    protected ArtifactRepository localRepository;
122
123    /**
124     * @parameter
125     */
126    protected Variable[] variables;
127
128    /**
129     * @component
130     */
131    private ArtifactMetadataSource artifactMetadataSource;
132
133    /**
134     * The artifact types which should be included in the generated Eclipse classpath.
135     *
136     * @parameter
137     */
138    private Set<String> classpathArtifactTypes = new LinkedHashSet<String>(Arrays.asList("jar","ejb"));
139
140    /**
141     * Whether or not transitive dependencies shall be included in any resources (i.e. .classpath
142     * that are generated by this mojo).
143     *
144     * @parameter expression="${resolveTransitiveDependencies}"
145     */
146    private boolean resolveTransitiveDependencies = true;
147
148    /**
149     * Allows non-generated configuration to be "merged" into the generated .classpath file.
150     *
151     * @parameter
152     */
153    private String classpathMerge;
154
155    /**
156     * Whether or not processing should be skipped (this is if you just want to force AndroMDA
157     * not to run on your model).
158     *
159     * @parameter expression="${andromda.run.skip}"
160     */
161    private boolean skipProcessing = false;
162
163    /**
164     * @see org.apache.maven.plugin.Mojo#execute()
165     */
166    public void execute()
167        throws MojoExecutionException
168    {
169        if (!this.skipProcessing)
170        {
171            try
172            {
173                final MavenProject rootProject = this.getRootProject();
174                final ProjectWriter projectWriter = new ProjectWriter(rootProject, this.getLog());
175                projectWriter.write();
176                final Map<MavenProject, Collection<String>> originalCompileSourceRoots = this.collectProjectCompileSourceRoots();
177                final List<MavenProject> projects = this.collectProjects();
178                this.processCompileSourceRoots(projects);
179                final ClasspathWriter classpathWriter = new ClasspathWriter(rootProject, this.getLog());
180                classpathWriter.write(
181                    projects,
182                    this.repositoryVariableName,
183                    this.artifactFactory,
184                    this.artifactResolver,
185                    this.localRepository,
186                    this.artifactMetadataSource,
187                    this.classpathArtifactTypes,
188                    this.project.getRemoteArtifactRepositories(),
189                    this.resolveTransitiveDependencies,
190                    this.variables,
191                    this.classpathMerge);
192                // - reset to the original source roots
193                for (final MavenProject project : projects)
194                {
195                    project.getCompileSourceRoots().clear();
196                    project.getCompileSourceRoots().addAll(originalCompileSourceRoots.get(project));
197                }
198            }
199            catch (Throwable throwable)
200            {
201                throw new MojoExecutionException("Error creating eclipse configuration", throwable);
202            }
203        }
204    }
205
206    /**
207     * Collects all existing project compile source roots.
208     *
209     * @return a collection of collections
210     */
211    private Map<MavenProject, Collection<String>> collectProjectCompileSourceRoots()
212        throws Exception
213    {
214        final Map<MavenProject, Collection<String>> sourceRoots = new LinkedHashMap<MavenProject, Collection<String>>();
215        for (final MavenProject project : this.collectProjects())
216        {
217            sourceRoots.put(project, new ArrayList<String>(project.getCompileSourceRoots()));
218        }
219        return sourceRoots;
220    }
221
222    private List<MavenProject> projects = new ArrayList<MavenProject>();
223
224    /**
225     * Collects all projects from all POMs within the current project.
226     *
227     * @return all applicable Maven project instances.
228     *
229     * @throws MojoExecutionException
230     */
231    private List<MavenProject> collectProjects()
232        throws Exception
233    {
234        if (projects.isEmpty())
235        {
236            final List<File> poms = this.getPoms();
237            for (File pom : poms)
238            {
239                try
240                {
241                    // - first attempt to get the existing project from the session
242                    final MavenProject project = ProjectUtils.getProject(this.projectBuilder, this.session, pom, this.getLog());
243                    if (project != null)
244                    {
245                        this.getLog().info("found project " + project.getId());
246                        projects.add(project);
247                    }
248                    else
249                    {
250                        if (this.getLog().isWarnEnabled())
251                        {
252                            this.getLog().warn("Could not load project from pom: " + pom + " - ignoring");
253                        }
254                    }
255                }
256                catch (ProjectBuildingException exception)
257                {
258                    throw new MojoExecutionException("Error loading " + pom, exception);
259                }
260            }
261        }
262        return projects;
263    }
264
265    /**
266     * Processes the project compile source roots (adds all appropriate ones to the projects)
267     * so that they're available to the eclipse mojos.
268     *
269     * @param projects the projects to process.
270     * @throws Exception
271     */
272    private void processCompileSourceRoots(final List<MavenProject> projects)
273        throws Exception
274    {
275        for (final MavenProject project : projects)
276        {
277            final Set<String> compileSourceRoots = new LinkedHashSet<String>(project.getCompileSourceRoots());
278            compileSourceRoots.addAll(this.getExtraSourceDirectories(project));
279            final String testSourceDirectory = project.getBuild().getTestSourceDirectory();
280            if (StringUtils.isNotBlank(testSourceDirectory))
281            {
282                compileSourceRoots.add(testSourceDirectory);
283            }
284            project.getCompileSourceRoots().clear();
285            project.getCompileSourceRoots().addAll(compileSourceRoots);
286        }
287    }
288
289    /**
290     * The artifact id for the multi source plugin.
291     */
292    private static final String MULTI_SOURCE_PLUGIN_ARTIFACT_ID = "build-helper-maven-plugin";
293
294    /**
295     * Retrieves any additional source directories which are defined within the build-helper-maven-plugin.
296     *
297     * @param project the maven project from which to retrieve the extra source directories.
298     * @return the list of extra source directories.
299     */
300    private List<String> getExtraSourceDirectories(final MavenProject project)
301    {
302        final List<String> sourceDirectories = new ArrayList<String>();
303        final Build build = project.getBuild();
304        if (build != null)
305        {
306            final PluginManagement pluginManagement = build.getPluginManagement();
307            if (pluginManagement != null && !pluginManagement.getPlugins().isEmpty())
308            {
309                Plugin multiSourcePlugin = null;
310                for (final Plugin plugin : pluginManagement.getPlugins())
311                {
312                    if (MULTI_SOURCE_PLUGIN_ARTIFACT_ID.equals(plugin.getArtifactId()))
313                    {
314                        multiSourcePlugin = plugin;
315                        break;
316                    }
317                }
318                final Xpp3Dom configuration = this.getConfiguration(multiSourcePlugin);
319                if (configuration != null && configuration.getChildCount() > 0)
320                {
321                    final Xpp3Dom directories = configuration.getChild(0);
322                    if (directories != null)
323                    {
324                        final int childCount = directories.getChildCount();
325                        if (childCount > 0)
326                        {
327                            final String baseDirectory =
328                                ResourceUtils.normalizePath(ObjectUtils.toString(project.getBasedir()) + '/');
329                            final Xpp3Dom[] children = directories.getChildren();
330                            for (int ctr = 0; ctr < childCount; ctr++)
331                            {
332                                final Xpp3Dom child = children[ctr];
333                                if (child != null)
334                                {
335                                    String directoryValue = ResourceUtils.normalizePath(child.getValue());
336                                    if (directoryValue != null)
337                                    {
338                                        if (!directoryValue.startsWith(baseDirectory))
339                                        {
340                                            directoryValue =
341                                                ResourceUtils.normalizePath(baseDirectory + directoryValue.trim());
342                                        }
343                                        sourceDirectories.add(directoryValue);
344                                    }
345                                }
346                            }
347                        }
348                    }
349                }
350            }
351        }
352        return sourceDirectories;
353    }
354
355    /**
356     * Retrieves the appropriate configuration instance (first tries
357     * to get the configuration from the plugin, then tries from the plugin's
358     * executions.
359     *
360     * @param plugin the plugin from which the retrieve the configuration.
361     * @return the plugin's configuration, or null if not found.
362     */
363    private Xpp3Dom getConfiguration(final Plugin plugin)
364    {
365        Xpp3Dom configuration = null;
366        if (plugin != null)
367        {
368            if (plugin.getConfiguration() != null)
369            {
370                configuration = (Xpp3Dom)plugin.getConfiguration();
371            }
372            else
373            {
374                final List<PluginExecution> executions = plugin.getExecutions();
375                if (executions != null && !executions.isEmpty())
376                {
377                    // - there should only be one execution so we get the first one
378                    final PluginExecution execution = plugin.getExecutions().iterator().next();
379                    configuration = (Xpp3Dom)execution.getConfiguration();
380                }
381            }
382        }
383        return configuration;
384    }
385
386    /**
387     * Stores the root project.
388     */
389    private MavenProject rootProject;
390
391    /**
392     * Retrieves the root project (i.e. the root parent project)
393     * for this project.
394     *
395     * @return the root project.
396     * @throws MojoExecutionException
397     */
398    private MavenProject getRootProject()
399        throws MojoExecutionException, ArtifactResolutionException, ArtifactNotFoundException
400    {
401        if (this.rootProject == null)
402        {
403            final MavenProject firstParent = this.project.getParent();
404            File rootFile = this.project.getFile();
405            if (firstParent != null && firstParent.getFile() != null )
406            {
407                for (this.rootProject = firstParent, rootFile = new File(rootFile.getParentFile().getParentFile(), POM_FILE_NAME);
408                     this.rootProject.getParent() != null && this.rootProject.getParent().getFile() != null;
409                     this.rootProject = this.rootProject.getParent(), rootFile = new File(rootFile.getParentFile().getParentFile(), POM_FILE_NAME))
410                {
411                }
412                // - if the project has no file defined, use the rootFile
413                if (this.rootProject != null && this.rootProject.getFile() == null && rootFile.exists())
414                {
415                    this.rootProject.setFile(rootFile);
416                }
417            }
418            else
419            {
420                this.rootProject = this.project;
421            }
422        }
423        return this.rootProject;
424    }
425
426    /**
427     * Retrieves all the POMs for the given project.
428     *
429     * @return all poms found.
430     * @throws MojoExecutionException
431     */
432    private List<File> getPoms()
433        throws Exception
434    {
435        final DirectoryScanner scanner = new DirectoryScanner();
436        scanner.setBasedir(this.getRootProject().getBasedir());
437        scanner.setIncludes(this.includes);
438
439        final List<String> excludes = new ArrayList<String>(Arrays.asList(this.excludes));
440        if (this.excludePoms != null)
441        {
442            excludes.addAll(Arrays.asList(excludePoms.split(",")));
443        }
444        scanner.setExcludes(excludes.toArray(new String[excludes.size()]));
445        scanner.scan();
446
447        List<File> poms = new ArrayList<File>();
448
449        for (int ctr = 0; ctr < scanner.getIncludedFiles().length; ctr++)
450        {
451            final File file = new File(
452                this.getRootProject().getBasedir(),
453                scanner.getIncludedFiles()[ctr]);
454            if (file.exists())
455            {
456                poms.add(file);
457            }
458        }
459
460        return poms;
461    }
462}