1 19 package org.netbeans.modules.java.source.save; 20 21 import com.sun.source.tree.Tree; 22 import java.util.prefs.Preferences ; 23 import javax.swing.text.BadLocationException ; 24 import org.netbeans.api.java.lexer.JavaTokenId; 25 import org.netbeans.api.java.source.WorkingCopy; 26 import org.netbeans.api.lexer.TokenSequence; 27 import org.netbeans.modules.java.source.engine.EngineEnvironment; 28 import org.netbeans.modules.java.source.engine.ExternalModificationException; 29 import org.netbeans.modules.java.source.engine.ReadOnlyFilesException; 30 import org.netbeans.modules.java.source.engine.SourceReader; 31 import org.netbeans.modules.java.source.engine.SourceRewriter; 32 import org.netbeans.modules.java.source.save.SourceBuffer; 33 import org.netbeans.api.java.source.query.QueryEnvironment; 34 import org.netbeans.api.java.source.query.Query; 35 import org.netbeans.modules.java.source.engine.JavaFormatOptions; 36 import org.netbeans.modules.java.source.pretty.VeryPretty; 37 import org.netbeans.modules.java.source.pretty.ImportAnalysis; 38 import org.netbeans.modules.java.source.engine.PropertySheetInfo; 39 40 import com.sun.source.tree.CompilationUnitTree; 41 42 import com.sun.tools.javac.code.*; 43 import com.sun.tools.javac.tree.JCTree; 44 import com.sun.tools.javac.tree.JCTree.*; 45 import com.sun.tools.javac.tree.TreeInfo; 46 import com.sun.tools.javac.util.*; 47 48 import java.io.*; 49 import java.util.ArrayList ; 50 import java.util.Set ; 51 import java.util.logging.*; 52 import org.netbeans.modules.java.source.engine.ASTModel; 53 import org.netbeans.modules.java.source.engine.RootTree; 54 55 import static org.netbeans.modules.java.source.save.TreeDiff.ListType.*; 56 import static org.netbeans.modules.java.source.save.TreeDiff.DiffTypes.*; 57 import static org.netbeans.modules.java.source.save.TreeDiff.LineInsertionType.*; 58 59 63 public class Commit extends Query<Void ,Object > { 64 boolean displayedError; 65 Context context; 66 ASTModel model; 67 Symtab symtab; 68 Types types; 69 int lastMargin; 70 private EngineEnvironment ee; 71 private boolean fixImports; 72 private boolean forceReformat; 73 private static final int POSITION_OFFSET = 1; 74 private ArrayList <String > readOnlyFiles = new ArrayList <String >(); 75 76 private static final Logger logger = Logger.getLogger("org.netbeans.modules.java.source"); 77 private WorkingCopy workingCopy; 78 private TokenSequence<JavaTokenId> tokenSequence; 79 80 public Commit() { 81 displayedError = false; 82 Preferences prefs = 83 Preferences.userRoot().node(getClass().getName().replace('.','/')); 84 fixImports = prefs.getBoolean("fixImports", false); 85 forceReformat = prefs.getBoolean("forceReformat", false); 86 } 87 88 public Commit(WorkingCopy workingCopy) { 89 this(); 90 this.workingCopy = workingCopy; 91 this.tokenSequence = workingCopy.getTokenHierarchy().tokenSequence(); 92 } 93 94 public boolean isEnabled(QueryEnvironment env) { 95 return env.getUndoList().getOld(env.getRootNode()) != null; 96 } 97 98 @Override 99 public void attach(QueryEnvironment env) { 100 super.attach(env); 101 ee = (EngineEnvironment)env; 102 context = ee.getContext(); 103 symtab = ee.getSymbolTable(); 104 types = Types.instance(context); 105 model = ((EngineEnvironment)env).getModel(); 106 } 107 108 public void apply() { 109 throw new AssertionError ("use commit() instead"); 110 } 111 112 public void commit() throws IOException { 113 if (getOriginalRoot() != getRootNode()) { 114 PropertySheetInfo.find(Commit.class).loadValues(this); 115 Options options = Options.instance(context); 116 apply(getRootNode()); 117 if (readOnlyFiles.size() > 0) 118 throwReadOnlyFilesException(); 119 } 120 } 121 122 private void throwReadOnlyFilesException() throws IOException { 123 StringBuffer sb = new StringBuffer (); 124 for (String s : readOnlyFiles) { 125 sb.append(s); 126 sb.append('\n'); 127 } 128 throw new ReadOnlyFilesException(sb.toString()); 129 } 130 131 private RootTree getOriginalRoot() { 132 RootTree root = (RootTree)getRootNode(); 133 for (;;) { 134 RootTree oldRoot = (RootTree)env.getUndoList().getOld(root); 135 if (oldRoot != null) 136 root = oldRoot; 137 else 138 break; 139 } 140 return root; 141 } 142 143 @Override 144 public Void visitCompilationUnit(CompilationUnitTree node, Object p) { 145 JCCompilationUnit topLevel = (JCCompilationUnit)node; 146 if (!isModified(topLevel)) 147 return null; 148 149 SourceRewriter out = null; 150 String srcFile = topLevel.sourcefile.toString(); 151 boolean doReformat = forceReformat; 152 doReformat = false; boolean shouldSave = true; 154 try { 155 out = ((EngineEnvironment)env).getSourceRewriter(node.getSourceFile()); 156 } catch (FileNotFoundException e) { 157 logger.warning("couldn\'t open original file: " + srcFile); 158 doReformat = true; 159 } catch (ExternalModificationException e) { 160 note("source file modified, reformatting: " + srcFile); 161 doReformat = true; 162 } catch (IOException e) { 163 logger.warning("couldn\'t write to original file: " + srcFile); 164 readOnlyFiles.add(srcFile); 165 return null; 166 } 167 logger.fine("original file: " + srcFile); 168 169 try { 170 if (doReformat) 171 reformat(topLevel, out); 172 else 173 commit(topLevel, out); 174 logger.info("Saved " + srcFile); 175 } 176 catch (IOException e) { 177 logError(e, topLevel); 178 displayedError = true; 179 shouldSave = false; 180 } 181 catch (Throwable err) { 182 error(err); 183 } 184 finally { 185 if (out != null) 186 try { 187 out.close(shouldSave); 188 } catch (IOException e) { 189 if (!displayedError) 190 logError(e, topLevel); 191 } 192 } 193 return null; 194 } 195 196 private void logError(IOException e, JCCompilationUnit topLevel) { 197 Throwable err = (Throwable )(e.getCause() == null ? ((Throwable )(e)) 198 : e.getCause()); 199 err.printStackTrace(); 200 String msg = "Error writing " + getSourceFileName(topLevel) + ": " + err; 201 try { 202 logger.severe(msg); 203 msg = null; 204 } 205 catch (Throwable t) { 206 } 207 if (msg != null) 208 error(msg); 209 } 210 211 214 public void reformat(JCCompilationUnit topLevel, SourceRewriter out) throws IOException, BadLocationException { 215 PrettyPrinter pretty = new PrettyPrinter(context); 216 ImportAnalysis imports = new ImportAnalysis(pretty.options.starThreshold, 217 pretty.options.useThreshold, 218 topLevel, symtab, types); 219 imports.decideImports(); 220 pretty.setImports(imports); 221 if (imports.containingClass != null) 222 pretty.enclClassName = imports.containingClass.name; 223 pretty.printUnit(topLevel); 224 out.writeTo(pretty.toString()); 225 } 226 227 230 public void commit(CompilationUnitTree unit, SourceRewriter out) throws IOException, BadLocationException { 231 JCCompilationUnit topLevel = (JCCompilationUnit)unit; 232 SourceReader in = null; 233 try { 234 String srcFile = topLevel.sourcefile.toString(); 235 SourceBuffer srcBuffer = new SourceBuffer(topLevel.sourcefile); 236 in = new SourceReader(srcBuffer.getChars()); 237 JCCompilationUnit oldTL = (JCCompilationUnit)getOriginalTree(topLevel); 238 List <TreeDiff.Diff> diffs; 239 if ("jackpot-orig".equals(System.getProperty("generatorType"))) { 240 diffs = TreeDiff.diff(context, workingCopy, oldTL, topLevel); 241 } else { 242 diffs = CasualDiff.diff(context, workingCopy, oldTL, topLevel); 243 } 244 TokenList tokenList = TokenList.scan(context, srcFile, srcBuffer); 245 246 PrettyPrinter pretty = new PrettyPrinter(context); 247 List <JCTree> defs = oldTL.defs; 249 while (diffs.nonEmpty() && 250 (defs.head == null || diffs.head.pos < defs.head.pos || 251 diffs.head.oldTree instanceof JCCompilationUnit)) { 252 TreeDiff.Diff d = diffs.head; 253 if (d.type != INSERT_COMMENT && d.type != MODIFY_COMMENT && 254 d.type != DELETE_COMMENT) 255 break; 256 applyCommentDiff(d, in, out, pretty, oldTL, srcBuffer); 257 diffs = diffs.tail; } 259 for (; diffs.nonEmpty(); diffs = diffs.tail) { 260 pretty.reset(0); 261 TreeDiff.Diff d = (TreeDiff.Diff)diffs.head; 262 if (shouldApplyDiff(d)) 263 switch (d.type) { 264 case INSERT: 265 out.copyTo(in, d.getPos()); 266 insertNewTree(d, in, out, pretty, d.pos, srcBuffer); 267 break; 268 case MODIFY: 269 applyModifyDiff(d, in, out, pretty, oldTL, srcBuffer); 270 break; 271 case DELETE: 272 removeOldTree(d, in, out, oldTL, srcBuffer); 273 break; 274 case NAME: 275 changeName((TreeDiff.NameDiff)d, in, out); 276 break; 277 case FLAGS: 278 changeFlags((TreeDiff.FlagsDiff)d, in, out, tokenList); 279 break; 280 case INSERT_OFFSET: 281 TreeDiff.OffsetDiff od = (TreeDiff.OffsetDiff) d; 282 out.copyTo(in, od.getStartOffset()); 283 if (od.getHead() != null) out.writeTo(od.getHead()); 284 if (d.getNew() != null) { 285 insertNewTree(d, in, out, pretty, d.pos, srcBuffer); 286 } 287 if (od.getTail() != null) out.writeTo(od.getTail()); 288 break; 289 case DELETE_OFFSET: 290 od = (TreeDiff.OffsetDiff) d; 291 out.copyTo(in, od.getStartOffset()); 292 out.skipThrough(in, od.getEndOffset()); 293 break; 294 case INSERT_TOKEN: { 295 out.copyTo(in, d.getPos()); 296 TreeDiff.TokenDiff td = (TreeDiff.TokenDiff) d; 297 if (td.getPreceding() != null) { 298 out.writeTo(td.getPreceding()); 299 } 300 if (PARAMETER == td.getItemType()) { 301 pretty.printExpr(d.getNew(), TreeInfo.noPrec); 302 out.writeTo(pretty.toString()); 303 } else { 304 insertNewTree(d, in, out, pretty, d.pos, srcBuffer); 305 } 306 if (td.getTail() != null) { 307 out.writeTo(td.getTail()); 308 } 309 break; 310 } 311 case DELETE_TOKEN: { 312 int startPos = d.getOld().getStartPosition(); 313 int endPos = getOldEndPos(d.getOld(), oldTL, srcBuffer); 314 TreeDiff.TokenDiff td = (TreeDiff.TokenDiff) d; 315 if (td.getPreceding() != null) { 316 int pos = TokenUtilities.moveBackToToken(tokenSequence, startPos, td.getPreceding()); 317 if (pos > 0) startPos = pos; 318 } 319 if (td.getTail() != null) { 320 int pos = TokenUtilities.moveFwdToToken(tokenSequence, endPos, td.getTail()); 321 if (pos > 0) endPos = pos + tokenSequence.token().length(); 322 } 323 if (in.getPos() < startPos) { 324 out.copyTo(in, startPos); 325 } 326 out.skipThrough(in, endPos); 327 break; 328 } 329 case MODIFY_TOKEN: 330 if (d.getNew() != null && d.getOld() != null) { 332 int oldStartPos = removeOldTree(d, in, out, oldTL, srcBuffer); 333 if (PARAMETER == ((TreeDiff.TokenDiff) d).getItemType()) { 334 pretty.printExpr(d.getNew(), TreeInfo.noPrec); 335 out.writeTo(pretty.toString()); 336 } else { 337 insertNewTree(d, in, out, pretty, oldStartPos, srcBuffer); 338 } 339 } else { 340 TreeDiff.TokenDiff td = (TreeDiff.TokenDiff) d; 341 if (td.getNew() == null && td.getOld() != null) { 342 if (td.getPreceding() != null) { 343 TokenUtilities.movePrevious(tokenSequence, td.getOld().getStartPosition()); 344 } 345 out.copyTo(in, tokenSequence.offset()); 346 if (td.getTail() != null) { 347 TokenUtilities.moveNext(tokenSequence, d.getPos()); 348 tokenSequence.moveNext(); 351 tokenSequence.movePrevious(); 352 354 } 355 out.skipThrough(in, tokenSequence.offset() + tokenSequence.token().length()); 356 } else { 357 out.copyTo(in, d.getPos()); 359 pretty.print(td.getPreceding()); 360 pretty.print(td.getNew()); 361 pretty.print(";\n\n"); 362 out.writeTo(pretty.toString()); 363 } 364 } 365 break; 366 case INSERT_COMMENT: 367 case MODIFY_COMMENT: 368 case DELETE_COMMENT: 369 applyCommentDiff(d, in, out, pretty, oldTL, srcBuffer); 370 break; 371 default: 372 throw new AssertionError ("unknown TreeDiff type: " + d.type); 373 } 374 } 375 out.copyRest(in); 376 } finally { 377 if (in != null) 378 in.close(); 379 } 380 } 381 382 private void applyModifyDiff(TreeDiff.Diff d, SourceReader in, SourceRewriter out, 383 PrettyPrinter pretty, JCCompilationUnit oldTL, 384 SourceBuffer srcBuffer) throws IOException, BadLocationException { 385 if (d.oldTree.getKind() == Tree.Kind.IF && d.newTree.getKind() == Tree.Kind.IF) { 387 JCIf oldT = (JCIf)d.oldTree; 388 JCIf newT = (JCIf)d.newTree; 389 if (oldT.elsepart == null && newT.elsepart != null) { 390 int endPos = getOldEndPos(oldT, oldTL, srcBuffer); 392 out.copyTo(in, endPos); 393 setIndent(pretty, endPos, srcBuffer); 394 if (newT.elsepart.getKind() == Tree.Kind.BLOCK && pretty.options.cuddleElse) 395 pretty.print(' '); 396 else { 397 pretty.blankline(); 398 pretty.toLeftMargin(); 399 } 400 pretty.print("else "); 401 newT.elsepart.accept(pretty); 402 out.writeTo(pretty.toString()); 403 return; 404 } 405 else if (oldT.elsepart != null && newT.elsepart == null) { 406 int endPos = getOldEndPos(oldT.thenpart, oldTL, srcBuffer); 408 out.copyTo(in, endPos); 409 out.skipThrough(in, getOldEndPos(oldT, oldTL, srcBuffer)); 410 return; 411 } 412 } 414 int oldStartPos = removeOldTree(d, in, out, oldTL, srcBuffer); 415 insertNewTree(d, in, out, pretty, oldStartPos, srcBuffer); 416 } 417 418 private void applyCommentDiff(TreeDiff.Diff d, SourceReader in, SourceRewriter out, 419 PrettyPrinter pretty, JCCompilationUnit oldTL, 420 SourceBuffer srcBuffer) throws IOException, BadLocationException { 421 int pos = d.oldComment != null ? d.oldComment.pos() : 422 getOldPos(d.oldTree); 423 int endPos = d.oldComment != null ? d.oldComment.endPos() : 424 getOldEndPos(d.oldTree, oldTL, srcBuffer); 425 out.copyTo(in, pos); 426 if (d.type == INSERT_COMMENT) 427 out.writeTo(d.newComment.getText()); 428 else if (d.type == MODIFY_COMMENT) { 429 out.skipThrough(in, endPos); 430 out.writeTo(d.newComment.getText()); 431 } 432 else if (d.type == DELETE_COMMENT) 433 out.skipThrough(in, endPos); 434 if (d.newLine == BEFORE) { 435 setIndent(pretty, pos, srcBuffer); 436 pretty.blankline(); 437 pretty.toLeftMargin(); 438 out.writeTo(pretty.toString()); 439 } 440 } 441 442 private boolean shouldApplyDiff(TreeDiff.Diff d) { 443 JCTree t = d.getOld(); 444 if (t == null) 445 t = d.getNew(); 446 return !fixImports || d.type == FLAGS || t.tag != JCTree.IMPORT; 447 } 448 449 private void insertNewTree(TreeDiff.Diff d, SourceReader in, SourceRewriter out, 450 PrettyPrinter pretty, int oldStartPos, SourceBuffer srcBuffer) 451 throws IOException, BadLocationException { 452 456 boolean printBody = d.type != NAME && d.type != FLAGS; 457 pretty.setPrintBody(printBody); 458 setIndent(pretty, oldStartPos, srcBuffer); 459 if (d.newLine == BEFORE) { 460 pretty.blankline(); 461 pretty.toLeftMargin(); 462 } 463 Name oldOwning = pretty.enclClassName; 464 465 try { 466 if (d.owningClassName != null) 467 pretty.enclClassName = d.owningClassName; 468 469 d.getNew().accept(pretty); 470 } finally { 471 pretty.enclClassName = oldOwning; 472 } 473 474 if (d.newLine == AFTER) { 475 pretty.newline(); 476 pretty.toLeftMargin(); 477 } 478 out.writeTo(pretty.toString()); 479 } 480 481 private int removeOldTree(TreeDiff.Diff d, SourceReader in, SourceRewriter out, 482 JCCompilationUnit oldTL, SourceBuffer srcBuffer) 483 throws IOException, BadLocationException { 484 JCTree oldT = d.getOld(); 485 int oldStartPos = getOldPos(oldT); 486 out.copyTo(in, oldStartPos); 487 out.skipThrough(in, getOldEndPos(oldT, oldTL, srcBuffer)); 488 return oldStartPos; 489 } 490 491 private static void changeName(TreeDiff.NameDiff d, SourceReader in, SourceRewriter out) 492 throws IOException, BadLocationException { 493 out.copyTo(in, d.getPos()); 494 out.skipThrough(in, d.getPos() + d.getOldName().length()); 495 out.writeTo(d.getNewName()); 496 } 497 498 private void changeFlags(TreeDiff.FlagsDiff d, SourceReader in, SourceRewriter out, 499 TokenList tokenList) throws IOException, BadLocationException { 500 int startPos = d.getPos(); 501 out.copyTo(in, startPos); 502 503 boolean hasOldFlags = (d.getOldEndPos() - startPos) > 0; 505 if (hasOldFlags) { 506 int first = tokenList.indexOf(startPos); 507 int last = tokenList.indexOfEndPos(d.getOldEndPos()); 508 assert first != -1 && last != -1; 509 boolean hadAnnotation = false; 510 for (int i = first; i <= last; i++) { 511 TokenList.Token tok = tokenList.get(i); 513 if (tok.isFlag()) { 514 if (hadAnnotation) { 515 StringBuilder sb = new StringBuilder (); 516 while (true) { 517 int c = in.read(); 518 if (c != -1 && Character.isWhitespace(c)) 519 sb.append((char)c); 520 else 521 break; 522 } 523 if (sb.length() > 0) 524 out.writeTo(sb.toString()); 525 } 526 out.skipThrough(in, tok.getEndPos()); 527 } 528 else 529 out.copyTo(in, tok.getEndPos()); 530 hadAnnotation = tok.isIdentifier(); 531 } 532 out.skipThrough(in, tokenList.get(last + 1).getPos()); 533 } 534 535 String s = TreeInfo.flagNames(d.getNewFlags()); 537 if (s.length() > 0) { 538 out.writeTo(s); 539 out.writeTo(" "); 540 } 541 } 542 543 static String getSourceFileName(JCCompilationUnit topLevel) { 544 JCClassDecl mainClass = getMainClassDef(topLevel); 545 return mainClass.sym.fullname.toString().replace('.', File.separatorChar) 546 + ".java"; 547 } 548 549 553 static JCClassDecl getMainClassDef(JCCompilationUnit topLevel) { 554 List <JCTree> defs = topLevel.defs; 555 JCClassDecl topClass = null; 556 while(defs.nonEmpty()) { 557 if (defs.head.tag == JCTree.CLASSDEF) { 558 JCClassDecl tree = (JCClassDecl)defs.head; 559 if ((tree.mods.flags & Flags.PUBLIC) != 0) 560 return tree; 561 topClass = tree; 562 } 563 defs = defs.tail; 564 } 565 return topClass; 566 } 567 568 int getOldEndPos(JCTree oldT, JCCompilationUnit oldTL, SourceBuffer srcbuf) { 569 if (oldT == null || oldTL == null) 570 return Query.NOPOS; 571 int pos = model.getEndPos(oldT, oldTL); 572 assert pos != Query.NOPOS; 573 if (oldT.tag == JCTree.VARDEF && srcbuf.src[pos-1] == ';') 574 pos--; 576 if (false) { String kind = ((com.sun.source.tree.Tree)oldT).getKind().toString(); 578 if (pos == oldT.pos && oldT.tag != JCTree.SKIP) 579 logger.info("no endPos for " + kind + " \"" + oldT.toString() + "\""); 580 else 581 logger.finer("endPos for " + kind + ": " + pos); 582 } 583 return pos; 584 } 585 586 591 void setIndent(PrettyPrinter pretty, int oldStartPos, SourceBuffer srcBuffer) { 592 if (oldStartPos != Query.NOPOS) { 593 int lineNo = srcBuffer.getLineNumber(oldStartPos); 594 char[] line = srcBuffer.getLine(lineNo); 595 lastMargin = getMargin(line); 596 } 597 pretty.reset(lastMargin); 598 } 599 600 CompilationUnitTree getOriginalTree(final CompilationUnitTree t) { 601 RootTree oldRoot = getOriginalRoot(); 602 for (CompilationUnitTree unit : oldRoot.getCompilationUnits()) 603 if (unit.getSourceFile() == t.getSourceFile()) 604 return unit; 605 return t; 606 } 607 608 private int getOldPos(JCTree oldT) { 609 return TreeDiff.getOldPos(oldT, model, env.getUndoList()); 610 } 611 612 private static boolean isDefaultConstructor(JCMethodDecl tree) { 613 return tree.sym.isConstructor() && tree.params.isEmpty(); 614 } 615 616 protected void printSortedImports(boolean fixImports, 618 PrettyPrinter pretty, 619 Set <Symbol> missingImports, 620 SourceRewriter out) throws IOException, BadLocationException { 621 if (fixImports) 622 pretty.printImports(false); else 624 pretty.printImports(missingImports); 625 StringWriter sw = new StringWriter(); 626 pretty.writeTo(sw); 627 String unsortedImports = sw.toString(); 628 BufferedReader br = 629 new BufferedReader(new StringReader(unsortedImports)); 630 java.util.TreeSet <String > sorter = new java.util.TreeSet <String >(); 631 String line; 632 while ((line = br.readLine()) != null && line.length() > 0) 633 sorter.add(line); 634 java.util.Iterator <String > iter = sorter.iterator(); 635 while (iter.hasNext()) 636 out.writeTo(iter.next() + '\n'); 637 } 638 639 private int getMargin(char[] line) { 640 int margin = 0; 641 int i = 0; 642 while (i < line.length) { 643 char c = line[i]; 644 if (c != ' ' && c != '\t') 645 break; 646 margin = calculateColumn(margin, c); 647 i++; 648 } 649 return margin; 650 } 651 652 657 private int calculateColumn(int curCol, char ch) { 658 if (ch == '\f' || ch == '\n') 659 return POSITION_OFFSET; 660 else if (ch == '\t') 661 return (((curCol - 1) / 8 * 8) + 8) + POSITION_OFFSET; 662 return curCol + POSITION_OFFSET; 663 } 664 665 public boolean isModified(JCCompilationUnit tree) { 666 return getOriginalTree(tree) != tree; 667 } 668 669 static class PrettyPrinter extends VeryPretty { 670 boolean printBody; 671 672 PrettyPrinter(Context context) { 673 super(context); 674 printBody = true; 675 } 676 677 public void toLeftMargin() { 678 super.toLeftMargin(); 679 } 680 681 void setPrintBody(boolean b) { 682 printBody = b; 683 } 684 685 protected void printClassBody(JCClassDecl tree) { 686 if (printBody) 687 super.printClassBody(tree); 688 } 689 690 protected void printMethodBody(JCMethodDecl tree) { 691 if (printBody) 692 super.printMethodBody(tree); 693 } 694 695 protected void printVarBody(JCVariableDecl tree) { 696 if (printBody) 697 super.printVarBody(tree); 698 } 699 } 700 701 } 702 | Popular Tags |