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