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