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