Example.java revision 3976:8be741555fa6
1/* 2 * Copyright (c) 2010, 2017, 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. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 24import java.io.*; 25import java.net.URL; 26import java.net.URLClassLoader; 27import java.util.*; 28import java.util.regex.*; 29import javax.annotation.processing.Processor; 30import javax.tools.Diagnostic; 31import javax.tools.DiagnosticCollector; 32import javax.tools.JavaCompiler; 33import javax.tools.JavaCompiler.CompilationTask; 34import javax.tools.JavaFileManager; 35import javax.tools.JavaFileObject; 36import javax.tools.StandardJavaFileManager; 37import javax.tools.ToolProvider; 38 39// The following two classes are both used, but cannot be imported directly 40// import com.sun.tools.javac.Main 41// import com.sun.tools.javac.main.Main 42 43import com.sun.tools.javac.api.ClientCodeWrapper; 44import com.sun.tools.javac.file.JavacFileManager; 45import com.sun.tools.javac.main.Main; 46import com.sun.tools.javac.util.Context; 47import com.sun.tools.javac.util.JavacMessages; 48import com.sun.tools.javac.util.JCDiagnostic; 49 50/** 51 * Class to handle example code designed to illustrate javac diagnostic messages. 52 */ 53class Example implements Comparable<Example> { 54 /* Create an Example from the files found at path. 55 * The head of the file, up to the first Java code, is scanned 56 * for information about the test, such as what resource keys it 57 * generates when run, what options are required to run it, and so on. 58 */ 59 Example(File file) { 60 this.file = file; 61 declaredKeys = new TreeSet<String>(); 62 srcFiles = new ArrayList<File>(); 63 procFiles = new ArrayList<File>(); 64 srcPathFiles = new ArrayList<File>(); 65 moduleSourcePathFiles = new ArrayList<File>(); 66 patchModulePathFiles = new ArrayList<File>(); 67 modulePathFiles = new ArrayList<File>(); 68 classPathFiles = new ArrayList<File>(); 69 additionalFiles = new ArrayList<File>(); 70 nonEmptySrcFiles = new ArrayList<File>(); 71 72 findFiles(file, srcFiles); 73 for (File f: srcFiles) { 74 parse(f); 75 } 76 77 if (infoFile == null) 78 throw new Error("Example " + file + " has no info file"); 79 } 80 81 private void findFiles(File f, List<File> files) { 82 if (f.isDirectory()) { 83 for (File c: f.listFiles()) { 84 if (files == srcFiles && c.getName().equals("processors")) 85 findFiles(c, procFiles); 86 else if (files == srcFiles && c.getName().equals("sourcepath")) { 87 srcPathDir = c; 88 findFiles(c, srcPathFiles); 89 } else if (files == srcFiles && c.getName().equals("modulesourcepath")) { 90 moduleSourcePathDir = c; 91 findFiles(c, moduleSourcePathFiles); 92 } else if (files == srcFiles && c.getName().equals("patchmodule")) { 93 patchModulePathDir = c; 94 findFiles(c, patchModulePathFiles); 95 } else if (files == srcFiles && c.getName().equals("additional")) { 96 additionalFilesDir = c; 97 findFiles(c, additionalFiles); 98 } else if (files == srcFiles && c.getName().equals("modulepath")) { 99 findFiles(c, modulePathFiles); 100 } else if (files == srcFiles && c.getName().equals("classpath")) { 101 findFiles(c, classPathFiles); 102 } else { 103 findFiles(c, files); 104 } 105 } 106 } else if (f.isFile()) { 107 if (f.getName().endsWith(".java")) { 108 files.add(f); 109 } else if (f.getName().equals("modulesourcepath")) { 110 moduleSourcePathDir = f; 111 } 112 } 113 } 114 115 private void parse(File f) { 116 Pattern keyPat = Pattern.compile(" *// *key: *([^ ]+) *"); 117 Pattern optPat = Pattern.compile(" *// *options: *(.*)"); 118 Pattern runPat = Pattern.compile(" *// *run: *(.*)"); 119 Pattern javaPat = Pattern.compile(" *@?[A-Za-z].*"); 120 try { 121 String[] lines = read(f).split("[\r\n]+"); 122 for (String line: lines) { 123 Matcher keyMatch = keyPat.matcher(line); 124 if (keyMatch.matches()) { 125 foundInfo(f); 126 declaredKeys.add(keyMatch.group(1)); 127 continue; 128 } 129 Matcher optMatch = optPat.matcher(line); 130 if (optMatch.matches()) { 131 foundInfo(f); 132 options = Arrays.asList(optMatch.group(1).trim().split(" +")); 133 continue; 134 } 135 Matcher runMatch = runPat.matcher(line); 136 if (runMatch.matches()) { 137 foundInfo(f); 138 runOpts = Arrays.asList(runMatch.group(1).trim().split(" +")); 139 } 140 if (javaPat.matcher(line).matches()) { 141 nonEmptySrcFiles.add(f); 142 break; 143 } 144 } 145 } catch (IOException e) { 146 throw new Error(e); 147 } 148 } 149 150 private void foundInfo(File file) { 151 if (infoFile != null && !infoFile.equals(file)) 152 throw new Error("multiple info files found: " + infoFile + ", " + file); 153 infoFile = file; 154 } 155 156 String getName() { 157 return file.getName(); 158 } 159 160 /** 161 * Get the set of resource keys that this test declares it will generate 162 * when it is run. 163 */ 164 Set<String> getDeclaredKeys() { 165 return declaredKeys; 166 } 167 168 /** 169 * Get the set of resource keys that this test generates when it is run. 170 * The test will be run if it has not already been run. 171 */ 172 Set<String> getActualKeys() { 173 if (actualKeys == null) 174 actualKeys = run(false); 175 return actualKeys; 176 } 177 178 /** 179 * Run the test. Information in the test header is used to determine 180 * how to run the test. 181 */ 182 void run(PrintWriter out, boolean raw, boolean verbose) { 183 if (out == null) 184 throw new NullPointerException(); 185 try { 186 run(out, null, raw, verbose); 187 } catch (IOException e) { 188 e.printStackTrace(out); 189 } 190 } 191 192 Set<String> run(boolean verbose) { 193 Set<String> keys = new TreeSet<String>(); 194 try { 195 run(null, keys, true, verbose); 196 } catch (IOException e) { 197 e.printStackTrace(System.err); 198 } 199 return keys; 200 } 201 202 /** 203 * Run the test. Information in the test header is used to determine 204 * how to run the test. 205 */ 206 private void run(PrintWriter out, Set<String> keys, boolean raw, boolean verbose) 207 throws IOException { 208 List<String> opts = new ArrayList<String>(); 209 if (!modulePathFiles.isEmpty()) { 210 File modulepathDir = new File(tempDir, "modulepath"); 211 modulepathDir.mkdirs(); 212 clean(modulepathDir); 213 List<String> sOpts = Arrays.asList("-d", modulepathDir.getPath(), 214 "--module-source-path", new File(file, "modulepath").getAbsolutePath()); 215 new Jsr199Compiler(verbose).run(null, null, false, sOpts, modulePathFiles); 216 opts.add("--module-path"); 217 opts.add(modulepathDir.getAbsolutePath()); 218 } 219 220 if (!classPathFiles.isEmpty()) { 221 File classpathDir = new File(tempDir, "classpath"); 222 classpathDir.mkdirs(); 223 clean(classpathDir); 224 List<String> sOpts = Arrays.asList("-d", classpathDir.getPath()); 225 new Jsr199Compiler(verbose).run(null, null, false, sOpts, classPathFiles); 226 opts.add("--class-path"); 227 opts.add(classpathDir.getAbsolutePath()); 228 } 229 230 File classesDir = new File(tempDir, "classes"); 231 classesDir.mkdirs(); 232 clean(classesDir); 233 234 opts.add("-d"); 235 opts.add(classesDir.getPath()); 236 if (options != null) 237 opts.addAll(options); 238 239 if (procFiles.size() > 0) { 240 List<String> pOpts = new ArrayList<>(Arrays.asList("-d", classesDir.getPath())); 241 242 // hack to automatically add exports; a better solution would be to grep the 243 // source for import statements or a magic comment 244 for (File pf: procFiles) { 245 if (pf.getName().equals("CreateBadClassFile.java")) { 246 pOpts.add("--add-modules=jdk.jdeps"); 247 pOpts.add("--add-exports=jdk.jdeps/com.sun.tools.classfile=ALL-UNNAMED"); 248 } 249 } 250 251 new Jsr199Compiler(verbose).run(null, null, false, pOpts, procFiles); 252 opts.add("-classpath"); // avoid using -processorpath for now 253 opts.add(classesDir.getPath()); 254 createAnnotationServicesFile(classesDir, procFiles); 255 } else if (options != null) { 256 int i = options.indexOf("-processor"); 257 // check for built-in anno-processor(s) 258 if (i != -1 && options.get(i + 1).equals("DocCommentProcessor")) { 259 opts.add("-classpath"); 260 opts.add(System.getProperty("test.classes")); 261 } 262 } 263 264 List<File> files = srcFiles; 265 266 if (srcPathDir != null) { 267 opts.add("-sourcepath"); 268 opts.add(srcPathDir.getPath()); 269 } 270 271 if (moduleSourcePathDir != null) { 272 opts.add("--module-source-path"); 273 opts.add(moduleSourcePathDir.getPath()); 274 files = new ArrayList<>(); 275 files.addAll(moduleSourcePathFiles); 276 files.addAll(nonEmptySrcFiles); // srcFiles containing declarations 277 } 278 279 if (patchModulePathDir != null) { 280 for (File mod : patchModulePathDir.listFiles()) { 281 opts.add("--patch-module"); 282 opts.add(mod.getName() + "=" + mod.getPath()); 283 } 284 files = new ArrayList<>(); 285 files.addAll(patchModulePathFiles); 286 files.addAll(nonEmptySrcFiles); // srcFiles containing declarations 287 } 288 289 if (additionalFiles.size() > 0) { 290 List<String> sOpts = Arrays.asList("-d", classesDir.getPath()); 291 new Jsr199Compiler(verbose).run(null, null, false, sOpts, additionalFiles); 292 } 293 294 try { 295 Compiler c = Compiler.getCompiler(runOpts, verbose); 296 c.run(out, keys, raw, opts, files); 297 } catch (IllegalArgumentException e) { 298 if (out != null) { 299 out.println("Invalid value for run tag: " + runOpts); 300 } 301 } 302 } 303 304 void createAnnotationServicesFile(File dir, List<File> procFiles) throws IOException { 305 File servicesDir = new File(new File(dir, "META-INF"), "services"); 306 servicesDir.mkdirs(); 307 File annoServices = new File(servicesDir, Processor.class.getName()); 308 Writer out = new FileWriter(annoServices); 309 try { 310 for (File f: procFiles) { 311 out.write(f.getName().toString().replace(".java", "")); 312 } 313 } finally { 314 out.close(); 315 } 316 } 317 318 @Override 319 public int compareTo(Example e) { 320 return file.compareTo(e.file); 321 } 322 323 @Override 324 public String toString() { 325 return file.getPath(); 326 } 327 328 /** 329 * Read the contents of a file. 330 */ 331 private String read(File f) throws IOException { 332 byte[] bytes = new byte[(int) f.length()]; 333 DataInputStream in = new DataInputStream(new FileInputStream(f)); 334 try { 335 in.readFully(bytes); 336 } finally { 337 in.close(); 338 } 339 return new String(bytes); 340 } 341 342 /** 343 * Clean the contents of a directory. 344 */ 345 boolean clean(File dir) { 346 boolean ok = true; 347 for (File f: dir.listFiles()) { 348 if (f.isDirectory()) 349 ok &= clean(f); 350 ok &= f.delete(); 351 } 352 return ok; 353 } 354 355 File file; 356 List<File> srcFiles; 357 List<File> procFiles; 358 File srcPathDir; 359 File moduleSourcePathDir; 360 File patchModulePathDir; 361 File additionalFilesDir; 362 List<File> srcPathFiles; 363 List<File> moduleSourcePathFiles; 364 List<File> patchModulePathFiles; 365 List<File> modulePathFiles; 366 List<File> classPathFiles; 367 List<File> additionalFiles; 368 List<File> nonEmptySrcFiles; 369 File infoFile; 370 private List<String> runOpts; 371 private List<String> options; 372 private Set<String> actualKeys; 373 private Set<String> declaredKeys; 374 375 static File tempDir = (System.getProperty("test.src") != null) ? 376 new File(System.getProperty("user.dir")): 377 new File(System.getProperty("java.io.tmpdir")); 378 379 static void setTempDir(File tempDir) { 380 Example.tempDir = tempDir; 381 } 382 383 abstract static class Compiler { 384 interface Factory { 385 Compiler getCompiler(List<String> opts, boolean verbose); 386 } 387 388 static class DefaultFactory implements Factory { 389 public Compiler getCompiler(List<String> opts, boolean verbose) { 390 String first; 391 String[] rest; 392 if (opts == null || opts.isEmpty()) { 393 first = null; 394 rest = new String[0]; 395 } else { 396 first = opts.get(0); 397 rest = opts.subList(1, opts.size()).toArray(new String[opts.size() - 1]); 398 } 399 // For more details on the different compilers, 400 // see their respective class doc comments. 401 // See also README.examples.txt in this directory. 402 if (first == null || first.equals("jsr199")) 403 return new Jsr199Compiler(verbose, rest); 404 else if (first.equals("simple")) 405 return new SimpleCompiler(verbose); 406 else if (first.equals("backdoor")) 407 return new BackdoorCompiler(verbose); 408 else if (first.equals("exec")) 409 return new ExecCompiler(verbose, rest); 410 else 411 throw new IllegalArgumentException(first); 412 } 413 } 414 415 static Factory factory; 416 417 static Compiler getCompiler(List<String> opts, boolean verbose) { 418 if (factory == null) 419 factory = new DefaultFactory(); 420 421 return factory.getCompiler(opts, verbose); 422 } 423 424 protected Compiler(boolean verbose) { 425 this.verbose = verbose; 426 } 427 428 abstract boolean run(PrintWriter out, Set<String> keys, boolean raw, 429 List<String> opts, List<File> files); 430 431 void setSupportClassLoader(ClassLoader cl) { 432 loader = cl; 433 } 434 435 protected void close(JavaFileManager fm) { 436 try { 437 fm.close(); 438 } catch (IOException e) { 439 throw new Error(e); 440 } 441 } 442 443 protected ClassLoader loader; 444 protected boolean verbose; 445 } 446 447 /** 448 * Compile using the JSR 199 API. The diagnostics generated are 449 * scanned for resource keys. Not all diagnostic keys are generated 450 * via the JSR 199 API -- for example, rich diagnostics are not directly 451 * accessible, and some diagnostics generated by the file manager may 452 * not be generated (for example, the JSR 199 file manager does not see 453 * -Xlint:path). 454 */ 455 static class Jsr199Compiler extends Compiler { 456 List<String> fmOpts; 457 458 Jsr199Compiler(boolean verbose, String... args) { 459 super(verbose); 460 for (int i = 0; i < args.length; i++) { 461 String arg = args[i]; 462 if (arg.equals("-filemanager") && (i + 1 < args.length)) { 463 fmOpts = Arrays.asList(args[++i].split(",")); 464 } else 465 throw new IllegalArgumentException(arg); 466 } 467 } 468 469 @Override 470 boolean run(PrintWriter out, Set<String> keys, boolean raw, List<String> opts, List<File> files) { 471 if (out != null && keys != null) 472 throw new IllegalArgumentException(); 473 474 if (verbose) 475 System.err.println("run_jsr199: " + opts + " " + files); 476 477 DiagnosticCollector<JavaFileObject> dc = null; 478 if (keys != null) 479 dc = new DiagnosticCollector<JavaFileObject>(); 480 481 if (raw) { 482 List<String> newOpts = new ArrayList<String>(); 483 newOpts.add("-XDrawDiagnostics"); 484 newOpts.addAll(opts); 485 opts = newOpts; 486 } 487 488 JavaCompiler c = ToolProvider.getSystemJavaCompiler(); 489 490 StandardJavaFileManager fm = c.getStandardFileManager(dc, null, null); 491 try { 492 if (fmOpts != null) 493 fm = new FileManager(fm, fmOpts); 494 495 Iterable<? extends JavaFileObject> fos = fm.getJavaFileObjectsFromFiles(files); 496 497 CompilationTask t = c.getTask(out, fm, dc, opts, null, fos); 498 Boolean ok = t.call(); 499 500 if (keys != null) { 501 for (Diagnostic<? extends JavaFileObject> d: dc.getDiagnostics()) { 502 scanForKeys(unwrap(d), keys); 503 } 504 } 505 506 return ok; 507 } finally { 508 close(fm); 509 } 510 } 511 512 /** 513 * Scan a diagnostic for resource keys. This will not detect additional 514 * sub diagnostics that might be generated by a rich diagnostic formatter. 515 */ 516 private static void scanForKeys(JCDiagnostic d, Set<String> keys) { 517 keys.add(d.getCode()); 518 for (Object o: d.getArgs()) { 519 if (o instanceof JCDiagnostic) { 520 scanForKeys((JCDiagnostic) o, keys); 521 } 522 } 523 for (JCDiagnostic sd: d.getSubdiagnostics()) 524 scanForKeys(sd, keys); 525 } 526 527 private JCDiagnostic unwrap(Diagnostic<? extends JavaFileObject> diagnostic) { 528 if (diagnostic instanceof JCDiagnostic) 529 return (JCDiagnostic) diagnostic; 530 if (diagnostic instanceof ClientCodeWrapper.DiagnosticSourceUnwrapper) 531 return ((ClientCodeWrapper.DiagnosticSourceUnwrapper)diagnostic).d; 532 throw new IllegalArgumentException(); 533 } 534 } 535 536 /** 537 * Run the test using the standard simple entry point. 538 */ 539 static class SimpleCompiler extends Compiler { 540 SimpleCompiler(boolean verbose) { 541 super(verbose); 542 } 543 544 @Override 545 boolean run(PrintWriter out, Set<String> keys, boolean raw, List<String> opts, List<File> files) { 546 if (out != null && keys != null) 547 throw new IllegalArgumentException(); 548 549 if (verbose) 550 System.err.println("run_simple: " + opts + " " + files); 551 552 List<String> args = new ArrayList<String>(); 553 554 if (keys != null || raw) 555 args.add("-XDrawDiagnostics"); 556 557 args.addAll(opts); 558 for (File f: files) 559 args.add(f.getPath()); 560 561 StringWriter sw = null; 562 PrintWriter pw; 563 if (keys != null) { 564 sw = new StringWriter(); 565 pw = new PrintWriter(sw); 566 } else 567 pw = out; 568 569 int rc = com.sun.tools.javac.Main.compile(args.toArray(new String[args.size()]), pw); 570 571 if (keys != null) { 572 pw.close(); 573 scanForKeys(sw.toString(), keys); 574 } 575 576 return (rc == 0); 577 } 578 579 private static void scanForKeys(String text, Set<String> keys) { 580 StringTokenizer st = new StringTokenizer(text, " ,\r\n():"); 581 while (st.hasMoreElements()) { 582 String t = st.nextToken(); 583 if (t.startsWith("compiler.")) 584 keys.add(t); 585 } 586 } 587 } 588 589 /** 590 * Run the test in a separate process. 591 */ 592 static class ExecCompiler extends Compiler { 593 List<String> vmOpts; 594 595 ExecCompiler(boolean verbose, String... args) { 596 super(verbose); 597 vmOpts = Arrays.asList(args); 598 } 599 600 @Override 601 boolean run(PrintWriter out, Set<String> keys, boolean raw, List<String> opts, List<File> files) { 602 if (out != null && keys != null) 603 throw new IllegalArgumentException(); 604 605 if (verbose) 606 System.err.println("run_exec: " + vmOpts + " " + opts + " " + files); 607 608 List<String> args = new ArrayList<String>(); 609 610 File javaHome = new File(System.getProperty("java.home")); 611 if (javaHome.getName().equals("jre")) 612 javaHome = javaHome.getParentFile(); 613 File javaExe = new File(new File(javaHome, "bin"), "java"); 614 args.add(javaExe.getPath()); 615 616 File toolsJar = new File(new File(javaHome, "lib"), "tools.jar"); 617 if (toolsJar.exists()) { 618 args.add("-classpath"); 619 args.add(toolsJar.getPath()); 620 } 621 622 args.addAll(vmOpts); 623 addOpts(args, "test.vm.opts"); 624 addOpts(args, "test.java.opts"); 625 args.add(com.sun.tools.javac.Main.class.getName()); 626 627 if (keys != null || raw) 628 args.add("-XDrawDiagnostics"); 629 630 args.addAll(opts); 631 for (File f: files) 632 args.add(f.getPath()); 633 634 try { 635 ProcessBuilder pb = new ProcessBuilder(args); 636 pb.redirectErrorStream(true); 637 Process p = pb.start(); 638 BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream())); 639 String line; 640 while ((line = in.readLine()) != null) { 641 if (keys != null) 642 scanForKeys(line, keys); 643 } 644 int rc = p.waitFor(); 645 646 return (rc == 0); 647 } catch (IOException | InterruptedException e) { 648 System.err.println("Exception execing javac" + e); 649 System.err.println("Command line: " + opts); 650 return false; 651 } 652 } 653 654 private static void scanForKeys(String text, Set<String> keys) { 655 StringTokenizer st = new StringTokenizer(text, " ,\r\n():"); 656 while (st.hasMoreElements()) { 657 String t = st.nextToken(); 658 if (t.startsWith("compiler.")) 659 keys.add(t); 660 } 661 } 662 663 private static void addOpts(List<String> args, String propName) { 664 String propValue = System.getProperty(propName); 665 if (propValue == null || propValue.isEmpty()) 666 return; 667 args.addAll(Arrays.asList(propValue.split(" +", 0))); 668 } 669 } 670 671 static class BackdoorCompiler extends Compiler { 672 BackdoorCompiler(boolean verbose) { 673 super(verbose); 674 } 675 676 @Override 677 boolean run(PrintWriter out, Set<String> keys, boolean raw, List<String> opts, List<File> files) { 678 if (out != null && keys != null) 679 throw new IllegalArgumentException(); 680 681 if (verbose) 682 System.err.println("run_simple: " + opts + " " + files); 683 684 List<String> args = new ArrayList<String>(); 685 686 if (out != null && raw) 687 args.add("-XDrawDiagnostics"); 688 689 args.addAll(opts); 690 for (File f: files) 691 args.add(f.getPath()); 692 693 StringWriter sw = null; 694 PrintWriter pw; 695 if (keys != null) { 696 sw = new StringWriter(); 697 pw = new PrintWriter(sw); 698 } else 699 pw = out; 700 701 Context c = new Context(); 702 JavacFileManager.preRegister(c); // can't create it until Log has been set up 703 MessageTracker.preRegister(c, keys); 704 705 try { 706 Main m = new Main("javac", pw); 707 Main.Result rc = m.compile(args.toArray(new String[args.size()]), c); 708 709 if (keys != null) { 710 pw.close(); 711 } 712 713 return rc.isOK(); 714 } finally { 715 close(c.get(JavaFileManager.class)); 716 } 717 } 718 719 static class MessageTracker extends JavacMessages { 720 721 MessageTracker(Context context) { 722 super(context); 723 } 724 725 static void preRegister(Context c, final Set<String> keys) { 726 if (keys != null) { 727 c.put(JavacMessages.messagesKey, new Context.Factory<JavacMessages>() { 728 public JavacMessages make(Context c) { 729 return new MessageTracker(c) { 730 @Override 731 public String getLocalizedString(Locale l, String key, Object... args) { 732 keys.add(key); 733 return super.getLocalizedString(l, key, args); 734 } 735 }; 736 } 737 }); 738 } 739 } 740 } 741 742 } 743} 744