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.install;
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.jar.JarEntry;
31  import java.util.jar.JarFile;
32  import java.util.regex.Pattern;
33  
34  import org.apache.maven.execution.MavenSession;
35  import org.apache.maven.model.Model;
36  import org.apache.maven.model.Parent;
37  import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
38  import org.apache.maven.model.io.xpp3.MavenXpp3Writer;
39  import org.apache.maven.plugin.AbstractMojo;
40  import org.apache.maven.plugin.MojoExecutionException;
41  import org.apache.maven.plugin.MojoFailureException;
42  import org.apache.maven.plugins.annotations.Component;
43  import org.apache.maven.plugins.annotations.Mojo;
44  import org.apache.maven.plugins.annotations.Parameter;
45  import org.codehaus.plexus.util.FileUtils;
46  import org.codehaus.plexus.util.IOUtil;
47  import org.codehaus.plexus.util.StringUtils;
48  import org.codehaus.plexus.util.xml.XmlStreamReader;
49  import org.codehaus.plexus.util.xml.XmlStreamWriter;
50  import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
51  import org.eclipse.aether.DefaultRepositoryCache;
52  import org.eclipse.aether.DefaultRepositorySystemSession;
53  import org.eclipse.aether.RepositorySystem;
54  import org.eclipse.aether.RepositorySystemSession;
55  import org.eclipse.aether.artifact.Artifact;
56  import org.eclipse.aether.artifact.ArtifactType;
57  import org.eclipse.aether.artifact.DefaultArtifact;
58  import org.eclipse.aether.installation.InstallRequest;
59  import org.eclipse.aether.installation.InstallationException;
60  import org.eclipse.aether.repository.LocalRepository;
61  import org.eclipse.aether.repository.LocalRepositoryManager;
62  import org.eclipse.aether.util.artifact.SubArtifact;
63  
64  /**
65   * Installs a file in the local repository.
66   *
67   * @author <a href="mailto:brett@apache.org">Brett Porter</a>
68   */
69  @Mojo(name = "install-file", requiresProject = false, aggregator = true, threadSafe = true)
70  public class InstallFileMojo extends AbstractMojo {
71      private static final String LS = System.getProperty("line.separator");
72  
73      @Component
74      private RepositorySystem repositorySystem;
75  
76      @Parameter(defaultValue = "${session}", required = true, readonly = true)
77      private MavenSession session;
78  
79      /**
80       * GroupId of the artifact to be installed. Retrieved from POM file if one is specified or extracted from
81       * {@code pom.xml} in jar if available.
82       */
83      @Parameter(property = "groupId")
84      private String groupId;
85  
86      /**
87       * ArtifactId of the artifact to be installed. Retrieved from POM file if one is specified or extracted from
88       * {@code pom.xml} in jar if available.
89       */
90      @Parameter(property = "artifactId")
91      private String artifactId;
92  
93      /**
94       * Version of the artifact to be installed. Retrieved from POM file if one is specified or extracted from
95       * {@code pom.xml} in jar if available.
96       */
97      @Parameter(property = "version")
98      private String version;
99  
100     /**
101      * Packaging type of the artifact to be installed. Retrieved from POM file if one is specified or extracted from
102      * {@code pom.xml} in jar if available.
103      */
104     @Parameter(property = "packaging")
105     private String packaging;
106 
107     /**
108      * Classifier type of the artifact to be installed. For example, "sources" or "javadoc". Defaults to none which
109      * means this is the project's main artifact.
110      *
111      * @since 2.2
112      */
113     @Parameter(property = "classifier")
114     private String classifier;
115 
116     /**
117      * The file to be installed in the local repository.
118      */
119     @Parameter(property = "file", required = true)
120     private File file;
121 
122     /**
123      * The bundled API docs for the artifact.
124      *
125      * @since 2.3
126      */
127     @Parameter(property = "javadoc")
128     private File javadoc;
129 
130     /**
131      * The bundled sources for the artifact.
132      *
133      * @since 2.3
134      */
135     @Parameter(property = "sources")
136     private File sources;
137 
138     /**
139      * Location of an existing POM file to be installed alongside the main artifact, given by the {@link #file}
140      * parameter.
141      *
142      * @since 2.1
143      */
144     @Parameter(property = "pomFile")
145     private File pomFile;
146 
147     /**
148      * Generate a minimal POM for the artifact if none is supplied via the parameter {@link #pomFile}. Defaults to
149      * <code>true</code> if there is no existing POM in the local repository yet.
150      *
151      * @since 2.1
152      */
153     @Parameter(property = "generatePom")
154     private Boolean generatePom;
155 
156     /**
157      * The path for a specific local repository directory. If not specified the local repository path configured in the
158      * Maven settings will be used.
159      *
160      * @since 2.2
161      */
162     @Parameter(property = "localRepositoryPath")
163     private File localRepositoryPath;
164 
165     @Override
166     public void execute() throws MojoExecutionException, MojoFailureException {
167         if (!file.exists()) {
168             String message = "The specified file '" + file.getPath() + "' does not exist";
169             getLog().error(message);
170             throw new MojoFailureException(message);
171         }
172 
173         RepositorySystemSession repositorySystemSession = session.getRepositorySession();
174         if (localRepositoryPath != null) {
175             // "clone" repository session and replace localRepository
176             DefaultRepositorySystemSession newSession =
177                     new DefaultRepositorySystemSession(session.getRepositorySession());
178             // Clear cache, since we're using a new local repository
179             newSession.setCache(new DefaultRepositoryCache());
180             // keep same repositoryType
181             String contentType = newSession.getLocalRepository().getContentType();
182             if ("enhanced".equals(contentType)) {
183                 contentType = "default";
184             }
185             LocalRepositoryManager localRepositoryManager = repositorySystem.newLocalRepositoryManager(
186                     newSession, new LocalRepository(localRepositoryPath, contentType));
187             newSession.setLocalRepositoryManager(localRepositoryManager);
188             repositorySystemSession = newSession;
189             getLog().debug("localRepoPath: "
190                     + localRepositoryManager.getRepository().getBasedir());
191         }
192 
193         File temporaryPom = null;
194 
195         if (pomFile != null) {
196             processModel(readModel(pomFile));
197         } else {
198             temporaryPom = readingPomFromJarFile();
199             if (!Boolean.TRUE.equals(generatePom)) {
200                 pomFile = temporaryPom;
201                 getLog().debug("Using JAR embedded POM as pomFile");
202             }
203         }
204 
205         if (groupId == null || artifactId == null || version == null || packaging == null) {
206             throw new MojoExecutionException("The artifact information is incomplete: 'groupId', 'artifactId', "
207                     + "'version' and 'packaging' are required.");
208         }
209 
210         if (!isValidId(groupId) || !isValidId(artifactId) || !isValidVersion(version)) {
211             throw new MojoExecutionException("The artifact information is not valid: uses invalid characters.");
212         }
213 
214         InstallRequest installRequest = new InstallRequest();
215 
216         boolean isFilePom = classifier == null && "pom".equals(packaging);
217         if (!isFilePom) {
218             ArtifactType artifactType =
219                     repositorySystemSession.getArtifactTypeRegistry().get(packaging);
220             if (artifactType != null
221                     && StringUtils.isEmpty(classifier)
222                     && !StringUtils.isEmpty(artifactType.getClassifier())) {
223                 classifier = artifactType.getClassifier();
224             }
225         }
226         Artifact mainArtifact = new DefaultArtifact(
227                         groupId, artifactId, classifier, isFilePom ? "pom" : getExtension(file), version)
228                 .setFile(file);
229         installRequest.addArtifact(mainArtifact);
230 
231         File artifactLocalFile = getLocalRepositoryFile(repositorySystemSession, mainArtifact);
232         File pomLocalFile = getPomLocalRepositoryFile(repositorySystemSession, mainArtifact);
233 
234         if (file.equals(artifactLocalFile)) {
235             throw new MojoFailureException("Cannot install artifact. " + "Artifact is already in the local repository."
236                     + LS + LS + "File in question is: " + file + LS);
237         }
238 
239         if (!"pom".equals(packaging)) {
240             if (pomFile != null) {
241                 installRequest.addArtifact(new SubArtifact(mainArtifact, "", "pom", pomFile));
242             } else {
243                 if (Boolean.TRUE.equals(generatePom) || (generatePom == null && !pomLocalFile.exists())) {
244                     temporaryPom = generatePomFile();
245                     getLog().debug("Installing generated POM");
246                     installRequest.addArtifact(new SubArtifact(mainArtifact, "", "pom", temporaryPom));
247                 } else if (generatePom == null) {
248                     getLog().debug("Skipping installation of generated POM, already present in local repository");
249                 }
250             }
251         }
252 
253         if (sources != null) {
254             installRequest.addArtifact(new SubArtifact(mainArtifact, "sources", "jar", sources));
255         }
256 
257         if (javadoc != null) {
258             installRequest.addArtifact(new SubArtifact(mainArtifact, "javadoc", "jar", javadoc));
259         }
260 
261         try {
262             repositorySystem.install(repositorySystemSession, installRequest);
263         } catch (InstallationException e) {
264             throw new MojoExecutionException(e.getMessage(), e);
265         } finally {
266             if (temporaryPom != null) {
267                 // noinspection ResultOfMethodCallIgnored
268                 temporaryPom.delete();
269             }
270         }
271     }
272 
273     private File readingPomFromJarFile() throws MojoExecutionException {
274         File pomFile = null;
275 
276         JarFile jarFile = null;
277         try {
278             Pattern pomEntry = Pattern.compile("META-INF/maven/.*/pom\\.xml");
279 
280             jarFile = new JarFile(file);
281 
282             Enumeration<JarEntry> jarEntries = jarFile.entries();
283 
284             while (jarEntries.hasMoreElements()) {
285                 JarEntry entry = jarEntries.nextElement();
286 
287                 if (pomEntry.matcher(entry.getName()).matches()) {
288                     getLog().debug("Loading " + entry.getName());
289 
290                     InputStream pomInputStream = null;
291                     OutputStream pomOutputStream = null;
292 
293                     try {
294                         pomInputStream = jarFile.getInputStream(entry);
295 
296                         String base = file.getName();
297                         if (base.indexOf('.') > 0) {
298                             base = base.substring(0, base.lastIndexOf('.'));
299                         }
300                         pomFile = File.createTempFile(base, ".pom");
301 
302                         pomOutputStream = Files.newOutputStream(pomFile.toPath());
303 
304                         IOUtil.copy(pomInputStream, pomOutputStream);
305 
306                         pomOutputStream.close();
307                         pomOutputStream = null;
308 
309                         pomInputStream.close();
310                         pomInputStream = null;
311 
312                         processModel(readModel(pomFile));
313 
314                         break;
315                     } finally {
316                         IOUtil.close(pomInputStream);
317                         IOUtil.close(pomOutputStream);
318                     }
319                 }
320             }
321 
322             if (pomFile == null) {
323                 getLog().info("pom.xml not found in " + file.getName());
324             }
325         } catch (IOException e) {
326             // ignore, artifact not packaged by Maven
327         } finally {
328             if (jarFile != null) {
329                 try {
330                     jarFile.close();
331                 } catch (IOException e) {
332                     // we did our best
333                 }
334             }
335         }
336         return pomFile;
337     }
338 
339     /**
340      * Parses a POM.
341      *
342      * @param pomFile The path of the POM file to parse, must not be <code>null</code>.
343      * @return The model from the POM file, never <code>null</code>.
344      * @throws MojoExecutionException If the POM could not be parsed.
345      */
346     private Model readModel(File pomFile) throws MojoExecutionException {
347         Reader reader = null;
348         try {
349             reader = new XmlStreamReader(pomFile);
350             final Model model = new MavenXpp3Reader().read(reader);
351             reader.close();
352             reader = null;
353             return model;
354         } catch (FileNotFoundException e) {
355             throw new MojoExecutionException("File not found " + pomFile, e);
356         } catch (IOException e) {
357             throw new MojoExecutionException("Error reading POM " + pomFile, e);
358         } catch (XmlPullParserException e) {
359             throw new MojoExecutionException("Error parsing POM " + pomFile, e);
360         } finally {
361             IOUtil.close(reader);
362         }
363     }
364 
365     /**
366      * Populates missing mojo parameters from the specified POM.
367      *
368      * @param model The POM to extract missing artifact coordinates from, must not be <code>null</code>.
369      */
370     private void processModel(Model model) {
371         Parent parent = model.getParent();
372 
373         if (this.groupId == null) {
374             this.groupId = model.getGroupId();
375             if (this.groupId == null && parent != null) {
376                 this.groupId = parent.getGroupId();
377             }
378         }
379         if (this.artifactId == null) {
380             this.artifactId = model.getArtifactId();
381         }
382         if (this.version == null) {
383             this.version = model.getVersion();
384             if (this.version == null && parent != null) {
385                 this.version = parent.getVersion();
386             }
387         }
388         if (this.packaging == null) {
389             this.packaging = model.getPackaging();
390         }
391     }
392 
393     /**
394      * Generates a minimal model from the user-supplied artifact information.
395      *
396      * @return The generated model, never <code>null</code>.
397      */
398     private Model generateModel() {
399         Model model = new Model();
400 
401         model.setModelVersion("4.0.0");
402 
403         model.setGroupId(groupId);
404         model.setArtifactId(artifactId);
405         model.setVersion(version);
406         model.setPackaging(packaging);
407 
408         model.setDescription("POM was created from install:install-file");
409 
410         return model;
411     }
412 
413     /**
414      * Generates a (temporary) POM file from the plugin configuration. It's the responsibility of the caller to delete
415      * the generated file when no longer needed.
416      *
417      * @return The path to the generated POM file, never <code>null</code>.
418      * @throws MojoExecutionException If the POM file could not be generated.
419      */
420     private File generatePomFile() throws MojoExecutionException {
421         Model model = generateModel();
422 
423         Writer writer = null;
424         try {
425             File pomFile = File.createTempFile("mvninstall", ".pom");
426 
427             writer = new XmlStreamWriter(pomFile);
428             new MavenXpp3Writer().write(writer, model);
429             writer.close();
430             writer = null;
431 
432             return pomFile;
433         } catch (IOException e) {
434             throw new MojoExecutionException("Error writing temporary POM file: " + e.getMessage(), e);
435         } finally {
436             IOUtil.close(writer);
437         }
438     }
439 
440     /**
441      * Gets the path of the specified artifact within the local repository. Note that the returned path need not exist
442      * (yet).
443      */
444     private File getLocalRepositoryFile(RepositorySystemSession session, Artifact artifact) {
445         String path = session.getLocalRepositoryManager().getPathForLocalArtifact(artifact);
446         return new File(session.getLocalRepository().getBasedir(), path);
447     }
448 
449     /**
450      * Gets the path of the specified artifact POM within the local repository. Note that the returned path need
451      * not exist (yet).
452      */
453     private File getPomLocalRepositoryFile(RepositorySystemSession session, Artifact artifact) {
454         SubArtifact pomArtifact = new SubArtifact(artifact, "", "pom");
455         String path = session.getLocalRepositoryManager().getPathForLocalArtifact(pomArtifact);
456         return new File(session.getLocalRepository().getBasedir(), path);
457     }
458 
459     // these below should be shared (duplicated in m-install-p, m-deploy-p)
460 
461     /**
462      * Specialization of {@link FileUtils#getExtension(String)} that honors various {@code tar.xxx} combinations.
463      */
464     private String getExtension(final File file) {
465         String filename = file.getName();
466         if (filename.contains(".tar.")) {
467             return "tar." + FileUtils.getExtension(filename);
468         } else {
469             return FileUtils.getExtension(filename);
470         }
471     }
472 
473     /**
474      * Returns {@code true} if passed in string is "valid Maven ID" (groupId or artifactId).
475      */
476     private boolean isValidId(String id) {
477         if (id == null) {
478             return false;
479         }
480         for (int i = 0; i < id.length(); i++) {
481             char c = id.charAt(i);
482             if (!(c >= 'a' && c <= 'z'
483                     || c >= 'A' && c <= 'Z'
484                     || c >= '0' && c <= '9'
485                     || c == '-'
486                     || c == '_'
487                     || c == '.')) {
488                 return false;
489             }
490         }
491         return true;
492     }
493 
494     private static final String ILLEGAL_VERSION_CHARS = "\\/:\"<>|?*[](){},";
495 
496     /**
497      * Returns {@code true} if passed in string is "valid Maven (simple. non range, expression, etc) version".
498      */
499     private boolean isValidVersion(String version) {
500         if (version == null) {
501             return false;
502         }
503         for (int i = version.length() - 1; i >= 0; i--) {
504             if (ILLEGAL_VERSION_CHARS.indexOf(version.charAt(i)) >= 0) {
505                 return false;
506             }
507         }
508         return true;
509     }
510 }