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 330 String additionalRepo = null; 331 332 try { 333 CmsProperty repoProp = cms.readPropertyObject( 334 referencePath, 335 CmsPropertyDefinition.PROPERTY_CATEGORY_REPOSITORY, 336 true); 337 338 if (repoProp.getValue() != null) { 339 LOG.debug("Found category.base with value " + repoProp.getValue() + " on " + repoProp.getOrigin()); 340 additionalRepo = repoProp.getValue(); 341 additionalRepo = CmsStringUtil.joinPaths("/", additionalRepo, "/"); 342 if (!additionalRepo.endsWith(categoryBase)) { 343 additionalRepo = CmsStringUtil.joinPaths(additionalRepo, categoryBase); 344 } 345 if (cms.existsResource(additionalRepo)) { 346 ret.add(additionalRepo); 347 } else { 348 LOG.warn("Additional category repository " + additionalRepo + " not found."); 349 } 350 } 351 } catch (CmsVfsResourceNotFoundException e) { 352 LOG.info(e.getLocalizedMessage(), e); 353 } catch (CmsException e) { 354 LOG.error(e.getLocalizedMessage(), e); 355 } 356 357 ret.add(CmsCategoryService.CENTRALIZED_REPOSITORY); 358 // the order is important in case of conflicts 359 Collections.reverse(ret); 360 return ret; 361 } 362 363 /** 364 * Returns the category repositories base folder name.<p> 365 * 366 * @param cms the cms context 367 * 368 * @return the category repositories base folder name 369 */ 370 public String getRepositoryBaseFolderName(CmsObject cms) { 371 372 String value = ""; 373 try { 374 value = cms.readPropertyObject( 375 CmsCategoryService.CENTRALIZED_REPOSITORY, 376 CmsPropertyDefinition.PROPERTY_DEFAULT_FILE, 377 false).getValue(); 378 } catch (CmsException e) { 379 if (LOG.isErrorEnabled()) { 380 LOG.error(e.getLocalizedMessage(), e); 381 } 382 } 383 if (CmsStringUtil.isEmptyOrWhitespaceOnly(value)) { 384 value = OpenCms.getWorkplaceManager().getCategoryFolder(); 385 } 386 if (CmsStringUtil.isEmptyOrWhitespaceOnly(value)) { 387 value = REPOSITORY_BASE_FOLDER; 388 } 389 if (!value.endsWith("/")) { 390 value += "/"; 391 } 392 if (!value.startsWith("/")) { 393 value = "/" + value; 394 } 395 return value; 396 } 397 398 /** 399 * Localizes a list of categories by reading locale-specific properties for their title and description, if possible.<p> 400 * 401 * This method does not modify its input list of categories, or the categories in it. 402 * 403 * @param cms the CMS context to use for reading resources 404 * @param categories the list of categories 405 * @param locale the locale to use 406 * 407 * @return the list of localized categories 408 */ 409 public List<CmsCategory> localizeCategories(CmsObject cms, List<CmsCategory> categories, Locale locale) { 410 411 List<CmsCategory> result = Lists.newArrayList(); 412 for (CmsCategory category : categories) { 413 result.add(localizeCategory(cms, category, locale)); 414 } 415 return result; 416 } 417 418 /** 419 * Localizes a single category by reading its locale-specific properties for title and description, if possible.<p> 420 * 421 * @param cms the CMS context to use for reading resources 422 * @param category the category to localize 423 * @param locale the locale to use 424 * 425 * @return the localized category 426 */ 427 public CmsCategory localizeCategory(CmsObject cms, CmsCategory category, Locale locale) { 428 429 try { 430 CmsUUID id = category.getId(); 431 CmsResource categoryRes = cms.readResource(id, CmsResourceFilter.IGNORE_EXPIRATION); 432 String title = cms.readPropertyObject( 433 categoryRes, 434 CmsPropertyDefinition.PROPERTY_TITLE, 435 false, 436 locale).getValue(); 437 String description = cms.readPropertyObject( 438 categoryRes, 439 CmsPropertyDefinition.PROPERTY_DESCRIPTION, 440 false, 441 locale).getValue(); 442 return new CmsCategory(category, title, description); 443 } catch (Exception e) { 444 LOG.error("Could not read localized category: " + e.getLocalizedMessage(), e); 445 return category; 446 } 447 } 448 449 /** 450 * Renames/Moves a category from the old path to the new one.<p> 451 * 452 * This method will keep all categories in their original repository.<p> 453 * 454 * @param cms the current cms context 455 * @param oldCatPath the path of the category to move 456 * @param newCatPath the new category path 457 * @param referencePath the reference path to find the category 458 * 459 * @throws CmsException if something goes wrong 460 */ 461 public void moveCategory(CmsObject cms, String oldCatPath, String newCatPath, String referencePath) 462 throws CmsException { 463 464 CmsCategory category = readCategory(cms, oldCatPath, referencePath); 465 String catPath = cms.getRequestContext().removeSiteRoot(category.getRootPath()); 466 CmsLock lock = cms.getLock(catPath); 467 if (lock.isNullLock()) { 468 cms.lockResource(catPath); 469 } else if (lock.isLockableBy(cms.getRequestContext().getCurrentUser())) { 470 cms.changeLock(catPath); 471 } 472 cms.moveResource( 473 catPath, 474 cms.getRequestContext().removeSiteRoot(internalCategoryRootPath(category.getBasePath(), newCatPath))); 475 } 476 477 /** 478 * Returns all categories given some search parameters.<p> 479 * 480 * @param cms the current cms context 481 * @param parentCategoryPath the path of the parent category to get the categories for 482 * @param includeSubCats if to include all categories, or first level child categories only 483 * @param referencePath the reference path to find all the category repositories 484 * 485 * @return a list of {@link CmsCategory} objects 486 * 487 * @throws CmsException if something goes wrong 488 */ 489 public List<CmsCategory> readCategories( 490 CmsObject cms, 491 String parentCategoryPath, 492 boolean includeSubCats, 493 String referencePath) 494 throws CmsException { 495 496 List<String> repositories = getCategoryRepositories(cms, referencePath); 497 return readCategoriesForRepositories(cms, parentCategoryPath, includeSubCats, repositories, false); 498 } 499 500 /** 501 * Returns all categories given some search parameters.<p> 502 * 503 * @param cms the current cms context 504 * @param parentCategoryPath the path of the parent category to get the categories for 505 * @param includeSubCats if to include all categories, or first level child categories only 506 * @param repositories a list of root paths 507 * @return a list of {@link CmsCategory} objects 508 * @throws CmsException if something goes wrong 509 */ 510 public List<CmsCategory> readCategoriesForRepositories( 511 CmsObject cms, 512 String parentCategoryPath, 513 boolean includeSubCats, 514 List<String> repositories) 515 throws CmsException { 516 517 return readCategoriesForRepositories(cms, parentCategoryPath, includeSubCats, repositories, false); 518 } 519 520 /** 521 * Returns all categories given some search parameters.<p> 522 * 523 * @param cms the current cms context 524 * @param parentCategoryPath the path of the parent category to get the categories for 525 * @param includeSubCats if to include all categories, or first level child categories only 526 * @param repositories a list of site paths 527 * @param includeRepositories flag, indicating if the repositories itself should be returned as category. 528 * @return a list of {@link CmsCategory} objects 529 * @throws CmsException if something goes wrong 530 */ 531 public List<CmsCategory> readCategoriesForRepositories( 532 CmsObject cms, 533 String parentCategoryPath, 534 boolean includeSubCats, 535 List<String> repositories, 536 boolean includeRepositories) 537 throws CmsException { 538 539 String catPath = parentCategoryPath; 540 if (catPath == null) { 541 catPath = ""; 542 } 543 544 Collection<CmsCategory> cats = includeRepositories ? new ArrayList<CmsCategory>() : new HashSet<CmsCategory>(); 545 546 // traverse in reverse order, to ensure the set will contain most global categories 547 Iterator<String> it = repositories.iterator(); 548 while (it.hasNext()) { 549 String repository = it.next(); 550 try { 551 if (includeRepositories) { 552 CmsCategory repo = getCategory(cms, cms.readResource(repository)); 553 cats.add(repo); 554 } 555 cats.addAll( 556 internalReadSubCategories(cms, internalCategoryRootPath(repository, catPath), includeSubCats)); 557 } catch (CmsVfsResourceNotFoundException e) { 558 // it may be that the given category is not defined in this repository 559 // just ignore 560 } 561 } 562 List<CmsCategory> ret = new ArrayList<CmsCategory>(cats); 563 if (!includeRepositories) { 564 Collections.sort(ret); 565 } 566 return ret; 567 } 568 569 /** 570 * Reads all categories identified by the given category path for the given reference path.<p> 571 * 572 * @param cms the current cms context 573 * @param categoryPath the path of the category to read 574 * @param referencePath the reference path to find all the category repositories 575 * 576 * @return a list of matching categories, could also be empty, if no category exists with the given path 577 * 578 * @throws CmsException if something goes wrong 579 */ 580 public CmsCategory readCategory(CmsObject cms, String categoryPath, String referencePath) throws CmsException { 581 582 // iterate all possible category repositories, starting with the most global one 583 Iterator<String> it = getCategoryRepositories(cms, referencePath).iterator(); 584 while (it.hasNext()) { 585 String repository = it.next(); 586 try { 587 return getCategory(cms, internalCategoryRootPath(repository, categoryPath)); 588 } catch (CmsVfsResourceNotFoundException e) { 589 // throw the exception if no repository left 590 if (!it.hasNext()) { 591 throw e; 592 } 593 } 594 } 595 // this will never be executed 596 return null; 597 } 598 599 /** 600 * Reads the resources for a category identified by the given category path.<p> 601 * 602 * @param cms the current cms context 603 * @param categoryPath the path of the category to read the resources for 604 * @param recursive <code>true</code> if including sub-categories 605 * @param referencePath the reference path to find all the category repositories 606 * 607 * @return a list of {@link CmsResource} objects 608 * 609 * @throws CmsException if something goes wrong 610 */ 611 public List<CmsResource> readCategoryResources( 612 CmsObject cms, 613 String categoryPath, 614 boolean recursive, 615 String referencePath) 616 throws CmsException { 617 618 return readCategoryResources(cms, categoryPath, recursive, referencePath, CmsResourceFilter.DEFAULT); 619 } 620 621 /** 622 * Reads the resources for a category identified by the given category path.<p> 623 * 624 * @param cms the current cms context 625 * @param categoryPath the path of the category to read the resources for 626 * @param recursive <code>true</code> if including sub-categories 627 * @param referencePath the reference path to find all the category repositories 628 * @param resFilter the resource filter to use 629 * 630 * @return a list of {@link CmsResource} objects 631 * 632 * @throws CmsException if something goes wrong 633 */ 634 public List<CmsResource> readCategoryResources( 635 CmsObject cms, 636 String categoryPath, 637 boolean recursive, 638 String referencePath, 639 CmsResourceFilter resFilter) 640 throws CmsException { 641 642 Set<CmsResource> resources = new HashSet<CmsResource>(); 643 CmsRelationFilter filter = CmsRelationFilter.SOURCES.filterType(CmsRelationType.CATEGORY); 644 if (recursive) { 645 filter = filter.filterIncludeChildren(); 646 } 647 CmsCategory category = readCategory(cms, categoryPath, referencePath); 648 Iterator<CmsRelation> itRelations = cms.getRelationsForResource( 649 cms.getRequestContext().removeSiteRoot(category.getRootPath()), 650 filter).iterator(); 651 while (itRelations.hasNext()) { 652 CmsRelation relation = itRelations.next(); 653 try { 654 resources.add(relation.getSource(cms, resFilter)); 655 } catch (CmsException e) { 656 // source does not match the filter 657 if (LOG.isDebugEnabled()) { 658 LOG.debug(e.getLocalizedMessage(), e); 659 } 660 } 661 } 662 List<CmsResource> result = new ArrayList<CmsResource>(resources); 663 Collections.sort(result); 664 return result; 665 } 666 667 /** 668 * Reads the categories for a resource.<p> 669 * 670 * @param cms the current cms context 671 * @param resource the resource to get the categories for 672 * 673 * @return the categories list 674 * 675 * @throws CmsException if something goes wrong 676 */ 677 public List<CmsCategory> readResourceCategories(CmsObject cms, CmsResource resource) throws CmsException { 678 679 return internalReadResourceCategories(cms, resource, false); 680 } 681 682 /** 683 * Reads the categories for a resource identified by the given resource name.<p> 684 * 685 * @param cms the current cms context 686 * @param resourceName the path of the resource to get the categories for 687 * 688 * @return the categories list 689 * 690 * @throws CmsException if something goes wrong 691 */ 692 public List<CmsCategory> readResourceCategories(CmsObject cms, String resourceName) throws CmsException { 693 694 return internalReadResourceCategories(cms, cms.readResource(resourceName), false); 695 } 696 697 /** 698 * Removes a resource identified by the given resource name from the given category.<p> 699 * 700 * The resource has to be previously locked.<p> 701 * 702 * @param cms the current cms context 703 * @param resourceName the site relative path to the resource to remove 704 * @param category the category to remove the resource from 705 * 706 * @throws CmsException if something goes wrong 707 */ 708 public void removeResourceFromCategory(CmsObject cms, String resourceName, CmsCategory category) 709 throws CmsException { 710 711 // remove the resource just from this category 712 CmsRelationFilter filter = CmsRelationFilter.TARGETS; 713 filter = filter.filterType(CmsRelationType.CATEGORY); 714 filter = filter.filterResource( 715 cms.readResource(cms.getRequestContext().removeSiteRoot(category.getRootPath()))); 716 filter = filter.filterIncludeChildren(); 717 cms.deleteRelationsFromResource(resourceName, filter); 718 } 719 720 /** 721 * Removes a resource identified by the given resource name from the category 722 * identified by the given category path.<p> 723 * 724 * The resource has to be previously locked.<p> 725 * 726 * @param cms the current cms context 727 * @param resourceName the site relative path to the resource to remove 728 * @param categoryPath the path of the category to remove the resource from 729 * 730 * @throws CmsException if something goes wrong 731 */ 732 public void removeResourceFromCategory(CmsObject cms, String resourceName, String categoryPath) 733 throws CmsException { 734 735 CmsCategory category = readCategory(cms, categoryPath, resourceName); 736 removeResourceFromCategory(cms, resourceName, category); 737 } 738 739 /** 740 * Repairs broken category relations.<p> 741 * 742 * This could be caused by renaming/moving a category folder, 743 * or changing the category repositories base folder name.<p> 744 * 745 * Also repairs problems when creating/deleting conflicting 746 * category folders across several repositories.<p> 747 * 748 * The resource has to be previously locked.<p> 749 * 750 * @param cms the cms context 751 * @param resource the resource to repair 752 * 753 * @throws CmsException if something goes wrong 754 */ 755 public void repairRelations(CmsObject cms, CmsResource resource) throws CmsException { 756 757 internalReadResourceCategories(cms, resource, true); 758 } 759 760 /** 761 * Repairs broken category relations.<p> 762 * 763 * This could be caused by renaming/moving a category folder, 764 * or changing the category repositories base folder name.<p> 765 * 766 * Also repairs problems when creating/deleting conflicting 767 * category folders across several repositories.<p> 768 * 769 * The resource has to be previously locked.<p> 770 * 771 * @param cms the cms context 772 * @param resourceName the site relative path to the resource to repair 773 * 774 * @throws CmsException if something goes wrong 775 */ 776 public void repairRelations(CmsObject cms, String resourceName) throws CmsException { 777 778 repairRelations(cms, cms.readResource(resourceName)); 779 } 780 781 /** 782 * Composes the category root path by appending the category path to the given category repository path.<p> 783 * 784 * @param basePath the category repository path 785 * @param categoryPath the category path 786 * 787 * @return the category root path 788 */ 789 private String internalCategoryRootPath(String basePath, String categoryPath) { 790 791 if (categoryPath.startsWith("/") && basePath.endsWith("/")) { 792 // one slash too much 793 return basePath + categoryPath.substring(1); 794 } else if (!categoryPath.startsWith("/") && !basePath.endsWith("/")) { 795 // one slash too less 796 return basePath + "/" + categoryPath; 797 } else { 798 return basePath + categoryPath; 799 } 800 } 801 802 /** 803 * Reads/Repairs the categories for a resource identified by the given resource name.<p> 804 * 805 * For reparation, the resource has to be previously locked.<p> 806 * 807 * @param cms the current cms context 808 * @param resource the resource to get the categories for 809 * @param repair if to repair broken relations 810 * 811 * @return the categories list 812 * 813 * @throws CmsException if something goes wrong 814 */ 815 private List<CmsCategory> internalReadResourceCategories(CmsObject cms, CmsResource resource, boolean repair) 816 throws CmsException { 817 818 List<CmsCategory> result = new ArrayList<CmsCategory>(); 819 String baseFolder = null; 820 Iterator<CmsRelation> itRelations = cms.getRelationsForResource( 821 resource, 822 CmsRelationFilter.TARGETS.filterType(CmsRelationType.CATEGORY)).iterator(); 823 if (repair && itRelations.hasNext()) { 824 baseFolder = getRepositoryBaseFolderName(cms); 825 } 826 String resourceName = cms.getSitePath(resource); 827 boolean repaired = false; 828 while (itRelations.hasNext()) { 829 CmsRelation relation = itRelations.next(); 830 try { 831 CmsResource res = relation.getTarget(cms, CmsResourceFilter.DEFAULT_FOLDERS); 832 CmsCategory category = getCategory(cms, res); 833 if (!repair) { 834 result.add(category); 835 } else { 836 CmsCategory actualCat = readCategory(cms, category.getPath(), resourceName); 837 if (!category.getId().equals(actualCat.getId())) { 838 // repair broken categories caused by creation/deletion of 839 // category folders across several repositories 840 CmsRelationFilter filter = CmsRelationFilter.TARGETS.filterType( 841 CmsRelationType.CATEGORY).filterResource(res); 842 cms.deleteRelationsFromResource(resourceName, filter); 843 repaired = true; 844 // set the right category 845 String catPath = cms.getRequestContext().removeSiteRoot(actualCat.getRootPath()); 846 cms.addRelationToResource(resourceName, catPath, CmsRelationType.CATEGORY.getName()); 847 } 848 result.add(actualCat); 849 } 850 } catch (CmsException e) { 851 if (!repair) { 852 if (LOG.isWarnEnabled()) { 853 LOG.warn(e.getLocalizedMessage(), e); 854 } 855 } else { 856 // repair broken categories caused by moving category folders 857 // could also happen when deleting an assigned category folder 858 if (LOG.isDebugEnabled()) { 859 LOG.debug(e.getLocalizedMessage(), e); 860 } 861 CmsRelationFilter filter = CmsRelationFilter.TARGETS.filterType( 862 CmsRelationType.CATEGORY).filterPath(relation.getTargetPath()); 863 if (!relation.getTargetId().isNullUUID()) { 864 filter = filter.filterStructureId(relation.getTargetId()); 865 } 866 cms.deleteRelationsFromResource(resourceName, filter); 867 repaired = true; 868 // try to set the right category again 869 try { 870 CmsCategory actualCat = readCategory( 871 cms, 872 CmsCategory.getCategoryPath(relation.getTargetPath(), baseFolder), 873 resourceName); 874 addResourceToCategory(cms, resourceName, actualCat); 875 result.add(actualCat); 876 } catch (CmsException ex) { 877 if (LOG.isDebugEnabled()) { 878 LOG.debug(e.getLocalizedMessage(), ex); 879 } 880 } 881 } 882 } 883 } 884 if (!repair) { 885 Collections.sort(result); 886 } else if (repaired) { 887 // be sure that no higher level category is missing 888 Iterator<CmsCategory> it = result.iterator(); 889 while (it.hasNext()) { 890 CmsCategory category = it.next(); 891 addResourceToCategory(cms, resourceName, category.getPath()); 892 } 893 } 894 return result; 895 } 896 897 /** 898 * Returns all sub categories of the given one, including sub sub categories if needed.<p> 899 * 900 * @param cms the current cms context 901 * @param rootPath the base category's root path (this category is not part of the result) 902 * @param includeSubCats flag to indicate if sub categories should also be read 903 * 904 * @return a list of {@link CmsCategory} objects 905 * 906 * @throws CmsException if something goes wrong 907 */ 908 private List<CmsCategory> internalReadSubCategories(CmsObject cms, String rootPath, boolean includeSubCats) 909 throws CmsException { 910 911 List<CmsCategory> categories = new ArrayList<CmsCategory>(); 912 List<CmsResource> resources = cms.readResources( 913 cms.getRequestContext().removeSiteRoot(rootPath), 914 CmsResourceFilter.DEFAULT.addRequireType(CmsResourceTypeFolder.RESOURCE_TYPE_ID), 915 includeSubCats); 916 Iterator<CmsResource> it = resources.iterator(); 917 while (it.hasNext()) { 918 CmsResource resource = it.next(); 919 categories.add(getCategory(cms, resource)); 920 } 921 return categories; 922 } 923}