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.Writer;
26  import java.nio.file.Files;
27  import java.nio.file.Path;
28  import java.nio.file.StandardCopyOption;
29  import java.util.ArrayList;
30  import java.util.List;
31  import java.util.jar.JarEntry;
32  import java.util.jar.JarFile;
33  import java.util.regex.Pattern;
34  
35  import org.apache.maven.api.Artifact;
36  import org.apache.maven.api.ProducedArtifact;
37  import org.apache.maven.api.Session;
38  import org.apache.maven.api.di.Inject;
39  import org.apache.maven.api.model.Model;
40  import org.apache.maven.api.model.Parent;
41  import org.apache.maven.api.plugin.Log;
42  import org.apache.maven.api.plugin.MojoException;
43  import org.apache.maven.api.plugin.annotations.Mojo;
44  import org.apache.maven.api.plugin.annotations.Parameter;
45  import org.apache.maven.api.services.ArtifactInstaller;
46  import org.apache.maven.api.services.ArtifactInstallerException;
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 a file in the local repository.
53   */
54  @Mojo(name = "install-file", projectRequired = false, aggregator = true)
55  @SuppressWarnings("unused")
56  public class InstallFileMojo implements org.apache.maven.api.plugin.Mojo {
57      private static final String TAR = "tar.";
58      private static final String ILLEGAL_VERSION_CHARS = "\\/:\"<>|?*[](){},";
59  
60      @Inject
61      private Log log;
62  
63      @Inject
64      private Session session;
65  
66      /**
67       * GroupId of the artifact to be installed. Retrieved from POM file if one is specified or extracted from
68       * {@code pom.xml} in jar if available.
69       */
70      @Parameter(property = "groupId")
71      private String groupId;
72  
73      /**
74       * ArtifactId of the artifact to be installed. Retrieved from POM file if one is specified or extracted from
75       * {@code pom.xml} in jar if available.
76       */
77      @Parameter(property = "artifactId")
78      private String artifactId;
79  
80      /**
81       * Version of the artifact to be installed. Retrieved from POM file if one is specified or extracted from
82       * {@code pom.xml} in jar if available.
83       */
84      @Parameter(property = "version")
85      private String version;
86  
87      /**
88       * Packaging type of the artifact to be installed. Retrieved from POM file if one is specified or extracted from
89       * {@code pom.xml} in jar if available.
90       */
91      @Parameter(property = "packaging")
92      private String packaging;
93  
94      /**
95       * Classifier type of the artifact to be installed. For example, "sources" or "javadoc". Defaults to none which
96       * means this is the project's main artifact.
97       *
98       * @since 2.2
99       */
100     @Parameter(property = "classifier")
101     private String classifier;
102 
103     /**
104      * The file to be installed in the local repository.
105      */
106     @Parameter(property = "file", required = true)
107     private Path file;
108 
109     /**
110      * The bundled API docs for the artifact.
111      *
112      * @since 2.3
113      */
114     @Parameter(property = "javadoc")
115     private Path javadoc;
116 
117     /**
118      * The bundled sources for the artifact.
119      *
120      * @since 2.3
121      */
122     @Parameter(property = "sources")
123     private Path sources;
124 
125     /**
126      * Location of an existing POM file to be installed alongside the main artifact, given by the {@link #file}
127      * parameter.
128      *
129      * @since 2.1
130      */
131     @Parameter(property = "pomFile")
132     private Path pomFile;
133 
134     /**
135      * Generate a minimal POM for the artifact if none is supplied via the parameter {@link #pomFile}. Defaults to
136      * <code>true</code> if there is no existing POM in the local repository yet.
137      *
138      * @since 2.1
139      */
140     @Parameter(property = "generatePom")
141     private Boolean generatePom;
142 
143     /**
144      * The path for a specific local repository directory. If not specified the local repository path configured in the
145      * Maven settings will be used.
146      *
147      * @since 2.2
148      */
149     @Parameter(property = "localRepositoryPath")
150     private Path localRepositoryPath;
151 
152     @Override
153     public void execute() {
154         if (!Files.exists(file)) {
155             String message = "The specified file '" + file + "' does not exist";
156             log.error(message);
157             throw new MojoException(message);
158         }
159 
160         Session session = this.session;
161 
162         List<ProducedArtifact> installableArtifacts = new ArrayList<>();
163 
164         // Override the default local repository
165         if (localRepositoryPath != null) {
166             session = session.withLocalRepository(session.createLocalRepository(localRepositoryPath));
167 
168             log.debug("localRepoPath: " + localRepositoryPath);
169         }
170 
171         Path deployedPom;
172         Path temporaryPom = null;
173         if (pomFile != null) {
174             deployedPom = pomFile;
175             processModel(readModel(deployedPom));
176         } else {
177             if (!Boolean.TRUE.equals(generatePom)) {
178                 temporaryPom = readingPomFromJarFile();
179                 deployedPom = temporaryPom;
180                 if (deployedPom != null) {
181                     log.debug("Using JAR embedded POM as pomFile");
182                 }
183             } else {
184                 deployedPom = null;
185             }
186         }
187 
188         if (groupId == null || artifactId == null || version == null || packaging == null) {
189             throw new MojoException("The artifact information is incomplete: 'groupId', 'artifactId', "
190                     + "'version' and 'packaging' are required.");
191         }
192 
193         if (!isValidId(groupId) || !isValidId(artifactId) || !isValidVersion(version)) {
194             throw new MojoException("The artifact information is not valid: uses invalid characters.");
195         }
196 
197         boolean isFilePom = classifier == null && "pom".equals(packaging);
198         ProducedArtifact artifact = session.createProducedArtifact(
199                 groupId, artifactId, version, classifier, isFilePom ? "pom" : getExtension(file), packaging);
200 
201         if (file.equals(getLocalRepositoryFile(artifact))) {
202             throw new MojoException("Cannot install artifact. "
203                     + "Artifact is already in the local repository.\n\nFile in question is: " + file + "\n");
204         }
205 
206         ArtifactManager artifactManager = session.getService(ArtifactManager.class);
207         artifactManager.setPath(artifact, file);
208         installableArtifacts.add(artifact);
209 
210         ProducedArtifact pomArtifact = null;
211         if (!isFilePom) {
212             pomArtifact = session.createProducedArtifact(groupId, artifactId, version, null, "pom", null);
213             if (deployedPom != null) {
214                 artifactManager.setPath(pomArtifact, deployedPom);
215                 installableArtifacts.add(pomArtifact);
216             } else {
217                 temporaryPom = generatePomFile();
218                 deployedPom = temporaryPom;
219                 artifactManager.setPath(pomArtifact, deployedPom);
220                 if (Boolean.TRUE.equals(generatePom)
221                         || (generatePom == null && !Files.exists(getLocalRepositoryFile(pomArtifact)))) {
222                     log.debug("Installing generated POM");
223                     installableArtifacts.add(pomArtifact);
224                 } else if (generatePom == null) {
225                     log.debug("Skipping installation of generated POM, already present in local repository");
226                 }
227             }
228         }
229 
230         if (sources != null) {
231             ProducedArtifact sourcesArtifact =
232                     session.createProducedArtifact(groupId, artifactId, version, "sources", "jar", null);
233             artifactManager.setPath(sourcesArtifact, sources);
234             installableArtifacts.add(sourcesArtifact);
235         }
236 
237         if (javadoc != null) {
238             ProducedArtifact javadocArtifact =
239                     session.createProducedArtifact(groupId, artifactId, version, "javadoc", "jar", null);
240             artifactManager.setPath(javadocArtifact, javadoc);
241             installableArtifacts.add(javadocArtifact);
242         }
243 
244         try {
245             ArtifactInstaller artifactInstaller = session.getService(ArtifactInstaller.class);
246             artifactInstaller.install(session, installableArtifacts);
247         } catch (ArtifactInstallerException e) {
248             throw new MojoException(e.getMessage(), e);
249         } finally {
250             if (temporaryPom != null) {
251                 try {
252                     Files.deleteIfExists(temporaryPom);
253                 } catch (IOException e) {
254                     // ignore
255                 }
256                 if (pomArtifact != null) {
257                     artifactManager.setPath(pomArtifact, null);
258                 }
259             }
260         }
261     }
262 
263     private Path readingPomFromJarFile() {
264         Pattern pomEntry = Pattern.compile("META-INF/maven/.*/pom\\.xml");
265         try {
266             try (JarFile jarFile = new JarFile(file.toFile())) {
267                 JarEntry entry = jarFile.stream()
268                         .filter(e -> pomEntry.matcher(e.getName()).matches())
269                         .findFirst()
270                         .orElse(null);
271                 if (entry != null) {
272                     log.debug("Loading " + entry.getName());
273 
274                     try (InputStream pomInputStream = jarFile.getInputStream(entry)) {
275                         String base = file.getFileName().toString();
276                         if (base.indexOf('.') > 0) {
277                             base = base.substring(0, base.lastIndexOf('.'));
278                         }
279                         Path pomFile = File.createTempFile(base, ".pom").toPath();
280 
281                         Files.copy(pomInputStream, pomFile, StandardCopyOption.REPLACE_EXISTING);
282 
283                         processModel(readModel(pomFile));
284 
285                         return pomFile;
286                     }
287                 } else {
288                     log.info("pom.xml not found in " + file.getFileName());
289                 }
290             }
291         } catch (IOException e) {
292             // ignore, artifact not packaged by Maven
293         }
294         return null;
295     }
296 
297     /**
298      * Parses a POM.
299      *
300      * @param pomFile The path of the POM file to parse, must not be <code>null</code>.
301      * @return The model from the POM file, never <code>null</code>.
302      * @throws MojoException If the POM could not be parsed.
303      */
304     private Model readModel(Path pomFile) throws MojoException {
305         try {
306             try (InputStream is = Files.newInputStream(pomFile)) {
307                 return session.getService(ModelXmlFactory.class).read(is);
308             }
309         } catch (FileNotFoundException e) {
310             throw new MojoException("File not found " + pomFile, e);
311         } catch (IOException e) {
312             throw new MojoException("Error reading POM " + pomFile, e);
313         } catch (XmlReaderException e) {
314             throw new MojoException("Error parsing POM " + pomFile, e);
315         }
316     }
317 
318     /**
319      * Populates missing mojo parameters from the specified POM.
320      *
321      * @param model The POM to extract missing artifact coordinates from, must not be <code>null</code>.
322      */
323     private void processModel(Model model) {
324         Parent parent = model.getParent();
325 
326         if (this.groupId == null) {
327             this.groupId = model.getGroupId();
328             if (this.groupId == null && parent != null) {
329                 this.groupId = parent.getGroupId();
330             }
331         }
332         if (this.artifactId == null) {
333             this.artifactId = model.getArtifactId();
334         }
335         if (this.version == null) {
336             this.version = model.getVersion();
337             if (this.version == null && parent != null) {
338                 this.version = parent.getVersion();
339             }
340         }
341         if (this.packaging == null) {
342             this.packaging = model.getPackaging();
343         }
344     }
345 
346     /**
347      * Generates a minimal model from the user-supplied artifact information.
348      *
349      * @return The generated model, never <code>null</code>.
350      */
351     private Model generateModel() {
352         return Model.newBuilder()
353                 .modelVersion("4.0.0")
354                 .groupId(groupId)
355                 .artifactId(artifactId)
356                 .version(version)
357                 .packaging(packaging)
358                 .description("POM was created from install:install-file")
359                 .build();
360     }
361 
362     /**
363      * Generates a (temporary) POM file from the plugin configuration. It's the responsibility of the caller to delete
364      * the generated file when no longer needed.
365      *
366      * @return The path to the generated POM file, never <code>null</code>.
367      * @throws MojoException If the POM file could not be generated.
368      */
369     private Path generatePomFile() throws MojoException {
370         Model model = generateModel();
371         try {
372             Path pomFile = File.createTempFile("mvninstall", ".pom").toPath();
373             try (Writer writer = Files.newBufferedWriter(pomFile)) {
374                 session.getService(ModelXmlFactory.class).write(model, writer);
375             }
376             return pomFile;
377         } catch (IOException e) {
378             throw new MojoException("Error writing temporary POM file: " + e.getMessage(), e);
379         }
380     }
381 
382     /**
383      * Gets the path of the specified artifact within the local repository. Note that the returned path need not exist
384      * (yet).
385      */
386     private Path getLocalRepositoryFile(Artifact artifact) {
387         return session.getPathForLocalArtifact(artifact);
388     }
389 
390     /**
391      * Get file extension, honoring various {@code tar.xxx} combinations.
392      */
393     private String getExtension(final Path file) {
394         String filename = file.getFileName().toString();
395         int lastDot = filename.lastIndexOf('.');
396         if (lastDot > 0 && lastDot < filename.length() - 1) {
397             String ext = filename.substring(lastDot + 1);
398             return filename.regionMatches(lastDot + 1 - TAR.length(), TAR, 0, TAR.length()) ? TAR + ext : ext;
399         }
400         return "";
401     }
402 
403     /**
404      * Returns {@code true} if passed in string is "valid Maven ID" (groupId or artifactId).
405      */
406     private boolean isValidId(String id) {
407         if (id == null) {
408             return false;
409         }
410         for (int i = 0; i < id.length(); i++) {
411             char c = id.charAt(i);
412             if (!(c >= 'a' && c <= 'z'
413                     || c >= 'A' && c <= 'Z'
414                     || c >= '0' && c <= '9'
415                     || c == '-'
416                     || c == '_'
417                     || c == '.')) {
418                 return false;
419             }
420         }
421         return true;
422     }
423 
424     /**
425      * Returns {@code true} if passed in string is "valid Maven (simple. non range, expression, etc) version".
426      */
427     private boolean isValidVersion(String version) {
428         if (version == null) {
429             return false;
430         }
431         for (int i = version.length() - 1; i >= 0; i--) {
432             if (ILLEGAL_VERSION_CHARS.indexOf(version.charAt(i)) >= 0) {
433                 return false;
434             }
435         }
436         return true;
437     }
438 }