KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > jboss > net > jmx > server > MBeanProvider


1 /*
2  * JBoss, the OpenSource J2EE webOS
3  *
4  * Distributable under LGPL license.
5  * See terms of license at gnu.org.
6  */

7
8 // $Id: MBeanProvider.java,v 1.23.2.1 2005/03/02 14:19:57 tdiesler Exp $
9

10 package org.jboss.net.jmx.server;
11
12 // axis package
13

14 import org.jboss.axis.AxisFault;
15 import org.jboss.axis.Message;
16 import org.jboss.axis.MessageContext;
17 import org.jboss.axis.description.OperationDesc;
18 import org.jboss.axis.description.ParameterDesc;
19 import org.jboss.axis.description.ServiceDesc;
20 import org.jboss.axis.encoding.TypeMapping;
21 import org.jboss.axis.handlers.soap.SOAPService;
22 import org.jboss.axis.message.RPCElement;
23 import org.jboss.axis.message.RPCParam;
24 import org.jboss.axis.message.SOAPEnvelopeAxisImpl;
25 import org.jboss.axis.providers.BasicProvider;
26 import org.jboss.axis.providers.java.JavaProvider;
27 import org.jboss.axis.utils.JavaUtils;
28 import org.jboss.axis.utils.Messages;
29 import org.jboss.axis.wsdl.fromJava.Emitter;
30 import org.jboss.axis.wsdl.fromJava.Types;
31 import org.jboss.logging.Logger;
32 import org.jboss.mx.util.MBeanServerLocator;
33 import org.jboss.util.Classes;
34 import org.w3c.dom.Document JavaDoc;
35 import org.xml.sax.SAXException JavaDoc;
36
37 import javax.management.Attribute JavaDoc;
38 import javax.management.AttributeNotFoundException JavaDoc;
39 import javax.management.InstanceNotFoundException JavaDoc;
40 import javax.management.IntrospectionException JavaDoc;
41 import javax.management.InvalidAttributeValueException JavaDoc;
42 import javax.management.MBeanAttributeInfo JavaDoc;
43 import javax.management.MBeanException JavaDoc;
44 import javax.management.MBeanInfo JavaDoc;
45 import javax.management.MBeanOperationInfo JavaDoc;
46 import javax.management.MBeanParameterInfo JavaDoc;
47 import javax.management.MBeanServer JavaDoc;
48 import javax.management.MalformedObjectNameException JavaDoc;
49 import javax.management.ObjectName JavaDoc;
50 import javax.management.ReflectionException JavaDoc;
51 import javax.wsdl.Definition;
52 import javax.wsdl.factory.WSDLFactory;
53 import javax.xml.namespace.QName JavaDoc;
54 import javax.xml.soap.SOAPException JavaDoc;
55 import java.util.Iterator JavaDoc;
56 import java.util.List JavaDoc;
57 import java.util.Map JavaDoc;
58
59 /**
60  * Exposes mbeans as targets (pivot-handlers) of web-services. To
61  * deploy a particular mbean as a web-service, a deployment descriptor
62  * would look like:
63  *
64  * <wsdd:deployment>
65  * <handler name="MBeanDispatcher" class="org.jboss.net.jmx.MBeanProvider"/>
66  * <wsdd:service name="${ServiceName}" handler="Handler">
67  * <option name="handlerClass" value="org.jboss.net.jmx.server.MBeanProvider"/>
68  * <option name="ObjectName" value="${JMX_ObjectName}"/>
69  * <option name="allowedMethods" value="method1 method2 method3"/>
70  * <option name="allowedWriteAttributes" value="attr1 attr2 attr3"/>
71  * <option name="allowedReadAttributes" value="attr4 attr5 attr6"/>
72  * </wsdd:service>
73  * </wsdd:deployment>
74  * <p>
75  * MBeanProvider is able to recognize an {@link org.jboss.net.axis.server.WsdlAwareHttpActionHandler} in its
76  * transport chain such that it will set the soap-action headers in the wsdl.
77  * </p>
78  * @created 1. Oktober 2001, 16:38
79  * @author <a HREF="mailto:Christoph.Jung@infor.de">Christoph G. Jung</a>
80  * @version $Revision: 1.23.2.1 $
81  */

