001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2018 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018//////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle.ant; 021 022import java.io.File; 023import java.io.IOException; 024import java.io.InputStream; 025import java.io.OutputStream; 026import java.nio.file.Files; 027import java.util.ArrayList; 028import java.util.Arrays; 029import java.util.List; 030import java.util.Locale; 031import java.util.Map; 032import java.util.Properties; 033import java.util.ResourceBundle; 034import java.util.stream.Collectors; 035 036import org.apache.tools.ant.AntClassLoader; 037import org.apache.tools.ant.BuildException; 038import org.apache.tools.ant.DirectoryScanner; 039import org.apache.tools.ant.Project; 040import org.apache.tools.ant.Task; 041import org.apache.tools.ant.taskdefs.LogOutputStream; 042import org.apache.tools.ant.types.EnumeratedAttribute; 043import org.apache.tools.ant.types.FileSet; 044import org.apache.tools.ant.types.Path; 045import org.apache.tools.ant.types.Reference; 046 047import com.puppycrawl.tools.checkstyle.Checker; 048import com.puppycrawl.tools.checkstyle.ConfigurationLoader; 049import com.puppycrawl.tools.checkstyle.DefaultLogger; 050import com.puppycrawl.tools.checkstyle.ModuleFactory; 051import com.puppycrawl.tools.checkstyle.PackageObjectFactory; 052import com.puppycrawl.tools.checkstyle.PropertiesExpander; 053import com.puppycrawl.tools.checkstyle.ThreadModeSettings; 054import com.puppycrawl.tools.checkstyle.XMLLogger; 055import com.puppycrawl.tools.checkstyle.api.AuditListener; 056import com.puppycrawl.tools.checkstyle.api.AutomaticBean; 057import com.puppycrawl.tools.checkstyle.api.CheckstyleException; 058import com.puppycrawl.tools.checkstyle.api.Configuration; 059import com.puppycrawl.tools.checkstyle.api.RootModule; 060import com.puppycrawl.tools.checkstyle.api.SeverityLevel; 061import com.puppycrawl.tools.checkstyle.api.SeverityLevelCounter; 062 063/** 064 * An implementation of a ANT task for calling checkstyle. See the documentation 065 * of the task for usage. 066 * @noinspection ClassLoaderInstantiation 067 */ 068public class CheckstyleAntTask extends Task { 069 070 /** Poor man's enum for an xml formatter. */ 071 private static final String E_XML = "xml"; 072 /** Poor man's enum for an plain formatter. */ 073 private static final String E_PLAIN = "plain"; 074 075 /** Suffix for time string. */ 076 private static final String TIME_SUFFIX = " ms."; 077 078 /** Contains the paths to process. */ 079 private final List<Path> paths = new ArrayList<>(); 080 081 /** Contains the filesets to process. */ 082 private final List<FileSet> fileSets = new ArrayList<>(); 083 084 /** Contains the formatters to log to. */ 085 private final List<Formatter> formatters = new ArrayList<>(); 086 087 /** Contains the Properties to override. */ 088 private final List<Property> overrideProps = new ArrayList<>(); 089 090 /** Class path to locate class files. */ 091 private Path classpath; 092 093 /** Name of file to check. */ 094 private String fileName; 095 096 /** Config file containing configuration. */ 097 private String config; 098 099 /** Whether to fail build on violations. */ 100 private boolean failOnViolation = true; 101 102 /** Property to set on violations. */ 103 private String failureProperty; 104 105 /** The name of the properties file. */ 106 private File properties; 107 108 /** The maximum number of errors that are tolerated. */ 109 private int maxErrors; 110 111 /** The maximum number of warnings that are tolerated. */ 112 private int maxWarnings = Integer.MAX_VALUE; 113 114 /** 115 * Whether to execute ignored modules - some modules may log above 116 * their severity depending on their configuration (e.g. WriteTag) so 117 * need to be included 118 */ 119 private boolean executeIgnoredModules; 120 121 //////////////////////////////////////////////////////////////////////////// 122 // Setters for ANT specific attributes 123 //////////////////////////////////////////////////////////////////////////// 124 125 /** 126 * Tells this task to write failure message to the named property when there 127 * is a violation. 128 * @param propertyName the name of the property to set 129 * in the event of an failure. 130 */ 131 public void setFailureProperty(String propertyName) { 132 failureProperty = propertyName; 133 } 134 135 /** 136 * Sets flag - whether to fail if a violation is found. 137 * @param fail whether to fail if a violation is found 138 */ 139 public void setFailOnViolation(boolean fail) { 140 failOnViolation = fail; 141 } 142 143 /** 144 * Sets the maximum number of errors allowed. Default is 0. 145 * @param maxErrors the maximum number of errors allowed. 146 */ 147 public void setMaxErrors(int maxErrors) { 148 this.maxErrors = maxErrors; 149 } 150 151 /** 152 * Sets the maximum number of warnings allowed. Default is 153 * {@link Integer#MAX_VALUE}. 154 * @param maxWarnings the maximum number of warnings allowed. 155 */ 156 public void setMaxWarnings(int maxWarnings) { 157 this.maxWarnings = maxWarnings; 158 } 159 160 /** 161 * Adds a path. 162 * @param path the path to add. 163 */ 164 public void addPath(Path path) { 165 paths.add(path); 166 } 167 168 /** 169 * Adds set of files (nested fileset attribute). 170 * @param fileSet the file set to add 171 */ 172 public void addFileset(FileSet fileSet) { 173 fileSets.add(fileSet); 174 } 175 176 /** 177 * Add a formatter. 178 * @param formatter the formatter to add for logging. 179 */ 180 public void addFormatter(Formatter formatter) { 181 formatters.add(formatter); 182 } 183 184 /** 185 * Add an override property. 186 * @param property the property to add 187 */ 188 public void addProperty(Property property) { 189 overrideProps.add(property); 190 } 191 192 /** 193 * Set the class path. 194 * @param classpath the path to locate classes 195 */ 196 public void setClasspath(Path classpath) { 197 if (this.classpath == null) { 198 this.classpath = classpath; 199 } 200 else { 201 this.classpath.append(classpath); 202 } 203 } 204 205 /** 206 * Set the class path from a reference defined elsewhere. 207 * @param classpathRef the reference to an instance defining the classpath 208 */ 209 public void setClasspathRef(Reference classpathRef) { 210 createClasspath().setRefid(classpathRef); 211 } 212 213 /** 214 * Creates classpath. 215 * @return a created path for locating classes 216 */ 217 public Path createClasspath() { 218 if (classpath == null) { 219 classpath = new Path(getProject()); 220 } 221 return classpath.createPath(); 222 } 223 224 /** 225 * Sets file to be checked. 226 * @param file the file to be checked 227 */ 228 public void setFile(File file) { 229 fileName = file.getAbsolutePath(); 230 } 231 232 /** 233 * Sets configuration file. 234 * @param configuration the configuration file, URL, or resource to use 235 */ 236 public void setConfig(String configuration) { 237 if (config != null) { 238 throw new BuildException("Attribute 'config' has already been set"); 239 } 240 config = configuration; 241 } 242 243 /** 244 * Sets flag - whether to execute ignored modules. 245 * @param omit whether to execute ignored modules 246 */ 247 public void setExecuteIgnoredModules(boolean omit) { 248 executeIgnoredModules = omit; 249 } 250 251 //////////////////////////////////////////////////////////////////////////// 252 // Setters for Root Module's configuration attributes 253 //////////////////////////////////////////////////////////////////////////// 254 255 /** 256 * Sets a properties file for use instead 257 * of individually setting them. 258 * @param props the properties File to use 259 */ 260 public void setProperties(File props) { 261 properties = props; 262 } 263 264 //////////////////////////////////////////////////////////////////////////// 265 // The doers 266 //////////////////////////////////////////////////////////////////////////// 267 268 @Override 269 public void execute() { 270 final long startTime = System.currentTimeMillis(); 271 272 try { 273 // output version info in debug mode 274 final ResourceBundle compilationProperties = ResourceBundle 275 .getBundle("checkstylecompilation", Locale.ROOT); 276 final String version = compilationProperties 277 .getString("checkstyle.compile.version"); 278 final String compileTimestamp = compilationProperties 279 .getString("checkstyle.compile.timestamp"); 280 log("checkstyle version " + version, Project.MSG_VERBOSE); 281 log("compiled on " + compileTimestamp, Project.MSG_VERBOSE); 282 283 // Check for no arguments 284 if (fileName == null 285 && fileSets.isEmpty() 286 && paths.isEmpty()) { 287 throw new BuildException( 288 "Must specify at least one of 'file' or nested 'fileset' or 'path'.", 289 getLocation()); 290 } 291 if (config == null) { 292 throw new BuildException("Must specify 'config'.", getLocation()); 293 } 294 realExecute(version); 295 } 296 finally { 297 final long endTime = System.currentTimeMillis(); 298 log("Total execution took " + (endTime - startTime) + TIME_SUFFIX, 299 Project.MSG_VERBOSE); 300 } 301 } 302 303 /** 304 * Helper implementation to perform execution. 305 * @param checkstyleVersion Checkstyle compile version. 306 */ 307 private void realExecute(String checkstyleVersion) { 308 // Create the root module 309 RootModule rootModule = null; 310 try { 311 rootModule = createRootModule(); 312 313 // setup the listeners 314 final AuditListener[] listeners = getListeners(); 315 for (AuditListener element : listeners) { 316 rootModule.addListener(element); 317 } 318 final SeverityLevelCounter warningCounter = 319 new SeverityLevelCounter(SeverityLevel.WARNING); 320 rootModule.addListener(warningCounter); 321 322 processFiles(rootModule, warningCounter, checkstyleVersion); 323 } 324 finally { 325 if (rootModule != null) { 326 rootModule.destroy(); 327 } 328 } 329 } 330 331 /** 332 * Scans and processes files by means given root module. 333 * @param rootModule Root module to process files 334 * @param warningCounter Root Module's counter of warnings 335 * @param checkstyleVersion Checkstyle compile version 336 */ 337 private void processFiles(RootModule rootModule, final SeverityLevelCounter warningCounter, 338 final String checkstyleVersion) { 339 final long startTime = System.currentTimeMillis(); 340 final List<File> files = getFilesToCheck(); 341 final long endTime = System.currentTimeMillis(); 342 log("To locate the files took " + (endTime - startTime) + TIME_SUFFIX, 343 Project.MSG_VERBOSE); 344 345 log("Running Checkstyle " + checkstyleVersion + " on " + files.size() 346 + " files", Project.MSG_INFO); 347 log("Using configuration " + config, Project.MSG_VERBOSE); 348 349 final int numErrs; 350 351 try { 352 final long processingStartTime = System.currentTimeMillis(); 353 numErrs = rootModule.process(files); 354 final long processingEndTime = System.currentTimeMillis(); 355 log("To process the files took " + (processingEndTime - processingStartTime) 356 + TIME_SUFFIX, Project.MSG_VERBOSE); 357 } 358 catch (CheckstyleException ex) { 359 throw new BuildException("Unable to process files: " + files, ex); 360 } 361 final int numWarnings = warningCounter.getCount(); 362 final boolean okStatus = numErrs <= maxErrors && numWarnings <= maxWarnings; 363 364 // Handle the return status 365 if (!okStatus) { 366 final String failureMsg = 367 "Got " + numErrs + " errors and " + numWarnings 368 + " warnings."; 369 if (failureProperty != null) { 370 getProject().setProperty(failureProperty, failureMsg); 371 } 372 373 if (failOnViolation) { 374 throw new BuildException(failureMsg, getLocation()); 375 } 376 } 377 } 378 379 /** 380 * Creates new instance of the root module. 381 * @return new instance of the root module 382 */ 383 private RootModule createRootModule() { 384 final RootModule rootModule; 385 try { 386 final Properties props = createOverridingProperties(); 387 final ThreadModeSettings threadModeSettings = 388 ThreadModeSettings.SINGLE_THREAD_MODE_INSTANCE; 389 final ConfigurationLoader.IgnoredModulesOptions ignoredModulesOptions; 390 if (executeIgnoredModules) { 391 ignoredModulesOptions = ConfigurationLoader.IgnoredModulesOptions.EXECUTE; 392 } 393 else { 394 ignoredModulesOptions = ConfigurationLoader.IgnoredModulesOptions.OMIT; 395 } 396 397 final Configuration configuration = ConfigurationLoader.loadConfiguration(config, 398 new PropertiesExpander(props), ignoredModulesOptions, threadModeSettings); 399 400 final ClassLoader moduleClassLoader = 401 Checker.class.getClassLoader(); 402 403 final ModuleFactory factory = new PackageObjectFactory( 404 Checker.class.getPackage().getName() + ".", moduleClassLoader); 405 406 rootModule = (RootModule) factory.createModule(configuration.getName()); 407 rootModule.setModuleClassLoader(moduleClassLoader); 408 409 if (rootModule instanceof Checker) { 410 final ClassLoader loader = new AntClassLoader(getProject(), 411 classpath); 412 413 ((Checker) rootModule).setClassLoader(loader); 414 } 415 416 rootModule.configure(configuration); 417 } 418 catch (final CheckstyleException ex) { 419 throw new BuildException(String.format(Locale.ROOT, "Unable to create Root Module: " 420 + "config {%s}, classpath {%s}.", config, classpath), ex); 421 } 422 return rootModule; 423 } 424 425 /** 426 * Create the Properties object based on the arguments specified 427 * to the ANT task. 428 * @return the properties for property expansion expansion 429 * @throws BuildException if an error occurs 430 */ 431 private Properties createOverridingProperties() { 432 final Properties returnValue = new Properties(); 433 434 // Load the properties file if specified 435 if (properties != null) { 436 try (InputStream inStream = Files.newInputStream(properties.toPath())) { 437 returnValue.load(inStream); 438 } 439 catch (final IOException ex) { 440 throw new BuildException("Error loading Properties file '" 441 + properties + "'", ex, getLocation()); 442 } 443 } 444 445 // override with Ant properties like ${basedir} 446 final Map<String, Object> antProps = getProject().getProperties(); 447 for (Map.Entry<String, Object> entry : antProps.entrySet()) { 448 final String value = String.valueOf(entry.getValue()); 449 returnValue.setProperty(entry.getKey(), value); 450 } 451 452 // override with properties specified in subelements 453 for (Property p : overrideProps) { 454 returnValue.setProperty(p.getKey(), p.getValue()); 455 } 456 457 return returnValue; 458 } 459 460 /** 461 * Return the list of listeners set in this task. 462 * @return the list of listeners. 463 */ 464 private AuditListener[] getListeners() { 465 final int formatterCount = Math.max(1, formatters.size()); 466 467 final AuditListener[] listeners = new AuditListener[formatterCount]; 468 469 // formatters 470 try { 471 if (formatters.isEmpty()) { 472 final OutputStream debug = new LogOutputStream(this, Project.MSG_DEBUG); 473 final OutputStream err = new LogOutputStream(this, Project.MSG_ERR); 474 listeners[0] = new DefaultLogger(debug, AutomaticBean.OutputStreamOptions.CLOSE, 475 err, AutomaticBean.OutputStreamOptions.CLOSE); 476 } 477 else { 478 for (int i = 0; i < formatterCount; i++) { 479 final Formatter formatter = formatters.get(i); 480 listeners[i] = formatter.createListener(this); 481 } 482 } 483 } 484 catch (IOException ex) { 485 throw new BuildException(String.format(Locale.ROOT, "Unable to create listeners: " 486 + "formatters {%s}.", formatters), ex); 487 } 488 return listeners; 489 } 490 491 /** 492 * Returns the list of files (full path name) to process. 493 * @return the list of files included via the fileName, filesets and paths. 494 */ 495 private List<File> getFilesToCheck() { 496 final List<File> allFiles = new ArrayList<>(); 497 if (fileName != null) { 498 // oops we've got an additional one to process, don't 499 // forget it. No sweat, it's fully resolved via the setter. 500 log("Adding standalone file for audit", Project.MSG_VERBOSE); 501 allFiles.add(new File(fileName)); 502 } 503 504 final List<File> filesFromFileSets = scanFileSets(); 505 allFiles.addAll(filesFromFileSets); 506 507 final List<File> filesFromPaths = scanPaths(); 508 allFiles.addAll(filesFromPaths); 509 510 return allFiles; 511 } 512 513 /** 514 * Retrieves all files from the defined paths. 515 * @return a list of files defined via paths. 516 */ 517 private List<File> scanPaths() { 518 final List<File> allFiles = new ArrayList<>(); 519 520 for (int i = 0; i < paths.size(); i++) { 521 final Path currentPath = paths.get(i); 522 final List<File> pathFiles = scanPath(currentPath, i + 1); 523 allFiles.addAll(pathFiles); 524 } 525 526 return allFiles; 527 } 528 529 /** 530 * Scans the given path and retrieves all files for the given path. 531 * 532 * @param path A path to scan. 533 * @param pathIndex The index of the given path. Used in log messages only. 534 * @return A list of files, extracted from the given path. 535 */ 536 private List<File> scanPath(Path path, int pathIndex) { 537 final String[] resources = path.list(); 538 log(pathIndex + ") Scanning path " + path, Project.MSG_VERBOSE); 539 final List<File> allFiles = new ArrayList<>(); 540 int concreteFilesCount = 0; 541 542 for (String resource : resources) { 543 final File file = new File(resource); 544 if (file.isFile()) { 545 concreteFilesCount++; 546 allFiles.add(file); 547 } 548 else { 549 final DirectoryScanner scanner = new DirectoryScanner(); 550 scanner.setBasedir(file); 551 scanner.scan(); 552 final List<File> scannedFiles = retrieveAllScannedFiles(scanner, pathIndex); 553 allFiles.addAll(scannedFiles); 554 } 555 } 556 557 if (concreteFilesCount > 0) { 558 log(String.format(Locale.ROOT, "%d) Adding %d files from path %s", 559 pathIndex, concreteFilesCount, path), Project.MSG_VERBOSE); 560 } 561 562 return allFiles; 563 } 564 565 /** 566 * Returns the list of files (full path name) to process. 567 * @return the list of files included via the filesets. 568 */ 569 protected List<File> scanFileSets() { 570 final List<File> allFiles = new ArrayList<>(); 571 572 for (int i = 0; i < fileSets.size(); i++) { 573 final FileSet fileSet = fileSets.get(i); 574 final DirectoryScanner scanner = fileSet.getDirectoryScanner(getProject()); 575 final List<File> scannedFiles = retrieveAllScannedFiles(scanner, i); 576 allFiles.addAll(scannedFiles); 577 } 578 579 return allFiles; 580 } 581 582 /** 583 * Retrieves all matched files from the given scanner. 584 * 585 * @param scanner A directory scanner. Note, that {@link DirectoryScanner#scan()} 586 * must be called before calling this method. 587 * @param logIndex A log entry index. Used only for log messages. 588 * @return A list of files, retrieved from the given scanner. 589 */ 590 private List<File> retrieveAllScannedFiles(DirectoryScanner scanner, int logIndex) { 591 final String[] fileNames = scanner.getIncludedFiles(); 592 log(String.format(Locale.ROOT, "%d) Adding %d files from directory %s", 593 logIndex, fileNames.length, scanner.getBasedir()), Project.MSG_VERBOSE); 594 595 return Arrays.stream(fileNames) 596 .map(name -> scanner.getBasedir() + File.separator + name) 597 .map(File::new) 598 .collect(Collectors.toList()); 599 } 600 601 /** 602 * Poor mans enumeration for the formatter types. 603 */ 604 public static class FormatterType extends EnumeratedAttribute { 605 606 /** My possible values. */ 607 private static final String[] VALUES = {E_XML, E_PLAIN}; 608 609 @Override 610 public String[] getValues() { 611 return VALUES.clone(); 612 } 613 614 } 615 616 /** 617 * Details about a formatter to be used. 618 */ 619 public static class Formatter { 620 621 /** The formatter type. */ 622 private FormatterType type; 623 /** The file to output to. */ 624 private File toFile; 625 /** Whether or not the write to the named file. */ 626 private boolean useFile = true; 627 628 /** 629 * Set the type of the formatter. 630 * @param type the type 631 */ 632 public void setType(FormatterType type) { 633 this.type = type; 634 } 635 636 /** 637 * Set the file to output to. 638 * @param destination destination the file to output to 639 */ 640 public void setTofile(File destination) { 641 toFile = destination; 642 } 643 644 /** 645 * Sets whether or not we write to a file if it is provided. 646 * @param use whether not not to use provided file. 647 */ 648 public void setUseFile(boolean use) { 649 useFile = use; 650 } 651 652 /** 653 * Creates a listener for the formatter. 654 * @param task the task running 655 * @return a listener 656 * @throws IOException if an error occurs 657 */ 658 public AuditListener createListener(Task task) throws IOException { 659 final AuditListener listener; 660 if (type != null 661 && E_XML.equals(type.getValue())) { 662 listener = createXmlLogger(task); 663 } 664 else { 665 listener = createDefaultLogger(task); 666 } 667 return listener; 668 } 669 670 /** 671 * Creates default logger. 672 * @param task the task to possibly log to 673 * @return a DefaultLogger instance 674 * @throws IOException if an error occurs 675 */ 676 private AuditListener createDefaultLogger(Task task) 677 throws IOException { 678 final AuditListener defaultLogger; 679 if (toFile == null || !useFile) { 680 defaultLogger = new DefaultLogger( 681 new LogOutputStream(task, Project.MSG_DEBUG), 682 AutomaticBean.OutputStreamOptions.CLOSE, 683 new LogOutputStream(task, Project.MSG_ERR), 684 AutomaticBean.OutputStreamOptions.CLOSE 685 ); 686 } 687 else { 688 final OutputStream infoStream = Files.newOutputStream(toFile.toPath()); 689 defaultLogger = 690 new DefaultLogger(infoStream, AutomaticBean.OutputStreamOptions.CLOSE, 691 infoStream, AutomaticBean.OutputStreamOptions.NONE); 692 } 693 return defaultLogger; 694 } 695 696 /** 697 * Creates XML logger. 698 * @param task the task to possibly log to 699 * @return an XMLLogger instance 700 * @throws IOException if an error occurs 701 */ 702 private AuditListener createXmlLogger(Task task) throws IOException { 703 final AuditListener xmlLogger; 704 if (toFile == null || !useFile) { 705 xmlLogger = new XMLLogger(new LogOutputStream(task, Project.MSG_INFO), 706 AutomaticBean.OutputStreamOptions.CLOSE); 707 } 708 else { 709 xmlLogger = new XMLLogger(Files.newOutputStream(toFile.toPath()), 710 AutomaticBean.OutputStreamOptions.CLOSE); 711 } 712 return xmlLogger; 713 } 714 715 } 716 717 /** 718 * Represents a property that consists of a key and value. 719 */ 720 public static class Property { 721 722 /** The property key. */ 723 private String key; 724 /** The property value. */ 725 private String value; 726 727 /** 728 * Gets key. 729 * @return the property key 730 */ 731 public String getKey() { 732 return key; 733 } 734 735 /** 736 * Sets key. 737 * @param key sets the property key 738 */ 739 public void setKey(String key) { 740 this.key = key; 741 } 742 743 /** 744 * Gets value. 745 * @return the property value 746 */ 747 public String getValue() { 748 return value; 749 } 750 751 /** 752 * Sets value. 753 * @param value set the property value 754 */ 755 public void setValue(String value) { 756 this.value = value; 757 } 758 759 /** 760 * Sets the property value from a File. 761 * @param file set the property value from a File 762 */ 763 public void setFile(File file) { 764 value = file.getAbsolutePath(); 765 } 766 767 } 768 769 /** Represents a custom listener. */ 770 public static class Listener { 771 772 /** Class name of the listener class. */ 773 private String className; 774 775 /** 776 * Gets class name. 777 * @return the class name 778 */ 779 public String getClassname() { 780 return className; 781 } 782 783 /** 784 * Sets class name. 785 * @param name set the class name 786 */ 787 public void setClassname(String name) { 788 className = name; 789 } 790 791 } 792 793}