KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > tigris > scarab > actions > admin > AttributeGroupEdit


1 package org.tigris.scarab.actions.admin;
2
3 /* ================================================================
4  * Copyright (c) 2000-2002 CollabNet. All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions are
8  * met:
9  *
10  * 1. Redistributions of source code must retain the above copyright
11  * notice, this list of conditions and the following disclaimer.
12  *
13  * 2. Redistributions in binary form must reproduce the above copyright
14  * notice, this list of conditions and the following disclaimer in the
15  * documentation and/or other materials provided with the distribution.
16  *
17  * 3. The end-user documentation included with the redistribution, if
18  * any, must include the following acknowlegement: "This product includes
19  * software developed by Collab.Net <http://www.Collab.Net/>."
20  * Alternately, this acknowlegement may appear in the software itself, if
21  * and wherever such third-party acknowlegements normally appear.
22  *
23  * 4. The hosted project names must not be used to endorse or promote
24  * products derived from this software without prior written
25  * permission. For written permission, please contact info@collab.net.
26  *
27  * 5. Products derived from this software may not use the "Tigris" or
28  * "Scarab" names nor may "Tigris" or "Scarab" appear in their names without
29  * prior written permission of Collab.Net.
30  *
31  * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
32  * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
33  * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
34  * IN NO EVENT SHALL COLLAB.NET OR ITS CONTRIBUTORS BE LIABLE FOR ANY
35  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
36  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
37  * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
38  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
39  * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
40  * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42  *
43  * ====================================================================
44  *
45  * This software consists of voluntary contributions made by many
46  * individuals on behalf of Collab.Net.
47  */

48
49 // Java Stuff
50
import java.util.ArrayList JavaDoc;
51 import java.util.Iterator JavaDoc;
52 import java.util.List JavaDoc;
53
54 import org.apache.fulcrum.intake.model.Group;
55 import org.apache.fulcrum.parser.ParameterParser;
56 import org.apache.torque.TorqueException;
57 import org.apache.torque.om.NumberKey;
58 import org.apache.turbine.RunData;
59 import org.apache.turbine.TemplateContext;
60 import org.apache.turbine.tool.IntakeTool;
61 import org.tigris.scarab.actions.base.RequireLoginFirstAction;
62 import org.tigris.scarab.om.Attribute;
63 import org.tigris.scarab.om.AttributeGroup;
64 import org.tigris.scarab.om.AttributeGroupManager;
65 import org.tigris.scarab.om.AttributeManager;
66 import org.tigris.scarab.om.GlobalParameter;
67 import org.tigris.scarab.om.GlobalParameterManager;
68 import org.tigris.scarab.om.IssueType;
69 import org.tigris.scarab.om.Module;
70 import org.tigris.scarab.om.RAttributeAttributeGroup;
71 import org.tigris.scarab.om.RIssueTypeAttribute;
72 import org.tigris.scarab.om.RModuleAttribute;
73 import org.tigris.scarab.om.RModuleIssueType;
74 import org.tigris.scarab.om.ScarabUser;
75 import org.tigris.scarab.services.cache.ScarabCache;
76 import org.tigris.scarab.services.security.ScarabSecurity;
77 import org.tigris.scarab.tools.ScarabLocalizationTool;
78 import org.tigris.scarab.tools.ScarabRequestTool;
79 import org.tigris.scarab.tools.localization.L10NKeySet;
80 import org.tigris.scarab.tools.localization.LocalizationKey;
81 import org.tigris.scarab.util.Log;
82 import org.tigris.scarab.util.ScarabException;
83 import org.tigris.scarab.util.ScarabLocalizedTorqueException;
84 import org.tigris.scarab.workflow.WorkflowFactory;
85
86 /**
87  * action methods on RModuleAttribute or RIssueTypeAttribute tables
88  *
89  * @author <a HREF="mailto:elicia@collab.net">Elicia David</a>
90  * @version $Id: AttributeGroupEdit.java 9508 2005-03-23 00:01:41Z dabbous $
91  */