82
83 public class MBeanProvider extends BasicProvider
84 {
85
86    private static Logger log = Logger.getLogger(MBeanProvider.class);
87
88    //
89
// Attributes
90
//
91

92    /** the server which we are tight to */
93    protected MBeanServer JavaDoc server;
94    /** the objectName which we are running against */
95    protected ObjectName JavaDoc name;
96    /** stores meta-data about mbean */
97    protected Map JavaDoc attributeData = new java.util.HashMap JavaDoc();
98    protected Map JavaDoc methodData = new java.util.HashMap JavaDoc();
99    /** which methods are allowed to be exposed */
100    protected String JavaDoc allowedMethodsOption = "allowedMethods";
101    /** which attributes are allowed to be exposed */
102    protected String JavaDoc allowedReadAttributesOption = "allowedReadAttributes";
103    /** which attributes are allowed to be exposed */
104    protected String JavaDoc allowedWriteAttributesOption = "allowedWriteAttributes";
105
106    //
107
// Constructors
108
//
109

110    /**
111     * Constructor MBeanProvider
112     */

113    public MBeanProvider()
114    {
115    }
116
117    //
118
// Public API
119
//
120

121    /*
122     * initialize the meta-data
123     * @see org.jboss.axis.providers.BasicProvider#initServiceDesc(SOAPService, MessageContext)
124     */

125    public void initServiceDesc(SOAPService service, MessageContext msgCtx)
126            throws AxisFault
127    {
128       // here comes the reflective part
129
try
130       {
131          // first some option preparation
132
String JavaDoc allowedMethods =
133                  (String JavaDoc)service.getOption(allowedMethodsOption);
134          String JavaDoc allowedReadAttributes =
135                  (String JavaDoc)service.getOption(allowedReadAttributesOption);
136          String JavaDoc allowedWriteAttributes =
137                  (String JavaDoc)service.getOption(allowedWriteAttributesOption);
138          String JavaDoc objectName =
139                  (String JavaDoc)service.getOption(Constants.OBJECTNAME_PROPERTY);
140          String JavaDoc serverId =
141                  (String JavaDoc)service.getOption(Constants.MBEAN_SERVER_ID_PROPERTY);
142          // process server id in order to find the right mbean server
143
try
144          {
145             server = MBeanServerLocator.locateJBoss();
146          }
147          catch (IllegalStateException JavaDoc e)
148          {
149             throw new AxisFault(Constants.NO_MBEAN_SERVER_FOUND);
150          }
151          // build objectname
152
name = new ObjectName JavaDoc(objectName);
153          MBeanInfo JavaDoc info = server.getMBeanInfo(name);
154          ServiceDesc serviceDesc = service.getServiceDescription();
155          java.lang.reflect.Field JavaDoc completeField =
156                  ServiceDesc.class.getDeclaredField("introspectionComplete");
157          completeField.setAccessible(true);
158          completeField.set(serviceDesc, Boolean.TRUE);
159
160          Class JavaDoc implClass = Classes.loadClass(info.getClassName(), msgCtx.getClassLoader());
161          serviceDesc.setImplClass(implClass);
162
163          serviceDesc.setTypeMapping((TypeMapping)service.getTypeMappingRegistry().getTypeMapping(org.jboss.axis.Constants.URI_DEFAULT_SOAP_ENC));
164          // first inspect operations
165
MBeanOperationInfo JavaDoc[] operations = info.getOperations();
166          for (int count = 0; count < operations.length; count++)
167          {
168             String JavaDoc operationName = operations[count].getName();
169             // is this method exposed?
170
if (allowedMethods != null
171                     && allowedMethods.equals("*")
172                     || allowedMethods.indexOf(operationName + " ") != -1
173                     || allowedMethods.indexOf(" " + operationName) != -1
174                     || allowedMethods.equals(operationName))
175             {
176                // yes, we build an operation description
177
OperationDesc opDesc = new OperationDesc();
178                // check overloading, if already present, attach the method count
179
if (methodData.containsKey(operationName))
180                {
181                   operationName = operationName + count;
182                }
183                opDesc.setName(operationName);
184                opDesc.setElementQName(new QName JavaDoc("", operationName));
185                // we cache the mbean operation infos
186
methodData.put(operationName, operations[count]);
187                // process parameters
188
MBeanParameterInfo JavaDoc[] parameters =
189                        operations[count].getSignature();
190                Class JavaDoc[] parameterTypes = new Class JavaDoc[parameters.length];
191                for (int count2 = 0; count2 < parameters.length; count2++)
192                {
193                   ParameterDesc param = new ParameterDesc();
194                   param.setName("arg" + count2);
195                   parameterTypes[count2] =
196                           forName(parameters[count2].getType(), msgCtx.getClassLoader());
197                   param.setJavaType(parameterTypes[count2]);
198                   param.setTypeQName(forName(parameterTypes[count2],
199                           serviceDesc.getTypeMapping()));
200                   opDesc.addParameter(param);
201                }
202                opDesc.setReturnClass(forName(operations[count].getReturnType(), msgCtx.getClassLoader()));
203                opDesc.setReturnType(forName(opDesc.getReturnClass(),
204                        serviceDesc.getTypeMapping()));
205                // TODO faultprocessing
206
serviceDesc.addOperationDesc(opDesc);
207             } // if
208
} // for
209
// next we do attribute processing
210
MBeanAttributeInfo JavaDoc[] attributes = info.getAttributes();
211          for (int count = 0; count < attributes.length; count++)
212          {
213             String JavaDoc attributeName = attributes[count].getName();
214             // is the attribute readable?
215
if (attributes[count].isReadable()
216                     && allowedReadAttributes != null
217                     && (allowedReadAttributes.equals("*")
218                     || allowedReadAttributes.indexOf(attributeName + " ") != -1
219                     || allowedReadAttributes.indexOf(" " + attributeName) != -1
220                     || allowedReadAttributes.equals(attributeName)))
221             {
222                OperationDesc opDesc = new OperationDesc();
223                if (attributes[count].getType().equals("boolean"))
224                {
225                   opDesc.setName("is" + attributeName);
226                }
227                else
228                {
229                   opDesc.setName("get" + attributeName);
230                }
231                if (methodData.containsKey(opDesc.getName()))
232                {
233                   opDesc.setName(opDesc.getName() + count + "A");
234                }
235                opDesc.setElementQName(new QName JavaDoc("", opDesc.getName()));
236                attributeData.put(opDesc.getName(), attributes[count]);
237                opDesc.setReturnClass(forName(attributes[count].getType(), msgCtx.getClassLoader()));
238                opDesc.setReturnType(forName(opDesc.getReturnClass(),
239                        serviceDesc.getTypeMapping()));
240                // TODO faultprocessing
241
serviceDesc.addOperationDesc(opDesc);
242             } // if
243
if (attributes[count].isWritable()
244                     && allowedWriteAttributes != null
245                     && (allowedWriteAttributes.equals("*")
246                     || allowedWriteAttributes.indexOf(attributeName + " ") != -1
247                     || allowedWriteAttributes.indexOf(" " + attributeName) != -1
248                     || allowedWriteAttributes.equals(attributeName)))
249             {
250                OperationDesc opDesc = new OperationDesc();
251                opDesc.setName("set" + attributeName);
252                if (methodData.containsKey(opDesc.getName()))
253                {
254                   opDesc.setName(opDesc.getName() + count + "A");
255                }
256                opDesc.setElementQName(new QName JavaDoc("", opDesc.getName()));
257                attributeData.put(opDesc.getName(), attributes[count]);
258                ParameterDesc p = new ParameterDesc();
259                p.setName("arg0");
260                p.setJavaType(forName(attributes[count].getType(), msgCtx.getClassLoader()));
261                p.setTypeQName(forName(p.getJavaType(), serviceDesc.getTypeMapping()));
262                opDesc.addParameter(p);
263                opDesc.setReturnType(null);
264                // TODO faultprocessing
265
serviceDesc.addOperationDesc(opDesc);
266             } // if
267
} // for
268
}
269       catch (InstanceNotFoundException JavaDoc e)
270       {
271          throw new AxisFault(Constants.NO_MBEAN_INSTANCE, e);
272       }
273       catch (IntrospectionException JavaDoc e)
274       {
275          throw new AxisFault(Constants.INTROSPECTION_EXCEPTION, e);
276       }
277       catch (ReflectionException JavaDoc e)
278       {
279          throw new AxisFault(Constants.INTROSPECTION_EXCEPTION, e);
280       }
281       catch (ClassNotFoundException JavaDoc e)
282       {
283          throw new AxisFault(Constants.INTROSPECTION_EXCEPTION, e);
284       }
285       catch (MalformedObjectNameException JavaDoc e)
286       {
287          throw new AxisFault(Constants.WRONG_OBJECT_NAME, e);
288       }
289       catch (IllegalAccessException JavaDoc e)
290       {
291          throw new AxisFault(Constants.INTROSPECTION_EXCEPTION, e);
292       }
293       catch (NoSuchFieldException JavaDoc e)
294       {
295          throw new AxisFault(Constants.INTROSPECTION_EXCEPTION, e);
296       }
297    }
298
299    /** resolve string-based jmx types */
300    protected Class JavaDoc forName(String JavaDoc string, ClassLoader JavaDoc loader) throws ClassNotFoundException JavaDoc
301    {
302       if ("void".equals(string))
303       {
304          return void.class;
305       }
306       else if ("boolean".equals(string))
307       {
308          return boolean.class;
309       }
310       else if ("float".equals(string))
311       {
312          return float.class;
313       }
314       else if ("double".equals(string))
315       {
316          return double.class;
317       }
318       else if ("int".equals(string))
319       {
320          return int.class;
321       }
322       else if ("long".equals(string))
323       {
324          return long.class;
325       }
326       else if ("short".equals(string))
327       {
328          return short.class;
329       }
330       else if ("byte".equals(string))
331       {
332          return byte.class;
333       }
334       else if ("char".equals(string))
335       {
336          return char.class;
337       }
338       else
339       {
340          return org.jboss.util.Classes.loadClass(string, loader);
341       }
342    }
343
344    /** resolve string-based jmx types */
345    protected QName JavaDoc forName(Class JavaDoc clazz, TypeMapping tm)
346            throws ClassNotFoundException JavaDoc
347    {
348       if (void.class.equals(clazz))
349       {
350          return null;
351       }
352       else
353       {
354          return tm.getTypeQName(clazz);
355       }
356    }
357
358    /**
359     * Invoke is called to do the actual work of the Handler object.
360     */

361    public void invoke(MessageContext msgContext) throws AxisFault
362    {
363       // the options of the service
364
String JavaDoc serviceName = msgContext.getTargetService();
365       // dissect the message
366
Message reqMsg = msgContext.getRequestMessage();
367       SOAPEnvelopeAxisImpl reqEnv = (SOAPEnvelopeAxisImpl)reqMsg.getSOAPEnvelope();
368       Message resMsg = msgContext.getResponseMessage();
369       SOAPEnvelopeAxisImpl resEnv =
370               (resMsg == null)
371               ? new SOAPEnvelopeAxisImpl()
372               : (SOAPEnvelopeAxisImpl)resMsg.getSOAPEnvelope();
373       // copied code from RobJ, duh?
374
if (msgContext.getResponseMessage() == null)
375       {
376          resMsg = new Message(resEnv);
377          msgContext.setResponseMessage(resMsg);
378       }
379       // navigate the bodies
380
Iterator JavaDoc allBodies = reqEnv.getBodyElements().iterator();
381       while (allBodies.hasNext())
382       {
383          Object JavaDoc nextBody = allBodies.next();
384          if (nextBody instanceof RPCElement)
385          {
386             RPCElement body = (RPCElement)nextBody;
387             String JavaDoc mName = body.getMethodName();
388             List JavaDoc args = null;
389             try
390             {
391                args = body.getParams();
392             }
393             catch (SAXException JavaDoc e)
394             {
395                throw new AxisFault(Constants.EXCEPTION_OCCURED, e);
396             }
397             Object JavaDoc result = null;
398             try
399             {
400                MBeanAttributeInfo JavaDoc attr =
401                        (MBeanAttributeInfo JavaDoc)attributeData.get(mName);
402                if (attr != null)
403                {
404                   if (mName.startsWith("get") || mName.startsWith("is"))
405                   {
406                      result = server.getAttribute(name, attr.getName());
407                   }
408                   else
409                   {
410                      RPCParam p = (RPCParam)args.get(0);
411                      Object JavaDoc arg =
412                              JavaUtils.convert(p.getValue(),
413                                      forName(attr.getType(), msgContext.getClassLoader()));
414                      server.setAttribute(name,
415                              new Attribute JavaDoc(attr.getName(), arg));
416                      result = null;
417                   }
418                }
419                else
420                {
421                   MBeanOperationInfo JavaDoc meth =
422                           (MBeanOperationInfo JavaDoc)methodData.get(mName);
423                   MBeanParameterInfo JavaDoc[] params = meth.getSignature();
424                   Object JavaDoc[] arguments = new Object JavaDoc[params.length];
425                   String JavaDoc[] classNames = new String JavaDoc[params.length];
426                   for (int count2 = 0; count2 < params.length; count2++)
427                   {
428                      classNames[count2] = params[count2].getType();
429                      if (args.size() > count2)
430                      {
431                         RPCParam param = (RPCParam)args.get(count2);
432                         arguments[count2] =
433                                 JavaUtils.convert(param.getValue(),
434                                         forName(classNames[count2], msgContext.getClassLoader()));
435                      }
436                      else
437                      {
438                         arguments[count2] = null;
439                      }
440                   }
441                   // now do the JMX call
442
result =
443                           server.invoke(name, meth.getName(), arguments, classNames);
444                }
445                // and encode it back to the response
446
RPCElement resBody = new RPCElement(mName + "Response");
447                resBody.setPrefix(body.getPrefix());
448                resBody.setNamespaceURI(body.getNamespaceURI());
449                RPCParam param = new RPCParam(mName + "Result", result);
450                resBody.addParam(param);
451                resEnv.addBodyElement(resBody);
452                resEnv.setEncodingStyle(org.jboss.axis.Constants.URI_DEFAULT_SOAP_ENC);
453             }
454             catch (InstanceNotFoundException JavaDoc e)
455             {
456                throw new AxisFault(Constants.NO_MBEAN_INSTANCE, e);
457             }
458             catch (AttributeNotFoundException JavaDoc e)
459             {
460                throw new AxisFault(Constants.NO_SUCH_ATTRIBUTE, e);
461             }
462             catch (InvalidAttributeValueException JavaDoc e)
463             {
464                throw new AxisFault(Constants.INVALID_ARGUMENT, e);
465             }
466             catch (MBeanException JavaDoc e)
467             {
468                throw new AxisFault(Constants.MBEAN_EXCEPTION, e);
469             }
470             catch (ClassNotFoundException JavaDoc e)
471             {
472                throw new AxisFault(Constants.CLASS_NOT_FOUND, e);
473             }
474             catch (ReflectionException JavaDoc e)
475             {
476                throw new AxisFault(Constants.EXCEPTION_OCCURED,
477                        e.getTargetException());
478             }
479             catch (SOAPException JavaDoc e)
480             {
481                throw new AxisFault(Constants.EXCEPTION_OCCURED, e);
482             }
483          } // if
484
} // for
485
}
486
487    /** generate wsdl document from meta-data */
488    public void generateWSDL(MessageContext msgCtx) throws AxisFault
489    {
490       SOAPService service = msgCtx.getService();
491       //ServiceDesc serviceDesc = service.getInitializedServiceDesc(msgCtx);
492
ServiceDesc serviceDesc = service.getInitializedServiceDesc(msgCtx);
493       // check whether there is an http action header present
494
if (msgCtx != null)
495       {
496          boolean isSoapAction =
497                  msgCtx.getProperty(Constants.ACTION_HANDLER_PRESENT_PROPERTY)
498                  == Boolean.TRUE;
499          // yes, then loop through the operation descriptions
500
for (Iterator JavaDoc alloperations = serviceDesc.getOperations().iterator();
501               alloperations.hasNext();
502                  )
503          {
504             OperationDesc opDesc = (OperationDesc)alloperations.next();
505             // and add a soap action tag with the name of the service
506
opDesc.setSoapAction(isSoapAction ? service.getName() : null);
507          }
508       }
509       try
510       {
511          // Location URL is whatever is explicitly set in the MC
512
String JavaDoc locationUrl =
513                  msgCtx.getStrProp(MessageContext.WSDLGEN_SERV_LOC_URL);
514          if (locationUrl == null)
515          {
516             // If nothing, try what's explicitly set in the ServiceDesc
517
locationUrl = serviceDesc.getEndpointURL();
518          }
519          if (locationUrl == null)
520          {
521             // If nothing, use the actual transport URL
522
locationUrl = msgCtx.getStrProp(MessageContext.TRANS_URL);
523          }
524          // Interface namespace is whatever is explicitly set
525
String JavaDoc interfaceNamespace =
526                  msgCtx.getStrProp(MessageContext.WSDLGEN_INTFNAMESPACE);
527          if (interfaceNamespace == null)
528          {
529             // If nothing, use the default namespace of the ServiceDesc
530
interfaceNamespace = serviceDesc.getDefaultNamespace();
531          }
532          if (interfaceNamespace == null)
533          {
534             // If nothing still, use the location URL determined above
535
interfaceNamespace = locationUrl;
536          }
537          Emitter emitter = new Emitter();
538          String JavaDoc alias = (String JavaDoc)service.getOption("alias");
539          if (alias != null)
540             emitter.setServiceElementName(alias);
541
542          // Set style/use
543
emitter.setCls(serviceDesc.getImplClass());
544
545          // If a wsdl target namespace was provided, use the targetNamespace.
546
// Otherwise use the interfaceNamespace constructed above.
547
String JavaDoc targetNamespace =
548                  (String JavaDoc)service.getOption(JavaProvider.OPTION_WSDL_TARGETNAMESPACE);
549          if (targetNamespace == null || targetNamespace.length() == 0)
550          {
551             targetNamespace = interfaceNamespace;
552          }
553          emitter.setIntfNamespace(targetNamespace);
554          emitter.setLocationUrl(locationUrl);
555          emitter.setServiceDesc(serviceDesc);
556          emitter.setTypeMapping((TypeMapping)service.getTypeMappingRegistry().getTypeMapping(org.jboss.axis.Constants.URI_DEFAULT_SOAP_ENC));
557          emitter.setDefaultTypeMapping((TypeMapping)msgCtx
558                  .getTypeMappingRegistry()
559                  .getDefaultTypeMapping());
560          String JavaDoc wsdlPortType =
561                  (String JavaDoc)service.getOption(JavaProvider.OPTION_WSDL_PORTTYPE);
562          String JavaDoc wsdlServiceElement =
563                  (String JavaDoc)service.getOption(JavaProvider.OPTION_WSDL_SERVICEELEMENT);
564          String JavaDoc wsdlServicePort =
565                  (String JavaDoc)service.getOption(JavaProvider.OPTION_WSDL_SERVICEPORT);
566          if (wsdlPortType != null && wsdlPortType.length() > 0)
567          {
568             emitter.setPortTypeName(wsdlPortType);
569          }
570          if (wsdlServiceElement != null && wsdlServiceElement.length() > 0)
571          {
572             emitter.setServiceElementName(wsdlServiceElement);
573          }
574          if (wsdlServicePort != null && wsdlServicePort.length() > 0)
575          {
576             emitter.setServicePortName(wsdlServicePort);
577          }
578          Definition def = emitter.getWSDL();
579          def.addNamespace("xsd99",
580                  org.jboss.axis.Constants.URI_1999_SCHEMA_XSD);
581          def.addNamespace("xsd00",
582                  org.jboss.axis.Constants.URI_2000_SCHEMA_XSD);
583          def.addNamespace("axis",
584                  org.jboss.axis.Constants.NS_URI_AXIS);
585          Document JavaDoc doc =
586                  WSDLFactory.newInstance().newWSDLWriter().getDocument(def);
587          java.lang.reflect.Field JavaDoc field =
588                  Emitter.class.getDeclaredField("types");
589          field.setAccessible(true);
590          ((Types)field.get(emitter)).insertTypesFragment(doc);
591          msgCtx.setProperty("WSDL", doc);
592       }
593       catch (NoClassDefFoundError JavaDoc e)
594       {
595          log.info(Messages.getMessage("toAxisFault00"), e);
596          throw new AxisFault(e.toString(), e);
597       }
598       catch (Exception JavaDoc e)
599       {
600          log.info(Messages.getMessage("toAxisFault00"), e);
601          throw AxisFault.makeFault(e);
602       }
603    }
604
605    /**
606     * TODO called when a fault occurs to 'undo' whatever 'invoke' did.
607     */

608    public void undo(MessageContext msgContext)
609    {
610       // unbelievable this foresight
611
}
612 }
613
Popular Tags