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