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.StringUtils;
47  import org.codehaus.plexus.util.xml.ReaderFactory;
48  import org.codehaus.plexus.util.xml.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                     && (classifier == null || classifier.isEmpty())
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 or cannot be read.
430      */
431     Model readModel(File pomFile) throws MojoExecutionException {
432         try (Reader reader = ReaderFactory.newXmlReader(pomFile)) {
433             return new MavenXpp3Reader().read(reader);
434         } catch (FileNotFoundException e) {
435             throw new MojoExecutionException("POM not found " + pomFile, e);
436         } catch (IOException e) {
437             throw new MojoExecutionException("Error reading POM " + pomFile, e);
438         } catch (XmlPullParserException e) {
439             throw new MojoExecutionException("Error parsing POM " + pomFile, e);
440         }
441     }
442 
443     /**
444      * Generates a minimal POM from the user-supplied artifact information.
445      *
446      * @return The path to the generated POM file, never <code>null</code>.
447      * @throws MojoExecutionException If the generation failed.
448      */
449     private File generatePomFile() throws MojoExecutionException {
450         Model model = generateModel();
451 
452         try {
453             File tempFile = File.createTempFile("mvndeploy", ".pom");
454             tempFile.deleteOnExit();
455 
456             try (Writer fw = WriterFactory.newXmlWriter(tempFile)) {
457                 new MavenXpp3Writer().write(fw, model);
458             }
459 
460             return tempFile;
461         } catch (IOException e) {
462             throw new MojoExecutionException("Error writing temporary pom file: " + e.getMessage(), e);
463         }
464     }
465 
466     /**
467      * Generates a minimal model from the user-supplied artifact information.
468      *
469      * @return The generated model, never <code>null</code>.
470      */
471     private Model generateModel() {
472         Model model = new Model();
473 
474         model.setModelVersion("4.0.0");
475 
476         model.setGroupId(groupId);
477         model.setArtifactId(artifactId);
478         model.setVersion(version);
479         model.setPackaging(packaging);
480 
481         model.setDescription(description);
482 
483         return model;
484     }
485 
486     void setGroupId(String groupId) {
487         this.groupId = groupId;
488     }
489 
490     void setArtifactId(String artifactId) {
491         this.artifactId = artifactId;
492     }
493 
494     void setVersion(String version) {
495         this.version = version;
496     }
497 
498     void setPackaging(String packaging) {
499         this.packaging = packaging;
500     }
501 
502     void setPomFile(File pomFile) {
503         this.pomFile = pomFile;
504     }
505 
506     String getGroupId() {
507         return groupId;
508     }
509 
510     String getArtifactId() {
511         return artifactId;
512     }
513 
514     String getVersion() {
515         return version;
516     }
517 
518     String getPackaging() {
519         return packaging;
520     }
521 
522     File getFile() {
523         return file;
524     }
525 
526     String getClassifier() {
527         return classifier;
528     }
529 
530     void setClassifier(String classifier) {
531         this.classifier = classifier;
532     }
533 
534     // these below should be shared (duplicated in m-install-p, m-deploy-p)
535 
536     /**
537      * Specialization of {@link FileUtils#getExtension(String)} that honors various {@code tar.xxx} combinations.
538      */
539     private String getExtension(final File file) {
540         String filename = file.getName();
541         if (filename.contains(".tar.")) {
542             return "tar." + FileUtils.getExtension(filename);
543         } else {
544             return FileUtils.getExtension(filename);
545         }
546     }
547 
548     /**
549      * Returns {@code true} if passed in string is "valid Maven ID" (groupId or artifactId).
550      */
551     private boolean isValidId(String id) {
552         if (id == null) {
553             return false;
554         }
555         for (int i = 0; i < id.length(); i++) {
556             char c = id.charAt(i);
557             if (!(c >= 'a' && c <= 'z'
558                     || c >= 'A' && c <= 'Z'
559                     || c >= '0' && c <= '9'
560                     || c == '-'
561                     || c == '_'
562                     || c == '.')) {
563                 return false;
564             }
565         }
566         return true;
567     }
568 
569     private static final String ILLEGAL_VERSION_CHARS = "\\/:\"<>|?*[](){},";
570 
571     /**
572      * Returns {@code true} if passed in string is "valid Maven (simple. non range, expression, etc) version".
573      */
574     private boolean isValidVersion(String version) {
575         if (version == null) {
576             return false;
577         }
578         for (int i = version.length() - 1; i >= 0; i--) {
579             if (ILLEGAL_VERSION_CHARS.indexOf(version.charAt(i)) >= 0) {
580                 return false;
581             }
582         }
583         return true;
584     }
585 }