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  package org.devzendo.xplp;
18  
19  import java.io.File;
20  import java.util.HashSet;
21  import java.util.List;
22  import java.util.Properties;
23  import java.util.Set;
24  
25  import org.apache.maven.artifact.Artifact;
26  import org.apache.maven.artifact.metadata.ArtifactMetadataSource;
27  import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
28  import org.apache.maven.artifact.resolver.ArtifactResolutionException;
29  import org.apache.maven.model.Resource;
30  import org.apache.maven.plugin.AbstractMojo;
31  import org.apache.maven.plugin.MojoExecutionException;
32  import org.apache.maven.plugin.MojoFailureException;
33  import org.apache.maven.plugins.annotations.Component;
34  import org.apache.maven.plugins.annotations.LifecyclePhase;
35  import org.apache.maven.plugins.annotations.Mojo;
36  import org.apache.maven.plugins.annotations.Parameter;
37  import org.apache.maven.project.artifact.InvalidDependencyVersionException;
38  
39  /**
40   * A Maven plugin that creates launcher directory structures for Windows
41   * (using Janel), Mac OS X (creating a .app or script structure) or Linux
42   * (using a shell script).
43   * 
44   * @author Matt Gumbley, DevZendo.org
45   *
46   */
47  @Mojo( name = "createlauncher",
48         defaultPhase = LifecyclePhase.GENERATE_RESOURCES )
49  public final class CreateLauncherMojo extends AbstractMojo {
50  
51      /**
52       * The Maven project.
53       */
54      @Parameter( defaultValue = "${project}", readonly = true)
55      private org.apache.maven.project.MavenProject mavenProject;
56  
57      @Component()
58      private org.apache.maven.artifact.factory.ArtifactFactory artifactFactory;
59  
60      @Component()
61      private org.apache.maven.artifact.resolver.ArtifactResolver artifactResolver;
62  
63      @Parameter( defaultValue = "${localRepository}", readonly = true)
64      private org.apache.maven.artifact.repository.ArtifactRepository localRepository;
65  
66      @Parameter( defaultValue = "${project.remoteArtifactRepositories}", readonly = true)
67      private java.util.List<?> remoteRepositories;
68  
69      @Component()
70      private ArtifactMetadataSource artifactMetadataSource;
71  
72      
73      /**
74       * The OS to generate a launcher for: Windows, MacOSX or Linux.
75       * 
76       * @required
77       *
78       */
79      @Parameter( defaultValue = "${xplp.os}", required = true )
80      private String os;
81      
82      /**
83       * The directory into which the output files will be placed.
84       * By default, this is the target directory. The launcher directory
85       * structure will be created in a subdirectory of this. This subdirectory
86       * will be named according to the platform specified in the os parameter,
87       * so: windows, linux or macosx.
88       */
89      @Parameter( defaultValue = "${project.build.directory}")
90      private File outputDirectory;
91      
92      /**
93       * The fully-qualified name of the main class of your application,
94       * i.e. contains a public static void main...
95       */
96      @Parameter( defaultValue = "${xplp.mainclassname}", required = true )
97      private String mainClassName;
98  
99      /**
100      * The name of the application for whom this launcher is to be
101      * generated. 
102      * 
103      * On Mac OS X, this is used to name the application menu.
104      * On Windows, this is used to name the Janel .exe/.lap files.
105      * If not specified the client project's artifact id will be used.
106      */
107     @Parameter( defaultValue = "${xplp.applicationname}", required = true )
108     private String applicationName;
109     
110     /**
111      * The directory where the application's jars are.
112      * By default, assume lib/
113      */
114     @Parameter( defaultValue = "${xplp.librarydirectory}")
115     private String libraryDirectory = "lib";
116     
117     /**
118      * A list of system properties, to be passed to the JVM via multiple
119      * -Dxxx=yyy parameters. When specifying system properties, omit the -D, and
120      * just give them as xxx=yyy. The platform-specific launcher will add in the
121      * -D if necessary.
122      */
123     @Parameter( defaultValue = "${xplp.systemproperty}")
124     private String[] systemProperties;
125     
126     /**
127      * A list of VM arguments, to be passed straight to the JVM, e.g. -Xmx2048.
128      * Note that on Mac OS X, your application should set -Xdock:name=MyApplication
129      * to have the correct name in the application menu and on the dock.
130      */
131     @Parameter( defaultValue = "${xplp.vmargument}")
132     private String[] vmArguments;
133 
134     /**
135      * A list of NAR (Native ARchive, from the Maven NAR Plugin) classifiers and
136      * types. These refer to native library files that have been unpacked using
137      * the nar-download, nar-unpack and nar-assembly goals of the Maven NAR
138      * Plugin, and reside in the target/nar/lib/classifier/type directories. 
139      * </p>
140      * Any files in these directories will be copied to the launcher's library
141      * directory.
142      * </p>
143      * The params you specify here must be in the form classifier:type, e.g.
144      * x86_64-MacOSX-g++:jni and you may specify as many as you like; only those
145      * directories that have anything in them will have their contents copied.
146      */
147     @Parameter( defaultValue = "${xplp.narClassifierType}")
148     private String[] narClassifierTypes;
149 
150     /**
151      * The launcher type, can be "Console" or "GUI". 
152      * For Windows, whether to use the Console or GUI Janel EXE.
153      * For Mac OS X, whether to create a script or .app structure.
154      */
155     @Parameter( defaultValue = "${xplp.launchertype}")
156     private String launcherType = "GUI";
157     
158     // Mac OS X Specific parameters -------------------------------
159     
160     /**
161      * Mac OS X only: Any file type that is associated with this application.
162      * This is registered in the Mac OS X Info.plist as CFBundleTypeExtensions.
163      */
164     @Parameter( defaultValue = "${xplp.filetype}")
165     private String fileType;
166     
167     /**
168      * Mac OS X only: The name of the icons file.
169      */
170     @Parameter( defaultValue = "${xplp.iconsfilename}")
171     private String iconsFileName;
172     
173     /**
174      * Mac OS X only: The bundle signature. Only use this if you have a registered
175      * creator code.
176      * This is registered in the Mac OS X Info.plist as CFBundleSignature, and
177      * in the PkgInfo prefixed with APPL
178      * </p>
179      * As stated at <a href="http://developer.apple.com/mac/library/documentation/Java/Conceptual/Java14Development/03-JavaDeployment/JavaDeployment.html">
180      * the Apple developer website</a> 
181      * "This is a simple text file that contains the string APPL optionally
182      * concatenated with a four letter creator code. If an application does not
183      * have a registered creator code, the string APPL???? should be used."
184      */
185     @Parameter( defaultValue = "${xplp.bundlesignature}")
186     private String bundleSignature = "????";
187     
188     /**
189      * Mac OS X only: The bundle OS type.
190      * This is registered in the Mac OS X Info.plist as CFBundleTypeOSTypes.
191      */
192     @Parameter( defaultValue = "${xplp.bundleostype}")
193     private String bundleOsType;
194     
195     /**
196      * Mac OS X only: The bundle type name
197      * This is registered in the Mac OS X Info.plist as CFBundleTypeName.
198      */
199     @Parameter( defaultValue = "${xplp.bundletypename}")
200     private String bundleTypeName;
201 
202     /**
203      * Mac OS X GUI launchers only: whether to use Apple's JavaApplicationStub, or Tobias Fischer's universal
204      * application stub. (Currently this Maven plugin supplies v2.1.0 of Tobias' stub)
205      * Please see https://github.com/tofi86/universalJavaApplicationStub for details of this.
206      * If you want to launch with something other than Apple Java 6, you need the universal stub.
207      *
208      * Can be "Apple" or "Universal". Default is "Apple" for backwards compatibility.
209      */
210     @Parameter( defaultValue = "${xplp.stubtype}")
211     private String stubType = "Apple";
212 
213     // Windows specific parameters
214     /**
215      * Windows only: Whether to use the Console or GUI Janel EXE.
216      * Can be "Console" or "GUI"
217      * 
218      * @deprecated Use launcherType instead.
219      */
220     @Deprecated
221     @Parameter( defaultValue = "${xplp.janeltype}")
222     private String janelType;
223     
224     /**
225      * Windows only: A list of lines of text that will be added to the Janel
226      * launcher file.
227      */
228     @Parameter( defaultValue = "${xplp.janelcustomline}")
229     private String[] janelCustomLines;
230 
231     /**
232      * Windows only: The (major/minor) version of Janel you wish to use to launch your program. Choices are:
233      * The Original Janel 3.0.2 by Timothy D. Kil that's been discontinued.
234      * The Enhanced Janel 4.2.0-98 continued by Michael Knigge.
235      * Both versions of Janel are embedded in the plugin; if you are using the Enhanced version, also consider setting
236      * the xplp.janelbits property.
237      *
238      * Can be "3.0" (currently 3.0.2) or "4.2" (currently 4.2.0-98).
239      * The default is "3.0" for backwards compatibility.
240      */
241     @Parameter( defaultValue = "${xplp.janelversion}")
242     private String janelVersion = "3.0";
243 
244     /**
245      * Windows only: If using the Enhanced Janel, are you using 32- or 64-bit? The original Janel makes no such
246      * distinction.
247      *
248      * Can be "32" or "64".
249      * The default is "64" as 32-bit systems are becoming rarer.
250      */
251     @Parameter( defaultValue = "${xplp.janelbits}")
252     private String janelBits = "64";
253 
254     /**
255      * Windows only: Should the Janel .EXE, .DLL and .LAP file be in the root directory (the backwards-compatible
256      * default), or in a 'bin' directory? This affects the path to the library directory in the .LAP file -
257      * if "root", we can refer to the library directory as "lib", but if the .LAP file is in a "bin" directory, then
258      * the path to the library directory would be "../lib".
259      *
260      * Can be "root" or "bin".
261      * The default is "root" for backwards compatibility.
262      */
263     @Parameter( defaultValue = "${xplp.janeldirectory}")
264     private String janelDirectory = "root";
265 
266     /**
267      * {@inheritDoc}
268      */
269     public void execute() throws MojoExecutionException, MojoFailureException {
270         getLog().info("Cross Platform Launcher Plugin");
271         if (os == null || os.equals("none")) {
272             throw new MojoExecutionException("No <os>Windows|MacOSX|Linux</os> specified in the <configuration>");
273         }
274         if (janelType != null && !janelType.equals("")) {
275             throw new MojoExecutionException("The janelType attribute has been changed to launcherType in v0.2.1 of the plugin");
276         }
277         validateNarClassifierTypes();
278         final Set<Artifact> transitiveArtifacts = getTransitiveDependencies();
279         final Set<File> resourceDirectories = getResourceDirectories();
280         final Properties parameterProperties = getParameterProperties();
281         getLog().info("Operating System:            " + os);
282         getLog().info("Output directory:            " + outputDirectory);
283         getLog().info("Main class name:             " + mainClassName);
284         getLog().info("Application name:            " + applicationName);
285         getLog().info("Library directory:           " + libraryDirectory);
286         getLog().info("System properties:           " + dumpArray(systemProperties));
287         getLog().info("VM Arguments:                " + dumpArray(vmArguments));
288         getLog().info("NAR Classifier:Types:        " + dumpArray(narClassifierTypes));
289 
290         LauncherCreator launcherCreator;
291         if (os.equals("MacOSX")) {
292             if (launcherType.equals("GUI")) { 
293                 launcherCreator = new MacOSXAppLauncherCreator(this,
294                     outputDirectory, mainClassName, applicationName,
295                     libraryDirectory, transitiveArtifacts,
296                     resourceDirectories,
297                     parameterProperties, systemProperties, vmArguments,
298                     narClassifierTypes, launcherType,
299                     fileType, iconsFileName, bundleSignature, bundleOsType,
300                     bundleTypeName, stubType);
301             } else {
302                 launcherCreator = new MacOSXScriptLauncherCreator(this,
303                     outputDirectory, mainClassName, applicationName,
304                     libraryDirectory, transitiveArtifacts,
305                     resourceDirectories, parameterProperties, systemProperties,
306                     vmArguments, narClassifierTypes, launcherType);
307             }
308         } else if (os.equals("Windows")) {
309             getLog().info("Janel custom lines:          " + dumpArray(janelCustomLines));
310 
311             launcherCreator = new WindowsLauncherCreator(this,
312                 outputDirectory, mainClassName, applicationName,
313                 libraryDirectory, transitiveArtifacts,
314                 resourceDirectories, parameterProperties, systemProperties,
315                 vmArguments, narClassifierTypes, launcherType, janelVersion, janelBits, janelCustomLines, janelDirectory);
316         } else if (os.equals("Linux")) {
317             launcherCreator = new LinuxLauncherCreator(this,
318                 outputDirectory, mainClassName, applicationName,
319                 libraryDirectory, transitiveArtifacts,
320                 resourceDirectories, parameterProperties, systemProperties,
321                 vmArguments, narClassifierTypes);
322         } else {
323             throw new MojoExecutionException("No <os>Windows|MacOSX|Linux</os> specified in the <configuration>");
324         }
325         try {
326             launcherCreator.createLauncher();
327         } catch (final Exception e) {
328             final StackTraceElement[] stackTrace = e.getStackTrace();
329             for (final StackTraceElement stackTraceElement : stackTrace) {
330                 getLog().info(stackTraceElement.toString());
331             }
332             throw new MojoFailureException("Could not create launcher: " + e.getMessage());
333         }
334     }
335     
336     private void validateNarClassifierTypes() throws MojoFailureException {
337         if (narClassifierTypes == null) {
338             narClassifierTypes = new String[0];
339             return;
340         }
341         boolean allOK = true;
342         for (final String narClassifierType : narClassifierTypes) {
343             if (!narClassifierType.matches("^\\S+:\\S+$")) {
344                 getLog().error("NAR Classifier:Type '" + narClassifierType + "' is not of the form Classifier:Type");
345                 allOK = false;
346             }
347         }
348         if (!allOK) {
349             throw new MojoFailureException("One or more NAR Classifier:Type parameters are incorrectly specified");
350         }
351     }
352 
353     private String dumpArray(final Object[] objects) {
354         final StringBuilder sb = new StringBuilder();
355         if (objects != null) {
356             sb.append('[');
357             if (objects.length != 0) {
358                 for (int i = 0; i < objects.length - 1; i++) {
359                     sb.append(objects[i]);
360                     sb.append(',');
361                 }
362                 sb.append(objects[objects.length - 1]);
363             }
364             sb.append(']');
365         }
366         return sb.toString();
367     }
368 
369     private Properties getParameterProperties() {
370         final Properties properties = new Properties();
371         // TODO there has to be an automated way of doing this!
372         // mavenProject.getProperties() ?
373         properties.put("xplp.os", nullToEmptyString(os));
374         properties.put("xplp.outputdirectory", nullToEmptyString(outputDirectory.getPath()));
375         properties.put("xplp.mainclassname", nullToEmptyString(mainClassName));
376         properties.put("xplp.applicationname", nullToEmptyString(applicationName));
377         properties.put("xplp.librarydirectory", nullToEmptyString(libraryDirectory));
378         properties.put("xplp.filetype", nullToEmptyString(fileType));
379         properties.put("xplp.iconsfilename", nullToEmptyString(iconsFileName));
380         properties.put("xplp.bundlesignature", nullToEmptyString(bundleSignature));
381         properties.put("xplp.bundleostype", nullToEmptyString(bundleOsType));
382         properties.put("xplp.bundletypename", nullToEmptyString(bundleTypeName));
383         properties.put("project.version", nullToEmptyString(mavenProject.getVersion()));
384         properties.put("project.description", nullToEmptyString(mavenProject.getDescription()));
385         return properties;
386     }
387 
388     private String nullToEmptyString(final String in) {
389         return in == null ? "" : in;
390     }
391 
392     @SuppressWarnings("unchecked")
393     private Set<File> getResourceDirectories() {
394         final HashSet<File> resourceDirs = new HashSet<File>();
395         final List<Resource> resources = mavenProject.getResources();
396         for (final Resource resource : resources) {
397             final String directory = resource.getDirectory();
398             final File directoryFile = new File(directory);
399             if (directoryFile.exists() && directoryFile.isDirectory()) {
400                 resourceDirs.add(directoryFile);
401             }
402         }
403         return resourceDirs;
404     }
405 
406     @SuppressWarnings("unchecked")
407     private Set<Artifact> getTransitiveDependencies() throws MojoFailureException {
408         getLog().info("Resolving transitive dependencies");
409         Set<?> artifacts;
410         try {
411             artifacts = mavenProject.createArtifacts(artifactFactory, null, null);
412             
413 //          For unknown reasons, this fails to filter - nothing's returned                
414 //            final TypeArtifactFilter typeFilter = new TypeArtifactFilter("jar");
415 //            final ScopeArtifactFilter scopeFilter = new ScopeArtifactFilter("compile");
416 //            final AndArtifactFilter filter = new AndArtifactFilter();
417 //            filter.add(typeFilter);
418 //            filter.add(scopeFilter);
419             
420             final Set<Artifact> result = artifactResolver.resolveTransitively(artifacts,
421                 mavenProject.getArtifact(), localRepository, remoteRepositories,
422                 artifactMetadataSource, null).getArtifacts(); 
423             for (final Artifact artifact : result) {
424                 getLog().debug("Transitive artifact: " + artifact.toString());
425                 getLog().debug("   File: " + artifact.getFile().getAbsolutePath());
426             }
427             getLog().info("Transitive dependencies resolved");
428             return result;
429         } catch (final InvalidDependencyVersionException e) {
430             final String message = "Invalid dependency version: " + e.getMessage();
431             getLog().warn(message);
432             throw new MojoFailureException(message);
433         } catch (final ArtifactResolutionException e) {
434             final String message = "Artifact failed to resolve: " + e.getMessage();
435             getLog().warn(message);
436             throw new MojoFailureException(message);
437         } catch (final ArtifactNotFoundException e) {
438             final String message = "Artifact not found: " + e.getMessage();
439             getLog().warn(message);
440             throw new MojoFailureException(message);
441         } 
442     }
443 }