1/*
2 * Copyright (c) 2003, 2012, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package sun.rmi.rmic.newrmic;
27
28import com.sun.javadoc.ClassDoc;
29import com.sun.javadoc.RootDoc;
30import java.io.File;
31import java.io.FileNotFoundException;
32import java.io.IOException;
33import java.io.OutputStream;
34import java.io.PrintStream;
35import java.io.PrintWriter;
36import java.lang.reflect.Constructor;
37import java.lang.reflect.InvocationTargetException;
38import java.util.ArrayList;
39import java.util.Collections;
40import java.util.HashMap;
41import java.util.HashSet;
42import java.util.List;
43import java.util.Map;
44import java.util.Set;
45import sun.rmi.rmic.newrmic.jrmp.JrmpGenerator;
46import sun.tools.util.CommandLine;
47
48/**
49 * The rmic front end.  This class contains the "main" method for rmic
50 * command line invocation.
51 *
52 * A Main instance contains the stream to output error messages and
53 * other diagnostics to.
54 *
55 * An rmic compilation batch (for example, one rmic command line
56 * invocation) is executed by invoking the "compile" method of a Main
57 * instance.
58 *
59 * WARNING: The contents of this source file are not part of any
60 * supported API.  Code that depends on them does so at its own risk:
61 * they are subject to change or removal without notice.
62 *
63 * NOTE: If and when there is a J2SE API for invoking SDK tools, this
64 * class should be updated to support that API.
65 *
66 * NOTE: This class is the front end for a "new" rmic implementation,
67 * which uses javadoc and the doclet API for reading class files and
68 * javac for compiling generated source files.  This implementation is
69 * incomplete: it lacks any CORBA-based back end implementations, and
70 * thus the command line options "-idl", "-iiop", and their related
71 * options are not yet supported.  The front end for the "old",
72 * oldjavac-based rmic implementation is sun.rmi.rmic.Main.
73 *
74 * @author Peter Jones
75 **/
76public class Main {
77
78    /*
79     * Implementation note:
80     *
81     * In order to use the doclet API to read class files, much of
82     * this implementation of rmic executes as a doclet within an
83     * invocation of javadoc.  This class is used as the doclet class
84     * for such javadoc invocations, via its static "start" and
85     * "optionLength" methods.  There is one javadoc invocation per
86     * rmic compilation batch.
87     *
88     * The only guaranteed way to pass data to a doclet through a
89     * javadoc invocation is through doclet-specific options on the
90     * javadoc "command line".  Rather than passing numerous pieces of
91     * individual data in string form as javadoc options, we use a
92     * single doclet-specific option ("-batchID") to pass a numeric
93     * identifier that uniquely identifies the rmic compilation batch
94     * that the javadoc invocation is for, and that identifier can
95     * then be used as a key in a global table to retrieve an object
96     * containing all of batch-specific data (rmic command line
97     * arguments, etc.).
98     */
99
100    /** guards "batchCount" */
101    private static final Object batchCountLock = new Object();
102
103    /** number of batches run; used to generated batch IDs */
104    private static long batchCount = 0;
105
106    /** maps batch ID to batch data */
107    private static final Map<Long,Batch> batchTable =
108        Collections.synchronizedMap(new HashMap<Long,Batch>());
109
110    /** stream to output error messages and other diagnostics to */
111    private final PrintStream out;
112
113    /** name of this program, to use in error messages */
114    private final String program;
115
116    /**
117     * Command line entry point.
118     **/
119    public static void main(String[] args) {
120        Main rmic = new Main(System.err, "rmic");
121        System.exit(rmic.compile(args) ? 0 : 1);
122    }
123
124    /**
125     * Creates a Main instance that writes output to the specified
126     * stream.  The specified program name is used in error messages.
127     **/
128    public Main(OutputStream out, String program) {
129        this.out = out instanceof PrintStream ?
130            (PrintStream) out : new PrintStream(out);
131        this.program = program;
132    }
133
134    /**
135     * Compiles a batch of input classes, as given by the specified
136     * command line arguments.  Protocol-specific generators are
137     * determined by the choice options on the command line.  Returns
138     * true if successful, or false if an error occurred.
139     *
140     * NOTE: This method is retained for transitional consistency with
141     * previous implementations.
142     **/
143    public boolean compile(String[] args) {
144        long startTime = System.currentTimeMillis();
145
146        long batchID;
147        synchronized (batchCountLock) {
148            batchID = batchCount++;     // assign batch ID
149        }
150
151        // process command line
152        Batch batch = parseArgs(args);
153        if (batch == null) {
154            return false;               // terminate if error occurred
155        }
156
157        /*
158         * With the batch data retrievable in the global table, run
159         * javadoc to continue the rest of the batch's compliation as
160         * a doclet.
161         */
162        boolean status;
163        try {
164            batchTable.put(batchID, batch);
165            status = invokeJavadoc(batch, batchID);
166        } finally {
167            batchTable.remove(batchID);
168        }
169
170        if (batch.verbose) {
171            long deltaTime = System.currentTimeMillis() - startTime;
172            output(Resources.getText("rmic.done_in",
173                                     Long.toString(deltaTime)));
174        }
175
176        return status;
177    }
178
179    /**
180     * Prints the specified string to the output stream of this Main
181     * instance.
182     **/
183    public void output(String msg) {
184        out.println(msg);
185    }
186
187    /**
188     * Prints an error message to the output stream of this Main
189     * instance.  The first argument is used as a key in rmic's
190     * resource bundle, and the rest of the arguments are used as
191     * arguments in the formatting of the resource string.
192     **/
193    public void error(String msg, String... args) {
194        output(Resources.getText(msg, args));
195    }
196
197    /**
198     * Prints rmic's usage message to the output stream of this Main
199     * instance.
200     *
201     * This method is public so that it can be used by the "parseArgs"
202     * methods of Generator implementations.
203     **/
204    public void usage() {
205        error("rmic.usage", program);
206    }
207
208    /**
209     * Processes rmic command line arguments.  Returns a Batch object
210     * representing the command line arguments if successful, or null
211     * if an error occurred.  Processed elements of the args array are
212     * set to null.
213     **/
214    private Batch parseArgs(String[] args) {
215        Batch batch = new Batch();
216
217        /*
218         * Pre-process command line for @file arguments.
219         */
220        try {
221            args = CommandLine.parse(args);
222        } catch (FileNotFoundException e) {
223            error("rmic.cant.read", e.getMessage());
224            return null;
225        } catch (IOException e) {
226            e.printStackTrace(out);
227            return null;
228        }
229
230        for (int i = 0; i < args.length; i++) {
231
232            if (args[i] == null) {
233                // already processed by a generator
234                continue;
235
236            } else if (args[i].equals("-Xnew")) {
237                // we're already using the "new" implementation
238                args[i] = null;
239
240            } else if (args[i].equals("-show")) {
241                // obselete: fail
242                error("rmic.option.unsupported", args[i]);
243                usage();
244                return null;
245
246            } else if (args[i].equals("-O")) {
247                // obselete: warn but tolerate
248                error("rmic.option.unsupported", args[i]);
249                args[i] = null;
250
251            } else if (args[i].equals("-debug")) {
252                // obselete: warn but tolerate
253                error("rmic.option.unsupported", args[i]);
254                args[i] = null;
255
256            } else if (args[i].equals("-depend")) {
257                // obselete: warn but tolerate
258                // REMIND: should this fail instead?
259                error("rmic.option.unsupported", args[i]);
260                args[i] = null;
261
262            } else if (args[i].equals("-keep") ||
263                       args[i].equals("-keepgenerated"))
264            {
265                batch.keepGenerated = true;
266                args[i] = null;
267
268            } else if (args[i].equals("-g")) {
269                batch.debug = true;
270                args[i] = null;
271
272            } else if (args[i].equals("-nowarn")) {
273                batch.noWarn = true;
274                args[i] = null;
275
276            } else if (args[i].equals("-nowrite")) {
277                batch.noWrite = true;
278                args[i] = null;
279
280            } else if (args[i].equals("-verbose")) {
281                batch.verbose = true;
282                args[i] = null;
283
284            } else if (args[i].equals("-Xnocompile")) {
285                batch.noCompile = true;
286                batch.keepGenerated = true;
287                args[i] = null;
288
289            } else if (args[i].equals("-bootclasspath")) {
290                if ((i + 1) >= args.length) {
291                    error("rmic.option.requires.argument", args[i]);
292                    usage();
293                    return null;
294                }
295                if (batch.bootClassPath != null) {
296                    error("rmic.option.already.seen", args[i]);
297                    usage();
298                    return null;
299                }
300                args[i] = null;
301                batch.bootClassPath = args[++i];
302                assert batch.bootClassPath != null;
303                args[i] = null;
304
305            } else if (args[i].equals("-extdirs")) {
306                if ((i + 1) >= args.length) {
307                    error("rmic.option.requires.argument", args[i]);
308                    usage();
309                    return null;
310                }
311                if (batch.extDirs != null) {
312                    error("rmic.option.already.seen", args[i]);
313                    usage();
314                    return null;
315                }
316                args[i] = null;
317                batch.extDirs = args[++i];
318                assert batch.extDirs != null;
319                args[i] = null;
320
321            } else if (args[i].equals("-classpath")) {
322                if ((i + 1) >= args.length) {
323                    error("rmic.option.requires.argument", args[i]);
324                    usage();
325                    return null;
326                }
327                if (batch.classPath != null) {
328                    error("rmic.option.already.seen", args[i]);
329                    usage();
330                    return null;
331                }
332                args[i] = null;
333                batch.classPath = args[++i];
334                assert batch.classPath != null;
335                args[i] = null;
336
337            } else if (args[i].equals("-d")) {
338                if ((i + 1) >= args.length) {
339                    error("rmic.option.requires.argument", args[i]);
340                    usage();
341                    return null;
342                }
343                if (batch.destDir != null) {
344                    error("rmic.option.already.seen", args[i]);
345                    usage();
346                    return null;
347                }
348                args[i] = null;
349                batch.destDir = new File(args[++i]);
350                assert batch.destDir != null;
351                args[i] = null;
352                if (!batch.destDir.exists()) {
353                    error("rmic.no.such.directory", batch.destDir.getPath());
354                    usage();
355                    return null;
356                }
357
358            } else if (args[i].equals("-v1.1") ||
359                       args[i].equals("-vcompat") ||
360                       args[i].equals("-v1.2"))
361            {
362                Generator gen = new JrmpGenerator();
363                batch.generators.add(gen);
364                // JrmpGenerator only requires base BatchEnvironment class
365                if (!gen.parseArgs(args, this)) {
366                    return null;
367                }
368
369            } else if (args[i].equalsIgnoreCase("-iiop")) {
370                error("rmic.option.unimplemented", args[i]);
371                return null;
372
373                // Generator gen = new IiopGenerator();
374                // batch.generators.add(gen);
375                // if (!batch.envClass.isAssignableFrom(gen.envClass())) {
376                //   error("rmic.cannot.use.both",
377                //         batch.envClass.getName(), gen.envClass().getName());
378                //   return null;
379                // }
380                // batch.envClass = gen.envClass();
381                // if (!gen.parseArgs(args, this)) {
382                //   return null;
383                // }
384
385            } else if (args[i].equalsIgnoreCase("-idl")) {
386                error("rmic.option.unimplemented", args[i]);
387                return null;
388
389                // see implementation sketch above
390
391            } else if (args[i].equalsIgnoreCase("-xprint")) {
392                error("rmic.option.unimplemented", args[i]);
393                return null;
394
395                // see implementation sketch above
396            }
397        }
398
399        /*
400         * At this point, all that remains non-null in the args
401         * array are input class names or illegal options.
402         */
403        for (int i = 0; i < args.length; i++) {
404            if (args[i] != null) {
405                if (args[i].startsWith("-")) {
406                    error("rmic.no.such.option", args[i]);
407                    usage();
408                    return null;
409                } else {
410                    batch.classes.add(args[i]);
411                }
412            }
413        }
414        if (batch.classes.isEmpty()) {
415            usage();
416            return null;
417        }
418
419        /*
420         * If options did not specify at least one protocol-specific
421         * generator, then JRMP is the default.
422         */
423        if (batch.generators.isEmpty()) {
424            batch.generators.add(new JrmpGenerator());
425        }
426        return batch;
427    }
428
429    /**
430     * Doclet class entry point.
431     **/
432    public static boolean start(RootDoc rootDoc) {
433
434        /*
435         * Find batch ID among javadoc options, and retrieve
436         * corresponding batch data from global table.
437         */
438        long batchID = -1;
439        for (String[] option : rootDoc.options()) {
440            if (option[0].equals("-batchID")) {
441                try {
442                    batchID = Long.parseLong(option[1]);
443                } catch (NumberFormatException e) {
444                    throw new AssertionError(e);
445                }
446            }
447        }
448        Batch batch = batchTable.get(batchID);
449        assert batch != null;
450
451        /*
452         * Construct batch environment using class agreed upon by
453         * generator implementations.
454         */
455        BatchEnvironment env;
456        try {
457            Constructor<? extends BatchEnvironment> cons =
458                batch.envClass.getConstructor(new Class<?>[] { RootDoc.class });
459            env = cons.newInstance(rootDoc);
460        } catch (NoSuchMethodException e) {
461            throw new AssertionError(e);
462        } catch (IllegalAccessException e) {
463            throw new AssertionError(e);
464        } catch (InstantiationException e) {
465            throw new AssertionError(e);
466        } catch (InvocationTargetException e) {
467            throw new AssertionError(e);
468        }
469
470        env.setVerbose(batch.verbose);
471
472        /*
473         * Determine the destination directory (the top of the package
474         * hierarchy) for the output of this batch; if no destination
475         * directory was specified on the command line, then the
476         * default is the current working directory.
477         */
478        File destDir = batch.destDir;
479        if (destDir == null) {
480            destDir = new File(System.getProperty("user.dir"));
481        }
482
483        /*
484         * Run each input class through each generator.
485         */
486        for (String inputClassName : batch.classes) {
487            ClassDoc inputClass = rootDoc.classNamed(inputClassName);
488            try {
489                for (Generator gen : batch.generators) {
490                    gen.generate(env, inputClass, destDir);
491                }
492            } catch (NullPointerException e) {
493                /*
494                 * We assume that this means that some class that was
495                 * needed (perhaps even a bootstrap class) was not
496                 * found, and that javadoc has already reported this
497                 * as an error.  There is nothing for us to do here
498                 * but try to continue with the next input class.
499                 *
500                 * REMIND: More explicit error checking throughout
501                 * would be preferable, however.
502                 */
503            }
504        }
505
506        /*
507         * Compile any generated source files, if configured to do so.
508         */
509        boolean status = true;
510        List<File> generatedFiles = env.generatedFiles();
511        if (!batch.noCompile && !batch.noWrite && !generatedFiles.isEmpty()) {
512            status = batch.enclosingMain().invokeJavac(batch, generatedFiles);
513        }
514
515        /*
516         * Delete any generated source files, if configured to do so.
517         */
518        if (!batch.keepGenerated) {
519            for (File file : generatedFiles) {
520                file.delete();
521            }
522        }
523
524        return status;
525    }
526
527    /**
528     * Doclet class method that indicates that this doclet class
529     * recognizes (only) the "-batchID" option on the javadoc command
530     * line, and that the "-batchID" option comprises two arguments on
531     * the javadoc command line.
532     **/
533    public static int optionLength(String option) {
534        if (option.equals("-batchID")) {
535            return 2;
536        } else {
537            return 0;
538        }
539    }
540
541    /**
542     * Runs the javadoc tool to invoke this class as a doclet, passing
543     * command line options derived from the specified batch data and
544     * indicating the specified batch ID.
545     *
546     * NOTE: This method currently uses a J2SE-internal API to run
547     * javadoc.  If and when there is a J2SE API for invoking SDK
548     * tools, this method should be updated to use that API instead.
549     **/
550    private boolean invokeJavadoc(Batch batch, long batchID) {
551        List<String> javadocArgs = new ArrayList<String>();
552
553        // include all types, regardless of language-level access
554        javadocArgs.add("-private");
555
556        // inputs are class names, not source files
557        javadocArgs.add("-Xclasses");
558
559        // reproduce relevant options from rmic invocation
560        if (batch.verbose) {
561            javadocArgs.add("-verbose");
562        }
563        if (batch.bootClassPath != null) {
564            javadocArgs.add("-bootclasspath");
565            javadocArgs.add(batch.bootClassPath);
566        }
567        if (batch.extDirs != null) {
568            javadocArgs.add("-extdirs");
569            javadocArgs.add(batch.extDirs);
570        }
571        if (batch.classPath != null) {
572            javadocArgs.add("-classpath");
573            javadocArgs.add(batch.classPath);
574        }
575
576        // specify batch ID
577        javadocArgs.add("-batchID");
578        javadocArgs.add(Long.toString(batchID));
579
580        /*
581         * Run javadoc on union of rmic input classes and all
582         * generators' bootstrap classes, so that they will all be
583         * available to the doclet code.
584         */
585        Set<String> classNames = new HashSet<String>();
586        for (Generator gen : batch.generators) {
587            classNames.addAll(gen.bootstrapClassNames());
588        }
589        classNames.addAll(batch.classes);
590        for (String s : classNames) {
591            javadocArgs.add(s);
592        }
593
594        // run javadoc with our program name and output stream
595        int status = com.sun.tools.javadoc.Main.execute(
596            program,
597            new PrintWriter(out, true),
598            new PrintWriter(out, true),
599            new PrintWriter(out, true),
600            this.getClass().getName(),          // doclet class is this class
601            javadocArgs.toArray(new String[javadocArgs.size()]));
602        return status == 0;
603    }
604
605    /**
606     * Runs the javac tool to compile the specified source files,
607     * passing command line options derived from the specified batch
608     * data.
609     *
610     * NOTE: This method currently uses a J2SE-internal API to run
611     * javac.  If and when there is a J2SE API for invoking SDK tools,
612     * this method should be updated to use that API instead.
613     **/
614    private boolean invokeJavac(Batch batch, List<File> files) {
615        List<String> javacArgs = new ArrayList<String>();
616
617        // rmic never wants to display javac warnings
618        javacArgs.add("-nowarn");
619
620        // reproduce relevant options from rmic invocation
621        if (batch.debug) {
622            javacArgs.add("-g");
623        }
624        if (batch.verbose) {
625            javacArgs.add("-verbose");
626        }
627        if (batch.bootClassPath != null) {
628            javacArgs.add("-bootclasspath");
629            javacArgs.add(batch.bootClassPath);
630        }
631        if (batch.extDirs != null) {
632            javacArgs.add("-extdirs");
633            javacArgs.add(batch.extDirs);
634        }
635        if (batch.classPath != null) {
636            javacArgs.add("-classpath");
637            javacArgs.add(batch.classPath);
638        }
639
640        /*
641         * For now, rmic still always produces class files that have a
642         * class file format version compatible with JDK 1.1.
643         */
644        javacArgs.add("-source");
645        javacArgs.add("1.3");
646        javacArgs.add("-target");
647        javacArgs.add("1.1");
648
649        // add source files to compile
650        for (File file : files) {
651            javacArgs.add(file.getPath());
652        }
653
654        // run javac with our output stream
655        int status = com.sun.tools.javac.Main.compile(
656            javacArgs.toArray(new String[javacArgs.size()]),
657            new PrintWriter(out, true));
658        return status == 0;
659    }
660
661    /**
662     * The data for an rmic compliation batch: the processed command
663     * line arguments.
664     **/
665    private class Batch {
666        boolean keepGenerated = false;  // -keep or -keepgenerated
667        boolean debug = false;          // -g
668        boolean noWarn = false;         // -nowarn
669        boolean noWrite = false;        // -nowrite
670        boolean verbose = false;        // -verbose
671        boolean noCompile = false;      // -Xnocompile
672        String bootClassPath = null;    // -bootclasspath
673        String extDirs = null;          // -extdirs
674        String classPath = null;        // -classpath
675        File destDir = null;            // -d
676        List<Generator> generators = new ArrayList<Generator>();
677        Class<? extends BatchEnvironment> envClass = BatchEnvironment.class;
678        List<String> classes = new ArrayList<String>();
679
680        Batch() { }
681
682        /**
683         * Returns the Main instance for this batch.
684         **/
685        Main enclosingMain() {
686            return Main.this;
687        }
688    }
689}
690