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     *******************************************************************************/
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( 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    }