Main.java |
1 /* 2 * Main.java 3 * 4 * Copyright (c) 1998-2005, The University of Sheffield. 5 * 6 * This file is part of GATE (see http://gate.ac.uk/), and is free 7 * software, licenced under the GNU Library General Public License, 8 * Version 2, June 1991 (in the distribution as file licence.html, 9 * and also available at http://gate.ac.uk/gate/licence.html). 10 * 11 * Hamish Cunningham, 1/Nov/00 12 * 13 * $Id: Main.java,v 1.55 2006/04/07 13:09:52 valyt Exp $ 14 */ 15 16 package gate; 17 18 import java.awt.*; 19 import java.io.*; 20 import java.net.MalformedURLException; 21 import java.net.URL; 22 import java.util.*; 23 import java.util.List; 24 25 import javax.swing.*; 26 27 import gate.gui.*; 28 import gate.util.*; 29 30 import gnu.getopt.Getopt; 31 32 33 /** Top-level entry point for the GATE command-line and GUI interfaces. 34 * <P> 35 */ 36 public class Main { 37 38 /** Debug flag */ 39 private static final boolean DEBUG = false; 40 41 /** Status flag for normal exit. */ 42 private static final int STATUS_NORMAL = 0; 43 44 /** Status flag for error exit. */ 45 private static final int STATUS_ERROR = 1; 46 47 /** Main routine for GATE. 48 * Command-line arguments: 49 * <UL> 50 * <LI> 51 * <B>-h</B> display a short help message 52 * <LI> 53 * <B>-d URL</B> define URL to be a location for CREOLE resoures 54 * <LI> 55 * <B>-i file</B> additional initialisation file (probably called 56 * <TT>gate.xml</TT>). Used for site-wide initialisation by the 57 * start-up scripts 58 * <LI> 59 * <B>-a</B> run the DB administration tool 60 * </UL> 61 */ 62 public static void main(String[] args) throws GateException { 63 Main.annotatorArgsMap = null; 64 // check we have a useable JDK 65 if( 66 System.getProperty("java.version").compareTo(Gate.getMinJdkVersion()) 67 < 0 68 ) { 69 throw new GateException( 70 "GATE requires JDK " + Gate.getMinJdkVersion() + " or newer" 71 ); 72 } 73 74 // process command-line options 75 processArgs(args); 76 77 // GATE builtins should be loaded from the jar (or classes dir), not 78 // from a web server (we load them over the web during testing to 79 // make sure that users can load their own that way) 80 Gate.setNetConnected(false); 81 Gate.setLocalWebServer(false); 82 83 84 // run the interface or do batch processing 85 if(batchMode) { 86 if(DEBUG) Out.prln("running batch process"); 87 batchProcess(); 88 } else if(dbAdminMode) { 89 if(DEBUG) Out.prln("running dbAdmin"); 90 dbAdmin(); 91 } else { 92 runGui(); 93 } 94 } // main 95 96 /** Register any CREOLE URLs that we got on the command line */ 97 private static void registerCreoleUrls() { 98 CreoleRegister reg = Gate.getCreoleRegister(); 99 Iterator iter = pendingCreoleUrls.iterator(); 100 while(iter.hasNext()) { 101 URL u = (URL) iter.next(); 102 try { 103 reg.registerDirectories(u); 104 } catch(GateException e) { 105 Err.prln("Couldn't register CREOLE directory: " + u); 106 Err.prln(e); 107 System.exit(STATUS_ERROR); 108 } 109 } 110 } // registerCreoleUrls() 111 112 /** Main Frame of the GUI; null when no GUI running */ 113 private static MainFrame frame; 114 115 /** The splash shown when Gate starts*/ 116 private static Splash splash; 117 118 /** 119 * Get the main frame of the GUI. If the GUI isn't running, it 120 * is started. 121 */ 122 public static MainFrame getMainFrame() throws GateException { 123 if(frame == null) 124 runGui(); 125 return frame; 126 } // getMainFrame() 127 128 /** Run the user interface. */ 129 private static void runGui() throws GateException { 130 131 Thread.currentThread().setPriority(Thread.MIN_PRIORITY); 132 //show the splash 133 SwingUtilities.invokeLater(new Runnable(){ 134 public void run(){ 135 //build the Splash 136 JPanel splashBox = new JPanel(); 137 splashBox.setLayout(new GridBagLayout()); 138 splashBox.setBackground(Color.white); 139 140 GridBagConstraints constraints = new GridBagConstraints(); 141 constraints.insets = new Insets(2, 2, 2, 2); 142 143 String splashName = 144 System.getProperty(GateConstants.APP_SPLASH_JAVA_PROPERTY_NAME); 145 if(splashName == null) { 146 splashName = "gateSplash.gif"; 147 } // if 148 149 constraints.gridy = 0; 150 constraints.gridwidth = 2; 151 constraints.fill = GridBagConstraints.NONE; 152 constraints.anchor = GridBagConstraints.CENTER; 153 JLabel gifLbl = new JLabel(MainFrame.getIcon("gateHeader.gif")); 154 splashBox.add(gifLbl, constraints); 155 156 constraints.gridy = 1; 157 constraints.gridwidth = 1; 158 gifLbl = new JLabel(MainFrame.getIcon(splashName)); 159 splashBox.add(gifLbl, constraints); 160 gifLbl = new JLabel(MainFrame.getIcon("sponsors.gif")); 161 splashBox.add(gifLbl, constraints); 162 163 splash = new Splash(splashBox); 164 splash.showSplash(); 165 } 166 }); 167 168 // initialise the library and load user CREOLE directories 169 try{ 170 Gate.init(); 171 }catch(Throwable t){ 172 int selection = JOptionPane.showOptionDialog( 173 null, 174 "Error during initialisation:\n" + t.toString() + 175 "\nDo you still want to start GATE?", 176 "GATE", JOptionPane.YES_NO_OPTION, JOptionPane.ERROR_MESSAGE, 177 null, new String[]{"Cancel", "Start anyway"}, 178 "Cancel"); 179 if(selection != 1){ 180 t.printStackTrace(); 181 System.exit(1); 182 } 183 } 184 185 186 //create the main frame, show it and hide the splash 187 SwingUtilities.invokeLater(new Runnable(){ 188 public void run(){ 189 //this needs to run before any GUI component is constructed. 190 //the initial gate splash is exempted from this rule. 191 applyUserPreferences(); 192 193 //all the defaults tables have been updated; build the GUI 194 if(Gate.isSlugGui()) { 195 frame = new ShellSlacFrame(); 196 if(DEBUG) Out.prln("constructing SLUG GUI"); 197 } 198 else { 199 frame = new MainFrame(); 200 if(DEBUG) Out.prln("constructing GUI"); 201 } // if - SLUG 202 203 // run the GUI 204 frame.setTitleChangable(true); 205 if(Gate.isSlugGui()) { 206 frame.setTitle("SLUG application"); 207 } 208 else { 209 frame.setTitle(name + " " + version + " build " + build); 210 } // if - SLUG 211 212 // Set title from Java properties 213 String title = 214 System.getProperty(GateConstants.TITLE_JAVA_PROPERTY_NAME); 215 if(title != null) { 216 frame.setTitle(title); 217 } // if 218 frame.setTitleChangable(false); 219 220 // Set icon from Java properties 221 // iconName could be absolute or "gate:/img/....gif" 222 String iconName = 223 System.getProperty(GateConstants.APP_ICON_JAVA_PROPERTY_NAME); 224 if(iconName != null) { 225 try { 226 frame.setIconImage(Toolkit.getDefaultToolkit().getImage( 227 new URL(iconName))); 228 } catch(MalformedURLException mue){ 229 mue.printStackTrace(Err.getPrintWriter()); 230 } 231 } // if 232 233 // Validate frames that have preset sizes 234 frame.validate(); 235 236 // Center the window 237 Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); 238 Dimension frameSize = frame.getSize(); 239 if (frameSize.height > screenSize.height) { 240 frameSize.height = screenSize.height; 241 } 242 if (frameSize.width > screenSize.width) { 243 frameSize.width = screenSize.width; 244 } 245 frame.setLocation((screenSize.width - frameSize.width) / 2, 246 (screenSize.height - frameSize.height) / 2); 247 248 frame.setVisible(true); 249 if(splash != null) splash.setVisible(false); 250 251 if(!Gate.isSlugGui()) { 252 //load session if required and available; 253 //do everything from a new thread. 254 Runnable runnable = new Runnable(){ 255 public void run(){ 256 try{ 257 File sessionFile = new File(Gate.getUserSessionFileName()); 258 if(sessionFile.exists()){ 259 MainFrame.lockGUI("Loading saved session..."); 260 gate.util.persistence.PersistenceManager.loadObjectFromFile(sessionFile); 261 } 262 }catch(Exception e){ 263 Err.prln("Failed to load session data:"); 264 e.printStackTrace(Err.getPrintWriter()); 265 }finally{ 266 MainFrame.unlockGUI(); 267 } 268 } 269 }; 270 Thread thread = new Thread(Thread.currentThread().getThreadGroup(), 271 runnable, "Session loader"); 272 thread.setPriority(Thread.MIN_PRIORITY); 273 thread.start(); 274 } // if - when no SLUG GUI load session 275 } 276 }); 277 registerCreoleUrls(); 278 } // runGui() 279 280 /** Run the db admin interface. */ 281 private static void dbAdmin() throws GateException { 282 try { UserGroupEditor.main(null); } catch(Exception e) { 283 throw new GateException(e); 284 } 285 } // dbAdmin() 286 287 /** 288 * Reads the user config data and applies the required settings. 289 */ 290 protected static void applyUserPreferences(){ 291 //look and feel 292 String lnfClassName = Gate.getUserConfig(). 293 getString(GateConstants.LOOK_AND_FEEL); 294 if(lnfClassName == null){ 295 //if running on Linux, default to Metal rather than GTK because GTK LnF 296 //doesn't play nicely with most Gnome themes 297 if(System.getProperty("os.name").toLowerCase().indexOf("linux") != -1){ 298 //running on Linux 299 lnfClassName = UIManager.getCrossPlatformLookAndFeelClassName(); 300 }else{ 301 lnfClassName = UIManager.getSystemLookAndFeelClassName(); 302 } 303 Gate.getUserConfig().put(GateConstants.LOOK_AND_FEEL, lnfClassName); 304 } 305 try { 306 UIManager.setLookAndFeel(lnfClassName); 307 } catch(Exception e) { 308 throw new gate.util.GateRuntimeException(e.toString()); 309 } 310 311 //read the user config data 312 OptionsMap userConfig = Gate.getUserConfig(); 313 314 //text font 315 Font font = userConfig.getFont(GateConstants.TEXT_COMPONENTS_FONT); 316 if(font == null){ 317 String fontName = Gate.guessUnicodeFont(); 318 if(fontName != null){ 319 font = new Font(fontName, Font.PLAIN, 12); 320 }else{ 321 font = UIManager.getFont("TextPane.font"); 322 } 323 } 324 325 if(font != null){ 326 OptionsDialog.setTextComponentsFont(font); 327 } 328 329 //menus font 330 font = userConfig.getFont(GateConstants.MENUS_FONT); 331 if(font == null){ 332 String fontName = Gate.guessUnicodeFont(); 333 if(fontName != null){ 334 font = new Font(fontName, Font.PLAIN, 12); 335 }else{ 336 font = UIManager.getFont("Menu.font"); 337 } 338 } 339 340 if(font != null){ 341 OptionsDialog.setMenuComponentsFont(font); 342 } 343 344 //other gui font 345 font = userConfig.getFont(GateConstants.OTHER_COMPONENTS_FONT); 346 if(font == null){ 347 String fontName = Gate.guessUnicodeFont(); 348 if(fontName != null){ 349 font = new Font(fontName, Font.PLAIN, 12); 350 }else{ 351 font = UIManager.getFont("Button.font"); 352 } 353 } 354 355 if(font != null){ 356 OptionsDialog.setComponentsFont(font); 357 } 358 359 360 } 361 362 363 364 // find out the version and build numbers 365 static { 366 // find out the version number 367 try { 368 InputStream ver = Files.getGateResourceAsStream("version.txt"); 369 if (ver==null) { 370 throw new IOException(); 371 } 372 BufferedReader reader = new BufferedReader(new InputStreamReader(ver, 373 "UTF-8")); 374 Main.version = reader.readLine(); 375 } catch(IOException ioe) { 376 Main.version = "3.0"; 377 } 378 379 // find out the build number 380 try{ 381 InputStream build = Files.getGateResourceAsStream("build.txt"); 382 if (build==null) { 383 throw new IOException(); 384 } 385 BufferedReader reader = new BufferedReader(new InputStreamReader(build, 386 "UTF-8")); 387 Main.build = reader.readLine(); 388 } catch(IOException ioe) { 389 Main.build = "0000"; 390 } 391 } // static initialiser finding build and version 392 393 394 /** 395 396 <BR> 397 <B>Options processing: </B> 398 399 <BR> 400 <TABLE> 401 <TR> 402 <TH ALIGN=left COLSPAN=15> 403 -a annotator arg(s) 404 </TH> 405 <TH ALIGN=left> 406 A CREOLE annotator to run on the collection, with zero or more 407 arguments. The set of such annotators will be run in the sequence 408 they appear in the arguments list. The arguments list must end with the 409 start of another option; otherwise add a "-" after the arguments to 410 terminate the list. 411 </TH> 412 </TR> 413 <TR> 414 <TH ALIGN=left COLSPAN=15> 415 -b 416 </TH> 417 <TH ALIGN=left> 418 Batch mode. Don't start the GUI, just process options and exit after 419 any actions (e.g. running annotators). 420 </TH> 421 </TR> 422 <TR> 423 <TH ALIGN=left COLSPAN=15> 424 -c collname 425 </TH> 426 <TH ALIGN=left> 427 Name of the collection to use. If the collection already exists then 428 it will be used as it stands, otherwise it will be created. See also 429 -f. 430 </TH> 431 </TR> 432 <TR> 433 <TH ALIGN=left COLSPAN=15> 434 -d 435 </TH> 436 <TH ALIGN=left> 437 Destroy the collection after use. (The default is to save it to 438 disk.) 439 </TH> 440 </TR> 441 <TR> 442 <TH ALIGN=left COLSPAN=15> 443 -f file(s) 444 </TH> 445 <TH ALIGN=left> 446 One or more files to create a collection with. If the collection 447 being used (see -c) already exists, these files are ignored. 448 Otherwise they are used to create the collection. 449 </TH> 450 </TR> 451 <TR> 452 <TH ALIGN=left COLSPAN=15> 453 -h 454 </TH> 455 <TH ALIGN=left> 456 Print a usage message and exit. 457 </TH> 458 </TR> 459 <TR> 460 <TH ALIGN=left COLSPAN=15> 461 -p creolepath 462 </TH> 463 <TH ALIGN=left> 464 Sets the search path for CREOLE modules. 465 </TH> 466 </TR> 467 <TR> 468 <TH ALIGN=left COLSPAN=15> 469 -v classname(s) 470 </TH> 471 <TH ALIGN=left> 472 Verbose: turns on debugging output. Takes zero or more class names 473 to debug. 474 </TH> 475 </TR> 476 </TABLE> 477 478 */ 479 /** Name of the collection we were asked to process. */ 480 private static String collName; 481 482 /** Search path for CREOLE modules. */ 483 private static String creolePath; 484 485 /** List of files we were asked to build a collection from. */ 486 private static List fileNames = new ArrayList(); 487 488 /** List of annotators we were asked to run on the collection. */ 489 private static List annotatorNames = new ArrayList(); 490 491 /** Map of annotator arguments. */ 492 private static Map annotatorArgsMap = new HashMap(); 493 494 /** List of classes we were asked to debug. */ 495 private static List debugNames = new ArrayList(); 496 497 /** Are we in batch mode? */ 498 public static boolean batchMode = false; 499 500 /** Are we in db admin mode? */ 501 public static boolean dbAdminMode = false; 502 503 /** Don't save collection after batch? */ 504 private static boolean destroyColl = false; 505 506 /** Verbose? */ 507 private static boolean verbose = false; 508 509 private static boolean runCorpusBenchmarkTool = false; 510 511 public static String name = "GATE"; 512 public static String version; 513 public static String build; 514 515 /** Process arguments and set up member fields appropriately. 516 * Will shut down the process (via System.exit) if there are 517 * incorrect arguments, or if the arguments ask for something 518 * simple like printing the help message. 519 */ 520 public static void processArgs(String[] args) { 521 522 Getopt g = new Getopt("GATE main", args, "hd:ei:asj"); 523 int c; 524 while( (c = g.getopt()) != -1 ) 525 switch(c) { 526 // -a 527 case 'a': 528 dbAdminMode = true; 529 break; 530 // -h 531 case 'h': 532 help(); 533 usage(); 534 System.exit(STATUS_NORMAL); 535 break; 536 // -d creole-dir 537 case 'd': 538 String urlString = g.getOptarg(); 539 URL u = null; 540 try { 541 u = new URL(urlString); 542 } catch(MalformedURLException e) { 543 Err.prln("Bad URL: " + urlString); 544 Err.prln(e); 545 System.exit(STATUS_ERROR); 546 } 547 pendingCreoleUrls.add(u); 548 Out.prln( 549 "CREOLE Directory " + urlString + " queued for registration" 550 ); 551 break; 552 // -i gate.xml site-wide init file 553 case 'i': 554 String optionString = g.getOptarg(); 555 URL u2 = null; 556 File f = new File(optionString); 557 try { 558 u2 = f.toURL(); 559 } catch(MalformedURLException e) { 560 Err.prln("Bad initialisation file: " + optionString); 561 Err.prln(e); 562 System.exit(STATUS_ERROR); 563 } 564 Gate.setSiteConfigFile(f); 565 if(DEBUG) 566 Out.prln( 567 "Initialisation file " + optionString + 568 " recorded for initialisation" 569 ); 570 break; 571 // -e runs the CorpusBenchmarkTool (e for evaluate) 572 case 'e': 573 try { 574 CorpusBenchmarkTool.main(args); 575 } catch (GateException ex) { 576 Out.prln("Error running the evaluation tool: " + ex.getMessage()); 577 System.exit(-1); 578 } 579 break; 580 // -s runs the SLUG GUI 581 case 's': 582 Gate.setSlugGui(true); 583 break; 584 // -j enable Jape Debugger 585 case 'j': 586 Gate.setEnableJapeDebug(true); 587 break; 588 589 590 591 /* 592 // -c collname 593 case '-c': 594 collName = g.getOptarg(); 595 break; 596 597 // -b 598 case '-b': 599 batchMode = true; 600 break; 601 602 // -a annotator(s) 603 case '-a': 604 if(++i == args.length) { usage(); return; } 605 String annotatorName = g.getOptarg(); 606 annotatorNames.add(annotatorName); 607 // collect any args for the annotator 608 break; 609 610 // -d 611 case '-d': 612 destroyColl = true; 613 break; 614 615 // -f file(s) 616 case '-f': 617 while(++i < args.length) 618 if(args[i].toCharArray()[0] == '-') { // start of another option 619 i--; 620 break; 621 } 622 else 623 fileNames.add(args[i]); 624 break; 625 626 // -p creolepath 627 case '-p': 628 if(++i < args.length) 629 creolePath = args[i]; 630 else 631 { usage(); return; } 632 break; 633 634 // -v classname(s) 635 case '-v': 636 verbose = true; 637 Debug.setDebug(true); 638 while(++i < args.length) { 639 if(args[i].toCharArray()[0] == '-') { // start of another option 640 i--; 641 break; 642 } 643 else 644 debugNames.add(args[i]); 645 } // while 646 break; 647 */ 648 649 case '?': 650 // leave the warning to getopt 651 System.exit(STATUS_ERROR); 652 break; 653 654 default: 655 // shouldn't happen! 656 Err.prln("getopt() returned " + c + "\n"); 657 System.exit(STATUS_ERROR); 658 break; 659 } // getopt switch 660 661 } // processArgs() 662 663 /** Run commands as a batch process. */ 664 private static void batchProcess() throws GateException{ 665 // initialise the library and load user CREOLE directories 666 Gate.init(); 667 registerCreoleUrls(); 668 669 /* 670 // turn debugging on where requested 671 if(verbose) { 672 for(ArrayIterator i = debugNames.begin(); ! i.atEnd(); i.advance()) { 673 try { Debug.setDebug(Class.forName(((String) i.get())), true); } 674 catch(ClassNotFoundException e) { 675 System.err.println( 676 "can't debug class " + (String) i.get() + ": " + e.toString() 677 ); 678 } 679 } // for 680 } // debugging on 681 682 // collection: does it exist and can we open it? 683 if(collName == null) { 684 System.err.println("no collection name given"); 685 usage(); 686 return; 687 } 688 File collDir = new File(collName); 689 JdmCollection coll = null; 690 if(collDir.exists()) { // open collection 691 Debug.prnl("opening collection " + collName); 692 try { 693 coll = new JdmCollection(collName); 694 } catch (JdmException e) { 695 System.err.println( 696 "Couldn't open collection " + collName + " " + e.toString() 697 ); 698 return; 699 } 700 } else { // create collection and add documents 701 Debug.prnl("creating collection " + collName); 702 JdmAttributeSequence attrs = new JdmAttributeSequence(); 703 try { 704 coll = new JdmCollection(collName, attrs); 705 } catch (JdmException e) { 706 System.err.println( 707 "Couldn't create collection " + collName + " " + e.toString() 708 ); 709 return; 710 } 711 712 // add the documents to the collection 713 for(ArrayIterator i = fileNames.begin(); ! i.atEnd(); i.advance()) { 714 Debug.prnl("adding document " + (String) i.get()); 715 try { 716 JdmDocument doc = coll.createDocument( 717 (String) i.get(), 718 null, 719 new JdmAnnotationSet(), 720 new JdmAttributeSequence() 721 ); 722 } catch (JdmException e) { 723 System.err.println( 724 "Can't add document " + (String) i.get() + ": " + e.toString() 725 ); 726 } // catch 727 } // for each filename 728 } // collection create 729 730 // run the annotators on each document in the collection 731 // for each document 732 JdmDocument doc = null; 733 if(coll.length() > 0) 734 try{ doc = coll.firstDocument(); } catch(JdmException e) { } 735 for(int i = 0; i<coll.length(); i++) { 736 if(doc == null) continue; // first and next doc shouldn't throw excptns! 737 738 // for each annotator 739 for(ArrayIterator j = annotatorNames.begin(); !j.atEnd(); j.advance()) { 740 String annotatorName = (String) j.get(); 741 Debug.prnl( 742 "calling annotator " + annotatorName + " on doc " + doc.getId() 743 ); 744 745 // load the annotator class 746 Annotator annotator = null; 747 Class annotatorClass = null; 748 try { 749 // cheat and assume that all annotators are on CLASSPATH 750 annotatorClass = Class.forName(annotatorName); 751 } catch (Exception ex) { 752 System.err.println( 753 "Could load class for CREOLE object " + annotatorName + ": " + 754 ex.toString() 755 ); 756 continue; 757 } 758 759 // construct the annotator 760 try { 761 annotator = (Annotator) annotatorClass.newInstance(); 762 } catch (Throwable ex) { // naughty chap 763 System.err.println( 764 "Could create instance of CREOLE object " + annotatorName + ": " + 765 ex.toString() 766 ); 767 continue; 768 } 769 770 // annotate this document 771 String[] args = (String[]) annotatorArgsMap.get(annotatorName); 772 if(args == null) args = new String[0]; 773 annotator.annotate(doc, args); 774 } // for each annotator 775 776 doc = null; 777 try { doc = coll.nextDocument(); } catch(JdmException e) { } 778 } // for each doc, annotate 779 780 // save collection? 781 if(! destroyColl) { 782 Debug.prnl("saving the collection"); 783 try { 784 coll.sync(); 785 } catch (JdmException e) { 786 System.err.println( 787 "Can't save collection " + collName + ": " + e.toString() 788 ); 789 } 790 } else { 791 Debug.prnl("destroying collection"); 792 try { coll.destroy(); } catch(JdmException e) { 793 // if we didn't sync we can't destroy, but that's not an error 794 } 795 } 796 797 Debug.prnl("done batch process"); 798 */ 799 } // batchProcess() 800 801 /** Display a usage message */ 802 public static void usage() { 803 Out.prln( 804 "Usage: java gate.Main " + 805 "[ -h [-d CREOLE-URL]" + 806 "" 807 ); 808 } // usage() 809 810 /** Display a help message */ 811 public static void help() { 812 String nl = Strings.getNl(); 813 Out.prln( 814 "For help on command-line options and other information " + nl + 815 "see the user manual in your GATE distribution or at " + nl + 816 "http://gate.ac.uk/sale/tao/" 817 ); 818 } // help() 819 820 /** The list of pending URLs to add to the CREOLE register */ 821 private static List pendingCreoleUrls = new ArrayList(); 822 823 } // class Main 824