View Javadoc

1   package org.apache.maven.plugins.site;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.File;
23  
24  import java.util.List;
25  import java.util.Locale;
26  
27  import org.apache.maven.artifact.manager.WagonConfigurationException;
28  import org.apache.maven.artifact.manager.WagonManager;
29  import org.apache.maven.doxia.site.decoration.inheritance.URIPathDescriptor;
30  import org.apache.maven.model.DistributionManagement;
31  import org.apache.maven.model.Site;
32  import org.apache.maven.plugin.MojoExecutionException;
33  import org.apache.maven.plugin.logging.Log;
34  import org.apache.maven.project.MavenProject;
35  import org.apache.maven.settings.Server;
36  import org.apache.maven.settings.Settings;
37  import org.apache.maven.wagon.CommandExecutionException;
38  import org.apache.maven.wagon.CommandExecutor;
39  import org.apache.maven.wagon.ConnectionException;
40  import org.apache.maven.wagon.ResourceDoesNotExistException;
41  import org.apache.maven.wagon.TransferFailedException;
42  import org.apache.maven.wagon.UnsupportedProtocolException;
43  import org.apache.maven.wagon.Wagon;
44  import org.apache.maven.wagon.authentication.AuthenticationException;
45  import org.apache.maven.wagon.authentication.AuthenticationInfo;
46  import org.apache.maven.wagon.authorization.AuthorizationException;
47  import org.apache.maven.wagon.observers.Debug;
48  import org.apache.maven.wagon.proxy.ProxyInfo;
49  import org.apache.maven.wagon.repository.Repository;
50  
51  import org.codehaus.plexus.PlexusConstants;
52  import org.codehaus.plexus.PlexusContainer;
53  import org.codehaus.plexus.component.configurator.ComponentConfigurationException;
54  import org.codehaus.plexus.component.configurator.ComponentConfigurator;
55  import org.codehaus.plexus.component.repository.exception.ComponentLifecycleException;
56  import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
57  import org.codehaus.plexus.configuration.PlexusConfiguration;
58  import org.codehaus.plexus.configuration.xml.XmlPlexusConfiguration;
59  import org.codehaus.plexus.context.Context;
60  import org.codehaus.plexus.context.ContextException;
61  import org.codehaus.plexus.personality.plexus.lifecycle.phase.Contextualizable;
62  import org.codehaus.plexus.util.StringUtils;
63  import org.codehaus.plexus.util.xml.Xpp3Dom;
64  
65  /**
66   * Abstract base class for deploy mojos.
67   * Since 2.3 this includes {@link SiteStageMojo} and {@link SiteStageDeployMojo}.
68   *
69   * @author ltheussl
70   *
71   * @since 2.3
72   */
73  public abstract class AbstractDeployMojo
74      extends AbstractSiteMojo implements Contextualizable
75  {
76      /**
77       * Directory containing the generated project sites and report distributions.
78       *
79       * @parameter alias="outputDirectory" expression="${project.reporting.outputDirectory}"
80       * @required
81       */
82      private File inputDirectory;
83  
84      /**
85       * Whether to run the "chmod" command on the remote site after the deploy.
86       * Defaults to "true".
87       *
88       * @parameter expression="${maven.site.chmod}" default-value="true"
89       * @since 2.1
90       */
91      private boolean chmod;
92  
93      /**
94       * The mode used by the "chmod" command. Only used if chmod = true.
95       * Defaults to "g+w,a+rX".
96       *
97       * @parameter expression="${maven.site.chmod.mode}" default-value="g+w,a+rX"
98       * @since 2.1
99       */
100     private String chmodMode;
101 
102     /**
103      * The options used by the "chmod" command. Only used if chmod = true.
104      * Defaults to "-Rf".
105      *
106      * @parameter expression="${maven.site.chmod.options}" default-value="-Rf"
107      * @since 2.1
108      */
109     private String chmodOptions;
110 
111     /**
112      * Set this to 'true' to skip site deployment.
113      *
114      * @parameter expression="${maven.site.deploy.skip}" default-value="false"
115      * @since 2.4
116      */
117     private boolean skipDeploy;
118 
119     /**
120      * @component
121      */
122     private WagonManager wagonManager;
123 
124     /**
125      * The current user system settings for use in Maven.
126      *
127      * @parameter expression="${settings}"
128      * @required
129      * @readonly
130      */
131     private Settings settings;
132 
133     private PlexusContainer container;
134 
135     /**
136      * The String "staging/".
137      */
138     protected static final String DEFAULT_STAGING_DIRECTORY = "staging/";
139 
140     /** {@inheritDoc} */
141     public void execute()
142         throws MojoExecutionException
143     {
144         if ( skipDeploy )
145         {
146             getLog().info( "maven.site.deploy.skip = true: Skipping site deployment" );
147             return;
148         }
149 
150         deployTo( new org.apache.maven.plugins.site.wagon.repository.Repository(
151             getDeployRepositoryID(),
152             appendSlash( getDeployRepositoryURL() ) ) );
153     }
154 
155     /**
156      * Make sure the given url ends with a slash.
157      *
158      * @param url a String.
159      *
160      * @return if url already ends with '/' it is returned unchanged,
161      *      otherwise a '/' character is appended.
162      */
163     protected static String appendSlash( final String url )
164     {
165         if ( url.endsWith( "/" ) )
166         {
167             return url;
168         }
169         else
170         {
171             return url + "/";
172         }
173     }
174 
175     /**
176      * Specifies the id to look up credential settings.
177      *
178      * @return the id to look up credentials for the deploy. Not null.
179      *
180      * @throws MojoExecutionException
181      *      if the ID cannot be determined
182      */
183     protected abstract String getDeployRepositoryID()
184         throws MojoExecutionException;
185 
186     /**
187      * Specifies the target URL for the deploy.
188      * This should be the top-level URL, ie above modules and locale sub-directories.
189      *
190      * @return the url to deploy to. Not null.
191      *
192      * @throws MojoExecutionException
193      *      if the URL cannot be constructed
194      */
195     protected abstract String getDeployRepositoryURL()
196         throws MojoExecutionException;
197 
198     /**
199      * Find the relative path between the distribution URLs of the top parent and the current project.
200      *
201      * @return the relative path or "./" if the two URLs are the same.
202      *
203      * @throws MojoExecutionException
204      */
205     private String getDeployModuleDirectory()
206         throws MojoExecutionException
207     {
208         String relative = siteTool.getRelativePath( getSite( project ).getUrl(),
209             getRootSite( project ).getUrl() );
210 
211         // SiteTool.getRelativePath() uses File.separatorChar,
212         // so we need to convert '\' to '/' in order for the URL to be valid for Windows users
213         relative = relative.replace( '\\', '/' );
214 
215         return ( "".equals( relative ) ) ? "./" : relative;
216     }
217 
218     /**
219      * Use wagon to deploy the generated site to a given repository.
220      *
221      * @param repository the repository to deply to.
222      *      This needs to contain a valid, non-null {@link Repository#getId() id}
223      *      to look up credentials for the deploy, and a valid, non-null
224      *      {@link Repository#getUrl() scm url} to deploy to.
225      *
226      * @throws MojoExecutionException if the deploy fails.
227      */
228     private void deployTo( final Repository repository )
229         throws MojoExecutionException
230     {
231         if ( !inputDirectory.exists() )
232         {
233             throw new MojoExecutionException( "The site does not exist, please run site:site first" );
234         }
235 
236         if ( getLog().isDebugEnabled() )
237         {
238             getLog().debug( "Deploying to '" + repository.getUrl()
239                 + "',\n    Using credentials from server id '" + repository.getId() + "'" );
240         }
241 
242         deploy( inputDirectory, repository );
243     }
244 
245     private void deploy( final File directory, final Repository repository )
246         throws MojoExecutionException
247     {
248         // TODO: work on moving this into the deployer like the other deploy methods
249         final Wagon wagon = getWagon( repository, wagonManager );
250 
251         try
252         {
253             configureWagon( wagon, repository.getId(), settings, container, getLog() );
254         }
255         catch ( WagonConfigurationException e )
256         {
257             throw new MojoExecutionException( "Unable to configure Wagon: '" + repository.getProtocol() + "'", e );
258         }
259 
260         try
261         {
262             final ProxyInfo proxyInfo = getProxyInfo( repository, wagonManager );
263 
264             push( directory, repository, wagonManager, wagon, proxyInfo,
265                 siteTool.getAvailableLocales( locales ), getDeployModuleDirectory(), getLog() );
266 
267             if ( chmod )
268             {
269                 chmod( wagon, repository, chmodOptions, chmodMode );
270             }
271         }
272         finally
273         {
274             try
275             {
276                 wagon.disconnect();
277             }
278             catch ( ConnectionException e )
279             {
280                 getLog().error( "Error disconnecting wagon - ignored", e );
281             }
282         }
283     }
284 
285     /**
286      * Find the build directory of the top level project in the reactor.
287      * If no top level project is found, the build directory of the current project is returned.
288      *
289      * @return the build directory of the top level project.
290      */
291     protected File getTopLevelBuildDirectory()
292     {
293         // Find the top level project in the reactor
294         final MavenProject topLevelProject = getTopLevelProject( reactorProjects );
295 
296         // Use the top level project's build directory if there is one, otherwise use this project's build directory
297         final File buildDirectory;
298 
299         if ( topLevelProject == null )
300         {
301             getLog().debug( "No top level project found in the reactor, using the current project." );
302 
303             buildDirectory = new File( project.getBuild().getDirectory() );
304         }
305         else
306         {
307             getLog().debug( "Using the top level project found in the reactor." );
308 
309             buildDirectory = new File( topLevelProject.getBuild().getDirectory() );
310         }
311 
312         return buildDirectory;
313     }
314 
315     private static Wagon getWagon( final Repository repository, final WagonManager manager )
316         throws MojoExecutionException
317     {
318         final Wagon wagon;
319 
320         try
321         {
322             wagon = manager.getWagon( repository );
323         }
324         catch ( UnsupportedProtocolException e )
325         {
326             throw new MojoExecutionException( "Unsupported protocol: '" + repository.getProtocol() + "'", e );
327         }
328         catch ( WagonConfigurationException e )
329         {
330             throw new MojoExecutionException( "Unable to configure Wagon: '" + repository.getProtocol() + "'", e );
331         }
332 
333         if ( !wagon.supportsDirectoryCopy() )
334         {
335             throw new MojoExecutionException(
336                 "Wagon protocol '" + repository.getProtocol() + "' doesn't support directory copying" );
337         }
338 
339         return wagon;
340     }
341 
342     private static void push( final File inputDirectory, final Repository repository, final WagonManager manager,
343                               final Wagon wagon, final ProxyInfo proxyInfo, final List<Locale> localesList,
344                               final String relativeDir, final Log log )
345         throws MojoExecutionException
346     {
347         AuthenticationInfo authenticationInfo = manager.getAuthenticationInfo( repository.getId() );
348         log.debug( "authenticationInfo with id '" + repository.getId() + "': "
349                    + ( ( authenticationInfo == null ) ? "-" : authenticationInfo.getUserName() ) );
350 
351         try
352         {
353             Debug debug = new Debug();
354 
355             wagon.addSessionListener( debug );
356 
357             wagon.addTransferListener( debug );
358 
359             if ( proxyInfo != null )
360             {
361                 log.debug( "connect with proxyInfo" );
362                 wagon.connect( repository, authenticationInfo, proxyInfo );
363             }
364             else if ( proxyInfo == null && authenticationInfo != null )
365             {
366                 log.debug( "connect with authenticationInfo and without proxyInfo" );
367                 wagon.connect( repository, authenticationInfo );
368             }
369             else
370             {
371                 log.debug( "connect without authenticationInfo and without proxyInfo" );
372                 wagon.connect( repository );
373             }
374 
375             log.info( "Pushing " + inputDirectory );
376 
377             // Default is first in the list
378             final String defaultLocale = localesList.get( 0 ).getLanguage();
379 
380             for ( Locale locale : localesList )
381             {
382                 if ( locale.getLanguage().equals( defaultLocale ) )
383                 {
384                     // TODO: this also uploads the non-default locales,
385                     // is there a way to exclude directories in wagon?
386                     log.info( "   >>> to " + repository.getUrl() + relativeDir );
387 
388                     wagon.putDirectory( inputDirectory, relativeDir );
389                 }
390                 else
391                 {
392                     log.info( "   >>> to " + repository.getUrl() + locale.getLanguage() + "/" + relativeDir );
393 
394                     wagon.putDirectory( new File( inputDirectory, locale.getLanguage() ),
395                         locale.getLanguage() + "/" + relativeDir );
396                 }
397             }
398         }
399         catch ( ResourceDoesNotExistException e )
400         {
401             throw new MojoExecutionException( "Error uploading site", e );
402         }
403         catch ( TransferFailedException e )
404         {
405             throw new MojoExecutionException( "Error uploading site", e );
406         }
407         catch ( AuthorizationException e )
408         {
409             throw new MojoExecutionException( "Error uploading site", e );
410         }
411         catch ( ConnectionException e )
412         {
413             throw new MojoExecutionException( "Error uploading site", e );
414         }
415         catch ( AuthenticationException e )
416         {
417             throw new MojoExecutionException( "Error uploading site", e );
418         }
419     }
420 
421     private static void chmod( final Wagon wagon, final Repository repository,
422         final String chmodOptions, final String chmodMode )
423         throws MojoExecutionException
424     {
425         try
426         {
427             if ( wagon instanceof CommandExecutor )
428             {
429                 CommandExecutor exec = (CommandExecutor) wagon;
430                 exec.executeCommand( "chmod " + chmodOptions + " " + chmodMode + " " + repository.getBasedir() );
431             }
432             // else ? silently ignore, FileWagon is not a CommandExecutor!
433         }
434         catch ( CommandExecutionException e )
435         {
436             throw new MojoExecutionException( "Error uploading site", e );
437         }
438     }
439 
440     /**
441      * <p>
442      * Get the <code>ProxyInfo</code> of the proxy associated with the <code>host</code>
443      * and the <code>protocol</code> of the given <code>repository</code>.
444      * </p>
445      * <p>
446      * Extract from <a href="http://java.sun.com/j2se/1.5.0/docs/guide/net/properties.html">
447      * J2SE Doc : Networking Properties - nonProxyHosts</a> : "The value can be a list of hosts,
448      * each separated by a |, and in addition a wildcard character (*) can be used for matching"
449      * </p>
450      * <p>
451      * Defensively support for comma (",") and semi colon (";") in addition to pipe ("|") as separator.
452      * </p>
453      *
454      * @param repository the Repository to extract the ProxyInfo from.
455      * @param wagonManager the WagonManager used to connect to the Repository.
456      * @return a ProxyInfo object instantiated or <code>null</code> if no matching proxy is found
457      */
458     public static ProxyInfo getProxyInfo( Repository repository, WagonManager wagonManager )
459     {
460         ProxyInfo proxyInfo = wagonManager.getProxy( repository.getProtocol() );
461 
462         if ( proxyInfo == null )
463         {
464             return null;
465         }
466 
467         String host = repository.getHost();
468         String nonProxyHostsAsString = proxyInfo.getNonProxyHosts();
469         for ( String nonProxyHost : StringUtils.split( nonProxyHostsAsString, ",;|" ) )
470         {
471             if ( StringUtils.contains( nonProxyHost, "*" ) )
472             {
473                 // Handle wildcard at the end, beginning or middle of the nonProxyHost
474                 final int pos = nonProxyHost.indexOf( '*' );
475                 String nonProxyHostPrefix = nonProxyHost.substring( 0, pos );
476                 String nonProxyHostSuffix = nonProxyHost.substring( pos + 1 );
477                 // prefix*
478                 if ( StringUtils.isNotEmpty( nonProxyHostPrefix ) && host.startsWith( nonProxyHostPrefix )
479                     && StringUtils.isEmpty( nonProxyHostSuffix ) )
480                 {
481                     return null;
482                 }
483                 // *suffix
484                 if ( StringUtils.isEmpty( nonProxyHostPrefix )
485                     && StringUtils.isNotEmpty( nonProxyHostSuffix ) && host.endsWith( nonProxyHostSuffix ) )
486                 {
487                     return null;
488                 }
489                 // prefix*suffix
490                 if ( StringUtils.isNotEmpty( nonProxyHostPrefix ) && host.startsWith( nonProxyHostPrefix )
491                     && StringUtils.isNotEmpty( nonProxyHostSuffix ) && host.endsWith( nonProxyHostSuffix ) )
492                 {
493                     return null;
494                 }
495             }
496             else if ( host.equals( nonProxyHost ) )
497             {
498                 return null;
499             }
500         }
501         return proxyInfo;
502     }
503 
504     /**
505      * Configure the Wagon with the information from serverConfigurationMap ( which comes from settings.xml )
506      *
507      * @todo Remove when {@link WagonManager#getWagon(Repository) is available}. It's available in Maven 2.0.5.
508      * @param wagon
509      * @param repositoryId
510      * @param settings
511      * @param container
512      * @param log
513      * @throws WagonConfigurationException
514      */
515     private static void configureWagon( Wagon wagon, String repositoryId, Settings settings, PlexusContainer container,
516                                         Log log )
517         throws WagonConfigurationException
518     {
519         log.debug( " configureWagon " );
520 
521         // MSITE-25: Make sure that the server settings are inserted
522         for ( Server server : settings.getServers() )
523         {
524             String id = server.getId();
525 
526             log.debug( "configureWagon server " + id );
527 
528             if ( id != null && id.equals( repositoryId ) && ( server.getConfiguration() != null ) )
529             {
530                 final PlexusConfiguration plexusConf =
531                     new XmlPlexusConfiguration( (Xpp3Dom) server.getConfiguration() );
532 
533                 ComponentConfigurator componentConfigurator = null;
534                 try
535                 {
536                     componentConfigurator = (ComponentConfigurator) container.lookup( ComponentConfigurator.ROLE );
537                     componentConfigurator.configureComponent( wagon, plexusConf, container.getContainerRealm() );
538                 }
539                 catch ( final ComponentLookupException e )
540                 {
541                     throw new WagonConfigurationException( repositoryId, "Unable to lookup wagon configurator."
542                         + " Wagon configuration cannot be applied.", e );
543                 }
544                 catch ( ComponentConfigurationException e )
545                 {
546                     throw new WagonConfigurationException( repositoryId, "Unable to apply wagon configuration.", e );
547                 }
548                 finally
549                 {
550                     if ( componentConfigurator != null )
551                     {
552                         try
553                         {
554                             container.release( componentConfigurator );
555                         }
556                         catch ( ComponentLifecycleException e )
557                         {
558                             log.error( "Problem releasing configurator - ignoring: " + e.getMessage() );
559                         }
560                     }
561                 }
562             }
563         }
564     }
565 
566     /** {@inheritDoc} */
567     public void contextualize( Context context )
568         throws ContextException
569     {
570         container = (PlexusContainer) context.get( PlexusConstants.PLEXUS_KEY );
571     }
572 
573     /**
574      * Find the top level parent in the reactor, i.e. the execution root.
575      *
576      * @param reactorProjects The projects in the reactor. May be null in which case null is returnned.
577      *
578      * @return The top level project in the reactor, or <code>null</code> if none can be found
579      */
580     private static MavenProject getTopLevelProject( List<MavenProject> reactorProjects )
581     {
582         if ( reactorProjects == null )
583         {
584             return null;
585         }
586 
587         for ( MavenProject reactorProject : reactorProjects )
588         {
589             if ( reactorProject.isExecutionRoot() )
590             {
591                 return reactorProject;
592             }
593         }
594 
595         return null;
596     }
597 
598     /**
599      * Extract the distributionManagment site from the given MavenProject.
600      *
601      * @param project the MavenProject. Not null.
602      *
603      * @return the project site. Not null.
604      *      Also site.getUrl() and site.getId() are guaranteed to be not null.
605      *
606      * @throws MojoExecutionException if any of the site info is missing.
607      */
608     protected static Site getSite( final MavenProject project )
609         throws MojoExecutionException
610     {
611         final String name = project.getName() + " ("
612             + project.getGroupId() + ":" + project.getArtifactId() + ":" + project.getVersion() + ")";
613 
614         final DistributionManagement distributionManagement = project.getDistributionManagement();
615 
616         if ( distributionManagement == null )
617         {
618             throw new MojoExecutionException( "Missing distribution management in project " + name );
619         }
620 
621         final Site site = distributionManagement.getSite();
622 
623         if ( site == null )
624         {
625             throw new MojoExecutionException(
626                 "Missing site information in the distribution management of the project " + name );
627         }
628 
629         if ( site.getUrl() == null || site.getId() == null )
630         {
631             throw new MojoExecutionException( "Missing site data: specify url and id for project " + name );
632         }
633 
634         return site;
635     }
636 
637     /**
638      * Extract the distributionManagment site of the top level parent of the given MavenProject.
639      * This climbs up the project hierarchy and returns the site of the last project
640      * for which {@link #getSite(org.apache.maven.project.MavenProject)} returns a site.
641      *
642      * @param project the MavenProject. Not null.
643      *
644      * @return the top level site. Not null.
645      *      Also site.getUrl() and site.getId() are guaranteed to be not null.
646      *
647      * @throws MojoExecutionException if no site info is found in the tree.
648      */
649     protected Site getRootSite( MavenProject project )
650         throws MojoExecutionException
651     {
652         Site site = getSite( project );
653 
654         MavenProject parent = project;
655 
656         while ( parent.getParent() != null )
657         {
658             // MSITE-585, MNG-1943
659             parent = siteTool.getParentProject( parent, reactorProjects, localRepository );
660 
661             Site oldSite = site;
662 
663             try
664             {
665                 site = getSite( parent );
666             }
667             catch ( MojoExecutionException e )
668             {
669                 break;
670             }
671 
672             // MSITE-600
673             URIPathDescriptor siteURI = new URIPathDescriptor( URIEncoder.encodeURI( site.getUrl() ), "" );
674             URIPathDescriptor oldSiteURI = new URIPathDescriptor( URIEncoder.encodeURI( oldSite.getUrl() ), "" );
675 
676             if ( !siteURI.sameSite( oldSiteURI.getBaseURI() ) )
677             {
678                 return oldSite;
679             }
680         }
681 
682         return site;
683     }
684 
685     private static class URIEncoder
686     {
687         private static final String mark = "-_.!~*'()";
688         private static final String reserved = ";/?:@&=+$,";
689 
690         public static String encodeURI( final String uriString )
691         {
692             final char[] chars = uriString.toCharArray();
693             final StringBuilder uri = new StringBuilder( chars.length );
694 
695             for ( int i = 0; i < chars.length; i++ )
696             {
697                 final char c = chars[i];
698                 if ( ( c >= '0' && c <= '9' ) || ( c >= 'a' && c <= 'z' ) || ( c >= 'A' && c <= 'Z' )
699                         || mark.indexOf( c ) != -1  || reserved.indexOf( c ) != -1 )
700                 {
701                     uri.append( c );
702                 }
703                 else
704                 {
705                     uri.append( '%' );
706                     uri.append( Integer.toHexString( (int) c ) );
707                 }
708             }
709             return uri.toString();
710         }
711     }
712 }