001 /*******************************************************************************
002 * Copyright (c) 2009 Progress Software, Inc.
003 * Copyright (c) 2004, 2007 IBM Corporation and others.
004 *
005 * All rights reserved. This program and the accompanying materials
006 * are made available under the terms of the Eclipse Public License v1.0
007 * which accompanies this distribution, and is available at
008 * http://www.eclipse.org/legal/epl-v10.html
009 *
010 *******************************************************************************/
011 package org.fusesource.hawtjni.generator;
012
013 import java.io.ByteArrayOutputStream;
014 import java.io.File;
015 import java.io.IOException;
016 import java.io.InputStream;
017 import java.io.PrintStream;
018 import java.io.PrintWriter;
019 import java.lang.reflect.Array;
020 import java.net.URL;
021 import java.net.URLClassLoader;
022 import java.util.ArrayList;
023 import java.util.Arrays;
024 import java.util.Calendar;
025 import java.util.HashSet;
026 import java.util.LinkedHashSet;
027 import java.util.List;
028 import java.util.Set;
029 import java.util.regex.Pattern;
030
031 import org.apache.commons.cli.CommandLine;
032 import org.apache.commons.cli.HelpFormatter;
033 import org.apache.commons.cli.Options;
034 import org.apache.commons.cli.ParseException;
035 import org.apache.commons.cli.PosixParser;
036 import org.apache.xbean.finder.ClassFinder;
037 import org.apache.xbean.finder.UrlSet;
038 import org.fusesource.hawtjni.generator.model.JNIClass;
039 import org.fusesource.hawtjni.generator.model.ReflectClass;
040 import org.fusesource.hawtjni.generator.util.FileSupport;
041 import org.fusesource.hawtjni.runtime.ClassFlag;
042 import org.fusesource.hawtjni.runtime.JniClass;
043
044 import static org.fusesource.hawtjni.generator.util.OptionBuilder.*;
045
046 /**
047 *
048 * @author <a href="http://hiramchirino.com">Hiram Chirino</a>
049 */
050 public class HawtJNI {
051 public static final String END_YEAR_TAG = "%END_YEAR%";
052
053 private ProgressMonitor progress;
054 private File nativeOutput = new File(".");
055 // private File javaOutputDir = new File(".");
056 private List<String> classpaths = new ArrayList<String>();
057 private List<String> packages = new ArrayList<String>();
058 private String name = "hawtjni_native";
059 private String copyright = "";
060 private boolean callbacks = true;
061
062 ///////////////////////////////////////////////////////////////////
063 // Command line entry point
064 ///////////////////////////////////////////////////////////////////
065 public static void main(String[] args) {
066 String jv = System.getProperty("java.version").substring(0, 3);
067 if (jv.compareTo("1.5") < 0) {
068 System.err.println("This application requires jdk 1.5 or higher to run, the current java version is " + System.getProperty("java.version"));
069 System.exit(-1);
070 return;
071 }
072
073 HawtJNI app = new HawtJNI();
074 System.exit(app.execute(args));
075 }
076
077 ///////////////////////////////////////////////////////////////////
078 // Entry point for an embedded users who want to call us with
079 // via command line arguments.
080 ///////////////////////////////////////////////////////////////////
081 public int execute(String[] args) {
082 CommandLine cli = null;
083 try {
084 cli = new PosixParser().parse(createOptions(), args, true);
085 } catch (ParseException e) {
086 System.err.println( "Unable to parse command line options: " + e.getMessage() );
087 displayHelp();
088 return 1;
089 }
090
091 if( cli.hasOption("h") ) {
092 displayHelp();
093 return 0;
094 }
095
096 if( cli.hasOption("v") ) {
097 progress = new ProgressMonitor() {
098 public void step() {
099 }
100 public void setTotal(int total) {
101 }
102 public void setMessage(String message) {
103 System.out.println(message);
104 }
105 };
106 }
107
108 name = cli.getOptionValue("n", "hawtjni_native");
109 nativeOutput = new File(cli.getOptionValue("o", "."));
110 // javaOutputDir = new File(cli.getOptionValue("j", "."));
111 String[] values = cli.getOptionValues("p");
112 if( values!=null ) {
113 packages = Arrays.asList(values);
114 }
115
116 values = cli.getArgs();
117 if( values!=null ) {
118 classpaths = Arrays.asList(values);
119 }
120
121 try {
122 if( classpaths.isEmpty() ) {
123 throw new UsageException("No classpath supplied.");
124 }
125 generate();
126 } catch (UsageException e) {
127 System.err.println("Invalid usage: "+e.getMessage());
128 displayHelp();
129 return 1;
130 } catch (Throwable e) {
131 System.out.flush();
132 System.err.println("Unexpected failure:");
133 e.printStackTrace();
134 Set<Throwable> exceptions = new HashSet<Throwable>();
135 exceptions.add(e);
136 for (int i = 0; i < 10; i++) {
137 e = e.getCause();
138 if (e != null && exceptions.add(e)) {
139 System.err.println("Reason: " + e);
140 e.printStackTrace();
141 } else {
142 break;
143 }
144 }
145 return 2;
146 }
147 return 0;
148 }
149
150
151 ///////////////////////////////////////////////////////////////////
152 // Entry point for an embedded users who want use us like a pojo
153 ///////////////////////////////////////////////////////////////////
154 public ProgressMonitor getProgress() {
155 return progress;
156 }
157
158 public void setProgress(ProgressMonitor progress) {
159 this.progress = progress;
160 }
161
162 public File getNativeOutput() {
163 return nativeOutput;
164 }
165
166 public void setNativeOutput(File nativeOutput) {
167 this.nativeOutput = nativeOutput;
168 }
169
170 public List<String> getClasspaths() {
171 return classpaths;
172 }
173
174 public void setClasspaths(List<String> classpaths) {
175 this.classpaths = classpaths;
176 }
177
178 public List<String> getPackages() {
179 return packages;
180 }
181
182 public void setPackages(List<String> packages) {
183 this.packages = packages;
184 }
185
186 public String getName() {
187 return name;
188 }
189
190 public void setName(String name) {
191 this.name = name;
192 }
193
194 public void setCopyright(String copyright) {
195 this.copyright = copyright;
196 }
197
198 public boolean isCallbacks() {
199 return callbacks;
200 }
201
202 public void setCallbacks(boolean enableCallbacks) {
203 this.callbacks = enableCallbacks;
204 }
205
206 public void generate() throws UsageException, IOException {
207 progress("Analyzing classes...");
208
209 ArrayList<JNIClass> natives = new ArrayList<JNIClass>();
210 ArrayList<JNIClass> structs = new ArrayList<JNIClass>();
211 findClasses(natives, structs);
212
213 if( natives.isEmpty() && structs.isEmpty() ) {
214 throw new RuntimeException("No @JniClass or @JniStruct annotated classes found.");
215 }
216
217
218 if (progress != null) {
219 int nativeCount = 0;
220 for (JNIClass clazz : natives) {
221 nativeCount += clazz.getNativeMethods().size();
222 }
223 int total = nativeCount * 4;
224 total += natives.size() * (3);
225 total += structs.size() * 2;
226 progress.setTotal(total);
227 }
228
229 File file;
230 nativeOutput.mkdirs();
231
232 progress("Generating...");
233 file = nativeFile(".c");
234 generate(new NativesGenerator(), natives, file);
235
236 file = nativeFile("_stats.h");
237 generate(new StatsGenerator(true), natives, file);
238
239 file = nativeFile("_stats.c");
240 generate(new StatsGenerator(false), natives, file);
241
242 file = nativeFile("_structs.h");
243 generate(new StructsGenerator(true), structs, file);
244
245 file = nativeFile("_structs.c");
246 generate(new StructsGenerator(false), structs, file);
247
248 file = new File(nativeOutput, "hawtjni.h");
249 generateFromResource("hawtjni.h", file);
250
251 file = new File(nativeOutput, "hawtjni.c");
252 generateFromResource("hawtjni.c", file);
253
254 file = new File(nativeOutput, "hawtjni-callback.c");
255 if( callbacks ) {
256 generateFromResource("hawtjni-callback.c", file);
257 } else {
258 file.delete();
259 }
260
261 file = new File(nativeOutput, "windows");
262 file.mkdirs();
263 file = new File(file, "stdint.h");
264 generateFromResource("windows/stdint.h", file);
265
266 progress("Done.");
267 }
268
269 ///////////////////////////////////////////////////////////////////
270 // Helper methods
271 ///////////////////////////////////////////////////////////////////
272
273 private void findClasses(ArrayList<JNIClass> jni, ArrayList<JNIClass> structs) throws UsageException {
274
275 ArrayList<URL> urls = new ArrayList<URL>();
276 for (String classpath : classpaths) {
277 String[] fileNames = classpath.replace(';', ':').split(":");
278 for (String fileName : fileNames) {
279 try {
280 File file = new File(fileName);
281 if( file.isDirectory() ) {
282 urls.add(new URL(url(file)+"/"));
283 } else {
284 urls.add(new URL(url(file)));
285 }
286 } catch (Exception e) {
287 throw new UsageException("Invalid class path. Not a valid file: "+fileName);
288 }
289 }
290 }
291 LinkedHashSet<Class<?>> jniClasses = new LinkedHashSet<Class<?>>();
292 try {
293 URLClassLoader classLoader = new URLClassLoader(array(URL.class, urls), JniClass.class.getClassLoader());
294 UrlSet urlSet = new UrlSet(classLoader);
295 urlSet = urlSet.excludeJavaHome();
296 ClassFinder finder = new ClassFinder(classLoader, urlSet.getUrls());
297 collectMatchingClasses(finder, JniClass.class, jniClasses);
298 } catch (Exception e) {
299 throw new RuntimeException(e);
300 }
301
302 for (Class<?> clazz : jniClasses) {
303 ReflectClass rc = new ReflectClass(clazz);
304 if( rc.getFlag(ClassFlag.STRUCT) ) {
305 structs.add(rc);
306 }
307 if( !rc.getNativeMethods().isEmpty() ) {
308 jni.add(rc);
309 }
310 }
311 }
312
313
314 static private Options createOptions() {
315 Options options = new Options();
316 options.addOption("h", "help", false, "Display help information");
317 options.addOption("v", "verbose", false, "Verbose generation");
318
319 options.addOption("o", "offline", false, "Work offline");
320 options.addOption(ob()
321 .id("n")
322 .name("name")
323 .arg("value")
324 .description("The base name of the library, used to determine generated file names. Defaults to 'hawtjni_native'.").op());
325
326 options.addOption(ob()
327 .id("o")
328 .name("native-output")
329 .arg("dir")
330 .description("Directory where generated native source code will be stored. Defaults to the current directory.").op());
331
332 // options.addOption(ob()
333 // .id("j")
334 // .name("java-output")
335 // .arg("dir")
336 // .description("Directory where generated native source code will be stored. Defaults to the current directory.").op());
337
338 options.addOption(ob()
339 .id("p")
340 .name("package")
341 .arg("package")
342 .description("Restrict looking for JNI classes to the specified package.").op());
343
344 return options;
345 }
346
347
348 private void displayHelp() {
349 System.err.flush();
350 String app = System.getProperty("hawtjni.application");
351 if( app == null ) {
352 try {
353 URL location = getClass().getProtectionDomain().getCodeSource().getLocation();
354 String[] split = location.toString().split("/");
355 if( split[split.length-1].endsWith(".jar") ) {
356 app = split[split.length-1];
357 }
358 } catch (Throwable e) {
359 }
360 if( app == null ) {
361 app = getClass().getSimpleName();
362 }
363 }
364
365 // The commented out line is 80 chars long. We have it here as a visual reference
366 // p(" ");
367 p();
368 p("Usage: "+ app +" [options] <classpath>");
369 p();
370 p("Description:");
371 p();
372 pw(" "+app+" is a code generator that produces the JNI code needed to implement java native methods.", 2);
373 p();
374
375 p("Options:");
376 p();
377 PrintWriter out = new PrintWriter(System.out);
378 HelpFormatter formatter = new HelpFormatter();
379 formatter.printOptions(out, 78, createOptions(), 2, 2);
380 out.flush();
381 p();
382 p("Examples:");
383 p();
384 pw(" "+app+" -o build foo.jar bar.jar ", 2);
385 pw(" "+app+" -o build foo.jar:bar.jar ", 2);
386 pw(" "+app+" -o build -p org.mypackage foo.jar;bar.jar ", 2);
387 p();
388 }
389
390 private void p() {
391 System.out.println();
392 }
393 private void p(String s) {
394 System.out.println(s);
395 }
396 private void pw(String message, int indent) {
397 PrintWriter out = new PrintWriter(System.out);
398 HelpFormatter formatter = new HelpFormatter();
399 formatter.printWrapped(out, 78, indent, message);
400 out.flush();
401 }
402
403 @SuppressWarnings("unchecked")
404 private void collectMatchingClasses(ClassFinder finder, Class annotation, LinkedHashSet<Class<?>> collector) {
405 List<Class> annotated = finder.findAnnotatedClasses(annotation);
406 for (Class<?> clazz : annotated) {
407 if( packages.isEmpty() ) {
408 collector.add(clazz);
409 } else {
410 if( packages.contains(clazz.getPackage().getName()) ) {
411 collector.add(clazz);
412 }
413 }
414 }
415 }
416
417 private void progress(String message) {
418 if (progress != null) {
419 progress.setMessage(message);
420 }
421 }
422
423 private void generate(JNIGenerator gen, ArrayList<JNIClass> classes, File target) throws IOException {
424 gen.setOutputName(name);
425 gen.setClasses(classes);
426 gen.setCopyright(getCopyright());
427 gen.setProgressMonitor(progress);
428 ByteArrayOutputStream out = new ByteArrayOutputStream();
429 gen.setOutput(new PrintStream(out));
430 gen.generate();
431 if (out.size() > 0) {
432 if( FileSupport.write(out.toByteArray(), target) ) {
433 progress("Wrote: "+target);
434 }
435 }
436 }
437
438 private void generateFromResource(String resource, File target) throws IOException {
439 ByteArrayOutputStream out = new ByteArrayOutputStream();
440 InputStream is = getClass().getClassLoader().getResourceAsStream(resource);
441 FileSupport.copy(is, out);
442 String content = new String(out.toByteArray(), "UTF-8");
443 String[] parts = content.split(Pattern.quote("/* == HEADER-SNIP-LOCATION == */"));
444 if( parts.length==2 ) {
445 content = parts[1];
446 }
447 out.reset();
448 PrintStream ps = new PrintStream(out);
449 ps.print(JNIGenerator.fixDelimiter(getCopyright()));
450 ps.print(JNIGenerator.fixDelimiter(content));
451 ps.close();
452 if( FileSupport.write(out.toByteArray(), target) ) {
453 progress("Wrote: "+target);
454 }
455 }
456
457 @SuppressWarnings("unchecked")
458 private <T> T[] array(Class<T> type, ArrayList<T> urls) {
459 return urls.toArray((T[])Array.newInstance(type, urls.size()));
460 }
461
462 private String url(File file) throws IOException {
463 return "file:"+(file.getCanonicalPath().replace(" ", "%20"));
464 }
465
466 @SuppressWarnings("serial")
467 public static class UsageException extends Exception {
468 public UsageException(String message) {
469 super(message);
470 }
471 }
472
473 private File nativeFile(String suffix) {
474 return new File(nativeOutput, name+suffix);
475 }
476
477 public String getCopyright() {
478 if (copyright == null)
479 return "";
480
481 int index = copyright.indexOf(END_YEAR_TAG);
482 if (index != -1) {
483 String temp = copyright.substring(0, index);
484 temp += Calendar.getInstance().get(Calendar.YEAR);
485 temp += copyright.substring(index + END_YEAR_TAG.length());
486 copyright = temp;
487 }
488
489 return copyright;
490 }
491
492 }