1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2019 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.ant;
21  
22  import java.io.File;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.OutputStream;
26  import java.nio.file.Files;
27  import java.util.ArrayList;
28  import java.util.Arrays;
29  import java.util.List;
30  import java.util.Locale;
31  import java.util.Map;
32  import java.util.Objects;
33  import java.util.Properties;
34  import java.util.stream.Collectors;
35  
36  import org.apache.tools.ant.AntClassLoader;
37  import org.apache.tools.ant.BuildException;
38  import org.apache.tools.ant.DirectoryScanner;
39  import org.apache.tools.ant.Project;
40  import org.apache.tools.ant.Task;
41  import org.apache.tools.ant.taskdefs.LogOutputStream;
42  import org.apache.tools.ant.types.EnumeratedAttribute;
43  import org.apache.tools.ant.types.FileSet;
44  import org.apache.tools.ant.types.Path;
45  import org.apache.tools.ant.types.Reference;
46  
47  import com.puppycrawl.tools.checkstyle.Checker;
48  import com.puppycrawl.tools.checkstyle.ConfigurationLoader;
49  import com.puppycrawl.tools.checkstyle.DefaultLogger;
50  import com.puppycrawl.tools.checkstyle.ModuleFactory;
51  import com.puppycrawl.tools.checkstyle.PackageObjectFactory;
52  import com.puppycrawl.tools.checkstyle.PropertiesExpander;
53  import com.puppycrawl.tools.checkstyle.ThreadModeSettings;
54  import com.puppycrawl.tools.checkstyle.XMLLogger;
55  import com.puppycrawl.tools.checkstyle.api.AuditListener;
56  import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
57  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
58  import com.puppycrawl.tools.checkstyle.api.Configuration;
59  import com.puppycrawl.tools.checkstyle.api.RootModule;
60  import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
61  import com.puppycrawl.tools.checkstyle.api.SeverityLevelCounter;
62  
63  /**
64   * An implementation of a ANT task for calling checkstyle. See the documentation
65   * of the task for usage.
66   * @noinspection ClassLoaderInstantiation
67   */
68  public class CheckstyleAntTask extends Task {
69  
70      /** Poor man's enum for an xml formatter. */
71      private static final String E_XML = "xml";
72      /** Poor man's enum for an plain formatter. */
73      private static final String E_PLAIN = "plain";
74  
75      /** Suffix for time string. */
76      private static final String TIME_SUFFIX = " ms.";
77  
78      /** Contains the paths to process. */
79      private final List<Path> paths = new ArrayList<>();
80  
81      /** Contains the filesets to process. */
82      private final List<FileSet> fileSets = new ArrayList<>();
83  
84      /** Contains the formatters to log to. */
85      private final List<Formatter> formatters = new ArrayList<>();
86  
87      /** Contains the Properties to override. */
88      private final List<Property> overrideProps = new ArrayList<>();
89  
90      /** Class path to locate class files. */
91      private Path classpath;
92  
93      /** Name of file to check. */
94      private String fileName;
95  
96      /** Config file containing configuration. */
97      private String config;
98  
99      /** 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             final String version = CheckstyleAntTask.class.getPackage().getImplementationVersion();
274 
275             log("checkstyle version " + version, Project.MSG_VERBOSE);
276 
277             // Check for no arguments
278             if (fileName == null
279                     && fileSets.isEmpty()
280                     && paths.isEmpty()) {
281                 throw new BuildException(
282                         "Must specify at least one of 'file' or nested 'fileset' or 'path'.",
283                         getLocation());
284             }
285             if (config == null) {
286                 throw new BuildException("Must specify 'config'.", getLocation());
287             }
288             realExecute(version);
289         }
290         finally {
291             final long endTime = System.currentTimeMillis();
292             log("Total execution took " + (endTime - startTime) + TIME_SUFFIX,
293                 Project.MSG_VERBOSE);
294         }
295     }
296 
297     /**
298      * Helper implementation to perform execution.
299      * @param checkstyleVersion Checkstyle compile version.
300      */
301     private void realExecute(String checkstyleVersion) {
302         // Create the root module
303         RootModule rootModule = null;
304         try {
305             rootModule = createRootModule();
306 
307             // setup the listeners
308             final AuditListener[] listeners = getListeners();
309             for (AuditListener element : listeners) {
310                 rootModule.addListener(element);
311             }
312             final SeverityLevelCounter warningCounter =
313                 new SeverityLevelCounter(SeverityLevel.WARNING);
314             rootModule.addListener(warningCounter);
315 
316             processFiles(rootModule, warningCounter, checkstyleVersion);
317         }
318         finally {
319             if (rootModule != null) {
320                 rootModule.destroy();
321             }
322         }
323     }
324 
325     /**
326      * Scans and processes files by means given root module.
327      * @param rootModule Root module to process files
328      * @param warningCounter Root Module's counter of warnings
329      * @param checkstyleVersion Checkstyle compile version
330      */
331     private void processFiles(RootModule rootModule, final SeverityLevelCounter warningCounter,
332             final String checkstyleVersion) {
333         final long startTime = System.currentTimeMillis();
334         final List<File> files = getFilesToCheck();
335         final long endTime = System.currentTimeMillis();
336         log("To locate the files took " + (endTime - startTime) + TIME_SUFFIX,
337             Project.MSG_VERBOSE);
338 
339         log("Running Checkstyle "
340                 + Objects.toString(checkstyleVersion, "")
341                 + " on " + files.size()
342                 + " files", Project.MSG_INFO);
343         log("Using configuration " + config, Project.MSG_VERBOSE);
344 
345         final int numErrs;
346 
347         try {
348             final long processingStartTime = System.currentTimeMillis();
349             numErrs = rootModule.process(files);
350             final long processingEndTime = System.currentTimeMillis();
351             log("To process the files took " + (processingEndTime - processingStartTime)
352                 + TIME_SUFFIX, Project.MSG_VERBOSE);
353         }
354         catch (CheckstyleException ex) {
355             throw new BuildException("Unable to process files: " + files, ex);
356         }
357         final int numWarnings = warningCounter.getCount();
358         final boolean okStatus = numErrs <= maxErrors && numWarnings <= maxWarnings;
359 
360         // Handle the return status
361         if (!okStatus) {
362             final String failureMsg =
363                     "Got " + numErrs + " errors and " + numWarnings
364                             + " warnings.";
365             if (failureProperty != null) {
366                 getProject().setProperty(failureProperty, failureMsg);
367             }
368 
369             if (failOnViolation) {
370                 throw new BuildException(failureMsg, getLocation());
371             }
372         }
373     }
374 
375     /**
376      * Creates new instance of the root module.
377      * @return new instance of the root module
378      */
379     private RootModule createRootModule() {
380         final RootModule rootModule;
381         try {
382             final Properties props = createOverridingProperties();
383             final ThreadModeSettings threadModeSettings =
384                     ThreadModeSettings.SINGLE_THREAD_MODE_INSTANCE;
385             final ConfigurationLoader.IgnoredModulesOptions ignoredModulesOptions;
386             if (executeIgnoredModules) {
387                 ignoredModulesOptions = ConfigurationLoader.IgnoredModulesOptions.EXECUTE;
388             }
389             else {
390                 ignoredModulesOptions = ConfigurationLoader.IgnoredModulesOptions.OMIT;
391             }
392 
393             final Configuration configuration = ConfigurationLoader.loadConfiguration(config,
394                     new PropertiesExpander(props), ignoredModulesOptions, threadModeSettings);
395 
396             final ClassLoader moduleClassLoader =
397                 Checker.class.getClassLoader();
398 
399             final ModuleFactory factory = new PackageObjectFactory(
400                     Checker.class.getPackage().getName() + ".", moduleClassLoader);
401 
402             rootModule = (RootModule) factory.createModule(configuration.getName());
403             rootModule.setModuleClassLoader(moduleClassLoader);
404 
405             if (rootModule instanceof Checker) {
406                 final ClassLoader loader = new AntClassLoader(getProject(),
407                         classpath);
408 
409                 ((Checker) rootModule).setClassLoader(loader);
410             }
411 
412             rootModule.configure(configuration);
413         }
414         catch (final CheckstyleException ex) {
415             throw new BuildException(String.format(Locale.ROOT, "Unable to create Root Module: "
416                     + "config {%s}, classpath {%s}.", config, classpath), ex);
417         }
418         return rootModule;
419     }
420 
421     /**
422      * Create the Properties object based on the arguments specified
423      * to the ANT task.
424      * @return the properties for property expansion expansion
425      * @throws BuildException if an error occurs
426      */
427     private Properties createOverridingProperties() {
428         final Properties returnValue = new Properties();
429 
430         // Load the properties file if specified
431         if (properties != null) {
432             try (InputStream inStream = Files.newInputStream(properties.toPath())) {
433                 returnValue.load(inStream);
434             }
435             catch (final IOException ex) {
436                 throw new BuildException("Error loading Properties file '"
437                         + properties + "'", ex, getLocation());
438             }
439         }
440 
441         // override with Ant properties like ${basedir}
442         final Map<String, Object> antProps = getProject().getProperties();
443         for (Map.Entry<String, Object> entry : antProps.entrySet()) {
444             final String value = String.valueOf(entry.getValue());
445             returnValue.setProperty(entry.getKey(), value);
446         }
447 
448         // override with properties specified in subelements
449         for (Property p : overrideProps) {
450             returnValue.setProperty(p.getKey(), p.getValue());
451         }
452 
453         return returnValue;
454     }
455 
456     /**
457      * Return the list of listeners set in this task.
458      * @return the list of listeners.
459      */
460     private AuditListener[] getListeners() {
461         final int formatterCount = Math.max(1, formatters.size());
462 
463         final AuditListener[] listeners = new AuditListener[formatterCount];
464 
465         // formatters
466         try {
467             if (formatters.isEmpty()) {
468                 final OutputStream debug = new LogOutputStream(this, Project.MSG_DEBUG);
469                 final OutputStream err = new LogOutputStream(this, Project.MSG_ERR);
470                 listeners[0] = new DefaultLogger(debug, AutomaticBean.OutputStreamOptions.CLOSE,
471                         err, AutomaticBean.OutputStreamOptions.CLOSE);
472             }
473             else {
474                 for (int i = 0; i < formatterCount; i++) {
475                     final Formatter formatter = formatters.get(i);
476                     listeners[i] = formatter.createListener(this);
477                 }
478             }
479         }
480         catch (IOException ex) {
481             throw new BuildException(String.format(Locale.ROOT, "Unable to create listeners: "
482                     + "formatters {%s}.", formatters), ex);
483         }
484         return listeners;
485     }
486 
487     /**
488      * Returns the list of files (full path name) to process.
489      * @return the list of files included via the fileName, filesets and paths.
490      */
491     private List<File> getFilesToCheck() {
492         final List<File> allFiles = new ArrayList<>();
493         if (fileName != null) {
494             // oops we've got an additional one to process, don't
495             // forget it. No sweat, it's fully resolved via the setter.
496             log("Adding standalone file for audit", Project.MSG_VERBOSE);
497             allFiles.add(new File(fileName));
498         }
499 
500         final List<File> filesFromFileSets = scanFileSets();
501         allFiles.addAll(filesFromFileSets);
502 
503         final List<File> filesFromPaths = scanPaths();
504         allFiles.addAll(filesFromPaths);
505 
506         return allFiles;
507     }
508 
509     /**
510      * Retrieves all files from the defined paths.
511      * @return a list of files defined via paths.
512      */
513     private List<File> scanPaths() {
514         final List<File> allFiles = new ArrayList<>();
515 
516         for (int i = 0; i < paths.size(); i++) {
517             final Path currentPath = paths.get(i);
518             final List<File> pathFiles = scanPath(currentPath, i + 1);
519             allFiles.addAll(pathFiles);
520         }
521 
522         return allFiles;
523     }
524 
525     /**
526      * Scans the given path and retrieves all files for the given path.
527      *
528      * @param path      A path to scan.
529      * @param pathIndex The index of the given path. Used in log messages only.
530      * @return A list of files, extracted from the given path.
531      */
532     private List<File> scanPath(Path path, int pathIndex) {
533         final String[] resources = path.list();
534         log(pathIndex + ") Scanning path " + path, Project.MSG_VERBOSE);
535         final List<File> allFiles = new ArrayList<>();
536         int concreteFilesCount = 0;
537 
538         for (String resource : resources) {
539             final File file = new File(resource);
540             if (file.isFile()) {
541                 concreteFilesCount++;
542                 allFiles.add(file);
543             }
544             else {
545                 final DirectoryScanner scanner = new DirectoryScanner();
546                 scanner.setBasedir(file);
547                 scanner.scan();
548                 final List<File> scannedFiles = retrieveAllScannedFiles(scanner, pathIndex);
549                 allFiles.addAll(scannedFiles);
550             }
551         }
552 
553         if (concreteFilesCount > 0) {
554             log(String.format(Locale.ROOT, "%d) Adding %d files from path %s",
555                 pathIndex, concreteFilesCount, path), Project.MSG_VERBOSE);
556         }
557 
558         return allFiles;
559     }
560 
561     /**
562      * Returns the list of files (full path name) to process.
563      * @return the list of files included via the filesets.
564      */
565     protected List<File> scanFileSets() {
566         final List<File> allFiles = new ArrayList<>();
567 
568         for (int i = 0; i < fileSets.size(); i++) {
569             final FileSet fileSet = fileSets.get(i);
570             final DirectoryScanner scanner = fileSet.getDirectoryScanner(getProject());
571             final List<File> scannedFiles = retrieveAllScannedFiles(scanner, i);
572             allFiles.addAll(scannedFiles);
573         }
574 
575         return allFiles;
576     }
577 
578     /**
579      * Retrieves all matched files from the given scanner.
580      *
581      * @param scanner  A directory scanner. Note, that {@link DirectoryScanner#scan()}
582      *                 must be called before calling this method.
583      * @param logIndex A log entry index. Used only for log messages.
584      * @return A list of files, retrieved from the given scanner.
585      */
586     private List<File> retrieveAllScannedFiles(DirectoryScanner scanner, int logIndex) {
587         final String[] fileNames = scanner.getIncludedFiles();
588         log(String.format(Locale.ROOT, "%d) Adding %d files from directory %s",
589             logIndex, fileNames.length, scanner.getBasedir()), Project.MSG_VERBOSE);
590 
591         return Arrays.stream(fileNames)
592             .map(name -> scanner.getBasedir() + File.separator + name)
593             .map(File::new)
594             .collect(Collectors.toList());
595     }
596 
597     /**
598      * Poor mans enumeration for the formatter types.
599      */
600     public static class FormatterType extends EnumeratedAttribute {
601 
602         /** My possible values. */
603         private static final String[] VALUES = {E_XML, E_PLAIN};
604 
605         @Override
606         public String[] getValues() {
607             return VALUES.clone();
608         }
609 
610     }
611 
612     /**
613      * Details about a formatter to be used.
614      */
615     public static class Formatter {
616 
617         /** The formatter type. */
618         private FormatterType type;
619         /** The file to output to. */
620         private File toFile;
621         /** Whether or not the write to the named file. */
622         private boolean useFile = true;
623 
624         /**
625          * Set the type of the formatter.
626          * @param type the type
627          */
628         public void setType(FormatterType type) {
629             this.type = type;
630         }
631 
632         /**
633          * Set the file to output to.
634          * @param destination destination the file to output to
635          */
636         public void setTofile(File destination) {
637             toFile = destination;
638         }
639 
640         /**
641          * Sets whether or not we write to a file if it is provided.
642          * @param use whether not not to use provided file.
643          */
644         public void setUseFile(boolean use) {
645             useFile = use;
646         }
647 
648         /**
649          * Creates a listener for the formatter.
650          * @param task the task running
651          * @return a listener
652          * @throws IOException if an error occurs
653          */
654         public AuditListener createListener(Task task) throws IOException {
655             final AuditListener listener;
656             if (type != null
657                     && E_XML.equals(type.getValue())) {
658                 listener = createXmlLogger(task);
659             }
660             else {
661                 listener = createDefaultLogger(task);
662             }
663             return listener;
664         }
665 
666         /**
667          * Creates default logger.
668          * @param task the task to possibly log to
669          * @return a DefaultLogger instance
670          * @throws IOException if an error occurs
671          */
672         private AuditListener createDefaultLogger(Task task)
673                 throws IOException {
674             final AuditListener defaultLogger;
675             if (toFile == null || !useFile) {
676                 defaultLogger = new DefaultLogger(
677                     new LogOutputStream(task, Project.MSG_DEBUG),
678                         AutomaticBean.OutputStreamOptions.CLOSE,
679                         new LogOutputStream(task, Project.MSG_ERR),
680                         AutomaticBean.OutputStreamOptions.CLOSE
681                 );
682             }
683             else {
684                 final OutputStream infoStream = Files.newOutputStream(toFile.toPath());
685                 defaultLogger =
686                         new DefaultLogger(infoStream, AutomaticBean.OutputStreamOptions.CLOSE,
687                                 infoStream, AutomaticBean.OutputStreamOptions.NONE);
688             }
689             return defaultLogger;
690         }
691 
692         /**
693          * Creates XML logger.
694          * @param task the task to possibly log to
695          * @return an XMLLogger instance
696          * @throws IOException if an error occurs
697          */
698         private AuditListener createXmlLogger(Task task) throws IOException {
699             final AuditListener xmlLogger;
700             if (toFile == null || !useFile) {
701                 xmlLogger = new XMLLogger(new LogOutputStream(task, Project.MSG_INFO),
702                         AutomaticBean.OutputStreamOptions.CLOSE);
703             }
704             else {
705                 xmlLogger = new XMLLogger(Files.newOutputStream(toFile.toPath()),
706                         AutomaticBean.OutputStreamOptions.CLOSE);
707             }
708             return xmlLogger;
709         }
710 
711     }
712 
713     /**
714      * Represents a property that consists of a key and value.
715      */
716     public static class Property {
717 
718         /** The property key. */
719         private String key;
720         /** The property value. */
721         private String value;
722 
723         /**
724          * Gets key.
725          * @return the property key
726          */
727         public String getKey() {
728             return key;
729         }
730 
731         /**
732          * Sets key.
733          * @param key sets the property key
734          */
735         public void setKey(String key) {
736             this.key = key;
737         }
738 
739         /**
740          * Gets value.
741          * @return the property value
742          */
743         public String getValue() {
744             return value;
745         }
746 
747         /**
748          * Sets value.
749          * @param value set the property value
750          */
751         public void setValue(String value) {
752             this.value = value;
753         }
754 
755         /**
756          * Sets the property value from a File.
757          * @param file set the property value from a File
758          */
759         public void setFile(File file) {
760             value = file.getAbsolutePath();
761         }
762 
763     }
764 
765     /** Represents a custom listener. */
766     public static class Listener {
767 
768         /** Class name of the listener class. */
769         private String className;
770 
771         /**
772          * Gets class name.
773          * @return the class name
774          */
775         public String getClassname() {
776             return className;
777         }
778 
779         /**
780          * Sets class name.
781          * @param name set the class name
782          */
783         public void setClassname(String name) {
784             className = name;
785         }
786 
787     }
788 
789 }