001/******************************************************************************* 002 * Copyright (C) 2009-2011 FuseSource Corp. 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 *******************************************************************************/ 011package org.fusesource.hawtjni.generator; 012 013import java.io.ByteArrayOutputStream; 014import java.io.File; 015import java.io.IOException; 016import java.io.InputStream; 017import java.io.PrintStream; 018import java.io.PrintWriter; 019import java.lang.reflect.Array; 020import java.net.URL; 021import java.net.URLClassLoader; 022import java.util.ArrayList; 023import java.util.Arrays; 024import java.util.Calendar; 025import java.util.HashSet; 026import java.util.LinkedHashSet; 027import java.util.List; 028import java.util.Set; 029import java.util.regex.Pattern; 030 031import org.apache.commons.cli.CommandLine; 032import org.apache.commons.cli.HelpFormatter; 033import org.apache.commons.cli.Options; 034import org.apache.commons.cli.ParseException; 035import org.apache.commons.cli.PosixParser; 036import org.apache.xbean.finder.ClassFinder; 037import org.apache.xbean.finder.UrlSet; 038import org.fusesource.hawtjni.generator.model.JNIClass; 039import org.fusesource.hawtjni.generator.model.ReflectClass; 040import org.fusesource.hawtjni.generator.util.FileSupport; 041import org.fusesource.hawtjni.runtime.ClassFlag; 042import org.fusesource.hawtjni.runtime.JniClass; 043 044import static org.fusesource.hawtjni.generator.util.OptionBuilder.*; 045 046/** 047 * 048 * @author <a href="http://hiramchirino.com">Hiram Chirino</a> 049 */ 050public 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( target.getName().endsWith(".c") && gen.isCPP ) { 433 target = new File(target.getParentFile(), target.getName()+"pp"); 434 } 435 if( FileSupport.write(out.toByteArray(), target) ) { 436 progress("Wrote: "+target); 437 } 438 } 439 } 440 441 private void generateFromResource(String resource, File target) throws IOException { 442 ByteArrayOutputStream out = new ByteArrayOutputStream(); 443 InputStream is = getClass().getClassLoader().getResourceAsStream(resource); 444 FileSupport.copy(is, out); 445 String content = new String(out.toByteArray(), "UTF-8"); 446 String[] parts = content.split(Pattern.quote("/* == HEADER-SNIP-LOCATION == */")); 447 if( parts.length==2 ) { 448 content = parts[1]; 449 } 450 out.reset(); 451 PrintStream ps = new PrintStream(out); 452 ps.print(JNIGenerator.fixDelimiter(getCopyright())); 453 ps.print(JNIGenerator.fixDelimiter(content)); 454 ps.close(); 455 if( FileSupport.write(out.toByteArray(), target) ) { 456 progress("Wrote: "+target); 457 } 458 } 459 460 @SuppressWarnings("unchecked") 461 private <T> T[] array(Class<T> type, ArrayList<T> urls) { 462 return urls.toArray((T[])Array.newInstance(type, urls.size())); 463 } 464 465 private String url(File file) throws IOException { 466 return "file:"+(file.getCanonicalPath().replace(" ", "%20")); 467 } 468 469 @SuppressWarnings("serial") 470 public static class UsageException extends Exception { 471 public UsageException(String message) { 472 super(message); 473 } 474 } 475 476 private File nativeFile(String suffix) { 477 return new File(nativeOutput, name+suffix); 478 } 479 480 public String getCopyright() { 481 if (copyright == null) 482 return ""; 483 484 int index = copyright.indexOf(END_YEAR_TAG); 485 if (index != -1) { 486 String temp = copyright.substring(0, index); 487 temp += Calendar.getInstance().get(Calendar.YEAR); 488 temp += copyright.substring(index + END_YEAR_TAG.length()); 489 copyright = temp; 490 } 491 492 return copyright; 493 } 494 495}