1 19 20 package org.netbeans.modules.j2ee.earproject.ui; 21 22 import java.awt.Image ; 23 import java.awt.event.ActionEvent ; 24 import java.beans.PropertyChangeEvent ; 25 import java.beans.PropertyChangeListener ; 26 import java.io.CharConversionException ; 27 import java.util.ArrayList ; 28 import java.util.Arrays ; 29 import java.util.Collection ; 30 import java.util.Enumeration ; 31 import java.util.HashMap ; 32 import java.util.HashSet ; 33 import java.util.Iterator ; 34 import java.util.List ; 35 import java.util.Map ; 36 import java.util.ResourceBundle ; 37 import java.util.Set ; 38 import java.util.StringTokenizer ; 39 import javax.swing.AbstractAction ; 40 import javax.swing.Action ; 41 import javax.swing.JSeparator ; 42 import javax.swing.event.ChangeEvent ; 43 import javax.swing.event.ChangeListener ; 44 import org.netbeans.api.java.platform.JavaPlatformManager; 45 import org.netbeans.api.project.FileOwnerQuery; 46 import org.netbeans.api.project.Project; 47 import org.netbeans.api.project.ProjectUtils; 48 import org.netbeans.api.project.SourceGroup; 49 import org.netbeans.api.project.Sources; 50 import org.netbeans.modules.j2ee.api.ejbjar.EjbProjectConstants; 51 import org.netbeans.modules.j2ee.common.ui.BrokenServerSupport; 52 import org.netbeans.modules.j2ee.deployment.devmodules.api.J2eeModule; 53 import org.netbeans.modules.j2ee.deployment.devmodules.spi.InstanceListener; 54 import org.netbeans.modules.j2ee.deployment.devmodules.spi.J2eeModuleProvider; 55 import org.netbeans.modules.j2ee.earproject.BrokenProjectSupport; 56 import org.netbeans.modules.j2ee.earproject.EarProject; 57 import org.netbeans.modules.j2ee.earproject.UpdateHelper; 58 import org.netbeans.modules.j2ee.earproject.ui.customizer.EarProjectProperties; 59 import org.netbeans.spi.java.project.support.ui.BrokenReferencesSupport; 60 import org.netbeans.spi.java.project.support.ui.PackageView; 61 import org.netbeans.spi.project.ActionProvider; 62 import org.netbeans.spi.project.support.ant.AntBasedProjectType; 63 import org.netbeans.spi.project.support.ant.AntProjectHelper; 64 import org.netbeans.spi.project.support.ant.PropertyEvaluator; 65 import org.netbeans.spi.project.support.ant.ReferenceHelper; 66 import org.netbeans.spi.project.ui.LogicalViewProvider; 67 import org.netbeans.spi.project.ui.support.CommonProjectActions; 68 import org.netbeans.spi.project.ui.support.DefaultProjectOperations; 69 import org.netbeans.spi.project.ui.support.ProjectSensitiveActions; 70 import org.openide.ErrorManager; 71 import org.openide.actions.FindAction; 72 import org.openide.actions.ToolsAction; 73 import org.openide.filesystems.FileObject; 74 import org.openide.filesystems.FileStateInvalidException; 75 import org.openide.filesystems.FileStatusEvent; 76 import org.openide.filesystems.FileStatusListener; 77 import org.openide.filesystems.FileSystem; 78 import org.openide.filesystems.FileUtil; 79 import org.openide.filesystems.Repository; 80 import org.openide.loaders.DataFolder; 81 import org.openide.loaders.DataObject; 82 import org.openide.loaders.DataObjectNotFoundException; 83 import org.openide.loaders.FolderLookup; 84 import org.openide.nodes.AbstractNode; 85 import org.openide.nodes.Node; 86 import org.openide.nodes.NodeOp; 87 import org.openide.util.ContextAwareAction; 88 import org.openide.util.HelpCtx; 89 import org.openide.util.Lookup; 90 import org.openide.util.NbBundle; 91 import org.openide.util.RequestProcessor; 92 import org.openide.util.Utilities; 93 import org.openide.util.WeakListeners; 94 import org.openide.util.actions.SystemAction; 95 import org.openide.util.lookup.Lookups; 96 import org.openide.xml.XMLUtil; 97 98 101 public class J2eeArchiveLogicalViewProvider implements LogicalViewProvider { 102 103 private final EarProject project; 104 protected final UpdateHelper helper; 105 private final PropertyEvaluator evaluator; 106 protected final ReferenceHelper resolver; 107 private final List <? extends Action > specialActions; 108 private final AntBasedProjectType abpt; 109 110 public J2eeArchiveLogicalViewProvider(EarProject project, UpdateHelper helper, 111 PropertyEvaluator evaluator, ReferenceHelper resolver, 112 List <? extends Action > specialActions, AntBasedProjectType abpt) { 113 this.project = project; 114 assert project != null; 115 this.helper = helper; 116 assert helper != null; 117 this.evaluator = evaluator; 118 assert evaluator != null; 119 this.resolver = resolver; 120 this.specialActions = specialActions; 121 this.abpt = abpt; 122 } 123 124 public Node createLogicalView() { 125 return new ArchiveLogicalViewRootNode(); 126 } 127 128 public Node findPath(Node root, Object target) { 129 Project project = (Project) root.getLookup().lookup(Project.class); 130 if (project == null) { 131 return null; 132 } 133 134 if (target instanceof FileObject) { 137 FileObject fo = (FileObject) target; 138 Project owner = FileOwnerQuery.getOwner(fo); 139 if (!project.equals(owner)) { 140 return null; } 142 Node result = findNodeUnderConfiguration(root, fo); 144 if (result != null) { 145 return result; 146 } 147 Node[] nodes = root.getChildren().getNodes(true); 149 for (int i = nodes.length-1; i >= 0; i--) { 150 result = PackageView.findPath(nodes[i], target); 151 if (result!=null) { 152 return result; 153 } 154 } 155 } 156 return null; 157 } 158 159 private Node findNodeUnderConfiguration(Node root, FileObject fo) { 160 FileObject rootfo = helper.getAntProjectHelper().resolveFileObject(evaluator.getProperty(EarProjectProperties.META_INF)); 161 String relPath = FileUtil.getRelativePath(rootfo, fo); 162 if (relPath == null) { 163 return null; 164 } 165 int idx = relPath.indexOf('.'); if (idx != -1) { 167 relPath = relPath.substring(0, idx); 168 } 169 StringTokenizer st = new StringTokenizer (relPath, "/"); Node result = NodeOp.findChild(root,rootfo.getName()); 171 while (st.hasMoreTokens()) { 172 result = NodeOp.findChild(result, st.nextToken()); 173 } 174 175 return result; 176 } 177 178 private static Lookup createLookup( Project project, AntProjectHelper c ) { 179 DataFolder rootFolder = DataFolder.findFolder( project.getProjectDirectory() ); 180 Lookup ret = null; 182 if (null == c) { 183 ret = Lookups.fixed( new Object [] { project, rootFolder }); 184 } else { 185 ret = Lookups.fixed( new Object [] { project, rootFolder, c } ); 186 } 187 return ret; 188 } 189 190 192 private static final String [] BREAKABLE_PROPERTIES = new String [] { 193 EarProjectProperties.JAVAC_CLASSPATH, 194 EarProjectProperties.DEBUG_CLASSPATH, 195 EarProjectProperties.JAR_CONTENT_ADDITIONAL, 196 }; 197 198 public static boolean hasBrokenLinks(AntProjectHelper helper, ReferenceHelper resolver) { 199 return BrokenReferencesSupport.isBroken(helper, resolver, BREAKABLE_PROPERTIES, 200 new String [] { EarProjectProperties.JAVA_PLATFORM}); 201 } 202 203 private String getIconBase() { 204 IconBaseProvider ibp = (IconBaseProvider) project.getLookup().lookup(IconBaseProvider.class); 205 return (null == ibp) 206 ? "org/netbeans/modules/j2ee/earproject/ui/resources/" : ibp.getIconBase(); 208 } 209 210 211 final class ArchiveLogicalViewRootNode extends AbstractNode implements Runnable , FileStatusListener, ChangeListener , PropertyChangeListener { 212 213 private static final String BROKEN_PROJECT_BADGE = "org/netbeans/modules/j2ee/earproject/ui/resources/brokenProjectBadge.gif"; 215 private Action brokenLinksAction; 216 private final BrokenServerAction brokenServerAction; 217 private final BrokenProjectSupport brokenProjectSupport; 218 private boolean broken; 219 220 private Set <FileObject> files; 222 private Map <FileSystem, FileStatusListener> fileSystemListeners; 223 private RequestProcessor.Task task; 224 private final Object privateLock = new Object (); 225 private boolean iconChange; 226 private boolean nameChange; 227 private ChangeListener sourcesListener; 228 private Map <SourceGroup, PropertyChangeListener > groupsListeners; 229 231 public ArchiveLogicalViewRootNode() { 232 super(new ArchiveViews.LogicalViewChildren(project, helper.getAntProjectHelper(), evaluator), 233 createLookup(project, helper.getAntProjectHelper())); 234 setIconBaseWithExtension(getIconBase() + "projectIcon.gif"); super.setName( ProjectUtils.getInformation( project ).getDisplayName() ); 236 if (hasBrokenLinks(helper.getAntProjectHelper(), resolver)) { 237 broken = true; 238 } 239 brokenServerAction = new BrokenServerAction(); 240 J2eeModuleProvider moduleProvider = (J2eeModuleProvider)project.getLookup().lookup(J2eeModuleProvider.class); 241 moduleProvider.addInstanceListener((InstanceListener)WeakListeners.create( 242 InstanceListener.class, brokenServerAction, moduleProvider)); 243 refreshProjectFiles(); 244 this.brokenProjectSupport = (BrokenProjectSupport) 245 project.getLookup().lookup(BrokenProjectSupport.class); 246 this.brokenProjectSupport.addChangeListener(new ChangeListener () { 247 public void stateChanged(ChangeEvent e) { 248 checkProjectValidity(); 249 } 250 }); 251 } 252 253 private void refreshProjectFiles() { 254 setFiles(getProjectFiles()); 255 } 256 257 258 Set <FileObject> getProjectFiles() { 259 Sources sources = ProjectUtils.getSources(project); if (sourcesListener == null) { 261 sourcesListener = WeakListeners.change(this, sources); 262 sources.addChangeListener(sourcesListener); 263 } 264 return getProjectFiles(Arrays.asList(sources.getSourceGroups(Sources.TYPE_GENERIC))); 265 } 266 267 private Set <FileObject> getProjectFiles(Collection <SourceGroup> groups) { 268 if (groupsListeners != null) { 269 for (SourceGroup group : groupsListeners.keySet()) { 270 PropertyChangeListener pcl = groupsListeners.get(group); 271 group.removePropertyChangeListener(pcl); 272 } 273 } 274 groupsListeners = new HashMap <SourceGroup, PropertyChangeListener >(); 275 Set <FileObject> files = new HashSet <FileObject>(); 276 for (SourceGroup group : groups) { 277 PropertyChangeListener pcl = WeakListeners.propertyChange(this, group); 278 groupsListeners.put(group, pcl); 279 group.addPropertyChangeListener(pcl); 280 FileObject groupRoot = group.getRootFolder(); 281 if (project.getProjectDirectory().equals(groupRoot)) { 282 Enumeration en = project.getProjectDirectory().getChildren(false); 285 while (en.hasMoreElements()) { 286 FileObject child = (FileObject) en.nextElement(); 287 if (FileOwnerQuery.getOwner(child) == project) { 288 files.add(child); 289 } 290 } 291 } else { files.add(groupRoot); 293 } 294 } 295 return files; 296 } 297 298 private final void setFiles(Set <FileObject> files) { 299 if (fileSystemListeners != null) { 300 for (FileSystem fs : fileSystemListeners.keySet()) { 301 FileStatusListener fsl = fileSystemListeners.get(fs); 302 fs.removeFileStatusListener(fsl); 303 } 304 } 305 306 fileSystemListeners = new HashMap <FileSystem, FileStatusListener>(); 307 this.files = files; 308 if (files == null) { 309 return; 310 } 311 312 Set <FileSystem> hookedFileSystems = new HashSet <FileSystem>(); 313 for (FileObject fo : files) { 314 try { 315 FileSystem fs = fo.getFileSystem(); 316 if (hookedFileSystems.contains(fs)) { 317 continue; 318 } 319 hookedFileSystems.add(fs); 320 FileStatusListener fsl = FileUtil.weakFileStatusListener(this, fs); 321 fs.addFileStatusListener(fsl); 322 fileSystemListeners.put(fs, fsl); 323 } catch (FileStateInvalidException e) { 324 ErrorManager err = ErrorManager.getDefault(); 325 err.annotate(e, "Can not get " + fo + " filesystem, ignoring..."); err.notify(ErrorManager.INFORMATIONAL, e); 327 } 328 } 329 } 330 331 private synchronized void checkProjectValidity() { 332 boolean old = broken; 333 broken = brokenProjectSupport.hasBrokenArtifacts(); 334 if (!broken) { 335 broken = hasBrokenLinks(helper.getAntProjectHelper(), resolver); 336 } 337 if (old != broken) { 338 getBrokenLinksAction().setEnabled(broken); 339 fireIconChange(); 340 fireOpenedIconChange(); 341 fireDisplayNameChange(null, null); 342 } 343 } 344 345 public Action getBrokenLinksAction() { 346 if (broken && brokenLinksAction == null) { 347 brokenLinksAction = new BrokenLinksAction(); 348 } 349 return brokenLinksAction; 350 } 351 352 public String getHtmlDisplayName() { 353 String dispName = super.getDisplayName(); 354 try { 355 dispName = XMLUtil.toElementContent(dispName); 356 } catch (CharConversionException ex) { 357 } 359 return broken || brokenServerAction.isEnabled() ? "<font color=\"#A40000\">" + dispName + "</font>" : null; } 361 362 public Image getIcon(int type) { 363 Image img = getMyIcon(type); 364 365 if (files != null && files.iterator().hasNext()) { 366 try { 367 FileObject fo = (FileObject) files.iterator().next(); 368 img = fo.getFileSystem().getStatus().annotateIcon(img, type, files); 369 } catch (FileStateInvalidException e) { 370 ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, e); 371 } 372 } 373 374 return img; 375 } 376 377 public Image getOpenedIcon(int type) { 378 Image img = getMyOpenedIcon(type); 379 380 if (files != null && files.iterator().hasNext()) { 381 try { 382 FileObject fo = (FileObject) files.iterator().next(); 383 img = fo.getFileSystem().getStatus().annotateIcon(img, type, files); 384 } catch (FileStateInvalidException e) { 385 ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, e); 386 } 387 } 388 389 return img; 390 } 391 392 public Action [] getActions( boolean context ) { 393 return context ? super.getActions(true) : getAdditionalActions(); 394 } 395 396 public boolean canRename() { 397 return true; 398 } 399 400 public void setName(String s) { 401 DefaultProjectOperations.performDefaultRenameOperation(project, s); 402 } 403 404 public void run() { 405 boolean fireIcon; 406 boolean fireName; 407 synchronized (privateLock) { 408 fireIcon = iconChange; 409 fireName = nameChange; 410 iconChange = false; 411 nameChange = false; 412 } 413 if (fireIcon) { 414 fireIconChange(); 415 fireOpenedIconChange(); 416 } 417 if (fireName) { 418 fireDisplayNameChange(null, null); 419 } 420 } 421 422 public void annotationChanged(FileStatusEvent event) { 423 if (task == null) { 424 task = RequestProcessor.getDefault().create(this); 425 } 426 427 synchronized (privateLock) { 428 if ((iconChange == false && event.isIconChange()) || (nameChange == false && event.isNameChange())) { 429 for (FileObject fo : files) { 430 if (event.hasChanged(fo)) { 431 iconChange |= event.isIconChange(); 432 nameChange |= event.isNameChange(); 433 } 434 } 435 } 436 } 437 438 task.schedule(50); } 440 441 public void stateChanged(ChangeEvent e) { 443 refreshProjectFiles(); 444 } 445 446 public void propertyChange(PropertyChangeEvent evt) { 448 refreshProjectFiles(); 449 } 450 451 public Image getMyIcon(int type) { 452 Image original = super.getIcon( type ); 453 return broken || brokenServerAction.isEnabled() 454 ? Utilities.mergeImages(original, Utilities.loadImage(BROKEN_PROJECT_BADGE), 8, 0) 455 : original; 456 } 457 458 public Image getMyOpenedIcon(int type) { 459 Image original = super.getOpenedIcon(type); 460 return broken || brokenServerAction.isEnabled() 461 ? Utilities.mergeImages(original, Utilities.loadImage(BROKEN_PROJECT_BADGE), 8, 0) 462 : original; 463 } 464 465 public HelpCtx getHelpCtx() { 466 return new HelpCtx(ArchiveLogicalViewRootNode.class); 467 } 468 469 471 private Action [] getAdditionalActions() { 472 473 ResourceBundle bundle = NbBundle.getBundle(J2eeArchiveLogicalViewProvider.class); 474 475 J2eeModuleProvider provider = (J2eeModuleProvider) project.getLookup().lookup(J2eeModuleProvider.class); 476 List <Action > actions = new ArrayList <Action >(); 477 actions.addAll(specialActions); 478 actions.addAll(Arrays.asList(new Action [] { 479 null, 480 ProjectSensitiveActions.projectCommandAction( ActionProvider.COMMAND_BUILD, bundle.getString( "LBL_BuildAction_Name" ), null ), ProjectSensitiveActions.projectCommandAction( ActionProvider.COMMAND_REBUILD, bundle.getString( "LBL_RebuildAction_Name" ), null ), ProjectSensitiveActions.projectCommandAction( ActionProvider.COMMAND_CLEAN, bundle.getString( "LBL_CleanAction_Name" ), null ), })); 484 if (provider != null && provider.hasVerifierSupport()) { 485 actions.add(ProjectSensitiveActions.projectCommandAction( "verify", bundle.getString( "LBL_VerifyAction_Name" ), null )); } 487 actions.addAll(Arrays.asList(new Action [] { 488 null, 489 ProjectSensitiveActions.projectCommandAction( ActionProvider.COMMAND_RUN, bundle.getString( "LBL_RunAction_Name" ), null ), ProjectSensitiveActions.projectCommandAction( ActionProvider.COMMAND_DEBUG, bundle.getString( "LBL_DebugAction_Name" ), null ), ProjectSensitiveActions.projectCommandAction( EjbProjectConstants.COMMAND_REDEPLOY, bundle.getString( "LBL_DeployAction_Name" ), null ), null, 493 CommonProjectActions.setAsMainProjectAction(), 494 CommonProjectActions.openSubprojectsAction(), 495 CommonProjectActions.closeProjectAction(), 496 null, 497 CommonProjectActions.renameProjectAction(), 498 CommonProjectActions.moveProjectAction(), 499 CommonProjectActions.copyProjectAction(), 500 CommonProjectActions.deleteProjectAction(), 501 null, 502 SystemAction.get( FindAction.class ), 503 null, 504 })); 505 506 try { 507 Repository repository = Repository.getDefault(); 508 FileSystem sfs = repository.getDefaultFileSystem(); 509 FileObject fo = sfs.findResource("Projects/Actions"); if (fo != null) { 511 DataObject dobj = DataObject.find(fo); 512 FolderLookup actionRegistry = new FolderLookup((DataFolder)dobj); 513 Lookup.Template query = new Lookup.Template(Object .class); 514 Lookup lookup = actionRegistry.getLookup(); 515 Iterator it = lookup.lookup(query).allInstances().iterator(); 516 if (it.hasNext()) { 517 actions.add(null); 518 } 519 while (it.hasNext()) { 520 Object next = it.next(); 521 if (next instanceof Action ) { 522 actions.add((Action ) next); 523 } else if (next instanceof JSeparator ) { 524 actions.add(null); 525 } 526 } 527 } 528 } catch (DataObjectNotFoundException ex) { 529 ErrorManager.getDefault().notify(ex); 531 } 532 533 actions.add(null); 534 actions.add(SystemAction.get(ToolsAction.class)); 535 actions.add(null); 536 537 if (broken) { 538 actions.add(getBrokenLinksAction()); 539 } 540 if (brokenServerAction.isEnabled()) { 541 actions.add(brokenServerAction); 542 } 543 actions.add(CommonProjectActions.customizeProjectAction()); 544 return (Action [])actions.toArray(new Action [actions.size()]); 545 } 546 547 550 private class BrokenLinksAction extends AbstractAction implements PropertyChangeListener , Runnable { 551 552 private RequestProcessor.Task task = null; 553 private final PropertyChangeListener weakPCL; 554 555 public BrokenLinksAction() { 556 evaluator.addPropertyChangeListener(WeakListeners.propertyChange(this, evaluator)); 557 putValue(Action.NAME, NbBundle.getMessage(J2eeArchiveLogicalViewProvider.class, "LBL_Fix_Broken_Links_Action")); 558 weakPCL = WeakListeners.propertyChange( this, JavaPlatformManager.getDefault() ); 559 JavaPlatformManager.getDefault().addPropertyChangeListener( weakPCL ); 560 } 561 562 public void actionPerformed(ActionEvent e) { 563 BrokenReferencesSupport.showCustomizer(helper.getAntProjectHelper(), resolver, BREAKABLE_PROPERTIES, new String []{ EarProjectProperties.JAVA_PLATFORM}); 564 brokenProjectSupport.adjustReferences(); 565 checkProjectValidity(); 566 } 567 568 public void propertyChange(PropertyChangeEvent evt) { 569 if (task == null) { 573 task = RequestProcessor.getDefault().create(this); 574 } 575 task.schedule(100); 576 } 577 578 public void run() { 579 checkProjectValidity(); 580 } 581 582 } 583 584 private class BrokenServerAction extends AbstractAction implements 585 InstanceListener, PropertyChangeListener { 586 587 private boolean brokenServer; 588 589 public BrokenServerAction() { 590 putValue(Action.NAME, NbBundle.getMessage(J2eeArchiveLogicalViewProvider.class, "LBL_Fix_Missing_Server_Action")); evaluator.addPropertyChangeListener(WeakListeners.propertyChange(this, evaluator)); 592 checkMissingServer(); 593 } 594 595 public boolean isEnabled() { 596 return brokenServer; 597 } 598 599 public void actionPerformed(ActionEvent e) { 600 EarProjectProperties app = new EarProjectProperties(project, resolver, abpt); 601 String j2eeSpec = (String ) app.get(EarProjectProperties.J2EE_PLATFORM); 602 String instance = BrokenServerSupport.selectServer(j2eeSpec, J2eeModule.EAR); 603 if (instance != null) { 604 app.put(EarProjectProperties.J2EE_SERVER_INSTANCE, instance); 605 app.store(); 606 } 607 checkMissingServer(); 608 } 609 610 public void propertyChange(PropertyChangeEvent evt) { 611 if (EarProjectProperties.J2EE_SERVER_INSTANCE.equals(evt.getPropertyName())) { 612 checkMissingServer(); 613 } 614 } 615 616 public void changeDefaultInstance(String oldServerInstanceID, String newServerInstanceID) { 617 } 618 619 public void instanceAdded(String serverInstanceID) { 620 checkMissingServer(); 621 } 622 623 public void instanceRemoved(String serverInstanceID) { 624 checkMissingServer(); 625 } 626 627 private void checkMissingServer() { 628 boolean old = brokenServer; 629 String serverInstanceID = helper.getAntProjectHelper().getStandardPropertyEvaluator().getProperty(EarProjectProperties.J2EE_SERVER_INSTANCE); 630 brokenServer = BrokenServerSupport.isBroken(serverInstanceID); 631 if (old != brokenServer) { 632 fireIconChange(); 633 fireOpenedIconChange(); 634 fireDisplayNameChange(null, null); 635 } 636 } 637 } 638 639 } 640 641 644 public static class Actions { 645 646 private Actions() {} 648 public static Action createAction( String key, String name, boolean global ) { 649 return new ActionImpl( key, name, global ? Utilities.actionsGlobalContext() : null ); 650 } 651 652 private static class ActionImpl extends AbstractAction implements ContextAwareAction { 653 654 Lookup context; 655 String name; 656 String command; 657 658 public ActionImpl( String command, String name, Lookup context ) { 659 super( name ); 660 this.context = context; 661 this.command = command; 662 this.name = name; 663 } 664 665 public void actionPerformed( ActionEvent e ) { 666 667 Project project = (Project)context.lookup( Project.class ); 668 ActionProvider ap = (ActionProvider)project.getLookup().lookup( ActionProvider.class); 669 670 ap.invokeAction( command, context ); 671 672 } 673 674 public Action createContextAwareInstance( Lookup lookup ) { 675 return new ActionImpl( command, name, lookup ); 676 } 677 } 678 679 } 680 681 } 682 | Popular Tags |