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.nio.file.Files;
22  import java.util.ArrayList;
23  import java.util.Collection;
24  import java.util.LinkedHashMap;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.regex.Matcher;
28  import java.util.regex.Pattern;
29  
30  import org.apache.maven.api.Artifact;
31  import org.apache.maven.api.MojoExecution;
32  import org.apache.maven.api.ProducedArtifact;
33  import org.apache.maven.api.Project;
34  import org.apache.maven.api.RemoteRepository;
35  import org.apache.maven.api.di.Inject;
36  import org.apache.maven.api.model.DistributionManagement;
37  import org.apache.maven.api.model.Plugin;
38  import org.apache.maven.api.model.PluginExecution;
39  import org.apache.maven.api.plugin.MojoException;
40  import org.apache.maven.api.plugin.annotations.Mojo;
41  import org.apache.maven.api.plugin.annotations.Parameter;
42  import org.apache.maven.api.services.ArtifactDeployer;
43  import org.apache.maven.api.services.ArtifactDeployerRequest;
44  import org.apache.maven.api.services.ArtifactManager;
45  import org.apache.maven.api.services.ProjectManager;
46  
47  /**
48   * Deploys an artifact to remote repository.
49   *
50   * @author <a href="mailto:evenisse@apache.org">Emmanuel Venisse</a>
51   * @author <a href="mailto:jdcasey@apache.org">John Casey (refactoring only)</a>
52   */
53  @Mojo(name = "deploy", defaultPhase = "deploy")
54  public class DeployMojo extends AbstractDeployMojo {
55      private static final Pattern ALT_LEGACY_REPO_SYNTAX_PATTERN = Pattern.compile("(.+?)::(.+?)::(.+)");
56  
57      private static final Pattern ALT_REPO_SYNTAX_PATTERN = Pattern.compile("(.+?)::(.+)");
58  
59      @Inject
60      private Project project;
61  
62      @Inject
63      private MojoExecution mojoExecution;
64  
65      /**
66       * Whether every project should be deployed during its own deploy-phase or at the end of the multimodule build. If
67       * set to {@code true} and the build fails, none of the reactor projects is deployed.
68       * <strong>(experimental)</strong>
69       *
70       * @since 2.8
71       */
72      @Parameter(defaultValue = "true", property = "deployAtEnd")
73      private boolean deployAtEnd;
74  
75      /**
76       * Specifies an alternative repository to which the project artifacts should be deployed (other than those specified
77       * in &lt;distributionManagement&gt;). <br/>
78       * Format: <code>id::url</code>
79       * <dl>
80       * <dt>id</dt>
81       * <dd>The id can be used to pick up the correct credentials from the settings.xml</dd>
82       * <dt>url</dt>
83       * <dd>The location of the repository</dd>
84       * </dl>
85       * <b>Note:</b> In version 2.x, the format was <code>id::<i>layout</i>::url</code> where <code><i>layout</i></code>
86       * could be <code>default</code> (ie. Maven 2) or <code>legacy</code> (ie. Maven 1), but since 3.0.0 the layout part
87       * has been removed because Maven 3 only supports Maven 2 repository layout.
88       */
89      @Parameter(property = "altDeploymentRepository")
90      private String altDeploymentRepository;
91  
92      /**
93       * The alternative repository to use when the project has a snapshot version.
94       *
95       * <b>Note:</b> In version 2.x, the format was <code>id::<i>layout</i>::url</code> where <code><i>layout</i></code>
96       * could be <code>default</code> (ie. Maven 2) or <code>legacy</code> (ie. Maven 1), but since 3.0.0 the layout part
97       * has been removed because Maven 3 only supports Maven 2 repository layout.
98       * @since 2.8
99       * @see DeployMojo#altDeploymentRepository
100      */
101     @Parameter(property = "altSnapshotDeploymentRepository")
102     private String altSnapshotDeploymentRepository;
103 
104     /**
105      * The alternative repository to use when the project has a final version.
106      *
107      * <b>Note:</b> In version 2.x, the format was <code>id::<i>layout</i>::url</code> where <code><i>layout</i></code>
108      * could be <code>default</code> (ie. Maven 2) or <code>legacy</code> (ie. Maven 1), but since 3.0.0 the layout part
109      * has been removed because Maven 3 only supports Maven 2 repository layout.
110      * @since 2.8
111      * @see DeployMojo#altDeploymentRepository
112      */
113     @Parameter(property = "altReleaseDeploymentRepository")
114     private String altReleaseDeploymentRepository;
115 
116     /**
117      * Set this to 'true' to bypass artifact deploy
118      * Since 3.0.0-M2 it's not anymore a real boolean as it can have more than 2 values:
119      * <ul>
120      *     <li><code>true</code>: will skip as usual</li>
121      *     <li><code>releases</code>: will skip if current version of the project is a release</li>
122      *     <li><code>snapshots</code>: will skip if current version of the project is a snapshot</li>
123      *     <li>any other values will be considered as <code>false</code></li>
124      * </ul>
125      * @since 2.4
126      */
127     @Parameter(property = "maven.deploy.skip", defaultValue = "false")
128     private String skip = Boolean.FALSE.toString();
129 
130     /**
131      * Set this to <code>true</code> to allow incomplete project processing. By default, such projects are forbidden
132      * and Mojo will fail to process them. Incomplete project is a Maven Project that has any other packaging than
133      * "pom" and has no main artifact packaged. In the majority of cases, what user really wants here is a project
134      * with "pom" packaging and some classified artifact attached (typical example is some assembly being packaged
135      * and attached with classifier).
136      *
137      * @since 3.1.1
138      */
139     @Parameter(defaultValue = "false", property = "allowIncompleteProjects")
140     private boolean allowIncompleteProjects;
141 
142     private enum State {
143         SKIPPED,
144         DEPLOYED,
145         TO_BE_DEPLOYED
146     }
147 
148     public DeployMojo() {}
149 
150     private void putState(State state) {
151         session.getPluginContext(project).put(State.class.getName(), state);
152     }
153 
154     private void putState(ArtifactDeployerRequest request) {
155         session.getPluginContext(project).put(ArtifactDeployerRequest.class.getName(), request);
156     }
157 
158     private State getState(Project project) {
159         return (State) session.getPluginContext(project).get(State.class.getName());
160     }
161 
162     private boolean hasState(Project project) {
163         return session.getPluginContext(project).containsKey(State.class.getName());
164     }
165 
166     public void execute() {
167         if (Boolean.parseBoolean(skip)
168                 || ("releases".equals(skip) && !session.isVersionSnapshot(project.getVersion()))
169                 || ("snapshots".equals(skip) && session.isVersionSnapshot(project.getVersion()))) {
170             getLog().info("Skipping artifact deployment");
171             putState(State.SKIPPED);
172         } else {
173             failIfOffline();
174             warnIfAffectedPackagingAndMaven(project.getPackaging().id());
175 
176             if (!deployAtEnd) {
177                 getLog().info("Deploying deploy for " + project.getGroupId() + ":" + project.getArtifactId() + ":"
178                         + project.getVersion() + " at end");
179                 deploy(createDeployerRequest());
180                 putState(State.DEPLOYED);
181             } else {
182                 // compute the request
183                 putState(State.TO_BE_DEPLOYED);
184                 putState(createDeployerRequest());
185                 if (!allProjectsMarked()) {
186                     getLog().info("Deferring deploy for " + project.getGroupId() + ":" + project.getArtifactId() + ":"
187                             + project.getVersion() + " at end");
188                 }
189             }
190         }
191 
192         if (allProjectsMarked()) {
193             deployAllAtOnce();
194         }
195     }
196 
197     private boolean allProjectsMarked() {
198         return session.getProjects().stream().allMatch(p -> hasState(p) || !hasDeployExecution(p));
199     }
200 
201     private boolean hasDeployExecution(Project p) {
202         String key = mojoExecution.getPlugin().getModel().getKey();
203         Plugin plugin = p.getBuild().getPluginsAsMap().get(key);
204         if (plugin != null) {
205             for (PluginExecution execution : plugin.getExecutions()) {
206                 if (!execution.getGoals().isEmpty() && !"none".equalsIgnoreCase(execution.getPhase())) {
207                     return true;
208                 }
209             }
210         }
211         return false;
212     }
213 
214     private void deployAllAtOnce() {
215         Map<RemoteRepository, Map<Integer, List<ProducedArtifact>>> flattenedRequests = new LinkedHashMap<>();
216         // flatten requests, grouping by remote repository and number of retries
217         for (Project reactorProject : session.getProjects()) {
218             State state = getState(reactorProject);
219             if (state == State.TO_BE_DEPLOYED) {
220                 ArtifactDeployerRequest request = (ArtifactDeployerRequest)
221                         session.getPluginContext(reactorProject).get(ArtifactDeployerRequest.class.getName());
222                 flattenedRequests
223                         .computeIfAbsent(request.getRepository(), r -> new LinkedHashMap<>())
224                         .computeIfAbsent(request.getRetryFailedDeploymentCount(), i -> new ArrayList<>())
225                         .addAll(request.getArtifacts());
226             }
227         }
228         // Re-group all requests
229         List<ArtifactDeployerRequest> requests = new ArrayList<>();
230         for (Map.Entry<RemoteRepository, Map<Integer, List<ProducedArtifact>>> entry1 : flattenedRequests.entrySet()) {
231             for (Map.Entry<Integer, List<ProducedArtifact>> entry2 :
232                     entry1.getValue().entrySet()) {
233                 requests.add(ArtifactDeployerRequest.builder()
234                         .session(session)
235                         .repository(entry1.getKey())
236                         .retryFailedDeploymentCount(entry2.getKey())
237                         .artifacts(entry2.getValue())
238                         .build());
239             }
240         }
241         // Deploy
242         if (!requests.isEmpty()) {
243             requests.forEach(this::deploy);
244         } else {
245             getLog().info("No actual deploy requests");
246         }
247     }
248 
249     private void deploy(ArtifactDeployerRequest request) {
250         try {
251             getLog().info("Deploying artifacts " + request.getArtifacts().toString() + " to repository "
252                     + request.getRepository());
253             getArtifactDeployer().deploy(request);
254         } catch (MojoException e) {
255             throw e;
256         } catch (Exception e) {
257             throw new MojoException(e.getMessage(), e);
258         }
259     }
260 
261     private ArtifactDeployerRequest createDeployerRequest() {
262         ProjectManager projectManager = getProjectManager();
263         Collection<ProducedArtifact> deployables = projectManager.getAllArtifacts(project);
264         Collection<ProducedArtifact> attachedArtifacts = projectManager.getAttachedArtifacts(project);
265 
266         ArtifactManager artifactManager = getArtifactManager();
267         if (artifactManager.getPath(project.getPomArtifact()).isEmpty()) {
268             artifactManager.setPath(project.getPomArtifact(), project.getPomPath());
269         }
270 
271         for (Artifact deployable : deployables) {
272             if (!isValidPath(deployable)) {
273                 if (deployable == project.getMainArtifact().orElse(null)) {
274                     if (attachedArtifacts.isEmpty()) {
275                         throw new MojoException(
276                                 "The packaging for this project did not assign a file to the build artifact");
277                     } else {
278                         if (allowIncompleteProjects) {
279                             getLog().warn("");
280                             getLog().warn("The packaging plugin for this project did not assign");
281                             getLog().warn(
282                                             "a main file to the project but it has attachments. Change packaging to 'pom'.");
283                             getLog().warn("");
284                             getLog().warn("Incomplete projects like this will fail in future Maven versions!");
285                             getLog().warn("");
286                         } else {
287                             throw new MojoException("The packaging plugin for this project did not assign "
288                                     + "a main file to the project but it has attachments. Change packaging to 'pom'.");
289                         }
290                     }
291                 } else {
292                     throw new MojoException("The packaging for this project did not assign "
293                             + "a file to the attached artifact: " + deployable);
294                 }
295             }
296         }
297 
298         ArtifactDeployerRequest request = ArtifactDeployerRequest.builder()
299                 .session(session)
300                 .repository(getDeploymentRepository(session.isVersionSnapshot(project.getVersion())))
301                 .artifacts((Collection) deployables)
302                 .retryFailedDeploymentCount(Math.max(1, Math.min(10, getRetryFailedDeploymentCount())))
303                 .build();
304 
305         return request;
306     }
307 
308     /**
309      * Visible for testing.
310      */
311     RemoteRepository getDeploymentRepository(boolean isSnapshot) throws MojoException {
312         RemoteRepository repo = null;
313 
314         String altDeploymentRepo;
315         if (isSnapshot && altSnapshotDeploymentRepository != null) {
316             altDeploymentRepo = altSnapshotDeploymentRepository;
317         } else if (!isSnapshot && altReleaseDeploymentRepository != null) {
318             altDeploymentRepo = altReleaseDeploymentRepository;
319         } else {
320             altDeploymentRepo = altDeploymentRepository;
321         }
322 
323         if (altDeploymentRepo != null) {
324             getLog().info("Using alternate deployment repository " + altDeploymentRepo);
325 
326             Matcher matcher = ALT_LEGACY_REPO_SYNTAX_PATTERN.matcher(altDeploymentRepo);
327 
328             if (matcher.matches()) {
329                 String id = matcher.group(1).trim();
330                 String layout = matcher.group(2).trim();
331                 String url = matcher.group(3).trim();
332 
333                 if ("default".equals(layout)) {
334                     getLog().warn("Using legacy syntax for alternative repository. " + "Use \"" + id + "::" + url
335                             + "\" instead.");
336                     repo = createDeploymentArtifactRepository(id, url);
337                 } else {
338                     throw new MojoException(
339                             altDeploymentRepo,
340                             "Invalid legacy syntax and layout for repository.",
341                             "Invalid legacy syntax and layout for alternative repository. Use \"" + id + "::" + url
342                                     + "\" instead, and only default layout is supported.");
343                 }
344             } else {
345                 matcher = ALT_REPO_SYNTAX_PATTERN.matcher(altDeploymentRepo);
346 
347                 if (!matcher.matches()) {
348                     throw new MojoException(
349                             altDeploymentRepo,
350                             "Invalid syntax for repository.",
351                             "Invalid syntax for alternative repository. Use \"id::url\".");
352                 } else {
353                     String id = matcher.group(1).trim();
354                     String url = matcher.group(2).trim();
355 
356                     repo = createDeploymentArtifactRepository(id, url);
357                 }
358             }
359         }
360 
361         if (repo == null) {
362             DistributionManagement dm = project.getModel().getDistributionManagement();
363             if (dm != null) {
364                 if (isSnapshot
365                         && dm.getSnapshotRepository() != null
366                         && isNotEmpty(dm.getSnapshotRepository().getId())
367                         && isNotEmpty(dm.getSnapshotRepository().getUrl())) {
368                     repo = session.createRemoteRepository(dm.getSnapshotRepository());
369                 } else if (dm.getRepository() != null
370                         && isNotEmpty(dm.getRepository().getId())
371                         && isNotEmpty(dm.getRepository().getUrl())) {
372                     repo = session.createRemoteRepository(dm.getRepository());
373                 }
374             }
375         }
376 
377         if (repo == null) {
378             String msg = "Deployment failed: repository element was not specified in the POM inside"
379                     + " distributionManagement element or in -DaltDeploymentRepository=id::url parameter";
380 
381             throw new MojoException(msg);
382         }
383 
384         return repo;
385     }
386 
387     private boolean isValidPath(Artifact a) {
388         return getArtifactManager().getPath(a).filter(Files::isRegularFile).isPresent();
389     }
390 
391     private static boolean isNotEmpty(String str) {
392         return str != null && !str.isEmpty();
393     }
394 
395     private ArtifactDeployer getArtifactDeployer() {
396         return session.getService(ArtifactDeployer.class);
397     }
398 
399     private ArtifactManager getArtifactManager() {
400         return session.getService(ArtifactManager.class);
401     }
402 
403     private ProjectManager getProjectManager() {
404         return session.getService(ProjectManager.class);
405     }
406 }