001/*
002 *  Copyright 2016 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.plugins.userdirectory;
017
018import java.text.Normalizer;
019import java.util.ArrayList;
020import java.util.HashMap;
021import java.util.HashSet;
022import java.util.LinkedHashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.Objects;
026import java.util.Set;
027import java.util.SortedSet;
028import java.util.TreeSet;
029import java.util.regex.Matcher;
030import java.util.regex.Pattern;
031import java.util.stream.Collectors;
032
033import org.apache.avalon.framework.activity.Initializable;
034import org.apache.avalon.framework.component.Component;
035import org.apache.avalon.framework.service.ServiceException;
036import org.apache.avalon.framework.service.ServiceManager;
037import org.apache.avalon.framework.service.Serviceable;
038import org.apache.commons.lang3.StringUtils;
039
040import org.ametys.cms.contenttype.ContentType;
041import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
042import org.ametys.cms.repository.Content;
043import org.ametys.cms.repository.ContentTypeExpression;
044import org.ametys.cms.repository.LanguageExpression;
045import org.ametys.core.cache.AbstractCacheManager;
046import org.ametys.core.cache.Cache;
047import org.ametys.plugins.core.impl.cache.AbstractCacheKey;
048import org.ametys.plugins.repository.AmetysObjectIterable;
049import org.ametys.plugins.repository.AmetysObjectResolver;
050import org.ametys.plugins.repository.AmetysRepositoryException;
051import org.ametys.plugins.repository.UnknownAmetysObjectException;
052import org.ametys.plugins.repository.provider.WorkspaceSelector;
053import org.ametys.plugins.repository.query.QueryHelper;
054import org.ametys.plugins.repository.query.SortCriteria;
055import org.ametys.plugins.repository.query.expression.AndExpression;
056import org.ametys.plugins.repository.query.expression.Expression;
057import org.ametys.plugins.repository.query.expression.Expression.Operator;
058import org.ametys.plugins.repository.query.expression.OrExpression;
059import org.ametys.plugins.repository.query.expression.StringExpression;
060import org.ametys.plugins.repository.query.expression.VirtualFactoryExpression;
061import org.ametys.plugins.userdirectory.page.VirtualUserDirectoryPageFactory;
062import org.ametys.runtime.i18n.I18nizableText;
063import org.ametys.runtime.plugin.component.AbstractLogEnabled;
064import org.ametys.web.repository.page.Page;
065import org.ametys.web.repository.page.PageQueryHelper;
066
067/**
068 * Component providing methods to retrieve user directory virtual pages, such as the user directory root,
069 * transitional page and user page.
070 */
071public class UserDirectoryPageHandler extends AbstractLogEnabled implements Component, Serviceable, Initializable
072{
073    /** The avalon role. */
074    public static final String ROLE = UserDirectoryPageHandler.class.getName();
075    
076    /** The data name for the content type of the user directory */
077    public static final String CONTENT_TYPE_DATA_NAME = "user-directory-root-contenttype";
078    /** The data name for the users' view to use */
079    public static final String USER_VIEW_NAME = "user-directory-root-view-name";
080    /** The data name for the classification attribute of the user directory */
081    public static final String CLASSIFICATION_ATTRIBUTE_DATA_NAME = "user-directory-root-classification-metadata";
082    /** The data name for the depth of the user directory */
083    public static final String DEPTH_DATA_NAME = "user-directory-root-depth";
084    /** The parent content type id */
085    public static final String ABSTRACT_USER_CONTENT_TYPE = "org.ametys.plugins.userdirectory.Content.user";
086    /** The user directory root pages cache id */
087    protected static final String ROOT_PAGES_CACHE = UserDirectoryPageHandler.class.getName() + "$rootPageIds";
088    /** The user directory transitional pages cache id */
089    protected static final String TRANSITIONAL_PAGES_CACHE = UserDirectoryPageHandler.class.getName() + "$transitionalPageIds";
090    /** The user directory user pages cache id */
091    protected static final String USER_PAGES_CACHE = UserDirectoryPageHandler.class.getName() + "$userPages";
092    
093    /** The workspace selector. */
094    protected WorkspaceSelector _workspaceSelector;
095    /** The ametys object resolver. */
096    protected AmetysObjectResolver _resolver;
097    /** The extension point for content types */
098    protected ContentTypeExtensionPoint _contentTypeEP;
099    /** The cache manager */
100    protected AbstractCacheManager _abstractCacheManager;
101    
102    @Override
103    public void service(ServiceManager manager) throws ServiceException
104    {
105        _workspaceSelector = (WorkspaceSelector) manager.lookup(WorkspaceSelector.ROLE);
106        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
107        _contentTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
108        _abstractCacheManager = (AbstractCacheManager) manager.lookup(AbstractCacheManager.ROLE);
109    }
110    
111    @Override
112    public void initialize() throws Exception
113    {
114        _abstractCacheManager.createMemoryCache(ROOT_PAGES_CACHE, 
115                new I18nizableText("plugin.user-directory", "PLUGINS_USER_DIRECTORY_CACHE_ROOT_PAGES_LABEL"),
116                new I18nizableText("plugin.user-directory", "PLUGINS_USER_DIRECTORY_CACHE_ROOT_PAGES_DESCRIPTION"),
117                true,
118                null);
119        _abstractCacheManager.createMemoryCache(TRANSITIONAL_PAGES_CACHE, 
120                new I18nizableText("plugin.user-directory", "PLUGINS_USER_DIRECTORY_CACHE_TRANSITIONAL_PAGES_LABEL"),
121                new I18nizableText("plugin.user-directory", "PLUGINS_USER_DIRECTORY_CACHE_TRANSITIONAL_PAGES_DESCRIPTION"),
122                true,
123                null);
124        _abstractCacheManager.createMemoryCache(USER_PAGES_CACHE, 
125                new I18nizableText("plugin.user-directory", "PLUGINS_USER_DIRECTORY_CACHE_USER_PAGES_LABEL"),
126                new I18nizableText("plugin.user-directory", "PLUGINS_USER_DIRECTORY_CACHE_USER_PAGES_DESCRIPTION"),
127                true,
128                null);
129    }
130    
131    /**
132     * Gets the user directory root pages from the given content type id, whatever the site.
133     * @param contentTypeId The content type id
134     * @return the user directory root pages.
135     * @throws AmetysRepositoryException  if an error occured.
136     */
137    public Set<Page> getUserDirectoryRootPages(String contentTypeId) throws AmetysRepositoryException
138    {
139        Expression expression = new VirtualFactoryExpression(VirtualUserDirectoryPageFactory.class.getName());
140        Expression contentTypeExp = new StringExpression(CONTENT_TYPE_DATA_NAME, Operator.EQ, contentTypeId);
141        
142        AndExpression andExp = new AndExpression(expression, contentTypeExp);
143        
144        String query = PageQueryHelper.getPageXPathQuery(null, null, null, andExp, null);
145        
146        AmetysObjectIterable<Page> pages = _resolver.query(query);
147        
148        return pages.stream().collect(Collectors.toSet());
149    }
150    
151    /**
152     * Gets the user directory root page of a specific content type.
153     * @param siteName The site name
154     * @param sitemapName The sitemap
155     * @param contentTypeId The content type id
156     * @return the user directory root pages.
157     * @throws AmetysRepositoryException  if an error occured.
158     */
159    public Page getUserDirectoryRootPage(String siteName, String sitemapName, String contentTypeId) throws AmetysRepositoryException
160    {
161        String contentTypeIdToCompare = contentTypeId != null ? contentTypeId : "";
162        
163        for (Page userDirectoryRootPage : getUserDirectoryRootPages(siteName, sitemapName))
164        {
165            if (contentTypeIdToCompare.equals(getContentTypeId(userDirectoryRootPage)))
166            {
167                return userDirectoryRootPage;
168            }
169        }
170        
171        return null;
172    }
173    
174    /**
175     * Gets the user directory root pages.
176     * @param siteName The site name
177     * @param sitemapName The sitemap
178     * @return the user directory root pages.
179     * @throws AmetysRepositoryException  if an error occured.
180     */
181    public Set<Page> getUserDirectoryRootPages(String siteName, String sitemapName) throws AmetysRepositoryException
182    {
183        Set<Page> rootPages = new HashSet<>();
184        
185        String workspace = _workspaceSelector.getWorkspace();
186        
187        Cache<RootPageCacheKey, Set<String>> cache = getRootPagesCache();
188        
189        RootPageCacheKey key = RootPageCacheKey.of(workspace, siteName, sitemapName);
190        if (cache.hasKey(key))
191        {
192            rootPages = cache.get(key).stream()
193                    .map(this::_resolvePage)
194                    .filter(Objects::nonNull)
195                    .collect(Collectors.toSet());
196        }
197        else
198        {
199            rootPages = _getUserDirectoryRootPages(siteName, sitemapName);
200            Set<String> userDirectoryRootPageIds = rootPages.stream()
201                    .map(Page::getId)
202                    .collect(Collectors.toSet());
203            cache.put(key, userDirectoryRootPageIds);
204        }
205        
206        return rootPages;
207    }
208    
209    private Page _resolvePage(String pageId)
210    {
211        try
212        {
213            return _resolver.resolveById(pageId);
214        }
215        catch (UnknownAmetysObjectException e)
216        {
217            // The page stored in cache may have been deleted
218            return null;
219        }
220    }
221    
222    /**
223     * Get the user directory root pages, without searching in the cache.
224     * @param siteName the current site.
225     * @param sitemapName the sitemap name.
226     * @return the user directory root pages
227     * @throws AmetysRepositoryException if an error occured.
228     */
229    protected Set<Page> _getUserDirectoryRootPages(String siteName, String sitemapName) throws AmetysRepositoryException
230    {
231        Expression expression = new VirtualFactoryExpression(VirtualUserDirectoryPageFactory.class.getName());
232        
233        String query = PageQueryHelper.getPageXPathQuery(siteName, sitemapName, null, expression, null);
234        
235        AmetysObjectIterable<Page> pages = _resolver.query(query);
236        
237        return pages.stream().collect(Collectors.toSet());
238    }
239    
240    /**
241     * Gets the depth of the user directory root page
242     * @param rootPage The user directory root page
243     * @return the depth of the user directory root page
244     */
245    public int getDepth(Page rootPage)
246    {
247        return Math.toIntExact(rootPage.getValue(DEPTH_DATA_NAME));
248    }
249    
250    /**
251     * Gets the name of the classification attribute
252     * @param rootPage The user directory root page
253     * @return the name of the classification attribute
254     */
255    public String getClassificationAttribute(Page rootPage)
256    {
257        return rootPage.getValue(CLASSIFICATION_ATTRIBUTE_DATA_NAME);
258    }
259    
260    /**
261     * Gets the content type id
262     * @param rootPage The user directory root page
263     * @return the content type id
264     */
265    public String getContentTypeId(Page rootPage)
266    {
267        return rootPage.getValue(CONTENT_TYPE_DATA_NAME);
268    }
269    
270    /**
271     * Gets the content type
272     * @param rootPage The user directory root page
273     * @return the content type
274     */
275    public ContentType getContentType(Page rootPage)
276    {
277        String contentTypeId = getContentTypeId(rootPage);
278        return StringUtils.isNotBlank(contentTypeId) ? _contentTypeEP.getExtension(contentTypeId) : null;
279    }
280    
281    /**
282     * Gets the value of the classification attribute for the given content, transformed for building tree hierarchy
283     * <br>The transformation takes the lower-case of all characters, removes non-alphanumeric characters,
284     * and takes the first characters to not have a string with a size bigger than the depth
285     * <br>For instance, if the value for the content is "Aéa Foo-bar" and the depth is 7,
286     * then this method will return "aeafoob"
287     * @param rootPage The user directory root page
288     * @param content The content
289     * @return the transformed value of the classification attribute for the given content. Can be null
290     */
291    public String getTransformedClassificationValue(Page rootPage, Content content)
292    {
293        String attribute = getClassificationAttribute(rootPage);
294        int depth = getDepth(rootPage);
295        
296        // 1) get value of the classification attribute
297        String classification = content.getValue(attribute);
298        
299        if (classification == null)
300        {
301            // The classification does not exists for the content
302            getLogger().info("The classification attribute '{}' does not exist for the content {}", attribute, content);
303            return null;
304        }
305        
306        try
307        {
308            // 2) replace special character
309            // 3) remove '-' characters
310            
311            // FIXME CMS-5758 FilterNameHelper.filterName do not authorized name with numbers only.
312            // So code of FilterNamehelper is temporarily duplicated here with a slightly modified RegExp
313//            String transformedValue = FilterNameHelper.filterName(classification).replace("-", "");
314            String transformedValue = _filterName(classification).replace("-", "");
315            
316            // 4) only keep 'depth' first characters (if depth = 3, "de" becomes "de", "debu" becomes "deb", etc.)
317            return StringUtils.substring(transformedValue, 0, depth);
318        }
319        catch (IllegalArgumentException e)
320        {
321            // The value of the classification attribute is not valid
322            getLogger().warn("The classification attribute '{}' does not have a valid value ({}) for the content {}", attribute, classification, content);
323            return null;
324        }
325    }
326    
327    private String _filterName(String name)
328    {
329        Pattern pattern = Pattern.compile("^()[0-9-_]*[a-z0-9].*$");
330        // Use lower case
331        // then remove accents
332        // then replace contiguous spaces with one dash
333        // and finally remove non-alphanumeric characters except -
334        String filteredName = Normalizer.normalize(name.toLowerCase(), Normalizer.Form.NFD).replaceAll("[\\p{InCombiningDiacriticalMarks}]", "").trim(); 
335        filteredName = filteredName.replaceAll("œ", "oe").replaceAll("æ", "ae").replaceAll(" +", "-").replaceAll("[^\\w-]", "-").replaceAll("-+", "-");
336
337        Matcher m = pattern.matcher(filteredName);
338        if (!m.matches())
339        {
340            throw new IllegalArgumentException(filteredName + " doesn't match the expected regular expression : " + pattern.pattern());
341        }
342
343        filteredName = filteredName.substring(m.end(1));
344
345        // Remove characters '-' and '_' at the start and the end of the string
346        return StringUtils.strip(filteredName, "-_");
347    }
348    
349    /**
350     * Get all transitional page child from page name
351     * @param rootPage the root page
352     * @param pagePath the page path
353     * @return all transitional page child from page name
354     */
355    public SortedSet<String> getTransitionalPagesName(Page rootPage, String pagePath)
356    {
357        String cachePagePath = getName(pagePath);
358        
359        String workspace = _workspaceSelector.getWorkspace();
360        String site = rootPage.getSiteName();
361        String contentType = getContentTypeId(rootPage);
362        _initializeCaches(rootPage, workspace, contentType);
363        PageCacheKey key = PageCacheKey.of(workspace, contentType, site, rootPage.getSitemapName(), cachePagePath);
364        Cache<PageCacheKey, SortedSet<String>> transitionalPagesCache = getTransitionalPagesCache();
365        return transitionalPagesCache.get(key, k -> new TreeSet<>());
366    }
367    
368    private void _initializeCaches(Page rootPage, String workspace, String contentType)
369    {
370        String language = rootPage.getSitemapName();
371        String site = rootPage.getSiteName();
372        
373        
374        // Get transitional page cache
375        PageCacheKey transitionalPagesCacheKey = PageCacheKey.of(workspace, contentType, site, language, null);
376        Cache<PageCacheKey, SortedSet<String>> transitionalPagesCache = getTransitionalPagesCache();
377        
378        // Get user page cache
379        PageCacheKey userPagesCacheKey = PageCacheKey.of(workspace, contentType, site, language, null);
380        Cache<PageCacheKey, Map<String, String>> userPagesCache = getUserPagesCache();
381        
382        if (transitionalPagesCache.hasKey(transitionalPagesCacheKey) && userPagesCache.hasKey(userPagesCacheKey))
383        {
384            // Both caches are initialized
385            getLogger().debug("TransitionalPageCache and UserPageCache are initialized for workspace '{}' and content type '{}' and language '{}'", workspace, contentType, language);
386            return;
387        }
388        
389        // Get all contents which will appear in the sitemap
390        AmetysObjectIterable<Content> contents = getContentsForRootPage(rootPage);
391        
392        // Get their classification attribute value
393        Map<Content, String> transformedValuesByContent = new LinkedHashMap<>();
394        for (Content content : contents)
395        {
396            String value = getTransformedClassificationValue(rootPage, content);
397            if (value != null)
398            {
399                transformedValuesByContent.put(content, value);
400            }
401        }
402        
403        if (!transitionalPagesCache.hasKey(transitionalPagesCacheKey))
404        {
405            Set<String> transformedValues = new HashSet<>(transformedValuesByContent.values());
406            _buildTransitionalPageCache(transformedValues, workspace, contentType, site, language);
407            getLogger().info("Transitional page cache was built for workspace '{}' and content type '{}' and language '{}'", workspace, contentType, language);
408        }
409        
410        if (!userPagesCache.hasKey(userPagesCacheKey))
411        {
412            int depth = getDepth(rootPage);
413            _buildUserPageCache(transformedValuesByContent, depth, workspace, contentType, site, language);
414            getLogger().info("User page cache was built for workspace '{}' and content type '{}' and language '{}'", workspace, contentType, language);
415        }
416    }
417    
418    private void _buildTransitionalPageCache(Set<String> transformedValues, String workspace, String contentTypeId, String siteName, String siteMapName)
419    {
420        Cache<PageCacheKey, SortedSet<String>> transitionalPagesCache = getTransitionalPagesCache();
421        for (String value : transformedValues)
422        {
423            char[] charArray = value.toCharArray();
424            for (int i = 0; i < charArray.length; i++)
425            {
426                String lastChar = String.valueOf(charArray[i]);
427                if (i == 0)
428                {
429                    // case _root
430                    SortedSet<String> root = transitionalPagesCache.get(PageCacheKey.of(workspace, contentTypeId, siteName, siteMapName, "_root"), k -> new TreeSet<>());
431                    if (!root.contains(lastChar))
432                    {
433                        root.add(lastChar);
434                    }
435                }
436                else
437                {
438                    String currentPrefixWithoutLastChar = value.substring(0, i); // if value == "debu", equals to "deb"
439                    String currentPathWithoutLastChar = StringUtils.join(currentPrefixWithoutLastChar.toCharArray(), '/'); // if value == "debu", equals to "d/e/b"
440                    SortedSet<String> childPageNames = transitionalPagesCache.get(PageCacheKey.of(workspace, contentTypeId, siteName, siteMapName, currentPathWithoutLastChar), k -> new TreeSet<>());
441                    
442                    if (!childPageNames.contains(lastChar))
443                    {
444                        childPageNames.add(lastChar); // if value == "debu", add "u" in childPageNames for key "d/e/b"
445                    }
446                }
447            }
448        }
449    }
450    
451    private void _buildUserPageCache(Map<Content, String> transformedValuesByContent, int depth, String workspace, String contentTypeId, String siteName, String language)
452    {
453        if (depth == 0)
454        {
455            Map<String, String> rootContents = new LinkedHashMap<>();
456            for (Content content : transformedValuesByContent.keySet())
457            {
458                rootContents.put(content.getName(), content.getId());
459            }
460            getUserPagesCache().put(PageCacheKey.of(workspace, contentTypeId, siteName, language, "_root"), rootContents);
461            return;
462        }
463        
464        for (Content content : transformedValuesByContent.keySet())
465        {
466            String transformedValue = transformedValuesByContent.get(content);
467            for (int i = 0; i < depth; i++)
468            {
469                String currentPrefix = StringUtils.substring(transformedValue, 0, i + 1);
470                String currentPath = StringUtils.join(currentPrefix.toCharArray(), '/');
471                PageCacheKey userPageKey = PageCacheKey.of(workspace, contentTypeId, siteName, language, currentPath);
472                Map<String, String> contentsForPath = getUserPagesCache().get(userPageKey, k -> new LinkedHashMap<>());
473                
474                String contentName = content.getName();
475                if (!contentsForPath.containsKey(contentName))
476                {
477                    contentsForPath.put(contentName, content.getId());
478                }
479            }
480        }
481    }
482    
483    /**
484     * Get all user page child from page name
485     * @param rootPage the root page
486     * @param pagePath the page path
487     * @return all user page child from page name
488     */
489    public Map<String, String> getUserPagesContent(Page rootPage, String pagePath)
490    {
491        String cachePagePath = getName(pagePath);
492        
493        String workspace = _workspaceSelector.getWorkspace();
494        String site = rootPage.getSiteName();
495        String contentType = getContentTypeId(rootPage);
496        _initializeCaches(rootPage, workspace, contentType);
497        
498        return getUserPagesCache().get(PageCacheKey.of(workspace, contentType, site, rootPage.getSitemapName(), cachePagePath), k -> new HashMap<>());
499    }
500    
501    /**
502     * Get the user contents for a given root page
503     * @param rootPage the root page
504     * @return the user contents
505     */
506    public AmetysObjectIterable<Content> getContentsForRootPage(Page rootPage)
507    {
508        String contentType = getContentTypeId(rootPage);
509        String lang = rootPage.getSitemapName();
510        
511        Set<String> subTypes = _contentTypeEP.getSubTypes(contentType);
512        
513        List<Expression> contentTypeExpressions = new ArrayList<>();
514        contentTypeExpressions.add(new ContentTypeExpression(Operator.EQ, contentType));
515        for (String subType : subTypes)
516        {
517            contentTypeExpressions.add(new ContentTypeExpression(Operator.EQ, subType));
518        }
519        
520        Expression contentTypeExpression = new OrExpression(contentTypeExpressions.toArray(new Expression[subTypes.size() + 1]));
521        
522        Expression finalExpr = new AndExpression(contentTypeExpression, new LanguageExpression(Operator.EQ, lang));
523        
524        SortCriteria sort = new SortCriteria();
525        sort.addCriterion(Content.ATTRIBUTE_TITLE, true, true);
526        
527        String xPathQuery = QueryHelper.getXPathQuery(null, "ametys:content", finalExpr, sort);
528        
529        return _resolver.query(xPathQuery);
530    }
531    
532    /**
533     * Gets name form path name
534     * @param pathName the path name
535     * @return the name
536     */
537    public String getName(String pathName)
538    {
539        String prefix = "page-";
540        String name = "";
541        for (String transitionalPageName : pathName.split("/"))
542        {
543            if (!name.equals(""))
544            {
545                name += "/";
546            }
547            name += StringUtils.startsWith(transitionalPageName, prefix) ? StringUtils.substringAfter(transitionalPageName, prefix) : transitionalPageName;
548        }
549        return name;
550    }
551    
552    /**
553     * Checks if name contains only Unicode digits and if so, prefix it with "page-"
554     * @param name The page name
555     * @return The potentially prefixed page name
556     */
557    public String getPathName(String name)
558    {
559        return StringUtils.isNumeric(name) ? "page-" + name : name; 
560    }
561    
562    /**
563     * Clear root page cache
564     * @param rootPage the root page
565     */
566    public void clearCache(Page rootPage)
567    {
568        clearCache(getContentTypeId(rootPage));
569    }
570    
571    /**
572     * Clear root page cache
573     * @param contentTypeId the content type id
574     */
575    public void clearCache(String contentTypeId)
576    {
577        getTransitionalPagesCache().invalidate(PageCacheKey.of(null, contentTypeId, null, null, null));
578        
579        getUserPagesCache().invalidate(PageCacheKey.of(null, contentTypeId, null, null, null));
580        
581        getRootPagesCache().invalidateAll();
582    }
583    
584    /**
585     * Cache of the user directory root pages.
586     * The cache store a Set of TODO indexed by the workspaceName, siteName, siteMapName
587     * @return the cache
588     */
589    protected Cache<RootPageCacheKey, Set<String>> getRootPagesCache()
590    {
591        return _abstractCacheManager.get(ROOT_PAGES_CACHE);
592    }
593    
594    /**
595     * Key to index a user directory root page in a cache
596     */
597    protected static final class RootPageCacheKey extends AbstractCacheKey
598    {
599        /**
600         * Basic constructor
601         * @param workspaceName the workspace name. Can be null.
602         * @param siteName the site name. Can be null.
603         * @param language the sitemap name. Can be null.
604         */
605        public RootPageCacheKey(String workspaceName, String siteName, String language)
606        {
607            super(workspaceName, siteName, language);
608        }
609        
610        /**
611         * Generate a cache key
612         * @param workspaceName the workspace name. Can be null.
613         * @param siteName the site name. Can be null.
614         * @param language the sitemap name. Can be null.
615         * @return the cache key
616         */
617        public static RootPageCacheKey of(String workspaceName, String siteName, String language)
618        {
619            return new RootPageCacheKey(workspaceName, siteName, language);
620        }
621    }
622    
623    /**
624     * Cache of the user directory transitional pages.
625     * The cache store a sorted set of TODO indexed by the workspaceName, siteName, siteMapName, pageName.
626     * @return the cache
627     */
628    protected Cache<PageCacheKey, SortedSet<String>> getTransitionalPagesCache()
629    {
630        return _abstractCacheManager.get(TRANSITIONAL_PAGES_CACHE);
631    }
632    
633    /**
634     * Cache of the user directory user pages.
635     * The cache store a {@link Map} of (content name, content id) of all the content of the page.
636     * The cache is indexed by workspaceName, siteName, siteMapName, pageName.
637     * @return the cache
638     */
639    protected Cache<PageCacheKey, Map<String, String>> getUserPagesCache()
640    {
641        return _abstractCacheManager.get(USER_PAGES_CACHE);
642    }
643    
644    /**
645     * Key to index a user directory page in a cache
646     */
647    protected static final class PageCacheKey extends AbstractCacheKey
648    {
649        
650        /**
651         * Basic constructor
652         * @param workspaceName the workspace name. Can be null.
653         * @param contentTypeId the contentType id. Can be null.
654         * @param siteName the site name. Can be null.
655         * @param language the sitemap name. Can be null.
656         * @param pageName the page name. Can be null.
657         */
658        public PageCacheKey(String workspaceName, String contentTypeId, String siteName, String language, String pageName)
659        {
660            super(workspaceName, contentTypeId, siteName, language, pageName);
661        }
662        
663        /**
664         * Generate a cache key
665         * @param workspaceName the workspace name. Can be null.
666         * @param contentTypeId the contentType id. Can be null.
667         * @param siteName the site name. Can be null.
668         * @param language the sitemap name. Can be null.
669         * @param pageName the page name. Can be null.
670         * @return the cache key
671         */
672        public static PageCacheKey of(String workspaceName, String contentTypeId, String siteName, String language, String pageName)
673        {
674            return new PageCacheKey(workspaceName, contentTypeId, siteName, language, pageName);
675        }
676    }
677}