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 598543 2007-11-27 07:44:51Z 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      * Specifies a remote repository to which generated artifacts should be deployed to. If this property is specified,
146      * artifacts are also deployed to the remote repo. The format for this parameter is <code>id::layout::url</code>
147      * 
148      * @parameter expression="${deployTo}"
149      */
150     private String deployTo;
151 
152     /**
153      * @see org.apache.maven.plugin.Mojo#execute()
154      */
155     public void execute()
156         throws MojoExecutionException, MojoFailureException
157     {
158         if ( eclipseDir == null )
159         {
160             getLog().info( "Eclipse directory? " );
161 
162             String eclipseDirString;
163             try
164             {
165                 eclipseDirString = inputHandler.readLine();
166             }
167             catch ( IOException e )
168             {
169                 throw new MojoFailureException( "Unable to read from standard input" );
170             }
171             eclipseDir = new File( eclipseDirString );
172         }
173 
174         if ( !eclipseDir.isDirectory() )
175         {
176             throw new MojoFailureException( "Directory " + eclipseDir.getAbsolutePath() + " doesn't exists" );
177         }
178 
179         File pluginDir = new File( eclipseDir, "plugins" );
180 
181         if ( !pluginDir.isDirectory() )
182         {
183             throw new MojoFailureException( "Plugin directory " + pluginDir.getAbsolutePath() + " doesn't exists" );
184         }
185 
186         File[] files = pluginDir.listFiles();
187 
188         ArtifactRepository remoteRepo = resolveRemoteRepo();
189 
190         if ( remoteRepo != null )
191         {
192             getLog().info( "Will deploy artifacts to remote repository " + deployTo );
193         }
194 
195         Map plugins = new HashMap();
196         Map models = new HashMap();
197 
198         for ( int j = 0; j < files.length; j++ )
199         {
200             File file = files[j];
201 
202             getLog().info( "Processing file " + file.getAbsolutePath() );
203             
204             processFile(file, plugins, models);
205         }
206 
207         int i = 1;
208         for ( Iterator it = plugins.keySet().iterator(); it.hasNext(); )
209         {
210             getLog().info( "Processing " + ( i++ ) + " of " + plugins.keySet().size() );
211             String key = (String) it.next();
212             EclipseOsgiPlugin plugin = (EclipseOsgiPlugin) plugins.get( key );
213             Model model = (Model) models.get( key );
214             writeArtifact( plugin, model, remoteRepo );
215         }
216     }
217 
218     protected void processFile( File file, Map plugins, Map models )
219         throws MojoExecutionException, MojoFailureException
220     {
221         EclipseOsgiPlugin plugin = getEclipsePlugin( file );
222 
223         if ( plugin == null )
224         {
225             getLog().warn( "Skipping file " + file.getAbsolutePath() );
226             return;
227         }
228 
229         Model model = createModel( plugin );
230 
231         if ( model == null )
232         {
233             return;
234         }
235 
236         processPlugin( plugin, model, plugins, models );
237     }
238 
239     protected void processPlugin( EclipseOsgiPlugin plugin, Model model, Map plugins, Map models )
240         throws MojoExecutionException, MojoFailureException
241     {
242         plugins.put( getKey( model ), plugin );
243         models.put( getKey( model ), model );
244     }
245 
246     protected String getKey( Model model )
247     {
248         return model.getGroupId() + "." + model.getArtifactId();
249     }
250 
251     private String getKey(Dependency dependency)
252     {
253         return dependency.getGroupId() + "." + dependency.getArtifactId();
254     }
255 
256     /**
257      * Resolve version ranges in the model provided, overriding version ranges with versions from the dependency in the
258      * provided map of models. TODO doesn't check if the version is in range, it just overwrites it
259      * 
260      * @param model
261      * @param models
262      * @throws MojoFailureException
263      */
264     protected void resolveVersionRanges( Model model, Map models )
265         throws MojoFailureException
266     {
267         for ( Iterator it = model.getDependencies().iterator(); it.hasNext(); )
268         {
269             Dependency dep = (Dependency) it.next();
270             if ( dep.getVersion().indexOf( "[" ) > -1 || dep.getVersion().indexOf( "(" ) > -1 )
271             {
272                 String key = getKey( model );
273                 Model dependencyModel = (Model) models.get( getKey( dep ) );
274                 if ( dependencyModel != null )
275                 {
276                     dep.setVersion( dependencyModel.getVersion() );
277                 }
278                 else
279                 {
280                     throw new MojoFailureException( "Unable to resolve version range for dependency " + dep +
281                         " in project " + key );
282                 }
283             }
284         }
285     }
286 
287     /**
288      * Get a {@link EclipseOsgiPlugin} object from a plugin jar/dir found in the target dir.
289      * 
290      * @param file plugin jar or dir
291      * @throws MojoExecutionException if anything bad happens while parsing files
292      */
293     private EclipseOsgiPlugin getEclipsePlugin( File file )
294         throws MojoExecutionException
295     {
296         if ( file.isDirectory() )
297         {
298             return new ExplodedPlugin( file );
299         }
300         else if ( file.getName().endsWith( ".jar" ) )
301         {
302             try
303             {
304                 return new PackagedPlugin( file );
305             }
306             catch ( IOException e )
307             {
308                 throw new MojoExecutionException( "Unable to access jar " + file.getAbsolutePath(), e );
309             }
310         }
311 
312         return null;
313     }
314 
315     /**
316      * Create the {@link Model} from a plugin manifest
317      * 
318      * @param plugin Eclipse plugin jar or dir
319      * @throws MojoExecutionException if anything bad happens while parsing files
320      */
321     private Model createModel( EclipseOsgiPlugin plugin )
322         throws MojoExecutionException
323     {
324 
325         String name, bundleName, version, groupId, artifactId, requireBundle;
326 
327         try
328         {
329             if ( !plugin.hasManifest() )
330             {
331                 getLog().warn( "Plugin " + plugin + " does not have a manifest; skipping.." );
332                 return null;
333             }
334 
335             Analyzer analyzer = new Analyzer();
336 
337             Map bundleSymbolicNameHeader =
338                 analyzer.parseHeader( plugin.getManifestAttribute( Analyzer.BUNDLE_SYMBOLICNAME ) );
339             bundleName = (String) bundleSymbolicNameHeader.keySet().iterator().next();
340             version = plugin.getManifestAttribute( Analyzer.BUNDLE_VERSION );
341 
342             if ( bundleName == null || version == null )
343             {
344                 getLog().error( "Unable to read bundle name/version from manifest, skipping..." );
345                 return null;
346             }
347 
348             version = osgiVersionToMavenVersion( version );
349 
350             name = plugin.getManifestAttribute( Analyzer.BUNDLE_NAME );
351 
352             requireBundle = plugin.getManifestAttribute( Analyzer.REQUIRE_BUNDLE );
353 
354         }
355         catch ( IOException e )
356         {
357             throw new MojoExecutionException( "Error processing plugin " + plugin, e );
358         }
359 
360         Dependency[] deps = parseDependencies( requireBundle );
361 
362         groupId = createGroupId( bundleName );
363         artifactId = createArtifactId( bundleName );
364 
365         Model model = new Model();
366         model.setModelVersion( "4.0.0" );
367         model.setGroupId( groupId );
368         model.setArtifactId( artifactId );
369         model.setName( name );
370         model.setVersion( version );
371 
372         model.setProperties( plugin.getPomProperties() );
373 
374         if ( groupId.startsWith( "org.eclipse" ) )
375         {
376             // why do we need a parent?
377 
378             // Parent parent = new Parent();
379             // parent.setGroupId( "org.eclipse" );
380             // parent.setArtifactId( "eclipse" );
381             // parent.setVersion( "1" );
382             // model.setParent( parent );
383 
384             // infer license for know projects, everything at eclipse is licensed under EPL
385             // maybe too simplicistic, but better than nothing
386             License license = new License();
387             license.setName( "Eclipse Public License - v 1.0" );
388             license.setUrl( "http://www.eclipse.org/org/documents/epl-v10.html" );
389             model.addLicense( license );
390         }
391 
392         if ( deps.length > 0 )
393         {
394             for ( int k = 0; k < deps.length; k++ )
395             {
396                 model.getDependencies().add( deps[k] );
397             }
398 
399         }
400 
401         return model;
402     }
403     
404     /**
405      * Writes the artifact to the repo
406      * 
407      * @param model
408      * @param remoteRepo remote repository (if set)
409      * @throws MojoExecutionException
410      */
411     private void writeArtifact( EclipseOsgiPlugin plugin, Model model, ArtifactRepository remoteRepo )
412         throws MojoExecutionException
413     {
414         Writer fw = null;
415         ArtifactMetadata metadata = null;
416         File pomFile = null;
417         Artifact pomArtifact =
418             artifactFactory.createArtifact( model.getGroupId(), model.getArtifactId(), model.getVersion(), null, "pom" );
419         Artifact artifact =
420             artifactFactory.createArtifact( model.getGroupId(), model.getArtifactId(), model.getVersion(), null,
421                                             Constants.PROJECT_PACKAGING_JAR );
422         try
423         {
424             pomFile = File.createTempFile( "pom-", ".xml" );
425 
426             // TODO use WriterFactory.newXmlWriter() when plexus-utils is upgraded to 1.4.5+
427             fw = new OutputStreamWriter( new FileOutputStream( pomFile ), "UTF-8" );
428             model.setModelEncoding( "UTF-8" ); // to be removed when encoding is detected instead of forced to UTF-8
429             pomFile.deleteOnExit();
430             new MavenXpp3Writer().write( fw, model );
431             metadata = new ProjectArtifactMetadata( pomArtifact, pomFile );
432             pomArtifact.addMetadata( metadata );
433         }
434         catch ( IOException e )
435         {
436             throw new MojoExecutionException( "Error writing temporary pom file: " + e.getMessage(), e );
437         }
438         finally
439         {
440             IOUtil.close( fw );
441         }
442 
443         try
444         {
445             File jarFile = plugin.getJarFile();
446 
447             if ( remoteRepo != null )
448             {
449                 deployer.deploy( pomFile, pomArtifact, remoteRepo, localRepository );
450                 deployer.deploy( jarFile, artifact, remoteRepo, localRepository );
451             }
452             else
453             {
454                 installer.install( pomFile, pomArtifact, localRepository );
455                 installer.install( jarFile, artifact, localRepository );
456             }
457         }
458         catch ( ArtifactDeploymentException e )
459         {
460             throw new MojoExecutionException( "Unable to deploy artifact to repository.", e );
461         }
462         catch ( ArtifactInstallationException e )
463         {
464             throw new MojoExecutionException( "Unable to install artifact to repository.", e );
465         }
466         catch ( IOException e )
467         {
468             throw new MojoExecutionException( "Error getting the jar file for plugin " + plugin, e );
469         }
470         finally
471         {
472             pomFile.delete();
473         }
474 
475     }
476 
477     protected String osgiVersionToMavenVersion( String version )
478     {
479         return osgiVersionToMavenVersion( version, null, false );
480     }
481 
482     /**
483      * The 4th (build) token MUST be separed with "-" and not with "." in maven. A version with 4 dots is not parsed,
484      * and the whole string is considered a qualifier. See tests in DefaultArtifactVersion for reference.
485      * 
486      * @param version initial version
487      * @param forcedQualifier build number
488      * @param stripQualifier always remove 4th token in version
489      * @return converted version
490      */
491     protected String osgiVersionToMavenVersion( String version, String forcedQualifier, boolean stripQualifier )
492     {
493         if ( stripQualifier && StringUtils.countMatches( version, "." ) > 2 )
494         {
495             version = StringUtils.substring( version, 0, version.lastIndexOf( "." ) );
496         }
497         else if ( StringUtils.countMatches( version, "." ) > 2 )
498         {
499             int lastDot = version.lastIndexOf( "." );
500             if ( StringUtils.isNotEmpty( forcedQualifier ) )
501             {
502                 version = StringUtils.substring( version, 0, lastDot ) + "-" + forcedQualifier;
503             }
504             else
505             {
506                 version =
507                     StringUtils.substring( version, 0, lastDot ) + "-" +
508                         StringUtils.substring( version, lastDot + 1, version.length() );
509             }
510         }
511         return version;
512     }
513 
514     /**
515      * Resolves the deploy<code>deployTo</code> parameter to an <code>ArtifactRepository</code> instance (if set).
516      * 
517      * @throws MojoFailureException
518      * @throws MojoExecutionException
519      * @return ArtifactRepository instance of null if <code>deployTo</code> is not set.
520      */
521     private ArtifactRepository resolveRemoteRepo()
522         throws MojoFailureException, MojoExecutionException
523     {
524         if ( deployTo != null )
525         {
526             Matcher matcher = DEPLOYTO_PATTERN.matcher( deployTo );
527 
528             if ( !matcher.matches() )
529             {
530                 throw new MojoFailureException( deployTo, "Invalid syntax for repository.",
531                                                 "Invalid syntax for remote repository. Use \"id::layout::url\"." );
532             }
533             else
534             {
535                 String id = matcher.group( 1 ).trim();
536                 String layout = matcher.group( 2 ).trim();
537                 String url = matcher.group( 3 ).trim();
538 
539                 ArtifactRepositoryLayout repoLayout;
540                 try
541                 {
542                     repoLayout = (ArtifactRepositoryLayout) container.lookup( ArtifactRepositoryLayout.ROLE, layout );
543                 }
544                 catch ( ComponentLookupException e )
545                 {
546                     throw new MojoExecutionException( "Cannot find repository layout: " + layout, e );
547                 }
548 
549                 return new DefaultArtifactRepository( id, url, repoLayout );
550             }
551         }
552         return null;
553     }
554 
555     /**
556      * {@inheritDoc}
557      */
558     public void contextualize( Context context )
559         throws ContextException
560     {
561         this.container = (PlexusContainer) context.get( PlexusConstants.PLEXUS_KEY );
562     }
563 
564     /**
565      * Get the group id as the tokens until last dot e.g. <code>org.eclipse.jdt</code> -> <code>org.eclipse</code>
566      * 
567      * @param bundleName bundle name
568      * @return group id
569      */
570     protected String createGroupId( String bundleName )
571     {
572         int i = bundleName.lastIndexOf( "." );
573         if ( i > 0 )
574         {
575             return bundleName.substring( 0, i );
576         }
577         else
578             return bundleName;
579     }
580 
581     /**
582      * Get the artifact id as the tokens after last dot e.g. <code>org.eclipse.jdt</code> -> <code>jdt</code>
583      * 
584      * @param bundleName bundle name
585      * @return artifact id
586      */
587     protected String createArtifactId( String bundleName )
588     {
589         int i = bundleName.lastIndexOf( "." );
590         if ( i > 0 )
591         {
592             return bundleName.substring( i + 1 );
593         }
594         else
595             return bundleName;
596     }
597 
598     /**
599      * Parses the "Require-Bundle" and convert it to a list of dependencies.
600      * 
601      * @param requireBundle "Require-Bundle" entry
602      * @return an array of <code>Dependency</code>
603      */
604     protected Dependency[] parseDependencies( String requireBundle )
605     {
606         if ( requireBundle == null )
607         {
608             return new Dependency[0];
609         }
610 
611         List dependencies = new ArrayList();
612 
613         Analyzer analyzer = new Analyzer();
614 
615         Map requireBundleHeader = analyzer.parseHeader( requireBundle );
616 
617         // now iterates on bundles and extract dependencies
618         for ( Iterator iter = requireBundleHeader.entrySet().iterator(); iter.hasNext(); )
619         {
620             Map.Entry entry = (Map.Entry) iter.next();
621             String bundleName = (String) entry.getKey();
622             Map attributes = (Map) entry.getValue();
623 
624             String version = (String) attributes.get( Analyzer.BUNDLE_VERSION.toLowerCase() );
625             boolean optional = "optional".equals( attributes.get( "resolution:" ) );
626 
627             if ( version == null )
628             {
629                 getLog().info( "Missing version for bundle " + bundleName + ", assuming any version > 0" );
630                 version = "[0,)";
631             }
632 
633             version = fixBuildNumberSeparator( version );
634 
635             Dependency dep = new Dependency();
636             dep.setGroupId( createGroupId( bundleName ) );
637             dep.setArtifactId( createArtifactId( bundleName ) );
638             dep.setVersion( version );
639             dep.setOptional( optional );
640 
641             dependencies.add( dep );
642 
643         }
644 
645         return (Dependency[]) dependencies.toArray( new Dependency[dependencies.size()] );
646 
647     }
648 
649     /**
650      * Fix the separator for the 4th token in a versions. In maven this must be "-", in OSGI it's "."
651      * 
652      * @param versionRange input range
653      * @return modified version range
654      */
655     protected String fixBuildNumberSeparator( String versionRange )
656     {
657         // should not be called with a null versionRange, but a check doesn't hurt...
658         if ( versionRange == null )
659         {
660             return null;
661         }
662 
663         StringBuffer newVersionRange = new StringBuffer();
664 
665         Matcher matcher = VERSION_PATTERN.matcher( versionRange );
666 
667         while ( matcher.find() )
668         {
669             String group = matcher.group();
670 
671             if ( StringUtils.countMatches( group, "." ) > 2 )
672             {
673                 // build number found, fix it
674                 int lastDot = group.lastIndexOf( "." );
675                 group =
676                     StringUtils.substring( group, 0, lastDot ) + "-" +
677                         StringUtils.substring( group, lastDot + 1, group.length() );
678             }
679             matcher.appendReplacement( newVersionRange, group );
680         }
681 
682         matcher.appendTail( newVersionRange );
683 
684         return newVersionRange.toString();
685     }
686 
687 }