001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software GmbH & Co. KG, please see the 018 * company website: http://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: http://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.relations; 029 030import org.opencms.file.CmsObject; 031import org.opencms.file.CmsProperty; 032import org.opencms.file.CmsPropertyDefinition; 033import org.opencms.file.CmsResource; 034import org.opencms.file.CmsResourceFilter; 035import org.opencms.file.CmsVfsResourceNotFoundException; 036import org.opencms.file.types.CmsResourceTypeFolder; 037import org.opencms.lock.CmsLock; 038import org.opencms.main.CmsException; 039import org.opencms.main.CmsLog; 040import org.opencms.main.OpenCms; 041import org.opencms.util.CmsStringUtil; 042import org.opencms.util.CmsUUID; 043 044import java.util.ArrayList; 045import java.util.Collection; 046import java.util.Collections; 047import java.util.HashSet; 048import java.util.Iterator; 049import java.util.List; 050import java.util.Locale; 051import java.util.Set; 052 053import org.apache.commons.logging.Log; 054 055import com.google.common.collect.Lists; 056 057/** 058 * Provides several simplified methods for manipulating category relations.<p> 059 * 060 * @since 6.9.2 061 * 062 * @see CmsCategory 063 */ 064public class CmsCategoryService { 065 066 /** The centralized path for categories. */ 067 public static final String CENTRALIZED_REPOSITORY = "/system/categories/"; 068 069 /** The folder for the local category repositories. */ 070 public static final String REPOSITORY_BASE_FOLDER = "/.categories/"; 071 072 /** The log object for this class. */ 073 private static final Log LOG = CmsLog.getLog(CmsCategoryService.class); 074 075 /** The singleton instance. */ 076 private static CmsCategoryService m_instance; 077 078 /** 079 * Returns the singleton instance.<p> 080 * 081 * @return the singleton instance 082 */ 083 public static CmsCategoryService getInstance() { 084 085 if (m_instance == null) { 086 m_instance = new CmsCategoryService(); 087 } 088 return m_instance; 089 } 090 091 /** 092 * Adds a resource identified by the given resource name to the given category.<p> 093 * 094 * The resource has to be locked.<p> 095 * 096 * @param cms the current cms context 097 * @param resourceName the site relative path to the resource to add 098 * @param category the category to add the resource to 099 * 100 * @throws CmsException if something goes wrong 101 */ 102 public void addResourceToCategory(CmsObject cms, String resourceName, CmsCategory category) throws CmsException { 103 104 if (readResourceCategories(cms, cms.readResource(resourceName, CmsResourceFilter.IGNORE_EXPIRATION)).contains( 105 category)) { 106 return; 107 } 108 String sitePath = cms.getRequestContext().removeSiteRoot(category.getRootPath()); 109 cms.addRelationToResource(resourceName, sitePath, CmsRelationType.CATEGORY.getName()); 110 111 String parentCatPath = category.getPath(); 112 // recursively add to higher level categories 113 if (parentCatPath.endsWith("/")) { 114 parentCatPath = parentCatPath.substring(0, parentCatPath.length() - 1); 115 } 116 if (parentCatPath.lastIndexOf('/') > 0) { 117 addResourceToCategory(cms, resourceName, parentCatPath.substring(0, parentCatPath.lastIndexOf('/') + 1)); 118 } 119 } 120 121 /** 122 * Adds a resource identified by the given resource name to the category 123 * identified by the given category path.<p> 124 * 125 * Only the most global category matching the given category path for the 126 * given resource will be affected.<p> 127 * 128 * The resource has to be locked.<p> 129 * 130 * @param cms the current cms context 131 * @param resourceName the site relative path to the resource to add 132 * @param categoryPath the path of the category to add the resource to 133 * 134 * @throws CmsException if something goes wrong 135 */ 136 public void addResourceToCategory(CmsObject cms, String resourceName, String categoryPath) throws CmsException { 137 138 CmsCategory category = readCategory(cms, categoryPath, resourceName); 139 addResourceToCategory(cms, resourceName, category); 140 } 141 142 /** 143 * Removes the given resource from all categories.<p> 144 * 145 * @param cms the cms context 146 * @param resourcePath the resource to reset the categories for 147 * 148 * @throws CmsException if something goes wrong 149 */ 150 public void clearCategoriesForResource(CmsObject cms, String resourcePath) throws CmsException { 151 152 CmsRelationFilter filter = CmsRelationFilter.TARGETS; 153 filter = filter.filterType(CmsRelationType.CATEGORY); 154 cms.deleteRelationsFromResource(resourcePath, filter); 155 } 156 157 /** 158 * Adds all categories from one resource to another, skipping categories that are not available for the resource copied to. 159 * 160 * The resource where categories are copied to has to be locked. 161 * 162 * @param cms the CmsObject used for reading and writing. 163 * @param fromResource the resource to copy the categories from. 164 * @param toResourceSitePath the full site path of the resource to copy the categories to. 165 * @throws CmsException thrown if copying the resources fails. 166 */ 167 public void copyCategories(CmsObject cms, CmsResource fromResource, String toResourceSitePath) throws CmsException { 168 169 List<CmsCategory> categories = readResourceCategories(cms, fromResource); 170 for (CmsCategory category : categories) { 171 addResourceToCategory(cms, toResourceSitePath, category); 172 } 173 } 174 175 /** 176 * Creates a new category.<p> 177 * 178 * Will use the same category repository as the parent if specified, 179 * or the closest category repository to the reference path if specified, 180 * or the centralized category repository in all other cases.<p> 181 * 182 * @param cms the current cms context 183 * @param parent the parent category or <code>null</code> for a new top level category 184 * @param name the name of the new category 185 * @param title the title 186 * @param description the description 187 * @param referencePath the reference path for the category repository 188 * 189 * @return the new created category 190 * 191 * @throws CmsException if something goes wrong 192 */ 193 public CmsCategory createCategory( 194 CmsObject cms, 195 CmsCategory parent, 196 String name, 197 String title, 198 String description, 199 String referencePath) 200 throws CmsException { 201 202 List<CmsProperty> properties = new ArrayList<CmsProperty>(); 203 if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(title)) { 204 properties.add(new CmsProperty(CmsPropertyDefinition.PROPERTY_TITLE, title, null)); 205 } 206 if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(description)) { 207 properties.add(new CmsProperty(CmsPropertyDefinition.PROPERTY_DESCRIPTION, description, null)); 208 } 209 String folderPath = ""; 210 if (parent != null) { 211 folderPath += parent.getRootPath(); 212 } else { 213 if (referencePath == null) { 214 folderPath += CmsCategoryService.CENTRALIZED_REPOSITORY; 215 } else { 216 List<String> repositories = getCategoryRepositories(cms, referencePath); 217 // take the last one 218 folderPath = repositories.get(repositories.size() - 1); 219 } 220 } 221 folderPath = cms.getRequestContext().removeSiteRoot(internalCategoryRootPath(folderPath, name)); 222 CmsResource resource; 223 try { 224 resource = cms.createResource(folderPath, CmsResourceTypeFolder.RESOURCE_TYPE_ID, null, properties); 225 } catch (CmsVfsResourceNotFoundException e) { 226 // may be is the centralized repository missing, try to create it 227 cms.createResource(CmsCategoryService.CENTRALIZED_REPOSITORY, CmsResourceTypeFolder.RESOURCE_TYPE_ID); 228 // now try again 229 resource = cms.createResource(folderPath, CmsResourceTypeFolder.RESOURCE_TYPE_ID, null, properties); 230 } 231 return getCategory(cms, resource); 232 } 233 234 /** 235 * Deletes the category identified by the given path.<p> 236 * 237 * Only the most global category matching the given category path for the 238 * given resource will be affected.<p> 239 * 240 * This method will try to lock the involved resource.<p> 241 * 242 * @param cms the current cms context 243 * @param categoryPath the path of the category to delete 244 * @param referencePath the reference path to find the category repositories 245 * 246 * @throws CmsException if something goes wrong 247 */ 248 public void deleteCategory(CmsObject cms, String categoryPath, String referencePath) throws CmsException { 249 250 CmsCategory category = readCategory(cms, categoryPath, referencePath); 251 String folderPath = cms.getRequestContext().removeSiteRoot(category.getRootPath()); 252 CmsLock lock = cms.getLock(folderPath); 253 if (lock.isNullLock()) { 254 cms.lockResource(folderPath); 255 } else if (lock.isLockableBy(cms.getRequestContext().getCurrentUser())) { 256 cms.changeLock(folderPath); 257 } 258 cms.deleteResource(folderPath, CmsResource.DELETE_PRESERVE_SIBLINGS); 259 } 260 261 /** 262 * Creates a category from the given resource.<p> 263 * 264 * @param cms the cms context 265 * @param resource the resource 266 * 267 * @return a category object 268 * 269 * @throws CmsException if something goes wrong 270 */ 271 public CmsCategory getCategory(CmsObject cms, CmsResource resource) throws CmsException { 272 273 CmsProperty title = cms.readPropertyObject(resource, CmsPropertyDefinition.PROPERTY_TITLE, false); 274 CmsProperty description = cms.readPropertyObject(resource, CmsPropertyDefinition.PROPERTY_DESCRIPTION, false); 275 return new CmsCategory( 276 resource.getStructureId(), 277 resource.getRootPath(), 278 title.getValue(resource.getName()), 279 description.getValue(""), 280 getRepositoryBaseFolderName(cms)); 281 } 282 283 /** 284 * Creates a category from the given category root path.<p> 285 * 286 * @param cms the cms context 287 * @param categoryRootPath the category root path 288 * 289 * @return a category object 290 * 291 * @throws CmsException if something goes wrong 292 */ 293 public CmsCategory getCategory(CmsObject cms, String categoryRootPath) throws CmsException { 294 295 CmsResource resource = cms.readResource(cms.getRequestContext().removeSiteRoot(categoryRootPath)); 296 return getCategory(cms, resource); 297 } 298 299 /** 300 * Returns all category repositories for the given reference path.<p> 301 * 302 * @param cms the cms context 303 * @param referencePath the reference path 304 * 305 * @return a list of root paths 306 */ 307 public List<String> getCategoryRepositories(CmsObject cms, String referencePath) { 308 309 List<String> ret = new ArrayList<String>(); 310 if (referencePath == null) { 311 ret.add(CmsCategoryService.CENTRALIZED_REPOSITORY); 312 return ret; 313 } 314 String path = referencePath; 315 if (!CmsResource.isFolder(path)) { 316 path = CmsResource.getParentFolder(path); 317 } 318 if (CmsStringUtil.isEmptyOrWhitespaceOnly(path)) { 319 path = "/"; 320 } 321 String categoryBase = getRepositoryBaseFolderName(cms); 322 do { 323 String repositoryPath = internalCategoryRootPath(path, categoryBase); 324 if (cms.existsResource(repositoryPath)) { 325 ret.add(repositoryPath); 326 } 327 path = CmsResource.getParentFolder(path); 328 } while (path != null); 329 ret.add(CmsCategoryService.CENTRALIZED_REPOSITORY); 330 // the order is important in case of conflicts 331 Collections.reverse(ret); 332 return ret; 333 } 334 335 /** 336 * Returns the category repositories base folder name.<p> 337 * 338 * @param cms the cms context 339 * 340 * @return the category repositories base folder name 341 */ 342 public String getRepositoryBaseFolderName(CmsObject cms) { 343 344 String value = ""; 345 try { 346 value = cms.readPropertyObject( 347 CmsCategoryService.CENTRALIZED_REPOSITORY, 348 CmsPropertyDefinition.PROPERTY_DEFAULT_FILE, 349 false).getValue(); 350 } catch (CmsException e) { 351 if (LOG.isErrorEnabled()) { 352 LOG.error(e.getLocalizedMessage(), e); 353 } 354 } 355 if (CmsStringUtil.isEmptyOrWhitespaceOnly(value)) { 356 value = OpenCms.getWorkplaceManager().getCategoryFolder(); 357 } 358 if (CmsStringUtil.isEmptyOrWhitespaceOnly(value)) { 359 value = REPOSITORY_BASE_FOLDER; 360 } 361 if (!value.endsWith("/")) { 362 value += "/"; 363 } 364 if (!value.startsWith("/")) { 365 value = "/" + value; 366 } 367 return value; 368 } 369 370 /** 371 * Localizes a list of categories by reading locale-specific properties for their title and description, if possible.<p> 372 * 373 * This method does not modify its input list of categories, or the categories in it. 374 * 375 * @param cms the CMS context to use for reading resources 376 * @param categories the list of categories 377 * @param locale the locale to use 378 * 379 * @return the list of localized categories 380 */ 381 public List<CmsCategory> localizeCategories(CmsObject cms, List<CmsCategory> categories, Locale locale) { 382 383 List<CmsCategory> result = Lists.newArrayList(); 384 for (CmsCategory category : categories) { 385 result.add(localizeCategory(cms, category, locale)); 386 } 387 return result; 388 } 389 390 /** 391 * Localizes a single category by reading its locale-specific properties for title and description, if possible.<p> 392 * 393 * @param cms the CMS context to use for reading resources 394 * @param category the category to localize 395 * @param locale the locale to use 396 * 397 * @return the localized category 398 */ 399 public CmsCategory localizeCategory(CmsObject cms, CmsCategory category, Locale locale) { 400 401 try { 402 CmsUUID id = category.getId(); 403 CmsResource categoryRes = cms.readResource(id, CmsResourceFilter.IGNORE_EXPIRATION); 404 String title = cms.readPropertyObject( 405 categoryRes, 406 CmsPropertyDefinition.PROPERTY_TITLE, 407 false, 408 locale).getValue(); 409 String description = cms.readPropertyObject( 410 categoryRes, 411 CmsPropertyDefinition.PROPERTY_DESCRIPTION, 412 false, 413 locale).getValue(); 414 return new CmsCategory(category, title, description); 415 } catch (Exception e) { 416 LOG.error("Could not read localized category: " + e.getLocalizedMessage(), e); 417 return category; 418 } 419 } 420 421 /** 422 * Renames/Moves a category from the old path to the new one.<p> 423 * 424 * This method will keep all categories in their original repository.<p> 425 * 426 * @param cms the current cms context 427 * @param oldCatPath the path of the category to move 428 * @param newCatPath the new category path 429 * @param referencePath the reference path to find the category 430 * 431 * @throws CmsException if something goes wrong 432 */ 433 public void moveCategory(CmsObject cms, String oldCatPath, String newCatPath, String referencePath) 434 throws CmsException { 435 436 CmsCategory category = readCategory(cms, oldCatPath, referencePath); 437 String catPath = cms.getRequestContext().removeSiteRoot(category.getRootPath()); 438 CmsLock lock = cms.getLock(catPath); 439 if (lock.isNullLock()) { 440 cms.lockResource(catPath); 441 } else if (lock.isLockableBy(cms.getRequestContext().getCurrentUser())) { 442 cms.changeLock(catPath); 443 } 444 cms.moveResource( 445 catPath, 446 cms.getRequestContext().removeSiteRoot(internalCategoryRootPath(category.getBasePath(), newCatPath))); 447 } 448 449 /** 450 * Returns all categories given some search parameters.<p> 451 * 452 * @param cms the current cms context 453 * @param parentCategoryPath the path of the parent category to get the categories for 454 * @param includeSubCats if to include all categories, or first level child categories only 455 * @param referencePath the reference path to find all the category repositories 456 * 457 * @return a list of {@link CmsCategory} objects 458 * 459 * @throws CmsException if something goes wrong 460 */ 461 public List<CmsCategory> readCategories( 462 CmsObject cms, 463 String parentCategoryPath, 464 boolean includeSubCats, 465 String referencePath) 466 throws CmsException { 467 468 List<String> repositories = getCategoryRepositories(cms, referencePath); 469 return readCategoriesForRepositories(cms, parentCategoryPath, includeSubCats, repositories, false); 470 } 471 472 /** 473 * Returns all categories given some search parameters.<p> 474 * 475 * @param cms the current cms context 476 * @param parentCategoryPath the path of the parent category to get the categories for 477 * @param includeSubCats if to include all categories, or first level child categories only 478 * @param repositories a list of root paths 479 * @return a list of {@link CmsCategory} objects 480 * @throws CmsException if something goes wrong 481 */ 482 public List<CmsCategory> readCategoriesForRepositories( 483 CmsObject cms, 484 String parentCategoryPath, 485 boolean includeSubCats, 486 List<String> repositories) 487 throws CmsException { 488 489 return readCategoriesForRepositories(cms, parentCategoryPath, includeSubCats, repositories, false); 490 } 491 492 /** 493 * Returns all categories given some search parameters.<p> 494 * 495 * @param cms the current cms context 496 * @param parentCategoryPath the path of the parent category to get the categories for 497 * @param includeSubCats if to include all categories, or first level child categories only 498 * @param repositories a list of site paths 499 * @param includeRepositories flag, indicating if the repositories itself should be returned as category. 500 * @return a list of {@link CmsCategory} objects 501 * @throws CmsException if something goes wrong 502 */ 503 public List<CmsCategory> readCategoriesForRepositories( 504 CmsObject cms, 505 String parentCategoryPath, 506 boolean includeSubCats, 507 List<String> repositories, 508 boolean includeRepositories) 509 throws CmsException { 510 511 String catPath = parentCategoryPath; 512 if (catPath == null) { 513 catPath = ""; 514 } 515 516 Collection<CmsCategory> cats = includeRepositories ? new ArrayList<CmsCategory>() : new HashSet<CmsCategory>(); 517 518 // traverse in reverse order, to ensure the set will contain most global categories 519 Iterator<String> it = repositories.iterator(); 520 while (it.hasNext()) { 521 String repository = it.next(); 522 try { 523 if (includeRepositories) { 524 CmsCategory repo = getCategory(cms, cms.readResource(repository)); 525 cats.add(repo); 526 } 527 cats.addAll( 528 internalReadSubCategories(cms, internalCategoryRootPath(repository, catPath), includeSubCats)); 529 } catch (CmsVfsResourceNotFoundException e) { 530 // it may be that the given category is not defined in this repository 531 // just ignore 532 } 533 } 534 List<CmsCategory> ret = new ArrayList<CmsCategory>(cats); 535 if (!includeRepositories) { 536 Collections.sort(ret); 537 } 538 return ret; 539 } 540 541 /** 542 * Reads all categories identified by the given category path for the given reference path.<p> 543 * 544 * @param cms the current cms context 545 * @param categoryPath the path of the category to read 546 * @param referencePath the reference path to find all the category repositories 547 * 548 * @return a list of matching categories, could also be empty, if no category exists with the given path 549 * 550 * @throws CmsException if something goes wrong 551 */ 552 public CmsCategory readCategory(CmsObject cms, String categoryPath, String referencePath) throws CmsException { 553 554 // iterate all possible category repositories, starting with the most global one 555 Iterator<String> it = getCategoryRepositories(cms, referencePath).iterator(); 556 while (it.hasNext()) { 557 String repository = it.next(); 558 try { 559 return getCategory(cms, internalCategoryRootPath(repository, categoryPath)); 560 } catch (CmsVfsResourceNotFoundException e) { 561 // throw the exception if no repository left 562 if (!it.hasNext()) { 563 throw e; 564 } 565 } 566 } 567 // this will never be executed 568 return null; 569 } 570 571 /** 572 * Reads the resources for a category identified by the given category path.<p> 573 * 574 * @param cms the current cms context 575 * @param categoryPath the path of the category to read the resources for 576 * @param recursive <code>true</code> if including sub-categories 577 * @param referencePath the reference path to find all the category repositories 578 * 579 * @return a list of {@link CmsResource} objects 580 * 581 * @throws CmsException if something goes wrong 582 */ 583 public List<CmsResource> readCategoryResources( 584 CmsObject cms, 585 String categoryPath, 586 boolean recursive, 587 String referencePath) 588 throws CmsException { 589 590 return readCategoryResources(cms, categoryPath, recursive, referencePath, CmsResourceFilter.DEFAULT); 591 } 592 593 /** 594 * Reads the resources for a category identified by the given category path.<p> 595 * 596 * @param cms the current cms context 597 * @param categoryPath the path of the category to read the resources for 598 * @param recursive <code>true</code> if including sub-categories 599 * @param referencePath the reference path to find all the category repositories 600 * @param resFilter the resource filter to use 601 * 602 * @return a list of {@link CmsResource} objects 603 * 604 * @throws CmsException if something goes wrong 605 */ 606 public List<CmsResource> readCategoryResources( 607 CmsObject cms, 608 String categoryPath, 609 boolean recursive, 610 String referencePath, 611 CmsResourceFilter resFilter) 612 throws CmsException { 613 614 Set<CmsResource> resources = new HashSet<CmsResource>(); 615 CmsRelationFilter filter = CmsRelationFilter.SOURCES.filterType(CmsRelationType.CATEGORY); 616 if (recursive) { 617 filter = filter.filterIncludeChildren(); 618 } 619 CmsCategory category = readCategory(cms, categoryPath, referencePath); 620 Iterator<CmsRelation> itRelations = cms.getRelationsForResource( 621 cms.getRequestContext().removeSiteRoot(category.getRootPath()), 622 filter).iterator(); 623 while (itRelations.hasNext()) { 624 CmsRelation relation = itRelations.next(); 625 try { 626 resources.add(relation.getSource(cms, resFilter)); 627 } catch (CmsException e) { 628 // source does not match the filter 629 if (LOG.isDebugEnabled()) { 630 LOG.debug(e.getLocalizedMessage(), e); 631 } 632 } 633 } 634 List<CmsResource> result = new ArrayList<CmsResource>(resources); 635 Collections.sort(result); 636 return result; 637 } 638 639 /** 640 * Reads the categories for a resource.<p> 641 * 642 * @param cms the current cms context 643 * @param resource the resource to get the categories for 644 * 645 * @return the categories list 646 * 647 * @throws CmsException if something goes wrong 648 */ 649 public List<CmsCategory> readResourceCategories(CmsObject cms, CmsResource resource) throws CmsException { 650 651 return internalReadResourceCategories(cms, resource, false); 652 } 653 654 /** 655 * Reads the categories for a resource identified by the given resource name.<p> 656 * 657 * @param cms the current cms context 658 * @param resourceName the path of the resource to get the categories for 659 * 660 * @return the categories list 661 * 662 * @throws CmsException if something goes wrong 663 */ 664 public List<CmsCategory> readResourceCategories(CmsObject cms, String resourceName) throws CmsException { 665 666 return internalReadResourceCategories(cms, cms.readResource(resourceName), false); 667 } 668 669 /** 670 * Removes a resource identified by the given resource name from the given category.<p> 671 * 672 * The resource has to be previously locked.<p> 673 * 674 * @param cms the current cms context 675 * @param resourceName the site relative path to the resource to remove 676 * @param category the category to remove the resource from 677 * 678 * @throws CmsException if something goes wrong 679 */ 680 public void removeResourceFromCategory(CmsObject cms, String resourceName, CmsCategory category) 681 throws CmsException { 682 683 // remove the resource just from this category 684 CmsRelationFilter filter = CmsRelationFilter.TARGETS; 685 filter = filter.filterType(CmsRelationType.CATEGORY); 686 filter = filter.filterResource( 687 cms.readResource(cms.getRequestContext().removeSiteRoot(category.getRootPath()))); 688 filter = filter.filterIncludeChildren(); 689 cms.deleteRelationsFromResource(resourceName, filter); 690 } 691 692 /** 693 * Removes a resource identified by the given resource name from the category 694 * identified by the given category path.<p> 695 * 696 * The resource has to be previously locked.<p> 697 * 698 * @param cms the current cms context 699 * @param resourceName the site relative path to the resource to remove 700 * @param categoryPath the path of the category to remove the resource from 701 * 702 * @throws CmsException if something goes wrong 703 */ 704 public void removeResourceFromCategory(CmsObject cms, String resourceName, String categoryPath) 705 throws CmsException { 706 707 CmsCategory category = readCategory(cms, categoryPath, resourceName); 708 removeResourceFromCategory(cms, resourceName, category); 709 } 710 711 /** 712 * Repairs broken category relations.<p> 713 * 714 * This could be caused by renaming/moving a category folder, 715 * or changing the category repositories base folder name.<p> 716 * 717 * Also repairs problems when creating/deleting conflicting 718 * category folders across several repositories.<p> 719 * 720 * The resource has to be previously locked.<p> 721 * 722 * @param cms the cms context 723 * @param resource the resource to repair 724 * 725 * @throws CmsException if something goes wrong 726 */ 727 public void repairRelations(CmsObject cms, CmsResource resource) throws CmsException { 728 729 internalReadResourceCategories(cms, resource, true); 730 } 731 732 /** 733 * Repairs broken category relations.<p> 734 * 735 * This could be caused by renaming/moving a category folder, 736 * or changing the category repositories base folder name.<p> 737 * 738 * Also repairs problems when creating/deleting conflicting 739 * category folders across several repositories.<p> 740 * 741 * The resource has to be previously locked.<p> 742 * 743 * @param cms the cms context 744 * @param resourceName the site relative path to the resource to repair 745 * 746 * @throws CmsException if something goes wrong 747 */ 748 public void repairRelations(CmsObject cms, String resourceName) throws CmsException { 749 750 repairRelations(cms, cms.readResource(resourceName)); 751 } 752 753 /** 754 * Composes the category root path by appending the category path to the given category repository path.<p> 755 * 756 * @param basePath the category repository path 757 * @param categoryPath the category path 758 * 759 * @return the category root path 760 */ 761 private String internalCategoryRootPath(String basePath, String categoryPath) { 762 763 if (categoryPath.startsWith("/") && basePath.endsWith("/")) { 764 // one slash too much 765 return basePath + categoryPath.substring(1); 766 } else if (!categoryPath.startsWith("/") && !basePath.endsWith("/")) { 767 // one slash too less 768 return basePath + "/" + categoryPath; 769 } else { 770 return basePath + categoryPath; 771 } 772 } 773 774 /** 775 * Reads/Repairs the categories for a resource identified by the given resource name.<p> 776 * 777 * For reparation, the resource has to be previously locked.<p> 778 * 779 * @param cms the current cms context 780 * @param resource the resource to get the categories for 781 * @param repair if to repair broken relations 782 * 783 * @return the categories list 784 * 785 * @throws CmsException if something goes wrong 786 */ 787 private List<CmsCategory> internalReadResourceCategories(CmsObject cms, CmsResource resource, boolean repair) 788 throws CmsException { 789 790 List<CmsCategory> result = new ArrayList<CmsCategory>(); 791 String baseFolder = null; 792 Iterator<CmsRelation> itRelations = cms.getRelationsForResource( 793 resource, 794 CmsRelationFilter.TARGETS.filterType(CmsRelationType.CATEGORY)).iterator(); 795 if (repair && itRelations.hasNext()) { 796 baseFolder = getRepositoryBaseFolderName(cms); 797 } 798 String resourceName = cms.getSitePath(resource); 799 boolean repaired = false; 800 while (itRelations.hasNext()) { 801 CmsRelation relation = itRelations.next(); 802 try { 803 CmsResource res = relation.getTarget(cms, CmsResourceFilter.DEFAULT_FOLDERS); 804 CmsCategory category = getCategory(cms, res); 805 if (!repair) { 806 result.add(category); 807 } else { 808 CmsCategory actualCat = readCategory(cms, category.getPath(), resourceName); 809 if (!category.getId().equals(actualCat.getId())) { 810 // repair broken categories caused by creation/deletion of 811 // category folders across several repositories 812 CmsRelationFilter filter = CmsRelationFilter.TARGETS.filterType( 813 CmsRelationType.CATEGORY).filterResource(res); 814 cms.deleteRelationsFromResource(resourceName, filter); 815 repaired = true; 816 // set the right category 817 String catPath = cms.getRequestContext().removeSiteRoot(actualCat.getRootPath()); 818 cms.addRelationToResource(resourceName, catPath, CmsRelationType.CATEGORY.getName()); 819 } 820 result.add(actualCat); 821 } 822 } catch (CmsException e) { 823 if (!repair) { 824 if (LOG.isWarnEnabled()) { 825 LOG.warn(e.getLocalizedMessage(), e); 826 } 827 } else { 828 // repair broken categories caused by moving category folders 829 // could also happen when deleting an assigned category folder 830 if (LOG.isDebugEnabled()) { 831 LOG.debug(e.getLocalizedMessage(), e); 832 } 833 CmsRelationFilter filter = CmsRelationFilter.TARGETS.filterType( 834 CmsRelationType.CATEGORY).filterPath(relation.getTargetPath()); 835 if (!relation.getTargetId().isNullUUID()) { 836 filter = filter.filterStructureId(relation.getTargetId()); 837 } 838 cms.deleteRelationsFromResource(resourceName, filter); 839 repaired = true; 840 // try to set the right category again 841 try { 842 CmsCategory actualCat = readCategory( 843 cms, 844 CmsCategory.getCategoryPath(relation.getTargetPath(), baseFolder), 845 resourceName); 846 addResourceToCategory(cms, resourceName, actualCat); 847 result.add(actualCat); 848 } catch (CmsException ex) { 849 if (LOG.isDebugEnabled()) { 850 LOG.debug(e.getLocalizedMessage(), ex); 851 } 852 } 853 } 854 } 855 } 856 if (!repair) { 857 Collections.sort(result); 858 } else if (repaired) { 859 // be sure that no higher level category is missing 860 Iterator<CmsCategory> it = result.iterator(); 861 while (it.hasNext()) { 862 CmsCategory category = it.next(); 863 addResourceToCategory(cms, resourceName, category.getPath()); 864 } 865 } 866 return result; 867 } 868 869 /** 870 * Returns all sub categories of the given one, including sub sub categories if needed.<p> 871 * 872 * @param cms the current cms context 873 * @param rootPath the base category's root path (this category is not part of the result) 874 * @param includeSubCats flag to indicate if sub categories should also be read 875 * 876 * @return a list of {@link CmsCategory} objects 877 * 878 * @throws CmsException if something goes wrong 879 */ 880 private List<CmsCategory> internalReadSubCategories(CmsObject cms, String rootPath, boolean includeSubCats) 881 throws CmsException { 882 883 List<CmsCategory> categories = new ArrayList<CmsCategory>(); 884 List<CmsResource> resources = cms.readResources( 885 cms.getRequestContext().removeSiteRoot(rootPath), 886 CmsResourceFilter.DEFAULT.addRequireType(CmsResourceTypeFolder.RESOURCE_TYPE_ID), 887 includeSubCats); 888 Iterator<CmsResource> it = resources.iterator(); 889 while (it.hasNext()) { 890 CmsResource resource = it.next(); 891 categories.add(getCategory(cms, resource)); 892 } 893 return categories; 894 } 895}