KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > netbeans > modules > ruby > rubyproject > ui > customizer > RubySourceRootsUi


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.ruby.rubyproject.ui.customizer;
21
22 import java.awt.*;
23 import java.awt.event.ActionEvent JavaDoc;
24 import java.awt.event.ActionListener JavaDoc;
25 import java.io.File JavaDoc;
26 import java.net.URI JavaDoc;
27 import java.net.URL JavaDoc;
28 import java.util.ArrayList JavaDoc;
29 import java.util.Iterator JavaDoc;
30 import java.util.HashSet JavaDoc;
31 import java.util.Set JavaDoc;
32 import java.util.Vector JavaDoc;
33 import java.text.MessageFormat JavaDoc;
34 import javax.swing.*;
35 import javax.swing.JButton JavaDoc;
36 import javax.swing.event.ListSelectionEvent JavaDoc;
37 import javax.swing.event.ListSelectionListener JavaDoc;
38 import javax.swing.event.CellEditorListener JavaDoc;
39 import javax.swing.event.ChangeEvent JavaDoc;
40 import javax.swing.table.DefaultTableCellRenderer JavaDoc;
41 import javax.swing.table.DefaultTableModel JavaDoc;
42 import org.netbeans.api.project.ProjectUtils;
43 import org.netbeans.api.project.SourceGroup;
44 import org.netbeans.api.project.Sources;
45 import org.netbeans.modules.ruby.rubyproject.RubyProject;
46 import org.netbeans.modules.ruby.rubyproject.ui.FoldersListSettings;
47 import org.netbeans.api.project.FileOwnerQuery;
48 import org.netbeans.api.project.Project;
49 import org.netbeans.api.project.ProjectInformation;
50 import org.netbeans.modules.ruby.rubyproject.SourceRoots;
51 import org.openide.DialogDisplayer;
52 import org.openide.DialogDescriptor;
53 import org.openide.NotifyDescriptor;
54 import org.openide.filesystems.FileObject;
55 import org.openide.filesystems.FileUtil;
56 import org.openide.util.NbBundle;
57 import org.openide.util.HelpCtx;
58
59 /** Handles adding, removing, reordering of source roots.
60  *
61  * @author Tomas Zezula
62  */

