KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > apache > tools > ant > taskdefs > Ant


1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one or more
3  * contributor license agreements. See the NOTICE file distributed with
4  * this work for additional information regarding copyright ownership.
5  * The ASF licenses this file to You under the Apache License, Version 2.0
6  * (the "License"); you may not use this file except in compliance with
7  * the License. You may obtain a copy of the License at
8  *
9  * http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  *
17  */

18
19 package org.apache.tools.ant.taskdefs;
20
21 import java.io.File JavaDoc;
22 import java.io.FileOutputStream JavaDoc;
23 import java.io.IOException JavaDoc;
24 import java.io.PrintStream JavaDoc;
25 import java.lang.reflect.Method JavaDoc;
26 import java.util.Enumeration JavaDoc;
27 import java.util.Hashtable JavaDoc;
28 import java.util.Iterator JavaDoc;
29 import java.util.Vector JavaDoc;
30 import java.util.Set JavaDoc;
31 import java.util.HashSet JavaDoc;
32 import org.apache.tools.ant.BuildException;
33 import org.apache.tools.ant.BuildListener;
34 import org.apache.tools.ant.DefaultLogger;
35 import org.apache.tools.ant.Project;
36 import org.apache.tools.ant.ProjectComponent;
37 import org.apache.tools.ant.ProjectHelper;
38 import org.apache.tools.ant.Target;
39 import org.apache.tools.ant.Task;
40 import org.apache.tools.ant.MagicNames;
41 import org.apache.tools.ant.Main;
42 import org.apache.tools.ant.types.PropertySet;
43 import org.apache.tools.ant.util.FileUtils;
44
45 /**
46  * Build a sub-project.
47  *
48  * <pre>
49  * &lt;target name=&quot;foo&quot; depends=&quot;init&quot;&gt;
50  * &lt;ant antfile=&quot;build.xml&quot; target=&quot;bar&quot; &gt;
51  * &lt;property name=&quot;property1&quot; value=&quot;aaaaa&quot; /&gt;
52  * &lt;property name=&quot;foo&quot; value=&quot;baz&quot; /&gt;
53  * &lt;/ant&gt;</span>
54  * &lt;/target&gt;</span>
55  *
56  * &lt;target name=&quot;bar&quot; depends=&quot;init&quot;&gt;
57  * &lt;echo message=&quot;prop is ${property1} ${foo}&quot; /&gt;
58  * &lt;/target&gt;
59  * </pre>
60  *
61  *
62  * @since Ant 1.1
63  *
64  * @ant.task category="control"
65  */

