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}