KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > netbeans > modules > versioning > system > cvss > util > Utils


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.netbeans.modules.versioning.system.cvss.util;
21
22 import java.awt.Dialog JavaDoc;
23 import java.awt.Frame JavaDoc;
24 import java.awt.KeyboardFocusManager JavaDoc;
25 import java.awt.Window JavaDoc;
26 import java.io.BufferedReader JavaDoc;
27 import java.io.File JavaDoc;
28 import java.io.FileNotFoundException JavaDoc;
29 import java.io.FileReader JavaDoc;
30 import java.io.IOException JavaDoc;
31 import java.lang.ref.Reference JavaDoc;
32 import java.lang.ref.WeakReference JavaDoc;
33 import java.util.*;
34 import java.util.regex.Pattern JavaDoc;
35
36 import org.netbeans.api.fileinfo.NonRecursiveFolder;
37 import org.netbeans.api.project.FileOwnerQuery;
38 import org.netbeans.api.project.Project;
39 import org.netbeans.api.project.ProjectUtils;
40 import org.netbeans.api.project.SourceGroup;
41 import org.netbeans.api.project.Sources;
42 import org.netbeans.lib.cvsclient.admin.Entry;
43 import org.netbeans.lib.cvsclient.command.log.LogInformation;
44 import org.netbeans.modules.versioning.system.cvss.CvsFileNode;
45 import org.netbeans.modules.versioning.system.cvss.CvsVersioningSystem;
46 import org.netbeans.modules.versioning.system.cvss.FileInformation;
47 import org.netbeans.modules.versioning.system.cvss.FileStatusCache;
48 import org.netbeans.modules.versioning.util.FlatFolder;
49 import org.openide.filesystems.FileObject;
50 import org.openide.filesystems.FileUtil;
51 import org.openide.loaders.DataObject;
52 import org.openide.loaders.DataShadow;
53 import org.openide.nodes.Node;
54 import org.openide.util.Lookup;
55 import org.openide.windows.TopComponent;
56 import org.openide.windows.WindowManager;
57
58 /**
59  * Provides static utility methods for CVS module.
60  *
61  * @author Maros Sandor
62  */

