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}