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.deploy;
20  
21  import java.io.File;
22  import java.util.ArrayList;
23  import java.util.LinkedHashMap;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.regex.Matcher;
27  import java.util.regex.Pattern;
28  
29  import org.apache.maven.RepositoryUtils;
30  import org.apache.maven.artifact.ArtifactUtils;
31  import org.apache.maven.model.Plugin;
32  import org.apache.maven.model.PluginExecution;
33  import org.apache.maven.plugin.MojoExecutionException;
34  import org.apache.maven.plugin.MojoFailureException;
35  import org.apache.maven.plugin.descriptor.PluginDescriptor;
36  import org.apache.maven.plugins.annotations.LifecyclePhase;
37  import org.apache.maven.plugins.annotations.Mojo;
38  import org.apache.maven.plugins.annotations.Parameter;
39  import org.apache.maven.project.MavenProject;
40  import org.apache.maven.project.artifact.ProjectArtifact;
41  import org.eclipse.aether.artifact.Artifact;
42  import org.eclipse.aether.deployment.DeployRequest;
43  import org.eclipse.aether.repository.RemoteRepository;
44  import org.eclipse.aether.util.artifact.ArtifactIdUtils;
45  
46  /**
47   * Deploys an artifact to remote repository.
48   *
49   * @author <a href="mailto:evenisse@apache.org">Emmanuel Venisse</a>
50   * @author <a href="mailto:jdcasey@apache.org">John Casey (refactoring only)</a>
51   */
52  @Mojo(name = "deploy", defaultPhase = LifecyclePhase.DEPLOY, threadSafe = true)
53  public class DeployMojo extends AbstractDeployMojo {
54      private static final Pattern ALT_LEGACY_REPO_SYNTAX_PATTERN = Pattern.compile("(.+?)::(.+?)::(.+)");
55  
56      private static final Pattern ALT_REPO_SYNTAX_PATTERN = Pattern.compile("(.+?)::(.+)");
57  
58      @Parameter(defaultValue = "${project}", readonly = true, required = true)
59      private MavenProject project;
60  
61      @Parameter(defaultValue = "${reactorProjects}", required = true, readonly = true)
62      private List<MavenProject> reactorProjects;
63  
64      @Parameter(defaultValue = "${plugin}", required = true, readonly = true)
65      private PluginDescriptor pluginDescriptor;
66  
67      /**
68       * Whether every project should be deployed during its own deploy-phase or at the end of the multimodule build. If
69       * set to {@code true} and the build fails, none of the reactor projects is deployed.
70       *
71       * @since 2.8
72       */
73      @Parameter(defaultValue = "false", property = "deployAtEnd")
74      private boolean deployAtEnd;
75  
76      /**
77       * Specifies an alternative repository to which the project artifacts should be deployed (other than those specified
78       * in &lt;distributionManagement&gt;). <br/>
79       * Format: <code>id::url</code>
80       * <dl>
81       * <dt>id</dt>
82       * <dd>The id can be used to pick up the correct credentials from the settings.xml</dd>
83       * <dt>url</dt>
84       * <dd>The location of the repository</dd>
85       * </dl>
86       * <b>Note:</b> In version 2.x, the format was <code>id::<i>layout</i>::url</code> where <code><i>layout</i></code>
87       * could be <code>default</code> (ie. Maven 2) or <code>legacy</code> (ie. Maven 1), but since 3.0.0 the layout part
88       * has been removed because Maven 3 only supports Maven 2 repository layout.
89       */
90      @Parameter(property = "altDeploymentRepository")
91      private String altDeploymentRepository;
92  
93      /**
94       * The alternative repository to use when the project has a snapshot version.
95       *
96       * <b>Note:</b> In version 2.x, the format was <code>id::<i>layout</i>::url</code> where <code><i>layout</i></code>
97       * could be <code>default</code> (ie. Maven 2) or <code>legacy</code> (ie. Maven 1), but since 3.0.0 the layout part
98       * has been removed because Maven 3 only supports Maven 2 repository layout.
99       * @since 2.8
100      * @see DeployMojo#altDeploymentRepository
101      */
102     @Parameter(property = "altSnapshotDeploymentRepository")
103     private String altSnapshotDeploymentRepository;
104 
105     /**
106      * The alternative repository to use when the project has a final version.
107      *
108      * <b>Note:</b> In version 2.x, the format was <code>id::<i>layout</i>::url</code> where <code><i>layout</i></code>
109      * could be <code>default</code> (ie. Maven 2) or <code>legacy</code> (ie. Maven 1), but since 3.0.0 the layout part
110      * has been removed because Maven 3 only supports Maven 2 repository layout.
111      * @since 2.8
112      * @see DeployMojo#altDeploymentRepository
113      */
114     @Parameter(property = "altReleaseDeploymentRepository")
115     private String altReleaseDeploymentRepository;
116 
117     /**
118      * Set this to 'true' to bypass artifact deploy
119      * Since since 3.0.0-M2 it's not anymore a real boolean as it can have more than 2 values:
120      * <ul>
121      *     <li><code>true</code>: will skip as usual</li>
122      *     <li><code>releases</code>: will skip if current version of the project is a release</li>
123      *     <li><code>snapshots</code>: will skip if current version of the project is a snapshot</li>
124      *     <li>any other values will be considered as <code>false</code></li>
125      * </ul>
126      * @since 2.4
127      */
128     @Parameter(property = "maven.deploy.skip", defaultValue = "false")
129     private String skip = Boolean.FALSE.toString();
130 
131     /**
132      * Set this to <code>true</code> to allow incomplete project processing. By default, such projects are forbidden
133      * and Mojo will fail to process them. Incomplete project is a Maven Project that has any other packaging than
134      * "pom" and has no main artifact packaged. In the majority of cases, what user really wants here is a project
135      * with "pom" packaging and some classified artifact attached (typical example is some assembly being packaged
136      * and attached with classifier).
137      *
138      * @since 3.1.1
139      */
140     @Parameter(defaultValue = "false", property = "allowIncompleteProjects")
141     private boolean allowIncompleteProjects;
142 
143     private enum State {
144         SKIPPED,
145         DEPLOYED,
146         TO_BE_DEPLOYED
147     }
148 
149     private static final String DEPLOY_PROCESSED_MARKER = DeployMojo.class.getName() + ".processed";
150 
151     private static final String DEPLOY_ALT_RELEASE_DEPLOYMENT_REPOSITORY =
152             DeployMojo.class.getName() + ".altReleaseDeploymentRepository";
153 
154     private static final String DEPLOY_ALT_SNAPSHOT_DEPLOYMENT_REPOSITORY =
155             DeployMojo.class.getName() + ".altSnapshotDeploymentRepository";
156 
157     private static final String DEPLOY_ALT_DEPLOYMENT_REPOSITORY =
158             DeployMojo.class.getName() + ".altDeploymentRepository";
159 
160     private void putState(State state) {
161         getPluginContext().put(DEPLOY_PROCESSED_MARKER, state.name());
162     }
163 
164     private void putPluginContextValue(String key, String value) {
165         if (value != null) {
166             getPluginContext().put(key, value);
167         }
168     }
169 
170     private String getPluginContextValue(Map<String, Object> pluginContext, String key) {
171         return (String) pluginContext.get(key);
172     }
173 
174     private State getState(Map<String, Object> pluginContext) {
175         return State.valueOf(getPluginContextValue(pluginContext, DEPLOY_PROCESSED_MARKER));
176     }
177 
178     private boolean hasState(MavenProject project) {
179         Map<String, Object> pluginContext = session.getPluginContext(pluginDescriptor, project);
180         return pluginContext.containsKey(DEPLOY_PROCESSED_MARKER);
181     }
182 
183     public void execute() throws MojoExecutionException, MojoFailureException {
184         State state;
185         if (Boolean.parseBoolean(skip)
186                 || ("releases".equals(skip) && !ArtifactUtils.isSnapshot(project.getVersion()))
187                 || ("snapshots".equals(skip) && ArtifactUtils.isSnapshot(project.getVersion()))) {
188             getLog().info("Skipping artifact deployment");
189             state = State.SKIPPED;
190         } else {
191             failIfOffline();
192             warnIfAffectedPackagingAndMaven(project.getPackaging());
193 
194             if (!deployAtEnd) {
195 
196                 RemoteRepository deploymentRepository = getDeploymentRepository(
197                         project,
198                         altSnapshotDeploymentRepository,
199                         altReleaseDeploymentRepository,
200                         altDeploymentRepository);
201 
202                 DeployRequest request = new DeployRequest();
203                 request.setRepository(deploymentRepository);
204                 processProject(project, request);
205                 deploy(request);
206                 state = State.DEPLOYED;
207             } else {
208                 putPluginContextValue(DEPLOY_ALT_SNAPSHOT_DEPLOYMENT_REPOSITORY, altSnapshotDeploymentRepository);
209                 putPluginContextValue(DEPLOY_ALT_RELEASE_DEPLOYMENT_REPOSITORY, altReleaseDeploymentRepository);
210                 putPluginContextValue(DEPLOY_ALT_DEPLOYMENT_REPOSITORY, altDeploymentRepository);
211                 state = State.TO_BE_DEPLOYED;
212             }
213         }
214 
215         putState(state);
216 
217         List<MavenProject> allProjectsUsingPlugin = getAllProjectsUsingPlugin();
218 
219         if (allProjectsMarked(allProjectsUsingPlugin)) {
220             deployAllAtOnce(allProjectsUsingPlugin);
221         } else if (state == State.TO_BE_DEPLOYED) {
222             getLog().info("Deferring deploy for " + project.getGroupId() + ":" + project.getArtifactId() + ":"
223                     + project.getVersion() + " at end");
224         }
225     }
226 
227     private void deployAllAtOnce(List<MavenProject> allProjectsUsingPlugin) throws MojoExecutionException {
228         Map<RemoteRepository, DeployRequest> requests = new LinkedHashMap<>();
229 
230         // collect all arifacts from all modules to deploy
231         // requests are grouped by used remote repository
232         for (MavenProject reactorProject : allProjectsUsingPlugin) {
233             Map<String, Object> pluginContext = session.getPluginContext(pluginDescriptor, reactorProject);
234             State state = getState(pluginContext);
235             if (state == State.TO_BE_DEPLOYED) {
236 
237                 RemoteRepository deploymentRepository = getDeploymentRepository(
238                         reactorProject,
239                         getPluginContextValue(pluginContext, DEPLOY_ALT_SNAPSHOT_DEPLOYMENT_REPOSITORY),
240                         getPluginContextValue(pluginContext, DEPLOY_ALT_RELEASE_DEPLOYMENT_REPOSITORY),
241                         getPluginContextValue(pluginContext, DEPLOY_ALT_DEPLOYMENT_REPOSITORY));
242 
243                 DeployRequest request = requests.computeIfAbsent(deploymentRepository, repo -> {
244                     DeployRequest newRequest = new DeployRequest();
245                     newRequest.setRepository(repo);
246                     return newRequest;
247                 });
248                 processProject(reactorProject, request);
249             }
250         }
251         // finally execute all deployments request, lets resolver to optimize deployment
252         for (DeployRequest request : requests.values()) {
253             deploy(request);
254         }
255     }
256 
257     private boolean allProjectsMarked(List<MavenProject> allProjectsUsingPlugin) {
258         for (MavenProject reactorProject : allProjectsUsingPlugin) {
259             if (!hasState(reactorProject)) {
260                 return false;
261             }
262         }
263         return true;
264     }
265 
266     private List<MavenProject> getAllProjectsUsingPlugin() {
267         ArrayList<MavenProject> result = new ArrayList<>();
268         for (MavenProject reactorProject : reactorProjects) {
269             if (hasExecution(reactorProject.getPlugin("org.apache.maven.plugins:maven-deploy-plugin"))) {
270                 result.add(reactorProject);
271             }
272         }
273         return result;
274     }
275 
276     private boolean hasExecution(Plugin plugin) {
277         if (plugin == null) {
278             return false;
279         }
280 
281         for (PluginExecution execution : plugin.getExecutions()) {
282             if (!execution.getGoals().isEmpty() && !"none".equalsIgnoreCase(execution.getPhase())) {
283                 return true;
284             }
285         }
286         return false;
287     }
288 
289     private void processProject(final MavenProject project, DeployRequest request) throws MojoExecutionException {
290         // always exists, as project exists
291         Artifact pomArtifact = RepositoryUtils.toArtifact(new ProjectArtifact(project));
292         // always exists, but at "init" is w/o file (packaging plugin assigns file to this when packaged)
293         Artifact projectArtifact = RepositoryUtils.toArtifact(project.getArtifact());
294 
295         // pom project: pomArtifact and projectArtifact are SAME
296         // jar project: pomArtifact and projectArtifact are DIFFERENT
297         // incomplete project: is not pom project and projectArtifact has no file
298 
299         // we must compare coordinates ONLY (as projectArtifact may not have file, and Artifact.equals factors it in)
300         // BUT if projectArtifact has file set, use that one
301         if (ArtifactIdUtils.equalsId(pomArtifact, projectArtifact)) {
302             if (isFile(projectArtifact.getFile())) {
303                 pomArtifact = projectArtifact;
304             }
305             projectArtifact = null;
306         }
307 
308         if (isFile(pomArtifact.getFile())) {
309             request.addArtifact(pomArtifact);
310         } else {
311             throw new MojoExecutionException(
312                     "The POM for project " + project.getArtifactId() + " could not be attached");
313         }
314 
315         // is not packaged, is "incomplete"
316         boolean isIncomplete = projectArtifact != null && !isFile(projectArtifact.getFile());
317         if (projectArtifact != null) {
318             if (!isIncomplete) {
319                 request.addArtifact(projectArtifact);
320             } else if (!project.getAttachedArtifacts().isEmpty()) {
321                 if (allowIncompleteProjects) {
322                     getLog().warn("");
323                     getLog().warn("The packaging plugin for project " + project.getArtifactId() + " did not assign");
324                     getLog().warn("a main file to the project but it has attachments. Change packaging to 'pom'.");
325                     getLog().warn("");
326                     getLog().warn("Incomplete projects like this will fail in future Maven versions!");
327                     getLog().warn("");
328                 } else {
329                     throw new MojoExecutionException("The packaging plugin for project " + project.getArtifactId()
330                             + " did not assign a main file to the project but it has attachments. Change packaging"
331                             + " to 'pom'.");
332                 }
333             } else {
334                 throw new MojoExecutionException("The packaging plugin for project " + project.getArtifactId()
335                         + " did not assign a file to the build artifact");
336             }
337         }
338 
339         for (org.apache.maven.artifact.Artifact attached : project.getAttachedArtifacts()) {
340             getLog().debug("Attaching for deploy: " + attached.getId());
341             request.addArtifact(RepositoryUtils.toArtifact(attached));
342         }
343     }
344 
345     private boolean isFile(File file) {
346         return file != null && file.isFile();
347     }
348 
349     /**
350      * Visible for testing.
351      */
352     RemoteRepository getDeploymentRepository(
353             final MavenProject project,
354             final String altSnapshotDeploymentRepository,
355             final String altReleaseDeploymentRepository,
356             final String altDeploymentRepository)
357             throws MojoExecutionException {
358         RemoteRepository repo = null;
359 
360         String altDeploymentRepo;
361         if (ArtifactUtils.isSnapshot(project.getVersion()) && altSnapshotDeploymentRepository != null) {
362             altDeploymentRepo = altSnapshotDeploymentRepository;
363         } else if (!ArtifactUtils.isSnapshot(project.getVersion()) && altReleaseDeploymentRepository != null) {
364             altDeploymentRepo = altReleaseDeploymentRepository;
365         } else {
366             altDeploymentRepo = altDeploymentRepository;
367         }
368 
369         if (altDeploymentRepo != null) {
370             getLog().info("Using alternate deployment repository " + altDeploymentRepo);
371 
372             Matcher matcher = ALT_LEGACY_REPO_SYNTAX_PATTERN.matcher(altDeploymentRepo);
373 
374             if (matcher.matches()) {
375                 String id = matcher.group(1).trim();
376                 String layout = matcher.group(2).trim();
377                 String url = matcher.group(3).trim();
378 
379                 if ("default".equals(layout)) {
380                     getLog().warn("Using legacy syntax for alternative repository. " + "Use \"" + id + "::" + url
381                             + "\" instead.");
382                     repo = getRemoteRepository(id, url);
383                 } else {
384                     throw new MojoExecutionException(
385                             altDeploymentRepo,
386                             "Invalid legacy syntax and layout for repository.",
387                             "Invalid legacy syntax and layout for alternative repository. Use \"" + id + "::" + url
388                                     + "\" instead, and only default layout is supported.");
389                 }
390             } else {
391                 matcher = ALT_REPO_SYNTAX_PATTERN.matcher(altDeploymentRepo);
392 
393                 if (!matcher.matches()) {
394                     throw new MojoExecutionException(
395                             altDeploymentRepo,
396                             "Invalid syntax for repository.",
397                             "Invalid syntax for alternative repository. Use \"id::url\".");
398                 } else {
399                     String id = matcher.group(1).trim();
400                     String url = matcher.group(2).trim();
401 
402                     repo = getRemoteRepository(id, url);
403                 }
404             }
405         }
406 
407         if (repo == null) {
408             repo = RepositoryUtils.toRepo(project.getDistributionManagementArtifactRepository());
409         }
410 
411         if (repo == null) {
412             String msg = "Deployment failed: repository element was not specified in the POM inside"
413                     + " distributionManagement element or in -DaltDeploymentRepository=id::url parameter";
414 
415             throw new MojoExecutionException(msg);
416         }
417 
418         return repo;
419     }
420 }