KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > eclipse > ant > internal > ui > datatransfer > EclipseClasspath


1 /*******************************************************************************
2  * Copyright (c) 2004, 2006 Richard Hoefter and others.
3  * All rights reserved. This program and the accompanying materials
4  * are made available under the terms of the Eclipse Public License v1.0
5  * which accompanies this distribution, and is available at
6  * http://www.eclipse.org/legal/epl-v10.html
7  *
8  * Contributors:
9  * Richard Hoefter (richard.hoefter@web.de) - initial API and implementation, bug 95298
10  * IBM Corporation - nlsing and incorporating into Eclipse, bug 108276
11  *******************************************************************************/

12
13 package org.eclipse.ant.internal.ui.datatransfer;
14
15 import java.io.File JavaDoc;
16 import java.util.ArrayList JavaDoc;
17 import java.util.Arrays JavaDoc;
18 import java.util.HashMap JavaDoc;
19 import java.util.LinkedHashMap JavaDoc;
20 import java.util.List JavaDoc;
21 import java.util.Map JavaDoc;
22
23 import org.eclipse.ant.internal.ui.AntUIPlugin;
24 import org.eclipse.core.resources.IFile;
25 import org.eclipse.core.resources.ResourcesPlugin;
26 import org.eclipse.core.runtime.CoreException;
27 import org.eclipse.core.runtime.IPath;
28 import org.eclipse.core.runtime.Path;
29 import org.eclipse.debug.core.ILaunchConfiguration;
30 import org.eclipse.jdt.core.IClasspathContainer;
31 import org.eclipse.jdt.core.IClasspathEntry;
32 import org.eclipse.jdt.core.IJavaProject;
33 import org.eclipse.jdt.core.IPackageFragmentRoot;
34 import org.eclipse.jdt.core.JavaCore;
35 import org.eclipse.jdt.core.JavaModelException;
36 import org.eclipse.jdt.launching.IRuntimeClasspathEntry;
37 import org.eclipse.jdt.launching.JavaRuntime;
38 import org.w3c.dom.Document JavaDoc;
39 import org.w3c.dom.Element JavaDoc;
40
41 /**
42  * Class to inspect classpath of an Eclipse project.
43  */

