View Javadoc

1   /*
2    * $Id: GroovyClassLoader.java,v 1.52 2005/07/14 13:54:52 blackdrag Exp $
3    *
4    * Copyright 2003 (C) James Strachan and Bob Mcwhirter. All Rights Reserved.
5    *
6    * Redistribution and use of this software and associated documentation
7    * ("Software"), with or without modification, are permitted provided that the
8    * following conditions are met:
9    *  1. Redistributions of source code must retain copyright statements and
10   * notices. Redistributions must also contain a copy of this document.
11   *  2. Redistributions in binary form must reproduce the above copyright
12   * notice, this list of conditions and the following disclaimer in the
13   * documentation and/or other materials provided with the distribution.
14   *  3. The name "groovy" must not be used to endorse or promote products
15   * derived from this Software without prior written permission of The Codehaus.
16   * For written permission, please contact info@codehaus.org.
17   *  4. Products derived from this Software may not be called "groovy" nor may
18   * "groovy" appear in their names without prior written permission of The
19   * Codehaus. "groovy" is a registered trademark of The Codehaus.
20   *  5. Due credit should be given to The Codehaus - http://groovy.codehaus.org/
21   *
22   * THIS SOFTWARE IS PROVIDED BY THE CODEHAUS AND CONTRIBUTORS ``AS IS'' AND ANY
23   * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
24   * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
25   * DISCLAIMED. IN NO EVENT SHALL THE CODEHAUS OR ITS CONTRIBUTORS BE LIABLE FOR
26   * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
27   * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
28   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
29   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30   * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
31   * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
32   * DAMAGE.
33   *
34   */
35  package groovy.lang;
36  
37  import java.io.BufferedInputStream;
38  import java.io.ByteArrayInputStream;
39  import java.io.ByteArrayOutputStream;
40  import java.io.File;
41  import java.io.IOException;
42  import java.io.InputStream;
43  import java.lang.reflect.Field;
44  import java.net.MalformedURLException;
45  import java.net.URL;
46  import java.security.AccessController;
47  import java.security.CodeSource;
48  import java.security.PrivilegedAction;
49  import java.security.ProtectionDomain;
50  import java.security.SecureClassLoader;
51  import java.util.ArrayList;
52  import java.util.Collection;
53  import java.util.HashMap;
54  import java.util.HashSet;
55  import java.util.Iterator;
56  import java.util.List;
57  import java.util.Map;
58  import java.util.Set;
59  import java.util.jar.Attributes;
60  import java.util.jar.JarEntry;
61  import java.util.jar.JarFile;
62  import java.util.jar.Manifest;
63  
64  import org.codehaus.groovy.ast.ClassNode;
65  import org.codehaus.groovy.classgen.Verifier;
66  import org.codehaus.groovy.control.CompilationFailedException;
67  import org.codehaus.groovy.control.CompilationUnit;
68  import org.codehaus.groovy.control.CompilerConfiguration;
69  import org.codehaus.groovy.control.Phases;
70  import org.objectweb.asm.ClassVisitor;
71  import org.objectweb.asm.ClassWriter;
72  
73  /***
74   * A ClassLoader which can load Groovy classes
75   *
76   * @author <a href="mailto:james@coredevelopers.net">James Strachan </a>
77   * @author Guillaume Laforge
78   * @author Steve Goetze
79   * @author Bing Ran
80   * @author <a href="mailto:scottstirling@rcn.com">Scott Stirling</a>
81   * @version $Revision: 1.52 $
82   */
83  public class GroovyClassLoader extends SecureClassLoader {
84  
85      private Map cache = new HashMap();
86      private Collection loadedClasses = null;
87  
88      public void removeFromCache(Class aClass) {
89          cache.remove(aClass);
90      }
91  
92      public static class PARSING {
93      }
94  
95      private class NOT_RESOLVED {
96      }
97  
98      private CompilerConfiguration config;
99  
100     private String[] searchPaths;
101 
102     private Set additionalPaths = new HashSet();
103 
104     public GroovyClassLoader() {
105         this(Thread.currentThread().getContextClassLoader());
106     }
107 
108     public GroovyClassLoader(ClassLoader loader) {
109         this(loader, null);
110     }
111 
112     public GroovyClassLoader(GroovyClassLoader parent) {
113         this(parent, parent.config);
114     }
115 
116     public GroovyClassLoader(ClassLoader loader, CompilerConfiguration config) {
117         super(loader);
118         if (config==null) config = CompilerConfiguration.DEFAULT;
119         this.config = config;
120         this.loadedClasses = new ArrayList();
121     }
122 
123     /***
124      * Loads the given class node returning the implementation Class
125      *
126      * @param classNode
127      * @return
128      */
129     public Class defineClass(ClassNode classNode, String file) {
130         return defineClass(classNode, file, "/groovy/defineClass");
131     }
132 
133     /***
134      * Loads the given class node returning the implementation Class
135      *
136      * @param classNode
137      * @return
138      */
139     public Class defineClass(ClassNode classNode, String file, String newCodeBase) {
140         CodeSource codeSource = null;
141         try {
142             codeSource = new CodeSource(new URL("file", "", newCodeBase), (java.security.cert.Certificate[]) null);
143         } catch (MalformedURLException e) {
144             //swallow
145         }
146 
147         //
148         // BUG: Why is this passing getParent() as the ClassLoader???
149 
150         CompilationUnit unit = new CompilationUnit(config, codeSource, getParent());
151         try {
152             ClassCollector collector = createCollector(unit);
153 
154             unit.addClassNode(classNode);
155             unit.setClassgenCallback(collector);
156             unit.compile(Phases.CLASS_GENERATION);
157 
158             return collector.generatedClass;
159         } catch (CompilationFailedException e) {
160             throw new RuntimeException(e);
161         }
162     }
163 
164     /***
165      * Parses the given file into a Java class capable of being run
166      *
167      * @param file the file name to parse
168      * @return the main class defined in the given script
169      */
170     public Class parseClass(File file) throws CompilationFailedException, IOException {
171         return parseClass(new GroovyCodeSource(file));
172     }
173 
174     /***
175      * Parses the given text into a Java class capable of being run
176      *
177      * @param text     the text of the script/class to parse
178      * @param fileName the file name to use as the name of the class
179      * @return the main class defined in the given script
180      */
181     public Class parseClass(String text, String fileName) throws CompilationFailedException {
182         return parseClass(new ByteArrayInputStream(text.getBytes()), fileName);
183     }
184 
185     /***
186      * Parses the given text into a Java class capable of being run
187      *
188      * @param text the text of the script/class to parse
189      * @return the main class defined in the given script
190      */
191     public Class parseClass(String text) throws CompilationFailedException {
192         return parseClass(new ByteArrayInputStream(text.getBytes()), "script" + System.currentTimeMillis() + ".groovy");
193     }
194 
195     /***
196      * Parses the given character stream into a Java class capable of being run
197      *
198      * @param in an InputStream
199      * @return the main class defined in the given script
200      */
201     public Class parseClass(InputStream in) throws CompilationFailedException {
202         return parseClass(in, "script" + System.currentTimeMillis() + ".groovy");
203     }
204 
205     public Class parseClass(final InputStream in, final String fileName) throws CompilationFailedException {
206         //For generic input streams, provide a catch-all codebase of
207         // GroovyScript
208         //Security for these classes can be administered via policy grants with
209         // a codebase
210         //of file:groovy.script
211         GroovyCodeSource gcs = (GroovyCodeSource) AccessController.doPrivileged(new PrivilegedAction() {
212             public Object run() {
213                 return new GroovyCodeSource(in, fileName, "/groovy/script");
214             }
215         });
216         return parseClass(gcs);
217     }
218 
219 
220     public Class parseClass(GroovyCodeSource codeSource) throws CompilationFailedException {
221         return parseClass(codeSource, true);
222     }
223 
224     /***
225      * Parses the given code source into a Java class capable of being run
226      *
227      * @return the main class defined in the given script
228      */
229     public Class parseClass(GroovyCodeSource codeSource, boolean shouldCache) throws CompilationFailedException {
230         String name = codeSource.getName();
231         Class answer = null;
232         //ASTBuilder.resolveName can call this recursively -- for example when
233         // resolving a Constructor
234         //invocation for a class that is currently being compiled.
235         synchronized (cache) {
236             answer = (Class) cache.get(name);
237             if (answer != null) {
238                 return (answer == PARSING.class ? null : answer);
239             } else {
240                 cache.put(name, PARSING.class);
241             }
242         }
243         //Was neither already loaded nor compiling, so compile and add to
244         // cache.
245         try {
246             CompilationUnit unit = new CompilationUnit(config, codeSource.getCodeSource(), this);
247             // try {
248             ClassCollector collector = createCollector(unit);
249 
250             if (codeSource.getFile()==null) {
251                 unit.addSource(name, codeSource.getInputStream());
252             } else {
253                 unit.addSource(codeSource.getFile());
254             }
255             unit.setClassgenCallback(collector);
256             int goalPhase = Phases.CLASS_GENERATION;
257             if (config != null && config.getTargetDirectory()!=null) goalPhase = Phases.OUTPUT;
258             unit.compile(goalPhase);
259 
260             answer = collector.generatedClass;
261             // }
262             // catch( CompilationFailedException e ) {
263             //     throw new RuntimeException( e );
264             // }
265             synchronized (this.loadedClasses) {
266                 this.loadedClasses.addAll(collector.getLoadedClasses());
267             }
268         } finally {
269             synchronized (cache) {
270                 if (answer == null || !shouldCache) {
271                     cache.remove(name);
272                 } else {
273                     cache.put(name, answer);
274                 }
275             }
276             try {
277                 codeSource.getInputStream().close();
278             } catch (IOException e) {
279                 throw new GroovyRuntimeException("unable to close stream",e);
280             }
281         }
282         return answer;
283     }
284 
285     /***
286      * Using this classloader you can load groovy classes from the system
287      * classpath as though they were already compiled. Note that .groovy classes
288      * found with this mechanism need to conform to the standard java naming
289      * convention - i.e. the public class inside the file must match the
290      * filename and the file must be located in a directory structure that
291      * matches the package structure.
292      */
293     /*protected Class findClass(final String name) throws ClassNotFoundException {
294         SecurityManager sm = System.getSecurityManager();
295         if (sm != null) {
296             String className = name.replace('/', '.');
297             int i = className.lastIndexOf('.');
298             if (i != -1) {
299                 sm.checkPackageDefinition(className.substring(0, i));
300             }
301         }
302         try {
303             return (Class) AccessController.doPrivileged(new PrivilegedExceptionAction() {
304                 public Object run() throws ClassNotFoundException {
305                     return findGroovyClass(name);
306                 }
307             });
308         } catch (PrivilegedActionException pae) {
309             throw (ClassNotFoundException) pae.getException();
310         }
311     }*/
312 
313 /*    protected Class findGroovyClass(String name) throws ClassNotFoundException {
314         //Use a forward slash here for the path separator. It will work as a
315         // separator
316         //for the File class on all platforms, AND it is required as a jar file
317         // entry separator.
318         String filename = name.replace('.', '/') + ".groovy";
319         String[] paths = getClassPath();
320         // put the absolute classname in a File object so we can easily
321         // pluck off the class name and the package path
322         File classnameAsFile = new File(filename);
323         // pluck off the classname without the package
324         String classname = classnameAsFile.getName();
325         String pkg = classnameAsFile.getParent();
326         String pkgdir;
327         for (int i = 0; i < paths.length; i++) {
328             String pathName = paths[i];
329             File path = new File(pathName);
330             if (path.exists()) {
331                 if (path.isDirectory()) {
332                     // patch to fix case preserving but case insensitive file
333                     // systems (like macosx)
334                     // JIRA issue 414
335                     //
336                     // first see if the file even exists, no matter what the
337                     // case is
338                     File nocasefile = new File(path, filename);
339                     if (!nocasefile.exists())
340                         continue;
341 
342                     // now we know the file is there is some form or another, so
343                     // let's look up all the files to see if the one we're
344                     // really
345                     // looking for is there
346                     if (pkg == null)
347                         pkgdir = pathName;
348                     else
349                         pkgdir = pathName + "/" + pkg;
350                     File pkgdirF = new File(pkgdir);
351                     // make sure the resulting path is there and is a dir
352                     if (pkgdirF.exists() && pkgdirF.isDirectory()) {
353                         File files[] = pkgdirF.listFiles();
354                         for (int j = 0; j < files.length; j++) {
355                             // do the case sensitive comparison
356                             if (files[j].getName().equals(classname)) {
357                                 try {
358                                     return parseClass(files[j]);
359                                 } catch (CompilationFailedException e) {
360                                     throw new ClassNotFoundException("Syntax error in groovy file: " + files[j].getAbsolutePath(), e);
361                                 } catch (IOException e) {
362                                     throw new ClassNotFoundException("Error reading groovy file: " + files[j].getAbsolutePath(), e);
363                                 }
364                             }
365                         }
366                     }
367                 } else {
368                     try {
369                         JarFile jarFile = new JarFile(path);
370                         JarEntry entry = jarFile.getJarEntry(filename);
371                         if (entry != null) {
372                             byte[] bytes = extractBytes(jarFile, entry);
373                             Certificate[] certs = entry.getCertificates();
374                             try {
375                                 return parseClass(new GroovyCodeSource(new ByteArrayInputStream(bytes), filename, path, certs));
376                             } catch (CompilationFailedException e1) {
377                                 throw new ClassNotFoundException("Syntax error in groovy file: " + filename, e1);
378                             }
379                         }
380 
381                     } catch (IOException e) {
382                         // Bad jar in classpath, ignore
383                     }
384                 }
385             }
386         }
387         throw new ClassNotFoundException(name);
388     }*/
389 
390     //Read the bytes from a non-null JarEntry. This is done here because the
391     // entry must be read completely
392     //in order to get verified certificates, which can only be obtained after a
393     // full read.
394     private byte[] extractBytes(JarFile jarFile, JarEntry entry) {
395         ByteArrayOutputStream baos = new ByteArrayOutputStream();
396         int b;
397         try {
398             BufferedInputStream bis = new BufferedInputStream(jarFile.getInputStream(entry));
399             while ((b = bis.read()) != -1) {
400                 baos.write(b);
401             }
402         } catch (IOException ioe) {
403             throw new GroovyRuntimeException("Could not read the jar bytes for " + entry.getName());
404         }
405         return baos.toByteArray();
406     }
407 
408       /***
409        * Workaround for Groovy-835
410        *
411        * @return the classpath as an array of strings, uses the classpath in the CompilerConfiguration object if possible,
412        *         otherwise defaults to the value of the <tt>java.class.path</tt> system property
413        */
414       protected String[] getClassPath() {
415         if (null == searchPaths) {
416           String classpath;
417           if(null != config && null != config.getClasspath()) {
418             //there's probably a better way to do this knowing the internals of
419             //Groovy, but it works for now
420             StringBuffer sb = new StringBuffer();
421             for(Iterator iter = config.getClasspath().iterator(); iter.hasNext(); ) {
422               sb.append(iter.next().toString());
423               sb.append(File.pathSeparatorChar);
424             }
425             //remove extra path separator
426             sb.deleteCharAt(sb.length()-1);
427             classpath = sb.toString();
428           } else {
429             classpath = System.getProperty("java.class.path", ".");
430           }
431           List pathList = new ArrayList(additionalPaths);
432           expandClassPath(pathList, null, classpath, false);
433           searchPaths = new String[pathList.size()];
434           searchPaths = (String[]) pathList.toArray(searchPaths);
435         }
436         return searchPaths;
437       }
438 
439     /***
440      * @param pathList an empty list that will contain the elements of the classpath
441      * @param classpath the classpath specified as a single string
442      */
443     protected void expandClassPath(List pathList, String base, String classpath, boolean isManifestClasspath) {
444 
445         // checking against null prevents an NPE when recursevely expanding the
446         // classpath
447         // in case the classpath is malformed
448         if (classpath != null) {
449 
450             // Sun's convention for the class-path attribute is to seperate each
451             // entry with spaces
452             // but some libraries don't respect that convention and add commas,
453             // colons, semi-colons
454             String[] paths;
455             if (isManifestClasspath) {
456                 paths = classpath.split("[// ,:;]");
457             } else {
458                 paths = classpath.split(File.pathSeparator);
459             }
460 
461             for (int i = 0; i < paths.length; i++) {
462                 if (paths.length > 0) {
463                     File path = null;
464 
465                     if ("".equals(base)) {
466                         path = new File(paths[i]);
467                     } else {
468                         path = new File(base, paths[i]);
469                     }
470 
471                     if (path.exists()) {
472                         if (!path.isDirectory()) {
473                             try {
474                                 JarFile jar = new JarFile(path);
475                                 pathList.add(paths[i]);
476 
477                                 Manifest manifest = jar.getManifest();
478                                 if (manifest != null) {
479                                     Attributes classPathAttributes = manifest.getMainAttributes();
480                                     String manifestClassPath = classPathAttributes.getValue("Class-Path");
481 
482                                     if (manifestClassPath != null)
483                                         expandClassPath(pathList, paths[i], manifestClassPath, true);
484                                 }
485                             } catch (IOException e) {
486                                 // Bad jar, ignore
487                                 continue;
488                             }
489                         } else {
490                             pathList.add(paths[i]);
491                         }
492                     }
493                 }
494             }
495         }
496     }
497 
498     /***
499      * A helper method to allow bytecode to be loaded. spg changed name to
500      * defineClass to make it more consistent with other ClassLoader methods
501      */
502     protected Class defineClass(String name, byte[] bytecode, ProtectionDomain domain) {
503         return defineClass(name, bytecode, 0, bytecode.length, domain);
504     }
505 
506     protected ClassCollector createCollector(CompilationUnit unit) {
507         return new ClassCollector(this, unit);
508     }
509 
510     public static class ClassCollector extends CompilationUnit.ClassgenCallback {
511         private Class generatedClass;
512 
513         private GroovyClassLoader cl;
514 
515         private CompilationUnit unit;
516 
517         private Collection loadedClasses = null;
518 
519         protected ClassCollector(GroovyClassLoader cl, CompilationUnit unit) {
520             this.cl = cl;
521             this.unit = unit;
522             this.loadedClasses = new ArrayList();
523         }
524 
525         protected Class onClassNode(ClassWriter classWriter, ClassNode classNode) {
526             byte[] code = classWriter.toByteArray();
527 
528             Class theClass = cl.defineClass(classNode.getName(), code, 0, code.length, unit.getAST().getCodeSource());
529             this.loadedClasses.add(theClass);
530 
531             if (generatedClass == null) {
532                 generatedClass = theClass;
533             }
534 
535             return theClass;
536         }
537 
538         public void call(ClassVisitor classWriter, ClassNode classNode) {
539             onClassNode((ClassWriter) classWriter, classNode);
540         }
541 
542         public Collection getLoadedClasses() {
543             return this.loadedClasses;
544         }
545     }
546 
547     /***
548      * open up the super class define that takes raw bytes
549      *
550      */
551     public Class defineClass(String name, byte[] b) {
552         return super.defineClass(name, b, 0, b.length);
553     }
554 
555     /*
556      * (non-Javadoc)
557      *
558      * @see java.lang.ClassLoader#loadClass(java.lang.String, boolean)
559      *      Implemented here to check package access prior to returning an
560      *      already loaded class. todo : br shall we search for the source
561      *      groovy here to see if the soource file has been updated first?
562      */
563     protected synchronized Class loadClass(final String name, boolean resolve) throws ClassNotFoundException {
564         synchronized (cache) {
565             Class cls = (Class) cache.get(name);
566             if (cls == NOT_RESOLVED.class) throw new ClassNotFoundException(name);
567             if (cls!=null) return cls;
568         }
569 
570         SecurityManager sm = System.getSecurityManager();
571         if (sm != null) {
572             String className = name.replace('/', '.');
573             int i = className.lastIndexOf('.');
574             if (i != -1) {
575                 sm.checkPackageAccess(className.substring(0, i));
576             }
577         }
578 
579         Class cls = null;
580         ClassNotFoundException last = null;
581         try {
582             cls = super.loadClass(name, resolve);
583 
584             boolean recompile = false;
585             if (getTimeStamp(cls) < Long.MAX_VALUE) {
586                 Class[] inters = cls.getInterfaces();
587                 for (int i = 0; i < inters.length; i++) {
588                     if (inters[i].getName().equals(GroovyObject.class.getName())) {
589                         recompile=true;
590                         break;
591                     }
592                 }
593             }
594             if (!recompile) return cls;
595         } catch (ClassNotFoundException cnfe) {
596             last = cnfe;
597         }
598 
599         // try groovy file
600         try {
601             File source = (File) AccessController.doPrivileged(new PrivilegedAction() {
602                 public Object run() {
603                     return getSourceFile(name);
604                 }
605             });
606             if (source != null) {
607                 if ((cls!=null && isSourceNewer(source, cls)) || (cls==null)) {
608                     synchronized (cache) {
609                         cache.put(name,PARSING.class);
610                     }
611                     cls = parseClass(source);
612                 }
613             }
614         } catch (Exception e) {
615             cls = null;
616             last = new ClassNotFoundException("Failed to parse groovy file: " + name, e);
617         }
618         if (cls==null) {
619             if (last==null) throw new AssertionError(true);
620             synchronized (cache) {
621                 cache.put(name, NOT_RESOLVED.class);
622             }
623             throw last;
624         }
625         synchronized (cache) {
626             cache.put(name, cls);
627         }
628         return cls;
629     }
630 
631     private long getTimeStamp(Class cls) {
632         Field field;
633         Long o;
634         try {
635             field = cls.getField(Verifier.__TIMESTAMP);
636             o = (Long) field.get(null);
637         } catch (Exception e) {
638             //throw new RuntimeException(e);
639             return Long.MAX_VALUE;
640         }
641         return o.longValue();
642     }
643 
644     //    static class ClassWithTimeTag {
645     //        final static ClassWithTimeTag NOT_RESOLVED = new ClassWithTimeTag(null,
646     // 0);
647     //        Class cls;
648     //        long lastModified;
649     //
650     //        public ClassWithTimeTag(Class cls, long lastModified) {
651     //            this.cls = cls;
652     //            this.lastModified = lastModified;
653     //        }
654     //    }
655 
656     private File getSourceFile(String name) {
657         File source = null;
658         String filename = name.replace('.', '/') + ".groovy";
659         String[] paths = getClassPath();
660         for (int i = 0; i < paths.length; i++) {
661             String pathName = paths[i];
662             File path = new File(pathName);
663             if (path.exists()) { // case sensitivity depending on OS!
664                 if (path.isDirectory()) {
665                     File file = new File(path, filename);
666                     if (file.exists()) {
667                         // file.exists() might be case insensitive. Let's do
668                         // case sensitive match for the filename
669                         boolean fileExists = false;
670                         int sepp = filename.lastIndexOf('/');
671                         String fn = filename;
672                         if (sepp >= 0) {
673                             fn = filename.substring(++sepp);
674                         }
675                         File parent = file.getParentFile();
676                         String[] files = parent.list();
677                         for (int j = 0; j < files.length; j++) {
678                             if (files[j].equals(fn)) {
679                                 fileExists = true;
680                                 break;
681                             }
682                         }
683 
684                         if (fileExists) {
685                             source = file;
686                             break;
687                         }
688                     }
689                 }
690             }
691         }
692         return source;
693     }
694 
695     private boolean isSourceNewer(File source, Class cls) {
696         return source.lastModified() > getTimeStamp(cls);
697     }
698 
699     public void addClasspath(String path) {
700         additionalPaths.add(path);
701         searchPaths = null;
702     }
703 
704     /***
705      * <p>Returns all Groovy classes loaded by this class loader.
706      *
707      * @return all classes loaded by this class loader
708      */
709     public Class[] getLoadedClasses() {
710         Class[] loadedClasses = null;
711         synchronized (this.loadedClasses) {
712             loadedClasses = (Class[])this.loadedClasses.toArray(new Class[this.loadedClasses.size()]);
713         }
714         return loadedClasses;
715     }
716 }