1 /* 2 * Copyright (c) 2013, 2018, 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 24 package toolbox; 25 26 import java.io.BufferedWriter; 27 import java.io.ByteArrayOutputStream; 28 import java.io.FilterOutputStream; 29 import java.io.FilterWriter; 30 import java.io.IOException; 31 import java.io.OutputStream; 32 import java.io.PrintStream; 33 import java.io.StringWriter; 34 import java.io.Writer; 35 import java.net.URI; 36 import java.nio.charset.Charset; 37 import java.nio.file.FileVisitResult; 38 import java.nio.file.Files; 39 import java.nio.file.Path; 40 import java.nio.file.Paths; 41 import java.nio.file.SimpleFileVisitor; 42 import java.nio.file.StandardCopyOption; 43 import java.nio.file.attribute.BasicFileAttributes; 44 import java.util.ArrayList; 45 import java.util.Arrays; 46 import java.util.Collections; 47 import java.util.HashMap; 48 import java.util.List; 49 import java.util.Locale; 50 import java.util.Map; 51 import java.util.Objects; 52 import java.util.Set; 53 import java.util.TreeSet; 54 import java.util.regex.Matcher; 55 import java.util.regex.Pattern; 56 import java.util.stream.Collectors; 57 import java.util.stream.StreamSupport; 58 59 import javax.tools.FileObject; 60 import javax.tools.ForwardingJavaFileManager; 61 import javax.tools.JavaFileManager; 62 import javax.tools.JavaFileObject; 63 import javax.tools.JavaFileObject.Kind; 64 import javax.tools.JavaFileManager.Location; 65 import javax.tools.SimpleJavaFileObject; 66 import javax.tools.ToolProvider; 67 68 /** 69 * Utility methods and classes for writing jtreg tests for 70 * javac, javah, javap, and sjavac. (For javadoc support, 71 * see JavadocTester.) 72 * 73 * <p>There is support for common file operations similar to 74 * shell commands like cat, cp, diff, mv, rm, grep. 75 * 76 * <p>There is also support for invoking various tools, like 77 * javac, javah, javap, jar, java and other JDK tools. 78 * 79 * <p><em>File separators</em>: for convenience, many operations accept strings 80 * to represent filenames. On all platforms on which JDK is supported, 81 * "/" is a legal filename component separator. In particular, even 82 * on Windows, where the official file separator is "\", "/" is a legal 83 * alternative. It is therefore recommended that any client code using 84 * strings to specify filenames should use "/". 85 * 86 * @author Vicente Romero (original) 87 * @author Jonathan Gibbons (revised) 88 */ 89 public class ToolBox { 90 /** The platform line separator. */ 91 public static final String lineSeparator = System.getProperty("line.separator"); 92 /** The platform OS name. */ 93 public static final String osName = System.getProperty("os.name"); 94 95 /** The location of the class files for this test, or null if not set. */ 96 public static final String testClasses = System.getProperty("test.classes"); 97 /** The location of the source files for this test, or null if not set. */ 98 public static final String testSrc = System.getProperty("test.src"); 99 /** The location of the test JDK for this test, or null if not set. */ 100 public static final String testJDK = System.getProperty("test.jdk"); 101 102 /** The current directory. */ 103 public static final Path currDir = Paths.get("."); 104 105 /** The stream used for logging output. */ 106 public PrintStream out = System.err; 107 108 /** 109 * Checks if the host OS is some version of Windows. 110 * @return true if the host OS is some version of Windows 111 */ 112 public boolean isWindows() { 113 return osName.toLowerCase(Locale.ENGLISH).startsWith("windows"); 114 } 115 116 /** 117 * Splits a string around matches of the given regular expression. 118 * If the string is empty, an empty list will be returned. 119 * @param text the string to be split 120 * @param sep the delimiting regular expression 121 * @return the strings between the separators 122 */ 123 public List<String> split(String text, String sep) { 124 if (text.isEmpty()) 125 return Collections.emptyList(); 126 return Arrays.asList(text.split(sep)); 127 } 128 129 /** 130 * Checks if two lists of strings are equal. 131 * @param l1 the first list of strings to be compared 132 * @param l2 the second list of strings to be compared 133 * @throws Error if the lists are not equal 134 */ 135 public void checkEqual(List<String> l1, List<String> l2) throws Error { 136 if (!Objects.equals(l1, l2)) { 137 // l1 and l2 cannot both be null 138 if (l1 == null) 139 throw new Error("comparison failed: l1 is null"); 140 if (l2 == null) 141 throw new Error("comparison failed: l2 is null"); 142 // report first difference 143 for (int i = 0; i < Math.min(l1.size(), l2.size()); i++) { 144 String s1 = l1.get(i); 145 String s2 = l2.get(i); 146 if (!Objects.equals(s1, s2)) { 147 throw new Error("comparison failed, index " + i + 148 ", (" + s1 + ":" + s2 + ")"); 149 } 150 } 151 throw new Error("comparison failed: l1.size=" + l1.size() + ", l2.size=" + l2.size()); 152 } 153 } 154 155 /** 156 * Filters a list of strings according to the given regular expression. 157 * @param regex the regular expression 158 * @param lines the strings to be filtered 159 * @return the strings matching the regular expression 160 */ 161 public List<String> grep(String regex, List<String> lines) { 162 return grep(Pattern.compile(regex), lines); 163 } 164 165 /** 166 * Filters a list of strings according to the given regular expression. 167 * @param pattern the regular expression 168 * @param lines the strings to be filtered 169 * @return the strings matching the regular expression 170 */ 171 public List<String> grep(Pattern pattern, List<String> lines) { 172 return lines.stream() 173 .filter(s -> pattern.matcher(s).find()) 174 .collect(Collectors.toList()); 175 } 176 177 /** 178 * Copies a file. 179 * If the given destination exists and is a directory, the copy is created 180 * in that directory. Otherwise, the copy will be placed at the destination, 181 * possibly overwriting any existing file. 182 * <p>Similar to the shell "cp" command: {@code cp from to}. 183 * @param from the file to be copied 184 * @param to where to copy the file 185 * @throws IOException if any error occurred while copying the file 186 */ 187 public void copyFile(String from, String to) throws IOException { 188 copyFile(Paths.get(from), Paths.get(to)); 189 } 190 191 /** 192 * Copies a file. 193 * If the given destination exists and is a directory, the copy is created 194 * in that directory. Otherwise, the copy will be placed at the destination, 195 * possibly overwriting any existing file. 196 * <p>Similar to the shell "cp" command: {@code cp from to}. 197 * @param from the file to be copied 198 * @param to where to copy the file 199 * @throws IOException if an error occurred while copying the file 200 */ 201 public void copyFile(Path from, Path to) throws IOException { 202 if (Files.isDirectory(to)) { 203 to = to.resolve(from.getFileName()); 204 } else { 205 Files.createDirectories(to.getParent()); 206 } 207 Files.copy(from, to, StandardCopyOption.REPLACE_EXISTING); 208 } 209 210 /** 211 * Creates one of more directories. 212 * For each of the series of paths, a directory will be created, 213 * including any necessary parent directories. 214 * <p>Similar to the shell command: {@code mkdir -p paths}. 215 * @param paths the directories to be created 216 * @throws IOException if an error occurred while creating the directories 217 */ 218 public void createDirectories(String... paths) throws IOException { 219 if (paths.length == 0) 220 throw new IllegalArgumentException("no directories specified"); 221 for (String p : paths) 222 Files.createDirectories(Paths.get(p)); 223 } 224 225 /** 226 * Creates one or more directories. 227 * For each of the series of paths, a directory will be created, 228 * including any necessary parent directories. 229 * <p>Similar to the shell command: {@code mkdir -p paths}. 230 * @param paths the directories to be created 231 * @throws IOException if an error occurred while creating the directories 232 */ 233 public void createDirectories(Path... paths) throws IOException { 234 if (paths.length == 0) 235 throw new IllegalArgumentException("no directories specified"); 236 for (Path p : paths) 237 Files.createDirectories(p); 238 } 239 240 /** 241 * Deletes one or more files. 242 * Any directories to be deleted must be empty. 243 * <p>Similar to the shell command: {@code rm files}. 244 * @param files the files to be deleted 245 * @throws IOException if an error occurred while deleting the files 246 */ 247 public void deleteFiles(String... files) throws IOException { 248 if (files.length == 0) 249 throw new IllegalArgumentException("no files specified"); 250 for (String file : files) 251 Files.delete(Paths.get(file)); 252 } 253 254 /** 255 * Deletes all content of a directory (but not the directory itself). 256 * @param root the directory to be cleaned 257 * @throws IOException if an error occurs while cleaning the directory 258 */ 259 public void cleanDirectory(Path root) throws IOException { 260 if (!Files.isDirectory(root)) { 261 throw new IOException(root + " is not a directory"); 262 } 263 Files.walkFileTree(root, new SimpleFileVisitor<Path>() { 264 @Override 265 public FileVisitResult visitFile(Path file, BasicFileAttributes a) throws IOException { 266 Files.delete(file); 267 return FileVisitResult.CONTINUE; 268 } 269 270 @Override 271 public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException { 272 if (e != null) { 273 throw e; 274 } 275 if (!dir.equals(root)) { 276 Files.delete(dir); 277 } 278 return FileVisitResult.CONTINUE; 279 } 280 }); 281 } 282 283 /** 284 * Moves a file. 285 * If the given destination exists and is a directory, the file will be moved 286 * to that directory. Otherwise, the file will be moved to the destination, 287 * possibly overwriting any existing file. 288 * <p>Similar to the shell "mv" command: {@code mv from to}. 289 * @param from the file to be moved 290 * @param to where to move the file 291 * @throws IOException if an error occurred while moving the file 292 */ 293 public void moveFile(String from, String to) throws IOException { 294 moveFile(Paths.get(from), Paths.get(to)); 295 } 296 297 /** 298 * Moves a file. 299 * If the given destination exists and is a directory, the file will be moved 300 * to that directory. Otherwise, the file will be moved to the destination, 301 * possibly overwriting any existing file. 302 * <p>Similar to the shell "mv" command: {@code mv from to}. 303 * @param from the file to be moved 304 * @param to where to move the file 305 * @throws IOException if an error occurred while moving the file 306 */ 307 public void moveFile(Path from, Path to) throws IOException { 308 if (Files.isDirectory(to)) { 309 to = to.resolve(from.getFileName()); 310 } else { 311 Files.createDirectories(to.getParent()); 312 } 313 Files.move(from, to, StandardCopyOption.REPLACE_EXISTING); 314 } 315 316 /** 317 * Reads the lines of a file. 318 * The file is read using the default character encoding. 319 * @param path the file to be read 320 * @return the lines of the file 321 * @throws IOException if an error occurred while reading the file 322 */ 323 public List<String> readAllLines(String path) throws IOException { 324 return readAllLines(path, null); 325 } 326 327 /** 328 * Reads the lines of a file. 329 * The file is read using the default character encoding. 330 * @param path the file to be read 331 * @return the lines of the file 332 * @throws IOException if an error occurred while reading the file 333 */ 334 public List<String> readAllLines(Path path) throws IOException { 335 return readAllLines(path, null); 336 } 337 338 /** 339 * Reads the lines of a file using the given encoding. 340 * @param path the file to be read 341 * @param encoding the encoding to be used to read the file 342 * @return the lines of the file. 343 * @throws IOException if an error occurred while reading the file 344 */ 345 public List<String> readAllLines(String path, String encoding) throws IOException { 346 return readAllLines(Paths.get(path), encoding); 347 } 348 349 /** 350 * Reads the lines of a file using the given encoding. 351 * @param path the file to be read 352 * @param encoding the encoding to be used to read the file 353 * @return the lines of the file 354 * @throws IOException if an error occurred while reading the file 355 */ 356 public List<String> readAllLines(Path path, String encoding) throws IOException { 357 return Files.readAllLines(path, getCharset(encoding)); 358 } 359 360 private Charset getCharset(String encoding) { 361 return (encoding == null) ? Charset.defaultCharset() : Charset.forName(encoding); 362 } 363 364 /** 365 * Find .java files in one or more directories. 366 * <p>Similar to the shell "find" command: {@code find paths -name \*.java}. 367 * @param paths the directories in which to search for .java files 368 * @return the .java files found 369 * @throws IOException if an error occurred while searching for files 370 */ 371 public Path[] findJavaFiles(Path... paths) throws IOException { 372 return findFiles(".java", paths); 373 } 374 375 /** 376 * Find files matching the file extension, in one or more directories. 377 * <p>Similar to the shell "find" command: {@code find paths -name \*.ext}. 378 * @param fileExtension the extension to search for 379 * @param paths the directories in which to search for files 380 * @return the files matching the file extension 381 * @throws IOException if an error occurred while searching for files 382 */ 383 public Path[] findFiles(String fileExtension, Path... paths) throws IOException { 384 Set<Path> files = new TreeSet<>(); // use TreeSet to force a consistent order 385 for (Path p : paths) { 386 Files.walkFileTree(p, new SimpleFileVisitor<Path>() { 387 @Override 388 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) 389 throws IOException { 390 if (file.getFileName().toString().endsWith(fileExtension)) { 391 files.add(file); 392 } 393 return FileVisitResult.CONTINUE; 394 } 395 }); 396 } 397 return files.toArray(new Path[files.size()]); 398 } 399 400 /** 401 * Writes a file containing the given content. 402 * Any necessary directories for the file will be created. 403 * @param path where to write the file 404 * @param content the content for the file 405 * @throws IOException if an error occurred while writing the file 406 */ 407 public void writeFile(String path, String content) throws IOException { 408 writeFile(Paths.get(path), content); 409 } 410 411 /** 412 * Writes a file containing the given content. 413 * Any necessary directories for the file will be created. 414 * @param path where to write the file 415 * @param content the content for the file 416 * @throws IOException if an error occurred while writing the file 417 */ 418 public void writeFile(Path path, String content) throws IOException { 419 Path dir = path.getParent(); 420 if (dir != null) 421 Files.createDirectories(dir); 422 try (BufferedWriter w = Files.newBufferedWriter(path)) { 423 w.write(content); 424 } 425 } 426 427 /** 428 * Writes one or more files containing Java source code. 429 * For each file to be written, the filename will be inferred from the 430 * given base directory, the package declaration (if present) and from the 431 * the name of the first class, interface or enum declared in the file. 432 * <p>For example, if the base directory is /my/dir/ and the content 433 * contains "package p; class C { }", the file will be written to 434 * /my/dir/p/C.java. 435 * <p>Note: the content is analyzed using regular expressions; 436 * errors can occur if any contents have initial comments that might trip 437 * up the analysis. 438 * @param dir the base directory 439 * @param contents the contents of the files to be written 440 * @throws IOException if an error occurred while writing any of the files. 441 */ 442 public void writeJavaFiles(Path dir, String... contents) throws IOException { 443 if (contents.length == 0) 444 throw new IllegalArgumentException("no content specified for any files"); 445 for (String c : contents) { 446 new JavaSource(c).write(dir); 447 } 448 } 449 450 /** 451 * Returns the path for the binary of a JDK tool within {@link testJDK}. 452 * @param tool the name of the tool 453 * @return the path of the tool 454 */ 455 public Path getJDKTool(String tool) { 456 return Paths.get(testJDK, "bin", tool); 457 } 458 459 /** 460 * Returns a string representing the contents of an {@code Iterable} as a list. 461 * @param <T> the type parameter of the {@code Iterable} 462 * @param items the iterable 463 * @return the string 464 */ 465 <T> String toString(Iterable<T> items) { 466 return StreamSupport.stream(items.spliterator(), false) 467 .map(Objects::toString) 468 .collect(Collectors.joining(",", "[", "]")); 469 } 470 471 472 /** 473 * An in-memory Java source file. 474 * It is able to extract the file name from simple source text using 475 * regular expressions. 476 */ 477 public static class JavaSource extends SimpleJavaFileObject { 478 private final String source; 479 480 /** 481 * Creates a in-memory file object for Java source code. 482 * @param className the name of the class 483 * @param source the source text 484 */ 485 public JavaSource(String className, String source) { 486 super(URI.create(className), JavaFileObject.Kind.SOURCE); 487 this.source = source; 488 } 489 490 /** 491 * Creates a in-memory file object for Java source code. 492 * The name of the class will be inferred from the source code. 493 * @param source the source text 494 */ 495 public JavaSource(String source) { 496 super(URI.create(getJavaFileNameFromSource(source)), 497 JavaFileObject.Kind.SOURCE); 498 this.source = source; 499 } 500 501 /** 502 * Writes the source code to a file in the current directory. 503 * @throws IOException if there is a problem writing the file 504 */ 505 public void write() throws IOException { 506 write(currDir); 507 } 508 509 /** 510 * Writes the source code to a file in a specified directory. 511 * @param dir the directory 512 * @throws IOException if there is a problem writing the file 513 */ 514 public void write(Path dir) throws IOException { 515 Path file = dir.resolve(getJavaFileNameFromSource(source)); 516 Files.createDirectories(file.getParent()); 517 try (BufferedWriter out = Files.newBufferedWriter(file)) { 518 out.write(source.replace("\n", lineSeparator)); 519 } 520 } 521 522 @Override 523 public CharSequence getCharContent(boolean ignoreEncodingErrors) { 524 return source; 525 } 526 527 private static Pattern commentPattern = 528 Pattern.compile("(?s)(\\s+//.*?\n|/\\*.*?\\*/)"); 529 private static Pattern modulePattern = 530 Pattern.compile("module\\s+((?:\\w+\\.)*)"); 531 private static Pattern packagePattern = 532 Pattern.compile("package\\s+(((?:\\w+\\.)*)(?:\\w+))"); 533 private static Pattern classPattern = 534 Pattern.compile("(?:public\\s+)?(?:class|enum|interface)\\s+(\\w+)"); 535 536 /** 537 * Extracts the Java file name from the class declaration. 538 * This method is intended for simple files and uses regular expressions. 539 * Comments in the source are stripped before looking for the 540 * declarations from which the name is derived. 541 */ 542 static String getJavaFileNameFromSource(String source) { 543 StringBuilder sb = new StringBuilder(); 544 Matcher matcher = commentPattern.matcher(source); 545 int start = 0; 546 while (matcher.find()) { 547 sb.append(source.substring(start, matcher.start())); 548 start = matcher.end(); 549 } 550 sb.append(source.substring(start)); 551 source = sb.toString(); 552 553 String packageName = null; 554 555 matcher = modulePattern.matcher(source); 556 if (matcher.find()) 557 return "module-info.java"; 558 559 matcher = packagePattern.matcher(source); 560 if (matcher.find()) 561 packageName = matcher.group(1).replace(".", "/"); 562 563 matcher = classPattern.matcher(source); 564 if (matcher.find()) { 565 String className = matcher.group(1) + ".java"; 566 return (packageName == null) ? className : packageName + "/" + className; 567 } else if (packageName != null) { 568 return packageName + "/package-info.java"; 569 } else { 570 throw new Error("Could not extract the java class " + 571 "name from the provided source"); 572 } 573 } 574 } 575 576 /** 577 * Extracts the Java file name from the class declaration. 578 * This method is intended for simple files and uses regular expressions, 579 * so comments matching the pattern can make the method fail. 580 * @deprecated This is a legacy method for compatibility with ToolBox v1. 581 * Use {@link JavaSource#getName JavaSource.getName} instead. 582 * @param source the source text 583 * @return the Java file name inferred from the source 584 */ 585 @Deprecated 586 public static String getJavaFileNameFromSource(String source) { 587 return JavaSource.getJavaFileNameFromSource(source); 588 } 589 590 /** 591 * A memory file manager, for saving generated files in memory. 592 * The file manager delegates to a separate file manager for listing and 593 * reading input files. 594 */ 595 public static class MemoryFileManager extends ForwardingJavaFileManager { 596 private interface Content { 597 byte[] getBytes(); 598 String getString(); 599 } 600 601 /** 602 * Maps binary class names to generated content. 603 */ 604 private final Map<Location, Map<String, Content>> files; 605 606 /** 607 * Construct a memory file manager which stores output files in memory, 608 * and delegates to a default file manager for input files. 609 */ 610 public MemoryFileManager() { 611 this(ToolProvider.getSystemJavaCompiler().getStandardFileManager(null, null, null)); 612 } 613 614 /** 615 * Construct a memory file manager which stores output files in memory, 616 * and delegates to a specified file manager for input files. 617 * @param fileManager the file manager to be used for input files 618 */ 619 public MemoryFileManager(JavaFileManager fileManager) { 620 super(fileManager); 621 files = new HashMap<>(); 622 } 623 624 @Override 625 public JavaFileObject getJavaFileForOutput(Location location, 626 String name, 627 JavaFileObject.Kind kind, 628 FileObject sibling) 629 { 630 return new MemoryFileObject(location, name, kind); 631 } 632 633 /** 634 * Returns the set of names of files that have been written to a given 635 * location. 636 * @param location the location 637 * @return the set of file names 638 */ 639 public Set<String> getFileNames(Location location) { 640 Map<String, Content> filesForLocation = files.get(location); 641 return (filesForLocation == null) 642 ? Collections.emptySet() : filesForLocation.keySet(); 643 } 644 645 /** 646 * Returns the content written to a file in a given location, 647 * or null if no such file has been written. 648 * @param location the location 649 * @param name the name of the file 650 * @return the content as an array of bytes 651 */ 652 public byte[] getFileBytes(Location location, String name) { 653 Content content = getFile(location, name); 654 return (content == null) ? null : content.getBytes(); 655 } 656 657 /** 658 * Returns the content written to a file in a given location, 659 * or null if no such file has been written. 660 * @param location the location 661 * @param name the name of the file 662 * @return the content as a string 663 */ 664 public String getFileString(Location location, String name) { 665 Content content = getFile(location, name); 666 return (content == null) ? null : content.getString(); 667 } 668 669 private Content getFile(Location location, String name) { 670 Map<String, Content> filesForLocation = files.get(location); 671 return (filesForLocation == null) ? null : filesForLocation.get(name); 672 } 673 674 private void save(Location location, String name, Content content) { 675 Map<String, Content> filesForLocation = files.get(location); 676 if (filesForLocation == null) 677 files.put(location, filesForLocation = new HashMap<>()); 678 filesForLocation.put(name, content); 679 } 680 681 /** 682 * A writable file object stored in memory. 683 */ 684 private class MemoryFileObject extends SimpleJavaFileObject { 685 private final Location location; 686 private final String name; 687 688 /** 689 * Constructs a memory file object. 690 * @param name binary name of the class to be stored in this file object 691 */ 692 MemoryFileObject(Location location, String name, JavaFileObject.Kind kind) { 693 super(URI.create("mfm:///" + name.replace('.','/') + kind.extension), 694 Kind.CLASS); 695 this.location = location; 696 this.name = name; 697 } 698 699 @Override 700 public OutputStream openOutputStream() { 701 return new FilterOutputStream(new ByteArrayOutputStream()) { 702 @Override 703 public void close() throws IOException { 704 out.close(); 705 byte[] bytes = ((ByteArrayOutputStream) out).toByteArray(); 706 save(location, name, new Content() { 707 @Override 708 public byte[] getBytes() { 709 return bytes; 710 } 711 @Override 712 public String getString() { 713 return new String(bytes); 714 } 715 716 }); 717 } 718 }; 719 } 720 721 @Override 722 public Writer openWriter() { 723 return new FilterWriter(new StringWriter()) { 724 @Override 725 public void close() throws IOException { 726 out.close(); 727 String text = ((StringWriter) out).toString(); 728 save(location, name, new Content() { 729 @Override 730 public byte[] getBytes() { 731 return text.getBytes(); 732 } 733 @Override 734 public String getString() { 735 return text; 736 } 737 738 }); 739 } 740 }; 741 } 742 } 743 } 744 } 745