44 public class EclipseClasspath
45 {
46     protected List JavaDoc srcDirs = new ArrayList JavaDoc();
47     protected List JavaDoc classDirs = new ArrayList JavaDoc();
48     protected List JavaDoc inclusionLists = new ArrayList JavaDoc();
49     protected List JavaDoc exclusionLists = new ArrayList JavaDoc();
50   
51     protected Map JavaDoc variable2valueMap = new LinkedHashMap JavaDoc();
52     protected List JavaDoc rawClassPathEntries = new ArrayList JavaDoc();
53     protected List JavaDoc rawClassPathEntriesAbsolute = new ArrayList JavaDoc();
54  
55     private IJavaProject project;
56     
57     private static Map JavaDoc userLibraryCache = new HashMap JavaDoc();
58
59     /**
60      * Initialize object with classpath of given project.
61      */

62     public EclipseClasspath(IJavaProject project) throws JavaModelException
63     {
64         init(project, project.getRawClasspath());
65     }
66   
67     /**
68      * Initialize object with runtime classpath of given launch configuration.
69      * @param project project that contains given launch configuration conf
70      * @param conf launch configuration
71      * @param bootstrap if true only bootstrap entries are added, if false only
72      * non-bootstrap entries are added
73      */

74     public EclipseClasspath(IJavaProject project, ILaunchConfiguration conf, boolean bootstrap)
75         throws JavaModelException
76     {
77         // convert IRuntimeClasspathEntry to IClasspathEntry
78
IRuntimeClasspathEntry[] runtimeEntries;
79         try
80         {
81             // see AbstractJavaLaunchConfigurationDelegate
82
runtimeEntries = JavaRuntime.computeUnresolvedRuntimeClasspath(conf);
83         }
84         catch (CoreException e)
85         {
86             throw new JavaModelException(e);
87         }
88         List JavaDoc classpathEntries = new ArrayList JavaDoc(runtimeEntries.length);
89         for (int i = 0; i < runtimeEntries.length; i++)
90         {
91             IRuntimeClasspathEntry entry = runtimeEntries[i];
92             if ( bootstrap && (entry.getClasspathProperty() == IRuntimeClasspathEntry.BOOTSTRAP_CLASSES) ||
93                 ! bootstrap && (entry.getClasspathProperty() != IRuntimeClasspathEntry.BOOTSTRAP_CLASSES))
94             {
95                 // NOTE: See AbstractJavaLaunchConfigurationDelegate.getBootpathExt()
96
// for an alternate bootclasspath detection
97
if (entry.getClass().getName().equals("org.eclipse.jdt.internal.launching.VariableClasspathEntry")) //$NON-NLS-1$
98
{
99                     IClasspathEntry e = convertVariableClasspathEntry(entry);
100                     if (e != null)
101                     {
102                         classpathEntries.add(e);
103                     }
104                 }
105                 else if (entry.getClass().getName().equals("org.eclipse.jdt.internal.launching.DefaultProjectClasspathEntry")) //$NON-NLS-1$
106
{
107                     IClasspathEntry e = JavaCore.newProjectEntry(entry.getPath());
108                     classpathEntries.add(e);
109                 }
110                 else if (entry.getClasspathEntry() != null)
111                 {
112                     classpathEntries.add(entry.getClasspathEntry());
113                 }
114             }
115         }
116         IClasspathEntry[] entries =
117             (IClasspathEntry[]) classpathEntries.toArray(new IClasspathEntry[classpathEntries.size()]);
118
119         init(project, entries);
120     }
121
122     private void init(IJavaProject javaProject, IClasspathEntry entries[]) throws JavaModelException
123     {
124         this.project = javaProject;
125         handle(entries);
126     }
127      
128     private void handle(IClasspathEntry[] entries) throws JavaModelException
129     {
130         for (int i = 0; i < entries.length; i++)
131         {
132             handleSources(entries[i]);
133             handleVariables(entries[i]);
134             handleJars(entries[i]);
135             handleLibraries(entries[i]);
136             handleProjects(entries[i]);
137         }
138     }
139
140     private void handleSources(IClasspathEntry entry) throws JavaModelException
141     {
142         String JavaDoc projectRoot = ExportUtil.getProjectRoot(project);
143         String JavaDoc defaultClassDir = project.getOutputLocation().toString();
144         String JavaDoc defaultClassDirAbsolute = ExportUtil.resolve(project.getOutputLocation());
145
146         if (entry.getContentKind() == IPackageFragmentRoot.K_SOURCE &&
147             entry.getEntryKind() == IClasspathEntry.CPE_SOURCE)
148         {
149             // found source path
150
IPath srcDirPath = entry.getPath();
151             IPath classDirPath = entry.getOutputLocation();
152             String JavaDoc srcDir = handleLinkedResource(srcDirPath);
153             ExportUtil.removeProjectRoot((srcDirPath != null) ? srcDirPath.toString() : projectRoot, project.getProject());
154             String JavaDoc classDir = ExportUtil.removeProjectRoot((classDirPath != null) ? classDirPath.toString() : defaultClassDir, project.getProject());
155             srcDirs.add(srcDir);
156             classDirs.add(classDir);
157             String JavaDoc classDirAbsolute = (classDirPath != null) ? ExportUtil.resolve(classDirPath) : defaultClassDirAbsolute;
158             rawClassPathEntries.add(classDir);
159             rawClassPathEntriesAbsolute.add(classDirAbsolute);
160             IPath[] inclusions = entry.getInclusionPatterns();
161             List JavaDoc inclusionList = new ArrayList JavaDoc();
162             for (int j = 0; j < inclusions.length; j++)
163             {
164                 if (inclusions[j] != null)
165                 {
166                     inclusionList.add(ExportUtil.removeProjectRoot(inclusions[j].toString(), project.getProject()));
167                 }
168             }
169             inclusionLists.add(inclusionList);
170             IPath[] exclusions = entry.getExclusionPatterns();
171             List JavaDoc exclusionList = new ArrayList JavaDoc();
172             for (int j = 0; j < exclusions.length; j++)
173             {
174                 if (exclusions[j] != null)
175                 {
176                     exclusionList.add(ExportUtil.removeProjectRoot(exclusions[j].toString(), project.getProject()));
177                 }
178             }
179             exclusionLists.add(exclusionList);
180         }
181     }
182     
183     /**
184      * Check if given source path is a linked resource. Add values to
185      * {@link #variable2valueMap} accordingly.
186      * @param srcDirPath source dir as IPath
187      * @return source directory with reference, e.g. ${MYPATH}/src, if it is no
188      * link, orginal source dir is returned
189      */

190     private String JavaDoc handleLinkedResource(IPath srcDirPath)
191     {
192         String JavaDoc projectRoot = ExportUtil.getProjectRoot(project);
193         String JavaDoc srcDir = ExportUtil.removeProjectRoot((srcDirPath != null) ? srcDirPath.toString() : projectRoot, project.getProject());
194         if (srcDirPath == null)
195         {
196             return srcDir;
197         }
198         IFile file;
199         try
200         {
201             file = ResourcesPlugin.getWorkspace().getRoot().getFile(srcDirPath);
202         }
203         catch (IllegalArgumentException JavaDoc e)
204         {
205             return srcDir;
206         }
207         if (file.isLinked())
208         {
209             String JavaDoc pathVariable = file.getRawLocation().segment(0).toString();
210             IPath pathVariableValue = file.getWorkspace().getPathVariableManager().getValue(pathVariable);
211             if (pathVariableValue != null)
212             {
213                 // path variable was used
214
String JavaDoc pathVariableExtension = file.getRawLocation().segment(1).toString();
215                 String JavaDoc relativePath = ExportUtil.getRelativePath(pathVariableValue.toString(),
216                         projectRoot);
217                 variable2valueMap.put(pathVariable + ".pathvariable", relativePath); //$NON-NLS-1$
218
variable2valueMap.put(srcDir + ".link", //$NON-NLS-1$
219
"${" + pathVariable + ".pathvariable}/" + pathVariableExtension); //$NON-NLS-1$ //$NON-NLS-2$
220
}
221             else
222             {
223                 String JavaDoc relativePath = ExportUtil.getRelativePath(file.getLocation() + "", //$NON-NLS-1$
224
projectRoot);
225                 variable2valueMap.put(srcDir + ".link", relativePath); //$NON-NLS-1$
226
}
227             srcDir = "${" + srcDir + ".link}"; //$NON-NLS-1$ //$NON-NLS-2$
228
}
229         return srcDir;
230     }
231
232     private void handleJars(IClasspathEntry entry)
233     {
234         if (entry.getContentKind() == IPackageFragmentRoot.K_BINARY &&
235             entry.getEntryKind() == IClasspathEntry.CPE_LIBRARY)
236         {
237             String JavaDoc jarFile = entry.getPath().toString();
238             StringBuffer JavaDoc jarFileBuffer = new StringBuffer JavaDoc();
239             StringBuffer JavaDoc jarFileAbsoluteBuffer = new StringBuffer JavaDoc();
240             String JavaDoc jarFileAbsolute = ExportUtil.resolve(entry.getPath());
241             if (jarFileAbsolute == null)
242             {
243                 jarFileAbsolute = jarFile; // jarFile was already absolute
244
if (handleSubProjectClassesDirectory(jarFile, jarFileBuffer, jarFileAbsoluteBuffer))
245                 {
246                     jarFile = jarFileBuffer.toString();
247                     jarFileAbsolute = jarFileAbsoluteBuffer.toString();
248                 }
249             }
250             String JavaDoc jarFileOld = jarFile;
251             jarFile = ExportUtil.removeProjectRoot(jarFile, project.getProject());
252             if (jarFile.equals(jarFileOld))
253             {
254                 if (handleSubProjectClassesDirectory(jarFile, jarFileBuffer, jarFileAbsoluteBuffer))
255                 {
256                     jarFile = jarFileBuffer.toString();
257                     jarFileAbsolute = jarFileAbsoluteBuffer.toString();
258                 }
259             }
260             rawClassPathEntries.add(jarFile);
261             rawClassPathEntriesAbsolute.add(jarFileAbsolute);
262         }
263     }
264
265     /**
266      * Checks if file is a class directory of a subproject and fills string buffers with resolved values.
267      * @param file file to check
268      * @param jarFile filled with file location with variable reference ${project.location},
269      * which is also added to variable2valueMap
270      * @param jarFileAbsolute filled with absolute file location
271      * @return true if file is a classes directory
272      */

273     private boolean handleSubProjectClassesDirectory(String JavaDoc file, StringBuffer JavaDoc jarFile, StringBuffer JavaDoc jarFileAbsolute)
274     {
275         // class directory of a subproject?
276
if (file != null && file.indexOf('/') == 0)
277         {
278             int i = file.indexOf('/', 1);
279             i = (i != -1) ? i : file.length();
280             String JavaDoc subproject = file.substring(1, i);
281             IJavaProject javaproject = ExportUtil.getJavaProjectByName(subproject);
282             if (javaproject != null)
283             {
284                 jarFile.setLength(0);
285                 jarFileAbsolute.setLength(0);
286                 String JavaDoc location = javaproject.getProject().getName() + ".location"; //$NON-NLS-1$
287
jarFileAbsolute.append(ExportUtil.replaceProjectRoot(file, javaproject.getProject(), ExportUtil.getProjectRoot(javaproject)));
288                 jarFile.append(ExportUtil.replaceProjectRoot(file, javaproject.getProject(), "${" + location + "}")); //$NON-NLS-1$ //$NON-NLS-2$
289
String JavaDoc projectRoot= ExportUtil.getProjectRoot(project);
290                 String JavaDoc relativePath = ExportUtil.getRelativePath(ExportUtil.getProjectRoot(javaproject),
291                         projectRoot);
292                 variable2valueMap.put(location, relativePath);
293                 return true;
294             }
295         }
296         return false;
297     }
298
299     private void handleVariables(IClasspathEntry entry)
300     {
301         if (entry.getContentKind() == IPackageFragmentRoot.K_SOURCE &&
302             entry.getEntryKind() == IClasspathEntry.CPE_VARIABLE)
303         {
304             // found variable
305
String JavaDoc e = entry.getPath().toString();
306             int index = e.indexOf('/');
307             if (index == -1)
308             {
309                 index = e.indexOf('\\');
310             }
311             String JavaDoc variable = e;
312             String JavaDoc path = ""; //$NON-NLS-1$
313
if (index != -1)
314             {
315                 variable = e.substring(0, index);
316                 path = e.substring(index);
317             }
318             IPath value = JavaCore.getClasspathVariable(variable);
319             if (value != null)
320             {
321                 String JavaDoc projectRoot = ExportUtil.getProjectRoot(project);
322                 String JavaDoc relativePath = ExportUtil.getRelativePath(value.toString(),
323                         projectRoot);
324                 variable2valueMap.put(variable, relativePath);
325             }
326             else if (variable2valueMap.get(variable) == null)
327             {
328                 // only add empty value, if variable is new
329
variable2valueMap.put(variable, ""); //$NON-NLS-1$
330
}
331             rawClassPathEntriesAbsolute.add(value + path);
332             rawClassPathEntries.add("${" + variable + "}" + path); //$NON-NLS-1$ //$NON-NLS-2$
333
}
334     }
335     
336     private void handleLibraries(IClasspathEntry entry) throws JavaModelException
337     {
338         if (entry.getContentKind() == IPackageFragmentRoot.K_SOURCE &&
339             entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER)
340         {
341             // found library
342
IClasspathContainer container = JavaCore.getClasspathContainer(entry.getPath(), project);
343             if (container == null) {
344                 // jar missing (project not compile clean)
345
return;
346             }
347             String JavaDoc jar = entry.getPath().toString();
348             if (jar.startsWith(JavaRuntime.JRE_CONTAINER))
349             {
350                 // JRE System Library
351
// ignore JRE libraries
352
//IClasspathEntry entries[] = container.getClasspathEntries();
353
//for (int i = 0; i < entries.length; i++)
354
//{
355
// handleJars(entries[i]);
356
//}
357
}
358             else if (jar.startsWith(JavaCore.USER_LIBRARY_CONTAINER_ID))
359             {
360                 // User Library
361
String JavaDoc libraryName = container.getDescription();
362                 String JavaDoc userlibraryRef = "${" + libraryName + ".userclasspath}"; //$NON-NLS-1$ //$NON-NLS-2$
363
if (container.getKind() == IClasspathContainer.K_SYSTEM)
364                 {
365                     userlibraryRef = "${" + libraryName + ".bootclasspath}"; //$NON-NLS-1$ //$NON-NLS-2$
366
}
367                 userLibraryCache.put(userlibraryRef, container);
368                 srcDirs.add(userlibraryRef);
369                 classDirs.add(userlibraryRef);
370                 rawClassPathEntries.add(userlibraryRef);
371                 rawClassPathEntriesAbsolute.add(userlibraryRef);
372                 inclusionLists.add(new ArrayList JavaDoc());
373                 exclusionLists.add(new ArrayList JavaDoc());
374             }
375             else
376             {
377                 // Library dependencies: e.g. Plug-in Dependencies
378
String JavaDoc libraryName = container.getDescription();
379                 String JavaDoc pluginRef = "${" + libraryName + ".libraryclasspath}"; //$NON-NLS-1$ //$NON-NLS-2$
380
userLibraryCache.put(pluginRef, container);
381                 srcDirs.add(pluginRef);
382                 classDirs.add(pluginRef);
383                 rawClassPathEntries.add(pluginRef);
384                 rawClassPathEntriesAbsolute.add(pluginRef);
385                 inclusionLists.add(new ArrayList JavaDoc());
386                 exclusionLists.add(new ArrayList JavaDoc());
387             }
388         }
389     }
390     
391     private void handleProjects(IClasspathEntry entry)
392     {
393         if (entry.getContentKind() == IPackageFragmentRoot.K_SOURCE &&
394             entry.getEntryKind() == IClasspathEntry.CPE_PROJECT)
395         {
396             // found required project on build path
397
String JavaDoc subProjectRoot = entry.getPath().toString();
398             IJavaProject subProject = ExportUtil.getJavaProject(subProjectRoot);
399             if (subProject == null)
400             {
401                 // project was not loaded in workspace
402
AntUIPlugin.log("project is not loaded in workspace: " + subProjectRoot, null); //$NON-NLS-1$
403
return;
404             }
405             // only add an indicator that this is a project reference
406
String JavaDoc classpathRef = "${" + subProject.getProject().getName() + ".classpath}"; //$NON-NLS-1$ //$NON-NLS-2$
407
srcDirs.add(classpathRef);
408             classDirs.add(classpathRef);
409             rawClassPathEntries.add(classpathRef);
410             rawClassPathEntriesAbsolute.add(classpathRef);
411             inclusionLists.add(new ArrayList JavaDoc());
412             exclusionLists.add(new ArrayList JavaDoc());
413         }
414     }
415
416     /**
417      * Get runtime classpath items for given project separated with path separator.
418      */

419     public static String JavaDoc getClasspath(IJavaProject project) throws CoreException
420     {
421         List JavaDoc items = getClasspathList(project);
422         return ExportUtil.toString(items, File.pathSeparator);
423     }
424
425     /**
426      * Get runtime classpath items for given project.
427      */

428     public static List JavaDoc getClasspathList(IJavaProject project) throws CoreException
429     {
430         String JavaDoc[] classpath = JavaRuntime.computeDefaultRuntimeClassPath(project);
431         return Arrays.asList(classpath);
432     }
433     
434     /**
435      * Check if given string is a reference.
436      */

437     public static boolean isReference(String JavaDoc s)
438     {
439         return isProjectReference(s) || isUserLibraryReference(s) ||
440             isUserSystemLibraryReference(s) || isLibraryReference(s);
441         // NOTE: A linked resource is no reference
442
}
443
444     /**
445      * Check if given string is a project reference.
446      */

447     public static boolean isProjectReference(String JavaDoc s)
448     {
449         return s.startsWith("${") && s.endsWith(".classpath}"); //$NON-NLS-1$ //$NON-NLS-2$
450
}
451
452     /**
453      * Resolves given project reference to a project.
454      * @return <code>null</code> if project is not resolvable
455      */

456     public static IJavaProject resolveProjectReference(String JavaDoc s)
457     {
458         String JavaDoc name = ExportUtil.removePrefixAndSuffix(s, "${", ".classpath}"); //$NON-NLS-1$ //$NON-NLS-2$
459
return ExportUtil.getJavaProjectByName(name);
460     }
461
462     /**
463      * Check if given string is a user library reference.
464      */

465     public static boolean isUserLibraryReference(String JavaDoc s)
466     {
467         return s.startsWith("${") && s.endsWith(".userclasspath}"); //$NON-NLS-1$ //$NON-NLS-2$
468
}
469
470     /**
471      * Check if given string is a user system library reference.
472      * This library is added to the compiler boot classpath.
473      */

474     public static boolean isUserSystemLibraryReference(String JavaDoc s)
475     {
476         return s.startsWith("${") && s.endsWith(".bootclasspath}"); //$NON-NLS-1$ //$NON-NLS-2$
477
}
478
479     /**
480      * Check if given string is a library reference. e.g. Plug-in dependencies
481      * are library references.
482      *
483      */

484     public static boolean isLibraryReference(String JavaDoc s)
485     {
486         return s.startsWith("${") && s.endsWith(".libraryclasspath}"); //$NON-NLS-1$ //$NON-NLS-2$
487
}
488
489     /**
490      * Resolves given user (system) library or plugin reference to its container.
491      *
492      * <p>NOTE: The library can only be resolved if an EclipseClasspath object
493      * was created which had a reference to this library. The class holds an
494      * internal cache to circumvent that UserLibraryManager is an internal
495      * class.
496      *
497      * @return null if library is not resolvable
498      */

499     public static IClasspathContainer resolveUserLibraryReference(String JavaDoc s)
500     {
501         return (IClasspathContainer) userLibraryCache.get(s);
502     }
503     
504     /**
505      * Check if given string is a linked resource.
506      *
507      */

508     public static boolean isLinkedResource(String JavaDoc s)
509     {
510         return s.startsWith("${") && s.endsWith(".link}"); //$NON-NLS-1$ //$NON-NLS-2$
511
}
512
513     /**
514      * Get source folder name of a linked resource.
515      *
516      * @see #isLinkedResource(String)
517      */

518     public static String JavaDoc getLinkedResourceName(String JavaDoc s)
519     {
520         return ExportUtil.removePrefixAndSuffix(s, "${", ".link}"); //$NON-NLS-1$ //$NON-NLS-2$
521
}
522     
523     /**
524      * Resolves given linked resource to an absolute file location.
525      */

526     public String JavaDoc resolveLinkedResource(String JavaDoc s)
527     {
528         String JavaDoc name = ExportUtil.removePrefixAndSuffix(s, "${", "}"); //$NON-NLS-1$ //$NON-NLS-2$
529
String JavaDoc value = (String JavaDoc) variable2valueMap.get(name);
530         String JavaDoc suffix = ".pathvariable}"; //$NON-NLS-1$
531
int i = value.indexOf(suffix);
532         if (i != -1)
533         {
534             // path variable
535
String JavaDoc pathVariable = value.substring(0, i + suffix.length() - 1);
536             pathVariable = ExportUtil.removePrefix(pathVariable, "${"); //$NON-NLS-1$
537
return (String JavaDoc) variable2valueMap.get(pathVariable) + value.substring(i + suffix.length());
538         }
539         return value;
540     }
541
542     /**
543      * Convert a VariableClasspathEntry to a IClasspathEntry.
544      *
545      * <p>This is a workaround as entry.getClasspathEntry() returns null.
546      */

547     private IClasspathEntry convertVariableClasspathEntry(IRuntimeClasspathEntry entry)
548     {
549         try
550         {
551             Document JavaDoc doc = ExportUtil.parseXmlString(entry.getMemento());
552             Element JavaDoc element = (Element JavaDoc) doc.getElementsByTagName("memento").item(0); //$NON-NLS-1$
553
String JavaDoc variableString = element.getAttribute("variableString"); //$NON-NLS-1$
554
ExportUtil.addVariable(variable2valueMap, variableString, ExportUtil.getProjectRoot(project));
555             // remove ${...} from string to be conform for handleVariables()
556
variableString = ExportUtil.removePrefix(variableString, "${");//$NON-NLS-1$
557
int i = variableString.indexOf('}');
558             if (i != -1)
559             {
560                 variableString = variableString.substring(0, i)
561                     + variableString.substring(i + 1);
562             }
563             IPath path = new Path(variableString);
564             return JavaCore.newVariableEntry(path, null, null);
565         }
566         catch (Exception JavaDoc e)
567         {
568             AntUIPlugin.log(e);
569             return null;
570         }
571
572     }
573 }
574
Popular Tags