View Javadoc
1   /**
2    * Copyright (C) 2008-2010 Matt Gumbley, DevZendo.org <http://devzendo.org>;
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *         http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  /**
18   * 
19   */
20  package org.devzendo.xplp;
21  
22  import java.io.BufferedReader;
23  import java.io.File;
24  import java.io.FileInputStream;
25  import java.io.FileNotFoundException;
26  import java.io.FileOutputStream;
27  import java.io.FileWriter;
28  import java.io.IOException;
29  import java.io.InputStream;
30  import java.io.InputStreamReader;
31  import java.io.OutputStream;
32  import java.util.HashSet;
33  import java.util.Properties;
34  import java.util.Set;
35  
36  import org.apache.maven.artifact.Artifact;
37  import org.apache.maven.plugin.AbstractMojo;
38  import org.codehaus.plexus.util.cli.CommandLineException;
39  import org.codehaus.plexus.util.cli.CommandLineUtils;
40  import org.codehaus.plexus.util.cli.Commandline;
41  import org.codehaus.plexus.util.cli.CommandLineUtils.StringStreamConsumer;
42  
43  /**
44   * A LauncherCreator is given all the parameters, extracted from
45   * the plugin configuration, and creates an appropriate
46   * launcher filesystem structure under the output directory.
47   * 
48   * @author matt
49   *
50   */
51  public abstract class LauncherCreator {
52      private final AbstractMojo mMojo;
53      private final File mOutputDirectory;
54      private final String mMainClassName;
55      private final String mApplicationName;
56      private final String mLibraryDirectory;
57      private final Set<Artifact> mTransitiveArtifacts;
58      private final Set<File> mResourceDirectories;
59      private final Properties mParameterProperties;
60      private final String[] mSystemProperties;
61      private final String[] mVmArguments;
62      private final String[] mNarClassifierTypes;
63  
64      /**
65       * @param mojo the parent mojo class
66       * @param outputDirectory where to create the .app structure 
67       * @param mainClassName the main class
68       * @param applicationName the name of the application
69       * @param libraryDirectory where the libraries are stored
70       * @param transitiveArtifacts the set of transitive artifact dependencies
71       * @param resourceDirectories the project's resource directories
72       * @param parameterProperties the plugin configuration parameters, as properties
73       * @param systemProperties an array of name=value system properties
74       * @param vmArguments an array of arguments to the VM
75       * @param narClassifierTypes an array of NAR classifier:types
76       */
77      public LauncherCreator(final AbstractMojo mojo,
78              final File outputDirectory,
79              final String mainClassName,
80              final String applicationName,
81              final String libraryDirectory,
82              final Set<Artifact> transitiveArtifacts,
83              final Set<File> resourceDirectories,
84              final Properties parameterProperties, 
85              final String[] systemProperties, 
86              final String[] vmArguments,
87              final String[] narClassifierTypes) {
88                  mMojo = mojo;
89                  mOutputDirectory = outputDirectory;
90                  mMainClassName = mainClassName;
91                  mApplicationName = applicationName;
92                  mLibraryDirectory = libraryDirectory;
93                  mTransitiveArtifacts = transitiveArtifacts;
94                  mResourceDirectories = resourceDirectories;
95                  mParameterProperties = parameterProperties;
96                  mSystemProperties = systemProperties;
97                  mVmArguments = vmArguments;
98                  mNarClassifierTypes = narClassifierTypes;
99      }
100 
101     /**
102      * @return the mojo that Austin Powers stole
103      */
104     protected final AbstractMojo getMojo() {
105         return mMojo;
106     }
107 
108     /**
109      * @return the transitiveArtifacts
110      */
111     protected final Set<Artifact> getTransitiveArtifacts() {
112         return mTransitiveArtifacts;
113     }
114 
115     /**
116      * @return the outputDirectory
117      */
118     protected final File getOutputDirectory() {
119         return mOutputDirectory;
120     }
121 
122     /**
123      * @return the mainClassName
124      */
125     protected final String getMainClassName() {
126         return mMainClassName;
127     }
128 
129     /**
130      * @return the applicationName
131      */
132     protected final String getApplicationName() {
133         return mApplicationName;
134     }
135 
136     /**
137      * @return the libraryDirectory
138      */
139     protected final String getLibraryDirectory() {
140         return mLibraryDirectory;
141     }
142 
143     /**
144      * @return the resourceDirectories
145      */
146     protected final Set<File> getResourceDirectories() {
147         return mResourceDirectories;
148     }
149 
150     /**
151      * @return the parameterProperties
152      */
153     protected final Properties getParameterProperties() {
154         return mParameterProperties;
155     }
156 
157     /**
158      * @return the array of system properties name=value pair strings
159      */
160     protected final String[] getSystemProperties() {
161         return mSystemProperties;
162     }
163 
164     /**
165      * @return the array of VM arguments
166      */
167     protected final String[] getVmArguments() {
168         return mVmArguments;
169     }
170 
171     /**
172      * Create a launcher specific to a given platform.
173      * @throws IOException on creation failure
174      */
175     public abstract void createLauncher() throws IOException;
176 
177     /**
178      * Copy a resource from the plugin's resources to a file.
179      * @param resourceName the name of the resource
180      * @param destinationFile the file to write it to
181      * @throws IOException on write failure
182      */
183     protected final void copyPluginResource(final String resourceName, final File destinationFile) throws IOException {
184         final InputStream resourceAsStream = getPluginResourceAsStream(resourceName);
185         if (resourceAsStream == null) {
186             final String message = "Could not open resource " + resourceName;
187             mMojo.getLog().warn(message);
188             throw new IOException(message);
189         }
190         
191         final OutputStream outputStream = createFileOutputStream(destinationFile);
192         final long bytesCopied = copyStream(resourceName, destinationFile.getAbsolutePath(),
193             resourceAsStream, outputStream);
194         mMojo.getLog().info("Created " + destinationFile.getAbsolutePath() + " [" + bytesCopied + " byte(s)]");
195     }
196 
197     /**
198      * Copy a resource from the user project to a file
199      * @param resourceName the name of the resource in the user project
200      * @param destinationFile the file to write it to
201      * @throws IOException on write failure
202      */
203     protected final void copyProjectResource(final String resourceName, final File destinationFile) throws IOException {
204         final InputStream resourceAsStream = getProjectResourceAsStream(resourceName);
205         final OutputStream outputStream = createFileOutputStream(destinationFile);
206         final long bytesCopied = copyStream(resourceName, destinationFile.getAbsolutePath(),
207             resourceAsStream, outputStream);
208         mMojo.getLog().info("Created " + destinationFile.getAbsolutePath() + " [" + bytesCopied + " byte(s)]");
209     }
210 
211     private OutputStream createFileOutputStream(final File destinationFile) throws IOException {
212         try {
213             return new FileOutputStream(destinationFile);
214         } catch (final FileNotFoundException e) {
215             final String message = "Could not create destination file " + destinationFile.getAbsolutePath() + ": " + e.getMessage();
216             mMojo.getLog().warn(message);
217             throw new IOException(message);
218         }
219     }
220 
221     private InputStream getProjectResourceAsStream(final String resourceName) throws IOException {
222         for (final File resourceDir : mResourceDirectories) {
223             try {
224                 final InputStream resourceAsStream = new FileInputStream(new File(resourceDir, resourceName));
225                 mMojo.getLog().debug("Located resource " + resourceName + " in directory " + resourceDir.getAbsolutePath());
226                 return resourceAsStream;
227             } catch (final FileNotFoundException e) {
228                 mMojo.getLog().debug("Resource " + resourceName + " not found in " + resourceDir.getAbsolutePath());
229             }
230         }
231         final String message = "Could not open resource " + resourceName;
232         mMojo.getLog().warn(message);
233         throw new IOException(message);
234     }
235     
236     private InputStream getPluginResourceAsStream(final String resourceName) {
237         final InputStream resourceAsStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(resourceName);
238         return resourceAsStream;
239     }
240 
241     /**
242      * Find all the compile-scoped .jar or .nar artifacts in a set, and ensure
243      * they're returned as .jars ('cos that's what they are).
244      * 
245      * @param transitiveArtifacts a set of artifacts
246      * @return the names of the filtered, transformed artifacts
247      */
248     protected Set<String> getTransitiveJarOrNarArtifactFileNames(final Set<Artifact> transitiveArtifacts) {
249         final Set<String> jarNarArtifacts = new HashSet<String>();
250         for (final Artifact transitiveArtifact : transitiveArtifacts) {
251             if (transitiveArtifact.getScope().equals("compile")
252                     && (transitiveArtifact.getType().equals("jar") 
253                             || transitiveArtifact.getType().equals("nar"))) {
254                 final String artifactFileName = transitiveArtifact.getFile().getName().replaceFirst("\\.nar$", ".jar");
255                 jarNarArtifacts.add(artifactFileName);
256             }
257         }
258         return jarNarArtifacts;
259     }
260 
261     /**
262      * Copy a file from its source to a destination directory.
263      * @param sourceFile the source file
264      * @param destinationDirectory the destination directory, which
265      * must exist
266      * @throws IOException on copy failure
267      */
268     protected final void copyFile(final File sourceFile, final File destinationDirectory) throws IOException {
269         final String destinationFileName = sourceFile.getName();
270         copyFileWithRename(sourceFile, destinationDirectory,
271             destinationFileName);
272     }
273 
274     private void copyFileWithRename(
275             final File sourceFile,
276             final File destinationDirectory,
277             final String destinationFileName) throws IOException {
278         InputStream inputStream;
279         try {
280             inputStream = new FileInputStream(sourceFile);
281         } catch (final IOException ioe) {
282             final String warning = "Could not open source file '" + sourceFile + "': " + ioe.getMessage();
283             mMojo.getLog().warn(warning);
284             throw new IOException(warning);
285         }
286         final File destinationFile = new File(destinationDirectory, destinationFileName);
287         OutputStream outputStream;
288         try {
289             outputStream = new FileOutputStream(destinationFile);
290         } catch (final FileNotFoundException e) {
291             final String message = "Could not create destination file " + destinationFile.getAbsolutePath() + ": " + e.getMessage();
292             mMojo.getLog().warn(message);
293             throw new IOException(message);
294         }
295         final long bytesCopied = copyStream(sourceFile.getAbsolutePath(), destinationFile.getAbsolutePath(),
296             inputStream, outputStream);
297         mMojo.getLog().info("Created "
298             + destinationDirectory.getAbsoluteFile()
299             + File.separatorChar + destinationFileName
300             + " from " + sourceFile.getAbsolutePath() 
301             + " [" + bytesCopied + " byte(s) copied]");
302     }
303 
304     private long copyStream(final String inName, final String outName,
305             final InputStream inputStream, final OutputStream outputStream) 
306     throws IOException {
307         final int bufsize = 16384;
308         final byte[] buf = new byte[bufsize];
309         long totalRead = 0;
310         int nRead;
311         try {
312             while ((nRead = inputStream.read(buf, 0, bufsize)) != -1) {
313                 outputStream.write(buf, 0, nRead);
314                 totalRead += nRead;
315             }
316             return totalRead;
317         } catch (final IOException e) {
318             final String message = "Could not copy " + inName + " to "
319             + outName + ": " + e.getMessage();
320             mMojo.getLog().warn(message);
321             throw new IOException(message);
322         } finally {
323             try {
324                 inputStream.close();
325             } catch (final IOException ioe) {
326             }
327             try {
328                 outputStream.close();
329             } catch (final IOException ioe) {
330             }
331         }
332     }
333 
334     /**
335      * Copy all compile-scoped jar-typed transitive artifacts into
336      * a destination directory.
337      * @param destinationDirectory the destination directory, which
338      * must exist
339      * @throws IOException on copy failure
340      */
341     protected final void copyTransitiveArtifacts(final File destinationDirectory) throws IOException {
342         getMojo().getLog().info("Copying transitive artifacts");
343         final Set<Artifact> transitiveArtifacts = getTransitiveArtifacts();
344         getMojo().getLog().info("There are " + transitiveArtifacts.size() + " transitive artifacts");
345         for (final Artifact artifact : transitiveArtifacts) {
346             if (artifact.getScope().equals("compile")) {
347                 copyTransitiveArtifact(destinationDirectory, artifact);
348             } else {
349                 getMojo().getLog().info("Not copying transitive artifact " + artifact + " since it is not a compile-scoped artifact");
350             }
351         }
352     }
353 
354     private void copyTransitiveArtifact(
355             final File destinationDirectory,
356             final Artifact artifact) throws IOException {
357         if (!(artifact.getType().equals("jar") || artifact.getType().equals("nar"))) {
358             getMojo().getLog().info("Not copying transitive artifact " + artifact + " since it is not a jar or nar artifact");
359             return;
360         }
361         
362         getMojo().getLog().info("Copying transitive artifact " + artifact);
363         final File artifactFile = artifact.getFile();
364         if (artifactFile.exists() && artifactFile.isFile()) {
365             if (artifact.getType().equals("nar")) {
366                 copyTransitiveNarArtifact(artifact, destinationDirectory);
367             } else {
368                 copyFile(artifactFile, destinationDirectory);
369             }
370         } else {
371             getMojo().getLog().warn("Not copying transitive artifact " + artifact + " since it either doesn't exist or is not a file");
372             getMojo().getLog().info("(Perhaps you're running this from an IDE, and this artifact is resolved as a project in your");
373             getMojo().getLog().info(" workspace and therefore on your classpath by the IDE?)");
374         }
375     }
376 
377     private void copyTransitiveNarArtifact(
378             final Artifact artifact, final File destinationDirectory) throws IOException {
379         final File artifactFile = artifact.getFile();
380         final String destinationJarFileName = artifactFile.getName().replace(".nar", ".jar");
381         copyFileWithRename(artifactFile, destinationDirectory, destinationJarFileName);
382         
383         for (final String narClassifierType : mNarClassifierTypes) {
384             final String[] split = narClassifierType.split(":");
385             copyNarClassifierType(artifact, destinationDirectory, split[0], split[1]);
386         }
387     }
388 
389     private void copyNarClassifierType(
390             final Artifact artifact,
391             final File destinationDirectory,
392             final String classifier,
393             final String type) throws IOException {
394         getMojo().getLog().info("Copying unpacked NAR files for artifact: " + artifact);
395         final File unpackedNarDirectory = new File(mOutputDirectory, "nar/lib/" + classifier + "/" + type);
396         if (!unpackedNarDirectory.exists() || !unpackedNarDirectory.isDirectory()) {
397             getMojo().getLog().warn(
398                 "NAR unpacked library directory " + unpackedNarDirectory.getAbsolutePath()
399                 + " for classifier:type " + classifier + ":" + type + " does not exist or is not a directory");
400             getMojo().getLog().warn("Did you forget to use the nar-assembly goal?");
401             return;
402         }
403         final File[] unpackedNarFiles = unpackedNarDirectory.listFiles();
404         for (final File file : unpackedNarFiles) {
405             final String name = file.getName();
406             getMojo().getLog().info("Considering NAR unpacked file: " + file.getAbsolutePath());
407             // Filter out files that are not library files, but unsure what the set
408             // of library files are: .jnilib, .so, .dll - but are there others?
409             if (name.equals("history.xml")) { // any other metadata files?
410                 getMojo().getLog().info("Not copying " + name + " as it is NAR metadata");
411                 continue;
412             }
413             final boolean isAnArtifactFile = name.contains(artifact.getArtifactId()) && name.contains(artifact.getVersion());
414             if (!isAnArtifactFile) {
415                 getMojo().getLog().warn("Not copying " + name + " as it does not refer to the artifact " + artifact);
416                 continue;
417             }
418             copyFile(file, destinationDirectory);
419         }
420     }
421 
422     /**
423      * Copy a user project resource to a file, interpolating with the configuration properties
424      * @param resourceName the name of the resource in the user project
425      * @param outputFile the file to write it to
426      * @throws IOException on write failure
427      */
428     protected final void copyInterpolatedProjectResource(final String resourceName, final File outputFile) throws IOException {
429         final BufferedReader br = new BufferedReader(new InputStreamReader(getProjectResourceAsStream(resourceName)));
430         copyInterpolatedResource(resourceName, outputFile, br);
431     }
432     
433     /**
434      * Copy a plugin resource to a file, interpolating with the configuration properties
435      * @param resourceName the name of the plugin resource
436      * @param outputFile the file to write it to
437      * @throws IOException on write failure
438      */
439     protected final void copyInterpolatedPluginResource(final String resourceName, final File outputFile) throws IOException {
440         final BufferedReader br = new BufferedReader(new InputStreamReader(getPluginResourceAsStream(resourceName)));
441         copyInterpolatedResource(resourceName, outputFile, br);
442     }
443 
444     private void copyInterpolatedResource(
445             final String resourceName,
446             final File outputFile,
447             final BufferedReader br) throws IOException {
448         long bytesCopied = 0;
449         final PropertiesInterpolator interpolator = new PropertiesInterpolator(getParameterProperties());
450         try {
451             final String lineSeparator = System.getProperty("line.separator");
452             final FileWriter fw = createFileWriter(outputFile);
453             try {
454                 while (true) {
455                     final String line = br.readLine();
456                     if (line == null) {
457                         break;
458                     }
459                     try {
460                         final String outLine = interpolator.interpolate(line);
461                         fw.write(outLine);
462                         bytesCopied += outLine.length();
463                         if (!outLine.endsWith(lineSeparator)) {
464                             fw.write(lineSeparator);
465                             bytesCopied += lineSeparator.length();
466                         }
467                     } catch (final IllegalStateException e) {
468                         final String message = "Cannot interpolate whilst processing " + resourceName + ": " + e.getMessage();
469                         getMojo().getLog().warn(message);
470                         throw new IOException(message);
471                     }
472                 }
473             } finally {
474                 try {
475                     fw.close();
476                 } catch (final IOException e) {
477                     // nothing
478                 }
479             }
480         } finally {
481             try {
482                 br.close();
483             } catch (final IOException e) {
484                 // nothing
485             }
486         }
487         getMojo().getLog().info("Created "
488             + outputFile.getAbsolutePath() 
489             + " [" + bytesCopied + " byte(s) copied]");
490         
491     }
492 
493     private FileWriter createFileWriter(final File outputFile) throws IOException {
494         try {
495             return new FileWriter(outputFile);
496         } catch (final IOException e) {
497             final String message = "Could not create destination file "
498                 + outputFile.getAbsolutePath() + ": " + e.getMessage();
499             mMojo.getLog().warn(message);
500             throw new IOException(message);
501         }
502     }
503 
504     /**
505      * Make a file executable, in the terms of the specific platform.
506      * @param nonExecutableFile some file that may not currently be executable
507      */
508     protected final void makeExecutable(final File nonExecutableFile) {
509         getMojo().getLog().info("Making " + nonExecutableFile + " executable");
510         final Commandline cl = new  Commandline("chmod");
511         cl.addArguments(new String [] {"a+x" , nonExecutableFile.getAbsolutePath()}); 
512         try {
513             final StringStreamConsumer output = new StringStreamConsumer();
514             final StringStreamConsumer error = new StringStreamConsumer(); 
515             final int returnValue = CommandLineUtils.executeCommandLine(cl, output, error);
516             if (returnValue != 0) {
517                 getMojo().getLog().warn("chmod exit code is " + returnValue);
518                 getMojo().getLog().warn("chmod output: " + output.getOutput());
519                 getMojo().getLog().warn("chmod error output: " + error.getOutput());
520             }
521         } catch (final CommandLineException e) {
522             getMojo().getLog().warn("Couldn't run chmod: " + e.getMessage());
523         }
524     }
525 }