1 31 32 package org.opencms.workplace.tools.content; 33 34 import org.opencms.file.CmsFile; 35 import org.opencms.file.CmsObject; 36 import org.opencms.file.CmsResource; 37 import org.opencms.file.CmsResourceFilter; 38 import org.opencms.file.types.CmsResourceTypeXmlPage; 39 import org.opencms.i18n.CmsLocaleManager; 40 import org.opencms.i18n.CmsMessages; 41 import org.opencms.jsp.CmsJspActionElement; 42 import org.opencms.main.CmsException; 43 import org.opencms.report.I_CmsReport; 44 import org.opencms.util.CmsStringUtil; 45 import org.opencms.workplace.CmsReport; 46 import org.opencms.workplace.CmsWorkplaceSettings; 47 import org.opencms.xml.page.CmsXmlPage; 48 import org.opencms.xml.page.CmsXmlPageFactory; 49 50 import java.util.ArrayList ; 51 import java.util.Iterator ; 52 import java.util.List ; 53 import java.util.Locale ; 54 55 import javax.servlet.http.HttpServletRequest ; 56 import javax.servlet.http.HttpServletResponse ; 57 import javax.servlet.jsp.JspException ; 58 import javax.servlet.jsp.PageContext ; 59 60 69 public class CmsMergePages extends CmsReport { 70 71 72 public static final String ALL = "ALL"; 73 74 75 public static final int FOLDER1_EXCLUSIVE = 0; 76 77 78 public static final int FOLDER2_EXCLUSIVE = 1; 79 80 81 public static final int FOLDERS_DIFFERENTTYPES = 4; 82 83 84 public static final int FOLDERS_EQUALNAMES = 3; 85 86 87 public static final int FOLDERS_SIBLING = 2; 88 89 90 public static final String DIALOG_TYPE = "mergepages"; 91 92 93 public static final String PARAM_FOLDER1 = "folder1"; 94 95 96 public static final String PARAM_FOLDER2 = "folder2"; 97 98 99 private CmsObject m_cms; 100 101 102 private String m_errorMessage; 103 104 105 private List m_folder1Exclusive; 106 107 108 private List m_folder2Exclusive; 109 110 111 private List m_foldersDifferenttypes; 112 113 114 private List m_foldersEqualnames; 115 116 117 private List m_foldersSibling; 118 119 120 private String m_paramFolder1; 121 122 123 private String m_paramFolder2; 124 125 126 private I_CmsReport m_report; 127 128 133 public CmsMergePages(CmsJspActionElement jsp) { 134 135 super(jsp); 136 m_folder1Exclusive = new ArrayList (); 137 m_folder2Exclusive = new ArrayList (); 138 m_foldersSibling = new ArrayList (); 139 m_foldersEqualnames = new ArrayList (); 140 m_foldersDifferenttypes = new ArrayList (); 141 } 142 143 151 public CmsMergePages(CmsObject cms, PageContext context, HttpServletRequest req, HttpServletResponse res) { 152 153 this(new CmsJspActionElement(context, req, res)); 154 m_cms = cms; 155 } 156 157 162 public void actionMerge(I_CmsReport report) { 163 164 m_report = report; 165 m_report.println(Messages.get().container(Messages.RPT_MERGE_PAGES_BEGIN_0), I_CmsReport.FORMAT_HEADLINE); 166 167 try { 168 collectResources(); 170 mergePages(); 172 cleanup(); 174 } catch (CmsException e) { 175 m_report.println(e); 176 } 177 178 } 179 180 185 public void actionReport() throws JspException { 186 187 getJsp().getRequest().setAttribute(SESSION_WORKPLACE_CLASS, this); 189 switch (getAction()) { 190 case ACTION_REPORT_END: 191 actionCloseDialog(); 192 break; 193 case ACTION_REPORT_UPDATE: 194 setParamAction(REPORT_UPDATE); 195 getJsp().include(FILE_REPORT_OUTPUT); 196 break; 197 case ACTION_REPORT_BEGIN: 198 case ACTION_CONFIRMED: 199 default: 200 CmsMergePagesThread thread = new CmsMergePagesThread(getCms(), this); 201 setParamAction(REPORT_BEGIN); 202 setParamThread(thread.getUUID().toString()); 203 getJsp().include(FILE_REPORT_OUTPUT); 204 break; 205 } 206 } 207 208 211 public CmsObject getCms() { 212 213 if (m_cms == null) { 214 return super.getCms(); 215 } 216 217 return m_cms; 218 } 219 220 225 public String getErrorMessage() { 226 227 if (CmsStringUtil.isEmpty(m_errorMessage)) { 228 return ""; 229 } 230 231 return m_errorMessage; 232 } 233 234 239 public String getParamFolder1() { 240 241 return m_paramFolder1; 242 } 243 244 249 public String getParamFolder2() { 250 251 return m_paramFolder2; 252 } 253 254 259 public void setErrorMessage(String errorMessage) { 260 261 m_errorMessage = errorMessage; 262 } 263 264 269 public void setParamFolder1(String folder1) { 270 271 m_paramFolder1 = folder1; 272 } 273 274 279 public void setParamFolder2(String folder2) { 280 281 m_paramFolder2 = folder2; 282 } 283 284 289 public void validateParameters(CmsObject cms) { 290 291 CmsMessages messages = Messages.get().getBundle(getLocale()); 292 StringBuffer validationErrors = new StringBuffer (); 293 if (CmsStringUtil.isEmpty(getParamFolder1())) { 294 validationErrors.append(messages.key(Messages.GUI_MERGE_PAGES_VALIDATE_FIRST_FOLDER_0)).append("<br>"); 295 } else { 296 try { 297 cms.readResource(getParamFolder1()); 298 } catch (CmsException e) { 299 validationErrors.append( 300 messages.key(Messages.GUI_MERGE_PAGES_VALIDATE_FIRST_FOLDER_1, getParamFolder1())).append("<br>"); 301 } 302 } 303 if (CmsStringUtil.isEmpty(getParamFolder2())) { 304 validationErrors.append(messages.key(Messages.GUI_MERGE_PAGES_VALIDATE_SECOND_FOLDER_0)).append("<br>"); 305 } else { 306 try { 307 cms.readResource(getParamFolder2()); 308 } catch (CmsException e) { 309 validationErrors.append( 310 messages.key(Messages.GUI_MERGE_PAGES_VALIDATE_SECOND_FOLDER_1, getParamFolder2())).append("<br>"); 311 } 312 } 313 if (getParamFolder1().equals(getParamFolder2())) { 314 validationErrors.append(messages.key(Messages.GUI_MERGE_PAGES_VALIDATE_SAME_FOLDER_0)).append("<br>"); 315 } 316 317 setErrorMessage(validationErrors.toString()); 318 } 319 320 323 protected void initWorkplaceRequestValues(CmsWorkplaceSettings settings, HttpServletRequest request) { 324 325 fillParamValues(request); 327 setParamDialogtype(DIALOG_TYPE); 329 if (DIALOG_CONFIRMED.equals(getParamAction())) { 332 setAction(ACTION_CONFIRMED); 333 } else if (DIALOG_OK.equals(getParamAction())) { 334 setAction(ACTION_OK); 335 } else if (DIALOG_CANCEL.equals(getParamAction())) { 336 setAction(ACTION_CANCEL); 337 } else if (REPORT_UPDATE.equals(getParamAction())) { 338 setAction(ACTION_REPORT_UPDATE); 339 } else if (REPORT_BEGIN.equals(getParamAction())) { 340 setAction(ACTION_REPORT_BEGIN); 341 } else if (REPORT_END.equals(getParamAction())) { 342 setAction(ACTION_REPORT_END); 343 } else { 344 setAction(ACTION_DEFAULT); 345 setParamTitle(Messages.get().getBundle(getLocale()).key(Messages.GUI_TITLE_MERGEPAGES_0)); 347 } 348 } 349 350 367 private int analyse(CmsResource res, String sourceMergeFolder, String targetMergefolder, int currentFolder) { 368 369 int retValue = -1; 370 String resourcenameOther = getResourceNameInOtherFolder( 371 m_cms.getSitePath(res), 372 sourceMergeFolder, 373 targetMergefolder); 374 try { 375 CmsResource otherRes = m_cms.readResource(resourcenameOther, CmsResourceFilter.IGNORE_EXPIRATION); 376 if (res.getResourceId().equals(otherRes.getResourceId())) { 379 retValue = FOLDERS_SIBLING; 381 } else { 382 if (res.getTypeId() == otherRes.getTypeId()) { 384 retValue = FOLDERS_EQUALNAMES; 386 } else { 387 retValue = FOLDERS_DIFFERENTTYPES; 389 } 390 } 391 } catch (CmsException e) { 392 if (currentFolder == 1) { 394 retValue = FOLDER1_EXCLUSIVE; 395 } else { 396 retValue = FOLDER2_EXCLUSIVE; 397 } 398 } 399 400 return retValue; 401 } 402 403 406 private void cleanup() { 407 408 m_folder1Exclusive = null; 409 m_folder2Exclusive = null; 410 m_foldersSibling = null; 411 m_foldersEqualnames = null; 412 m_foldersDifferenttypes = null; 413 } 414 415 423 private void collectFolder(String sourceMergeFolder, String targetMergefolder, int currentFolder) 424 throws CmsException { 425 426 CmsResourceFilter filter = CmsResourceFilter.IGNORE_EXPIRATION.addRequireType(CmsResourceTypeXmlPage.getStaticTypeId()); 428 List folderResources = m_cms.readResources(sourceMergeFolder, filter, true); 429 Iterator i = folderResources.iterator(); 430 int size = folderResources.size(); 431 m_report.println(Messages.get().container( 433 Messages.RPT_SCAN_PAGES_IN_FOLDER_BEGIN_2, 434 sourceMergeFolder, 435 new Integer (size)), I_CmsReport.FORMAT_HEADLINE); 436 int count = 1; 437 while (i.hasNext()) { 438 CmsResource res = (CmsResource)i.next(); 439 String resName = m_cms.getSitePath(res); 440 441 m_report.print(org.opencms.report.Messages.get().container( 442 org.opencms.report.Messages.RPT_SUCCESSION_2, 443 String.valueOf(count++), 444 String.valueOf(size)), I_CmsReport.FORMAT_NOTE); 445 m_report.println(Messages.get().container(Messages.RPT_PROCESS_1, resName), I_CmsReport.FORMAT_NOTE); 446 447 int action = analyse(res, sourceMergeFolder, targetMergefolder, currentFolder); 449 switch (action) { 451 case FOLDER1_EXCLUSIVE: 452 m_folder1Exclusive.add(resName); 453 m_report.println( 454 Messages.get().container(Messages.RPT_FOLDER1_EXCLUSIVE_0), 455 I_CmsReport.FORMAT_OK); 456 break; 457 case FOLDER2_EXCLUSIVE: 458 m_folder2Exclusive.add(resName); 459 m_report.println( 460 Messages.get().container(Messages.RPT_FOLDER2_EXCLUSIVE_0), 461 I_CmsReport.FORMAT_OK); 462 break; 463 case FOLDERS_SIBLING: 464 if (!m_foldersSibling.contains(getResourceNameInOtherFolder( 465 resName, 466 sourceMergeFolder, 467 targetMergefolder))) { 468 m_foldersSibling.add(resName); 469 } 470 m_report.println(Messages.get().container(Messages.RPT_FOLDERS_SIBLING_0), I_CmsReport.FORMAT_OK); 471 break; 472 case FOLDERS_EQUALNAMES: 473 if (!m_foldersEqualnames.contains(getResourceNameInOtherFolder( 474 resName, 475 sourceMergeFolder, 476 targetMergefolder))) { 477 m_foldersEqualnames.add(resName); 478 } 479 m_report.println( 480 Messages.get().container(Messages.RPT_FOLDERS_EQUALNAMES_0), 481 I_CmsReport.FORMAT_OK); 482 break; 483 case FOLDERS_DIFFERENTTYPES: 484 if (!m_foldersDifferenttypes.contains(getResourceNameInOtherFolder( 485 resName, 486 sourceMergeFolder, 487 targetMergefolder))) { 488 m_foldersDifferenttypes.add(resName); 489 } 490 m_report.println( 491 Messages.get().container(Messages.RPT_FOLDERS_DIFFERENTTYPES_0), 492 I_CmsReport.FORMAT_OK); 493 break; 494 default: 495 break; 496 } 497 res = null; 498 } 499 folderResources = null; 500 m_report.println( 501 Messages.get().container(Messages.RPT_SCAN_PAGES_IN_FOLDER_END_0), 502 I_CmsReport.FORMAT_HEADLINE); 503 504 } 505 506 521 private void collectResources() throws CmsException { 522 523 String defaultLocale = CmsLocaleManager.getDefaultLocale().toString(); 524 String locale1 = m_cms.readPropertyObject(getParamFolder1(), "locale", true).getValue(defaultLocale); 525 String locale2 = m_cms.readPropertyObject(getParamFolder2(), "locale", true).getValue(defaultLocale); 526 m_report.println( 527 Messages.get().container(Messages.RPT_CREATE_EXTERNAL_LINK_0, getParamFolder1(), locale1), 528 I_CmsReport.FORMAT_NOTE); 529 m_report.println( 530 Messages.get().container(Messages.RPT_CREATE_EXTERNAL_LINK_0, getParamFolder2(), locale2), 531 I_CmsReport.FORMAT_NOTE); 532 533 collectFolder(getParamFolder1(), getParamFolder2(), 1); 535 collectFolder(getParamFolder2(), getParamFolder1(), 2); 537 538 m_report.println(Messages.get().container(Messages.RPT_SCANNING_RESULTS_0), I_CmsReport.FORMAT_HEADLINE); 540 541 m_report.println(Messages.get().container(Messages.RPT_FOLDER1_EXCLUSIVE_0), I_CmsReport.FORMAT_HEADLINE); 542 reportList(m_folder1Exclusive, false); 543 m_report.println(Messages.get().container(Messages.RPT_FOLDER2_EXCLUSIVE_0), I_CmsReport.FORMAT_HEADLINE); 544 reportList(m_folder2Exclusive, false); 545 m_report.println(Messages.get().container(Messages.RPT_FOLDERS_SIBLING_0), I_CmsReport.FORMAT_HEADLINE); 546 reportList(m_foldersSibling, false); 547 m_report.println(Messages.get().container(Messages.RPT_FOLDERS_EQUALNAMES_0), I_CmsReport.FORMAT_HEADLINE); 548 reportList(m_foldersEqualnames, true); 549 m_report.println(Messages.get().container(Messages.RPT_FOLDERS_DIFFERENTTYPES_0), I_CmsReport.FORMAT_HEADLINE); 550 reportList(m_foldersDifferenttypes, false); 551 552 } 553 554 562 private String getResourceNameInOtherFolder(String resName, String sourceMergeFolder, String targetMergefolder) { 563 564 String resourcename = resName.substring(sourceMergeFolder.length()); 566 return targetMergefolder + resourcename; 568 } 569 570 574 private void mergePages() throws CmsException { 575 576 int size = m_foldersEqualnames.size(); 577 if (size > 0) { 578 579 m_report.println( 580 Messages.get().container(Messages.RPT_MERGE_PAGES_BEGIN_1, String.valueOf(size)), 581 I_CmsReport.FORMAT_HEADLINE); 582 String defaultLocale = CmsLocaleManager.getDefaultLocale().toString(); 583 String locale2 = m_cms.readPropertyObject(getParamFolder2(), "locale", true).getValue(defaultLocale); 584 585 m_report.print(Messages.get().container(Messages.RPT_LOCK_FOLDER_0), I_CmsReport.FORMAT_NOTE); 587 m_report.print(org.opencms.report.Messages.get().container( 588 org.opencms.report.Messages.RPT_ARGUMENT_1, 589 getParamFolder1())); 590 m_report.print(org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_DOTS_0)); 591 m_cms.lockResource(getParamFolder1()); 592 m_report.println( 593 org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_OK_0), 594 I_CmsReport.FORMAT_OK); 595 596 m_report.print(Messages.get().container(Messages.RPT_LOCK_FOLDER_0), I_CmsReport.FORMAT_NOTE); 597 m_report.print(org.opencms.report.Messages.get().container( 598 org.opencms.report.Messages.RPT_ARGUMENT_1, 599 getParamFolder2())); 600 m_report.print(org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_DOTS_0)); 601 m_cms.lockResource(getParamFolder2()); 602 m_report.println( 603 org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_OK_0), 604 I_CmsReport.FORMAT_OK); 605 606 int count = 1; 608 Iterator i = m_foldersEqualnames.iterator(); 609 while (i.hasNext()) { 610 String resFolder1Name = (String )i.next(); 611 try { 612 String resFolder2Name = getResourceNameInOtherFolder( 613 resFolder1Name, 614 getParamFolder1(), 615 getParamFolder2()); 616 m_report.print(org.opencms.report.Messages.get().container( 617 org.opencms.report.Messages.RPT_SUCCESSION_2, 618 String.valueOf(count++), 619 String.valueOf(size)), I_CmsReport.FORMAT_NOTE); 620 m_report.print(Messages.get().container(Messages.RPT_PROCESS_0), I_CmsReport.FORMAT_NOTE); 621 m_report.print(org.opencms.report.Messages.get().container( 622 org.opencms.report.Messages.RPT_ARGUMENT_1, 623 resFolder1Name)); 624 m_report.print(Messages.get().container(Messages.RPT_DOUBLE_ARROW_0), I_CmsReport.FORMAT_NOTE); 625 m_report.print(org.opencms.report.Messages.get().container( 626 org.opencms.report.Messages.RPT_ARGUMENT_1, 627 resFolder2Name)); 628 m_report.println(org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_DOTS_0)); 629 630 String locale = m_cms.readPropertyObject(resFolder1Name, "locale", true).getValue(defaultLocale); 632 m_report.print( 633 Messages.get().container(Messages.RPT_READ_CONTENT_2, resFolder1Name, locale), 634 I_CmsReport.FORMAT_NOTE); 635 CmsResource resFolder1 = m_cms.readResource(resFolder1Name, CmsResourceFilter.IGNORE_EXPIRATION); 636 CmsFile fileFolder1 = CmsFile.upgrade(resFolder1, m_cms); 637 CmsXmlPage pageFolder1 = CmsXmlPageFactory.unmarshal(m_cms, fileFolder1); 638 m_report.println( 639 org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_OK_0), 640 I_CmsReport.FORMAT_OK); 641 642 locale = m_cms.readPropertyObject(resFolder2Name, "locale", true).getValue(defaultLocale); 644 m_report.print( 645 Messages.get().container(Messages.RPT_READ_CONTENT_2, resFolder2Name, locale), 646 I_CmsReport.FORMAT_NOTE); 647 CmsResource resFolder2 = m_cms.readResource(resFolder2Name, CmsResourceFilter.IGNORE_EXPIRATION); 648 CmsFile fileFolder2 = CmsFile.upgrade(resFolder2, m_cms); 649 CmsXmlPage pageFolder2 = CmsXmlPageFactory.unmarshal(m_cms, fileFolder2); 650 m_report.println( 651 org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_OK_0), 652 I_CmsReport.FORMAT_OK); 653 654 Locale loc = new Locale (locale2); 656 List textElements2 = pageFolder2.getNames(loc); 657 Iterator j = textElements2.iterator(); 658 while (j.hasNext()) { 659 String textElementName = (String )j.next(); 660 m_report.print( 661 Messages.get().container(Messages.RPT_PROCESS_TEXT_ELEM_1, textElementName), 662 I_CmsReport.FORMAT_NOTE); 663 String textElement = pageFolder2.getValue(textElementName, loc).getStringValue(m_cms); 665 if (!pageFolder1.hasValue(textElementName, loc)) { 668 pageFolder1.addValue(textElementName, loc); 669 } 670 pageFolder1.setStringValue(m_cms, textElementName, loc, textElement); 671 m_report.println(org.opencms.report.Messages.get().container( 672 org.opencms.report.Messages.RPT_OK_0), I_CmsReport.FORMAT_OK); 673 } 674 676 m_report.print( 677 Messages.get().container(Messages.RPT_WRITE_CONTENT_1, resFolder1Name), 678 I_CmsReport.FORMAT_NOTE); 679 fileFolder1.setContents(pageFolder1.marshal()); 680 m_cms.writeFile(fileFolder1); 681 m_report.println( 682 org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_OK_0), 683 I_CmsReport.FORMAT_OK); 684 685 m_report.print( 687 Messages.get().container(Messages.RPT_READ_PROPERTIES_1, resFolder2Name), 688 I_CmsReport.FORMAT_NOTE); 689 List properties = m_cms.readPropertyObjects(resFolder2Name, false); 690 m_report.println( 691 org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_OK_0), 692 I_CmsReport.FORMAT_OK); 693 694 m_report.print( 696 Messages.get().container(Messages.RPT_DELETE_PAGE_1, resFolder2Name), 697 I_CmsReport.FORMAT_NOTE); 698 m_cms.deleteResource(resFolder2Name, CmsResource.DELETE_PRESERVE_SIBLINGS); 699 m_report.println( 700 org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_OK_0), 701 I_CmsReport.FORMAT_OK); 702 703 m_report.print( 705 Messages.get().container(Messages.RPT_COPY_2, resFolder1Name, resFolder2Name), 706 I_CmsReport.FORMAT_NOTE); 707 m_cms.copyResource(resFolder1Name, resFolder2Name, CmsResource.COPY_AS_SIBLING); 708 m_report.println( 709 org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_OK_0), 710 I_CmsReport.FORMAT_OK); 711 712 m_report.print( 714 Messages.get().container(Messages.RPT_RESORE_PROPERTIES_1, resFolder2Name), 715 I_CmsReport.FORMAT_NOTE); 716 m_cms.writePropertyObjects(resFolder2Name, properties); 717 m_report.println( 718 org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_OK_0), 719 I_CmsReport.FORMAT_OK); 720 721 resFolder1 = null; 722 resFolder2 = null; 723 fileFolder1 = null; 724 fileFolder2 = null; 725 pageFolder1 = null; 726 pageFolder2 = null; 727 728 } catch (CmsException e) { 729 m_report.println(e); 730 } 731 732 } 733 m_report.print( 735 Messages.get().container(Messages.RPT_UNLOCK_1, getParamFolder1()), 736 I_CmsReport.FORMAT_NOTE); 737 m_cms.unlockResource(getParamFolder1()); 738 m_report.println( 739 org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_OK_0), 740 I_CmsReport.FORMAT_OK); 741 742 m_report.print( 743 Messages.get().container(Messages.RPT_UNLOCK_1, getParamFolder2()), 744 I_CmsReport.FORMAT_NOTE); 745 m_cms.unlockResource(getParamFolder2()); 746 m_report.println( 747 org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_OK_0), 748 I_CmsReport.FORMAT_OK); 749 750 m_report.println(Messages.get().container(Messages.RPT_MERGE_PAGES_END_0), I_CmsReport.FORMAT_HEADLINE); 751 } 752 } 753 754 760 private void reportList(List collected, boolean doReport) { 761 762 int size = collected.size(); 763 m_report.println( 765 Messages.get().container(Messages.RPT_NUM_PAGES_1, new Integer (size)), 766 I_CmsReport.FORMAT_HEADLINE); 767 if (doReport) { 768 int count = 1; 769 770 Iterator i = collected.iterator(); 771 while (i.hasNext()) { 772 String resName = (String )i.next(); 773 m_report.print(org.opencms.report.Messages.get().container( 774 org.opencms.report.Messages.RPT_SUCCESSION_2, 775 String.valueOf(count++), 776 String.valueOf(size)), I_CmsReport.FORMAT_NOTE); 777 m_report.println(Messages.get().container(Messages.RPT_PROCESS_1, resName), I_CmsReport.FORMAT_NOTE); 778 } 779 } 780 m_report.println(Messages.get().container(Messages.RPT_MERGE_PAGES_END_0), I_CmsReport.FORMAT_HEADLINE); 781 } 782 } | Popular Tags |