1 /*
   2  * Copyright (c) 2003, 2005, 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 
  26 package sun.rmi.rmic.newrmic;
  27 
  28 import com.sun.javadoc.ClassDoc;
  29 import com.sun.javadoc.RootDoc;
  30 import java.io.File;
  31 import java.io.FileNotFoundException;
  32 import java.io.IOException;
  33 import java.io.OutputStream;
  34 import java.io.PrintStream;
  35 import java.io.PrintWriter;
  36 import java.lang.reflect.Constructor;
  37 import java.lang.reflect.InvocationTargetException;
  38 import java.util.ArrayList;
  39 import java.util.Collections;
  40 import java.util.HashMap;
  41 import java.util.HashSet;
  42 import java.util.List;
  43 import java.util.Map;
  44 import java.util.Set;
  45 import sun.rmi.rmic.newrmic.jrmp.JrmpGenerator;
  46 import 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  **/
  76 public 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 }