KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > openide > filesystems > XMLFileSystem


1 /*
2  * The contents of this file are subject to the terms of the Common Development
3  * and Distribution License (the License). You may not use this file except in
4  * compliance with the License.
5  *
6  * You can obtain a copy of the License at http://www.netbeans.org/cddl.html
7  * or http://www.netbeans.org/cddl.txt.
8  *
9  * When distributing Covered Code, include this CDDL Header Notice in each file
10  * and include the License file at http://www.netbeans.org/cddl.txt.
11  * If applicable, add the following below the CDDL Header, with the fields
12  * enclosed by brackets [] replaced by your own identifying information:
13  * "Portions Copyrighted [year] [name of copyright owner]"
14  *
15  * The Original Software is NetBeans. The Initial Developer of the Original
16  * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
17  * Microsystems, Inc. All Rights Reserved.
18  */

19
20 package org.openide.filesystems;
21
22 import java.beans.PropertyVetoException JavaDoc;
23 import java.io.ByteArrayInputStream JavaDoc;
24 import java.io.File JavaDoc;
25 import java.io.FileNotFoundException JavaDoc;
26 import java.io.IOException JavaDoc;
27 import java.io.InputStream JavaDoc;
28 import java.io.ObjectInputStream JavaDoc;
29 import java.io.OutputStream JavaDoc;
30 import java.lang.ref.Reference JavaDoc;
31 import java.lang.ref.WeakReference JavaDoc;
32 import java.net.MalformedURLException JavaDoc;
33 import java.net.URI JavaDoc;
34 import java.net.URL JavaDoc;
35 import java.net.URLConnection JavaDoc;
36 import java.util.ArrayList JavaDoc;
37 import java.util.Arrays JavaDoc;
38 import java.util.Collection JavaDoc;
39 import java.util.Collections JavaDoc;
40 import java.util.Date JavaDoc;
41 import java.util.Enumeration JavaDoc;
42 import java.util.HashMap JavaDoc;
43 import java.util.HashSet JavaDoc;
44 import java.util.Iterator JavaDoc;
45 import java.util.Map JavaDoc;
46 import java.util.Set JavaDoc;
47 import java.util.Stack JavaDoc;
48 import org.openide.util.Enumerations;
49 import org.openide.util.NbBundle;
50 import org.openide.xml.XMLUtil;
51 import org.xml.sax.Attributes JavaDoc;
52 import org.xml.sax.InputSource JavaDoc;
53 import org.xml.sax.SAXException JavaDoc;
54 import org.xml.sax.SAXParseException JavaDoc;
55 import org.xml.sax.XMLReader JavaDoc;
56 import org.xml.sax.helpers.DefaultHandler JavaDoc;
57
58 /** XML-based filesystem.
59  * <PRE>
60  * Description of format of XML file (which can be parsed by XMLFileSystem)
61  * ==================================================================
62  * Allowed Elements: filesystem,file,folder,attr
63  *
64  * Mandatory attributes:
65  * -for filesystem version=... (e.g. "1.0")
66  * -for file,folder,attr name=.... (e.g.: &lt;folder name="Config"&gt;)
67  * -for attr is mandatory one of bytevalue,shortvalue,intvalue,longvalue,floatvalue,doublevalue,boolvalue,charvalue,stringvalue,methodvalue,serialvalue,urlvalue
68  *
69  * Allowed atributes:
70  * -for file: url=.... (e.g.: &lt;file name="sample.xml" url="file:/c:/sample.xml"&gt;)
71  * -for folder,filesystem nothing allowed
72  *
73  *
74  *
75  * Note: file can contain content e.g.:
76  * &lt; file name="sample.java"&gt;
77  * &lt; ![CDATA[
78  * package org.sample;
79  * import java.io;
80  * ]]&gt;
81  * &lt; /file&gt;
82  * But using url="..." is preferred.
83  *
84  *
85  * This class implements virtual FileSystem. It is special case of FileSystem in XML format.
86  *
87  * Description of this format best ilustrate DTD file that is showed in next lines:
88  * &lt; !ELEMENT filesystem (file | folder)*&gt;
89  * &lt; !ATTLIST filesystem version CDATA #REQUIRED&gt; //version not checkked yet
90  * &lt; !ELEMENT folder (file |folder | attr)*&gt;
91  * &lt; !ATTLIST folder name CDATA #REQUIRED&gt; //name of folder
92  * &lt; !ELEMENT file (#PCDATA | attr)*&gt;
93  * &lt; !ATTLIST file name CDATA #REQUIRED&gt; //name of file
94  * &lt; !ATTLIST file url CDATA #IMPLIED&gt; //content of the file can be find at url
95  * &lt; !ELEMENT attr EMPTY&gt;
96  * &lt; !ATTLIST attr name CDATA #REQUIRED&gt; //name of attribute
97  * &lt; !ATTLIST attr bytevalue CDATA #IMPLIED&gt;//the rest - types of attributes
98  * &lt; !ATTLIST attr shortvalue CDATA #IMPLIED&gt;
99  * &lt; !ATTLIST attr intvalue CDATA #IMPLIED&gt;
100  * &lt; !ATTLIST attribute longvalue CDATA #IMPLIED&gt;
101  * &lt; !ATTLIST attr floatvalue CDATA #IMPLIED&gt;
102  * &lt; !ATTLIST attr doublevalue CDATA #IMPLIED&gt;
103  * &lt; !ATTLIST attr boolvalue CDATA #IMPLIED&gt;
104  * &lt; !ATTLIST attr charvalue CDATA #IMPLIED&gt;
105  * &lt; !ATTLIST attr stringvalue CDATA #IMPLIED&gt;
106  * &lt; !ATTLIST attr methodvalue CDATA #IMPLIED&gt;
107  * &lt; !ATTLIST attr serialvalue CDATA #IMPLIED&gt;
108  * &lt; !ATTLIST attr urlvalue CDATA #IMPLIED&gt;
109  * </PRE>
110  *
111  * <p>
112  * The methodvalue attribute can be in form of <code>pgk1.pkg2.ClassName.methodName</code>
113  * which should point to existing class with static method usually having no, one
114  * or two arguments. This method does not need to be public or in public class, if
115  * the filesystem has permissions to call the method. The method can take one
116  * of the following signatures:
117  * <pre>
118  * static Value methodName();
119  * static Value methodName(FileObject fo);
120  * static Value methodName(FileObject fo, String attrName);
121  * static Value methodName(Map<String,Object> attrs); // since 7.0
122  * static Value methodName(Map<String,Object> attrs, String attrName); // since 7.0
123  * </pre>
124  * where <code>Value</code> can be any java type.
125  *
126  *
127  * @author Radek Matous
128  */

