blob: 235553479ea3e67f2164166c56da35688b2347e0 [file] [log] [blame]
package org.apache.maven.plugins.site.deploy;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import org.apache.maven.artifact.manager.WagonManager;
import org.apache.maven.doxia.site.decoration.inheritance.URIPathDescriptor;
import org.apache.maven.execution.MavenExecutionRequest;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.DistributionManagement;
import org.apache.maven.model.Site;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.site.AbstractSiteMojo;
import org.apache.maven.project.MavenProject;
import org.apache.maven.settings.Proxy;
import org.apache.maven.settings.Server;
import org.apache.maven.settings.Settings;
import org.apache.maven.settings.crypto.DefaultSettingsDecryptionRequest;
import org.apache.maven.settings.crypto.SettingsDecrypter;
import org.apache.maven.settings.crypto.SettingsDecryptionResult;
import org.apache.maven.wagon.CommandExecutionException;
import org.apache.maven.wagon.CommandExecutor;
import org.apache.maven.wagon.ConnectionException;
import org.apache.maven.wagon.ResourceDoesNotExistException;
import org.apache.maven.wagon.TransferFailedException;
import org.apache.maven.wagon.UnsupportedProtocolException;
import org.apache.maven.wagon.Wagon;
import org.apache.maven.wagon.authentication.AuthenticationException;
import org.apache.maven.wagon.authentication.AuthenticationInfo;
import org.apache.maven.wagon.authorization.AuthorizationException;
import org.apache.maven.wagon.observers.Debug;
import org.apache.maven.wagon.proxy.ProxyInfo;
import org.apache.maven.wagon.repository.Repository;
import org.codehaus.plexus.PlexusConstants;
import org.codehaus.plexus.PlexusContainer;
import org.codehaus.plexus.component.configurator.ComponentConfigurationException;
import org.codehaus.plexus.component.configurator.ComponentConfigurator;
import org.codehaus.plexus.component.repository.exception.ComponentLifecycleException;
import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
import org.codehaus.plexus.configuration.PlexusConfiguration;
import org.codehaus.plexus.configuration.xml.XmlPlexusConfiguration;
import org.codehaus.plexus.context.Context;
import org.codehaus.plexus.context.ContextException;
import org.codehaus.plexus.personality.plexus.lifecycle.phase.Contextualizable;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.xml.Xpp3Dom;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.Locale;
import java.util.Set;
/**
* Abstract base class for deploy mojos.
* Since 2.3 this includes {@link SiteStageMojo} and {@link SiteStageDeployMojo}.
*
* @author ltheussl
* @since 2.3
*/
public abstract class AbstractDeployMojo
extends AbstractSiteMojo
implements Contextualizable
{
/**
* Directory containing the generated project sites and report distributions.
*
* @since 2.3
*/
@Parameter( alias = "outputDirectory", defaultValue = "${project.reporting.outputDirectory}", required = true )
private File inputDirectory;
/**
* Whether to run the "chmod" command on the remote site after the deploy.
* Defaults to "true".
*
* @since 2.1
*/
@Parameter( property = "maven.site.chmod", defaultValue = "true" )
private boolean chmod;
/**
* The mode used by the "chmod" command. Only used if chmod = true.
* Defaults to "g+w,a+rX".
*
* @since 2.1
*/
@Parameter( property = "maven.site.chmod.mode", defaultValue = "g+w,a+rX" )
private String chmodMode;
/**
* The options used by the "chmod" command. Only used if chmod = true.
* Defaults to "-Rf".
*
* @since 2.1
*/
@Parameter( property = "maven.site.chmod.options", defaultValue = "-Rf" )
private String chmodOptions;
/**
* Set this to 'true' to skip site deployment.
*
* @since 3.0
*/
@Parameter( property = "maven.site.deploy.skip", defaultValue = "false" )
private boolean skipDeploy;
/**
*/
@Component
private WagonManager wagonManager; // maven-compat
/**
* The current user system settings for use in Maven.
*/
@Parameter( defaultValue = "${settings}", readonly = true )
private Settings settings;
/**
* @since 3.0-beta-2
*/
@Parameter( defaultValue = "${session}", readonly = true )
protected MavenSession mavenSession;
private String topDistributionManagementSiteUrl;
private Site deploySite;
private PlexusContainer container;
/**
* {@inheritDoc}
*/
public void execute()
throws MojoExecutionException
{
if ( skip && isDeploy() )
{
getLog().info( "maven.site.skip = true: Skipping site deployment" );
return;
}
if ( skipDeploy && isDeploy() )
{
getLog().info( "maven.site.deploy.skip = true: Skipping site deployment" );
return;
}
deployTo( new Repository( getDeploySite().getId(), getDeploySite().getUrl() ) );
}
/**
* Make sure the given URL ends with a slash.
*
* @param url a String
* @return if url already ends with '/' it is returned unchanged.
* Otherwise a '/' character is appended.
*/
protected static String appendSlash( final String url )
{
if ( url.endsWith( "/" ) )
{
return url;
}
else
{
return url + "/";
}
}
/**
* Detect if the mojo is staging or deploying.
*
* @return <code>true</code> if the mojo is for deploy and not staging (local or deploy)
*/
protected abstract boolean isDeploy();
/**
* Get the top distribution management site url, used for module relative path calculations.
* This should be a top-level URL, ie above modules and locale sub-directories. Each deploy mojo
* can tweak algorithm to determine this top site by implementing determineTopDistributionManagementSiteUrl().
*
* @return the site for deployment
* @throws MojoExecutionException in case of issue
* @see #determineTopDistributionManagementSiteUrl()
*/
protected String getTopDistributionManagementSiteUrl()
throws MojoExecutionException
{
if ( topDistributionManagementSiteUrl == null )
{
topDistributionManagementSiteUrl = determineTopDistributionManagementSiteUrl();
if ( !isDeploy() )
{
getLog().debug( "distributionManagement.site.url relative path: " + getDeployModuleDirectory() );
}
}
return topDistributionManagementSiteUrl;
}
protected abstract String determineTopDistributionManagementSiteUrl()
throws MojoExecutionException;
/**
* Get the site used for deployment, with its id to look up credential settings and the target URL for the deploy.
* This should be a top-level URL, ie above modules and locale sub-directories. Each deploy mojo
* can tweak algorithm to determine this deploy site by implementing determineDeploySite().
*
* @return the site for deployment
* @throws MojoExecutionException in case of issue
* @see #determineDeploySite()
*/
protected Site getDeploySite()
throws MojoExecutionException
{
if ( deploySite == null )
{
deploySite = determineDeploySite();
}
return deploySite;
}
protected abstract Site determineDeploySite()
throws MojoExecutionException;
/**
* Find the relative path between the distribution URLs of the top site and the current project.
*
* @return the relative path or "./" if the two URLs are the same.
* @throws MojoExecutionException in case of issue
*/
protected String getDeployModuleDirectory()
throws MojoExecutionException
{
String to = getSite( project ).getUrl();
getLog().debug( "Mapping url source calculation: " );
String from = getTopDistributionManagementSiteUrl();
String relative = siteTool.getRelativePath( to, from );
// SiteTool.getRelativePath() uses File.separatorChar,
// so we need to convert '\' to '/' in order for the URL to be valid for Windows users
relative = relative.replace( '\\', '/' );
return ( "".equals( relative ) ) ? "./" : relative;
}
/**
* Use wagon to deploy the generated site to a given repository.
*
* @param repository the repository to deploy to.
* This needs to contain a valid, non-null {@link Repository#getId() id}
* to look up credentials for the deploy, and a valid, non-null
* {@link Repository#getUrl() scm url} to deploy to.
* @throws MojoExecutionException if the deploy fails.
*/
private void deployTo( final Repository repository )
throws MojoExecutionException
{
if ( !inputDirectory.exists() )
{
throw new MojoExecutionException( "The site does not exist, please run site:site first" );
}
if ( getLog().isDebugEnabled() )
{
getLog().debug( "Deploying to '" + repository.getUrl() + "',\n Using credentials from server id '"
+ repository.getId() + "'" );
}
deploy( inputDirectory, repository );
}
private void deploy( final File directory, final Repository repository )
throws MojoExecutionException
{
// TODO: work on moving this into the deployer like the other deploy methods
final Wagon wagon = getWagon( repository, wagonManager );
try
{
configureWagon( wagon, repository.getId(), settings, container, getLog() );
SettingsDecrypter settingsDecrypter = container.lookup( SettingsDecrypter.class );
ProxyInfo proxyInfo = getProxy( repository, settingsDecrypter );
push( directory, repository, wagon, proxyInfo, getLocales(), getDeployModuleDirectory() );
if ( chmod )
{
chmod( wagon, repository, chmodOptions, chmodMode );
}
}
catch ( ComponentLookupException cle )
{
throw new MojoExecutionException( "Unable to lookup SettingsDecrypter: " + cle.getMessage(), cle );
}
catch ( TransferFailedException e )
{
throw new MojoExecutionException( "Unable to configure Wagon: '" + repository.getProtocol() + "'", e );
}
finally
{
try
{
wagon.disconnect();
}
catch ( ConnectionException e )
{
getLog().error( "Error disconnecting wagon - ignored", e );
}
}
}
private Wagon getWagon( final Repository repository, final WagonManager manager )
throws MojoExecutionException
{
final Wagon wagon;
try
{
wagon = manager.getWagon( repository );
}
catch ( UnsupportedProtocolException e )
{
String shortMessage = "Unsupported protocol: '" + repository.getProtocol() + "' for site deployment to "
+ "distributionManagement.site.url=" + repository.getUrl() + ".";
String longMessage =
"\n" + shortMessage + "\n" + "Currently supported protocols are: " + getSupportedProtocols() + ".\n"
+ " Protocols may be added through wagon providers.\n" + " For more information, see "
+ "https://maven.apache.org/plugins/maven-site-plugin/examples/adding-deploy-protocol.html";
getLog().error( longMessage );
throw new MojoExecutionException( shortMessage );
}
catch ( TransferFailedException e )
{
throw new MojoExecutionException( "Unable to configure Wagon: '" + repository.getProtocol() + "'", e );
}
if ( !wagon.supportsDirectoryCopy() )
{
throw new MojoExecutionException(
"Wagon protocol '" + repository.getProtocol() + "' doesn't support directory copying" );
}
return wagon;
}
private String getSupportedProtocols()
{
try
{
Set<String> protocols = container.lookupMap( Wagon.class ).keySet();
return StringUtils.join( protocols.iterator(), ", " );
}
catch ( ComponentLookupException e )
{
// in the unexpected case there is a problem when instantiating a wagon provider
getLog().error( e );
}
return "";
}
private void push( final File inputDirectory, final Repository repository, final Wagon wagon,
final ProxyInfo proxyInfo, final List<Locale> localesList, final String relativeDir )
throws MojoExecutionException
{
AuthenticationInfo authenticationInfo = wagonManager.getAuthenticationInfo( repository.getId() );
getLog().debug( "authenticationInfo with id '" + repository.getId() + "': "
+ ( ( authenticationInfo == null ) ? "-" : authenticationInfo.getUserName() ) );
try
{
if ( getLog().isDebugEnabled() )
{
Debug debug = new Debug();
wagon.addSessionListener( debug );
wagon.addTransferListener( debug );
}
if ( proxyInfo != null )
{
getLog().debug( "connect with proxyInfo" );
wagon.connect( repository, authenticationInfo, proxyInfo );
}
else if ( proxyInfo == null && authenticationInfo != null )
{
getLog().debug( "connect with authenticationInfo and without proxyInfo" );
wagon.connect( repository, authenticationInfo );
}
else
{
getLog().debug( "connect without authenticationInfo and without proxyInfo" );
wagon.connect( repository );
}
getLog().info( "Pushing " + inputDirectory );
// Default is first in the list
final String defaultLocale = localesList.get( 0 ).getLanguage();
for ( Locale locale : localesList )
{
if ( locale.getLanguage().equals( defaultLocale ) )
{
// TODO: this also uploads the non-default locales,
// is there a way to exclude directories in wagon?
getLog().info( " >>> to " + appendSlash( repository.getUrl() ) + relativeDir );
wagon.putDirectory( inputDirectory, relativeDir );
}
else
{
getLog().info( " >>> to " + appendSlash( repository.getUrl() ) + locale.getLanguage() + "/"
+ relativeDir );
wagon.putDirectory( new File( inputDirectory, locale.getLanguage() ),
locale.getLanguage() + "/" + relativeDir );
}
}
}
catch ( ResourceDoesNotExistException | TransferFailedException | AuthorizationException | ConnectionException
| AuthenticationException e )
{
throw new MojoExecutionException( "Error uploading site", e );
}
}
private static void chmod( final Wagon wagon, final Repository repository, final String chmodOptions,
final String chmodMode )
throws MojoExecutionException
{
try
{
if ( wagon instanceof CommandExecutor )
{
CommandExecutor exec = (CommandExecutor) wagon;
exec.executeCommand( "chmod " + chmodOptions + " " + chmodMode + " " + repository.getBasedir() );
}
// else ? silently ignore, FileWagon is not a CommandExecutor!
}
catch ( CommandExecutionException e )
{
throw new MojoExecutionException( "Error uploading site", e );
}
}
/**
* Get proxy information.
* <p>
* Get the <code>ProxyInfo</code> of the proxy associated with the <code>host</code>
* and the <code>protocol</code> of the given <code>repository</code>.
* </p>
* <p>
* Extract from <a href="https://docs.oracle.com/javase/1.5.0/docs/guide/net/properties.html">
* J2SE Doc : Networking Properties - nonProxyHosts</a> : "The value can be a list of hosts,
* each separated by a |, and in addition a wildcard character (*) can be used for matching"
* </p>
* <p>
* Defensively support comma (",") and semi colon (";") in addition to pipe ("|") as separator.
* </p>
*
* @param repository the Repository to extract the ProxyInfo from
* @param wagonManager the WagonManager used to connect to the Repository
* @return a ProxyInfo object instantiated or <code>null</code> if no matching proxy is found
*/
public static ProxyInfo getProxyInfo( Repository repository, WagonManager wagonManager )
{
ProxyInfo proxyInfo = wagonManager.getProxy( repository.getProtocol() );
if ( proxyInfo == null )
{
return null;
}
String host = repository.getHost();
String nonProxyHostsAsString = proxyInfo.getNonProxyHosts();
for ( String nonProxyHost : StringUtils.split( nonProxyHostsAsString, ",;|" ) )
{
if ( StringUtils.contains( nonProxyHost, "*" ) )
{
// Handle wildcard at the end, beginning or middle of the nonProxyHost
final int pos = nonProxyHost.indexOf( '*' );
String nonProxyHostPrefix = nonProxyHost.substring( 0, pos );
String nonProxyHostSuffix = nonProxyHost.substring( pos + 1 );
// prefix*
if ( StringUtils.isNotEmpty( nonProxyHostPrefix ) && host.startsWith( nonProxyHostPrefix )
&& StringUtils.isEmpty( nonProxyHostSuffix ) )
{
return null;
}
// *suffix
if ( StringUtils.isEmpty( nonProxyHostPrefix ) && StringUtils.isNotEmpty( nonProxyHostSuffix )
&& host.endsWith( nonProxyHostSuffix ) )
{
return null;
}
// prefix*suffix
if ( StringUtils.isNotEmpty( nonProxyHostPrefix ) && host.startsWith( nonProxyHostPrefix )
&& StringUtils.isNotEmpty( nonProxyHostSuffix ) && host.endsWith( nonProxyHostSuffix ) )
{
return null;
}
}
else if ( host.equals( nonProxyHost ) )
{
return null;
}
}
return proxyInfo;
}
/**
* Get proxy information.
*
* @param repository the Repository to extract the ProxyInfo from
* @param settingsDecrypter settings password decrypter
* @return a ProxyInfo object instantiated or <code>null</code> if no matching proxy is found.
*/
private ProxyInfo getProxy( Repository repository, SettingsDecrypter settingsDecrypter )
{
String protocol = repository.getProtocol();
String url = repository.getUrl();
getLog().debug( "repository protocol " + protocol );
String originalProtocol = protocol;
// olamy: hackish here protocol (wagon hint in fact !) is dav
// but the real protocol (transport layer) is http(s)
// and it's the one use in wagon to find the proxy arghhh
// so we will check both
if ( StringUtils.equalsIgnoreCase( "dav", protocol ) && url.startsWith( "dav:" ) )
{
url = url.substring( 4 );
if ( url.startsWith( "http" ) )
{
try
{
URL urlSite = new URL( url );
protocol = urlSite.getProtocol();
getLog().debug( "found dav protocol so transform to real transport protocol " + protocol );
}
catch ( MalformedURLException e )
{
getLog().warn( "fail to build URL with " + url );
}
}
}
else
{
getLog().debug( "getProxy 'protocol': " + protocol );
}
if ( mavenSession != null && protocol != null )
{
MavenExecutionRequest request = mavenSession.getRequest();
if ( request != null )
{
List<Proxy> proxies = request.getProxies();
if ( proxies != null )
{
for ( Proxy proxy : proxies )
{
if ( proxy.isActive() && ( protocol.equalsIgnoreCase( proxy.getProtocol() )
|| originalProtocol.equalsIgnoreCase( proxy.getProtocol() ) ) )
{
SettingsDecryptionResult result =
settingsDecrypter.decrypt( new DefaultSettingsDecryptionRequest( proxy ) );
proxy = result.getProxy();
ProxyInfo proxyInfo = new ProxyInfo();
proxyInfo.setHost( proxy.getHost() );
// so hackish for wagon the protocol is https for site dav:
// dav:https://dav.codehaus.org/mojo/
proxyInfo.setType( protocol ); //proxy.getProtocol() );
proxyInfo.setPort( proxy.getPort() );
proxyInfo.setNonProxyHosts( proxy.getNonProxyHosts() );
proxyInfo.setUserName( proxy.getUsername() );
proxyInfo.setPassword( proxy.getPassword() );
getLog().debug( "found proxyInfo "
+ ( "host:port " + proxyInfo.getHost() + ":" + proxyInfo.getPort()
+ ", " + proxyInfo.getUserName() ) );
return proxyInfo;
}
}
}
}
}
getLog().debug( "getProxy 'protocol': " + protocol + " no ProxyInfo found" );
return null;
}
/**
* Configure the Wagon with the information from serverConfigurationMap ( which comes from settings.xml )
*
* @todo Remove when {@link WagonManager#getWagon(Repository) is available}. It's available in Maven 2.0.5.
*/
private static void configureWagon( Wagon wagon, String repositoryId, Settings settings, PlexusContainer container,
Log log )
throws TransferFailedException
{
log.debug( " configureWagon " );
// MSITE-25: Make sure that the server settings are inserted
for ( Server server : settings.getServers() )
{
String id = server.getId();
log.debug( "configureWagon server " + id );
if ( id != null && id.equals( repositoryId ) && ( server.getConfiguration() != null ) )
{
final PlexusConfiguration plexusConf =
new XmlPlexusConfiguration( (Xpp3Dom) server.getConfiguration() );
ComponentConfigurator componentConfigurator = null;
try
{
componentConfigurator =
(ComponentConfigurator) container.lookup( ComponentConfigurator.ROLE, "basic" );
componentConfigurator.configureComponent( wagon, plexusConf, container.getContainerRealm() );
}
catch ( final ComponentLookupException e )
{
throw new TransferFailedException(
"While configuring wagon for \'" + repositoryId + "\': Unable to lookup wagon configurator."
+ " Wagon configuration cannot be applied.", e );
}
catch ( ComponentConfigurationException e )
{
throw new TransferFailedException( "While configuring wagon for \'" + repositoryId
+ "\': Unable to apply wagon configuration.", e );
}
finally
{
if ( componentConfigurator != null )
{
try
{
container.release( componentConfigurator );
}
catch ( ComponentLifecycleException e )
{
log.error( "Problem releasing configurator - ignoring: " + e.getMessage() );
}
}
}
}
}
}
/**
* {@inheritDoc}
*/
public void contextualize( Context context )
throws ContextException
{
container = (PlexusContainer) context.get( PlexusConstants.PLEXUS_KEY );
}
/**
* Extract the distributionManagement site from the given MavenProject.
*
* @param project the MavenProject. Not null.
* @return the project site. Not null.
* Also site.getUrl() and site.getId() are guaranteed to be not null.
* @throws MojoExecutionException if any of the site info is missing.
*/
protected static Site getSite( final MavenProject project )
throws MojoExecutionException
{
final DistributionManagement distributionManagement = project.getDistributionManagement();
if ( distributionManagement == null )
{
throw new MojoExecutionException( "Missing distribution management in project " + getFullName( project ) );
}
final Site site = distributionManagement.getSite();
if ( site == null )
{
throw new MojoExecutionException( "Missing site information in the distribution management of the project "
+ getFullName( project ) );
}
if ( site.getUrl() == null || site.getId() == null )
{
throw new MojoExecutionException( "Missing site data: specify url and id for project "
+ getFullName( project ) );
}
return site;
}
private static String getFullName( MavenProject project )
{
return project.getName() + " (" + project.getGroupId() + ':' + project.getArtifactId() + ':'
+ project.getVersion() + ')';
}
/**
* Extract the distributionManagement site of the top level parent of the given MavenProject.
* This climbs up the project hierarchy and returns the site of the last project
* for which {@link #getSite(org.apache.maven.project.MavenProject)} returns a site that resides in the
* same site. Notice that it doesn't take into account if the parent is in the reactor or not.
*
* @param project the MavenProject. Not <code>null</code>.
* @return the top level site. Not <code>null</code>.
* Also site.getUrl() and site.getId() are guaranteed to be not <code>null</code>.
* @throws MojoExecutionException if no site info is found in the tree.
* @see URIPathDescriptor#sameSite(java.net.URI)
*/
protected MavenProject getTopLevelProject( MavenProject project )
throws MojoExecutionException
{
Site site = getSite( project );
MavenProject parent = project;
while ( parent.getParent() != null )
{
MavenProject oldProject = parent;
// MSITE-585, MNG-1943
parent = siteTool.getParentProject( parent, reactorProjects, localRepository );
Site oldSite = site;
try
{
site = getSite( parent );
}
catch ( MojoExecutionException e )
{
return oldProject;
}
// MSITE-600
URIPathDescriptor siteURI = new URIPathDescriptor( URIEncoder.encodeURI( site.getUrl() ), "" );
URIPathDescriptor oldSiteURI = new URIPathDescriptor( URIEncoder.encodeURI( oldSite.getUrl() ), "" );
if ( !siteURI.sameSite( oldSiteURI.getBaseURI() ) )
{
return oldProject;
}
}
return parent;
}
private static class URIEncoder
{
private static final String MARK = "-_.!~*'()";
private static final String RESERVED = ";/?:@&=+$,";
private static String encodeURI( final String uriString )
{
final char[] chars = uriString.toCharArray();
final StringBuilder uri = new StringBuilder( chars.length );
// MSITE-750: wagon dav: pseudo-protocol
if ( uriString.startsWith( "dav:http" ) )
{
// transform dav:http to dav-http
chars[3] = '-';
}
for ( char c : chars )
{
if ( ( c >= '0' && c <= '9' ) || ( c >= 'a' && c <= 'z' ) || ( c >= 'A' && c <= 'Z' )
|| MARK.indexOf( c ) != -1 || RESERVED.indexOf( c ) != -1 )
{
uri.append( c );
}
else
{
uri.append( '%' );
uri.append( Integer.toHexString( (int) c ) );
}
}
return uri.toString();
}
}
}