92 public class AttributeGroupEdit extends RequireLoginFirstAction
93 {
94     /**
95      * Updates attribute group info.
96      */

97     public boolean doSaveinfo (RunData data, TemplateContext context)
98         throws Exception JavaDoc
99     {
100         boolean success = true;
101         // Set properties for group info
102
IntakeTool intake = getIntakeTool(context);
103         ScarabRequestTool scarabR = getScarabRequestTool(context);
104         ScarabLocalizationTool l10n = getLocalizationTool(context);
105         IssueType issueType = scarabR.getIssueType();
106         if (issueType.isSystemDefined())
107         {
108             scarabR.setAlertMessage(L10NKeySet.SystemSpecifiedIssueType);
109             return false;
110         }
111         String JavaDoc groupId = data.getParameters().getString("groupId");
112         AttributeGroup ag = AttributeGroupManager
113                             .getInstance(new NumberKey(groupId), false);
114         Group agGroup = intake.get("AttributeGroup",
115                                     ag.getQueryKey(), false);
116         if (!ag.isGlobal() && scarabR.getIssueType().getLocked())
117         {
118             scarabR.setAlertMessage(L10NKeySet.LockedIssueType);
119             return false;
120         }
121         if (intake.isAllValid())
122         {
123             agGroup.setProperties(ag);
124             ag.save();
125             scarabR.setConfirmMessage(DEFAULT_MSG);
126         }
127         else
128         {
129             success = false;
130             scarabR.setAlertMessage(ERROR_MESSAGE);
131         }
132         return success;
133     }
134
135     /**
136      * Changes the properties of existing AttributeGroups and their attributes.
137      */

138     public boolean doSaveattributes (RunData data, TemplateContext context)
139         throws Exception JavaDoc
140     {
141         boolean success = true;
142         ScarabRequestTool scarabR = getScarabRequestTool(context);
143         ScarabLocalizationTool l10n = getLocalizationTool(context);
144         IntakeTool intake = getIntakeTool(context);
145         IssueType issueType = scarabR.getIssueType();
146         Module module = scarabR.getCurrentModule();
147
148         // Check if issue type is system-defined, hence unmodifyable
149
if (issueType.isSystemDefined())
150         {
151             scarabR.setAlertMessage(L10NKeySet.SystemSpecifiedIssueType);
152             return false;
153         }
154
155         String JavaDoc groupId = data.getParameters().getString("groupId");
156         AttributeGroup ag = AttributeGroupManager
157                             .getInstance(new NumberKey(groupId), false);
158         
159         LocalizationKey l10nKey = DEFAULT_MSG;
160
161         // Check if issue type is locked
162
if (!ag.isGlobal() && issueType.getLocked())
163         {
164             scarabR.setAlertMessage(L10NKeySet.LockedIssueType);
165             return false;
166         }
167
168         // Check for duplicate sequence numbers
169
if (areThereDupeSequences(ag.getRAttributeAttributeGroups(), intake,
170                 "RAttributeAttributeGroup", "Order", 0))
171         {
172             scarabR.setAlertMessage(
173                 l10n.format("DuplicateSequenceNumbersFound",
174                 l10n.get(L10NKeySet.Attributes).toLowerCase()));
175             return false;
176         }
177
178         List JavaDoc rmas = ag.getRModuleAttributes();
179         ArrayList JavaDoc lockedAttrs = new ArrayList JavaDoc();
180
181         if (intake.isAllValid())
182         {
183             // First iterate thru and check for required attributes
184
// That have no active options
185
Iterator JavaDoc i = rmas.iterator();
186             while (i.hasNext())
187             {
188                 RModuleAttribute rma = (RModuleAttribute)i.next();
189                 Group rmaGroup = intake.get("RModuleAttribute",
190                                  rma.getQueryKey(), false);
191                 Attribute attr = rma.getAttribute();
192                 if (attr.isOptionAttribute() && rmaGroup.get("Required").toString().equals("true"))
193                 {
194                     List JavaDoc options = module.getRModuleOptions(rma.getAttribute(), issueType, true);
195                     if (options == null || options.isEmpty())
196                     {
197                         scarabR.setAlertMessage(L10NKeySet.CannotRequireAttributeWithNoOptions);
198                         success = false;
199                     }
200                 }
201             }
202             if (success)
203             {
204                 
205
206                 // Check whether a module specific statusAttribute was selected
207
// and store it in the GLOBAL_PARAMETER table
208
String JavaDoc key = "status_attribute_"+issueType.getIssueTypeId();
209                 String JavaDoc statusAttributeKey = data.getParameters()
210                    .getString(key);
211                 if ( statusAttributeKey != null )
212                 {
213                     String JavaDoc attributeId = GlobalParameterManager.getString(key,module);
214                     if(attributeId == null || !attributeId.equals(statusAttributeKey))
215                     {
216                         GlobalParameterManager.setString(key, module, statusAttributeKey);
217                     }
218                 }
219                 
220                 
221                 i = rmas.iterator();
222                 while (i.hasNext())
223                 {
224                     boolean locked = false;
225                     // Set properties for module-attribute mapping
226
RModuleAttribute rma = (RModuleAttribute)i.next();
227                     Group rmaGroup = intake.get("RModuleAttribute",
228                                      rma.getQueryKey(), false);
229                     Attribute attr = rma.getAttribute();
230
231                     // Test to see if attribute is locked
232
RModuleAttribute rmaTest = rma.copy();
233                     rmaTest.setModified(false);
234                     rmaGroup.setProperties(rmaTest);
235                     if (rmaTest.isModified())
236                     {
237                         RIssueTypeAttribute ria = issueType.getRIssueTypeAttribute(attr);
238                         if (ria != null && ria.getLocked())
239                         {
240                              lockedAttrs.add(attr);
241                              locked = true;
242                         }
243                     }
244
245                     if (!locked)
246                     {
247                         // if attribute gets set to inactive, delete dependencies
248
String JavaDoc newActive = rmaGroup.get("Active").toString();
249                         String JavaDoc oldActive = String.valueOf(rma.getActive());
250                         if (newActive.equals("false") && oldActive.equals("true"))
251                         {
252                             WorkflowFactory.getInstance()
253                                 .deleteWorkflowsForAttribute(attr, module,
254                                                              issueType);
255                         }
256                         rmaGroup.setProperties(rma);
257                         String JavaDoc defaultTextKey = data.getParameters()
258                           .getString("default_text");
259                         if (defaultTextKey != null &&
260                              defaultTextKey.equals(rma.getAttributeId().toString()))
261                         {
262                             if (!rma.getRequired())
263                             {
264                                 l10nKey = L10NKeySet.ChangesSavedButDefaultTextAttributeRequired;
265                                 intake.remove(rmaGroup);
266                             }
267                             rma.setIsDefaultText(true);
268                             rma.setRequired(true);
269                         }
270                         
271                         try
272                         {
273                             rma.save();
274                             // Set properties for attribute-attribute group mapping
275
RAttributeAttributeGroup raag =
276                                 ag.getRAttributeAttributeGroup(attr);
277                             Group raagGroup = intake.get("RAttributeAttributeGroup",
278                                          raag.getQueryKey(), false);
279                             raagGroup.setProperties(raag);
280                             raag.save();
281                             scarabR.setConfirmMessage(l10nKey);
282                         }
283                         catch (ScarabLocalizedTorqueException slte)
284                         {
285                             String JavaDoc msg = slte.getMessage(l10n);
286                             scarabR.setAlertMessage(msg);
287                         }
288                         catch (TorqueException te)
289                         {
290                             String JavaDoc msg = te.getMessage();
291                             scarabR.setAlertMessage(msg);
292                         }
293                     }
294
295                     // If they attempted to modify locked attributes, give message.
296
if (lockedAttrs.size() > 0)
297                     {
298                         setLockedMessage(lockedAttrs, context);
299                     }
300                 }
301             }
302         }
303         else
304         {
305             success = false;
306             scarabR.setAlertMessage(L10NKeySet.MoreInformationWasRequired);
307         }
308         return success;
309     }
310
311
312     /**
313      * Changes the properties of global AttributeGroups and their attributes.
314      */

315     public boolean doSaveglobal (RunData data, TemplateContext context)
316         throws Exception JavaDoc
317     {
318         boolean success = true;
319         IntakeTool intake = getIntakeTool(context);
320         ScarabRequestTool scarabR = getScarabRequestTool(context);
321         ScarabLocalizationTool l10n = getLocalizationTool(context);
322         String JavaDoc groupId = data.getParameters().getString("groupId");
323         AttributeGroup ag = AttributeGroupManager
324                             .getInstance(new NumberKey(groupId), false);
325         IssueType issueType = scarabR.getIssueType();
326
327         // Check if issue type is system defined, hence unmodifyable
328
if (issueType.isSystemDefined())
329         {
330             scarabR.setAlertMessage(L10NKeySet.SystemSpecifiedIssueType);
331             return false;
332         }
333         if (issueType.getIssueTypeId() == null)
334         {
335             scarabR.setAlertMessage(L10NKeySet.IssueTypeNotFound);
336             return false;
337         }
338
339
340         // Check for duplicate sequence numbers
341
if (areThereDupeSequences(ag.getRAttributeAttributeGroups(), intake,
342                                        "RAttributeAttributeGroup", "Order",0))
343         {
344             scarabR.setAlertMessage(l10n.format("DuplicateSequenceNumbersFound",
345                 l10n.get("Attributes").toLowerCase()));
346             return false;
347         }
348         String JavaDoc l10nMsg = l10n.get(DEFAULT_MSG);
349
350         if (intake.isAllValid())
351         {
352             List JavaDoc rias = ag.getRIssueTypeAttributes();
353
354             // first check if there are required attributes
355
// Without active options
356
Iterator JavaDoc i = rias.iterator();
357             while (i.hasNext())
358             {
359                 RIssueTypeAttribute ria = (RIssueTypeAttribute)i.next();
360                 Group riaGroup = intake.get("RIssueTypeAttribute",
361                                  ria.getQueryKey(), false);
362                 Attribute attr = ria.getAttribute();
363                 if (attr.isOptionAttribute() && riaGroup.get("Required").toString().equals("true"))
364                 {
365                     List JavaDoc options = issueType.getRIssueTypeOptions(ria.getAttribute(), true);
366                     if (options == null || options.isEmpty())
367                     if (issueType.getRIssueTypeOptions(attr, true).isEmpty())
368                     {
369                         scarabR.setAlertMessage(L10NKeySet.CannotRequireAttributeWithNoOptions);
370                         success = false;
371                     }
372                 }
373             }
374             i = rias.iterator();
375             if (success)
376             {
377                 while (i.hasNext())
378                 {
379                     RIssueTypeAttribute ria = (RIssueTypeAttribute)i.next();
380                     Group riaGroup = intake.get("RIssueTypeAttribute",
381                                      ria.getQueryKey(), false);
382                     riaGroup.setProperties(ria);
383                     String JavaDoc defaultTextKey = data.getParameters()
384                         .getString("default_text");
385                     if (defaultTextKey != null &&
386                          defaultTextKey.equals(ria.getAttributeId().toString()))
387                     {
388                         if (!ria.getRequired())
389                         {
390                             l10nMsg = l10n.get(L10NKeySet.ChangesSavedButDefaultTextAttributeRequired);
391                         }
392                         ria.setIsDefaultText(true);
393                         ria.setRequired(true);
394                         intake.remove(riaGroup);
395                     }
396                     ria.save();
397
398                     // Set properties for attribute-attribute group mapping
399
RAttributeAttributeGroup raag =
400                         ag.getRAttributeAttributeGroup(ria.getAttribute());
401                     Group raagGroup = intake.get("RAttributeAttributeGroup",
402                                  raag.getQueryKey(), false);
403                     raagGroup.setProperties(raag);
404                     raag.save();
405                 }
406                 scarabR.setConfirmMessage(l10nMsg);
407             }
408         }
409         else
410         {
411             success = false;
412             scarabR.setAlertMessage(l10nMsg);
413         }
414         return success;
415     }
416
417     /**
418      * Unmaps attributes to modules.
419      */

420     public void doDeleteattributes(RunData data, TemplateContext context)
421         throws Exception JavaDoc
422     {
423         ScarabRequestTool scarabR = getScarabRequestTool(context);
424         ScarabLocalizationTool l10n = getLocalizationTool(context);
425         Module module = scarabR.getCurrentModule();
426         IssueType issueType = scarabR.getIssueType();
427         if (issueType.isSystemDefined())
428         {
429             scarabR.setAlertMessage(L10NKeySet.SystemSpecifiedIssueType);
430             return;
431         }
432         ScarabUser user = (ScarabUser)data.getUser();
433         String JavaDoc groupId = data.getParameters().getString("groupId");
434         AttributeGroup ag = AttributeGroupManager
435             .getInstance(new NumberKey(groupId), false);
436         boolean hasAttributes = false;
437
438         if (!user.hasPermission(ScarabSecurity.MODULE__EDIT, module))
439         {
440             scarabR.setAlertMessage(NO_PERMISSION_MESSAGE);
441             return;
442         }
443         if (!ag.isGlobal() && issueType.getLocked())
444         {
445             scarabR.setAlertMessage(L10NKeySet.LockedIssueType);
446             return;
447         }
448         ParameterParser params = data.getParameters();
449         Object JavaDoc[] keys = params.getKeys();
450         String JavaDoc key;
451         String JavaDoc attributeId;
452         ArrayList JavaDoc lockedAttrs = new ArrayList JavaDoc();
453
454         for (int i =0; i<keys.length; i++)
455         {
456             key = keys[i].toString();
457             if (key.startsWith("att_delete_"))
458             {
459                 hasAttributes = true;
460                 attributeId = key.substring(11);
461                 Attribute attribute = AttributeManager
462                    .getInstance(new NumberKey(attributeId), false);
463                 RIssueTypeAttribute ria = issueType.getRIssueTypeAttribute(attribute);
464                 if (!ag.isGlobal() && ria != null && ria.getLocked())
465                 {
466                     lockedAttrs.add(attribute);
467                 }
468                 else
469                 {
470                     try
471                     {
472                         ag.deleteAttribute(attribute, user, module);
473                     }
474                     catch (ScarabException e)
475                     {
476                         scarabR.setAlertMessage(l10n.getMessage(e));
477                         Log.get().warn(
478                             "This is an application error, if it is not permission related.", e);
479                     }
480                 }
481             }
482         }
483         if(!hasAttributes)
484         {
485             scarabR.setAlertMessage(L10NKeySet.NoAttributeSelected);
486         }
487
488         // If there are no attributes in any of the dedupe
489
// Attribute groups, turn off deduping in the module
490
boolean areThereDedupeAttrs = false;
491         List JavaDoc attributeGroups = issueType.getAttributeGroups(module, true);
492         if (attributeGroups.size() > 0)
493         {
494             for (int j=0; j<attributeGroups.size(); j++)
495             {
496                 AttributeGroup agTemp = (AttributeGroup)attributeGroups.get(j);
497                 if (agTemp.getDedupe() && !agTemp.getAttributes().isEmpty())
498                 {
499                    areThereDedupeAttrs = true;
500                 }
501             }
502             if (!areThereDedupeAttrs)
503             {
504                 if (module == null)
505                 {
506                     issueType.setDedupe(false);
507                     issueType.save();
508                 }
509                 else
510                 {
511                     RModuleIssueType rmit = module.getRModuleIssueType(issueType);
512                     rmit.setDedupe(false);
513                     rmit.save();
514                 }
515             }
516         }
517
518         // If they attempted to modify locked attributes, give message.
519
if (lockedAttrs.size() > 0)
520         {
521             setLockedMessage(lockedAttrs, context);
522         }
523         ScarabCache.clear();
524         if(hasAttributes)
525         {
526             scarabR.setConfirmMessage(DEFAULT_MSG);
527         }
528     }
529
530     /**
531      * This manages clicking the create new button on AttributeSelect.vm
532      */

533     public void doCreatenewglobalattribute(RunData data,
534                                             TemplateContext context)
535         throws Exception JavaDoc
536     {
537         IntakeTool intake = getIntakeTool(context);
538         ScarabRequestTool scarabR = getScarabRequestTool(context);
539         IssueType issueType = scarabR.getIssueType();
540         if (issueType.isSystemDefined())
541         {
542             scarabR.setAlertMessage(L10NKeySet.SystemSpecifiedIssueType);
543             return;
544         }
545         Group attGroup = intake.get("Attribute", IntakeTool.DEFAULT_KEY);
546         intake.remove(attGroup);
547         scarabR.setAttribute(null);
548         setTarget(data, getOtherTemplate(data));
549     }
550
551
552     /**
553      * Selects attribute to add to issue type and attribute group.
554      */

555     public void doSelectattribute(RunData data, TemplateContext context)
556         throws Exception JavaDoc
557     {
558         ScarabRequestTool scarabR = getScarabRequestTool(context);
559         ScarabLocalizationTool l10n = getLocalizationTool(context);
560         IssueType issueType = scarabR.getIssueType();
561         if (issueType.isSystemDefined())
562         {
563             scarabR.setAlertMessage(L10NKeySet.SystemSpecifiedIssueType);
564             return;
565         }
566         AttributeGroup ag = scarabR.getAttributeGroup();
567
568         if (!ag.isGlobal() && scarabR.getIssueType().getLocked())
569         {
570             scarabR.setAlertMessage(L10NKeySet.LockedIssueType);
571             return;
572         }
573         String JavaDoc[] attributeIds = data.getParameters()
574                                     .getStrings("attribute_ids");
575  
576         if (attributeIds == null || attributeIds.length <= 0)
577         {
578             scarabR.setAlertMessage(L10NKeySet.SelectAttribute);
579             return;
580         }
581         else
582         {
583             boolean alreadySubmited = false;
584             for (int i=0; i < attributeIds.length; i++)
585             {
586                 Attribute attribute =
587                     scarabR.getAttribute(new Integer JavaDoc(attributeIds[i]));
588                 try
589                 {
590                     ag.addAttribute(attribute);
591                 }
592                 catch (TorqueException e)
593                 {
594                     alreadySubmited = true;
595                     scarabR.setAlertMessage(L10NKeySet.ResubmitError);
596                 }
597             }
598             doCancel(data, context);
599             if (!alreadySubmited)
600             {
601                 scarabR.setConfirmMessage(DEFAULT_MSG);
602             }
603         }
604     }
605
606     /**
607      * Saves all data when Done is clicked.
608      */

609     public void doDone (RunData data, TemplateContext context)
610         throws Exception JavaDoc
611     {
612         String JavaDoc groupId = data.getParameters().getString("groupId");
613         AttributeGroup ag = AttributeGroupManager
614                             .getInstance(new NumberKey(groupId), false);
615         boolean infoSuccess = doSaveinfo(data, context);
616         boolean attrSuccess = false;
617         if (ag.isGlobal())
618         {
619             attrSuccess = doSaveglobal(data, context);
620         }
621         else
622         {
623             attrSuccess = doSaveattributes(data, context);
624         }
625         if (infoSuccess && attrSuccess)
626         {
627             doCancel(data, context);
628         }
629     }
630         
631
632     /**
633      * If user attempts to modify locked attributes, gives message.
634      */

635     private void setLockedMessage (List JavaDoc lockedAttrs, TemplateContext context)
636         throws Exception JavaDoc
637     {
638         StringBuffer JavaDoc buf = new StringBuffer JavaDoc();
639         for (int i=0; i<lockedAttrs.size(); i++)
640         {
641             Attribute attr = (Attribute)lockedAttrs.get(i);
642             buf.append(attr.getName());
643             if (i == lockedAttrs.size()-1)
644             {
645                 buf.append(".");
646             }
647             else
648             {
649                 buf.append(",");
650             }
651         }
652         getScarabRequestTool(context).setAlertMessage(getLocalizationTool(context).format("LockedAttributes", buf.toString()));
653     }
654 }
655
Popular Tags