129 public final class XMLFileSystem extends AbstractFileSystem {
130     static final long serialVersionUID = 28974107313702326L;
131
132     // <?xml version="1.0"?>
133
// <!DOCTYPE filesystem PUBLIC "-//NetBeans//DTD Filesystem 1.0//EN" "http://www.netbeans.org/dtds/filesystem-1_0.dtd">
134
// <filesystem>...</filesystem>
135
private static final Map JavaDoc<String JavaDoc, String JavaDoc> DTD_MAP = new HashMap JavaDoc<String JavaDoc, String JavaDoc>();
136
137     static {
138         DTD_MAP.put("-//NetBeans//DTD Filesystem 1.0//EN", "org/openide/filesystems/filesystem.dtd"); //NOI18N
139
DTD_MAP.put("-//NetBeans//DTD Filesystem 1.1//EN", "org/openide/filesystems/filesystem1_1.dtd"); //NOI18N
140
}
141
142     /** Url location of XML document */
143     private URL JavaDoc[] urlsToXml = new URL JavaDoc[] { };
144     private transient FileObjRef<? extends FileObject> rootRef;
145
146     /** Constructor. Creates new XMLFileSystem */
147     public XMLFileSystem() {
148         Impl impl = new Impl(this);
149         this.list = impl;
150         this.info = impl;
151         this.change = impl;
152         this.attr = impl;
153     }
154
155     /** Constructor. Creates new XMLFileSystem.
156      * @param uri to file with definition of XMLFileSystem
157      * @throws SAXException if parsing is not succesful
158      */

159     public XMLFileSystem(String JavaDoc uri) throws SAXException JavaDoc {
160         this();
161
162         if (uri == null) {
163             throw new NullPointerException JavaDoc("Null uri"); // NOI18N
164
}
165
166         try {
167             setXmlUrl(new URL JavaDoc(uri));
168         } catch (Exception JavaDoc e) {
169             throw (SAXException JavaDoc) ExternalUtil.copyAnnotation(new SAXException JavaDoc(e.getMessage()), e);
170         }
171     }
172
173     /** Constructor. Creates new XMLFileSystem.
174      * @param url to definition of XMLFileSystem
175      * @throws SAXException if parsing not succesful
176      */

177     public XMLFileSystem(URL JavaDoc url) throws SAXException JavaDoc {
178         this();
179
180         if (url == null) {
181             throw new NullPointerException JavaDoc("Null url"); // NOI18N
182
}
183
184         try {
185             setXmlUrl(url);
186         } catch (Exception JavaDoc e) {
187             throw (SAXException JavaDoc) ExternalUtil.copyAnnotation(new SAXException JavaDoc(e.getMessage()), e);
188         }
189     }
190
191     /** Constructor. Allows user to provide own capabilities
192     * for this filesystem.
193     * @param cap capabilities for this filesystem
194      * @deprecated Useless.
195     */

196     @Deprecated JavaDoc
197     public XMLFileSystem(FileSystemCapability cap) {
198         this();
199         setCapability(cap);
200     }
201
202     /** Getter of url field.
203      * @return URL associated with XMLFileSystem or null if no URL was set.
204      * In case that definition of XMLFileSystem
205      * is merged from more URLs than the first is returned.
206      */

207     public URL JavaDoc getXmlUrl() {
208         return (urlsToXml.length > 0) ? urlsToXml[0] : null;
209     }
210
211     /**
212      * Setter of url field. Set name of the XML file.
213      * @param url with definition of XMLFileSystem
214      * @throws PropertyVetoException if the change is not allowed by a listener
215      * @throws IOException if the file is not valid
216      */

217     public synchronized void setXmlUrl(URL JavaDoc url) throws IOException JavaDoc, PropertyVetoException JavaDoc {
218         setXmlUrl(url, false);
219     }
220
221     /**
222      * Setter of url field. Set name of the XML file.
223      * @param url with definition of XMLFileSystem
224      * @param validate sets validating of SAXParser
225      * @throws PropertyVetoException if the change is not allowed by a listener
226      * @throws IOException if the file is not valid
227      */

228     public void setXmlUrl(URL JavaDoc url, boolean validate) throws IOException JavaDoc, PropertyVetoException JavaDoc {
229         try {
230             beginAtomicAction();
231
232             synchronized (this) {
233                 setXmlUrls(new URL JavaDoc[] { url }, validate);
234             }
235         } finally {
236             finishAtomicAction();
237         }
238     }
239
240     /** Getter of url fields.
241      * @return URLs associated with XMLFileSystem.
242      * @deprecated experimental method. Nobody should rely on this method yet.
243      * @since 1.14
244      */

245     @Deprecated JavaDoc
246     public URL JavaDoc[] getXmlUrls() {
247         return urlsToXml;
248     }
249
250     /** Setter of url fields. First URL in array sets name of XMLFileSystem.
251      * If more then one url in array of URLs defines the same FileObject, then
252      * url with lower index in array overrides (means content and attributes) the other.
253      * @param urls array of definitions (in xml form) of XMLFileSystem
254      * @throws IOException if the file is not valid
255      * @throws PropertyVetoException if the change is not allowed by a listener
256      * @since 1.14
257      */

258     public void setXmlUrls(URL JavaDoc[] urls) throws IOException JavaDoc, PropertyVetoException JavaDoc {
259         try {
260             beginAtomicAction();
261
262             synchronized (this) {
263                 setXmlUrls(urls, false);
264             }
265         } finally {
266             finishAtomicAction();
267         }
268     }
269
270     @SuppressWarnings JavaDoc("deprecation") // need to set it for compat
271
private void _setSystemName(String JavaDoc s) throws PropertyVetoException JavaDoc {
272         setSystemName(s);
273     }
274
275     private synchronized void setXmlUrls(URL JavaDoc[] urls, boolean validate)
276     throws IOException JavaDoc, PropertyVetoException JavaDoc {
277         if ((urls == null) || Arrays.asList(urls).contains(null)) {
278             throw new NullPointerException JavaDoc("Null URL list or member"); // NOI18N
279
}
280
281         ResourceElem rootElem;
282         String JavaDoc oldDisplayName = getDisplayName();
283
284         if (urls.length == 0) {
285             urlsToXml = new URL JavaDoc[] { };
286             refreshChildrenInAtomicAction((AbstractFolder) getRoot(), rootElem = new ResourceElem(true, urls, null)); // NOI18N
287
rootElem = null;
288
289             return;
290         }
291
292         Handler JavaDoc handler = new Handler JavaDoc(DTD_MAP, rootElem = new ResourceElem(true, urls, null), validate); // NOI18N
293

294         URL JavaDoc[] origUrls = urlsToXml;
295         urlsToXml = new URL JavaDoc[urls.length];
296
297         try {
298             _setSystemName("XML_" + urls[0].toExternalForm().replace('/','-')); // NOI18N
299
} catch (PropertyVetoException JavaDoc pvx) {
300             urlsToXml = origUrls;
301             rootElem = null;
302             throw pvx;
303         }
304
305         URL JavaDoc act = null;
306
307         try {
308             XMLReader JavaDoc xp = XMLUtil.createXMLReader(validate, false);
309             xp.setEntityResolver(handler);
310             xp.setContentHandler(handler);
311             xp.setErrorHandler(handler);
312
313             for (int index = 0; index < urls.length; index++) {
314                 act = urls[index];
315                 urlsToXml[index] = act;
316                 handler.urlContext = act;
317
318                 String JavaDoc systemId = act.toExternalForm();
319
320                 xp.parse(systemId);
321             }
322
323             refreshChildrenInAtomicAction((AbstractFolder) getRoot(), rootElem);
324         } catch (IOException JavaDoc iox) {
325             urlsToXml = origUrls;
326             throw iox;
327         } catch (Exception JavaDoc e) {
328             throw (IOException JavaDoc) new IOException JavaDoc(act + ": " + e.toString()).initCause(e); // NOI18N
329
} finally {
330             rootElem = null;
331         }
332
333         firePropertyChange(PROP_DISPLAY_NAME, oldDisplayName, getDisplayName());
334     }
335
336     /**
337     * @return if value of lastModified should be cached
338     */

339     boolean isLastModifiedCacheEnabled() {
340         return false;
341     }
342
343     /**
344      * Test if the file is folder or contains data.
345      * @param name name of the file
346      * @return true if the file is folder, false otherwise
347      */

348     private boolean isFolder(String JavaDoc name) {
349         Reference JavaDoc ref = findReference(name);
350
351         if ((ref != null) && (ref instanceof FileObjRef)) {
352             return ((FileObjRef) ref).isFolder();
353         }
354
355         return false;
356     }
357
358     /**
359     * Get input stream.
360     *
361     * @param name the file to test
362     * @return an input stream to read the contents of this file
363     * @exception FileNotFoundException if the file does not exists or is invalid
364     */

365     private InputStream JavaDoc getInputStream(String JavaDoc name) throws java.io.FileNotFoundException JavaDoc {
366         Reference JavaDoc ref = findReference(name);
367
368         if ((ref != null) && (ref instanceof FileObjRef)) {
369             return (((FileObjRef) ref).getInputStream(name));
370         }
371
372         throw new FileNotFoundException JavaDoc(NbBundle.getMessage(XMLFileSystem.class, "EXC_CanntRead", name)); // NOI18N
373
}
374
375     /**
376     * Get URL.
377     *
378     * @param name of the file to test
379     * @return URL of resource or null
380     * @exception FileNotFoundException if the file does not exists or is invalid
381     */

382     URL JavaDoc getURL(String JavaDoc name) throws java.io.FileNotFoundException JavaDoc {
383         Reference JavaDoc ref = findReference(name);
384
385         if ((ref != null) && (ref instanceof FileObjRef)) {
386             return ((FileObjRef) ref).createAbsoluteUrl(name);
387         }
388
389         throw new FileNotFoundException JavaDoc(NbBundle.getMessage(XMLFileSystem.class, "EXC_CanntRead", name)); // NOI18N
390
}
391
392     /** Get size of stream*/
393     private long getSize(String JavaDoc name) {
394         Reference JavaDoc ref = findReference(name);
395
396         if ((ref != null) && (ref instanceof FileObjRef)) {
397             return ((FileObjRef) ref).getSize(name);
398         }
399
400         return 0;
401     }
402
403     /**returns value of last modification*/
404     private java.util.Date JavaDoc lastModified(String JavaDoc name) {
405         Reference JavaDoc ref = findReference(name);
406
407         if ((ref != null) && (ref instanceof FileObjRef)) {
408             return ((FileObjRef) ref).lastModified(name);
409         }
410
411         /**return value for resource that does not exists*/
412         return new Date JavaDoc(0);
413     }
414
415     /** Provides a name for the system that can be presented to the user.
416      * @return user presentable name of the filesystem
417      */

418     public String JavaDoc getDisplayName() {
419         if ((urlsToXml.length == 0) || (urlsToXml[0] == null) || (urlsToXml[0].toExternalForm().length() == 0)) {
420             return NbBundle.getMessage(XMLFileSystem.class, "XML_NotValidXMLFileSystem"); // NOI18N
421
}
422
423         return "XML:" + urlsToXml[0].toExternalForm().trim(); // NOI18N
424
}
425
426     /** Test if the filesystem is read-only or not.
427      * @return true if the system is read-only
428      */

429     public boolean isReadOnly() {
430         return true;
431     }
432
433     /** Initializes the root of FS.
434     */

435     private void readObject(ObjectInputStream JavaDoc ois) throws IOException JavaDoc, ClassNotFoundException JavaDoc {
436         //ois.defaultReadObject ();
437
ObjectInputStream.GetField JavaDoc fields = ois.readFields();
438         URL JavaDoc[] urls = (URL JavaDoc[]) fields.get("urlsToXml", null); // NOI18N
439

440         if (urls == null) {
441             urls = new URL JavaDoc[1];
442             urls[0] = (URL JavaDoc) fields.get("uriId", null); // NOI18N
443
}
444
445         try {
446             if (urlsToXml.length != 1) {
447                 setXmlUrls(urlsToXml);
448             } else {
449                 setXmlUrl(urlsToXml[0]);
450             }
451         } catch (PropertyVetoException JavaDoc ex) {
452             IOException JavaDoc x = new IOException JavaDoc(ex.getMessage());
453             ExternalUtil.copyAnnotation(x, ex);
454             throw x;
455         }
456     }
457
458     /** Notifies this filesystem that it has been added to the repository.
459      * Various initialization tasks should go here. Default implementation is noop.
460      */

461     public void addNotify() {
462     }
463
464     /** Notifies this filesystem that it has been removed from the repository.
465      * Concrete filesystem implementations should perform clean-up here.
466      * Default implementation is noop.
467      */

468     public void removeNotify() {
469     }
470
471     protected <T extends FileObject> Reference JavaDoc<T> createReference(T fo) {
472         return new FileObjRef<T>(fo);
473     }
474
475     private void refreshChildrenInAtomicAction(AbstractFolder fo, ResourceElem resElem) {
476         try {
477             beginAtomicAction();
478             Collection JavaDoc<AbstractFolder> oldChildren = new HashSet JavaDoc<AbstractFolder>(Collections.list(fo.existingSubFiles(true)));
479             
480             refreshChildren(fo, resElem);
481             
482             Collection JavaDoc<AbstractFolder> newChildren = Collections.list(fo.existingSubFiles(true));
483             oldChildren.removeAll(newChildren);
484             for (Iterator JavaDoc<AbstractFolder> it = oldChildren.iterator(); it.hasNext();) {
485                 AbstractFileObject invalid = (AbstractFileObject)it.next();
486                 if (invalid.validFlag) {
487                     invalid.validFlag = false;
488                     invalid.fileDeleted0(new FileEvent(invalid));
489                 }
490             }
491         } finally {
492             finishAtomicAction();
493         }
494     }
495
496     /** refreshes children recursively.*/
497     private void refreshChildren(AbstractFolder fo, ResourceElem resElem) {
498         if (fo.isRoot()) {
499             initializeReference(rootRef = new FileObjRef<AbstractFolder>(fo), resElem);
500         }
501
502         java.util.List JavaDoc<String JavaDoc> nameList = resElem.getChildren();
503         String JavaDoc[] names = new String JavaDoc[nameList.size()];
504         ResourceElem[] children = new ResourceElem[names.length];
505
506         nameList.toArray(names);
507
508         for (int i = 0; i < names.length; i++)
509             children[i] = resElem.getChild(names[i]);
510
511         fo.refresh(null, null, true, true, names);
512
513         for (int i = 0; i < children.length; i++) {
514             AbstractFolder fo2 = (AbstractFolder) fo.getFileObject(names[i]);
515             @SuppressWarnings JavaDoc("unchecked")
516             FileObjRef currentRef = (FileObjRef) findReference(fo2.getPath());
517             int diff = initializeReference(currentRef, children[i]);
518             fo2.lastModified();
519
520             if (fo2.isFolder()) {
521                 refreshChildren(fo2, children[i]);
522             } else {
523                 if ((diff & 0x01) != 0) {
524                     fo2.fileChanged0(new FileEvent(fo2));
525                 } else {
526                     if ((diff & 0x02) != 0) {
527                         fo2.fileAttributeChanged0(new FileAttributeEvent(fo2, null, null, null));
528                     }
529                 }
530             }
531         }
532     }
533
534     /** Initialize a reference with parsed element.
535      * @param currentRef the reference
536      * @param resElem the new element
537      * @return ret&0x01 if content changed, ret&0x02 if attributes changed.
538      */

539     private int initializeReference(FileObjRef currentRef, ResourceElem resElem) {
540         if (!currentRef.isInitialized()) {
541             currentRef.initialize(resElem);
542             return 0x00;
543         } else {
544             boolean attrDiff = currentRef.attacheAttrs(resElem.getAttr(false));
545             currentRef.setUrlContext(resElem.getUrlContext());
546
547             boolean diff = false;
548             if (resElem.getContent() != null) {
549                 diff = !(currentRef.content instanceof byte[]) || !java.util.Arrays.equals((byte[])currentRef.content, resElem.getContent());
550                 currentRef.content = resElem.getContent();
551             } else if (resElem.getURI() != null) {
552                 diff = !resElem.getURI().equals(currentRef.content);
553                 currentRef.content = resElem.getURI();
554             }
555             
556             return (diff ? 0x01 : 0x00) + (attrDiff ? 0x02 : 0x00);
557         }
558     }
559     
560     /** Temporary hierarchical structure of resources. Used while parsing.*/
561     private static class ResourceElem {
562         private java.util.List JavaDoc<ResourceElem> children;
563         private java.util.List JavaDoc<String JavaDoc> names;
564         private byte[] content;
565         private java.util.List JavaDoc<URL JavaDoc> urlContext = new ArrayList JavaDoc<URL JavaDoc>();
566         private XMLMapAttr foAttrs;
567         private boolean isFolder;
568         private String JavaDoc uri;
569
570         public ResourceElem(boolean isFolder, URL JavaDoc[] urlContext, String JavaDoc uri) {
571             this.isFolder = isFolder;
572             this.uri = uri;
573             this.urlContext.addAll(Arrays.asList(urlContext));
574
575             if (isFolder) {
576                 children = new ArrayList JavaDoc<ResourceElem>();
577                 names = new ArrayList JavaDoc<String JavaDoc>();
578             }
579         }
580
581         public ResourceElem(boolean isFolder, URL JavaDoc urlContext, String JavaDoc uri) {
582             this.isFolder = isFolder;
583             this.uri = uri;
584             this.urlContext.add(urlContext);
585
586             if (isFolder) {
587                 children = new ArrayList JavaDoc<ResourceElem>();
588                 names = new ArrayList JavaDoc<String JavaDoc>();
589             }
590         }
591
592         ResourceElem addChild(String JavaDoc name, ResourceElem child) {
593             if (!isFolder) {
594                 throw new IllegalArgumentException JavaDoc("not a folder"); // NOI18N
595
}
596
597             ResourceElem retVal = child;
598             int idx = names.indexOf(name);
599
600             if (idx == -1) {
601                 names.add(name);
602                 children.add(child);
603             } else {
604                 retVal = children.get(idx);
605             }
606
607             return retVal;
608         }
609
610         java.util.List JavaDoc<String JavaDoc> getChildren() {
611             return names;
612         }
613
614         ResourceElem getChild(String JavaDoc name) {
615             return children.get(names.indexOf(name));
616         }
617
618         XMLMapAttr getAttr(boolean create) {
619             if (create && (foAttrs == null)) {
620                 foAttrs = new XMLMapAttr();
621             }
622
623             return foAttrs;
624         }
625
626         byte[] getContent() {
627             return content;
628         }
629
630         URL JavaDoc[] getUrlContext() {
631             URL JavaDoc[] retVal = new URL JavaDoc[urlContext.size()];
632             urlContext.toArray(retVal);
633
634             return retVal;
635         }
636
637         String JavaDoc getURI() {
638             return uri;
639         }
640
641         void setContent(byte[] content) {
642             if (this.content == null) {
643                 byte[] alloc = new byte[content.length];
644                 System.arraycopy(content, 0, alloc, 0, content.length);
645                 this.content = alloc;
646             }
647         }
648
649         boolean isFolder() {
650             return isFolder;
651         }
652     }
653
654     //private void debugInfo(String dbgStr) { System.out.println(dbgStr);}
655

656     /** Implementation of all interfaces List, Change, Info and Attr
657      * that delegates to XMLFileSystem
658      */

659     public static class Impl extends Object JavaDoc implements AbstractFileSystem.List, AbstractFileSystem.Info,
660         AbstractFileSystem.Change, AbstractFileSystem.Attr {
661         /** generated Serialized Version UID */
662         static final long serialVersionUID = -67233358102597232L;
663
664         /** the pointer to filesystem */
665         private XMLFileSystem fs;
666
667         /** Constructor.
668          * @param fs the filesystem to delegate to
669          */

670         public Impl(XMLFileSystem fs) {
671             this.fs = fs;
672         }
673
674         /**
675          *
676          * Scans children for given name
677          * @return array of children`s names
678          * @param name the folder, by name; e.g. <code>top/next/afterthat</code>
679          */

680         public String JavaDoc[] children(String JavaDoc name) {
681             FileObject fo2name;
682
683             if ((fo2name = fs.findResource(name)) == null) {
684                 return new String JavaDoc[] { };
685             }
686
687             synchronized (fo2name) {
688                 return ((AbstractFolder) fo2name).getChildrenArray();
689             }
690         }
691
692         //
693
// Change
694
//
695

696         /**
697          * Creates new folder named name.
698          * @param name name of folder
699          * @throws IOException if operation fails
700          */

701         public void createFolder(String JavaDoc name) throws java.io.IOException JavaDoc {
702             throw new IOException JavaDoc();
703         }
704
705         /**
706          * Create new data file.
707          *
708          * @param name name of the file
709          * @exception IOException if the file cannot be created (e.g. already exists) */

710         public void createData(String JavaDoc name) throws IOException JavaDoc {
711             throw new IOException JavaDoc();
712         }
713
714         /** Rename a file.
715          * @param oldName old name of the file; fully qualified
716          * @param newName new name of the file; fully qualified
717          * @exception IOException if it could not be renamed
718          */

719         public void rename(String JavaDoc oldName, String JavaDoc newName)
720         throws IOException JavaDoc {
721             throw new IOException JavaDoc();
722         }
723
724         /**
725          * Delete the file.
726          *
727          * @param name name of file
728          * @exception IOException if the file could not be deleted
729          */

730         public void delete(String JavaDoc name) throws IOException JavaDoc {
731             throw new IOException JavaDoc();
732         }
733
734         // Info
735

736         /**
737          *
738          * Get last modification time.
739          * @param name the file to test
740          * @return the date
741          */

742         public java.util.Date JavaDoc lastModified(String JavaDoc name) {
743             return fs.lastModified(name);
744         }
745
746         /**
747          * Test if the file is folder or contains data.
748          * @param name name of the file
749          * @return true if the file is folder, false otherwise
750          */

751         public boolean folder(String JavaDoc name) {
752             return fs.isFolder(name);
753         }
754
755         /**
756          * Test whether this file can be written to or not.
757          * @param name the file to test
758          * @return <CODE>true</CODE> if file is read-only
759          */

760         public boolean readOnly(String JavaDoc name) {
761             return true;
762         }
763
764         /** Get the MIME type of the file. If filesystem has no special support
765         * for MIME types then can simply return null. FileSystem can register
766         * MIME types for a well-known extensions: FileUtil.setMIMEType(String ext, String mimeType)
767         * or together with filesystem supply some resolvers subclassed from MIMEResolver.
768         *
769         * @param name the file to test
770         * @return the MIME type textual representation (e.g. <code>"text/plain"</code>)
771         * or null if no special support for recognizing MIME is implemented.
772          */

773         public String JavaDoc mimeType(String JavaDoc name) {
774             return null;
775         }
776
777         /**
778          * Get the size of the file.
779          *
780          * @param name the file to test
781          * @return the size of the file in bytes or zero if the file does not contain data (does not
782          * exist or is a folder).
783          */

784         public long size(String JavaDoc name) {
785             if (fs.isFolder(name)) {
786                 return 0;
787             }
788
789             return fs.getSize(name);
790         }
791
792         /**
793          * Get input stream.
794          *
795          * @param name the file to test
796          * @return an input stream to read the contents of this file
797          * @exception FileNotFoundException if the file does not exists or is invalid
798          */

799         public InputStream JavaDoc inputStream(String JavaDoc name) throws java.io.FileNotFoundException JavaDoc {
800             InputStream JavaDoc is = fs.getInputStream(name);
801
802             if (is == null) {
803                 throw new java.io.FileNotFoundException JavaDoc(name);
804             }
805
806             return is;
807         }
808
809         /**
810          * Get output stream.
811          *
812          * @param name the file to test
813          * @return output stream to overwrite the contents of this file
814          * @exception IOException if an error occures (the file is invalid, etc.)
815          */

816         public OutputStream JavaDoc outputStream(String JavaDoc name) throws java.io.IOException JavaDoc {
817             throw new IOException JavaDoc();
818         }
819
820         /**
821          * Does nothing to lock the file.
822          *
823          * @param name name of the file
824          * @throws IOException if cannot be locked
825          */

826         public void lock(String JavaDoc name) throws IOException JavaDoc {
827             FSException.io("EXC_CannotLock", name, fs.getDisplayName(), name); // NOI18N
828
}
829
830         /**
831          * Does nothing to unlock the file.
832          *
833          * @param name name of the file
834          */

835         public void unlock(String JavaDoc name) {
836         }
837
838         /**
839          * Does nothing.
840          *
841          * @param name the file to mark
842          */

843         public void markUnimportant(String JavaDoc name) {
844         }
845
846         /** Get the file attribute with the specified name.
847          * @param name the file
848          * @param attrName name of the attribute
849          * @return appropriate (serializable) value or <CODE>null</CODE> if the attribute is unset (or could not be properly restored for some reason)
850          */

851         public Object JavaDoc readAttribute(String JavaDoc name, String JavaDoc attrName) {
852             @SuppressWarnings JavaDoc("unchecked")
853             FileObjRef ref = (FileObjRef) fs.findReference(name);
854
855             if ((ref == null) && (name.length() == 0) && (fs.rootRef != null)) {
856                 ref = fs.rootRef;
857             }
858
859             if (ref == null) {
860                 return null;
861             }
862
863             return ref.readAttribute(attrName);
864         }
865
866         /** Set the file attribute with the specified name.
867          * @param name the file
868          * @param attrName name of the attribute
869          * @param value new value or <code>null</code> to clear the attribute. Must be serializable, although particular filesystems may or may not use serialization to store attribute values.
870          * @exception IOException if the attribute cannot be set. If serialization is used to store it, this may in fact be a subclass such as {@link NotSerializableException}.
871          */

872         public void writeAttribute(String JavaDoc name, String JavaDoc attrName, Object JavaDoc value)
873         throws IOException JavaDoc {
874             throw new IOException JavaDoc();
875         }
876
877         /** Get all file attribute names for the file.
878          * @param name the file
879          * @return enumeration of keys (as strings)
880          */

881         public Enumeration JavaDoc<String JavaDoc> attributes(String JavaDoc name) {
882             @SuppressWarnings JavaDoc("unchecked")
883             FileObjRef<? extends FileObject> ref = (FileObjRef) fs.findReference(name);
884
885             if ((ref == null) && (name.length() == 0) && (fs.rootRef != null)) {
886                 ref = fs.rootRef;
887             }
888
889             if (ref == null) {
890                 return Enumerations.empty();
891             }
892
893             return ref.attributes();
894         }
895
896         /** Called when a file is renamed, to appropriately update its attributes.
897          * @param oldName old name of the file
898          * @param newName new name of the file
899          */

900         public void renameAttributes(String JavaDoc oldName, String JavaDoc newName) {
901         }
902
903         /** Called when a file is deleted, to also delete its attributes.
904          *
905          * @param name name of the file
906          */

907         public void deleteAttributes(String JavaDoc name) {
908         }
909     }
910
911     /** Strong reference to FileObject. To FileObject may be attached attributes (XMLMapAttr)
912      * and info about if it is folder or not.
913      */

914     private static class FileObjRef<T extends FileObject> extends WeakReference JavaDoc<T> {
915         private T fo;
916         private Object JavaDoc content;
917         private XMLMapAttr foAttrs;
918         byte isFolder = -1;
919         Object JavaDoc urlContext = null;
920
921         public FileObjRef(T fo) {
922             super(fo);
923             this.fo = fo;
924         }
925
926         public boolean isInitialized() {
927             return (isFolder != -1);
928         }
929
930         public void initialize(ResourceElem res) {
931             content = res.getContent();
932
933             XMLMapAttr tmp = res.getAttr(false);
934
935             if ((tmp != null) && !tmp.isEmpty()) {
936                 foAttrs = tmp;
937             }
938
939             isFolder = (byte) (res.isFolder() ? 1 : 0);
940
941             if (content == null) {
942                 content = res.getURI();
943             }
944
945             setUrlContext(res.getUrlContext());
946         }
947
948         public boolean isFolder() {
949             return (isFolder == 1);
950         }
951
952         /** @return true if at lest one attribute changed */
953         public boolean attacheAttrs(XMLMapAttr attrs) {
954             if ((attrs == null) || attrs.isEmpty()) {
955                 return false;
956             }
957
958             if (foAttrs == null) {
959                 foAttrs = new XMLMapAttr();
960             }
961
962             Iterator JavaDoc it = attrs.entrySet().iterator();
963             boolean ch = false;
964             while (it.hasNext()) {
965                 Map.Entry JavaDoc attrEntry = (Map.Entry JavaDoc) it.next();
966                 Object JavaDoc prev = foAttrs.put(attrEntry.getKey(), attrEntry.getValue());
967                 
968                 ch |= (prev == null && attrEntry.getValue() != null) || !prev.equals(attrEntry.getValue());
969             }
970             
971             return ch;
972         }
973
974         public void setUrlContext(URL JavaDoc[] ctx) {
975             if (ctx.length > 0) {
976                 if (ctx.length > 1) {
977                     urlContext = ctx;
978                 } else {
979                     urlContext = ctx[0];
980                 }
981             }
982         }
983
984         public Enumeration JavaDoc<String JavaDoc> attributes() {
985             if (foAttrs == null) {
986                 return Enumerations.empty();
987             } else {
988                 Set JavaDoc<String JavaDoc> s = new HashSet JavaDoc<String JavaDoc>(foAttrs.keySet());
989
990                 return Collections.enumeration(s);
991             }
992         }
993
994         private URL JavaDoc[] getLayers() {
995             if (urlContext == null) {
996                 return null;
997             }
998
999             if (urlContext instanceof URL JavaDoc[]) {
1000                return (URL JavaDoc[]) urlContext;
1001            }
1002
1003            return new URL JavaDoc[] { (URL JavaDoc) urlContext };
1004        }
1005
1006        public Object JavaDoc readAttribute(String JavaDoc attrName) {
1007            if (attrName.equals("layers")) { //NOI18N
1008

1009                return getLayers();
1010            }
1011
1012            if (foAttrs == null) {
1013                return null;
1014            }
1015
1016            FileObject topFO = MultiFileObject.attrAskedFileObject.get();
1017            FileObject f = (topFO == null) ? fo : topFO;
1018
1019            MultiFileObject.attrAskedFileObject.set(null);
1020
1021            try {
1022                Object JavaDoc[] objs = new Object JavaDoc[] { f, attrName };
1023
1024                return foAttrs.get(attrName, objs);
1025            } finally {
1026                MultiFileObject.attrAskedFileObject.set(topFO);
1027            }
1028        }
1029
1030        /**
1031         * Get input stream.
1032         *
1033         * @return an input stream to read the contents of this file
1034         * @param context
1035         * @param name the file to test
1036         * @exception FileNotFoundException if the file does not exists or is invalid */

1037        public InputStream JavaDoc getInputStream(String JavaDoc name)
1038        throws java.io.FileNotFoundException JavaDoc {
1039            InputStream JavaDoc is = null;
1040
1041            if (content == null) {
1042                return new ByteArrayInputStream JavaDoc(new byte[] { });
1043            }
1044
1045            if (content instanceof String JavaDoc) {
1046                URL JavaDoc absURL = createAbsoluteUrl(name);
1047
1048                try {
1049                    is = absURL.openStream();
1050                } catch (IOException JavaDoc iox) {
1051                    FileNotFoundException JavaDoc x = new FileNotFoundException JavaDoc(name);
1052                    ExternalUtil.copyAnnotation(x, iox);
1053                    throw x;
1054                }
1055            }
1056
1057            if (content instanceof byte[]) {
1058                is = new ByteArrayInputStream JavaDoc((byte[]) content);
1059            }
1060
1061            if (is == null) {
1062                throw new FileNotFoundException JavaDoc(name);
1063            }
1064
1065            return is;
1066        }
1067
1068        private URL JavaDoc createAbsoluteUrl(String JavaDoc name) throws java.io.FileNotFoundException JavaDoc {
1069            if (!(content instanceof String JavaDoc)) {
1070                return null;
1071            }
1072
1073            String JavaDoc uri = (String JavaDoc) content;
1074
1075            try {
1076                URL JavaDoc[] uc = getLayers();
1077                URL JavaDoc retVal = ((uc == null) || (uc.length == 0)) ? new URL JavaDoc(uri) : new URL JavaDoc(uc[0], uri);
1078
1079                return retVal;
1080            } catch (IOException JavaDoc ex) { // neni koser osetreni - RM
1081

1082                FileNotFoundException JavaDoc x = new FileNotFoundException JavaDoc(name);
1083                ExternalUtil.copyAnnotation(x, ex);
1084                throw x;
1085            }
1086        }
1087
1088        public long getSize(String JavaDoc name) {
1089            if (content == null) {
1090                return 0;
1091            }
1092
1093            if (content instanceof byte[]) {
1094                return ((byte[]) content).length;
1095            }
1096
1097            if (content instanceof String JavaDoc) {
1098                try {
1099                    URL JavaDoc absURL = createAbsoluteUrl(name);
1100                    URLConnection JavaDoc urlConnection = absURL.openConnection();
1101
1102                    try {
1103                        return urlConnection.getContentLength();
1104                    } finally {
1105                        urlConnection.getInputStream().close();
1106                    }
1107                } catch (IOException JavaDoc iex) {
1108                }
1109            }
1110
1111            return 0;
1112        }
1113
1114        public Date JavaDoc lastModified(String JavaDoc name) {
1115            URL JavaDoc url = null;
1116            Date JavaDoc retval = null;
1117            
1118            if ((content == null) || !(content instanceof String JavaDoc)) {
1119                URL JavaDoc[] all = getLayers();
1120                url = (all != null && all.length > 0) ? all[0] : null;
1121            } else {
1122                try {
1123                    url = createAbsoluteUrl(name);
1124                } catch (IOException JavaDoc iex) {
1125                    url = null;
1126                }
1127            }
1128            
1129            if (url != null) {
1130                String JavaDoc protocol = url.getProtocol();
1131                if ("jar".equals(protocol)) {//NOI18N
1132
URL JavaDoc tmp = FileUtil.getArchiveFile(url);
1133                    url = (tmp != null) ? tmp : url;
1134                }
1135                
1136                if ("file".equals(protocol)) { //NOI18N
1137
File JavaDoc f = new File JavaDoc(URI.create(url.toExternalForm()));
1138                    retval = (f.exists()) ? new Date JavaDoc(f.lastModified()) : null;
1139                } else {
1140                    retval = timeFromDateHeaderField(url);
1141                }
1142            }
1143            
1144            if (retval == null) {
1145                retval = new Date JavaDoc(0);
1146            }
1147            
1148            return retval;
1149        }
1150
1151        /** can return null*/
1152        private static String JavaDoc getLocalResource(URL JavaDoc url) {
1153            if (url == null) {
1154                return null;
1155            }
1156
1157            if (url.getProtocol().equals("jar")) { // NOI18N
1158

1159                URL JavaDoc testURL = null;
1160
1161                try {
1162                    testURL = new URL JavaDoc(url.getFile());
1163
1164                    if (testURL.getProtocol().equals("file")) { // NOI18N
1165

1166                        return testURL.getFile();
1167                    }
1168                } catch (MalformedURLException JavaDoc mfx) {
1169                    return null;
1170                }
1171            }
1172
1173            if (url.getProtocol().equals("file")) { // NOI18N
1174

1175                return url.getFile();
1176            }
1177
1178            return null;
1179        }
1180
1181        /** can return null*/
1182        private static File JavaDoc getFileFromResourceString(String JavaDoc localResource) {
1183            if (localResource == null) {
1184                return null;
1185            }
1186
1187            int idx = localResource.indexOf('!');
1188            String JavaDoc fileName = (idx != -1) ? localResource.substring(0, idx) : localResource;
1189            File JavaDoc f = new File JavaDoc(fileName);
1190
1191            if (f.exists()) {
1192                return f;
1193            }
1194
1195            return null;
1196        }
1197
1198        private java.util.Date JavaDoc timeFromDateHeaderField(URL JavaDoc url) {
1199            URLConnection JavaDoc urlConn;
1200
1201            try {
1202                urlConn = url.openConnection();
1203
1204                try {
1205                    return new Date JavaDoc(urlConn.getLastModified());
1206                } finally {
1207                    urlConn.getInputStream().close();
1208                }
1209            } catch (IOException JavaDoc ie) {
1210                return new java.util.Date JavaDoc(0);
1211            }
1212        }
1213
1214        private java.util.Date JavaDoc timeFromDateHeaderField(URL JavaDoc[] urls) {
1215            return (urls.length > 0) ? timeFromDateHeaderField(urls[0]) : new java.util.Date JavaDoc(0);
1216        }
1217    }
1218
1219    /** Class that can be used to parse XML document (Expects array of ElementHandler clasess). Calls handler methods of ElementHandler clasess.
1220     */

1221    static class Handler extends DefaultHandler JavaDoc {
1222        private static final int FOLDER_CODE = "folder".hashCode(); // NOI18N
1223
private static final int FILE_CODE = "file".hashCode(); // NOI18N
1224
private static final int ATTR_CODE = "attr".hashCode(); // NOI18N
1225
private ResourceElem rootElem;
1226        private boolean validate = false;
1227        Stack JavaDoc<ResourceElem> resElemStack = new Stack JavaDoc<ResourceElem>();
1228        Stack JavaDoc<String JavaDoc> elementStack = new Stack JavaDoc<String JavaDoc>();
1229        URL JavaDoc urlContext;
1230        private Map JavaDoc dtdMap;
1231        private ResourceElem topRE;
1232        private StringBuffer JavaDoc pcdata = new StringBuffer JavaDoc();
1233
1234        Handler(Map JavaDoc dtdMap, ResourceElem rootElem, boolean validate) {
1235            this.dtdMap = dtdMap;
1236            this.rootElem = rootElem;
1237            this.validate = validate;
1238        }
1239
1240        public void error(SAXParseException JavaDoc exception)
1241        throws SAXException JavaDoc {
1242            throw exception;
1243        }
1244
1245        public void warning(SAXParseException JavaDoc exception)
1246        throws SAXException JavaDoc {
1247            throw exception;
1248        }
1249
1250        public void fatalError(SAXParseException JavaDoc exception)
1251        throws SAXException JavaDoc {
1252            throw exception;
1253        }
1254
1255        public void startElement(String JavaDoc xmluri, String JavaDoc lname, String JavaDoc name, Attributes JavaDoc amap)
1256        throws SAXException JavaDoc {
1257            int controlCode = name.hashCode();
1258
1259            elementStack.push(name);
1260
1261            String JavaDoc foName = amap.getValue("name"); // NOI18N
1262

1263            if (controlCode == FOLDER_CODE) {
1264                if (foName == null) {
1265                    throw new SAXException JavaDoc(NbBundle.getMessage(XMLFileSystem.class, "XML_MisssingAttr")); // NOI18N
1266
}
1267
1268                ResourceElem newRes = new ResourceElem(true, urlContext, null);
1269
1270                topRE = topRE.addChild(foName, newRes);
1271                resElemStack.push(topRE);
1272
1273                return;
1274            }
1275
1276            if (controlCode == FILE_CODE) {
1277                if (foName == null) {
1278                    throw new SAXException JavaDoc(NbBundle.getMessage(XMLFileSystem.class, "XML_MisssingAttr")); // NOI18N
1279
}
1280
1281                foName = foName.intern();
1282
1283                String JavaDoc uri = null;
1284
1285                if (amap.getLength() > 1) {
1286                    uri = amap.getValue("url"); // NOI18N
1287
}
1288
1289                ResourceElem newRes = new ResourceElem(false, urlContext, uri);
1290
1291                topRE = topRE.addChild(foName, newRes);
1292                resElemStack.push(topRE);
1293
1294                pcdata.setLength(0);
1295
1296                return;
1297            }
1298
1299            if (controlCode == ATTR_CODE) {
1300                if (foName == null) {
1301                    throw new SAXException JavaDoc(NbBundle.getMessage(XMLFileSystem.class, "XML_MisssingAttr")); // NOI18N
1302
}
1303
1304                int len = amap.getLength();
1305
1306                for (int i = 0; i < len; i++) {
1307                    String JavaDoc key = amap.getQName(i);
1308                    String JavaDoc value = amap.getValue(i);
1309
1310                    if (XMLMapAttr.Attr.isValid(key) != -1) {
1311                        XMLMapAttr.Attr attr = XMLMapAttr.createAttributeAndDecode(key, value);
1312                        XMLMapAttr attrMap = topRE.getAttr(true);
1313                        Object JavaDoc retVal = attrMap.put(foName, attr);
1314
1315                        if (retVal != null) {
1316                            attrMap.put(foName, retVal);
1317                        }
1318                    }
1319                }
1320
1321                return;
1322            }
1323        }
1324
1325        public void endElement(String JavaDoc uri, String JavaDoc lname, String JavaDoc name) throws SAXException JavaDoc {
1326            if ((elementStack.peek().hashCode() == FILE_CODE) && !topRE.isFolder()) {
1327                String JavaDoc string = pcdata.toString().trim();
1328
1329                if (string.length() > 0) {
1330                    topRE.setContent(string.getBytes());
1331                }
1332
1333                pcdata.setLength(0);
1334            }
1335
1336            int controlCode = name.hashCode();
1337
1338            elementStack.pop();
1339
1340            if ((controlCode == FOLDER_CODE) || (controlCode == FILE_CODE)) {
1341                resElemStack.pop();
1342                topRE = resElemStack.peek();
1343
1344                return;
1345            }
1346        }
1347
1348        public void characters(char[] ch, int start, int length)
1349        throws SAXException JavaDoc {
1350            if (elementStack.peek().hashCode() != FILE_CODE) {
1351                return;
1352            }
1353
1354            if (topRE.isFolder()) {
1355                return;
1356            }
1357
1358            pcdata.append(new String JavaDoc(ch, start, length));
1359        }
1360
1361        public InputSource JavaDoc resolveEntity(String JavaDoc pid, String JavaDoc sid)
1362        throws SAXException JavaDoc {
1363            String JavaDoc publicURL = (String JavaDoc) dtdMap.get(pid);
1364
1365            if (publicURL != null) {
1366                if (validate) {
1367                    publicURL = getClass().getClassLoader().getResource(publicURL).toExternalForm();
1368
1369                    return new InputSource JavaDoc(publicURL);
1370                } else {
1371                    return new InputSource JavaDoc(new ByteArrayInputStream JavaDoc(new byte[0]));
1372                }
1373            }
1374
1375            return new InputSource JavaDoc(sid);
1376        }
1377
1378        public void startDocument() throws SAXException JavaDoc {
1379            super.startDocument();
1380            resElemStack = new Stack JavaDoc<ResourceElem>();
1381            resElemStack.push(rootElem);
1382            topRE = rootElem;
1383
1384            elementStack = new Stack JavaDoc<String JavaDoc>();
1385            elementStack.push("<root>"); // NOI18N
1386
}
1387
1388        public void endDocument() throws SAXException JavaDoc {
1389            super.endDocument();
1390            resElemStack.pop();
1391            elementStack.pop();
1392        }
1393    }
1394}
1395
Popular Tags