1 17 package org.apache.bcel.generic; 18 19 import java.util.ArrayList ; 20 import java.util.Hashtable ; 21 import java.util.Iterator ; 22 import java.util.List ; 23 import java.util.Stack ; 24 import org.apache.bcel.Constants; 25 import org.apache.bcel.classfile.Attribute; 26 import org.apache.bcel.classfile.Code; 27 import org.apache.bcel.classfile.CodeException; 28 import org.apache.bcel.classfile.ExceptionTable; 29 import org.apache.bcel.classfile.LineNumber; 30 import org.apache.bcel.classfile.LineNumberTable; 31 import org.apache.bcel.classfile.LocalVariable; 32 import org.apache.bcel.classfile.LocalVariableTable; 33 import org.apache.bcel.classfile.Method; 34 import org.apache.bcel.classfile.Utility; 35 import org.apache.bcel.util.BCELComparator; 36 37 53 public class MethodGen extends FieldGenOrMethodGen { 54 55 private String class_name; 56 private Type[] arg_types; 57 private String [] arg_names; 58 private int max_locals; 59 private int max_stack; 60 private InstructionList il; 61 private boolean strip_attributes; 62 private List variable_vec = new ArrayList (); 63 private List line_number_vec = new ArrayList (); 64 private List exception_vec = new ArrayList (); 65 private List throws_vec = new ArrayList (); 66 private List code_attrs_vec = new ArrayList (); 67 private static BCELComparator _cmp = new BCELComparator() { 68 69 public boolean equals( Object o1, Object o2 ) { 70 MethodGen THIS = (MethodGen) o1; 71 MethodGen THAT = (MethodGen) o2; 72 return THIS.getName().equals(THAT.getName()) 73 && THIS.getSignature().equals(THAT.getSignature()); 74 } 75 76 77 public int hashCode( Object o ) { 78 MethodGen THIS = (MethodGen) o; 79 return THIS.getSignature().hashCode() ^ THIS.getName().hashCode(); 80 } 81 }; 82 83 84 106 public MethodGen(int access_flags, Type return_type, Type[] arg_types, String [] arg_names, 107 String method_name, String class_name, InstructionList il, ConstantPoolGen cp) { 108 setAccessFlags(access_flags); 109 setType(return_type); 110 setArgumentTypes(arg_types); 111 setArgumentNames(arg_names); 112 setName(method_name); 113 setClassName(class_name); 114 setInstructionList(il); 115 setConstantPool(cp); 116 boolean abstract_ = isAbstract() || isNative(); 117 InstructionHandle start = null; 118 InstructionHandle end = null; 119 if (!abstract_) { 120 start = il.getStart(); 121 end = il.getEnd(); 122 124 if (!isStatic() && (class_name != null)) { addLocalVariable("this", new ObjectType(class_name), start, end); 126 } 127 } 128 if (arg_types != null) { 129 int size = arg_types.length; 130 for (int i = 0; i < size; i++) { 131 if (Type.VOID == arg_types[i]) { 132 throw new ClassGenException("'void' is an illegal argument type for a method"); 133 } 134 } 135 if (arg_names != null) { if (size != arg_names.length) { 137 throw new ClassGenException("Mismatch in argument array lengths: " + size 138 + " vs. " + arg_names.length); 139 } 140 } else { arg_names = new String [size]; 142 for (int i = 0; i < size; i++) { 143 arg_names[i] = "arg" + i; 144 } 145 setArgumentNames(arg_names); 146 } 147 if (!abstract_) { 148 for (int i = 0; i < size; i++) { 149 addLocalVariable(arg_names[i], arg_types[i], start, end); 150 } 151 } 152 } 153 } 154 155 156 163 public MethodGen(Method m, String class_name, ConstantPoolGen cp) { 164 this(m.getAccessFlags(), Type.getReturnType(m.getSignature()), Type.getArgumentTypes(m 165 .getSignature()), null 166 , m.getName(), class_name, 167 ((m.getAccessFlags() & (Constants.ACC_ABSTRACT | Constants.ACC_NATIVE)) == 0) 168 ? new InstructionList(m.getCode().getCode()) 169 : null, cp); 170 Attribute[] attributes = m.getAttributes(); 171 for (int i = 0; i < attributes.length; i++) { 172 Attribute a = attributes[i]; 173 if (a instanceof Code) { 174 Code c = (Code) a; 175 setMaxStack(c.getMaxStack()); 176 setMaxLocals(c.getMaxLocals()); 177 CodeException[] ces = c.getExceptionTable(); 178 if (ces != null) { 179 for (int j = 0; j < ces.length; j++) { 180 CodeException ce = ces[j]; 181 int type = ce.getCatchType(); 182 ObjectType c_type = null; 183 if (type > 0) { 184 String cen = m.getConstantPool().getConstantString(type, 185 Constants.CONSTANT_Class); 186 c_type = new ObjectType(cen); 187 } 188 int end_pc = ce.getEndPC(); 189 int length = m.getCode().getCode().length; 190 InstructionHandle end; 191 if (length == end_pc) { end = il.getEnd(); 193 } else { 194 end = il.findHandle(end_pc); 195 end = end.getPrev(); } 197 addExceptionHandler(il.findHandle(ce.getStartPC()), end, il.findHandle(ce 198 .getHandlerPC()), c_type); 199 } 200 } 201 Attribute[] c_attributes = c.getAttributes(); 202 for (int j = 0; j < c_attributes.length; j++) { 203 a = c_attributes[j]; 204 if (a instanceof LineNumberTable) { 205 LineNumber[] ln = ((LineNumberTable) a).getLineNumberTable(); 206 for (int k = 0; k < ln.length; k++) { 207 LineNumber l = ln[k]; 208 InstructionHandle ih = il.findHandle(l.getStartPC()); 209 if (ih != null) { 210 addLineNumber(ih, l.getLineNumber()); 211 } 212 } 213 } else if (a instanceof LocalVariableTable) { 214 LocalVariable[] lv = ((LocalVariableTable) a).getLocalVariableTable(); 215 removeLocalVariables(); 216 for (int k = 0; k < lv.length; k++) { 217 LocalVariable l = lv[k]; 218 InstructionHandle start = il.findHandle(l.getStartPC()); 219 InstructionHandle end = il.findHandle(l.getStartPC() + l.getLength()); 220 if (null == start) { 222 start = il.getStart(); 223 } 224 if (null == end) { 225 end = il.getEnd(); 226 } 227 addLocalVariable(l.getName(), Type.getType(l.getSignature()), l 228 .getIndex(), start, end); 229 } 230 } else { 231 addCodeAttribute(a); 232 } 233 } 234 } else if (a instanceof ExceptionTable) { 235 String [] names = ((ExceptionTable) a).getExceptionNames(); 236 for (int j = 0; j < names.length; j++) { 237 addException(names[j]); 238 } 239 } else { 240 addAttribute(a); 241 } 242 } 243 } 244 245 246 258 public LocalVariableGen addLocalVariable( String name, Type type, int slot, 259 InstructionHandle start, InstructionHandle end ) { 260 byte t = type.getType(); 261 if (t != Constants.T_ADDRESS) { 262 int add = type.getSize(); 263 if (slot + add > max_locals) { 264 max_locals = slot + add; 265 } 266 LocalVariableGen l = new LocalVariableGen(slot, name, type, start, end); 267 int i; 268 if ((i = variable_vec.indexOf(l)) >= 0) { 269 variable_vec.set(i, l); 270 } else { 271 variable_vec.add(l); 272 } 273 return l; 274 } else { 275 throw new IllegalArgumentException ("Can not use " + type 276 + " as type for local variable"); 277 } 278 } 279 280 281 293 public LocalVariableGen addLocalVariable( String name, Type type, InstructionHandle start, 294 InstructionHandle end ) { 295 return addLocalVariable(name, type, max_locals, start, end); 296 } 297 298 299 303 public void removeLocalVariable( LocalVariableGen l ) { 304 variable_vec.remove(l); 305 } 306 307 308 311 public void removeLocalVariables() { 312 variable_vec.clear(); 313 } 314 315 316 319 private static final void sort( LocalVariableGen[] vars, int l, int r ) { 320 int i = l, j = r; 321 int m = vars[(l + r) / 2].getIndex(); 322 LocalVariableGen h; 323 do { 324 while (vars[i].getIndex() < m) { 325 i++; 326 } 327 while (m < vars[j].getIndex()) { 328 j--; 329 } 330 if (i <= j) { 331 h = vars[i]; 332 vars[i] = vars[j]; 333 vars[j] = h; i++; 335 j--; 336 } 337 } while (i <= j); 338 if (l < j) { 339 sort(vars, l, j); 340 } 341 if (i < r) { 342 sort(vars, i, r); 343 } 344 } 345 346 347 353 public LocalVariableGen[] getLocalVariables() { 354 int size = variable_vec.size(); 355 LocalVariableGen[] lg = new LocalVariableGen[size]; 356 variable_vec.toArray(lg); 357 for (int i = 0; i < size; i++) { 358 if (lg[i].getStart() == null) { 359 lg[i].setStart(il.getStart()); 360 } 361 if (lg[i].getEnd() == null) { 362 lg[i].setEnd(il.getEnd()); 363 } 364 } 365 if (size > 1) { 366 sort(lg, 0, size - 1); 367 } 368 return lg; 369 } 370 371 372 375 public LocalVariableTable getLocalVariableTable( ConstantPoolGen cp ) { 376 LocalVariableGen[] lg = getLocalVariables(); 377 int size = lg.length; 378 LocalVariable[] lv = new LocalVariable[size]; 379 for (int i = 0; i < size; i++) { 380 lv[i] = lg[i].getLocalVariable(cp); 381 } 382 return new LocalVariableTable(cp.addUtf8("LocalVariableTable"), 2 + lv.length * 10, lv, cp 383 .getConstantPool()); 384 } 385 386 387 394 public LineNumberGen addLineNumber( InstructionHandle ih, int src_line ) { 395 LineNumberGen l = new LineNumberGen(ih, src_line); 396 line_number_vec.add(l); 397 return l; 398 } 399 400 401 404 public void removeLineNumber( LineNumberGen l ) { 405 line_number_vec.remove(l); 406 } 407 408 409 412 public void removeLineNumbers() { 413 line_number_vec.clear(); 414 } 415 416 417 420 public LineNumberGen[] getLineNumbers() { 421 LineNumberGen[] lg = new LineNumberGen[line_number_vec.size()]; 422 line_number_vec.toArray(lg); 423 return lg; 424 } 425 426 427 430 public LineNumberTable getLineNumberTable( ConstantPoolGen cp ) { 431 int size = line_number_vec.size(); 432 LineNumber[] ln = new LineNumber[size]; 433 try { 434 for (int i = 0; i < size; i++) { 435 ln[i] = ((LineNumberGen) line_number_vec.get(i)).getLineNumber(); 436 } 437 } catch (ArrayIndexOutOfBoundsException e) { 438 } return new LineNumberTable(cp.addUtf8("LineNumberTable"), 2 + ln.length * 4, ln, cp 440 .getConstantPool()); 441 } 442 443 444 455 public CodeExceptionGen addExceptionHandler( InstructionHandle start_pc, 456 InstructionHandle end_pc, InstructionHandle handler_pc, ObjectType catch_type ) { 457 if ((start_pc == null) || (end_pc == null) || (handler_pc == null)) { 458 throw new ClassGenException("Exception handler target is null instruction"); 459 } 460 CodeExceptionGen c = new CodeExceptionGen(start_pc, end_pc, handler_pc, catch_type); 461 exception_vec.add(c); 462 return c; 463 } 464 465 466 469 public void removeExceptionHandler( CodeExceptionGen c ) { 470 exception_vec.remove(c); 471 } 472 473 474 477 public void removeExceptionHandlers() { 478 exception_vec.clear(); 479 } 480 481 482 485 public CodeExceptionGen[] getExceptionHandlers() { 486 CodeExceptionGen[] cg = new CodeExceptionGen[exception_vec.size()]; 487 exception_vec.toArray(cg); 488 return cg; 489 } 490 491 492 495 private CodeException[] getCodeExceptions() { 496 int size = exception_vec.size(); 497 CodeException[] c_exc = new CodeException[size]; 498 try { 499 for (int i = 0; i < size; i++) { 500 CodeExceptionGen c = (CodeExceptionGen) exception_vec.get(i); 501 c_exc[i] = c.getCodeException(cp); 502 } 503 } catch (ArrayIndexOutOfBoundsException e) { 504 } 505 return c_exc; 506 } 507 508 509 514 public void addException( String class_name ) { 515 throws_vec.add(class_name); 516 } 517 518 519 522 public void removeException( String c ) { 523 throws_vec.remove(c); 524 } 525 526 527 530 public void removeExceptions() { 531 throws_vec.clear(); 532 } 533 534 535 538 public String [] getExceptions() { 539 String [] e = new String [throws_vec.size()]; 540 throws_vec.toArray(e); 541 return e; 542 } 543 544 545 548 private ExceptionTable getExceptionTable( ConstantPoolGen cp ) { 549 int size = throws_vec.size(); 550 int[] ex = new int[size]; 551 try { 552 for (int i = 0; i < size; i++) { 553 ex[i] = cp.addClass((String ) throws_vec.get(i)); 554 } 555 } catch (ArrayIndexOutOfBoundsException e) { 556 } 557 return new ExceptionTable(cp.addUtf8("Exceptions"), 2 + 2 * size, ex, cp.getConstantPool()); 558 } 559 560 561 570 public void addCodeAttribute( Attribute a ) { 571 code_attrs_vec.add(a); 572 } 573 574 575 578 public void removeCodeAttribute( Attribute a ) { 579 code_attrs_vec.remove(a); 580 } 581 582 583 586 public void removeCodeAttributes() { 587 code_attrs_vec.clear(); 588 } 589 590 591 594 public Attribute[] getCodeAttributes() { 595 Attribute[] attributes = new Attribute[code_attrs_vec.size()]; 596 code_attrs_vec.toArray(attributes); 597 return attributes; 598 } 599 600 601 607 public Method getMethod() { 608 String signature = getSignature(); 609 int name_index = cp.addUtf8(name); 610 int signature_index = cp.addUtf8(signature); 611 613 byte[] byte_code = null; 614 if (il != null) { 615 byte_code = il.getByteCode(); 616 } 617 LineNumberTable lnt = null; 618 LocalVariableTable lvt = null; 619 621 if ((variable_vec.size() > 0) && !strip_attributes) { 622 addCodeAttribute(lvt = getLocalVariableTable(cp)); 623 } 624 if ((line_number_vec.size() > 0) && !strip_attributes) { 625 addCodeAttribute(lnt = getLineNumberTable(cp)); 626 } 627 Attribute[] code_attrs = getCodeAttributes(); 628 630 int attrs_len = 0; 631 for (int i = 0; i < code_attrs.length; i++) { 632 attrs_len += (code_attrs[i].getLength() + 6); 633 } 634 CodeException[] c_exc = getCodeExceptions(); 635 int exc_len = c_exc.length * 8; Code code = null; 637 if ((il != null) && !isAbstract() && !isNative()) { 638 Attribute[] attributes = getAttributes(); 640 for (int i = 0; i < attributes.length; i++) { 641 Attribute a = attributes[i]; 642 if (a instanceof Code) { 643 removeAttribute(a); 644 } 645 } 646 code = new Code(cp.addUtf8("Code"), 8 + byte_code.length + 2 + exc_len + 2 + attrs_len, max_stack, max_locals, byte_code, c_exc, code_attrs, cp.getConstantPool()); 650 addAttribute(code); 651 } 652 ExceptionTable et = null; 653 if (throws_vec.size() > 0) { 654 addAttribute(et = getExceptionTable(cp)); 655 } 657 Method m = new Method(access_flags, name_index, signature_index, getAttributes(), cp 658 .getConstantPool()); 659 if (lvt != null) { 661 removeCodeAttribute(lvt); 662 } 663 if (lnt != null) { 664 removeCodeAttribute(lnt); 665 } 666 if (code != null) { 667 removeAttribute(code); 668 } 669 if (et != null) { 670 removeAttribute(et); 671 } 672 return m; 673 } 674 675 676 681 public void removeNOPs() { 682 if (il != null) { 683 InstructionHandle next; 684 686 for (InstructionHandle ih = il.getStart(); ih != null; ih = next) { 687 next = ih.next; 688 if ((next != null) && (ih.getInstruction() instanceof NOP)) { 689 try { 690 il.delete(ih); 691 } catch (TargetLostException e) { 692 InstructionHandle[] targets = e.getTargets(); 693 for (int i = 0; i < targets.length; i++) { 694 InstructionTargeter[] targeters = targets[i].getTargeters(); 695 for (int j = 0; j < targeters.length; j++) { 696 targeters[j].updateTarget(targets[i], next); 697 } 698 } 699 } 700 } 701 } 702 } 703 } 704 705 706 709 public void setMaxLocals( int m ) { 710 max_locals = m; 711 } 712 713 714 public int getMaxLocals() { 715 return max_locals; 716 } 717 718 719 722 public void setMaxStack( int m ) { 723 max_stack = m; 724 } 725 726 727 public int getMaxStack() { 728 return max_stack; 729 } 730 731 732 734 public String getClassName() { 735 return class_name; 736 } 737 738 739 public void setClassName( String class_name ) { 740 this.class_name = class_name; 741 } 742 743 744 public void setReturnType( Type return_type ) { 745 setType(return_type); 746 } 747 748 749 public Type getReturnType() { 750 return getType(); 751 } 752 753 754 public void setArgumentTypes( Type[] arg_types ) { 755 this.arg_types = arg_types; 756 } 757 758 759 public Type[] getArgumentTypes() { 760 return (Type[]) arg_types.clone(); 761 } 762 763 764 public void setArgumentType( int i, Type type ) { 765 arg_types[i] = type; 766 } 767 768 769 public Type getArgumentType( int i ) { 770 return arg_types[i]; 771 } 772 773 774 public void setArgumentNames( String [] arg_names ) { 775 this.arg_names = arg_names; 776 } 777 778 779 public String [] getArgumentNames() { 780 return (String []) arg_names.clone(); 781 } 782 783 784 public void setArgumentName( int i, String name ) { 785 arg_names[i] = name; 786 } 787 788 789 public String getArgumentName( int i ) { 790 return arg_names[i]; 791 } 792 793 794 public InstructionList getInstructionList() { 795 return il; 796 } 797 798 799 public void setInstructionList( InstructionList il ) { 800 this.il = il; 801 } 802 803 804 public String getSignature() { 805 return Type.getMethodSignature(type, arg_types); 806 } 807 808 809 812 public void setMaxStack() { 813 if (il != null) { 814 max_stack = getMaxStack(cp, il, getExceptionHandlers()); 815 } else { 816 max_stack = 0; 817 } 818 } 819 820 821 824 public void setMaxLocals() { 825 if (il != null) { 826 int max = isStatic() ? 0 : 1; 827 if (arg_types != null) { 828 for (int i = 0; i < arg_types.length; i++) { 829 max += arg_types[i].getSize(); 830 } 831 } 832 for (InstructionHandle ih = il.getStart(); ih != null; ih = ih.getNext()) { 833 Instruction ins = ih.getInstruction(); 834 if ((ins instanceof LocalVariableInstruction) || (ins instanceof RET) 835 || (ins instanceof IINC)) { 836 int index = ((IndexedInstruction) ins).getIndex() 837 + ((TypedInstruction) ins).getType(cp).getSize(); 838 if (index > max) { 839 max = index; 840 } 841 } 842 } 843 max_locals = max; 844 } else { 845 max_locals = 0; 846 } 847 } 848 849 850 853 public void stripAttributes( boolean flag ) { 854 strip_attributes = flag; 855 } 856 857 static final class BranchTarget { 858 859 InstructionHandle target; 860 int stackDepth; 861 862 863 BranchTarget(InstructionHandle target, int stackDepth) { 864 this.target = target; 865 this.stackDepth = stackDepth; 866 } 867 } 868 869 static final class BranchStack { 870 871 Stack branchTargets = new Stack (); 872 Hashtable visitedTargets = new Hashtable (); 873 874 875 public void push( InstructionHandle target, int stackDepth ) { 876 if (visited(target)) { 877 return; 878 } 879 branchTargets.push(visit(target, stackDepth)); 880 } 881 882 883 public BranchTarget pop() { 884 if (!branchTargets.empty()) { 885 BranchTarget bt = (BranchTarget) branchTargets.pop(); 886 return bt; 887 } 888 return null; 889 } 890 891 892 private final BranchTarget visit( InstructionHandle target, int stackDepth ) { 893 BranchTarget bt = new BranchTarget(target, stackDepth); 894 visitedTargets.put(target, bt); 895 return bt; 896 } 897 898 899 private final boolean visited( InstructionHandle target ) { 900 return (visitedTargets.get(target) != null); 901 } 902 } 903 904 905 910 public static int getMaxStack( ConstantPoolGen cp, InstructionList il, CodeExceptionGen[] et ) { 911 BranchStack branchTargets = new BranchStack(); 912 917 for (int i = 0; i < et.length; i++) { 918 InstructionHandle handler_pc = et[i].getHandlerPC(); 919 if (handler_pc != null) { 920 branchTargets.push(handler_pc, 1); 921 } 922 } 923 int stackDepth = 0, maxStackDepth = 0; 924 InstructionHandle ih = il.getStart(); 925 while (ih != null) { 926 Instruction instruction = ih.getInstruction(); 927 short opcode = instruction.getOpcode(); 928 int delta = instruction.produceStack(cp) - instruction.consumeStack(cp); 929 stackDepth += delta; 930 if (stackDepth > maxStackDepth) { 931 maxStackDepth = stackDepth; 932 } 933 if (instruction instanceof BranchInstruction) { 935 BranchInstruction branch = (BranchInstruction) instruction; 936 if (instruction instanceof Select) { 937 Select select = (Select) branch; 939 InstructionHandle[] targets = select.getTargets(); 940 for (int i = 0; i < targets.length; i++) { 941 branchTargets.push(targets[i], stackDepth); 942 } 943 ih = null; 945 } else if (!(branch instanceof IfInstruction)) { 946 if (opcode == Constants.JSR || opcode == Constants.JSR_W) { 949 branchTargets.push(ih.getNext(), stackDepth - 1); 950 } 951 ih = null; 952 } 953 branchTargets.push(branch.getTarget(), stackDepth); 957 } else { 958 if (opcode == Constants.ATHROW || opcode == Constants.RET 960 || (opcode >= Constants.IRETURN && opcode <= Constants.RETURN)) { 961 ih = null; 962 } 963 } 964 if (ih != null) { 966 ih = ih.getNext(); 967 } 968 if (ih == null) { 970 BranchTarget bt = branchTargets.pop(); 971 if (bt != null) { 972 ih = bt.target; 973 stackDepth = bt.stackDepth; 974 } 975 } 976 } 977 return maxStackDepth; 978 } 979 980 private List observers; 981 982 983 985 public void addObserver( MethodObserver o ) { 986 if (observers == null) { 987 observers = new ArrayList (); 988 } 989 observers.add(o); 990 } 991 992 993 995 public void removeObserver( MethodObserver o ) { 996 if (observers != null) { 997 observers.remove(o); 998 } 999 } 1000 1001 1002 1006 public void update() { 1007 if (observers != null) { 1008 for (Iterator e = observers.iterator(); e.hasNext();) { 1009 ((MethodObserver) e.next()).notify(this); 1010 } 1011 } 1012 } 1013 1014 1015 1021 public final String toString() { 1022 String access = Utility.accessToString(access_flags); 1023 String signature = Type.getMethodSignature(type, arg_types); 1024 signature = Utility.methodSignatureToString(signature, name, access, true, 1025 getLocalVariableTable(cp)); 1026 StringBuffer buf = new StringBuffer (signature); 1027 if (throws_vec.size() > 0) { 1028 for (Iterator e = throws_vec.iterator(); e.hasNext();) { 1029 buf.append("\n\t\tthrows ").append(e.next()); 1030 } 1031 } 1032 return buf.toString(); 1033 } 1034 1035 1036 1038 public MethodGen copy( String class_name, ConstantPoolGen cp ) { 1039 Method m = ((MethodGen) clone()).getMethod(); 1040 MethodGen mg = new MethodGen(m, class_name, this.cp); 1041 if (this.cp != cp) { 1042 mg.setConstantPool(cp); 1043 mg.getInstructionList().replaceConstantPool(this.cp, cp); 1044 } 1045 return mg; 1046 } 1047 1048 1049 1052 public static BCELComparator getComparator() { 1053 return _cmp; 1054 } 1055 1056 1057 1060 public static void setComparator( BCELComparator comparator ) { 1061 _cmp = comparator; 1062 } 1063 1064 1065 1072 public boolean equals( Object obj ) { 1073 return _cmp.equals(this, obj); 1074 } 1075 1076 1077 1083 public int hashCode() { 1084 return _cmp.hashCode(this); 1085 } 1086} 1087 | Popular Tags |