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.io.FileNotFoundException;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.Writer;
26  import java.nio.file.Files;
27  import java.nio.file.Path;
28  import java.nio.file.Paths;
29  import java.nio.file.StandardCopyOption;
30  import java.util.ArrayList;
31  import java.util.List;
32  import java.util.jar.JarEntry;
33  import java.util.jar.JarFile;
34  import java.util.regex.Pattern;
35  
36  import org.apache.maven.api.Artifact;
37  import org.apache.maven.api.ProducedArtifact;
38  import org.apache.maven.api.RemoteRepository;
39  import org.apache.maven.api.model.Model;
40  import org.apache.maven.api.model.Parent;
41  import org.apache.maven.api.plugin.MojoException;
42  import org.apache.maven.api.plugin.annotations.Mojo;
43  import org.apache.maven.api.plugin.annotations.Parameter;
44  import org.apache.maven.api.services.ArtifactDeployer;
45  import org.apache.maven.api.services.ArtifactDeployerException;
46  import org.apache.maven.api.services.ArtifactDeployerRequest;
47  import org.apache.maven.api.services.ArtifactManager;
48  import org.apache.maven.api.services.xml.ModelXmlFactory;
49  import org.apache.maven.api.services.xml.XmlReaderException;
50  
51  /**
52   * Installs the artifact in the remote repository.
53   *
54   * @author <a href="mailto:aramirez@apache.org">Allan Ramirez</a>
55   */
56  @Mojo(name = "deploy-file", projectRequired = false)
57  @SuppressWarnings("unused")
58  public class DeployFileMojo extends AbstractDeployMojo {
59      private static final String TAR = "tar.";
60      private static final String ILLEGAL_VERSION_CHARS = "\\/:\"<>|?*[](){},";
61  
62      /**
63       * GroupId of the artifact to be deployed. Retrieved from POM file if specified.
64       */
65      @Parameter(property = "groupId")
66      private String groupId;
67  
68      /**
69       * ArtifactId of the artifact to be deployed. Retrieved from POM file if specified.
70       */
71      @Parameter(property = "artifactId")
72      private String artifactId;
73  
74      /**
75       * Version of the artifact to be deployed. Retrieved from POM file if specified.
76       */
77      @Parameter(property = "version")
78      private String version;
79  
80      /**
81       * Type of the artifact to be deployed. Retrieved from the &lt;packaging&gt element of the POM file if a POM file
82       * specified. Defaults to the file extension if it is not specified via command line or POM.<br/>
83       * Maven uses two terms to refer to this datum: the &lt;packaging&gt; element for the entire POM, and the
84       * &lt;type&gt; element in a dependency specification.
85       */
86      @Parameter(property = "packaging")
87      private String packaging;
88  
89      /**
90       * Description passed to a generated POM file (in case of generatePom=true)
91       */
92      @Parameter(property = "generatePom.description")
93      private String description;
94  
95      /**
96       * File to be deployed.
97       */
98      @Parameter(property = "file", required = true)
99      Path file;
100 
101     /**
102      * The bundled API docs for the artifact.
103      *
104      * @since 2.6
105      */
106     @Parameter(property = "javadoc")
107     private Path javadoc;
108 
109     /**
110      * The bundled sources for the artifact.
111      *
112      * @since 2.6
113      */
114     @Parameter(property = "sources")
115     private Path sources;
116 
117     /**
118      * Server Id to map on the &lt;id&gt; under &lt;server&gt; section of settings.xml In most cases, this parameter
119      * will be required for authentication.
120      */
121     @Parameter(property = "repositoryId", defaultValue = "remote-repository", required = true)
122     private String repositoryId;
123 
124     /**
125      * URL where the artifact will be deployed. <br/>
126      * ie ( file:///C:/m2-repo or scp://host.com/path/to/repo )
127      */
128     @Parameter(property = "url", required = true)
129     private String url;
130 
131     /**
132      * Location of an existing POM file to be deployed alongside the main artifact, given by the ${file} parameter.
133      */
134     @Parameter(property = "pomFile")
135     private Path pomFile;
136 
137     /**
138      * Upload a POM for this artifact. Will generate a default POM if none is supplied with the pomFile argument.
139      */
140     @Parameter(property = "generatePom", defaultValue = "true")
141     private boolean generatePom;
142 
143     /**
144      * Add classifier to the artifact
145      */
146     @Parameter(property = "classifier")
147     private String classifier;
148 
149     /**
150      * A comma separated list of types for each of the extra side artifacts to deploy. If there is a mis-match in the
151      * number of entries in {@link #files} or {@link #classifiers}, then an error will be raised.
152      */
153     @Parameter(property = "types")
154     private String types;
155 
156     /**
157      * A comma separated list of classifiers for each of the extra side artifacts to deploy. If there is a mis-match in
158      * the number of entries in {@link #files} or {@link #types}, then an error will be raised.
159      */
160     @Parameter(property = "classifiers")
161     private String classifiers;
162 
163     /**
164      * A comma separated list of files for each of the extra side artifacts to deploy. If there is a mis-match in the
165      * number of entries in {@link #types} or {@link #classifiers}, then an error will be raised.
166      */
167     @Parameter(property = "files")
168     private String files;
169 
170     /**
171      * Set this to 'true' to bypass artifact deploy
172      * It's not a real boolean as it can have more than 2 values:
173      * <ul>
174      *     <li><code>true</code>: will skip as usual</li>
175      *     <li><code>releases</code>: will skip if current version of the project is a release</li>
176      *     <li><code>snapshots</code>: will skip if current version of the project is a snapshot</li>
177      *     <li>any other values will be considered as <code>false</code></li>
178      * </ul>
179      * @since 3.1.0
180      */
181     @Parameter(property = "maven.deploy.file.skip", defaultValue = "false")
182     private String skip = Boolean.FALSE.toString();
183 
184     void initProperties() throws MojoException {
185         Path deployedPom;
186         if (pomFile != null) {
187             deployedPom = pomFile;
188             processModel(readModel(deployedPom));
189         } else {
190             deployedPom = readingPomFromJarFile();
191             if (deployedPom != null) {
192                 pomFile = deployedPom;
193             }
194         }
195 
196         if (packaging == null && file != null) {
197             packaging = getExtension(file);
198         }
199     }
200 
201     private Path readingPomFromJarFile() {
202         Pattern pomEntry = Pattern.compile("META-INF/maven/.*/pom\\.xml");
203         try {
204             try (JarFile jarFile = new JarFile(file.toFile())) {
205                 JarEntry entry = jarFile.stream()
206                         .filter(e -> pomEntry.matcher(e.getName()).matches())
207                         .findFirst()
208                         .orElse(null);
209                 if (entry != null) {
210                     getLog().debug("Using " + entry.getName() + " as pomFile");
211 
212                     try (InputStream pomInputStream = jarFile.getInputStream(entry)) {
213                         String base = file.getFileName().toString();
214                         if (base.indexOf('.') > 0) {
215                             base = base.substring(0, base.lastIndexOf('.'));
216                         }
217                         Path pomFile = File.createTempFile(base, ".pom").toPath();
218 
219                         Files.copy(pomInputStream, pomFile, StandardCopyOption.REPLACE_EXISTING);
220 
221                         processModel(readModel(pomFile));
222 
223                         return pomFile;
224                     }
225                 } else {
226                     getLog().info("pom.xml not found in " + file.getFileName());
227                 }
228             }
229         } catch (IOException e) {
230             // ignore, artifact not packaged by Maven
231         }
232         return null;
233     }
234 
235     @SuppressWarnings("checkstyle:MethodLength")
236     public void execute() throws MojoException {
237         if (Boolean.parseBoolean(skip)
238                 || ("releases".equals(skip) && !session.isVersionSnapshot(version))
239                 || ("snapshots".equals(skip) && session.isVersionSnapshot(version))) {
240             getLog().info("Skipping artifact deployment");
241             return;
242         }
243 
244         if (!Files.exists(file)) {
245             String message = "The specified file '" + file + "' does not exist";
246             getLog().error(message);
247             throw new MojoException(message);
248         }
249 
250         initProperties();
251 
252         RemoteRepository deploymentRepository =
253                 createDeploymentArtifactRepository(repositoryId, url.replace(File.separator, "/"));
254 
255         if (deploymentRepository.getProtocol().isEmpty()) {
256             throw new MojoException("No transfer protocol found.");
257         }
258 
259         Path deployedPom;
260         if (pomFile != null) {
261             deployedPom = pomFile;
262             processModel(readModel(deployedPom));
263         } else {
264             deployedPom = readingPomFromJarFile();
265         }
266 
267         if (groupId == null || artifactId == null || version == null || packaging == null) {
268             throw new MojoException("The artifact information is incomplete: 'groupId', 'artifactId', "
269                     + "'version' and 'packaging' are required.");
270         }
271 
272         if (!isValidId(groupId) || !isValidId(artifactId) || !isValidVersion(version)) {
273             throw new MojoException("The artifact information is not valid: uses invalid characters.");
274         }
275 
276         failIfOffline();
277         warnIfAffectedPackagingAndMaven(packaging);
278 
279         List<ProducedArtifact> deployables = new ArrayList<>();
280 
281         boolean isFilePom = classifier == null && "pom".equals(packaging);
282         ProducedArtifact artifact = session.createProducedArtifact(
283                 groupId, artifactId, version, classifier, isFilePom ? "pom" : getExtension(file), packaging);
284 
285         if (file.equals(getLocalRepositoryFile(artifact))) {
286             throw new MojoException("Cannot deploy artifact from the local repository: " + file);
287         }
288 
289         ArtifactManager artifactManager = session.getService(ArtifactManager.class);
290         artifactManager.setPath(artifact, file);
291         deployables.add(artifact);
292 
293         ProducedArtifact pomArtifact = null;
294         if (!isFilePom) {
295             pomArtifact = session.createProducedArtifact(groupId, artifactId, version, "", "pom", null);
296             if (deployedPom != null) {
297                 artifactManager.setPath(pomArtifact, deployedPom);
298                 deployables.add(pomArtifact);
299             } else {
300                 deployedPom = generatePomFile();
301                 artifactManager.setPath(pomArtifact, deployedPom);
302                 if (generatePom) {
303                     getLog().debug("Deploying generated POM");
304                     deployables.add(pomArtifact);
305                 } else {
306                     getLog().debug("Skipping deploying POM");
307                 }
308             }
309         }
310 
311         if (sources != null) {
312             ProducedArtifact sourcesArtifact =
313                     session.createProducedArtifact(groupId, artifactId, version, "sources", "jar", null);
314             artifactManager.setPath(sourcesArtifact, sources);
315             deployables.add(sourcesArtifact);
316         }
317 
318         if (javadoc != null) {
319             ProducedArtifact javadocArtifact =
320                     session.createProducedArtifact(groupId, artifactId, version, "javadoc", "jar", null);
321             artifactManager.setPath(javadocArtifact, javadoc);
322             deployables.add(javadocArtifact);
323         }
324 
325         if (files != null) {
326             if (types == null) {
327                 throw new MojoException("You must specify 'types' if you specify 'files'");
328             }
329             if (classifiers == null) {
330                 throw new MojoException("You must specify 'classifiers' if you specify 'files'");
331             }
332             int filesLength = countCommas(files);
333             int typesLength = countCommas(types);
334             int classifiersLength = countCommas(classifiers);
335             if (typesLength != filesLength) {
336                 throw new MojoException("You must specify the same number of entries in 'files' and "
337                         + "'types' (respectively " + filesLength + " and " + typesLength + " entries )");
338             }
339             if (classifiersLength != filesLength) {
340                 throw new MojoException("You must specify the same number of entries in 'files' and "
341                         + "'classifiers' (respectively " + filesLength + " and " + classifiersLength + " entries )");
342             }
343             int fi = 0;
344             int ti = 0;
345             int ci = 0;
346             for (int i = 0; i <= filesLength; i++) {
347                 int nfi = files.indexOf(',', fi);
348                 if (nfi == -1) {
349                     nfi = files.length();
350                 }
351                 int nti = types.indexOf(',', ti);
352                 if (nti == -1) {
353                     nti = types.length();
354                 }
355                 int nci = classifiers.indexOf(',', ci);
356                 if (nci == -1) {
357                     nci = classifiers.length();
358                 }
359                 Path file = Paths.get(files.substring(fi, nfi).replace("/", File.separator));
360                 if (!Files.isRegularFile(file)) {
361                     // try relative to the project basedir just in case
362                     file = Paths.get(files.substring(fi, nfi));
363                 }
364                 if (Files.isRegularFile(file)) {
365                     String extension = getExtension(file);
366                     String type = types.substring(ti, nti).trim();
367 
368                     ProducedArtifact deployable = session.createProducedArtifact(
369                             artifact.getGroupId(),
370                             artifact.getArtifactId(),
371                             artifact.getVersion().asString(),
372                             classifiers.substring(ci, nci).trim(),
373                             extension,
374                             type);
375                     artifactManager.setPath(deployable, file);
376                     deployables.add(deployable);
377                 } else {
378                     throw new MojoException("Specified side artifact " + file + " does not exist");
379                 }
380                 fi = nfi + 1;
381                 ti = nti + 1;
382                 ci = nci + 1;
383             }
384         } else {
385             if (types != null) {
386                 throw new MojoException("You must specify 'files' if you specify 'types'");
387             }
388             if (classifiers != null) {
389                 throw new MojoException("You must specify 'files' if you specify 'classifiers'");
390             }
391         }
392 
393         try {
394             ArtifactDeployerRequest deployRequest = ArtifactDeployerRequest.builder()
395                     .session(session)
396                     .repository(deploymentRepository)
397                     .artifacts(deployables)
398                     .retryFailedDeploymentCount(Math.max(1, Math.min(10, getRetryFailedDeploymentCount())))
399                     .build();
400 
401             getLog().info("Deploying artifacts " + deployables + " to repository " + deploymentRepository);
402             ArtifactDeployer artifactDeployer = session.getService(ArtifactDeployer.class);
403             artifactDeployer.deploy(deployRequest);
404         } catch (ArtifactDeployerException e) {
405             throw new MojoException(e.getMessage(), e);
406         } finally {
407             if (pomFile == null && deployedPom != null) {
408                 try {
409                     Files.deleteIfExists(deployedPom);
410                 } catch (IOException e) {
411                     // ignore
412                 }
413                 if (pomArtifact != null) {
414                     artifactManager.setPath(pomArtifact, null);
415                 }
416             }
417         }
418     }
419 
420     /**
421      * Gets the path of the specified artifact within the local repository. Note that the returned path need not exist
422      * (yet).
423      */
424     private Path getLocalRepositoryFile(Artifact artifact) {
425         return session.getPathForLocalArtifact(artifact);
426     }
427 
428     /**
429      * Process the supplied pomFile to get groupId, artifactId, version, and packaging
430      *
431      * @param model The POM to extract missing artifact coordinates from, must not be <code>null</code>.
432      */
433     private void processModel(Model model) {
434         Parent parent = model.getParent();
435 
436         if (this.groupId == null) {
437             this.groupId = model.getGroupId();
438             if (this.groupId == null && parent != null) {
439                 this.groupId = parent.getGroupId();
440             }
441         }
442         if (this.artifactId == null) {
443             this.artifactId = model.getArtifactId();
444         }
445         if (this.version == null) {
446             this.version = model.getVersion();
447             if (this.version == null && parent != null) {
448                 this.version = parent.getVersion();
449             }
450         }
451         if (this.packaging == null) {
452             this.packaging = model.getPackaging();
453         }
454     }
455 
456     /**
457      * Extract the model from the specified POM file.
458      *
459      * @param pomFile The path of the POM file to parse, must not be <code>null</code>.
460      * @return The model from the POM file, never <code>null</code>.
461      * @throws MojoException If the file doesn't exist of cannot be read.
462      */
463     Model readModel(Path pomFile) throws MojoException {
464         try (InputStream is = Files.newInputStream(pomFile)) {
465             ModelXmlFactory modelXmlFactory = session.getService(ModelXmlFactory.class);
466             return modelXmlFactory.read(is);
467         } catch (FileNotFoundException e) {
468             throw new MojoException("POM not found " + pomFile, e);
469         } catch (IOException e) {
470             throw new MojoException("Error reading POM " + pomFile, e);
471         } catch (XmlReaderException e) {
472             throw new MojoException("Error parsing POM " + pomFile, e);
473         }
474     }
475 
476     /**
477      * Generates a minimal POM from the user-supplied artifact information.
478      *
479      * @return The path to the generated POM file, never <code>null</code>.
480      * @throws MojoException If the generation failed.
481      */
482     private Path generatePomFile() throws MojoException {
483         Model model = generateModel();
484         try {
485             Path pomFile = File.createTempFile("mvndeploy", ".pom").toPath();
486             try (Writer writer = Files.newBufferedWriter(pomFile)) {
487                 ModelXmlFactory modelXmlFactory = session.getService(ModelXmlFactory.class);
488                 modelXmlFactory.write(model, writer);
489             }
490             return pomFile;
491         } catch (IOException e) {
492             throw new MojoException("Error writing temporary POM file: " + e.getMessage(), e);
493         }
494     }
495 
496     /**
497      * Generates a minimal model from the user-supplied artifact information.
498      *
499      * @return The generated model, never <code>null</code>.
500      */
501     private Model generateModel() {
502         return Model.newBuilder()
503                 .modelVersion("4.0.0")
504                 .groupId(groupId)
505                 .artifactId(artifactId)
506                 .version(version)
507                 .packaging(packaging)
508                 .description(description)
509                 .build();
510     }
511 
512     void setGroupId(String groupId) {
513         this.groupId = groupId;
514     }
515 
516     void setArtifactId(String artifactId) {
517         this.artifactId = artifactId;
518     }
519 
520     void setVersion(String version) {
521         this.version = version;
522     }
523 
524     void setPackaging(String packaging) {
525         this.packaging = packaging;
526     }
527 
528     void setPomFile(Path pomFile) {
529         this.pomFile = pomFile;
530     }
531 
532     String getGroupId() {
533         return groupId;
534     }
535 
536     String getArtifactId() {
537         return artifactId;
538     }
539 
540     String getVersion() {
541         return version;
542     }
543 
544     String getPackaging() {
545         return packaging;
546     }
547 
548     Path getFile() {
549         return file;
550     }
551 
552     String getClassifier() {
553         return classifier;
554     }
555 
556     void setClassifier(String classifier) {
557         this.classifier = classifier;
558     }
559 
560     // these below should be shared (duplicated in m-install-p, m-deploy-p)
561 
562     private static int countCommas(String str) {
563         int count = 0;
564         int idx = 0;
565         while ((idx = str.indexOf(',', idx)) != -1) {
566             count++;
567             idx++;
568         }
569         return count;
570     }
571 
572     /**
573      * Get file extension, honoring various {@code tar.xxx} combinations.
574      */
575     private String getExtension(final Path file) {
576         String filename = file.getFileName().toString();
577         int lastDot = filename.lastIndexOf('.');
578         if (lastDot > 0 && lastDot < filename.length() - 1) {
579             String ext = filename.substring(lastDot + 1);
580             return filename.regionMatches(lastDot + 1 - TAR.length(), TAR, 0, TAR.length()) ? TAR + ext : ext;
581         }
582         return "";
583     }
584 
585     /**
586      * Returns {@code true} if passed in string is "valid Maven ID" (groupId or artifactId).
587      */
588     private boolean isValidId(String id) {
589         if (id == null) {
590             return false;
591         }
592         for (int i = 0; i < id.length(); i++) {
593             char c = id.charAt(i);
594             if (!(c >= 'a' && c <= 'z'
595                     || c >= 'A' && c <= 'Z'
596                     || c >= '0' && c <= '9'
597                     || c == '-'
598                     || c == '_'
599                     || c == '.')) {
600                 return false;
601             }
602         }
603         return true;
604     }
605 
606     /**
607      * Returns {@code true} if passed in string is "valid Maven (simple. non range, expression, etc) version".
608      */
609     private boolean isValidVersion(String version) {
610         if (version == null) {
611             return false;
612         }
613         for (int i = version.length() - 1; i >= 0; i--) {
614             if (ILLEGAL_VERSION_CHARS.indexOf(version.charAt(i)) >= 0) {
615                 return false;
616             }
617         }
618         return true;
619     }
620 }