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.plugins.site.deploy;
20  
21  import java.io.File;
22  import java.net.MalformedURLException;
23  import java.net.URL;
24  import java.util.List;
25  import java.util.Locale;
26  
27  import org.apache.maven.doxia.site.inheritance.URIPathDescriptor;
28  import org.apache.maven.doxia.tools.SiteTool;
29  import org.apache.maven.execution.MavenExecutionRequest;
30  import org.apache.maven.execution.MavenSession;
31  import org.apache.maven.model.DistributionManagement;
32  import org.apache.maven.model.Site;
33  import org.apache.maven.plugin.MojoExecutionException;
34  import org.apache.maven.plugins.annotations.Component;
35  import org.apache.maven.plugins.annotations.Parameter;
36  import org.apache.maven.plugins.site.AbstractSiteMojo;
37  import org.apache.maven.project.MavenProject;
38  import org.apache.maven.settings.Proxy;
39  import org.apache.maven.settings.Server;
40  import org.apache.maven.settings.Settings;
41  import org.apache.maven.settings.crypto.DefaultSettingsDecryptionRequest;
42  import org.apache.maven.settings.crypto.SettingsDecrypter;
43  import org.apache.maven.settings.crypto.SettingsDecryptionResult;
44  import org.apache.maven.wagon.CommandExecutionException;
45  import org.apache.maven.wagon.CommandExecutor;
46  import org.apache.maven.wagon.ConnectionException;
47  import org.apache.maven.wagon.ResourceDoesNotExistException;
48  import org.apache.maven.wagon.TransferFailedException;
49  import org.apache.maven.wagon.Wagon;
50  import org.apache.maven.wagon.authentication.AuthenticationException;
51  import org.apache.maven.wagon.authentication.AuthenticationInfo;
52  import org.apache.maven.wagon.authorization.AuthorizationException;
53  import org.apache.maven.wagon.observers.Debug;
54  import org.apache.maven.wagon.proxy.ProxyInfo;
55  import org.apache.maven.wagon.repository.Repository;
56  import org.codehaus.plexus.PlexusContainer;
57  import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
58  
59  /**
60   * Abstract base class for deploy mojos.
61   * Since 2.3 this includes {@link SiteStageMojo} and {@link SiteStageDeployMojo}.
62   *
63   * @author ltheussl
64   * @since 2.3
65   */
66  public abstract class AbstractDeployMojo extends AbstractSiteMojo {
67      /**
68       * Directory containing the generated project sites and report distributions.
69       *
70       * @since 2.3
71       */
72      @Parameter(alias = "outputDirectory", defaultValue = "${project.reporting.outputDirectory}", required = true)
73      private File inputDirectory;
74  
75      /**
76       * Whether to run the "chmod" command on the remote site after the deploy.
77       * Defaults to "true".
78       *
79       * @since 2.1
80       */
81      @Parameter(property = "maven.site.chmod", defaultValue = "true")
82      private boolean chmod;
83  
84      /**
85       * The mode used by the "chmod" command. Only used if chmod = true.
86       * Defaults to "g+w,a+rX".
87       *
88       * @since 2.1
89       */
90      @Parameter(property = "maven.site.chmod.mode", defaultValue = "g+w,a+rX")
91      private String chmodMode;
92  
93      /**
94       * The options used by the "chmod" command. Only used if chmod = true.
95       * Defaults to "-Rf".
96       *
97       * @since 2.1
98       */
99      @Parameter(property = "maven.site.chmod.options", defaultValue = "-Rf")
100     private String chmodOptions;
101 
102     /**
103      * Set this to 'true' to skip site deployment.
104      *
105      * @since 3.0
106      */
107     @Parameter(property = "maven.site.deploy.skip", defaultValue = "false")
108     private boolean skipDeploy;
109 
110     /**
111      * The current user system settings for use in Maven.
112      */
113     @Parameter(defaultValue = "${settings}", readonly = true)
114     private Settings settings;
115 
116     /**
117      * @since 3.0-beta-2
118      */
119     @Parameter(defaultValue = "${session}", readonly = true)
120     protected MavenSession mavenSession;
121 
122     private String topDistributionManagementSiteUrl;
123 
124     private Site deploySite;
125 
126     @Component
127     private PlexusContainer container;
128 
129     @Component
130     SettingsDecrypter settingsDecrypter;
131 
132     /**
133      * {@inheritDoc}
134      */
135     public void execute() throws MojoExecutionException {
136         if (skip && isDeploy()) {
137             getLog().info("maven.site.skip = true: Skipping site deployment");
138             return;
139         }
140 
141         if (skipDeploy && isDeploy()) {
142             getLog().info("maven.site.deploy.skip = true: Skipping site deployment");
143             return;
144         }
145 
146         deployTo(new Repository(getDeploySite().getId(), getDeploySite().getUrl()));
147     }
148 
149     /**
150      * Make sure the given URL ends with a slash.
151      *
152      * @param url a String
153      * @return if url already ends with '/' it is returned unchanged.
154      *         Otherwise a '/' character is appended.
155      */
156     protected static String appendSlash(final String url) {
157         if (url.endsWith("/")) {
158             return url;
159         } else {
160             return url + "/";
161         }
162     }
163 
164     /**
165      * Detect if the mojo is staging or deploying.
166      *
167      * @return <code>true</code> if the mojo is for deploy and not staging (local or deploy)
168      */
169     protected abstract boolean isDeploy();
170 
171     /**
172      * Get the top distribution management site url, used for module relative path calculations.
173      * This should be a top-level URL, ie above modules and locale sub-directories. Each deploy mojo
174      * can tweak algorithm to determine this top site by implementing determineTopDistributionManagementSiteUrl().
175      *
176      * @return the site for deployment
177      * @throws MojoExecutionException in case of issue
178      * @see #determineTopDistributionManagementSiteUrl()
179      */
180     protected String getTopDistributionManagementSiteUrl() throws MojoExecutionException {
181         if (topDistributionManagementSiteUrl == null) {
182             topDistributionManagementSiteUrl = determineTopDistributionManagementSiteUrl();
183 
184             if (!isDeploy()) {
185                 getLog().debug("distributionManagement.site.url relative path: " + getDeployModuleDirectory());
186             }
187         }
188         return topDistributionManagementSiteUrl;
189     }
190 
191     protected abstract String determineTopDistributionManagementSiteUrl() throws MojoExecutionException;
192 
193     /**
194      * Get the site used for deployment, with its id to look up credential settings and the target URL for the deploy.
195      * This should be a top-level URL, ie above modules and locale sub-directories. Each deploy mojo
196      * can tweak algorithm to determine this deploy site by implementing determineDeploySite().
197      *
198      * @return the site for deployment
199      * @throws MojoExecutionException in case of issue
200      * @see #determineDeploySite()
201      */
202     protected Site getDeploySite() throws MojoExecutionException {
203         if (deploySite == null) {
204             deploySite = determineDeploySite();
205         }
206         return deploySite;
207     }
208 
209     protected abstract Site determineDeploySite() throws MojoExecutionException;
210 
211     /**
212      * Find the relative path between the distribution URLs of the top site and the current project.
213      *
214      * @return the relative path or "./" if the two URLs are the same.
215      * @throws MojoExecutionException in case of issue
216      */
217     protected String getDeployModuleDirectory() throws MojoExecutionException {
218         String to = getSite(project).getUrl();
219 
220         getLog().debug("Mapping url source calculation: ");
221         String from = getTopDistributionManagementSiteUrl();
222 
223         String relative = siteTool.getRelativePath(to, from);
224 
225         // SiteTool.getRelativePath() uses File.separatorChar,
226         // so we need to convert '\' to '/' in order for the URL to be valid for Windows users
227         relative = relative.replace('\\', '/');
228 
229         return ("".equals(relative)) ? "./" : relative;
230     }
231 
232     /**
233      * Use wagon to deploy the generated site to a given repository.
234      *
235      * @param repository the repository to deploy to.
236      *                   This needs to contain a valid, non-null {@link Repository#getId() id}
237      *                   to look up credentials for the deploy, and a valid, non-null
238      *                   {@link Repository#getUrl() scm url} to deploy to.
239      * @throws MojoExecutionException if the deploy fails.
240      */
241     private void deployTo(final Repository repository) throws MojoExecutionException {
242         if (!inputDirectory.exists()) {
243             throw new MojoExecutionException("The site does not exist, please run site:site first");
244         }
245 
246         if (getLog().isDebugEnabled()) {
247             getLog().debug("Deploying to '" + repository.getUrl() + "',\n    Using credentials from server id '"
248                     + repository.getId() + "'");
249         }
250 
251         deploy(inputDirectory, repository);
252     }
253 
254     private void deploy(final File directory, final Repository repository) throws MojoExecutionException {
255         // TODO: work on moving this into the deployer like the other deploy methods
256         final Wagon wagon = getWagon(repository);
257 
258         try {
259             ProxyInfo proxyInfo = getProxy(repository, settingsDecrypter);
260 
261             push(directory, repository, wagon, proxyInfo, getLocales(), getDeployModuleDirectory());
262 
263             if (chmod) {
264                 chmod(wagon, repository, chmodOptions, chmodMode);
265             }
266         } finally {
267             try {
268                 wagon.disconnect();
269             } catch (ConnectionException e) {
270                 getLog().error("Error disconnecting wagon - ignored", e);
271             }
272         }
273     }
274 
275     private Wagon getWagon(final Repository repository) throws MojoExecutionException {
276         String protocol = repository.getProtocol();
277         if (protocol == null) {
278             throw new MojoExecutionException("Unspecified protocol");
279         }
280         try {
281             Wagon wagon = container.lookup(Wagon.class, protocol.toLowerCase(Locale.ROOT));
282             if (!wagon.supportsDirectoryCopy()) {
283                 throw new MojoExecutionException(
284                         "Wagon protocol '" + repository.getProtocol() + "' doesn't support directory copying");
285             }
286             return wagon;
287         } catch (ComponentLookupException e) {
288             throw new MojoExecutionException("Cannot find wagon which supports the requested protocol: " + protocol, e);
289         }
290     }
291 
292     public AuthenticationInfo getAuthenticationInfo(String id) {
293         if (id != null) {
294             List<Server> servers = settings.getServers();
295 
296             if (servers != null) {
297                 for (Server server : servers) {
298                     if (id.equalsIgnoreCase(server.getId())) {
299                         SettingsDecryptionResult result =
300                                 settingsDecrypter.decrypt(new DefaultSettingsDecryptionRequest(server));
301                         server = result.getServer();
302 
303                         AuthenticationInfo authInfo = new AuthenticationInfo();
304                         authInfo.setUserName(server.getUsername());
305                         authInfo.setPassword(server.getPassword());
306                         authInfo.setPrivateKey(server.getPrivateKey());
307                         authInfo.setPassphrase(server.getPassphrase());
308 
309                         return authInfo;
310                     }
311                 }
312             }
313         }
314 
315         return null;
316     }
317 
318     private void push(
319             final File inputDirectory,
320             final Repository repository,
321             final Wagon wagon,
322             final ProxyInfo proxyInfo,
323             final List<Locale> localesList,
324             final String relativeDir)
325             throws MojoExecutionException {
326         AuthenticationInfo authenticationInfo = getAuthenticationInfo(repository.getId());
327         if (authenticationInfo != null) {
328             getLog().debug("authenticationInfo with id '" + repository.getId() + "'");
329         }
330 
331         try {
332             if (getLog().isDebugEnabled()) {
333                 Debug debug = new Debug();
334 
335                 wagon.addSessionListener(debug);
336 
337                 wagon.addTransferListener(debug);
338             }
339 
340             if (proxyInfo != null) {
341                 getLog().debug("connect with proxyInfo");
342                 wagon.connect(repository, authenticationInfo, proxyInfo);
343             } else if (authenticationInfo != null) {
344                 getLog().debug("connect with authenticationInfo and without proxyInfo");
345                 wagon.connect(repository, authenticationInfo);
346             } else {
347                 getLog().debug("connect without authenticationInfo and without proxyInfo");
348                 wagon.connect(repository);
349             }
350 
351             getLog().info("Pushing " + inputDirectory);
352 
353             for (Locale locale : localesList) {
354                 if (!locale.equals(SiteTool.DEFAULT_LOCALE)) {
355                     getLog().info("   >>> to " + appendSlash(repository.getUrl()) + locale + "/" + relativeDir);
356 
357                     wagon.putDirectory(new File(inputDirectory, locale.toString()), locale + "/" + relativeDir);
358                 } else {
359                     // TODO: this also uploads the non-default locales,
360                     // is there a way to exclude directories in wagon?
361                     getLog().info("   >>> to " + appendSlash(repository.getUrl()) + relativeDir);
362 
363                     wagon.putDirectory(inputDirectory, relativeDir);
364                 }
365             }
366         } catch (ResourceDoesNotExistException
367                 | TransferFailedException
368                 | AuthorizationException
369                 | ConnectionException
370                 | AuthenticationException e) {
371             throw new MojoExecutionException("Error uploading site", e);
372         }
373     }
374 
375     private static void chmod(
376             final Wagon wagon, final Repository repository, final String chmodOptions, final String chmodMode)
377             throws MojoExecutionException {
378         try {
379             if (wagon instanceof CommandExecutor) {
380                 CommandExecutor exec = (CommandExecutor) wagon;
381                 exec.executeCommand("chmod " + chmodOptions + " " + chmodMode + " " + repository.getBasedir());
382             }
383             // else ? silently ignore, FileWagon is not a CommandExecutor!
384         } catch (CommandExecutionException e) {
385             throw new MojoExecutionException("Error uploading site", e);
386         }
387     }
388 
389     /**
390      * Get proxy information.
391      *
392      * @param repository        the Repository to extract the ProxyInfo from
393      * @param settingsDecrypter settings password decrypter
394      * @return a ProxyInfo object instantiated or <code>null</code> if no matching proxy is found.
395      */
396     private ProxyInfo getProxy(Repository repository, SettingsDecrypter settingsDecrypter) {
397         String protocol = repository.getProtocol();
398         String url = repository.getUrl();
399 
400         getLog().debug("repository protocol " + protocol);
401 
402         String originalProtocol = protocol;
403         // olamy: hackish here protocol (wagon hint in fact !) is dav
404         // but the real protocol (transport layer) is http(s)
405         // and it's the one use in wagon to find the proxy arghhh
406         // so we will check both
407         if ("dav".equalsIgnoreCase(protocol) && url.startsWith("dav:")) {
408             url = url.substring(4);
409             if (url.startsWith("http")) {
410                 try {
411                     URL urlSite = new URL(url);
412                     protocol = urlSite.getProtocol();
413                     getLog().debug("found dav protocol so transform to real transport protocol " + protocol);
414                 } catch (MalformedURLException e) {
415                     getLog().warn("fail to build URL with " + url);
416                 }
417             }
418         } else {
419             getLog().debug("getProxy 'protocol': " + protocol);
420         }
421 
422         if (mavenSession != null && protocol != null) {
423             MavenExecutionRequest request = mavenSession.getRequest();
424 
425             if (request != null) {
426                 List<Proxy> proxies = request.getProxies();
427 
428                 if (proxies != null) {
429                     for (Proxy proxy : proxies) {
430                         if (proxy.isActive()
431                                 && (protocol.equalsIgnoreCase(proxy.getProtocol())
432                                         || originalProtocol.equalsIgnoreCase(proxy.getProtocol()))) {
433                             SettingsDecryptionResult result =
434                                     settingsDecrypter.decrypt(new DefaultSettingsDecryptionRequest(proxy));
435                             proxy = result.getProxy();
436 
437                             ProxyInfo proxyInfo = new ProxyInfo();
438                             proxyInfo.setHost(proxy.getHost());
439                             // so hackish for wagon the protocol is https for site dav:
440                             // dav:https://dav.codehaus.org/mojo/
441                             proxyInfo.setType(protocol); // proxy.getProtocol() );
442                             proxyInfo.setPort(proxy.getPort());
443                             proxyInfo.setNonProxyHosts(proxy.getNonProxyHosts());
444                             proxyInfo.setUserName(proxy.getUsername());
445                             proxyInfo.setPassword(proxy.getPassword());
446 
447                             getLog().debug("found proxyInfo "
448                                     + ("host:port " + proxyInfo.getHost() + ":" + proxyInfo.getPort() + ", "
449                                             + proxyInfo.getUserName()));
450 
451                             return proxyInfo;
452                         }
453                     }
454                 }
455             }
456         }
457         getLog().debug("getProxy 'protocol': " + protocol + " no ProxyInfo found");
458         return null;
459     }
460 
461     /**
462      * Extract the distributionManagement site from the given MavenProject.
463      *
464      * @param project the MavenProject. Not null.
465      * @return the project site. Not null.
466      *         Also site.getUrl() and site.getId() are guaranteed to be not null.
467      * @throws MojoExecutionException if any of the site info is missing.
468      */
469     protected static Site getSite(final MavenProject project) throws MojoExecutionException {
470         final DistributionManagement distributionManagement = project.getDistributionManagement();
471 
472         if (distributionManagement == null) {
473             throw new MojoExecutionException("Missing distribution management in project " + getFullName(project));
474         }
475 
476         final Site site = distributionManagement.getSite();
477 
478         if (site == null) {
479             throw new MojoExecutionException(
480                     "Missing site information in the distribution management of the project " + getFullName(project));
481         }
482 
483         if (site.getUrl() == null || site.getId() == null) {
484             throw new MojoExecutionException(
485                     "Missing site data: specify url and id for project " + getFullName(project));
486         }
487 
488         return site;
489     }
490 
491     private static String getFullName(MavenProject project) {
492         return project.getName() + " (" + project.getGroupId() + ':' + project.getArtifactId() + ':'
493                 + project.getVersion() + ')';
494     }
495 
496     /**
497      * Extract the distributionManagement site of the top level parent of the given MavenProject.
498      * This climbs up the project hierarchy and returns the site of the last project
499      * for which {@link #getSite(org.apache.maven.project.MavenProject)} returns a site that resides in the
500      * same site. Notice that it doesn't take into account if the parent is in the reactor or not.
501      *
502      * @param project the MavenProject. Not <code>null</code>.
503      * @return the top level site. Not <code>null</code>.
504      *         Also site.getUrl() and site.getId() are guaranteed to be not <code>null</code>.
505      * @throws MojoExecutionException if no site info is found in the tree.
506      * @see URIPathDescriptor#sameSite(java.net.URI)
507      */
508     protected MavenProject getTopLevelProject(MavenProject project) throws MojoExecutionException {
509         Site site = getSite(project);
510 
511         MavenProject parent = project;
512 
513         while (parent.getParent() != null) {
514             MavenProject oldProject = parent;
515             // MSITE-585, MNG-1943
516             parent = parent.getParent();
517 
518             Site oldSite = site;
519 
520             try {
521                 site = getSite(parent);
522             } catch (MojoExecutionException e) {
523                 return oldProject;
524             }
525 
526             // MSITE-600
527             URIPathDescriptor siteURI = new URIPathDescriptor(URIEncoder.encodeURI(site.getUrl()), "");
528             URIPathDescriptor oldSiteURI = new URIPathDescriptor(URIEncoder.encodeURI(oldSite.getUrl()), "");
529 
530             if (!siteURI.sameSite(oldSiteURI.getBaseURI())) {
531                 return oldProject;
532             }
533         }
534 
535         return parent;
536     }
537 
538     private static class URIEncoder {
539         private static final String MARK = "-_.!~*'()";
540         private static final String RESERVED = ";/?:@&=+$,";
541 
542         private static String encodeURI(final String uriString) {
543             final char[] chars = uriString.toCharArray();
544             final StringBuilder uri = new StringBuilder(chars.length);
545 
546             // MSITE-750: wagon dav: pseudo-protocol
547             if (uriString.startsWith("dav:http")) {
548                 // transform dav:http to dav-http
549                 chars[3] = '-';
550             }
551 
552             for (char c : chars) {
553                 if ((c >= '0' && c <= '9')
554                         || (c >= 'a' && c <= 'z')
555                         || (c >= 'A' && c <= 'Z')
556                         || MARK.indexOf(c) != -1
557                         || RESERVED.indexOf(c) != -1) {
558                     uri.append(c);
559                 } else {
560                     uri.append('%');
561                     uri.append(Integer.toHexString((int) c));
562                 }
563             }
564             return uri.toString();
565         }
566     }
567 }