KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > groovy > lang > GroovyClassLoader


1 /*
2  * $Id: GroovyClassLoader.java,v 1.45 2005/06/14 22:33:05 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 org.codehaus.groovy.ast.ClassNode;
38 import org.codehaus.groovy.classgen.Verifier;
39 import org.codehaus.groovy.control.CompilationFailedException;
40 import org.codehaus.groovy.control.CompilationUnit;
41 import org.codehaus.groovy.control.CompilerConfiguration;
42 import org.codehaus.groovy.control.Phases;
43 import org.objectweb.asm.ClassVisitor;
44 import org.objectweb.asm.ClassWriter;
45
46 import java.io.*;
47 import java.lang.reflect.Field JavaDoc;
48 import java.net.MalformedURLException JavaDoc;
49 import java.net.URL JavaDoc;
50 import java.security.*;
51 import java.security.cert.Certificate JavaDoc;
52 import java.util.*;
53 import java.util.jar.Attributes JavaDoc;
54 import java.util.jar.JarEntry JavaDoc;
55 import java.util.jar.JarFile JavaDoc;
56 import java.util.jar.Manifest JavaDoc;
57
58 /**
59  * A ClassLoader which can load Groovy classes
60  *
61  * @author <a HREF="mailto:james@coredevelopers.net">James Strachan </a>
62  * @author Guillaume Laforge
63  * @author Steve Goetze
64  * @author Bing Ran
65  * @author <a HREF="mailto:scottstirling@rcn.com">Scott Stirling</a>
66  * @version $Revision: 1.45 $
67  */