63 public final class RubySourceRootsUi {
64   
65     public static DefaultTableModel JavaDoc createModel( SourceRoots roots ) {
66         
67         String JavaDoc[] rootLabels = roots.getRootNames();
68         String JavaDoc[] rootProps = roots.getRootProperties();
69         URL JavaDoc[] rootURLs = roots.getRootURLs();
70         Object JavaDoc[][] data = new Object JavaDoc[rootURLs.length] [2];
71         for (int i=0; i< rootURLs.length; i++) {
72             data[i][0] = new File JavaDoc (URI.create (rootURLs[i].toExternalForm()));
73             data[i][1] = roots.getRootDisplayName(rootLabels[i], rootProps[i]);
74         }
75         return new SourceRootsModel(data);
76                 
77     }
78     
79     public static EditMediator registerEditMediator( RubyProject master,
80                                              SourceRoots sourceRoots,
81                                              JTable rootsList,
82                                              JButton JavaDoc addFolderButton,
83                                              JButton JavaDoc removeButton,
84                                              JButton JavaDoc upButton,
85                                              JButton JavaDoc downButton) {
86         
87         EditMediator em = new EditMediator( master,
88                                             sourceRoots,
89                                             rootsList,
90                                             addFolderButton,
91                                             removeButton,
92                                             upButton,
93                                             downButton);
94         
95         // Register the listeners
96
// On all buttons
97
addFolderButton.addActionListener( em );
98         removeButton.addActionListener( em );
99         upButton.addActionListener( em );
100         downButton.addActionListener( em );
101         // On list selection
102
rootsList.getSelectionModel().addListSelectionListener( em );
103         DefaultCellEditor editor = new DefaultCellEditor(new JTextField());
104         editor.addCellEditorListener (em);
105         rootsList.setDefaultRenderer( File JavaDoc.class, new FileRenderer (FileUtil.toFile(master.getProjectDirectory())));
106         rootsList.setDefaultEditor(String JavaDoc.class, editor);
107         // Set the initial state of the buttons
108
em.valueChanged( null );
109         
110         DefaultTableModel JavaDoc model = (DefaultTableModel JavaDoc)rootsList.getModel();
111         String JavaDoc[] columnNames = new String JavaDoc[2];
112         columnNames[0] = NbBundle.getMessage( RubySourceRootsUi.class,"CTL_PackageFolders");
113         columnNames[1] = NbBundle.getMessage( RubySourceRootsUi.class,"CTL_PackageLabels");
114         model.setColumnIdentifiers(columnNames);
115         rootsList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
116         
117         return em;
118     }
119     
120     /**
121      * Opens the standard dialog for warning an user about illegal source roots.
122      * @param roots the set of illegal source/test roots
123      */

124     public static void showIllegalRootsDialog (Set JavaDoc/*<File>*/ roots) {
125         JButton JavaDoc closeOption = new JButton JavaDoc (NbBundle.getMessage(RubySourceRootsUi.class,"CTL_RubySourceRootsUi_Close"));
126         closeOption.getAccessibleContext ().setAccessibleDescription (NbBundle.getMessage(RubySourceRootsUi.class,"AD_RubySourceRootsUi_Close"));
127         JPanel warning = new WarningDlg (roots);
128         String JavaDoc message = NbBundle.getMessage(RubySourceRootsUi.class,"MSG_InvalidRoot");
129         JOptionPane optionPane = new JOptionPane (new Object JavaDoc[] {message, warning},
130             JOptionPane.WARNING_MESSAGE,
131             0,
132             null,
133             new Object JavaDoc[0],
134             null);
135         optionPane.getAccessibleContext().setAccessibleDescription (NbBundle.getMessage(RubySourceRootsUi.class,"AD_InvalidRootDlg"));
136         DialogDescriptor dd = new DialogDescriptor (optionPane,
137             NbBundle.getMessage(RubySourceRootsUi.class,"TITLE_InvalidRoot"),
138             true,
139             new Object JavaDoc[] {
140                 closeOption,
141             },
142             closeOption,
143             DialogDescriptor.DEFAULT_ALIGN,
144             null,
145             null);
146         DialogDisplayer.getDefault().notify(dd);
147     }
148         
149     // Private innerclasses ----------------------------------------------------------------
150

151     public static class EditMediator implements ActionListener JavaDoc, ListSelectionListener JavaDoc, CellEditorListener JavaDoc {
152
153         
154         final JTable rootsList;
155         final JButton JavaDoc addFolderButton;
156         final JButton JavaDoc removeButton;
157         final JButton JavaDoc upButton;
158         final JButton JavaDoc downButton;
159         private final Project project;
160         private final SourceRoots sourceRoots;
161         private final Set JavaDoc ownedFolders;
162         private DefaultTableModel JavaDoc rootsModel;
163         private EditMediator relatedEditMediator;
164         private File JavaDoc lastUsedDir; //Last used current folder in JFileChooser
165

166         
167         public EditMediator( RubyProject master,
168                              SourceRoots sourceRoots,
169                              JTable rootsList,
170                              JButton JavaDoc addFolderButton,
171                              JButton JavaDoc removeButton,
172                              JButton JavaDoc upButton,
173                              JButton JavaDoc downButton) {
174
175             if ( !( rootsList.getModel() instanceof DefaultTableModel JavaDoc ) ) {
176                 throw new IllegalArgumentException JavaDoc( "Jtable's model has to be of class DefaultTableModel" ); // NOI18N
177
}
178                     
179             this.rootsList = rootsList;
180             this.addFolderButton = addFolderButton;
181             this.removeButton = removeButton;
182             this.upButton = upButton;
183             this.downButton = downButton;
184             this.ownedFolders = new HashSet JavaDoc();
185
186             this.project = master;
187             this.sourceRoots = sourceRoots;
188
189             this.ownedFolders.clear();
190             this.rootsModel = (DefaultTableModel JavaDoc)rootsList.getModel();
191             Vector JavaDoc data = rootsModel.getDataVector();
192             for (Iterator JavaDoc it = data.iterator(); it.hasNext();) {
193                 Vector JavaDoc row = (Vector JavaDoc) it.next ();
194                 File JavaDoc f = (File JavaDoc) row.elementAt(0);
195                 this.ownedFolders.add (f);
196             }
197         }
198         
199         public void setRelatedEditMediator(EditMediator rem) {
200             this.relatedEditMediator = rem;
201         }
202         
203         // Implementation of ActionListener ------------------------------------
204

205         /** Handles button events
206          */

207         public void actionPerformed( ActionEvent JavaDoc e ) {
208             
209             Object JavaDoc source = e.getSource();
210             
211             if ( source == addFolderButton ) {
212                 
213                 // Let user search for the Jar file
214
JFileChooser chooser = new JFileChooser();
215                 FileUtil.preventFileChooserSymlinkTraversal(chooser, null);
216                 chooser.setFileSelectionMode( JFileChooser.DIRECTORIES_ONLY );
217                 chooser.setMultiSelectionEnabled( true );
218                 if (this.sourceRoots.isTest()) {
219                     chooser.setDialogTitle( NbBundle.getMessage( RubySourceRootsUi.class, "LBL_TestFolder_DialogTitle" )); // NOI18N
220
}
221                 else {
222                     chooser.setDialogTitle( NbBundle.getMessage( RubySourceRootsUi.class, "LBL_SourceFolder_DialogTitle" )); // NOI18N
223
}
224                 File JavaDoc curDir = this.lastUsedDir;
225                 if (curDir == null) {
226                     curDir = FileUtil.toFile(this.project.getProjectDirectory());
227                 }
228                 if (curDir != null) {
229                     chooser.setCurrentDirectory (curDir);
230                 }
231                 int option = chooser.showOpenDialog( SwingUtilities.getWindowAncestor( addFolderButton ) ); // Sow the chooser
232

233                 if ( option == JFileChooser.APPROVE_OPTION ) {
234                     curDir = chooser.getCurrentDirectory();
235                     if (curDir != null) {
236                         this.lastUsedDir = curDir;
237                         if (this.relatedEditMediator != null) {
238                             this.relatedEditMediator.lastUsedDir = curDir;
239                         }
240                     }
241                     File JavaDoc files[] = chooser.getSelectedFiles();
242                     addFolders( files );
243                 }
244                 
245             }
246             else if ( source == removeButton ) {
247                 removeElements();
248             }
249             else if ( source == upButton ) {
250                 moveUp();
251             }
252             else if ( source == downButton ) {
253                 moveDown();
254             }
255         }
256         
257         // Selection listener implementation ----------------------------------
258

259         /** Handles changes in the selection
260          */

261         public void valueChanged( ListSelectionEvent JavaDoc e ) {
262             
263             int[] si = rootsList.getSelectedRows();
264             
265             // addJar allways enabled
266

267             // addLibrary allways enabled
268

269             // addArtifact allways enabled
270

271             // edit enabled only if selection is not empty
272
boolean edit = si != null && si.length > 0;
273
274             // remove enabled only if selection is not empty
275
boolean remove = si != null && si.length > 0;
276             // and when the selection does not contain unremovable item
277

278             // up button enabled if selection is not empty
279
// and the first selected index is not the first row
280
boolean up = si != null && si.length > 0 && si[0] != 0;
281             
282             // up button enabled if selection is not empty
283
// and the laset selected index is not the last row
284
boolean down = si != null && si.length > 0 && si[si.length-1] !=rootsList.getRowCount() - 1;
285
286             removeButton.setEnabled( remove );
287             upButton.setEnabled( up );
288             downButton.setEnabled( down );
289                         
290             //System.out.println("Selection changed " + edit + ", " + remove + ", " + + ", " + + ", ");
291

292         }
293
294         public void editingCanceled(ChangeEvent JavaDoc e) {
295
296         }
297
298         public void editingStopped(ChangeEvent JavaDoc e) {
299             // fireActionPerformed();
300
}
301         
302         private void addFolders( File JavaDoc files[] ) {
303             int[] si = rootsList.getSelectedRows();
304             int lastIndex = si == null || si.length == 0 ? -1 : si[si.length - 1];
305             ListSelectionModel selectionModel = this.rootsList.getSelectionModel();
306             selectionModel.clearSelection();
307             Set JavaDoc rootsFromOtherProjects = new HashSet JavaDoc ();
308             Set JavaDoc rootsFromRelatedSourceRoots = new HashSet JavaDoc();
309 out: for( int i = 0; i < files.length; i++ ) {
310                 File JavaDoc normalizedFile = FileUtil.normalizeFile(files[i]);
311                 Project p;
312                 if (ownedFolders.contains(normalizedFile)) {
313                     Vector JavaDoc dataVector = rootsModel.getDataVector();
314                     for (int j=0; j<dataVector.size();j++) {
315                         //Sequential search in this minor case is faster than update of positions during each modification
316
File JavaDoc f = (File JavaDoc )((Vector JavaDoc)dataVector.elementAt(j)).elementAt(0);
317                         if (f.equals(normalizedFile)) {
318                             selectionModel.addSelectionInterval(j,j);
319                         }
320                     }
321                 }
322                 else if (this.relatedEditMediator != null && this.relatedEditMediator.ownedFolders.contains(normalizedFile)) {
323                     rootsFromRelatedSourceRoots.add (normalizedFile);
324                     continue;
325                 }
326                 if ((p=FileOwnerQuery.getOwner(normalizedFile.toURI()))!=null && !p.getProjectDirectory().equals(project.getProjectDirectory())) {
327                     final Sources sources = (Sources) p.getLookup().lookup (Sources.class);
328                     if (sources == null) {
329                         rootsFromOtherProjects.add (normalizedFile);
330                         continue;
331                     }
332                     final SourceGroup[] sourceGroups = sources.getSourceGroups(Sources.TYPE_GENERIC);
333                     final SourceGroup[] javaGroups = sources.getSourceGroups(RubyProject.SOURCES_TYPE_RUBY);
334                     final SourceGroup[] groups = new SourceGroup [sourceGroups.length + javaGroups.length];
335                     System.arraycopy(sourceGroups,0,groups,0,sourceGroups.length);
336                     System.arraycopy(javaGroups,0,groups,sourceGroups.length,javaGroups.length);
337                     final FileObject projectDirectory = p.getProjectDirectory();
338                     final FileObject fileObject = FileUtil.toFileObject(normalizedFile);
339                     if (projectDirectory == null || fileObject == null) {
340                         rootsFromOtherProjects.add (normalizedFile);
341                         continue;
342                     }
343                     for (int j=0; j<groups.length; j++) {
344                         final FileObject sgRoot = groups[j].getRootFolder();
345                         if (fileObject.equals(sgRoot)) {
346                             rootsFromOtherProjects.add (normalizedFile);
347                             continue out;
348                         }
349                         if (!projectDirectory.equals(sgRoot) && FileUtil.isParentOf(sgRoot, fileObject)) {
350                             rootsFromOtherProjects.add (normalizedFile);
351                             continue out;
352                         }
353                     }
354                 }
355                 int current = lastIndex + 1 + i;
356                 rootsModel.insertRow( current, new Object JavaDoc[] {normalizedFile, sourceRoots.createInitialDisplayName(normalizedFile)}); //NOI18N
357
selectionModel.addSelectionInterval(current,current);
358                 this.ownedFolders.add (normalizedFile);
359             }
360             if (rootsFromOtherProjects.size() > 0 || rootsFromRelatedSourceRoots.size() > 0) {
361                 rootsFromOtherProjects.addAll(rootsFromRelatedSourceRoots);
362                 showIllegalRootsDialog (rootsFromOtherProjects);
363             }
364             // fireActionPerformed();
365
}
366
367         private void removeElements() {
368
369             int[] si = rootsList.getSelectedRows();
370
371             if( si == null || si.length == 0 ) {
372                 assert false : "Remove button should be disabled"; // NOI18N
373
}
374
375             // Remove the items
376
for( int i = si.length - 1 ; i >= 0 ; i-- ) {
377                 this.ownedFolders.remove(((Vector JavaDoc)rootsModel.getDataVector().elementAt(si[i])).elementAt(0));
378                 rootsModel.removeRow( si[i] );
379             }
380
381
382             if ( rootsModel.getRowCount() != 0) {
383                 // Select reasonable item
384
int selectedIndex = si[si.length - 1] - si.length + 1;
385                 if ( selectedIndex > rootsModel.getRowCount() - 1) {
386                     selectedIndex = rootsModel.getRowCount() - 1;
387                 }
388                 rootsList.setRowSelectionInterval( selectedIndex, selectedIndex );
389             }
390
391             // fireActionPerformed();
392

393         }
394
395         private void moveUp() {
396
397             int[] si = rootsList.getSelectedRows();
398
399             if( si == null || si.length == 0 ) {
400                 assert false : "MoveUp button should be disabled"; // NOI18N
401
}
402
403             // Move the items up
404
ListSelectionModel selectionModel = this.rootsList.getSelectionModel();
405             selectionModel.clearSelection();
406             for( int i = 0; i < si.length; i++ ) {
407                 Vector JavaDoc item = (Vector JavaDoc) rootsModel.getDataVector().elementAt(si[i]);
408                 int newIndex = si[i]-1;
409                 rootsModel.removeRow( si[i] );
410                 rootsModel.insertRow( newIndex, item );
411                 selectionModel.addSelectionInterval(newIndex,newIndex);
412             }
413             // fireActionPerformed();
414
}
415
416         private void moveDown() {
417
418             int[] si = rootsList.getSelectedRows();
419
420             if( si == null || si.length == 0 ) {
421                 assert false : "MoveDown button should be disabled"; // NOI18N
422
}
423
424             // Move the items up
425
ListSelectionModel selectionModel = this.rootsList.getSelectionModel();
426             selectionModel.clearSelection();
427             for( int i = si.length -1 ; i >= 0 ; i-- ) {
428                 Vector JavaDoc item = (Vector JavaDoc) rootsModel.getDataVector().elementAt(si[i]);
429                 int newIndex = si[i] + 1;
430                 rootsModel.removeRow( si[i] );
431                 rootsModel.insertRow( newIndex, item );
432                 selectionModel.addSelectionInterval(newIndex,newIndex);
433             }
434             // fireActionPerformed();
435
}
436         
437
438     }
439
440     private static class SourceRootsModel extends DefaultTableModel JavaDoc {
441
442         public SourceRootsModel (Object JavaDoc[][] data) {
443             super (data,new Object JavaDoc[]{"location","label"});//NOI18N
444
}
445
446         public boolean isCellEditable(int row, int column) {
447             return column == 1;
448         }
449
450         public Class JavaDoc getColumnClass(int columnIndex) {
451             switch (columnIndex) {
452                 case 0:
453                     return File JavaDoc.class;
454                 case 1:
455                     return String JavaDoc.class;
456                 default:
457                     return super.getColumnClass (columnIndex);
458             }
459         }
460     }
461     
462     private static class FileRenderer extends DefaultTableCellRenderer JavaDoc {
463         
464         private File JavaDoc projectFolder;
465         
466         public FileRenderer (File JavaDoc projectFolder) {
467             this.projectFolder = projectFolder;
468         }
469         
470         public Component getTableCellRendererComponent(JTable table, Object JavaDoc value, boolean isSelected, boolean hasFocus,int row, int column) {
471             String JavaDoc displayName;
472             if (value instanceof File JavaDoc) {
473                 File JavaDoc root = (File JavaDoc) value;
474                 String JavaDoc pfPath = projectFolder.getAbsolutePath() + File.separatorChar;
475                 String JavaDoc srPath = root.getAbsolutePath();
476                 if (srPath.startsWith(pfPath)) {
477                     displayName = srPath.substring(pfPath.length());
478                 }
479                 else {
480                     displayName = srPath;
481                 }
482             }
483             else {
484                 displayName = null;
485             }
486             Component c = super.getTableCellRendererComponent(table, displayName, isSelected, hasFocus, row, column);
487             if (c instanceof JComponent) {
488                 ((JComponent) c).setToolTipText (displayName);
489             }
490             return c;
491         }
492         
493     }
494
495     private static class WarningDlg extends JPanel {
496
497         public WarningDlg (Set JavaDoc invalidRoots) {
498             this.initGui (invalidRoots);
499         }
500
501         private void initGui (Set JavaDoc invalidRoots) {
502             setLayout( new GridBagLayout ());
503             JLabel label = new JLabel ();
504             label.setText (NbBundle.getMessage(RubySourceRootsUi.class,"LBL_InvalidRoot"));
505             label.setDisplayedMnemonic(NbBundle.getMessage(RubySourceRootsUi.class,"MNE_InvalidRoot").charAt(0));
506             GridBagConstraints c = new GridBagConstraints();
507             c.gridx = GridBagConstraints.RELATIVE;
508             c.gridy = GridBagConstraints.RELATIVE;
509             c.gridwidth = GridBagConstraints.REMAINDER;
510             c.fill = GridBagConstraints.HORIZONTAL;
511             c.anchor = GridBagConstraints.NORTHWEST;
512             c.weightx = 1.0;
513             c.insets = new Insets (12,0,6,0);
514             ((GridBagLayout)this.getLayout()).setConstraints(label,c);
515             this.add (label);
516             JList roots = new JList (invalidRoots.toArray());
517             roots.setCellRenderer (new InvalidRootRenderer(true));
518             JScrollPane p = new JScrollPane (roots);
519             c = new GridBagConstraints();
520             c.gridx = GridBagConstraints.RELATIVE;
521             c.gridy = GridBagConstraints.RELATIVE;
522             c.gridwidth = GridBagConstraints.REMAINDER;
523             c.fill = GridBagConstraints.BOTH;
524             c.anchor = GridBagConstraints.NORTHWEST;
525             c.weightx = c.weighty = 1.0;
526             c.insets = new Insets (0,0,12,0);
527             ((GridBagLayout)this.getLayout()).setConstraints(p,c);
528             this.add (p);
529             label.setLabelFor(roots);
530             roots.getAccessibleContext().setAccessibleDescription (NbBundle.getMessage(RubySourceRootsUi.class,"AD_InvalidRoot"));
531             JLabel label2 = new JLabel ();
532             label2.setText (NbBundle.getMessage(RubySourceRootsUi.class,"MSG_InvalidRoot2"));
533             c = new GridBagConstraints();
534             c.gridx = GridBagConstraints.RELATIVE;
535             c.gridy = GridBagConstraints.RELATIVE;
536             c.gridwidth = GridBagConstraints.REMAINDER;
537             c.fill = GridBagConstraints.HORIZONTAL;
538             c.anchor = GridBagConstraints.NORTHWEST;
539             c.weightx = 1.0;
540             c.insets = new Insets (0,0,0,0);
541             ((GridBagLayout)this.getLayout()).setConstraints(label2,c);
542             this.add (label2);
543         }
544
545         private static class InvalidRootRenderer extends DefaultListCellRenderer {
546
547             private boolean projectConflict;
548
549             public InvalidRootRenderer (boolean projectConflict) {
550                 this.projectConflict = projectConflict;
551             }
552
553             public Component getListCellRendererComponent(JList list, Object JavaDoc value, int index, boolean isSelected, boolean cellHasFocus) {
554                 File JavaDoc f = (File JavaDoc) value;
555                 String JavaDoc message = f.getAbsolutePath();
556                 if (projectConflict) {
557                     Project p = FileOwnerQuery.getOwner(f.toURI());
558                     if (p!=null) {
559                         ProjectInformation pi = ProjectUtils.getInformation(p);
560                         String JavaDoc projectName = pi.getDisplayName();
561                         message = MessageFormat.format (NbBundle.getMessage(RubySourceRootsUi.class,"TXT_RootOwnedByProject"), new Object JavaDoc[] {
562                             message,
563                             projectName});
564                     }
565                 }
566                 return super.getListCellRendererComponent(list, message, index, isSelected, cellHasFocus);
567             }
568         }
569     }
570
571 }
572
Popular Tags