1 19 20 package org.netbeans.modules.web.jsf.palette.items; 21 22 import java.io.IOException ; 23 import java.net.URL ; 24 import java.text.MessageFormat ; 25 import java.util.ArrayList ; 26 import java.util.Collection ; 27 import java.util.Iterator ; 28 import java.util.LinkedList ; 29 import java.util.List ; 30 import javax.lang.model.element.AnnotationMirror; 31 import javax.lang.model.element.AnnotationValue; 32 import javax.lang.model.element.Element; 33 import javax.lang.model.element.ElementKind; 34 import javax.lang.model.element.ExecutableElement; 35 import javax.lang.model.element.Name; 36 import javax.lang.model.element.TypeElement; 37 import javax.lang.model.element.VariableElement; 38 import javax.lang.model.type.DeclaredType; 39 import javax.lang.model.type.TypeKind; 40 import javax.lang.model.type.TypeMirror; 41 import javax.lang.model.util.ElementFilter; 42 import javax.lang.model.util.Types; 43 import javax.swing.text.BadLocationException ; 44 import javax.swing.text.Caret ; 45 import javax.swing.text.Document ; 46 import javax.swing.text.JTextComponent ; 47 import org.netbeans.api.java.classpath.ClassPath; 48 import org.netbeans.api.java.source.CompilationController; 49 import org.netbeans.api.java.source.JavaSource; 50 import org.netbeans.modules.editor.NbEditorUtilities; 51 import org.netbeans.modules.j2ee.common.source.AbstractTask; 52 import org.netbeans.modules.web.api.webmodule.WebModule; 53 import org.netbeans.modules.web.jsf.JSFConfigUtilities; 54 import org.netbeans.modules.web.jsf.palette.JSFPaletteUtilities; 55 import org.netbeans.modules.web.jsf.wizards.JSFClientGenerator; 56 import org.netbeans.spi.java.classpath.support.ClassPathSupport; 57 import org.openide.ErrorManager; 58 import org.openide.filesystems.FileObject; 59 import org.openide.loaders.DataObject; 60 import org.openide.text.ActiveEditorDrop; 61 62 66 public final class JsfForm implements ActiveEditorDrop { 67 68 72 public static final int FORM_TYPE_EMPTY = 0; 73 public static final int FORM_TYPE_DETAIL = 1; 74 public static final int FORM_TYPE_NEW = 2; 75 public static final int FORM_TYPE_EDIT = 3; 76 77 private static String [] BEGIN = { 78 "<h:form>\n", 79 "<h2>Detail</h2>\n <h:form>\n<h:panelGrid columns=\"2\">\n", 80 "<h2>Create</h2>\n <h:form>\n<h:panelGrid columns=\"2\">\n", 81 "<h2>Edit</h2>\n <h:form>\n<h:panelGrid columns=\"2\">\n", 82 }; 83 private static String [] END = { 84 "</h:form>\n", 85 "</h:panelGrid>\n </h:form>\n", 86 "</h:panelGrid>\n </h:form>\n", 87 "</h:panelGrid>\n </h:form>\n", 88 }; 89 private static String [] ITEM = { 90 "", 91 "<h:outputText value=\"{0}:\"/>\n <h:outputText value=\"#'{'{1}.{2}}\" title=\"{0}\" />\n", 92 "<h:outputText value=\"{0}:\"/>\n <h:inputText id=\"{2}\" value=\"#'{'{1}.{2}}\" title=\"{0}\" />\n", 93 "<h:outputText value=\"{0}:\"/>\n <h:inputText id=\"{2}\" value=\"#'{'{1}.{2}}\" title=\"{0}\" />\n", 94 "<h:outputText value=\"{0}:\"/>\n <h:selectOneMenu id=\"{2}\" value=\"#'{'{1}.{2}}\" title=\"{0}\">\n <f:selectItems value=\"#'{'{3}.{2}s'}'\"/>\n </h:selectOneMenu>\n", 96 "<h:outputText value=\"{0} ({4}):\"/>\n <h:inputText id=\"{2}\" value=\"#'{'{1}.{2}}\" title=\"{0}\" >\n <f:convertDateTime type=\"{3}\" pattern=\"{4}\" />\n</h:inputText>\n", 98 "<h:outputText value=\"{0}:\" rendered=\"#'{'{1}.{2} == null}\"/>\n <h:selectOneMenu id=\"{2}\" value=\"#'{'{1}.{2}}\" title=\"{0}\" rendered=\"#'{'{1}.{2} == null}\">\n <f:selectItems value=\"#'{'{3}.{2}s'}'\"/>\n </h:selectOneMenu>\n", 100 }; 101 102 private String variable = ""; 103 private String bean = ""; 104 private int formType = 0; 105 106 public JsfForm() { 107 } 108 109 public boolean handleTransfer(JTextComponent targetComponent) { 110 111 JsfFormCustomizer jsfFormCustomizer = new JsfFormCustomizer(this, targetComponent); 112 boolean accept = jsfFormCustomizer.showDialog(); 113 if (accept) { 114 try { 115 Caret caret = targetComponent.getCaret(); 116 int position0 = Math.min(caret.getDot(), caret.getMark()); 117 int position1 = Math.max(caret.getDot(), caret.getMark()); 118 int len = targetComponent.getDocument().getLength() - position1; 119 boolean containsFView = targetComponent.getText(0, position0).contains("<f:view>") 120 && targetComponent.getText(position1, len).contains("</f:view>"); 121 String body = createBody(targetComponent, !containsFView); 122 JSFPaletteUtilities.insert(body, targetComponent); 123 } catch (IOException ioe) { 124 accept = false; 125 } catch (BadLocationException ble) { 126 accept = false; 127 } 128 } 129 130 return accept; 131 } 132 133 private String createBody(JTextComponent target, boolean surroundWithFView) throws IOException { 134 final StringBuffer stringBuffer = new StringBuffer (); 135 if (surroundWithFView) { 136 stringBuffer.append("<f:view>\n"); 137 } 138 stringBuffer.append(MessageFormat.format(BEGIN [formType], new Object [] {variable})); 139 140 FileObject fileObject = getFO(target); 141 JavaSource javaSource = JavaSource.forFileObject(fileObject); 142 javaSource.runUserActionTask(new AbstractTask<CompilationController>() { 143 public void run(CompilationController controller) throws IOException { 144 controller.toPhase(JavaSource.Phase.ELEMENTS_RESOLVED); 145 TypeElement typeElement = controller.getElements().getTypeElement(bean); 146 createForm(controller, typeElement, formType, variable, stringBuffer, false); 147 } 148 }, true); 149 150 stringBuffer.append(END [formType]); 151 if (surroundWithFView) { 152 stringBuffer.append("</f:view>\n"); 153 } 154 return stringBuffer.toString(); 155 } 156 157 public static final int REL_NONE = 0; 158 public static final int REL_TO_ONE = 1; 159 public static final int REL_TO_MANY = 2; 160 161 public static int isRelationship(CompilationController controller, ExecutableElement method, boolean isFieldAccess) { 162 Element element = isFieldAccess ? guessField(controller, method) : method; 163 if (element != null) { 164 if (isAnnotatedWith(element, "javax.persistence.OneToOne") || isAnnotatedWith(element, "javax.persistence.ManyToOne")) { 165 return REL_TO_ONE; 166 } 167 if (isAnnotatedWith(element, "javax.persistence.OneToMany") || isAnnotatedWith(element, "javax.persistence.ManyToMany")) { 168 return REL_TO_MANY; 169 } 170 } 171 return REL_NONE; 172 } 173 174 public static ExecutableElement getOtherSideOfRelation(Types types, ExecutableElement executableElement, boolean isFieldAccess) { 175 TypeMirror passedReturnType = executableElement.getReturnType(); 176 if (TypeKind.DECLARED == passedReturnType.getKind()) { 177 TypeElement typeElement = (TypeElement) passedReturnType; 178 for (ExecutableElement method : getEntityMethods(typeElement)) { 180 TypeMirror iteratedReturnType = method.getReturnType(); 181 if (types.isSameType(passedReturnType, iteratedReturnType)) { 182 return method; 183 } 184 } 185 } 186 return null; 187 } 188 189 192 public static ExecutableElement[] getEntityMethods(TypeElement entityTypeElement) { 193 List <ExecutableElement> result = new LinkedList <ExecutableElement>(); 194 TypeElement typeElement = entityTypeElement; 195 while (typeElement != null) { 196 boolean isEntityOrMapped = false; 197 if (isAnnotatedWith(typeElement, "javax.persistence.Entity") || isAnnotatedWith(typeElement, "javax.persistence.MappedSuperclass")) { 198 isEntityOrMapped = true; 199 break; 200 } 201 if (isEntityOrMapped) { 202 result.addAll(ElementFilter.methodsIn(typeElement.getEnclosedElements())); 203 } 204 Element enclosingElement = typeElement.getEnclosingElement(); 205 if (ElementKind.CLASS == enclosingElement.getKind()) { 206 typeElement = (TypeElement) enclosingElement; 207 } 208 } 209 return result.toArray(new ExecutableElement[result.size()]); 210 } 211 212 static boolean isId(CompilationController controller, ExecutableElement method, boolean isFieldAccess) { 213 Element element = isFieldAccess ? guessField(controller, method) : method; 214 if (element != null) { 215 if (isAnnotatedWith(element, "javax.persistence.Id") || isAnnotatedWith(element, "javax.persistence.EmbeddedId")) { 216 return true; 217 } 218 } 219 return false; 220 } 221 222 static boolean isGenerated(CompilationController controller, ExecutableElement method, boolean isFieldAccess) { 223 Element element = isFieldAccess ? guessField(controller, method) : method; 224 if (element != null) { 225 if (isAnnotatedWith(element, "javax.persistence.GeneratedValue")) { 226 return true; 227 } 228 } 229 return false; 230 } 231 232 static String getTemporal(CompilationController controller, ExecutableElement method, boolean isFieldAccess) { 233 Element element = isFieldAccess ? guessField(controller, method) : method; 234 if (element != null) { 235 AnnotationMirror annotationMirror = findAnnotation(element, "javax.persistence.Temporal"); 236 if (annotationMirror != null) { 237 Collection <? extends AnnotationValue> attributes = annotationMirror.getElementValues().values(); 238 if (attributes.iterator().hasNext()) { 239 AnnotationValue annotationValue = attributes.iterator().next(); 240 if (annotationValue != null) { 241 return null; } 244 } 245 } 246 } 247 return null; 248 } 249 250 static FileObject getFO(JTextComponent target) { 251 Document doc = target.getDocument(); 252 if (doc != null) { 253 DataObject dobj = NbEditorUtilities.getDataObject(doc); 254 if (dobj != null) { 255 return dobj.getPrimaryFile(); 256 } 257 } 258 return null; 259 } 260 261 static ClassPath getFullClasspath(FileObject fileObject) { 262 ArrayList entries = new ArrayList (); 263 ArrayList urls = new ArrayList (); 264 entries.addAll(ClassPath.getClassPath(fileObject, ClassPath.SOURCE).entries()); 265 entries.addAll(ClassPath.getClassPath(fileObject, ClassPath.BOOT).entries()); 266 entries.addAll(ClassPath.getClassPath(fileObject, ClassPath.COMPILE).entries()); 267 for (Iterator it = entries.iterator(); it.hasNext();) { 268 ClassPath.Entry classPathEntry = (ClassPath.Entry) it.next(); 269 urls.add(classPathEntry.getURL()); 270 } 271 return ClassPathSupport.createClassPath((URL []) urls.toArray(new URL [urls.size()])); 272 } 273 274 static boolean hasModuleJsf(JTextComponent target) { 275 FileObject fileObject = getFO(target); 276 if (fileObject != null) { 277 WebModule webModule = WebModule.getWebModule(fileObject); 278 String [] configFiles = JSFConfigUtilities.getConfigFiles(webModule.getDeploymentDescriptor()); 279 return configFiles != null && configFiles.length > 0; 280 } 281 return false; 282 } 283 284 public static boolean isEntityClass(TypeElement typeElement) { 285 if (isAnnotatedWith(typeElement, "javax.persistence.Entity")) { 286 return true; 287 } 288 return false; 289 } 290 291 public static boolean isEmbeddableClass(TypeElement typeElement) { 292 if (isAnnotatedWith(typeElement, "javax.persistence.Embeddable")) { 293 return true; 294 } 295 return false; 296 } 297 298 public static boolean isFieldAccess(TypeElement clazz) { 299 boolean fieldAccess = false; 300 boolean accessTypeDetected = false; 301 TypeElement typeElement = clazz; 302 while (typeElement != null) { 303 for (Element element : typeElement.getEnclosedElements()) { 304 if (isAnnotatedWith(element, "javax.persistence.Id") || isAnnotatedWith(element, "javax.persistence.EmbeddedId")) { 305 if (ElementKind.FIELD == element.getKind()) { 306 fieldAccess = true; 307 } 308 accessTypeDetected = true; 309 } 310 } 311 if (!accessTypeDetected) { 312 ErrorManager.getDefault().log(ErrorManager.WARNING, "Failed to detect correct access type for class:" + typeElement.getQualifiedName()); 313 } 314 typeElement = (TypeElement) typeElement.getEnclosingElement(); 315 } 316 return fieldAccess; 317 } 318 319 public static VariableElement guessField(CompilationController controller, ExecutableElement getter) { 320 String name = getter.getSimpleName().toString().substring(3); 321 String guessFieldName = name.substring(0,1).toLowerCase() + name.substring(1); 322 TypeElement typeElement = (TypeElement) getter.getEnclosingElement(); 323 for (VariableElement variableElement : ElementFilter.fieldsIn(typeElement.getEnclosedElements())) { 324 if (variableElement.getSimpleName().contentEquals(guessFieldName)) { 325 return variableElement; 326 } 327 } 328 ErrorManager.getDefault().log(ErrorManager.WARNING, "Cannot detect the field associated with property: " + guessFieldName); 329 return null; 330 } 331 332 333 public static boolean isReadOnly(Types types, ExecutableElement getter) { 334 String setterName = "set" + getter.getSimpleName().toString().substring(3); TypeMirror propertyType = getter.getReturnType(); 336 TypeElement enclosingClass = (TypeElement) getter.getEnclosingElement(); 337 for (ExecutableElement executableElement : ElementFilter.methodsIn(enclosingClass.getEnclosedElements())) { 338 if (executableElement.getSimpleName().contentEquals(setterName)) { 339 if (executableElement.getParameters().size() == 1) { 340 VariableElement firstParam = executableElement.getParameters().get(0); 341 if (types.isSameType(firstParam.asType(), propertyType)) { 342 return false; 343 } 344 } 345 } 346 } 347 return true; 348 } 349 350 static String getDateTimeFormat(String temporal) { 351 if ("DATE".equals(temporal)) { 352 return "MM/dd/yyyy"; 353 } else if ("TIME".equals(temporal)) { 354 return "hh:mm:ss"; 355 } else { 356 return "MM/dd/yyyy, hh:mm:ss"; 357 } 358 } 359 360 public static void createForm(CompilationController controller, TypeElement bean, int formType, String variable, StringBuffer stringBuffer, boolean createSelectForRel) { 361 ExecutableElement methods [] = getEntityMethods(bean); 362 boolean fieldAccess = isFieldAccess(bean); 363 TypeMirror dateTypeMirror = controller.getElements().getTypeElement("java.util.Date").asType(); 364 for (ExecutableElement method : methods) { 365 String methodName = method.getSimpleName().toString(); 366 if (methodName.startsWith("get")) { 367 int isRelationship = isRelationship(controller, method, fieldAccess); 368 String name = methodName.substring(3); 369 String propName = JSFClientGenerator.getPropNameFromMethod(methodName); 370 if (formType == FORM_TYPE_NEW && 371 ((isId(controller, method, fieldAccess) && isGenerated(controller, method, fieldAccess)) || 372 isReadOnly(controller.getTypes(), method))) { 373 } else if (formType == FORM_TYPE_EDIT && (isId(controller, method, fieldAccess) || isReadOnly(controller.getTypes(), method))) { 375 stringBuffer.append(MessageFormat.format(ITEM [FORM_TYPE_DETAIL], new Object [] {name, variable, propName})); 377 } else if ((formType == FORM_TYPE_NEW || formType == FORM_TYPE_EDIT) && controller.getTypes().isSameType(dateTypeMirror, method.getReturnType())) { 378 String temporal = getTemporal(controller, method, fieldAccess); 379 if (temporal == null) { 380 stringBuffer.append(MessageFormat.format(ITEM [formType], new Object [] {name, variable, propName})); 381 } else { 382 stringBuffer.append(MessageFormat.format(ITEM [5], new Object [] {name, variable, propName, temporal, getDateTimeFormat(temporal)})); 384 } 385 } else if ((formType == FORM_TYPE_DETAIL && isRelationship == REL_TO_ONE) || isRelationship == REL_NONE) { 386 stringBuffer.append(MessageFormat.format(ITEM [formType], new Object [] {name, variable, propName})); 388 } else if (isRelationship == REL_TO_ONE) { 389 stringBuffer.append(MessageFormat.format(formType == FORM_TYPE_EDIT ? ITEM [4] : ITEM[6], new Object [] {name, variable, propName, variable.substring(0, variable.lastIndexOf('.'))})); 391 } 392 } 393 } 394 } 395 396 public static void createTablesForRelated(CompilationController controller, TypeElement bean, int formType, String variable, 397 String idProperty, boolean isInjection, StringBuffer stringBuffer) { 398 ExecutableElement methods [] = getEntityMethods(bean); 399 String simpleClass = bean.getSimpleName().toString(); 400 String managedBean = JSFClientGenerator.getManagedBeanName(simpleClass); 401 boolean fieldAccess = isFieldAccess(bean); 402 if (formType == FORM_TYPE_DETAIL) { 404 for (ExecutableElement method : methods) { 405 String methodName = method.getSimpleName().toString(); 406 if (methodName.startsWith("get")) { 407 int isRelationship = isRelationship(controller, method, fieldAccess); 408 String name = methodName.substring(3); 409 String propName = JSFClientGenerator.getPropNameFromMethod(methodName); 410 if (isRelationship == REL_TO_MANY) { 411 ExecutableElement otherSide = getOtherSideOfRelation(controller.getTypes(), method, fieldAccess); 412 int otherSideMultiplicity = REL_TO_ONE; 413 if (otherSide != null) { 414 TypeElement relClass = (TypeElement) otherSide.getEnclosingElement(); 415 boolean isRelFieldAccess = isFieldAccess(relClass); 416 otherSideMultiplicity = isRelationship(controller, otherSide, isRelFieldAccess); 417 } 418 419 List <TypeElement> typeParameters = getTypeParameters(method.getReturnType()); 420 TypeElement typeElement = typeParameters.size() > 0 ? typeParameters.get(0) : null; 421 422 if (typeElement != null) { 423 boolean relatedIsFieldAccess = isFieldAccess(typeElement); 424 String getterName = getIdGetter(controller, relatedIsFieldAccess, typeElement).getSimpleName().toString(); 425 String relatedIdProperty = JSFClientGenerator.getPropNameFromMethod(getterName); 426 String relatedClass = typeElement.getSimpleName().toString(); 427 String relatedManagedBean = JSFClientGenerator.getManagedBeanName(relatedClass); 428 String detailManagedBean = bean.getSimpleName().toString(); 429 stringBuffer.append("<h2>List of " + name + "</h2>\n"); 430 stringBuffer.append("<h:outputText rendered=\"#{not " + relatedManagedBean + ".detail" + relatedClass + "s.rowAvailable}\" value=\"No " + name + "\"/><br>\n"); 431 stringBuffer.append("<h:dataTable value=\"#{" + relatedManagedBean + ".detail" + relatedClass + "s}\" var=\"item\" \n"); 432 stringBuffer.append("border=\"1\" cellpadding=\"2\" cellspacing=\"0\" \n rendered=\"#{not empty " + relatedManagedBean + ".detail" + relatedClass + "s}\">\n"); String removeItems = "remove" + methodName.substring(3); 434 String commands = " <h:column>\n <h:commandLink value=\"Destroy\" action=\"#'{'" + relatedManagedBean + ".destroyFrom" + detailManagedBean + "'}'\">\n" 435 + "<f:param name=\"" + relatedIdProperty +"\" value=\"#'{'{0}." + relatedIdProperty + "'}'\"/>\n" 436 + "<f:param name=\"relatedId\" value=\"#'{'" + variable + "." + idProperty + "'}'\"/>\n" 437 + "</h:commandLink>\n <h:outputText value=\" \"/>\n" 438 + " <h:commandLink value=\"Edit\" action=\"#'{'" + relatedManagedBean + ".editSetup'}'\">\n" 439 + "<f:param name=\"" + relatedIdProperty +"\" value=\"#'{'{0}." + relatedIdProperty + "'}'\"/>\n" 440 + "<h:outputText value=\" \"/>\n </h:commandLink>\n" 441 + (otherSideMultiplicity == REL_TO_MANY ? "<h:commandLink value=\"Remove\" action=\"#'{'" + managedBean + "." + removeItems + "'}'\"/>" : "") 442 + "</h:column>\n"; 443 444 JsfTable.createTable(controller, typeElement, variable + "." + propName, stringBuffer, commands, "detailSetup"); 445 stringBuffer.append("</h:dataTable>\n"); 446 if (otherSideMultiplicity == REL_TO_MANY) { 447 stringBuffer.append("<br>\n Add " + relatedClass + "s:\n <br>\n"); 448 String itemsToAdd = JSFClientGenerator.getPropNameFromMethod(methodName + "ToAdd"); 449 stringBuffer.append("<h:selectManyListbox id=\"add" + relatedClass + "s\" value=\"#{" 450 + managedBean + "." + itemsToAdd + "}\" title=\"Add " + name + ":\">\n"); 451 String availableItems = JSFClientGenerator.getPropNameFromMethod(methodName + "Available"); 452 stringBuffer.append("<f:selectItems value=\"#{" + managedBean + "." + availableItems + "}\"/>\n"); 453 stringBuffer.append("</h:selectManyListbox>\n"); 454 String addItems = "add" + methodName.substring(3); 455 stringBuffer.append("<h:commandButton value=\"Add\" action=\"#{" + managedBean + "." + addItems + "}\"/>\n <br>\n"); 456 } 457 stringBuffer.append("<h:commandLink value=\"New " + name + "\" action=\"#{" + relatedManagedBean + ".createFrom" + detailManagedBean + "Setup}\">\n"); 458 stringBuffer.append("<f:param name=\"relatedId\" value=\"#{" + variable + "." + idProperty + "}\"/>\n"); 459 stringBuffer.append("</h:commandLink>\n <br>\n <br>\n"); 460 } else { 461 ErrorManager.getDefault().log(ErrorManager.INFORMATIONAL, "cannot find referenced class: " + method.getReturnType()); 462 } 463 } 464 } 465 } 466 } 467 } 468 469 public static ExecutableElement getIdGetter(CompilationController controller, final boolean isFieldAccess, final TypeElement typeElement) { 470 ExecutableElement[] methods = getEntityMethods(typeElement); 471 for (ExecutableElement method : methods) { 472 String methodName = method.getSimpleName().toString(); 473 if (methodName.startsWith("get")) { 474 Element element = isFieldAccess ? JsfForm.guessField(controller, method) : method; 475 if (element != null) { 476 if (isAnnotatedWith(element, "javax.persistence.Id") || isAnnotatedWith(element, "javax.persistence.EmbeddedId")) { 477 return method; 478 } 479 } 480 } 481 } 482 ErrorManager.getDefault().log(ErrorManager.WARNING, "Cannot find ID getter in class: " + typeElement.getQualifiedName()); 483 return null; 484 } 485 486 public String getVariable() { 487 return variable; 488 } 489 490 public void setVariable(String variable) { 491 this.variable = variable; 492 } 493 494 public String getBean() { 495 return bean; 496 } 497 498 public void setBean(String collection) { 499 this.bean = collection; 500 } 501 502 public int getFormType() { 503 return formType; 504 } 505 506 public void setFormType(int formType) { 507 this.formType = formType; 508 } 509 510 private static boolean isAnnotatedWith(Element element, String annotationFqn) { 511 return findAnnotation(element, annotationFqn) != null; 512 } 513 514 private static AnnotationMirror findAnnotation(Element element, String annotationFqn) { 515 for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) { 516 DeclaredType annotationDeclaredType = annotationMirror.getAnnotationType(); 517 TypeElement annotationTypeElement = (TypeElement) annotationDeclaredType.asElement(); 518 Name name = annotationTypeElement.getQualifiedName(); 519 if (name.contentEquals(annotationFqn)) { 520 return annotationMirror; 521 } 522 } 523 return null; 524 } 525 526 private static List <TypeElement> getTypeParameters(TypeMirror typeMirrror) { 527 List <TypeElement> result = new ArrayList <TypeElement>(); 528 return result; 530 } 531 532 } 533 | Popular Tags |