001    /*
002     * $Id: GroovyClassLoader.java,v 1.52 2005/07/14 13:54:52 blackdrag Exp $
003     *
004     * Copyright 2003 (C) James Strachan and Bob Mcwhirter. All Rights Reserved.
005     *
006     * Redistribution and use of this software and associated documentation
007     * ("Software"), with or without modification, are permitted provided that the
008     * following conditions are met:
009     *  1. Redistributions of source code must retain copyright statements and
010     * notices. Redistributions must also contain a copy of this document.
011     *  2. Redistributions in binary form must reproduce the above copyright
012     * notice, this list of conditions and the following disclaimer in the
013     * documentation and/or other materials provided with the distribution.
014     *  3. The name "groovy" must not be used to endorse or promote products
015     * derived from this Software without prior written permission of The Codehaus.
016     * For written permission, please contact info@codehaus.org.
017     *  4. Products derived from this Software may not be called "groovy" nor may
018     * "groovy" appear in their names without prior written permission of The
019     * Codehaus. "groovy" is a registered trademark of The Codehaus.
020     *  5. Due credit should be given to The Codehaus - http://groovy.codehaus.org/
021     *
022     * THIS SOFTWARE IS PROVIDED BY THE CODEHAUS AND CONTRIBUTORS ``AS IS'' AND ANY
023     * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
024     * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
025     * DISCLAIMED. IN NO EVENT SHALL THE CODEHAUS OR ITS CONTRIBUTORS BE LIABLE FOR
026     * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
027     * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
028     * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
029     * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
030     * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
031     * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
032     * DAMAGE.
033     *
034     */
035    package groovy.lang;
036    
037    import java.io.BufferedInputStream;
038    import java.io.ByteArrayInputStream;
039    import java.io.ByteArrayOutputStream;
040    import java.io.File;
041    import java.io.IOException;
042    import java.io.InputStream;
043    import java.lang.reflect.Field;
044    import java.net.MalformedURLException;
045    import java.net.URL;
046    import java.security.AccessController;
047    import java.security.CodeSource;
048    import java.security.PrivilegedAction;
049    import java.security.ProtectionDomain;
050    import java.security.SecureClassLoader;
051    import java.util.ArrayList;
052    import java.util.Collection;
053    import java.util.HashMap;
054    import java.util.HashSet;
055    import java.util.Iterator;
056    import java.util.List;
057    import java.util.Map;
058    import java.util.Set;
059    import java.util.jar.Attributes;
060    import java.util.jar.JarEntry;
061    import java.util.jar.JarFile;
062    import java.util.jar.Manifest;
063    
064    import org.codehaus.groovy.ast.ClassNode;
065    import org.codehaus.groovy.classgen.Verifier;
066    import org.codehaus.groovy.control.CompilationFailedException;
067    import org.codehaus.groovy.control.CompilationUnit;
068    import org.codehaus.groovy.control.CompilerConfiguration;
069    import org.codehaus.groovy.control.Phases;
070    import org.objectweb.asm.ClassVisitor;
071    import org.objectweb.asm.ClassWriter;
072    
073    /**
074     * A ClassLoader which can load Groovy classes
075     *
076     * @author <a href="mailto:james@coredevelopers.net">James Strachan </a>
077     * @author Guillaume Laforge
078     * @author Steve Goetze
079     * @author Bing Ran
080     * @author <a href="mailto:scottstirling@rcn.com">Scott Stirling</a>
081     * @version $Revision: 1.52 $
082     */
083    public class GroovyClassLoader extends SecureClassLoader {
084    
085        private Map cache = new HashMap();
086        private Collection loadedClasses = null;
087    
088        public void removeFromCache(Class aClass) {
089            cache.remove(aClass);
090        }
091    
092        public static class PARSING {
093        }
094    
095        private class NOT_RESOLVED {
096        }
097    
098        private CompilerConfiguration config;
099    
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    }