63 public class Utils {
64
65     private static final Pattern JavaDoc metadataPattern = Pattern.compile(".*\\" + File.separatorChar + "CVS(\\" + File.separatorChar + ".*|$)");
66     
67     private static Reference JavaDoc/*<Node[]>*/ contextNodesCached = new /* #72006 */ WeakReference JavaDoc(null);
68     private static Context contextCached;
69
70     /**
71      * Semantics is similar to {@link org.openide.windows.TopComponent#getActivatedNodes()} except that this
72      * method returns File objects instead od Nodes. Every node is examined for Files it represents. File and Folder
73      * nodes represent their underlying files or folders. Project nodes are represented by their source groups. Other
74      * logical nodes must provide FileObjects in their Lookup.
75      *
76      * @return File [] array of activated files
77      * @param nodes or null (then taken from windowsystem, it may be wrong on editor tabs #66700).
78      */

79     public static Context getCurrentContext(Node[] nodes) {
80         if (nodes == null) {
81             nodes = TopComponent.getRegistry().getActivatedNodes();
82         }
83         if (Arrays.equals((Node[]) contextNodesCached.get(), nodes)) return contextCached;
84         Set files = new HashSet(nodes.length);
85         Set rootFiles = new HashSet(nodes.length);
86         Set rootFileExclusions = new HashSet(5);
87         for (int i = 0; i < nodes.length; i++) {
88             Node node = nodes[i];
89             CvsFileNode cvsNode = node.getLookup().lookup(CvsFileNode.class);
90             if (cvsNode != null) {
91                 files.add(cvsNode.getFile());
92                 rootFiles.add(cvsNode.getFile());
93                 continue;
94             }
95             Project project = node.getLookup().lookup(Project.class);
96             if (project != null) {
97                 addProjectFiles(files, rootFiles, rootFileExclusions, project);
98                 continue;
99             }
100             addFileObjects(node, files, rootFiles);
101         }
102         
103         contextCached = new Context(files, rootFiles, rootFileExclusions);
104         contextNodesCached = new WeakReference JavaDoc(nodes);
105         return contextCached;
106     }
107
108     
109     /**
110      * Semantics is similar to {@link org.openide.windows.TopComponent#getActivatedNodes()} except that this
111      * method returns File objects instead od Nodes. Every node is examined for Files it represents. File and Folder
112      * nodes represent their underlying files or folders. Project nodes are represented by their source groups. Other
113      * logical nodes must provide FileObjects in their Lookup.
114      *
115      * @param nodes null (then taken from windowsystem, it may be wrong on editor tabs #66700).
116      * @param includingFileStatus if any activated file does not have this CVS status, an empty array is returned
117      * @param includingFolderStatus if any activated folder does not have this CVS status, an empty array is returned
118      * @return File [] array of activated files, or an empty array if any of examined files/folders does not have given status
119      */

120     public static Context getCurrentContext(Node[] nodes, int includingFileStatus, int includingFolderStatus) {
121         Context context = getCurrentContext(nodes);
122         FileStatusCache cache = CvsVersioningSystem.getInstance().getStatusCache();
123         File JavaDoc [] files = context.getRootFiles();
124         for (int i = 0; i < files.length; i++) {
125             File JavaDoc file = files[i];
126             FileInformation fi = cache.getStatus(file);
127             if (file.isDirectory()) {
128                 if ((fi.getStatus() & includingFolderStatus) == 0) return Context.Empty;
129             } else {
130                 if ((fi.getStatus() & includingFileStatus) == 0) return Context.Empty;
131             }
132         }
133         // if there are no exclusions, we may safely return this context because filtered files == root files
134
if (context.getExclusions().size() == 0) return context;
135
136         // in this code we remove files from filteredFiles to NOT include any files that do not have required status
137
// consider a freeform project that has 'build' in filteredFiles, the Branch action would try to branch it
138
// so, it is OK to have BranchAction enabled but the context must be a bit adjusted here
139
Set<File JavaDoc> filteredFiles = new HashSet<File JavaDoc>(Arrays.asList(context.getFiles()));
140         Set<File JavaDoc> rootFiles = new HashSet<File JavaDoc>(Arrays.asList(context.getRootFiles()));
141         Set<File JavaDoc> rootFileExclusions = new HashSet<File JavaDoc>(context.getExclusions());
142
143         for (Iterator<File JavaDoc> i = filteredFiles.iterator(); i.hasNext(); ) {
144             File JavaDoc file = i.next();
145             if (file.isDirectory()) {
146                 if ((cache.getStatus(file).getStatus() & includingFolderStatus) == 0) i.remove();
147             } else {
148                 if ((cache.getStatus(file).getStatus() & includingFileStatus) == 0) i.remove();
149             }
150         }
151         return new Context(filteredFiles, rootFiles, rootFileExclusions);
152     }
153
154     /**
155      * @return <code>true</code> if
156      * <ul>
157      * <li> the node contains a project in its lookup and
158      * <li> the project contains at least one CVS versioned source group
159      * </ul>
160      * otherwise <code>false</code>.
161      */

162     public static boolean isVersionedProject(Node node) {
163         Lookup lookup = node.getLookup();
164         Project project = lookup.lookup(Project.class);
165         return isVersionedProject(project);
166     }
167
168     /**
169      * @return <code>true</code> if
170      * <ul>
171      * <li> the project != null and
172      * <li> the project contains at least one CVS versioned source group
173      * </ul>
174      * otherwise <code>false</code>.
175      */

176     public static boolean isVersionedProject(Project project) {
177         if (project != null) {
178             FileStatusCache cache = CvsVersioningSystem.getInstance().getStatusCache();
179             Sources sources = ProjectUtils.getSources(project);
180             SourceGroup [] sourceGroups = sources.getSourceGroups(Sources.TYPE_GENERIC);
181             for (int j = 0; j < sourceGroups.length; j++) {
182                 SourceGroup sourceGroup = sourceGroups[j];
183                 File JavaDoc f = FileUtil.toFile(sourceGroup.getRootFolder());
184                 if (f != null) {
185                     if ((cache.getStatus(f).getStatus() & FileInformation.STATUS_MANAGED) != 0) return true;
186                 }
187             }
188         }
189         return false;
190     }
191
192     private static void addFileObjects(Node node, Set files, Set rootFiles) {
193         Collection folders = node.getLookup().lookup(new Lookup.Template(NonRecursiveFolder.class)).allInstances();
194         List nodeFiles = new ArrayList();
195         if (folders.size() > 0) {
196             for (Iterator j = folders.iterator(); j.hasNext();) {
197                 NonRecursiveFolder nonRecursiveFolder = (NonRecursiveFolder) j.next();
198                 nodeFiles.add(new FlatFolder(FileUtil.toFile(nonRecursiveFolder.getFolder()).getAbsolutePath()));
199             }
200         } else {
201             Collection fileObjects = node.getLookup().lookup(new Lookup.Template(FileObject.class)).allInstances();
202             if (fileObjects.size() > 0) {
203                 nodeFiles.addAll(toFileCollection(fileObjects));
204             } else {
205                 DataObject dataObject = node.getCookie(DataObject.class);
206                 if (dataObject instanceof DataShadow) {
207                     dataObject = ((DataShadow) dataObject).getOriginal();
208                 }
209                 if (dataObject != null) {
210                     Collection doFiles = toFileCollection(dataObject.files());
211                     nodeFiles.addAll(doFiles);
212                 }
213             }
214         }
215         files.addAll(nodeFiles);
216         rootFiles.addAll(nodeFiles);
217     }
218
219     /**
220      * Determines all files and folders that belong to a given project and adds them to the supplied Collection.
221      *
222      * @param filteredFiles destination collection of Files
223      * @param project project to examine
224      */

225     public static void addProjectFiles(Collection filteredFiles, Collection rootFiles, Collection rootFilesExclusions, Project project) {
226         FileStatusCache cache = CvsVersioningSystem.getInstance().getStatusCache();
227         Sources sources = ProjectUtils.getSources(project);
228         SourceGroup [] sourceGroups = sources.getSourceGroups(Sources.TYPE_GENERIC);
229         for (int j = 0; j < sourceGroups.length; j++) {
230             SourceGroup sourceGroup = sourceGroups[j];
231             FileObject srcRootFo = sourceGroup.getRootFolder();
232             File JavaDoc rootFile = FileUtil.toFile(srcRootFo);
233             try {
234                 getCVSRootFor(rootFile);
235             } catch (IOException JavaDoc e) {
236                 // the folder is not under a versioned root
237
continue;
238             }
239             rootFiles.add(rootFile);
240             boolean containsSubprojects = false;
241             FileObject [] rootChildren = srcRootFo.getChildren();
242             Set projectFiles = new HashSet(rootChildren.length);
243             for (int i = 0; i < rootChildren.length; i++) {
244                 FileObject rootChildFo = rootChildren[i];
245                 if (CvsVersioningSystem.FILENAME_CVS.equals(rootChildFo.getNameExt())) continue;
246                 File JavaDoc child = FileUtil.toFile(rootChildFo);
247                 // #67900 Added special treatment for .cvsignore files
248
if (sourceGroup.contains(rootChildFo) || CvsVersioningSystem.FILENAME_CVSIGNORE.equals(rootChildFo.getNameExt())) {
249                     // TODO: #60516 deep scan is required here but not performed due to performace reasons
250
projectFiles.add(child);
251                 } else {
252                     int status = cache.getStatus(child).getStatus();
253                     if (status != FileInformation.STATUS_NOTVERSIONED_EXCLUDED) {
254                         rootFilesExclusions.add(child);
255                         containsSubprojects = true;
256                     }
257                 }
258             }
259             if (containsSubprojects) {
260                 filteredFiles.addAll(projectFiles);
261             } else {
262                 filteredFiles.add(rootFile);
263             }
264         }
265     }
266
267     /**
268      * May take a long time for many projects, consider making the call from worker threads.
269      *
270      * @param projects projects to examine
271      * @return Context context that defines list of supplied projects
272      */

273     public static Context getProjectsContext(Project [] projects) {
274         Set filtered = new HashSet();
275         Set roots = new HashSet();
276         Set exclusions = new HashSet();
277         for (int i = 0; i < projects.length; i++) {
278             addProjectFiles(filtered, roots, exclusions, projects[i]);
279         }
280         return new Context(filtered, roots, exclusions);
281     }
282
283     private static Collection toFileCollection(Collection fileObjects) {
284         Set files = new HashSet(fileObjects.size()*4/3+1);
285         for (Iterator i = fileObjects.iterator(); i.hasNext();) {
286             files.add(FileUtil.toFile((FileObject) i.next()));
287         }
288         files.remove(null);
289         return files;
290     }
291
292     public static File JavaDoc [] toFileArray(Collection fileObjects) {
293         Set files = new HashSet(fileObjects.size()*4/3+1);
294         for (Iterator i = fileObjects.iterator(); i.hasNext();) {
295             files.add(FileUtil.toFile((FileObject) i.next()));
296         }
297         files.remove(null);
298         return (File JavaDoc[]) files.toArray(new File JavaDoc[files.size()]);
299     }
300
301     /**
302      * Determines CVS repository root for the given file. It does that by reading the CVS/Root file from
303      * its parent directory, its parent and so on until CVS/Root is found.
304      *
305      * @param file the file in question
306      * @return CVS root for the given file
307      * @throws IOException if CVS/Root file is unreadable
308      */

309     public static String JavaDoc getCVSRootFor(File JavaDoc file) throws IOException JavaDoc {
310         if (file.isFile()) file = file.getParentFile();
311         for (; file != null; file = file.getParentFile()) {
312             File JavaDoc rootFile = new File JavaDoc(file, "CVS/Root"); // NOI18N
313
BufferedReader JavaDoc br = null;
314             try {
315                 br = new BufferedReader JavaDoc(new FileReader JavaDoc(rootFile));
316                 return br.readLine();
317             } catch (FileNotFoundException JavaDoc e) {
318                 continue;
319             } finally {
320                 if (br != null) br.close();
321             }
322         }
323         throw new IOException JavaDoc("CVS/Root not found"); // NOI18N
324
}
325     
326     public static Window JavaDoc getCurrentWindow() {
327         Window JavaDoc wnd = KeyboardFocusManager.getCurrentKeyboardFocusManager().getActiveWindow();
328         if (wnd instanceof Dialog JavaDoc || wnd instanceof Frame JavaDoc) {
329             return wnd;
330         } else {
331             return WindowManager.getDefault().getMainWindow();
332         }
333     }
334
335     /**
336      * Tests parent/child relationship of files.
337      *
338      * @param parent file to be parent of the second parameter
339      * @param file file to be a child of the first parameter
340      * @return true if the second parameter represents the same file as the first parameter OR is its descendant (child)
341      */

342     public static boolean isParentOrEqual(File JavaDoc parent, File JavaDoc file) {
343         for (; file != null; file = file.getParentFile()) {
344             if (file.equals(parent)) return true;
345         }
346         return false;
347     }
348
349     /**
350      * Computes path of this file to repository root.
351      *
352      * @param file a file
353      * @return String path of this file in repsitory. If this path does not describe a
354      * versioned file, this method returns an empty string
355      */

356     public static String JavaDoc getRelativePath(File JavaDoc file) {
357         try {
358             return CvsVersioningSystem.getInstance().getAdminHandler().getRepositoryForDirectory(file.getParent(), "").substring(1); // NOI18N
359
} catch (IOException JavaDoc e) {
360             return ""; // NOI18N
361
}
362     }
363
364     /**
365      * Determines the sticky information for a given file. If the file is new then it
366      * returns its parent directory's sticky info, if any.
367      *
368      * @param file file to examine
369      * @return String sticky information for a file (with leading D or T specifier) or null
370      */

371     public static String JavaDoc getSticky(File JavaDoc file) {
372         if (file == null) return null;
373         FileInformation info = CvsVersioningSystem.getInstance().getStatusCache().getStatus(file);
374         if (info.getStatus() == FileInformation.STATUS_NOTVERSIONED_NEWLOCALLY) {
375             return getSticky(file.getParentFile());
376         } else if (info.getStatus() == FileInformation.STATUS_NOTVERSIONED_EXCLUDED) {
377             return null;
378         }
379         if (file.isDirectory()) {
380             return CvsVersioningSystem.getInstance().getAdminHandler().getStickyTagForDirectory(file);
381         }
382         Entry entry = info.getEntry(file);
383         if (entry != null) {
384             String JavaDoc stickyInfo = null;
385             if (entry.getTag() != null) stickyInfo = "T" + entry.getTag(); // NOI18N
386
else if (entry.getDate() != null) stickyInfo = "D" + entry.getDateFormatted(); // NOI18N
387
return stickyInfo;
388         }
389         return null;
390     }
391
392     /**
393      * Computes previous revision or <code>null</code>
394      * for initial.
395      *
396      * @param revision num.dot revision or <code>null</code>
397      */

398     public static String JavaDoc previousRevision(String JavaDoc revision) {
399         if (revision == null) return null;
400         String JavaDoc[] nums = revision.split("\\."); // NOI18N
401
assert (nums.length % 2) == 0 : "File revisions must consist from even tokens: " + revision; // NOI18N
402

403         // eliminate branches
404
int lastIndex = nums.length -1;
405         boolean cutoff = false;
406         while (lastIndex>1 && "1".equals(nums[lastIndex])) { // NOI18N
407
lastIndex -= 2;
408             cutoff = true;
409         }
410         if (lastIndex <= 0) {
411             return null;
412         } else if (lastIndex == 1 && "1".equals(nums[lastIndex])) { // NOI18N
413
return null;
414         } else {
415             int rev = Integer.parseInt(nums[lastIndex]);
416             if (!cutoff) rev--;
417             StringBuffer JavaDoc sb = new StringBuffer JavaDoc(nums[0]);
418             for (int i = 1; i<lastIndex; i++) {
419                 sb.append('.').append(nums[i]); // NOI18N
420
}
421             sb.append('.').append(rev); // NOI18N
422
return sb.toString();
423         }
424     }
425
426     /**
427      * Determines parent project for a file.
428      *
429      * @param file file to examine
430      * @return Project owner of the file or null if the file does not belong to a project
431      */

432     public static Project getProject(File JavaDoc file) {
433         if (file == null) return null;
434         FileObject fo = FileUtil.toFileObject(file);
435         if (fo == null) return getProject(file.getParentFile());
436         return FileOwnerQuery.getOwner(fo);
437     }
438
439     public static String JavaDoc createBranchRevisionNumber(String JavaDoc branchNumber) {
440         StringBuilder JavaDoc sb = new StringBuilder JavaDoc();
441         int idx = branchNumber.lastIndexOf('.'); // NOI18N
442
sb.append(branchNumber.substring(0, idx));
443         sb.append(".0"); // NOI18N
444
sb.append(branchNumber.substring(idx));
445         return sb.toString();
446     }
447
448     public static String JavaDoc formatBranches(LogInformation.Revision revision, boolean useNumbersIfNamesNotAvailable) {
449         String JavaDoc branches = revision.getBranches();
450         if (branches == null) return ""; // NOI18N
451

452         boolean branchNamesAvailable = true;
453         StringBuilder JavaDoc branchNames = new StringBuilder JavaDoc();
454         StringTokenizer st = new StringTokenizer(branches, ";"); // NOI18N
455
while (st.hasMoreTokens()) {
456             String JavaDoc branchNumber = st.nextToken().trim();
457             List<LogInformation.SymName> names = revision.getLogInfoHeader().getSymNamesForRevision(createBranchRevisionNumber(branchNumber));
458             if (names.size() > 0) {
459                 branchNames.append(names.get(0).getName());
460             } else {
461                 branchNamesAvailable = false;
462                 if (useNumbersIfNamesNotAvailable) {
463                     branchNames.append(branchNumber);
464                 } else {
465                     break;
466                 }
467             }
468             branchNames.append("; "); // NOI18N
469
}
470         if (branchNamesAvailable || useNumbersIfNamesNotAvailable) {
471             branchNames.delete(branchNames.length() - 2, branchNames.length());
472         } else {
473             branchNames.delete(0, branchNames.length());
474         }
475         return branchNames.toString();
476     }
477
478     /**
479      * Compares two {@link FileInformation} objects by importance of statuses they represent.
480      */

481     public static class ByImportanceComparator implements Comparator {
482         public int compare(Object JavaDoc o1, Object JavaDoc o2) {
483             FileInformation i1 = (FileInformation) o1;
484             FileInformation i2 = (FileInformation) o2;
485             return getComparableStatus(i1.getStatus()) - getComparableStatus(i2.getStatus());
486         }
487     }
488     
489     /**
490      * Gets integer status that can be used in comparators. The more important the status is for the user,
491      * the lower value it has. Conflict is 0, unknown status is 100.
492      *
493      * @return status constant suitable for 'by importance' comparators
494      */

495     public static int getComparableStatus(int status) {
496         switch (status) {
497         case FileInformation.STATUS_VERSIONED_CONFLICT:
498             return 0;
499         case FileInformation.STATUS_VERSIONED_MERGE:
500             return 1;
501         case FileInformation.STATUS_VERSIONED_DELETEDLOCALLY:
502             return 10;
503         case FileInformation.STATUS_VERSIONED_REMOVEDLOCALLY:
504             return 11;
505         case FileInformation.STATUS_NOTVERSIONED_NEWLOCALLY:
506             return 12;
507         case FileInformation.STATUS_VERSIONED_ADDEDLOCALLY:
508             return 13;
509         case FileInformation.STATUS_VERSIONED_MODIFIEDLOCALLY:
510             return 14;
511         case FileInformation.STATUS_VERSIONED_REMOVEDINREPOSITORY:
512             return 30;
513         case FileInformation.STATUS_VERSIONED_NEWINREPOSITORY:
514             return 31;
515         case FileInformation.STATUS_VERSIONED_MODIFIEDINREPOSITORY:
516             return 32;
517         case FileInformation.STATUS_VERSIONED_UPTODATE:
518             return 50;
519         case FileInformation.STATUS_NOTVERSIONED_EXCLUDED:
520             return 100;
521         case FileInformation.STATUS_NOTVERSIONED_NOTMANAGED:
522             return 101;
523         case FileInformation.STATUS_UNKNOWN:
524             return 102;
525         default:
526             throw new IllegalArgumentException JavaDoc("Unknown status: " + status); // NOI18N
527
}
528     }
529     
530     public static boolean isPartOfCVSMetadata(File JavaDoc file) {
531         return metadataPattern.matcher(file.getAbsolutePath()).matches();
532     }
533         
534     /** Like mkdirs but but using openide filesystems (firing events) */
535     public static FileObject mkfolders(File JavaDoc file) throws IOException JavaDoc {
536         if (file.isDirectory()) return FileUtil.toFileObject(file);
537
538         File JavaDoc parent = file.getParentFile();
539         
540         String JavaDoc path = file.getName();
541         while (parent.isDirectory() == false) {
542             path = parent.getName() + "/" + path; // NOI18N
543
parent = parent.getParentFile();
544         }
545
546         FileObject fo = FileUtil.toFileObject(parent);
547         return FileUtil.createFolder(fo, path);
548     }
549
550     /**
551      * 1) A tag must start with a letter
552      * 2) A tag must not contain characters: $,.:;@ SPACE TAB NEWLINE
553      * 3) Reserved tag names: HEAD BASE
554      *
555      * @param name
556      * @return true if the name of the tag is valid, false otherwise
557      */

558     public static boolean isTagValid(String JavaDoc name) {
559         if (name == null || name.length() == 0) return false;
560         char c = name.charAt(0);
561         if ((c < 'a' || c > 'z') && (c < 'A' || c > 'Z')) return false;
562         for (int i = 1; i < name.length(); i++) {
563             c = name.charAt(i);
564             if (c == '$' || c == ',' || c=='.' || c == ':' || c == ';' || c =='@' || c == ' ' || c == '\t' || c == '\n') return false;
565         }
566         if (name.equals("HEAD") || name.equals("BASE")) return false;
567         return true;
568     }
569 }
570
Popular Tags