66 public class Ant extends Task {
67
68     private static final FileUtils FILE_UTILS = FileUtils.getFileUtils();
69
70     /** the basedir where is executed the build file */
71     private File JavaDoc dir = null;
72
73     /**
74      * the build.xml file (can be absolute) in this case dir will be
75      * ignored
76      */

77     private String JavaDoc antFile = null;
78
79     /** the output */
80     private String JavaDoc output = null;
81
82     /** should we inherit properties from the parent ? */
83     private boolean inheritAll = true;
84
85     /** should we inherit references from the parent ? */
86     private boolean inheritRefs = false;
87
88     /** the properties to pass to the new project */
89     private Vector JavaDoc properties = new Vector JavaDoc();
90
91     /** the references to pass to the new project */
92     private Vector JavaDoc references = new Vector JavaDoc();
93
94     /** the temporary project created to run the build file */
95     private Project newProject;
96
97     /** The stream to which output is to be written. */
98     private PrintStream JavaDoc out = null;
99
100     /** the sets of properties to pass to the new project */
101     private Vector JavaDoc propertySets = new Vector JavaDoc();
102
103     /** the targets to call on the new project */
104     private Vector JavaDoc targets = new Vector JavaDoc();
105
106     /** whether the target attribute was specified **/
107     private boolean targetAttributeSet = false;
108
109     /**
110      * simple constructor
111      */

112     public Ant() {
113         //default
114
}
115
116     /**
117      * create a task bound to its creator
118      * @param owner owning task
119      */

120     public Ant(Task owner) {
121         bindToOwner(owner);
122     }
123
124
125     /**
126      * If true, pass all properties to the new Ant project.
127      * Defaults to true.
128      * @param value if true pass all properties to the new Ant project.
129      */

130     public void setInheritAll(boolean value) {
131         inheritAll = value;
132     }
133
134     /**
135      * If true, pass all references to the new Ant project.
136      * Defaults to false.
137      * @param value if true, pass all references to the new Ant project
138      */

139     public void setInheritRefs(boolean value) {
140         inheritRefs = value;
141     }
142
143     /**
144      * Creates a Project instance for the project to call.
145      */

146     public void init() {
147         newProject = getProject().createSubProject();
148         newProject.setJavaVersionProperty();
149     }
150
151     /**
152      * Called in execute or createProperty (via getNewProject())
153      * if newProject is null.
154      *
155      * <p>This can happen if the same instance of this task is run
156      * twice as newProject is set to null at the end of execute (to
157      * save memory and help the GC).</p>
158      * <p>calls init() again</p>
159      *
160      */

161     private void reinit() {
162         init();
163     }
164
165     /**
166      * Attaches the build listeners of the current project to the new
167      * project, configures a possible logfile, transfers task and
168      * data-type definitions, transfers properties (either all or just
169      * the ones specified as user properties to the current project,
170      * depending on inheritall), transfers the input handler.
171      */

172     private void initializeProject() {
173         newProject.setInputHandler(getProject().getInputHandler());
174
175         Iterator JavaDoc iter = getBuildListeners();
176         while (iter.hasNext()) {
177             newProject.addBuildListener((BuildListener) iter.next());
178         }
179
180         if (output != null) {
181             File JavaDoc outfile = null;
182             if (dir != null) {
183                 outfile = FILE_UTILS.resolveFile(dir, output);
184             } else {
185                 outfile = getProject().resolveFile(output);
186             }
187             try {
188                 out = new PrintStream JavaDoc(new FileOutputStream JavaDoc(outfile));
189                 DefaultLogger logger = new DefaultLogger();
190                 logger.setMessageOutputLevel(Project.MSG_INFO);
191                 logger.setOutputPrintStream(out);
192                 logger.setErrorPrintStream(out);
193                 newProject.addBuildListener(logger);
194             } catch (IOException JavaDoc ex) {
195                 log("Ant: Can't set output to " + output);
196             }
197         }
198         // set user-defined properties
199
getProject().copyUserProperties(newProject);
200
201         if (!inheritAll) {
202            // set Java built-in properties separately,
203
// b/c we won't inherit them.
204
newProject.setSystemProperties();
205
206         } else {
207             // set all properties from calling project
208
addAlmostAll(getProject().getProperties());
209         }
210
211         Enumeration JavaDoc e = propertySets.elements();
212         while (e.hasMoreElements()) {
213             PropertySet ps = (PropertySet) e.nextElement();
214             addAlmostAll(ps.getProperties());
215         }
216     }
217
218     /**
219      * Handles output.
220      * Send it the the new project if is present, otherwise
221      * call the super class.
222      * @param outputToHandle The string output to output.
223      * @see Task#handleOutput(String)
224      * @since Ant 1.5
225      */

226     public void handleOutput(String JavaDoc outputToHandle) {
227         if (newProject != null) {
228             newProject.demuxOutput(outputToHandle, false);
229         } else {
230             super.handleOutput(outputToHandle);
231         }
232     }
233
234     /**
235      * Handles input.
236      * Deleate to the created project, if present, otherwise
237      * call the super class.
238      * @param buffer the buffer into which data is to be read.
239      * @param offset the offset into the buffer at which data is stored.
240      * @param length the amount of data to read.
241      *
242      * @return the number of bytes read.
243      *
244      * @exception IOException if the data cannot be read.
245      * @see Task#handleInput(byte[], int, int)
246      * @since Ant 1.6
247      */

248     public int handleInput(byte[] buffer, int offset, int length)
249         throws IOException JavaDoc {
250         if (newProject != null) {
251             return newProject.demuxInput(buffer, offset, length);
252         }
253         return super.handleInput(buffer, offset, length);
254     }
255
256     /**
257      * Handles output.
258      * Send it the the new project if is present, otherwise
259      * call the super class.
260      * @param toFlush The string to output.
261      * @see Task#handleFlush(String)
262      * @since Ant 1.5.2
263      */

264     public void handleFlush(String JavaDoc toFlush) {
265         if (newProject != null) {
266             newProject.demuxFlush(toFlush, false);
267         } else {
268             super.handleFlush(toFlush);
269         }
270     }
271
272     /**
273      * Handle error output.
274      * Send it the the new project if is present, otherwise
275      * call the super class.
276      * @param errorOutputToHandle The string to output.
277      *
278      * @see Task#handleErrorOutput(String)
279      * @since Ant 1.5
280      */

281     public void handleErrorOutput(String JavaDoc errorOutputToHandle) {
282         if (newProject != null) {
283             newProject.demuxOutput(errorOutputToHandle, true);
284         } else {
285             super.handleErrorOutput(errorOutputToHandle);
286         }
287     }
288
289     /**
290      * Handle error output.
291      * Send it the the new project if is present, otherwise
292      * call the super class.
293      * @param errorOutputToFlush The string to output.
294      * @see Task#handleErrorFlush(String)
295      * @since Ant 1.5.2
296      */

297     public void handleErrorFlush(String JavaDoc errorOutputToFlush) {
298         if (newProject != null) {
299             newProject.demuxFlush(errorOutputToFlush, true);
300         } else {
301             super.handleErrorFlush(errorOutputToFlush);
302         }
303     }
304
305     /**
306      * Do the execution.
307      * @throws BuildException if a target tries to call itself;
308      * probably also if a BuildException is thrown by the new project.
309      */

310     public void execute() throws BuildException {
311         File JavaDoc savedDir = dir;
312         String JavaDoc savedAntFile = antFile;
313         Vector JavaDoc locals = new Vector JavaDoc(targets);
314         try {
315             getNewProject();
316
317             if (dir == null && inheritAll) {
318                 dir = getProject().getBaseDir();
319             }
320
321             initializeProject();
322
323             if (dir != null) {
324                 newProject.setBaseDir(dir);
325                 if (savedDir != null) {
326                     // has been set explicitly
327
newProject.setInheritedProperty(MagicNames.PROJECT_BASEDIR,
328                                                     dir.getAbsolutePath());
329                 }
330             } else {
331                 dir = getProject().getBaseDir();
332             }
333
334             overrideProperties();
335
336             if (antFile == null) {
337                 antFile = Main.DEFAULT_BUILD_FILENAME;
338             }
339
340             File JavaDoc file = FILE_UTILS.resolveFile(dir, antFile);
341             antFile = file.getAbsolutePath();
342
343             log("calling target(s) "
344                 + ((locals.size() > 0) ? locals.toString() : "[default]")
345                 + " in build file " + antFile, Project.MSG_VERBOSE);
346             newProject.setUserProperty(MagicNames.ANT_FILE , antFile);
347
348             String JavaDoc thisAntFile = getProject().getProperty(MagicNames.ANT_FILE);
349             // Are we trying to call the target in which we are defined (or
350
// the build file if this is a top level task)?
351
if (thisAntFile != null
352                 && file.equals(getProject().resolveFile(thisAntFile))
353                 && getOwningTarget() != null) {
354
355                 if (getOwningTarget().getName().equals("")) {
356                     if (getTaskName().equals("antcall")) {
357                         throw new BuildException("antcall must not be used at"
358                                                  + " the top level.");
359                     }
360                     throw new BuildException(getTaskName() + " task at the"
361                                 + " top level must not invoke"
362                                 + " its own build file.");
363                 }
364             }
365
366             try {
367                 ProjectHelper.configureProject(newProject, file);
368             } catch (BuildException ex) {
369                 throw ProjectHelper.addLocationToBuildException(
370                     ex, getLocation());
371             }
372
373             if (locals.size() == 0) {
374                 String JavaDoc defaultTarget = newProject.getDefaultTarget();
375                 if (defaultTarget != null) {
376                     locals.add(defaultTarget);
377                 }
378             }
379
380             if (newProject.getProperty(MagicNames.ANT_FILE)
381                 .equals(getProject().getProperty(MagicNames.ANT_FILE))
382                 && getOwningTarget() != null) {
383
384                 String JavaDoc owningTargetName = getOwningTarget().getName();
385
386                 if (locals.contains(owningTargetName)) {
387                     throw new BuildException(getTaskName() + " task calling "
388                                              + "its own parent target.");
389                 }
390                 boolean circular = false;
391                 for (Iterator JavaDoc it = locals.iterator();
392                      !circular && it.hasNext();) {
393                     Target other =
394                         (Target) (getProject().getTargets().get(it.next()));
395                     circular |= (other != null
396                                  && other.dependsOn(owningTargetName));
397                 }
398                 if (circular) {
399                     throw new BuildException(getTaskName()
400                                              + " task calling a target"
401                                              + " that depends on"
402                                              + " its parent target \'"
403                                              + owningTargetName
404                                              + "\'.");
405                 }
406             }
407
408             addReferences();
409
410             if (locals.size() > 0 && !(locals.size() == 1
411                                        && "".equals(locals.get(0)))) {
412                 BuildException be = null;
413                 try {
414                     log("Entering " + antFile + "...", Project.MSG_VERBOSE);
415                     newProject.fireSubBuildStarted();
416                     newProject.executeTargets(locals);
417                 } catch (BuildException ex) {
418                     be = ProjectHelper
419                         .addLocationToBuildException(ex, getLocation());
420                     throw be;
421                 } finally {
422                     log("Exiting " + antFile + ".", Project.MSG_VERBOSE);
423                     newProject.fireSubBuildFinished(be);
424                 }
425             }
426         } finally {
427             // help the gc
428
newProject = null;
429             Enumeration JavaDoc e = properties.elements();
430             while (e.hasMoreElements()) {
431                 Property p = (Property) e.nextElement();
432                 p.setProject(null);
433             }
434
435             if (output != null && out != null) {
436                 try {
437                     out.close();
438                 } catch (final Exception JavaDoc ex) {
439                     //ignore
440
}
441             }
442             dir = savedDir;
443             antFile = savedAntFile;
444         }
445     }
446
447     /**
448      * Override the properties in the new project with the one
449      * explicitly defined as nested elements here.
450      * @throws BuildException under unknown circumstances.
451      */

452     private void overrideProperties() throws BuildException {
453         // remove duplicate properties - last property wins
454
// Needed for backward compatibility
455
Set JavaDoc set = new HashSet JavaDoc();
456         for (int i = properties.size() - 1; i >= 0; --i) {
457             Property p = (Property) properties.get(i);
458             if (p.getName() != null && !p.getName().equals("")) {
459                 if (set.contains(p.getName())) {
460                     properties.remove(i);
461                 } else {
462                     set.add(p.getName());
463                 }
464             }
465         }
466         Enumeration JavaDoc e = properties.elements();
467         while (e.hasMoreElements()) {
468             Property p = (Property) e.nextElement();
469             p.setProject(newProject);
470             p.execute();
471         }
472         getProject().copyInheritedProperties(newProject);
473     }
474
475     /**
476      * Add the references explicitly defined as nested elements to the
477      * new project. Also copy over all references that don't override
478      * existing references in the new project if inheritrefs has been
479      * requested.
480      * @throws BuildException if a reference does not have a refid.
481      */

482     private void addReferences() throws BuildException {
483         Hashtable JavaDoc thisReferences
484             = (Hashtable JavaDoc) getProject().getReferences().clone();
485         Hashtable JavaDoc newReferences = newProject.getReferences();
486         Enumeration JavaDoc e;
487         if (references.size() > 0) {
488             for (e = references.elements(); e.hasMoreElements();) {
489                 Reference ref = (Reference) e.nextElement();
490                 String JavaDoc refid = ref.getRefId();
491                 if (refid == null) {
492                     throw new BuildException("the refid attribute is required"
493                                              + " for reference elements");
494                 }
495                 if (!thisReferences.containsKey(refid)) {
496                     log("Parent project doesn't contain any reference '"
497                         + refid + "'",
498                         Project.MSG_WARN);
499                     continue;
500                 }
501
502                 thisReferences.remove(refid);
503                 String JavaDoc toRefid = ref.getToRefid();
504                 if (toRefid == null) {
505                     toRefid = refid;
506                 }
507                 copyReference(refid, toRefid);
508             }
509         }
510
511         // Now add all references that are not defined in the
512
// subproject, if inheritRefs is true
513
if (inheritRefs) {
514             for (e = thisReferences.keys(); e.hasMoreElements();) {
515                 String JavaDoc key = (String JavaDoc) e.nextElement();
516                 if (newReferences.containsKey(key)) {
517                     continue;
518                 }
519                 copyReference(key, key);
520                 newProject.inheritIDReferences(getProject());
521             }
522         }
523     }
524
525     /**
526      * Try to clone and reconfigure the object referenced by oldkey in
527      * the parent project and add it to the new project with the key newkey.
528      *
529      * <p>If we cannot clone it, copy the referenced object itself and
530      * keep our fingers crossed.</p>
531      * @param oldKey the reference id in the current project.
532      * @param newKey the reference id in the new project.
533      */

534     private void copyReference(String JavaDoc oldKey, String JavaDoc newKey) {
535         Object JavaDoc orig = getProject().getReference(oldKey);
536         if (orig == null) {
537             log("No object referenced by " + oldKey + ". Can't copy to "
538                 + newKey,
539                 Project.MSG_WARN);
540             return;
541         }
542
543         Class JavaDoc c = orig.getClass();
544         Object JavaDoc copy = orig;
545         try {
546             Method JavaDoc cloneM = c.getMethod("clone", new Class JavaDoc[0]);
547             if (cloneM != null) {
548                 copy = cloneM.invoke(orig, new Object JavaDoc[0]);
549                 log("Adding clone of reference " + oldKey, Project.MSG_DEBUG);
550             }
551         } catch (Exception JavaDoc e) {
552             // not Clonable
553
}
554
555
556         if (copy instanceof ProjectComponent) {
557             ((ProjectComponent) copy).setProject(newProject);
558         } else {
559             try {
560                 Method JavaDoc setProjectM =
561                     c.getMethod("setProject", new Class JavaDoc[] {Project.class});
562                 if (setProjectM != null) {
563                     setProjectM.invoke(copy, new Object JavaDoc[] {newProject});
564                 }
565             } catch (NoSuchMethodException JavaDoc e) {
566                 // ignore this if the class being referenced does not have
567
// a set project method.
568
} catch (Exception JavaDoc e2) {
569                 String JavaDoc msg = "Error setting new project instance for "
570                     + "reference with id " + oldKey;
571                 throw new BuildException(msg, e2, getLocation());
572             }
573         }
574         newProject.addReference(newKey, copy);
575     }
576
577     /**
578      * Copies all properties from the given table to the new project -
579      * omitting those that have already been set in the new project as
580      * well as properties named basedir or ant.file.
581      * @param props properties <code>Hashtable</code> to copy to the
582      * new project.
583      * @since Ant 1.6
584      */

585     private void addAlmostAll(Hashtable JavaDoc props) {
586         Enumeration JavaDoc e = props.keys();
587         while (e.hasMoreElements()) {
588             String JavaDoc key = e.nextElement().toString();
589             if (MagicNames.PROJECT_BASEDIR.equals(key) || MagicNames.ANT_FILE.equals(key)) {
590                 // basedir and ant.file get special treatment in execute()
591
continue;
592             }
593
594             String JavaDoc value = props.get(key).toString();
595             // don't re-set user properties, avoid the warning message
596
if (newProject.getProperty(key) == null) {
597                 // no user property
598
newProject.setNewProperty(key, value);
599             }
600         }
601     }
602
603     /**
604      * The directory to use as a base directory for the new Ant project.
605      * Defaults to the current project's basedir, unless inheritall
606      * has been set to false, in which case it doesn't have a default
607      * value. This will override the basedir setting of the called project.
608      * @param dir new directory as <code>File</code>.
609      */

610     public void setDir(File JavaDoc dir) {
611         this.dir = dir;
612     }
613
614     /**
615      * The build file to use. Defaults to "build.xml". This file is expected
616      * to be a filename relative to the dir attribute given.
617      * @param antFile the <code>String</code> build file name.
618      */

619     public void setAntfile(String JavaDoc antFile) {
620         // @note: it is a string and not a file to handle relative/absolute
621
// otherwise a relative file will be resolved based on the current
622
// basedir.
623
this.antFile = antFile;
624     }
625
626     /**
627      * The target of the new Ant project to execute.
628      * Defaults to the new project's default target.
629      * @param targetToAdd the name of the target to invoke.
630      */

631     public void setTarget(String JavaDoc targetToAdd) {
632         if (targetToAdd.equals("")) {
633             throw new BuildException("target attribute must not be empty");
634         }
635         targets.add(targetToAdd);
636         targetAttributeSet = true;
637     }
638
639     /**
640      * Set the filename to write the output to. This is relative to the value
641      * of the dir attribute if it has been set or to the base directory of the
642      * current project otherwise.
643      * @param outputFile the name of the file to which the output should go.
644      */

645     public void setOutput(String JavaDoc outputFile) {
646         this.output = outputFile;
647     }
648
649     /**
650      * Property to pass to the new project.
651      * The property is passed as a 'user property'.
652      * @return the created <code>Property</code> object.
653      */

654     public Property createProperty() {
655         Property p = new Property(true, getProject());
656         p.setProject(getNewProject());
657         p.setTaskName("property");
658         properties.addElement(p);
659         return p;
660     }
661
662     /**
663      * Add a Reference element identifying a data type to carry
664      * over to the new project.
665      * @param ref <code>Reference</code> to add.
666      */

667     public void addReference(Reference ref) {
668         references.addElement(ref);
669     }
670
671     /**
672      * Add a target to this Ant invocation.
673      * @param t the <code>TargetElement</code> to add.
674      * @since Ant 1.6.3
675      */

676     public void addConfiguredTarget(TargetElement t) {
677         if (targetAttributeSet) {
678             throw new BuildException(
679                 "nested target is incompatible with the target attribute");
680         }
681         String JavaDoc name = t.getName();
682         if (name.equals("")) {
683             throw new BuildException("target name must not be empty");
684         }
685         targets.add(name);
686     }
687
688     /**
689      * Add a set of properties to pass to the new project.
690      *
691      * @param ps <code>PropertySet</code> to add.
692      * @since Ant 1.6
693      */

694     public void addPropertyset(PropertySet ps) {
695         propertySets.addElement(ps);
696     }
697
698     /*
699      * Get the (sub)-Project instance currently in use.
700      * @return Project
701      * @since Ant 1.7
702      */

703     protected Project getNewProject() {
704         if (newProject == null) {
705             reinit();
706         }
707         return newProject;
708     }
709
710     /**
711      * @since Ant 1.6.2
712      */

713     private Iterator JavaDoc getBuildListeners() {
714         return getProject().getBuildListeners().iterator();
715     }
716
717     /**
718      * Helper class that implements the nested &lt;reference&gt;
719      * element of &lt;ant&gt; and &lt;antcall&gt;.
720      */

721     public static class Reference
722         extends org.apache.tools.ant.types.Reference {
723
724         /** Creates a reference to be configured by Ant. */
725         public Reference() {
726                 super();
727         }
728
729         private String JavaDoc targetid = null;
730
731         /**
732          * Set the id that this reference to be stored under in the
733          * new project.
734          *
735          * @param targetid the id under which this reference will be passed to
736          * the new project. */

737         public void setToRefid(String JavaDoc targetid) {
738             this.targetid = targetid;
739         }
740
741         /**
742          * Get the id under which this reference will be stored in the new
743          * project.
744          *
745          * @return the id of the reference in the new project.
746          */

747         public String JavaDoc getToRefid() {
748             return targetid;
749         }
750     }
751
752     /**
753      * Helper class that implements the nested &lt;target&gt;
754      * element of &lt;ant&gt; and &lt;antcall&gt;.
755      * @since Ant 1.6.3
756      */

757     public static class TargetElement {
758         private String JavaDoc name;
759
760         /**
761          * Default constructor.
762          */

763         public TargetElement() {
764                 //default
765
}
766
767         /**
768          * Set the name of this TargetElement.
769          * @param name the <code>String</code> target name.
770          */

771         public void setName(String JavaDoc name) {
772             this.name = name;
773         }
774
775         /**
776          * Get the name of this TargetElement.
777          * @return <code>String</code>.
778          */

779         public String JavaDoc getName() {
780             return name;
781         }
782     }
783 }
784
Popular Tags