68 public class GroovyClassLoader extends SecureClassLoader {
69
70     private Map cache = new HashMap();
71
72     /**
73      * Mirror the value in the superclass since it's private and we need to
74      * access it for the classpath.
75      */

76     private String JavaDoc[] _searchPaths;
77
78     public void removeFromCache(Class JavaDoc aClass) {
79         cache.remove(aClass);
80     }
81
82     private class PARSING {
83     }
84
85     private class NOT_RESOLVED {
86     }
87
88     private CompilerConfiguration config;
89
90     private String JavaDoc[] searchPaths;
91
92     private List additionalPaths = new ArrayList();
93
94     public GroovyClassLoader() {
95         this(Thread.currentThread().getContextClassLoader());
96     }
97
98     public GroovyClassLoader(ClassLoader JavaDoc loader) {
99         this(loader, new CompilerConfiguration());
100     }
101
102     public GroovyClassLoader(GroovyClassLoader parent) {
103         this(parent, parent.config);
104     }
105
106     public GroovyClassLoader(ClassLoader JavaDoc loader, CompilerConfiguration config) {
107         super(loader);
108         this.config = config;
109     }
110
111     /**
112      * Loads the given class node returning the implementation Class
113      *
114      * @param classNode
115      * @return
116      */

117     public Class JavaDoc defineClass(ClassNode classNode, String JavaDoc file) {
118         return defineClass(classNode, file, "/groovy/defineClass");
119     }
120
121     /**
122      * Loads the given class node returning the implementation Class
123      *
124      * @param classNode
125      * @return
126      */

127     public Class JavaDoc defineClass(ClassNode classNode, String JavaDoc file, String JavaDoc newCodeBase) {
128         CodeSource codeSource = null;
129         try {
130             codeSource = new CodeSource(new URL JavaDoc("file", "", newCodeBase), (java.security.cert.Certificate JavaDoc[]) null);
131         } catch (MalformedURLException JavaDoc e) {
132             //swallow
133
}
134
135         //
136
// BUG: Why is this passing getParent() as the ClassLoader???
137

138         CompilationUnit unit = new CompilationUnit(config, codeSource, getParent());
139         try {
140             ClassCollector collector = createCollector(unit);
141
142             unit.addClassNode(classNode);
143             unit.setClassgenCallback(collector);
144             unit.compile(Phases.CLASS_GENERATION);
145
146             return collector.generatedClass;
147         } catch (CompilationFailedException e) {
148             throw new RuntimeException JavaDoc(e);
149         }
150     }
151
152     /**
153      * Parses the given file into a Java class capable of being run
154      *
155      * @param file the file name to parse
156      * @return the main class defined in the given script
157      */

158     public Class JavaDoc parseClass(File JavaDoc file) throws CompilationFailedException, IOException {
159         return parseClass(new GroovyCodeSource(file));
160     }
161
162     /**
163      * Parses the given text into a Java class capable of being run
164      *
165      * @param text the text of the script/class to parse
166      * @param fileName the file name to use as the name of the class
167      * @return the main class defined in the given script
168      */

169     public Class JavaDoc parseClass(String JavaDoc text, String JavaDoc fileName) throws CompilationFailedException {
170         return parseClass(new ByteArrayInputStream(text.getBytes()), fileName);
171     }
172
173     /**
174      * Parses the given text into a Java class capable of being run
175      *
176      * @param text the text of the script/class to parse
177      * @return the main class defined in the given script
178      */

179     public Class JavaDoc parseClass(String JavaDoc text) throws CompilationFailedException {
180         return parseClass(new ByteArrayInputStream(text.getBytes()), "script" + System.currentTimeMillis() + ".groovy");
181     }
182
183     /**
184      * Parses the given character stream into a Java class capable of being run
185      *
186      * @param in an InputStream
187      * @return the main class defined in the given script
188      */

189     public Class JavaDoc parseClass(InputStream in) throws CompilationFailedException {
190         return parseClass(in, "script" + System.currentTimeMillis() + ".groovy");
191     }
192
193     public Class JavaDoc parseClass(final InputStream in, final String JavaDoc fileName) throws CompilationFailedException {
194         //For generic input streams, provide a catch-all codebase of
195
// GroovyScript
196
//Security for these classes can be administered via policy grants with
197
// a codebase
198
//of file:groovy.script
199
GroovyCodeSource gcs = (GroovyCodeSource) AccessController.doPrivileged(new PrivilegedAction() {
200             public Object JavaDoc run() {
201                 return new GroovyCodeSource(in, fileName, "/groovy/script");
202             }
203         });
204         return parseClass(gcs);
205     }
206
207
208     public Class JavaDoc parseClass(GroovyCodeSource codeSource) throws CompilationFailedException {
209         return parseClass(codeSource, true);
210     }
211
212     /**
213      * Parses the given code source into a Java class capable of being run
214      *
215      * @return the main class defined in the given script
216      */

217     public Class JavaDoc parseClass(GroovyCodeSource codeSource, boolean shouldCache) throws CompilationFailedException {
218         String JavaDoc name = codeSource.getName();
219         Class JavaDoc answer = null;
220         //ASTBuilder.resolveName can call this recursively -- for example when
221
// resolving a Constructor
222
//invocation for a class that is currently being compiled.
223
synchronized (cache) {
224             answer = (Class JavaDoc) cache.get(name);
225             if (answer != null) {
226                 return (answer == PARSING.class ? null : answer);
227             } else {
228                 cache.put(name, PARSING.class);
229             }
230         }
231         //Was neither already loaded nor compiling, so compile and add to
232
// cache.
233
try {
234             CompilationUnit unit = new CompilationUnit(config, codeSource.getCodeSource(), this);
235             // try {
236
ClassCollector collector = createCollector(unit);
237
238             if (codeSource.getFile()==null) {
239                 unit.addSource(name, codeSource.getInputStream());
240             } else {
241                 unit.addSource(codeSource.getFile());
242             }
243             unit.setClassgenCallback(collector);
244             int goalPhase = Phases.CLASS_GENERATION;
245             if (config != null && config.getTargetDirectory()!=null) goalPhase = Phases.OUTPUT;
246             unit.compile(goalPhase);
247
248             answer = collector.generatedClass;
249             // }
250
// catch( CompilationFailedException e ) {
251
// throw new RuntimeException( e );
252
// }
253
} finally {
254             synchronized (cache) {
255                 if (answer == null || !shouldCache) {
256                     cache.remove(name);
257                 } else {
258                     cache.put(name, answer);
259                 }
260             }
261             try {
262                 codeSource.getInputStream().close();
263             } catch (IOException e) {
264                 throw new GroovyRuntimeException("unable to close stream",e);
265             }
266         }
267         return answer;
268     }
269
270     /**
271      * Using this classloader you can load groovy classes from the system
272      * classpath as though they were already compiled. Note that .groovy classes
273      * found with this mechanism need to conform to the standard java naming
274      * convention - i.e. the public class inside the file must match the
275      * filename and the file must be located in a directory structure that
276      * matches the package structure.
277      */

278     /*protected Class findClass(final String name) throws ClassNotFoundException {
279         SecurityManager sm = System.getSecurityManager();
280         if (sm != null) {
281             String className = name.replace('/', '.');
282             int i = className.lastIndexOf('.');
283             if (i != -1) {
284                 sm.checkPackageDefinition(className.substring(0, i));
285             }
286         }
287         try {
288             return (Class) AccessController.doPrivileged(new PrivilegedExceptionAction() {
289                 public Object run() throws ClassNotFoundException {
290                     return findGroovyClass(name);
291                 }
292             });
293         } catch (PrivilegedActionException pae) {
294             throw (ClassNotFoundException) pae.getException();
295         }
296     }*/

297
298     protected Class JavaDoc findGroovyClass(String JavaDoc name) throws ClassNotFoundException JavaDoc {
299         //Use a forward slash here for the path separator. It will work as a
300
// separator
301
//for the File class on all platforms, AND it is required as a jar file
302
// entry separator.
303
String JavaDoc filename = name.replace('.', '/') + ".groovy";
304         String JavaDoc[] paths = getClassPath();
305         // put the absolute classname in a File object so we can easily
306
// pluck off the class name and the package path
307
File JavaDoc classnameAsFile = new File JavaDoc(filename);
308         // pluck off the classname without the package
309
String JavaDoc classname = classnameAsFile.getName();
310         String JavaDoc pkg = classnameAsFile.getParent();
311         String JavaDoc pkgdir;
312         for (int i = 0; i < paths.length; i++) {
313             String JavaDoc pathName = paths[i];
314             File JavaDoc path = new File JavaDoc(pathName);
315             if (path.exists()) {
316                 if (path.isDirectory()) {
317                     // patch to fix case preserving but case insensitive file
318
// systems (like macosx)
319
// JIRA issue 414
320
//
321
// first see if the file even exists, no matter what the
322
// case is
323
File JavaDoc nocasefile = new File JavaDoc(path, filename);
324                     if (!nocasefile.exists())
325                         continue;
326
327                     // now we know the file is there is some form or another, so
328
// let's look up all the files to see if the one we're
329
// really
330
// looking for is there
331
if (pkg == null)
332                         pkgdir = pathName;
333                     else
334                         pkgdir = pathName + "/" + pkg;
335                     File JavaDoc pkgdirF = new File JavaDoc(pkgdir);
336                     // make sure the resulting path is there and is a dir
337
if (pkgdirF.exists() && pkgdirF.isDirectory()) {
338                         File JavaDoc files[] = pkgdirF.listFiles();
339                         for (int j = 0; j < files.length; j++) {
340                             // do the case sensitive comparison
341
if (files[j].getName().equals(classname)) {
342                                 try {
343                                     return parseClass(files[j]);
344                                 } catch (CompilationFailedException e) {
345                                     throw new ClassNotFoundException JavaDoc("Syntax error in groovy file: " + files[j].getAbsolutePath(), e);
346                                 } catch (IOException e) {
347                                     throw new ClassNotFoundException JavaDoc("Error reading groovy file: " + files[j].getAbsolutePath(), e);
348                                 }
349                             }
350                         }
351                     }
352                 } else {
353                     try {
354                         JarFile JavaDoc jarFile = new JarFile JavaDoc(path);
355                         JarEntry JavaDoc entry = jarFile.getJarEntry(filename);
356                         if (entry != null) {
357                             byte[] bytes = extractBytes(jarFile, entry);
358                             Certificate JavaDoc[] certs = entry.getCertificates();
359                             try {
360                                 return parseClass(new GroovyCodeSource(new ByteArrayInputStream(bytes), filename, path, certs));
361                             } catch (CompilationFailedException e1) {
362                                 throw new ClassNotFoundException JavaDoc("Syntax error in groovy file: " + filename, e1);
363                             }
364                         }
365
366                     } catch (IOException e) {
367                         // Bad jar in classpath, ignore
368
}
369                 }
370             }
371         }
372         throw new ClassNotFoundException JavaDoc(name);
373     }
374
375     //Read the bytes from a non-null JarEntry. This is done here because the
376
// entry must be read completely
377
//in order to get verified certificates, which can only be obtained after a
378
// full read.
379
private byte[] extractBytes(JarFile JavaDoc jarFile, JarEntry JavaDoc entry) {
380         ByteArrayOutputStream baos = new ByteArrayOutputStream();
381         int b;
382         try {
383             BufferedInputStream bis = new BufferedInputStream(jarFile.getInputStream(entry));
384             while ((b = bis.read()) != -1) {
385                 baos.write(b);
386             }
387         } catch (IOException ioe) {
388             throw new GroovyRuntimeException("Could not read the jar bytes for " + entry.getName());
389         }
390         return baos.toByteArray();
391     }
392
393       /**
394        * Workaround for Groovy-835
395        */

396       protected String JavaDoc[] getClassPath() {
397         if (null == _searchPaths) {
398           final String JavaDoc classpath;
399           if(null != config && null != config.getClasspath()) {
400             //there's probably a better way to do this knowing the internals of
401
//Groovy, but it works for now
402
final List paths = config.getClasspath();
403             final StringBuffer JavaDoc sb = new StringBuffer JavaDoc();
404             for(Iterator iter = paths.iterator(); iter.hasNext(); ) {
405               sb.append(iter.next().toString());
406               sb.append(File.pathSeparatorChar);
407             }
408             //remove extra path separator
409
sb.deleteCharAt(sb.length()-1);
410             classpath = sb.toString();
411           } else {
412             classpath = System.getProperty("java.class.path", ".");
413           }
414           final List pathList = new ArrayList();
415           expandClassPath(pathList, null, classpath);
416           _searchPaths = new String JavaDoc[pathList.size()];
417           _searchPaths = (String JavaDoc[]) pathList.toArray(_searchPaths);
418         }
419         return _searchPaths;
420       }
421
422     /**
423      * @param pathList
424      * @param classpath
425      */

426     protected void expandClassPath(List pathList, String JavaDoc base, String JavaDoc classpath) {
427
428         // checking against null prevents an NPE when recursevely expanding the
429
// classpath
430
// in case the classpath is malformed
431
if (classpath != null) {
432
433             // Sun's convention for the class-path attribute is to seperate each
434
// entry with spaces
435
// but some libraries don't respect that convention and add commas,
436
// colons, semi-colons
437
String JavaDoc[] paths = classpath.split("[\\ ,:;]");
438
439             for (int i = 0; i < paths.length; i++) {
440                 if (paths.length > 0) {
441                     File JavaDoc path = null;
442
443                     if ("".equals(base)) {
444                         path = new File JavaDoc(paths[i]);
445                     } else {
446                         path = new File JavaDoc(base, paths[i]);
447                     }
448
449                     if (path.exists()) {
450                         if (!path.isDirectory()) {
451                             try {
452                                 JarFile JavaDoc jar = new JarFile JavaDoc(path);
453                                 pathList.add(paths[i]);
454
455                                 Manifest JavaDoc manifest = jar.getManifest();
456                                 if (manifest != null) {
457                                     Attributes JavaDoc classPathAttributes = manifest.getMainAttributes();
458                                     String JavaDoc manifestClassPath = classPathAttributes.getValue("Class-Path");
459
460                                     if (manifestClassPath != null)
461                                         expandClassPath(pathList, paths[i], manifestClassPath);
462                                 }
463                             } catch (IOException e) {
464                                 // Bad jar, ignore
465
continue;
466                             }
467                         } else {
468                             pathList.add(paths[i]);
469                         }
470                     }
471                 }
472             }
473         }
474     }
475
476     /**
477      * A helper method to allow bytecode to be loaded. spg changed name to
478      * defineClass to make it more consistent with other ClassLoader methods
479      */

480     protected Class JavaDoc defineClass(String JavaDoc name, byte[] bytecode, ProtectionDomain domain) {
481         return defineClass(name, bytecode, 0, bytecode.length, domain);
482     }
483
484     protected ClassCollector createCollector(CompilationUnit unit) {
485         return new ClassCollector(this, unit);
486     }
487
488     public static class ClassCollector extends CompilationUnit.ClassgenCallback {
489         private Class JavaDoc generatedClass;
490
491         private GroovyClassLoader cl;
492
493         private CompilationUnit unit;
494
495         protected ClassCollector(GroovyClassLoader cl, CompilationUnit unit) {
496             this.cl = cl;
497             this.unit = unit;
498         }
499
500         protected Class JavaDoc onClassNode(ClassWriter classWriter, ClassNode classNode) {
501             byte[] code = classWriter.toByteArray();
502
503             Class JavaDoc theClass = cl.defineClass(classNode.getName(), code, 0, code.length, unit.getAST().getCodeSource());
504             
505             if (generatedClass == null) {
506                 generatedClass = theClass;
507             }
508
509             return theClass;
510         }
511
512         public void call(ClassVisitor classWriter, ClassNode classNode) {
513             onClassNode((ClassWriter) classWriter, classNode);
514         }
515     }
516
517     /**
518      * open up the super class define that takes raw bytes
519      *
520      */

521     public Class JavaDoc defineClass(String JavaDoc name, byte[] b) {
522         return super.defineClass(name, b, 0, b.length);
523     }
524
525     /*
526      * (non-Javadoc)
527      *
528      * @see java.lang.ClassLoader#loadClass(java.lang.String, boolean)
529      * Implemented here to check package access prior to returning an
530      * already loaded class. todo : br shall we search for the source
531      * groovy here to see if the soource file has been updated first?
532      */

533     protected synchronized Class JavaDoc loadClass(final String JavaDoc name, boolean resolve) throws ClassNotFoundException JavaDoc {
534         synchronized (cache) {
535             Class JavaDoc cls = (Class JavaDoc) cache.get(name);
536             if (cls == NOT_RESOLVED.class) throw new ClassNotFoundException JavaDoc(name);
537             if (cls!=null) return cls;
538         }
539         
540         SecurityManager JavaDoc sm = System.getSecurityManager();
541         if (sm != null) {
542             String JavaDoc className = name.replace('/', '.');
543             int i = className.lastIndexOf('.');
544             if (i != -1) {
545                 sm.checkPackageAccess(className.substring(0, i));
546             }
547         }
548         
549         Class JavaDoc cls = null;
550         ClassNotFoundException JavaDoc last = null;
551         try {
552             cls = super.loadClass(name, resolve);
553     
554             boolean recompile = false;
555             if (getTimeStamp(cls) < Long.MAX_VALUE) {
556                 Class JavaDoc[] inters = cls.getInterfaces();
557                 for (int i = 0; i < inters.length; i++) {
558                     if (inters[i].getName().equals(GroovyObject.class.getName())) {
559                         recompile=true;
560                         break;
561                     }
562                 }
563             }
564             if (!recompile) return cls;
565         } catch (ClassNotFoundException JavaDoc cnfe) {
566             last = cnfe;
567         }
568         
569         // try groovy file
570
try {
571             File JavaDoc source = (File JavaDoc) AccessController.doPrivileged(new PrivilegedAction() {
572                 public Object JavaDoc run() {
573                     return getSourceFile(name);
574                 }
575             });
576             if (source != null) {
577                 if ((cls!=null && isSourceNewer(source, cls)) || (cls==null)) {
578                     cls = parseClass(source);
579                 }
580             }
581         } catch (Exception JavaDoc e) {
582             cls = null;
583             last = new ClassNotFoundException JavaDoc("Failed to parse groovy file: " + name, e);
584         }
585         if (cls==null) {
586             if (last==null) throw new AssertionError JavaDoc(true);
587             synchronized (cache) {
588                 cache.put(name, NOT_RESOLVED.class);
589             }
590             throw last;
591         }
592         synchronized (cache) {
593             cache.put(name, cls);
594         }
595         return cls;
596     }
597
598     private long getTimeStamp(Class JavaDoc cls) {
599         Field JavaDoc field;
600         Long JavaDoc o;
601         try {
602             field = cls.getField(Verifier.__TIMESTAMP);
603             o = (Long JavaDoc) field.get(null);
604         } catch (Exception JavaDoc e) {
605             //throw new RuntimeException(e);
606
return Long.MAX_VALUE;
607         }
608         return o.longValue();
609     }
610
611     // static class ClassWithTimeTag {
612
// final static ClassWithTimeTag NOT_RESOLVED = new ClassWithTimeTag(null,
613
// 0);
614
// Class cls;
615
// long lastModified;
616
//
617
// public ClassWithTimeTag(Class cls, long lastModified) {
618
// this.cls = cls;
619
// this.lastModified = lastModified;
620
// }
621
// }
622

623     private File JavaDoc getSourceFile(String JavaDoc name) {
624         File JavaDoc source = null;
625         String JavaDoc filename = name.replace('.', '/') + ".groovy";
626         String JavaDoc[] paths = getClassPath();
627         for (int i = 0; i < paths.length; i++) {
628             String JavaDoc pathName = paths[i];
629             File JavaDoc path = new File JavaDoc(pathName);
630             if (path.exists()) { // case sensitivity depending on OS!
631
if (path.isDirectory()) {
632                     File JavaDoc file = new File JavaDoc(path, filename);
633                     if (file.exists()) {
634                         // file.exists() might be case insensitive. Let's do
635
// case sensitive match for the filename
636
boolean fileExists = false;
637                         int sepp = filename.lastIndexOf('/');
638                         String JavaDoc fn = filename;
639                         if (sepp >= 0) {
640                             fn = filename.substring(++sepp);
641                         }
642                         File JavaDoc parent = file.getParentFile();
643                         String JavaDoc[] files = parent.list();
644                         for (int j = 0; j < files.length; j++) {
645                             if (files[j].equals(fn)) {
646                                 fileExists = true;
647                                 break;
648                             }
649                         }
650
651                         if (fileExists) {
652                             source = file;
653                             break;
654                         }
655                     }
656                 }
657             }
658         }
659         return source;
660     }
661
662     private boolean isSourceNewer(File JavaDoc source, Class JavaDoc cls) {
663         return source.lastModified() > getTimeStamp(cls);
664     }
665
666     public void addClasspath(String JavaDoc path) {
667         additionalPaths.add(path);
668         searchPaths = null;
669     }
670 }
671
Popular Tags