View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugin.eclipse;
20  
21  import java.io.File;
22  import java.io.FileOutputStream;
23  import java.io.IOException;
24  import java.io.OutputStreamWriter;
25  import java.io.Writer;
26  import java.util.ArrayList;
27  import java.util.HashMap;
28  import java.util.Iterator;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.regex.Matcher;
32  import java.util.regex.Pattern;
33  
34  import aQute.lib.osgi.Analyzer;
35  
36  import org.apache.maven.artifact.Artifact;
37  import org.apache.maven.artifact.deployer.ArtifactDeployer;
38  import org.apache.maven.artifact.deployer.ArtifactDeploymentException;
39  import org.apache.maven.artifact.factory.ArtifactFactory;
40  import org.apache.maven.artifact.installer.ArtifactInstallationException;
41  import org.apache.maven.artifact.installer.ArtifactInstaller;
42  import org.apache.maven.artifact.metadata.ArtifactMetadata;
43  import org.apache.maven.artifact.repository.ArtifactRepository;
44  import org.apache.maven.artifact.repository.DefaultArtifactRepository;
45  import org.apache.maven.artifact.repository.layout.ArtifactRepositoryLayout;
46  import org.apache.maven.model.Dependency;
47  import org.apache.maven.model.License;
48  import org.apache.maven.model.Model;
49  import org.apache.maven.model.io.xpp3.MavenXpp3Writer;
50  import org.apache.maven.plugin.AbstractMojo;
51  import org.apache.maven.plugin.MojoExecutionException;
52  import org.apache.maven.plugin.MojoFailureException;
53  import org.apache.maven.plugin.eclipse.osgiplugin.EclipseOsgiPlugin;
54  import org.apache.maven.plugin.eclipse.osgiplugin.ExplodedPlugin;
55  import org.apache.maven.plugin.eclipse.osgiplugin.PackagedPlugin;
56  import org.apache.maven.project.artifact.ProjectArtifactMetadata;
57  import org.codehaus.plexus.PlexusConstants;
58  import org.codehaus.plexus.PlexusContainer;
59  import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
60  import org.codehaus.plexus.components.interactivity.InputHandler;
61  import org.codehaus.plexus.context.Context;
62  import org.codehaus.plexus.context.ContextException;
63  import org.codehaus.plexus.personality.plexus.lifecycle.phase.Contextualizable;
64  import org.codehaus.plexus.util.IOUtil;
65  import org.codehaus.plexus.util.StringUtils;
66  
67  /**
68   * Add eclipse artifacts from an eclipse installation to the local repo. This mojo automatically analize the eclipse
69   * directory, copy plugins jars to the local maven repo, and generates appropriate poms. This is the official central
70   * repository builder for Eclipse plugins, so it has the necessary default values. For customized repositories see
71   * {@link MakeArtifactsMojo} Typical usage:
72   * <code>mvn eclipse:to-maven -DdeployTo=maven.org::default::scpexe://repo1.maven.org/home/maven/repository-staging/to-ibiblio/eclipse-staging -DeclipseDir=.</code>
73   * 
74   * @author Fabrizio Giustina
75   * @author <a href="mailto:carlos@apache.org">Carlos Sanchez</a>
76   * @version $Id: EclipseToMavenMojo.java 638635 2008-03-18 23:24:40Z carlos $
77   * @goal to-maven
78   * @requiresProject false
79   */
80  public class EclipseToMavenMojo
81      extends AbstractMojo
82      implements Contextualizable
83  {
84  
85      /**
86       * A pattern the <code>deployTo</code> param should match.
87       */
88      private static final Pattern DEPLOYTO_PATTERN = Pattern.compile( "(.+)::(.+)::(.+)" );
89  
90      /**
91       * A pattern for a 4 digit osgi version number.
92       */
93      private static final Pattern VERSION_PATTERN = Pattern.compile( "(([0-9]+\\.)+[0-9]+)" );
94  
95      /**
96       * Plexus container, needed to manually lookup components for deploy of artifacts.
97       */
98      private PlexusContainer container;
99  
100     /**
101      * Local maven repository.
102      * 
103      * @parameter expression="${localRepository}"
104      * @required
105      * @readonly
106      */
107     private ArtifactRepository localRepository;
108 
109     /**
110      * ArtifactFactory component.
111      * 
112      * @component
113      */
114     private ArtifactFactory artifactFactory;
115 
116     /**
117      * ArtifactInstaller component.
118      * 
119      * @component
120      */
121     protected ArtifactInstaller installer;
122 
123     /**
124      * ArtifactDeployer component.
125      * 
126      * @component
127      */
128     private ArtifactDeployer deployer;
129 
130     /**
131      * Eclipse installation dir. If not set, a value for this parameter will be asked on the command line.
132      * 
133      * @parameter expression="${eclipseDir}"
134      */
135     private File eclipseDir;
136 
137     /**
138      * Input handler, needed for comand line handling.
139      * 
140      * @component
141      */
142     protected InputHandler inputHandler;
143     
144     /**
145      * Strip qualifier (fourth token) from the plugin version. Qualifiers are for eclipse plugin the equivalent of
146      * timestamped snapshot versions for Maven, but the date is maintained also for released version (e.g. a jar for the
147      * release <code>3.2</code> can be named <code>org.eclipse.core.filesystem_1.0.0.v20060603.jar</code>. It's
148      * usually handy to not to include this qualifier when generating maven artifacts for major releases, while it's
149      * needed when working with eclipse integration/nightly builds.
150      * 
151      * @parameter expression="${stripQualifier}" default-value="false"
152      */
153     private boolean stripQualifier;
154 
155     /**
156      * Specifies a remote repository to which generated artifacts should be deployed to. If this property is specified,
157      * artifacts are also deployed to the remote repo. The format for this parameter is <code>id::layout::url</code>
158      * 
159      * @parameter expression="${deployTo}"
160      */
161     private String deployTo;
162 
163     /**
164      * @see org.apache.maven.plugin.Mojo#execute()
165      */
166     public void execute()
167         throws MojoExecutionException, MojoFailureException
168     {
169         if ( eclipseDir == null )
170         {
171             getLog().info( "Eclipse directory? " );
172 
173             String eclipseDirString;
174             try
175             {
176                 eclipseDirString = inputHandler.readLine();
177             }
178             catch ( IOException e )
179             {
180                 throw new MojoFailureException( "Unable to read from standard input" );
181             }
182             eclipseDir = new File( eclipseDirString );
183         }
184 
185         if ( !eclipseDir.isDirectory() )
186         {
187             throw new MojoFailureException( "Directory " + eclipseDir.getAbsolutePath() + " doesn't exists" );
188         }
189 
190         File pluginDir = new File( eclipseDir, "plugins" );
191 
192         if ( !pluginDir.isDirectory() )
193         {
194             throw new MojoFailureException( "Plugin directory " + pluginDir.getAbsolutePath() + " doesn't exists" );
195         }
196 
197         File[] files = pluginDir.listFiles();
198 
199         ArtifactRepository remoteRepo = resolveRemoteRepo();
200 
201         if ( remoteRepo != null )
202         {
203             getLog().info( "Will deploy artifacts to remote repository " + deployTo );
204         }
205 
206         Map plugins = new HashMap();
207         Map models = new HashMap();
208 
209         for ( int j = 0; j < files.length; j++ )
210         {
211             File file = files[j];
212 
213             getLog().info( "Processing file " + file.getAbsolutePath() );
214 
215             processFile( file, plugins, models );
216         }
217 
218         int i = 1;
219         for ( Iterator it = plugins.keySet().iterator(); it.hasNext(); )
220         {
221             getLog().info( "Processing " + ( i++ ) + " of " + plugins.keySet().size() );
222             String key = (String) it.next();
223             EclipseOsgiPlugin plugin = (EclipseOsgiPlugin) plugins.get( key );
224             Model model = (Model) models.get( key );
225             writeArtifact( plugin, model, remoteRepo );
226         }
227     }
228 
229     protected void processFile( File file, Map plugins, Map models )
230         throws MojoExecutionException, MojoFailureException
231     {
232         EclipseOsgiPlugin plugin = getEclipsePlugin( file );
233 
234         if ( plugin == null )
235         {
236             getLog().warn( "Skipping file " + file.getAbsolutePath() );
237             return;
238         }
239 
240         Model model = createModel( plugin );
241 
242         if ( model == null )
243         {
244             return;
245         }
246 
247         processPlugin( plugin, model, plugins, models );
248     }
249 
250     protected void processPlugin( EclipseOsgiPlugin plugin, Model model, Map plugins, Map models )
251         throws MojoExecutionException, MojoFailureException
252     {
253         plugins.put( getKey( model ), plugin );
254         models.put( getKey( model ), model );
255     }
256 
257     protected String getKey( Model model )
258     {
259         return model.getGroupId() + "." + model.getArtifactId();
260     }
261 
262     private String getKey( Dependency dependency )
263     {
264         return dependency.getGroupId() + "." + dependency.getArtifactId();
265     }
266 
267     /**
268      * Resolve version ranges in the model provided, overriding version ranges with versions from the dependency in the
269      * provided map of models. TODO doesn't check if the version is in range, it just overwrites it
270      * 
271      * @param model
272      * @param models
273      * @throws MojoFailureException
274      */
275     protected void resolveVersionRanges( Model model, Map models )
276         throws MojoFailureException
277     {
278         for ( Iterator it = model.getDependencies().iterator(); it.hasNext(); )
279         {
280             Dependency dep = (Dependency) it.next();
281             if ( dep.getVersion().indexOf( "[" ) > -1 || dep.getVersion().indexOf( "(" ) > -1 )
282             {
283                 String key = getKey( model );
284                 Model dependencyModel = (Model) models.get( getKey( dep ) );
285                 if ( dependencyModel != null )
286                 {
287                     dep.setVersion( dependencyModel.getVersion() );
288                 }
289                 else
290                 {
291                     throw new MojoFailureException( "Unable to resolve version range for dependency " + dep +
292                         " in project " + key );
293                 }
294             }
295         }
296     }
297 
298     /**
299      * Get a {@link EclipseOsgiPlugin} object from a plugin jar/dir found in the target dir.
300      * 
301      * @param file plugin jar or dir
302      * @throws MojoExecutionException if anything bad happens while parsing files
303      */
304     private EclipseOsgiPlugin getEclipsePlugin( File file )
305         throws MojoExecutionException
306     {
307         if ( file.isDirectory() )
308         {
309             return new ExplodedPlugin( file );
310         }
311         else if ( file.getName().endsWith( ".jar" ) )
312         {
313             try
314             {
315                 return new PackagedPlugin( file );
316             }
317             catch ( IOException e )
318             {
319                 throw new MojoExecutionException( "Unable to access jar " + file.getAbsolutePath(), e );
320             }
321         }
322 
323         return null;
324     }
325 
326     /**
327      * Create the {@link Model} from a plugin manifest
328      * 
329      * @param plugin Eclipse plugin jar or dir
330      * @throws MojoExecutionException if anything bad happens while parsing files
331      */
332     private Model createModel( EclipseOsgiPlugin plugin )
333         throws MojoExecutionException
334     {
335 
336         String name, bundleName, version, groupId, artifactId, requireBundle;
337 
338         try
339         {
340             if ( !plugin.hasManifest() )
341             {
342                 getLog().warn( "Plugin " + plugin + " does not have a manifest; skipping.." );
343                 return null;
344             }
345 
346             Analyzer analyzer = new Analyzer();
347 
348             Map bundleSymbolicNameHeader =
349                 analyzer.parseHeader( plugin.getManifestAttribute( Analyzer.BUNDLE_SYMBOLICNAME ) );
350             bundleName = (String) bundleSymbolicNameHeader.keySet().iterator().next();
351             version = plugin.getManifestAttribute( Analyzer.BUNDLE_VERSION );
352 
353             if ( bundleName == null || version == null )
354             {
355                 getLog().error( "Unable to read bundle name/version from manifest, skipping..." );
356                 return null;
357             }
358 
359             version = osgiVersionToMavenVersion( version );
360 
361             name = plugin.getManifestAttribute( Analyzer.BUNDLE_NAME );
362 
363             requireBundle = plugin.getManifestAttribute( Analyzer.REQUIRE_BUNDLE );
364 
365         }
366         catch ( IOException e )
367         {
368             throw new MojoExecutionException( "Error processing plugin " + plugin, e );
369         }
370 
371         Dependency[] deps = parseDependencies( requireBundle );
372 
373         groupId = createGroupId( bundleName );
374         artifactId = createArtifactId( bundleName );
375 
376         Model model = new Model();
377         model.setModelVersion( "4.0.0" );
378         model.setGroupId( groupId );
379         model.setArtifactId( artifactId );
380         model.setName( name );
381         model.setVersion( version );
382 
383         model.setProperties( plugin.getPomProperties() );
384 
385         if ( groupId.startsWith( "org.eclipse" ) )
386         {
387             // why do we need a parent?
388 
389             // Parent parent = new Parent();
390             // parent.setGroupId( "org.eclipse" );
391             // parent.setArtifactId( "eclipse" );
392             // parent.setVersion( "1" );
393             // model.setParent( parent );
394 
395             // infer license for know projects, everything at eclipse is licensed under EPL
396             // maybe too simplicistic, but better than nothing
397             License license = new License();
398             license.setName( "Eclipse Public License - v 1.0" );
399             license.setUrl( "http://www.eclipse.org/org/documents/epl-v10.html" );
400             model.addLicense( license );
401         }
402 
403         if ( deps.length > 0 )
404         {
405             for ( int k = 0; k < deps.length; k++ )
406             {
407                 model.getDependencies().add( deps[k] );
408             }
409 
410         }
411 
412         return model;
413     }
414 
415     /**
416      * Writes the artifact to the repo
417      * 
418      * @param model
419      * @param remoteRepo remote repository (if set)
420      * @throws MojoExecutionException
421      */
422     private void writeArtifact( EclipseOsgiPlugin plugin, Model model, ArtifactRepository remoteRepo )
423         throws MojoExecutionException
424     {
425         Writer fw = null;
426         ArtifactMetadata metadata = null;
427         File pomFile = null;
428         Artifact pomArtifact =
429             artifactFactory.createArtifact( model.getGroupId(), model.getArtifactId(), model.getVersion(), null, "pom" );
430         Artifact artifact =
431             artifactFactory.createArtifact( model.getGroupId(), model.getArtifactId(), model.getVersion(), null,
432                                             Constants.PROJECT_PACKAGING_JAR );
433         try
434         {
435             pomFile = File.createTempFile( "pom-", ".xml" );
436 
437             // TODO use WriterFactory.newXmlWriter() when plexus-utils is upgraded to 1.4.5+
438             fw = new OutputStreamWriter( new FileOutputStream( pomFile ), "UTF-8" );
439             model.setModelEncoding( "UTF-8" ); // to be removed when encoding is detected instead of forced to UTF-8
440             pomFile.deleteOnExit();
441             new MavenXpp3Writer().write( fw, model );
442             metadata = new ProjectArtifactMetadata( pomArtifact, pomFile );
443             pomArtifact.addMetadata( metadata );
444         }
445         catch ( IOException e )
446         {
447             throw new MojoExecutionException( "Error writing temporary pom file: " + e.getMessage(), e );
448         }
449         finally
450         {
451             IOUtil.close( fw );
452         }
453 
454         try
455         {
456             File jarFile = plugin.getJarFile();
457 
458             if ( remoteRepo != null )
459             {
460                 deployer.deploy( pomFile, pomArtifact, remoteRepo, localRepository );
461                 deployer.deploy( jarFile, artifact, remoteRepo, localRepository );
462             }
463             else
464             {
465                 installer.install( pomFile, pomArtifact, localRepository );
466                 installer.install( jarFile, artifact, localRepository );
467             }
468         }
469         catch ( ArtifactDeploymentException e )
470         {
471             throw new MojoExecutionException( "Unable to deploy artifact to repository.", e );
472         }
473         catch ( ArtifactInstallationException e )
474         {
475             throw new MojoExecutionException( "Unable to install artifact to repository.", e );
476         }
477         catch ( IOException e )
478         {
479             throw new MojoExecutionException( "Error getting the jar file for plugin " + plugin, e );
480         }
481         finally
482         {
483             pomFile.delete();
484         }
485 
486     }
487 
488     protected String osgiVersionToMavenVersion( String version )
489     {
490         return osgiVersionToMavenVersion( version, null, stripQualifier );
491     }
492 
493     /**
494      * The 4th (build) token MUST be separed with "-" and not with "." in maven. A version with 4 dots is not parsed,
495      * and the whole string is considered a qualifier. See tests in DefaultArtifactVersion for reference.
496      * 
497      * @param version initial version
498      * @param forcedQualifier build number
499      * @param stripQualifier always remove 4th token in version
500      * @return converted version
501      */
502     protected String osgiVersionToMavenVersion( String version, String forcedQualifier, boolean stripQualifier )
503     {
504         if ( stripQualifier && StringUtils.countMatches( version, "." ) > 2 )
505         {
506             version = StringUtils.substring( version, 0, version.lastIndexOf( "." ) );
507         }
508         else if ( StringUtils.countMatches( version, "." ) > 2 )
509         {
510             int lastDot = version.lastIndexOf( "." );
511             if ( StringUtils.isNotEmpty( forcedQualifier ) )
512             {
513                 version = StringUtils.substring( version, 0, lastDot ) + "-" + forcedQualifier;
514             }
515             else
516             {
517                 version =
518                     StringUtils.substring( version, 0, lastDot ) + "-" +
519                         StringUtils.substring( version, lastDot + 1, version.length() );
520             }
521         }
522         return version;
523     }
524 
525     /**
526      * Resolves the deploy<code>deployTo</code> parameter to an <code>ArtifactRepository</code> instance (if set).
527      * 
528      * @throws MojoFailureException
529      * @throws MojoExecutionException
530      * @return ArtifactRepository instance of null if <code>deployTo</code> is not set.
531      */
532     private ArtifactRepository resolveRemoteRepo()
533         throws MojoFailureException, MojoExecutionException
534     {
535         if ( deployTo != null )
536         {
537             Matcher matcher = DEPLOYTO_PATTERN.matcher( deployTo );
538 
539             if ( !matcher.matches() )
540             {
541                 throw new MojoFailureException( deployTo, "Invalid syntax for repository.",
542                                                 "Invalid syntax for remote repository. Use \"id::layout::url\"." );
543             }
544             else
545             {
546                 String id = matcher.group( 1 ).trim();
547                 String layout = matcher.group( 2 ).trim();
548                 String url = matcher.group( 3 ).trim();
549 
550                 ArtifactRepositoryLayout repoLayout;
551                 try
552                 {
553                     repoLayout = (ArtifactRepositoryLayout) container.lookup( ArtifactRepositoryLayout.ROLE, layout );
554                 }
555                 catch ( ComponentLookupException e )
556                 {
557                     throw new MojoExecutionException( "Cannot find repository layout: " + layout, e );
558                 }
559 
560                 return new DefaultArtifactRepository( id, url, repoLayout );
561             }
562         }
563         return null;
564     }
565 
566     /**
567      * {@inheritDoc}
568      */
569     public void contextualize( Context context )
570         throws ContextException
571     {
572         this.container = (PlexusContainer) context.get( PlexusConstants.PLEXUS_KEY );
573     }
574 
575     /**
576      * Get the group id as the tokens until last dot e.g. <code>org.eclipse.jdt</code> -> <code>org.eclipse</code>
577      * 
578      * @param bundleName bundle name
579      * @return group id
580      */
581     protected String createGroupId( String bundleName )
582     {
583         int i = bundleName.lastIndexOf( "." );
584         if ( i > 0 )
585         {
586             return bundleName.substring( 0, i );
587         }
588         else
589             return bundleName;
590     }
591 
592     /**
593      * Get the artifact id as the tokens after last dot e.g. <code>org.eclipse.jdt</code> -> <code>jdt</code>
594      * 
595      * @param bundleName bundle name
596      * @return artifact id
597      */
598     protected String createArtifactId( String bundleName )
599     {
600         int i = bundleName.lastIndexOf( "." );
601         if ( i > 0 )
602         {
603             return bundleName.substring( i + 1 );
604         }
605         else
606             return bundleName;
607     }
608 
609     /**
610      * Parses the "Require-Bundle" and convert it to a list of dependencies.
611      * 
612      * @param requireBundle "Require-Bundle" entry
613      * @return an array of <code>Dependency</code>
614      */
615     protected Dependency[] parseDependencies( String requireBundle )
616     {
617         if ( requireBundle == null )
618         {
619             return new Dependency[0];
620         }
621 
622         List dependencies = new ArrayList();
623 
624         Analyzer analyzer = new Analyzer();
625 
626         Map requireBundleHeader = analyzer.parseHeader( requireBundle );
627 
628         // now iterates on bundles and extract dependencies
629         for ( Iterator iter = requireBundleHeader.entrySet().iterator(); iter.hasNext(); )
630         {
631             Map.Entry entry = (Map.Entry) iter.next();
632             String bundleName = (String) entry.getKey();
633             Map attributes = (Map) entry.getValue();
634 
635             String version = (String) attributes.get( Analyzer.BUNDLE_VERSION.toLowerCase() );
636             boolean optional = "optional".equals( attributes.get( "resolution:" ) );
637 
638             if ( version == null )
639             {
640                 getLog().info( "Missing version for bundle " + bundleName + ", assuming any version > 0" );
641                 version = "[0,)";
642             }
643 
644             version = fixBuildNumberSeparator( version );
645 
646             Dependency dep = new Dependency();
647             dep.setGroupId( createGroupId( bundleName ) );
648             dep.setArtifactId( createArtifactId( bundleName ) );
649             dep.setVersion( version );
650             dep.setOptional( optional );
651 
652             dependencies.add( dep );
653 
654         }
655 
656         return (Dependency[]) dependencies.toArray( new Dependency[dependencies.size()] );
657 
658     }
659 
660     /**
661      * Fix the separator for the 4th token in a versions. In maven this must be "-", in OSGI it's "."
662      * 
663      * @param versionRange input range
664      * @return modified version range
665      */
666     protected String fixBuildNumberSeparator( String versionRange )
667     {
668         // should not be called with a null versionRange, but a check doesn't hurt...
669         if ( versionRange == null )
670         {
671             return null;
672         }
673 
674         StringBuffer newVersionRange = new StringBuffer();
675 
676         Matcher matcher = VERSION_PATTERN.matcher( versionRange );
677 
678         while ( matcher.find() )
679         {
680             String group = matcher.group();
681 
682             if ( StringUtils.countMatches( group, "." ) > 2 )
683             {
684                 // build number found, fix it
685                 int lastDot = group.lastIndexOf( "." );
686                 group =
687                     StringUtils.substring( group, 0, lastDot ) + "-" +
688                         StringUtils.substring( group, lastDot + 1, group.length() );
689             }
690             matcher.appendReplacement( newVersionRange, group );
691         }
692 
693         matcher.appendTail( newVersionRange );
694 
695         return newVersionRange.toString();
696     }
697 
698 }