1 16 package org.directwebremoting.spring; 17 18 import java.util.ArrayList ; 19 import java.util.HashMap ; 20 import java.util.Iterator ; 21 import java.util.List ; 22 import java.util.Map ; 23 import java.util.Properties ; 24 25 import org.directwebremoting.create.NewCreator; 26 import org.directwebremoting.filter.ExtraLatencyAjaxFilter; 27 import org.directwebremoting.util.Logger; 28 import org.springframework.beans.FatalBeanException; 29 import org.springframework.beans.PropertyValue; 30 import org.springframework.beans.factory.BeanFactory; 31 import org.springframework.beans.factory.BeanInitializationException; 32 import org.springframework.beans.factory.HierarchicalBeanFactory; 33 import org.springframework.beans.factory.config.BeanDefinition; 34 import org.springframework.beans.factory.config.BeanDefinitionHolder; 35 import org.springframework.beans.factory.config.RuntimeBeanReference; 36 import org.springframework.beans.factory.support.BeanDefinitionBuilder; 37 import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; 38 import org.springframework.beans.factory.support.BeanDefinitionRegistry; 39 import org.springframework.beans.factory.support.ChildBeanDefinition; 40 import org.springframework.beans.factory.support.ManagedList; 41 import org.springframework.beans.factory.support.ManagedMap; 42 import org.springframework.beans.factory.xml.BeanDefinitionDecorator; 43 import org.springframework.beans.factory.xml.BeanDefinitionParser; 44 import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate; 45 import org.springframework.beans.factory.xml.NamespaceHandlerSupport; 46 import org.springframework.beans.factory.xml.ParserContext; 47 import org.springframework.util.ClassUtils; 48 import org.springframework.util.StringUtils; 49 import org.springframework.util.xml.DomUtils; 50 import org.w3c.dom.Element ; 51 import org.w3c.dom.Node ; 52 import org.w3c.dom.NodeList ; 53 54 65 public class DwrNamespaceHandler extends NamespaceHandlerSupport 66 { 67 70 public void init() 71 { 72 registerBeanDefinitionParser("configuration", new ConfigurationBeanDefinitionParser()); 74 registerBeanDefinitionParser("controller", new ControllerBeanDefinitionParser()); 75 76 registerBeanDefinitionDecorator("init", new InitDefinitionDecorator()); 77 registerBeanDefinitionDecorator("create", new CreatorBeanDefinitionDecorator()); 78 registerBeanDefinitionDecorator("convert", new ConverterBeanDefinitionDecorator()); 79 registerBeanDefinitionDecorator("signatures", new SignaturesBeanDefinitionDecorator()); 80 registerBeanDefinitionDecorator("remote", new RemoteBeanDefinitionDecorator()); 81 } 82 83 86 protected BeanDefinition registerSpringConfiguratorIfNecessary(BeanDefinitionRegistry registry) 87 { 88 if (!registry.containsBeanDefinition(DEFAULT_SPRING_CONFIGURATOR_ID)) 89 { 90 BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(SpringConfigurator.class); 91 builder.addPropertyValue("creators", new ManagedMap()); 92 builder.addPropertyValue("converters", new ManagedMap()); 93 registry.registerBeanDefinition(DEFAULT_SPRING_CONFIGURATOR_ID, builder.getBeanDefinition()); 94 } 95 return registry.getBeanDefinition(DEFAULT_SPRING_CONFIGURATOR_ID); 96 } 97 98 105 protected void registerCreator(BeanDefinitionRegistry registry, String javascript, BeanDefinitionBuilder creatorConfig, Map params, NodeList children) 106 { 107 registerSpringConfiguratorIfNecessary(registry); 108 109 List includes = new ArrayList (); 110 creatorConfig.addPropertyValue("includes", includes); 111 112 List excludes = new ArrayList (); 113 creatorConfig.addPropertyValue("excludes", excludes); 114 115 Properties auth = new Properties (); 116 creatorConfig.addPropertyValue("auth", auth); 117 118 for (int i = 0; i < children.getLength(); i++) 120 { 121 Node node = children.item(i); 122 123 if (node.getNodeType() == Node.TEXT_NODE || node.getNodeType() == Node.COMMENT_NODE) 124 { 125 continue; 126 } 127 128 Element child = (Element ) node; 129 130 if (node.getNodeName().equals("dwr:latencyfilter")) 131 { 132 BeanDefinitionBuilder beanFilter = BeanDefinitionBuilder.rootBeanDefinition(ExtraLatencyAjaxFilter.class); 133 beanFilter.addPropertyValue("delay", child.getAttribute("delay")); 134 BeanDefinitionHolder holder2 = new BeanDefinitionHolder(beanFilter.getBeanDefinition(), "__latencyFilter_" + javascript); 135 BeanDefinitionReaderUtils.registerBeanDefinition(holder2, registry); 136 137 ManagedList filterList = new ManagedList(); 138 filterList.add(new RuntimeBeanReference("__latencyFilter_" + javascript)); 139 creatorConfig.addPropertyValue("filters", filterList); 140 } 141 else if (node.getNodeName().equals("dwr:include")) 142 { 143 includes.add(child.getAttribute("method")); 144 } 145 else if (node.getNodeName().equals("dwr:exclude")) 146 { 147 excludes.add(child.getAttribute("method")); 148 } 149 else if (node.getNodeName().equals("dwr:auth")) 150 { 151 auth.setProperty(child.getAttribute("method"), child.getAttribute("role")); 152 } 153 else if (node.getNodeName().equals("dwr:convert")) 154 { 155 Element element = (Element ) node; 156 String type = element.getAttribute("type"); 157 String className = element.getAttribute("class"); 158 159 ConverterConfig converterConfig = new ConverterConfig(); 160 converterConfig.setType(type); 161 parseConverterSettings(converterConfig, element); 162 lookupConverters(registry).put(className, converterConfig); 163 } 164 else if (node.getNodeName().equals("dwr:filter")) 165 { 166 Element element = (Element ) node; 167 String filterClass = element.getAttribute("class"); 168 BeanDefinitionBuilder beanFilter; 169 try 170 { 171 beanFilter = BeanDefinitionBuilder.rootBeanDefinition(ClassUtils.forName(filterClass)); 172 } 173 catch (ClassNotFoundException e) 174 { 175 throw new IllegalArgumentException ("DWR filter class '" + filterClass + "' was not found. " + 176 "Check the class name specified in <dwr:filter class=\"" + filterClass + 177 "\" /> exists"); 178 } 179 BeanDefinitionHolder holder2 = new BeanDefinitionHolder(beanFilter.getBeanDefinition(), "__filter_" + filterClass + "_" + javascript); 180 BeanDefinitionReaderUtils.registerBeanDefinition(holder2, registry); 181 182 ManagedList filterList = new ManagedList(); 183 filterList.add(new RuntimeBeanReference("__filter_" + filterClass + "_" + javascript)); 184 creatorConfig.addPropertyValue("filters", filterList); 185 } 186 else if (node.getNodeName().equals("dwr:param")) 187 { 188 Element element = (Element ) node; 189 String name = element.getAttribute("name"); 190 String value = element.getAttribute("value"); 191 params.put(name, value); 192 } 193 else 194 { 195 throw new RuntimeException ("an unknown dwr:remote sub node was fouund: " + node.getNodeName()); 196 } 197 } 198 creatorConfig.addPropertyValue("params", params); 199 200 String creatorConfigName = "__" + javascript; 201 BeanDefinitionHolder holder3 = new BeanDefinitionHolder(creatorConfig.getBeanDefinition(), creatorConfigName); 202 BeanDefinitionReaderUtils.registerBeanDefinition(holder3, registry); 203 204 lookupCreators(registry).put(javascript, new RuntimeBeanReference(creatorConfigName)); 205 } 206 207 protected class ConfigurationBeanDefinitionParser implements BeanDefinitionParser 208 { 209 210 public BeanDefinition parse(Element element, ParserContext parserContext) 211 { 212 BeanDefinitionRegistry registry = parserContext.getRegistry(); 213 BeanDefinition beanDefinition = registerSpringConfiguratorIfNecessary(registry); 214 215 Element initElement = DomUtils.getChildElementByTagName(element, "init"); 216 if (initElement != null) 217 { 218 decorate(initElement, new BeanDefinitionHolder(beanDefinition, DEFAULT_SPRING_CONFIGURATOR_ID), parserContext); 219 } 220 221 List createElements = DomUtils.getChildElementsByTagName(element, "create"); 222 Iterator iter = createElements.iterator(); 223 while (iter.hasNext()) 224 { 225 Element createElement = (Element ) iter.next(); 226 decorate(createElement, new BeanDefinitionHolder(beanDefinition, DEFAULT_SPRING_CONFIGURATOR_ID), parserContext); 227 } 228 229 List convertElements = DomUtils.getChildElementsByTagName(element, "convert"); 230 iter = convertElements.iterator(); 231 while (iter.hasNext()) 232 { 233 Element convertElement = (Element ) iter.next(); 234 decorate(convertElement, new BeanDefinitionHolder(beanDefinition, DEFAULT_SPRING_CONFIGURATOR_ID), parserContext); 235 } 236 237 List signatureElements = DomUtils.getChildElementsByTagName(element, "signatures"); 238 for (Iterator i = signatureElements.iterator(); i.hasNext();) 239 { 240 Element signatureElement = (Element ) i.next(); 241 decorate(signatureElement, new BeanDefinitionHolder(beanDefinition, DEFAULT_SPRING_CONFIGURATOR_ID), parserContext); 242 } 243 244 return beanDefinition; 245 } 246 } 247 248 protected class ControllerBeanDefinitionParser implements BeanDefinitionParser 249 { 250 public BeanDefinition parse(Element element, ParserContext parserContext) 251 { 252 BeanDefinitionBuilder dwrController = BeanDefinitionBuilder.rootBeanDefinition(DwrController.class); 253 List configurators = new ManagedList(); 254 configurators.add(new RuntimeBeanReference(DEFAULT_SPRING_CONFIGURATOR_ID)); 255 dwrController.addPropertyValue("configurators", configurators); 256 257 String debug = element.getAttribute("debug"); 258 if (StringUtils.hasText(debug)) 259 { 260 dwrController.addPropertyValue("debug", debug); 261 } 262 263 String beanName = element.getAttribute(BeanDefinitionParserDelegate.ID_ATTRIBUTE); 264 String nameAttr = element.getAttribute(BeanDefinitionParserDelegate.NAME_ATTRIBUTE); 265 String [] aliases = null; 266 if (!StringUtils.hasText(beanName)) 267 { 268 beanName = element.getAttribute("name"); 269 } 270 else 271 { 272 String aliasName = element.getAttribute("name"); 273 if (StringUtils.hasText(aliasName)) 274 { 275 aliases = StringUtils.tokenizeToStringArray(nameAttr, BeanDefinitionParserDelegate.BEAN_NAME_DELIMITERS); 276 } 277 } 278 279 parseControllerParameters(dwrController, element); 280 281 BeanDefinitionHolder holder = new BeanDefinitionHolder(dwrController.getBeanDefinition(), beanName, aliases); 282 BeanDefinitionReaderUtils.registerBeanDefinition(holder, parserContext.getRegistry()); 283 284 return dwrController.getBeanDefinition(); 285 } 286 287 protected void parseControllerParameters(BeanDefinitionBuilder dwrControllerDefinition, Element parent) 288 { 289 NodeList children = parent.getChildNodes(); 290 Map params = new HashMap (); 291 for (int i = 0; i < children.getLength(); i++) 292 { 293 Node node = children.item(i); 294 295 if (node.getNodeType() == Node.TEXT_NODE || node.getNodeType() == Node.COMMENT_NODE) 296 { 297 continue; 298 } 299 300 Element child = (Element ) node; 301 if (child.getNodeName().equals("dwr:config-param")) 302 { 303 String paramName = child.getAttribute("name"); 304 String value = child.getAttribute("value"); 305 params.put(paramName, value); 306 } 307 else 308 { 309 throw new RuntimeException ("an unknown dwr:controller sub node was found: " + node.getNodeName()); 310 } 311 } 312 dwrControllerDefinition.addPropertyValue("configParams", params); 313 } 314 } 315 316 protected class RemoteBeanDefinitionDecorator implements BeanDefinitionDecorator 317 { 318 321 public BeanDefinitionHolder decorate(Node node, BeanDefinitionHolder definition, ParserContext parserContext) 322 { 323 Element element = (Element ) node; 324 325 String javascript = element.getAttribute("javascript"); 326 327 BeanDefinitionBuilder beanCreator = BeanDefinitionBuilder.rootBeanDefinition(BeanCreator.class); 328 329 try 330 { 331 String beanClassName = resolveBeanClassname(definition.getBeanDefinition(), parserContext.getRegistry()); 332 if (beanClassName == null) 333 { 334 throw new FatalBeanException("Unabled to find type for beanName '" + definition.getBeanName() + 335 "'. " + "Check your bean has a correctly configured parent or provide a class for " + 336 " the bean definition"); 337 } 338 beanCreator.addPropertyValue("beanClass", ClassUtils.forName(beanClassName)); 339 } 340 catch (ClassNotFoundException e) 341 { 342 throw new FatalBeanException("Unable to create DWR bean creator for '" + definition.getBeanName() + "'.", e); 343 } 344 345 String name = definition.getBeanName(); 346 if (name.startsWith("scopedTarget.")) 347 { 348 name = name.substring(name.indexOf(".") + 1); 349 } 350 beanCreator.addPropertyValue("beanId", name); 351 beanCreator.addPropertyValue("javascript", javascript); 352 353 BeanDefinitionBuilder creatorConfig = BeanDefinitionBuilder.rootBeanDefinition(CreatorConfig.class); 354 creatorConfig.addPropertyValue("creator", beanCreator.getBeanDefinition()); 355 registerCreator(parserContext.getRegistry(), javascript, creatorConfig, new HashMap (), node.getChildNodes()); 356 357 return definition; 358 } 359 360 368 private String resolveBeanClassname(BeanDefinition definition, BeanDefinitionRegistry registry) 369 { 370 String beanClassName = definition.getBeanClassName(); 371 if (!StringUtils.hasText(beanClassName)) 372 { 373 while (definition instanceof ChildBeanDefinition ) 374 { 375 String parentName = ((ChildBeanDefinition)definition).getParentName(); 376 BeanDefinition parentDefinition = findParentDefinition(parentName, registry); 377 if (parentDefinition == null) 378 { 379 if (log.isDebugEnabled()) 380 { 381 log.debug("No parent bean named '" + parentName + "' could be found in the " + 382 "hierarchy of BeanFactorys. Check you've defined a bean called '" + parentName + "'"); 383 } 384 break; 385 } 386 beanClassName = parentDefinition.getBeanClassName(); 387 if (StringUtils.hasText(beanClassName )) 388 { 389 break; 391 } 392 definition = parentDefinition; 393 } 394 } 395 396 return beanClassName; 397 } 398 399 private BeanDefinition findParentDefinition(String parentName, BeanDefinitionRegistry registry) 400 { 401 if (registry != null) 402 { 403 if (registry.containsBeanDefinition(parentName)) 404 { 405 return registry.getBeanDefinition(parentName); 406 } 407 else if (registry instanceof HierarchicalBeanFactory) 408 { 409 BeanFactory parentBeanFactory = ((HierarchicalBeanFactory)registry).getParentBeanFactory(); 411 return findParentDefinition(parentName, (BeanDefinitionRegistry)parentBeanFactory); 412 } 413 } 414 415 return null; 417 } 418 } 419 420 protected class ConverterBeanDefinitionDecorator implements BeanDefinitionDecorator 421 { 422 423 public BeanDefinitionHolder decorate(Node node, BeanDefinitionHolder definition, ParserContext parserContext) 424 { 425 Element element = (Element ) node; 426 String type = element.getAttribute("type"); 427 String className = element.getAttribute("class"); 428 String javascriptClassName = element.getAttribute("javascript"); 429 430 BeanDefinitionRegistry registry = parserContext.getRegistry(); 431 432 ConverterConfig converterConfig = new ConverterConfig(); 433 converterConfig.setType(type); 434 converterConfig.setJavascriptClassName(javascriptClassName); 435 parseConverterSettings(converterConfig, element); 436 lookupConverters(registry).put(className, converterConfig); 437 438 return definition; 439 } 440 } 441 442 protected void parseConverterSettings(ConverterConfig converterConfig, Element parent) 443 { 444 NodeList children = parent.getChildNodes(); 445 446 for (int i = 0; i < children.getLength(); i++) 448 { 449 Node node = children.item(i); 450 451 if (node.getNodeType() == Node.TEXT_NODE || node.getNodeType() == Node.COMMENT_NODE) 452 { 453 continue; 454 } 455 456 Element child = (Element ) node; 457 if (child.getNodeName().equals("dwr:include")) 458 { 459 converterConfig.addInclude(child.getAttribute("method")); 460 } 461 else if (child.getNodeName().equals("dwr:exclude")) 462 { 463 converterConfig.addExclude(child.getAttribute("method")); 464 } 465 471 else 472 { 473 throw new RuntimeException ("an unknown dwr:remote sub node was found: " + node.getNodeName()); 474 } 475 } 476 477 } 478 479 482 protected class InitDefinitionDecorator implements BeanDefinitionDecorator 483 { 484 public BeanDefinitionHolder decorate(Node parent, BeanDefinitionHolder definition, ParserContext parserContext) 485 { 486 Map converters = new HashMap (); 487 Map creators = new HashMap (); 488 NodeList inits = parent.getChildNodes(); 489 for (int j = 0; j < inits.getLength(); j++) 490 { 491 Node node = inits.item(j); 492 if (node.getNodeType() == Node.TEXT_NODE || node.getNodeType() == Node.COMMENT_NODE) 493 { 494 continue; 495 } 496 497 Element child = (Element )inits.item(j); 498 if (child.getNodeName().equals(ELEMENT_CREATOR)) 499 { 500 String id = child.getAttribute(ATTRIBUTE_ID); 501 String className = child.getAttribute(ATTRIBUTE_CLASS); 502 creators.put(id, className); 503 } 504 else if (child.getNodeName().equals(ELEMENT_CONVERTER)) 505 { 506 String id = child.getAttribute(ATTRIBUTE_ID); 507 String className = child.getAttribute(ATTRIBUTE_CLASS); 508 converters.put(id, className); 509 } 510 else 511 { 512 throw new RuntimeException ("An unknown sub node '" + child.getNodeName() + 513 "' was found while parsing dwr:init"); 514 } 515 } 516 517 518 BeanDefinition configurator = registerSpringConfiguratorIfNecessary(parserContext.getRegistry()); 519 configurator.getPropertyValues().addPropertyValue("creatorTypes", creators); 520 configurator.getPropertyValues().addPropertyValue("converterTypes", converters); 521 522 return definition; 523 } 524 } 525 526 527 531 protected class CreatorBeanDefinitionDecorator implements BeanDefinitionDecorator 532 { 533 534 public BeanDefinitionHolder decorate(Node node, BeanDefinitionHolder definition, ParserContext parserContext) 535 { 536 Element element = (Element ) node; 537 String javascript = element.getAttribute("javascript"); 538 String creatorType = element.getAttribute("type"); 539 540 BeanDefinitionBuilder creatorConfig = BeanDefinitionBuilder.rootBeanDefinition(CreatorConfig.class); 541 542 BeanDefinitionBuilder creator; 545 Map params = new HashMap (); 546 if ("spring".equals(creatorType)) 547 { 548 creator = BeanDefinitionBuilder.rootBeanDefinition(SpringCreator.class); 550 try 552 { 553 creator.addPropertyValue("beanClass", Class.forName(definition.getBeanDefinition().getBeanClassName())); 554 } 555 catch (ClassNotFoundException e) 556 { 557 throw new FatalBeanException("Unable to create DWR bean creator for '" + definition.getBeanName() + "'.", e); 558 } 559 creator.addPropertyValue("javascript", javascript); 560 creatorConfig.addPropertyValue("creator", creator.getBeanDefinition()); 561 } 562 else if ("new".equals(creatorType)) 563 { 564 creator = BeanDefinitionBuilder.rootBeanDefinition(NewCreator.class); 565 creator.addPropertyValue("className", node.getAttributes().getNamedItem("class").getNodeValue()); 566 creator.addPropertyValue("javascript", javascript); 567 creatorConfig.addPropertyValue("creator", creator.getBeanDefinition()); 568 } 569 else if ("null".equals(creatorType)) 570 { 571 creatorConfig.addPropertyValue("creatorType", "none"); 572 String className = element.getAttribute("class"); 573 if (className == null || "".equals(className)) 574 { 575 throw new BeanInitializationException("'class' is a required attribute for the declaration <dwr:creator type=\"null\"" + 576 " javascript=\"" + javascript + "\" ... />"); 577 } 578 params.put("class", className); 579 } 580 else if ("pageflow".equals(creatorType)) 581 { 582 creatorConfig.addPropertyValue("creatorType", creatorType); 583 } 584 else if ("jsf".equals(creatorType) || "scripted".equals(creatorType) || "struts".equals(creatorType)) 585 { 586 creatorConfig.addPropertyValue("creatorType", creatorType); 587 } 588 else 589 { 590 if (log.isDebugEnabled()) 591 { 592 log.debug("Looking up creator type '" + creatorType + "'"); 593 } 594 BeanDefinition configurator = registerSpringConfiguratorIfNecessary(parserContext.getRegistry()); 597 PropertyValue registeredCreators = configurator.getPropertyValues().getPropertyValue("creatorTypes"); 598 Map registeredCreatorMap = (Map )registeredCreators.getValue(); 599 String creatorClass = (String )registeredCreatorMap.get(creatorType); 600 if (creatorClass == null) 601 { 602 throw new UnsupportedOperationException ("Type " + creatorType + " is not supported " + 604 " or the custom creator has not been registered dwr:init"); 605 } 606 else 607 { 608 try 609 { 610 Class clazz = Class.forName(creatorClass); 611 creator = BeanDefinitionBuilder.rootBeanDefinition(clazz); 612 creatorConfig.addPropertyValue("creator", creator.getBeanDefinition()); 613 String className = element.getAttribute("class"); 614 if (StringUtils.hasText(className)) 615 { 616 params.put("class", className); 617 } 618 } 619 catch (ClassNotFoundException ex) 620 { 621 throw new FatalBeanException("ClassNotFoundException trying to register " + 622 " creator '" + creatorClass + "' for javascript type '" + javascript +"'. Check the " + 623 " class in the classpath and that the creator is register in dwr:init", ex); 624 } 625 } 626 } 627 628 registerCreator(parserContext.getRegistry(), javascript, creatorConfig, params, node.getChildNodes()); 629 630 return definition; 631 } 632 } 633 634 protected class SignaturesBeanDefinitionDecorator implements BeanDefinitionDecorator 635 { 636 637 public BeanDefinitionHolder decorate(Node node, BeanDefinitionHolder definition, ParserContext parserContext) 638 { 639 BeanDefinitionRegistry registry = parserContext.getRegistry(); 640 BeanDefinition config = registerSpringConfiguratorIfNecessary(registry); 641 642 StringBuffer sigtext = new StringBuffer (); 643 NodeList children = node.getChildNodes(); 644 for (int i = 0; i < children.getLength(); i++) 645 { 646 Node child = children.item(i); 647 if (child.getNodeType() != Node.TEXT_NODE && child.getNodeType() != Node.CDATA_SECTION_NODE) 648 { 649 log.warn("Ignoring illegal node type: " + child.getNodeType()); 650 continue; 651 } 652 sigtext.append(child.getNodeValue()); 653 } 654 655 config.getPropertyValues().addPropertyValue("signatures", sigtext.toString()); 656 657 return definition; 658 } 659 660 } 661 662 protected Map lookupCreators(BeanDefinitionRegistry registry) 663 { 664 BeanDefinition config = registerSpringConfiguratorIfNecessary(registry); 665 return (Map ) config.getPropertyValues().getPropertyValue("creators").getValue(); 666 } 667 668 protected Map lookupConverters(BeanDefinitionRegistry registry) 669 { 670 BeanDefinition config = registerSpringConfiguratorIfNecessary(registry); 671 return (Map ) config.getPropertyValues().getPropertyValue("converters").getValue(); 672 } 673 674 protected final static String DEFAULT_SPRING_CONFIGURATOR_ID = "__dwrConfiguration"; 675 676 679 protected static final Logger log = Logger.getLogger(DwrNamespaceHandler.class); 680 681 684 private static final String ELEMENT_CONVERTER = "dwr:converter"; 685 686 private static final String ELEMENT_CREATOR = "dwr:creator"; 687 688 691 private static final String ATTRIBUTE_ID = "id"; 692 693 private static final String ATTRIBUTE_CLASS = "class"; 694 695 696 } 697
| Popular Tags
|