001/*
002 *  Copyright 2012 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.odfweb.repository;
017
018import java.util.Arrays;
019import java.util.Collections;
020import java.util.Iterator;
021import java.util.List;
022import java.util.Map;
023import java.util.Objects;
024import java.util.Set;
025import java.util.stream.Collectors;
026
027import javax.jcr.Node;
028import javax.jcr.RepositoryException;
029import javax.jcr.Value;
030
031import org.apache.avalon.framework.activity.Initializable;
032import org.apache.avalon.framework.component.Component;
033import org.apache.avalon.framework.service.ServiceException;
034import org.apache.avalon.framework.service.ServiceManager;
035import org.apache.avalon.framework.service.Serviceable;
036import org.apache.commons.lang3.StringUtils;
037
038import org.ametys.cms.content.ContentHelper;
039import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
040import org.ametys.cms.contenttype.ContentTypesHelper;
041import org.ametys.cms.repository.Content;
042import org.ametys.core.cache.AbstractCacheManager;
043import org.ametys.core.cache.Cache;
044import org.ametys.core.ui.Callable;
045import org.ametys.core.util.I18nUtils;
046import org.ametys.core.util.URIUtils;
047import org.ametys.odf.ProgramItem;
048import org.ametys.odf.catalog.CatalogsManager;
049import org.ametys.odf.course.Course;
050import org.ametys.odf.enumeration.OdfReferenceTableHelper;
051import org.ametys.odf.orgunit.RootOrgUnitProvider;
052import org.ametys.odf.program.AbstractProgram;
053import org.ametys.odf.program.Program;
054import org.ametys.odf.program.SubProgram;
055import org.ametys.odf.tree.OdfClassificationHandler;
056import org.ametys.odf.tree.OdfClassificationHandler.LevelValue;
057import org.ametys.plugins.core.impl.cache.AbstractCacheKey;
058import org.ametys.plugins.odfweb.restrictions.OdfProgramRestriction;
059import org.ametys.plugins.odfweb.restrictions.OdfProgramRestrictionManager;
060import org.ametys.plugins.repository.AmetysObjectIterable;
061import org.ametys.plugins.repository.AmetysObjectResolver;
062import org.ametys.plugins.repository.AmetysRepositoryException;
063import org.ametys.plugins.repository.jcr.JCRAmetysObject;
064import org.ametys.plugins.repository.jcr.NameHelper;
065import org.ametys.plugins.repository.provider.WorkspaceSelector;
066import org.ametys.plugins.repository.query.expression.Expression;
067import org.ametys.plugins.repository.query.expression.VirtualFactoryExpression;
068import org.ametys.runtime.i18n.I18nizableText;
069import org.ametys.runtime.model.ModelItem;
070import org.ametys.runtime.plugin.component.AbstractLogEnabled;
071import org.ametys.web.repository.page.Page;
072import org.ametys.web.repository.page.PageQueryHelper;
073import org.ametys.web.repository.site.Site;
074import org.ametys.web.repository.sitemap.Sitemap;
075
076import com.google.common.collect.ImmutableList;
077
078/**
079 * Component providing methods to retrieve ODF virtual pages, such as the ODF root,
080 * level 1 and 2 metadata names, and so on.
081 */
082public class OdfPageHandler extends AbstractLogEnabled implements Component, Initializable, Serviceable
083{
084    /** The avalon role. */
085    public static final String ROLE = OdfPageHandler.class.getName();
086    
087    /** First level attribute name. */
088    public static final String LEVEL1_ATTRIBUTE_NAME = "firstLevel";
089    
090    /** Second level attribute name. */
091    public static final String LEVEL2_ATTRIBUTE_NAME = "secondLevel";
092    
093    /** Catalog data name. */
094    public static final String CATALOG_DATA_NAME = "odf-root-catalog";
095    
096    /** Content types that are not eligible for first and second level */
097    // See ODF-1115 Exclude the mentions enumerator from the list :
098    protected static final List<String> NON_ELIGIBLE_CTYPES_FOR_LEVEL = Arrays.asList("org.ametys.plugins.odf.Content.programItem", "odf-enumeration.Mention");
099    
100    private static final String __ODF_ROOT_PAGES_CACHE = OdfPageHandler.class.getName() + "$odfRootPages";
101    private static final String __HAS_ODF_ROOT_CACHE = OdfPageHandler.class.getName() + "$hasOdfRootPage";
102    private static final String __PROGRAM_LEVEL_PATH_CACHE = OdfPageHandler.class.getName() + "$programLevelPath";
103    private static final String __PROGRAM_RESTRICTION_CACHE = OdfPageHandler.class.getName() + "$programRestriction";
104    
105    private static final String __ROOT_CACHE_ALL_SITES_KEY = "ALL";
106    private static final String __ROOT_CACHE_ALL_SITEMAPS_KEY = "ALL";
107    
108    /** The ametys object resolver. */
109    protected AmetysObjectResolver _resolver;
110    
111    /** The i18n utils. */
112    protected I18nUtils _i18nUtils;
113    
114    /** The content type extension point. */
115    protected ContentTypeExtensionPoint _cTypeEP;
116    
117    /** The ODF Catalog enumeration */
118    protected CatalogsManager _catalogsManager;
119    
120    /** The workspace selector. */
121    protected WorkspaceSelector _workspaceSelector;
122    
123    /** Avalon service manager */
124    protected ServiceManager _manager;
125    
126    /** Restriction manager */
127    protected OdfProgramRestrictionManager _odfRestrictionsManager;
128    
129    /** Content types helper */
130    protected ContentTypesHelper _contentTypesHelper;
131    
132    /** Content helper */
133    protected ContentHelper _contentHelper;
134    
135    /** Odf reference table helper */
136    protected OdfReferenceTableHelper _odfReferenceTableHelper;
137    
138    /** Root orgunit provider */
139    protected RootOrgUnitProvider _orgUnitProvider;
140    
141    /** Root orgunit provider */
142    protected OdfClassificationHandler _odfClassificationHandler;
143    
144    /** The cache manager */
145    protected AbstractCacheManager _cacheManager;
146
147    @Override
148    public void service(ServiceManager serviceManager) throws ServiceException
149    {
150        _manager = serviceManager;
151        _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
152        _i18nUtils = (I18nUtils) serviceManager.lookup(I18nUtils.ROLE);
153        _cTypeEP = (ContentTypeExtensionPoint) serviceManager.lookup(ContentTypeExtensionPoint.ROLE);
154        _workspaceSelector = (WorkspaceSelector) serviceManager.lookup(WorkspaceSelector.ROLE);
155        _catalogsManager = (CatalogsManager) serviceManager.lookup(CatalogsManager.ROLE);
156        _odfRestrictionsManager = (OdfProgramRestrictionManager) serviceManager.lookup(OdfProgramRestrictionManager.ROLE);
157        _contentTypesHelper = (ContentTypesHelper) serviceManager.lookup(ContentTypesHelper.ROLE);
158        _contentHelper = (ContentHelper) serviceManager.lookup(ContentHelper.ROLE);
159        _odfReferenceTableHelper = (OdfReferenceTableHelper) serviceManager.lookup(OdfReferenceTableHelper.ROLE);
160        _orgUnitProvider = (RootOrgUnitProvider) serviceManager.lookup(RootOrgUnitProvider.ROLE);
161        _odfClassificationHandler = (OdfClassificationHandler) serviceManager.lookup(OdfClassificationHandler.ROLE);
162        _cacheManager = (AbstractCacheManager) serviceManager.lookup(AbstractCacheManager.ROLE);
163    }
164    
165    @Override
166    public void initialize() throws Exception
167    {
168        _cacheManager.createMemoryCache(__ODF_ROOT_PAGES_CACHE,
169                new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_ODF_ROOT_PAGES_LABEL"),
170                new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_ODF_ROOT_PAGES_DESCRIPTION"),
171                true,
172                null);
173        
174        _cacheManager.createMemoryCache(__HAS_ODF_ROOT_CACHE,
175                new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_HAS_ODF_ROOT_PAGE_LABEL"),
176                new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_HAS_ODF_ROOT_PAGE_DESCRIPTION"),
177                true,
178                null);
179        
180        _cacheManager.createRequestCache(__PROGRAM_LEVEL_PATH_CACHE,
181                new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_PROGRAM_LEVEL_PATH_LABEL"),
182                new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_PROGRAM_LEVEL_PATH_DESCRIPTION"),
183                false);
184        
185        _cacheManager.createRequestCache(__PROGRAM_RESTRICTION_CACHE,
186                new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_PROGRAM_RESTRICTION_LABEL"),
187                new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_PROGRAM_RESTRICTION_DESCRIPTION"),
188                false);
189    }
190    
191    /**
192     * Get the first ODF root page.
193     * @param siteName The site name
194     * @param sitemapName The sitemap's name
195     * @return a ODF root page or null if not found.
196     * @throws AmetysRepositoryException if an error occurs
197     */
198    public Page getOdfRootPage(String siteName, String sitemapName) throws AmetysRepositoryException
199    {
200        Set<Page> rootPages = getOdfRootPages(siteName, sitemapName);
201        return rootPages.isEmpty() ? null : rootPages.iterator().next();
202    }
203    
204    /**
205     * Get ODF root page of a specific catalog.
206     * @param siteName The site name
207     * @param sitemapName The sitemap name
208     * @param catalogName The catalog name
209     * @return the ODF root page or null if not found.
210     * @throws AmetysRepositoryException if an error occurs
211     */
212    public Page getOdfRootPage(String siteName, String sitemapName, String catalogName) throws AmetysRepositoryException
213    {
214        String catalogToCompare = catalogName != null ? catalogName : "";
215        
216        for (Page odfRootPage : getOdfRootPages(siteName, sitemapName))
217        {
218            if (catalogToCompare.equals(getCatalog(odfRootPage)))
219            {
220                return odfRootPage;
221            }
222        }
223        
224        return null;
225    }
226    
227    /**
228     * Get the id of ODF root pages
229     * @param siteName The site name
230     * @param sitemapName The sitemap name
231     * @return The ids of ODF root pages
232     * @throws AmetysRepositoryException if an error occurs.
233     */
234    @Callable(rights = Callable.NO_CHECK_REQUIRED) // only retrieve page ids
235    public List<String> getOdfRootPageIds(String siteName, String sitemapName) throws AmetysRepositoryException
236    {
237        Set<Page> pages = getOdfRootPages(siteName, sitemapName);
238        return pages.stream().map(p -> p.getId()).collect(Collectors.toList());
239    }
240    
241    /**
242     * Get the ODF root pages.
243     * @param siteName the current site.
244     * @param sitemapName the current sitemap/language.
245     * @return the ODF root pages
246     * @throws AmetysRepositoryException if an error occurs.
247     */
248    public Set<Page> getOdfRootPages(String siteName, String sitemapName) throws AmetysRepositoryException
249    {
250        Cache<OdfRootPageCacheKey, Set<String>> cache = _getOdfRootPagesCache();
251        
252        String workspaceName = _workspaceSelector.getWorkspace();
253        
254        Set<String> rootPageIds = cache.get(OdfRootPageCacheKey.of(workspaceName, Objects.toString(siteName, __ROOT_CACHE_ALL_SITES_KEY), Objects.toString(sitemapName, __ROOT_CACHE_ALL_SITEMAPS_KEY)), item -> {
255            return _getOdfRootPages(siteName, sitemapName)
256                    .stream()
257                    .map(Page::getId)
258                    .collect(Collectors.toSet());
259        });
260        
261        return rootPageIds.stream()
262                   .map(id -> (Page) _resolver.resolveById(id))
263                   .collect(Collectors.toSet());
264    }
265    
266    /**
267     * Test if the given site has at least one sitemap with an odf root page.
268     * @param site the site to test.
269     * @return true if the site has at least one sitemap with an odf root page, false otherwise.
270     */
271    public boolean hasOdfRootPage(Site site)
272    {
273        Cache<HasOdfRootPageCacheKey, Boolean> cache = _getHasOdfRootPageCache();
274        
275        String workspace = _workspaceSelector.getWorkspace();
276        
277        return cache.get(HasOdfRootPageCacheKey.of(workspace, site.getName()), item -> {
278            
279            Iterator<Sitemap> sitemaps = site.getSitemaps().iterator();
280            while (sitemaps.hasNext())
281            {
282                String sitemapName = sitemaps.next().getName();
283                
284                if (!getOdfRootPages(site.getName(), sitemapName).isEmpty())
285                {
286                    return true;
287                }
288            }
289            
290            return false;
291        });
292    }
293    
294    /**
295     * Determines if the program is part of the site restrictions
296     * @param rootPage The ODF root page
297     * @param program The program
298     * @return <code>true</code> the program is part of the site restrictions
299     */
300    public boolean isValidRestriction(Page rootPage, Program program)
301    {
302        Cache<ProgramInRootCacheKey, Boolean> cache = _cacheManager.get(__PROGRAM_RESTRICTION_CACHE);
303        
304        return cache.get(ProgramInRootCacheKey.of(rootPage.getId(), program.getId()), isValid -> {
305            // Check catalog
306            if (!program.getCatalog().equals(getCatalog(rootPage)))
307            {
308                return false;
309            }
310            
311            // Check language
312            if (!program.getLanguage().equals(rootPage.getSitemapName()))
313            {
314                return false;
315            }
316            
317            // Check site restrictions
318            OdfProgramRestriction restriction = _odfRestrictionsManager.getRestriction(rootPage);
319            if (restriction != null)
320            {
321                return restriction.contains(program);
322            }
323            
324            return true;
325        });
326    }
327    
328    /**
329     * Clear the ODF root page cache.
330     */
331    public void clearRootCache()
332    {
333        _getOdfRootPagesCache().invalidateAll();
334        _getHasOdfRootPageCache().invalidateAll();
335    }
336    
337    /**
338     * Clear the ODF root page cache for a given site and language.
339     * @param siteName the current site.
340     * @param sitemapName the current sitemap/language.
341     */
342    public void clearRootCache(String siteName, String sitemapName)
343    {
344        _getOdfRootPagesCache().invalidate(OdfRootPageCacheKey.of(null, siteName, sitemapName));
345        _getHasOdfRootPageCache().invalidate(HasOdfRootPageCacheKey.of(null, siteName));
346    }
347    
348    /**
349     * Determines if the page is a ODF root page
350     * @param page The page to test
351     * @return true if the page is a ODF root page
352     */
353    public boolean isODFRootPage (Page page)
354    {
355        if (page instanceof JCRAmetysObject)
356        {
357            try
358            {
359                JCRAmetysObject jcrPage = (JCRAmetysObject) page;
360                Node node = jcrPage.getNode();
361                
362                if (node.hasProperty(AmetysObjectResolver.VIRTUAL_PROPERTY))
363                {
364                    Value[] values = node.getProperty(AmetysObjectResolver.VIRTUAL_PROPERTY).getValues();
365                    
366                    boolean hasValue = false;
367                    for (int i = 0; i < values.length && !hasValue; i++)
368                    {
369                        hasValue = FirstLevelPageFactory.class.getName().equals(values[i].getString());
370                    }
371                    
372                    return hasValue;
373                }
374                else
375                {
376                    return false;
377                }
378            }
379            catch (RepositoryException e)
380            {
381                return false;
382            }
383        }
384        
385        return false;
386        
387    }
388    
389    /**
390     * Get the ODF root page.
391     * @param siteName the current site.
392     * @param sitemapName the current sitemap/language.
393     * @return the ODF root page or null if not found.
394     * @throws AmetysRepositoryException if an error occurs.
395     */
396    protected Page _getOdfRootPage(String siteName, String sitemapName) throws AmetysRepositoryException
397    {
398        Expression expression = new VirtualFactoryExpression(FirstLevelPageFactory.class.getName());
399        String query = PageQueryHelper.getPageXPathQuery(siteName, sitemapName, null, expression, null);
400        
401        AmetysObjectIterable<Page> pages = _resolver.query(query);
402        Page page = pages.stream().findFirst().orElse(null);
403        
404        return page;
405    }
406    
407    /**
408     * Get the ODF root page.
409     * @param siteName the current site.
410     * @param sitemapName the current sitemap/language.
411     * @return the ODF root page or null if not found.
412     * @throws AmetysRepositoryException if an error occurs.
413     */
414    protected Set<Page> _getOdfRootPages(String siteName, String sitemapName) throws AmetysRepositoryException
415    {
416        Expression expression = new VirtualFactoryExpression(FirstLevelPageFactory.class.getName());
417        String query = PageQueryHelper.getPageXPathQuery(siteName, sitemapName, null, expression, null);
418        
419        return _resolver.<Page>query(query).stream().collect(Collectors.toSet());
420    }
421    
422    /**
423     * Get the catalog value of the ODF root page
424     * @param rootPage The ODF root page
425     * @return the catalog value
426     */
427    public String getCatalog (Page rootPage)
428    {
429        return rootPage.getValue(CATALOG_DATA_NAME, StringUtils.EMPTY);
430    }
431    
432    /**
433     * Get the first level metadata name.
434     * @param siteName the site name.
435     * @param sitemapName the sitemap name.
436     * @param catalog the current selected catalog.
437     * @return the first level metadata name.
438     */
439    public String getLevel1Metadata(String siteName, String sitemapName, String catalog)
440    {
441        Page rootPage = getOdfRootPage(siteName, sitemapName, catalog);
442        
443        return getLevel1Metadata(rootPage);
444    }
445    
446    /**
447     * Get the first level metadata name.
448     * @param rootPage the ODF root page.
449     * @return the first level metadata name.
450     */
451    public String getLevel1Metadata(Page rootPage)
452    {
453        return rootPage.getValue(LEVEL1_ATTRIBUTE_NAME);
454    }
455    
456    /**
457     * Get the second level metadata name.
458     * @param siteName the site name.
459     * @param sitemapName the sitemap name.
460     * @param catalog the current selected catalog.
461     * @return the second level metadata name.
462     */
463    public String getLevel2Metadata(String siteName, String sitemapName, String catalog)
464    {
465        Page rootPage = getOdfRootPage(siteName, sitemapName, catalog);
466        
467        return getLevel2Metadata(rootPage);
468    }
469    
470    /**
471     * Get the second level metadata name.
472     * @param rootPage the ODF root page.
473     * @return the second level metadata name.
474     */
475    public String getLevel2Metadata(Page rootPage)
476    {
477        return rootPage.getValue(LEVEL2_ATTRIBUTE_NAME);
478    }
479    
480    /**
481     * Get the first level metadata values (with translated label).
482     * @param siteName the site name.
483     * @param sitemapName the sitemap name.
484     * @param catalog the current selected catalog.
485     * @return the first level metadata values.
486     */
487    public Map<String, LevelValue> getLevel1Values(String siteName, String sitemapName, String catalog)
488    {
489        Page rootPage = getOdfRootPage(siteName, sitemapName, catalog);
490        
491        return getLevel1Values(rootPage);
492    }
493    
494    /**
495     * Get the level value of a program by extracting and transforming the raw program value at the desired metadata path
496     * @param program The program
497     * @param levelMetaPath The desired metadata path that represent a level
498     * @return The final level value
499     */
500    public String getProgramLevelValue(Program program, String levelMetaPath)
501    {
502        List<String> programLevelValue = _odfClassificationHandler.getProgramLevelValues(program, levelMetaPath);
503        return programLevelValue.isEmpty() ? null : programLevelValue.get(0);
504    }
505    
506    /**
507     * Get the first level value of a program by extracting and transforming the raw program value
508     * @param rootPage The root page
509     * @param program The program
510     * @return The final level value or <code>null</code> if not found
511     */
512    public String getProgramLevel1Value(Page rootPage, Program program)
513    {
514        String level1Metadata = getLevel1Metadata(rootPage);
515        if (StringUtils.isNotBlank(level1Metadata))
516        {
517            List<String> programLevelValue = _odfClassificationHandler.getProgramLevelValues(program, level1Metadata);
518            return programLevelValue.isEmpty() ? null : programLevelValue.get(0);
519        }
520        else
521        {
522            return null;
523        }
524    }
525    
526    /**
527     * Get the second level value of a program by extracting and transforming the raw program value
528     * @param rootPage The root page
529     * @param program The program
530     * @return The final level value or <code>null</code> if not found
531     */
532    public String getProgramLevel2Value(Page rootPage, Program program)
533    {
534        String level2Metadata = getLevel2Metadata(rootPage);
535        if (StringUtils.isNotBlank(level2Metadata))
536        {
537            List<String> programLevelValue = _odfClassificationHandler.getProgramLevelValues(program, level2Metadata);
538            return programLevelValue.isEmpty() ? null : programLevelValue.get(0);
539        }
540        else
541        {
542            return null;
543        }
544    }
545    
546    /**
547     * Get the orgunit identifier given an uai code
548     * @param rootPage Odf root page
549     * @param uaiCode The uai code
550     * @return The orgunit id or null if not found
551     */
552    public String getOrgunitIdFromUaiCode(Page rootPage, String uaiCode)
553    {
554        return _odfClassificationHandler.getOrgunitIdFromUaiCode(rootPage.getSitemapName(), uaiCode);
555    }
556    
557    /**
558     * Get the programs available for a ODF root page, taking account the site's restrictions
559     * @param rootPage The ODF root page
560     * @param level1 filters results with a level1 value. Can be null.
561     * @param level2 filters results with a level2 value. Can be null.
562     * @param programCode expected program code. Can be null.
563     * @param programName expected program name. Can be null.
564     * @return an iterator over resulting programs
565     */
566    public AmetysObjectIterable<Program> getProgramsWithRestrictions(Page rootPage, String level1, String level2, String programCode, String programName)
567    {
568        return getProgramsWithRestrictions(rootPage, getLevel1Metadata(rootPage), level1, getLevel2Metadata(rootPage), level2, programCode, programName);
569    }
570
571    /**
572     * Get the programs available for a ODF root page, taking account the site's restrictions
573     * @param rootPage The ODF root page
574     * @param level1Metadata metadata name for first level
575     * @param level1 filters results with a level1 value. Can be null.
576     * @param level2Metadata metadata name for second level
577     * @param level2 filters results with a level2 value. Can be null.
578     * @param programCode expected program code. Can be null.
579     * @param programName expected program name. Can be null.
580     * @return an iterator over resulting programs
581     */
582    public AmetysObjectIterable<Program> getProgramsWithRestrictions(Page rootPage, String level1Metadata, String level1, String level2Metadata, String level2, String programCode, String programName)
583    {
584        OdfProgramRestriction restriction = _odfRestrictionsManager.getRestriction(rootPage);
585        return _odfClassificationHandler.getPrograms(getCatalog(rootPage), rootPage.getSitemapName(), level1Metadata, level1, level2Metadata, level2, programCode, programName, restriction == null ? null : ImmutableList.of(restriction.getExpression()));
586    }
587    
588    /**
589     * Get the first level metadata values (with translated label)
590     * @param rootPage the ODF root page.
591     * @return the first level metadata values. Can be empty if there is no level1 attribute on root page.
592     */
593    public Map<String, LevelValue> getLevel1Values(Page rootPage)
594    {
595        String level1Value = getLevel1Metadata(rootPage);
596        if (StringUtils.isNotBlank(level1Value))
597        {
598            return _odfClassificationHandler.getLevelValues(level1Value, rootPage.getSitemapName());
599        }
600        else
601        {
602            return Collections.EMPTY_MAP;
603        }
604    }
605    
606    /**
607     * Get the second level metadata values (with translated label).
608     * @param siteName the site name.
609     * @param sitemapName the sitemap name.
610     * @param catalog the current selected catalog.
611     * @return the second level metadata values.
612     */
613    public Map<String, LevelValue> getLevel2Values(String siteName, String sitemapName, String catalog)
614    {
615        Page rootPage = getOdfRootPage(siteName, sitemapName, catalog);
616        
617        return getLevel2Values(rootPage);
618    }
619    
620    /**
621     * Get the second level metadata values (with translated label).
622     * @param rootPage the ODF root page.
623     * @return the second level metadata values. Can be empty if there is no level2 attribute on root page.
624     */
625    public Map<String, LevelValue> getLevel2Values(Page rootPage)
626    {
627        String level2Value = getLevel2Metadata(rootPage);
628        if (StringUtils.isNotBlank(level2Value))
629        {
630            return _odfClassificationHandler.getLevelValues(level2Value, rootPage.getSitemapName());
631        }
632        else
633        {
634            return Collections.EMPTY_MAP;
635        }
636    }
637
638    /**
639     * Encode level value to be use into a URI.
640     * @param value The raw value
641     * @return the encoded value
642     */
643    public String encodeLevelValue(String value)
644    {
645        String encodedValue = StringUtils.replace(value, "-", "@2D");
646        encodedValue = StringUtils.replace(encodedValue, "/", "@2F");
647        encodedValue = StringUtils.replace(encodedValue, ":", "@3A");
648        encodedValue = StringUtils.replace(encodedValue, "?", "@3F");
649        return URIUtils.encodePathSegment(encodedValue);
650    }
651    
652    /**
653     * Decode level value used in a URI
654     * @param value The encoded value
655     * @return the decoded value
656     */
657    public String decodeLevelValue(String value)
658    {
659        String decodedValue =  URIUtils.decode(value);
660        decodedValue = StringUtils.replace(decodedValue, "@3F", "?");
661        decodedValue = StringUtils.replace(decodedValue, "@3A", ":");
662        decodedValue = StringUtils.replace(decodedValue, "@2F", "/");
663        return StringUtils.replace(decodedValue, "@2D", "-");
664    }
665    
666    /**
667     * Returns the page's name of a {@link ProgramItem}.
668     * Only {@link AbstractProgram} and {@link Course} can have a page.
669     * @param item The program item
670     * @return The page's name
671     * @throws IllegalArgumentException if the program item is not a {@link AbstractProgram} nor a {@link Course}.
672     */
673    public String getPageName (ProgramItem item)
674    {
675        if (item instanceof AbstractProgram || item instanceof Course)
676        {
677            String filteredTitle = "";
678            try
679            {
680                filteredTitle = NameHelper.filterName(((Content) item).getTitle());
681            }
682            catch (IllegalArgumentException e)
683            {
684                // title does not match the expected regular expression : ^([0-9-_]*)[a-z].*$, use default title
685                if (item instanceof Program)
686                {
687                    filteredTitle = "program";
688                }
689                else if (item instanceof SubProgram)
690                {
691                    filteredTitle = "subprogram";
692                }
693                else if (item instanceof Course)
694                {
695                    filteredTitle = "course";
696                }
697            }
698            
699            return filteredTitle + "-" + item.getCode();
700        }
701        else
702        {
703            throw new IllegalArgumentException("Illegal program item : no page can be associated for a program item of type " + item.getClass().getName());
704        }
705    }
706
707    /**
708     * Get the eligible enumerated attribute definitions for ODF page level
709     * @return the eligible attribute definitions
710     */
711    public Map<String, ModelItem> getEligibleAttributesForLevel()
712    {
713        return _odfClassificationHandler.getEligibleAttributesForLevel();
714    }
715
716    /**
717     * Get the ODF catalogs
718     * @return the ODF catalogs
719     */
720    public Map<String, I18nizableText> getCatalogs()
721    {
722        return _odfClassificationHandler.getCatalogs();
723    }
724
725    /**
726     * Get the enumerated attribute definitions for the given content type.
727     * Attribute with enumerator or content attribute are considered as enumerated
728     * @param programContentTypeId The content type's id
729     * @param allowMultiple <code>true</code> true to allow multiple attributes
730     * @return The definitions of enumerated attributes
731     */
732    public Map<String, ModelItem> getEnumeratedAttributes(String programContentTypeId, boolean allowMultiple)
733    {
734        return _odfClassificationHandler.getEnumeratedAttributes(programContentTypeId, allowMultiple);
735    }
736    
737    /**
738     * Compute the path from the root odf page, representing the first and second level pages.
739     * @param rootPage The odf root page
740     * @param parentProgram The program to compute
741     * @return the path, can be empty if no levels defined, and null if the parent program does not have values for levels attributes
742     */
743    public String computeLevelsPath(Page rootPage, Program parentProgram)
744    {
745        Cache<ProgramInRootCacheKey, String> levelCache = _cacheManager.get(__PROGRAM_LEVEL_PATH_CACHE);
746        
747        return levelCache.get(ProgramInRootCacheKey.of(rootPage.getId(), parentProgram.getId()), item -> {
748            // Level 1 is defined => Check the value
749            if (getLevel1Metadata(rootPage) != null)
750            {
751                String level1 = getProgramLevel1Value(rootPage, parentProgram);
752                
753                // Value for level 1 is defined => Check for the second level
754                if (StringUtils.isNotBlank(level1))
755                {
756                    // Level 2 is defined => Check the value
757                    if (getLevel2Metadata(rootPage) != null)
758                    {
759                        String level2 = getProgramLevel2Value(rootPage, parentProgram);
760                        
761                        // Value for level 2 is defined => Return the level 2 page
762                        if (StringUtils.isNotBlank(level2))
763                        {
764                            Page secondLevelPage = findSecondLevelPage(rootPage, level1, level2);
765                            return secondLevelPage.getParent().getName() + "/" + secondLevelPage.getName();
766                        }
767                        
768                        // Value for level 2 is not defined => Return null
769                        return null;
770                    }
771                    
772                    // Level 2 is not defined => Return the level 1 page
773                    return findFirstLevelPage(rootPage, level1).getName();
774                }
775
776                // Value for level 1 is not defined => Return null
777                return null;
778            }
779            
780            // Level 1 is not defined => Return an empty path, all pages are on the root page
781            return StringUtils.EMPTY;
782        });
783    }
784    
785    /**
786     * Build the level 1 identifier
787     * @param rootPage The ODF root page
788     * @param level1Value The level1 name
789     * @return the identifier beginning by odfLevel1://...
790     */
791    public String buildLevel1Id(Page rootPage, String level1Value)
792    {
793        // E.g: odfLevel1://XA?rootId=...
794        return "odfLevel1://" + encodeLevelValue(level1Value) + "?rootId=" + rootPage.getId();
795    }
796    
797    /**
798     * Build the level 2 identifier
799     * @param rootPage The ODF root page
800     * @param level1Value The level1 name
801     * @param level2Value The level2 name
802     * @return the identifier beginning by odfLevel2://...
803     */
804    public String buildLevel2Id(Page rootPage, String level1Value, String level2Value)
805    {
806        // E.g: odfLevel2://XA/ALL?rootId=...
807        return "odfLevel2://" + encodeLevelValue(level1Value) + "/" + encodeLevelValue(level2Value) + "?rootId=" + rootPage.getId();
808    }
809    
810    /**
811     * Get the first level page from the given root page with the level 1 value.
812     * @param rootPage The odf root page
813     * @param level1Value The first level value
814     * @return a first level page
815     */
816    public FirstLevelPage findFirstLevelPage(Page rootPage, String level1Value)
817    {
818        // Calculate the real path
819        return _resolver.resolveById(buildLevel1Id(rootPage, level1Value));
820    }
821    
822    /**
823     * Get the second level page from the given root page with the level 1 and level 2 values.
824     * @param rootPage The odf root page
825     * @param level1Value The first level value
826     * @param level2Value The second level value
827     * @return a second level page
828     */
829    public SecondLevelPage findSecondLevelPage(Page rootPage, String level1Value, String level2Value)
830    {
831        // Calculate the real path
832        return _resolver.resolveById(buildLevel2Id(rootPage, level1Value, level2Value));
833    }
834    
835    /**
836     * Add an intermediate redirect page if the called page name doesn't match the real page name.
837     * @param page The page
838     * @param name The called page name
839     * @return The page maybe included in a {@link RedirectPage}
840     */
841    public Page addRedirectIfNeeded(Page page, String name)
842    {
843        // Decode both because in the case of page.getName(), only the code is encoded (it is
844        // normal) and the name is already partially decoded before, so we encode both to be
845        // sure to compare the same values.
846        if (!decodeLevelValue(name).equals(decodeLevelValue(page.getName())))
847        {
848            getLogger().warn("Redirect path '{}' to '{}' page", name, page.getName());
849            return new RedirectPage(page instanceof RedirectPage redirectPage ? redirectPage.getRedirectPage() : page);
850        }
851        return page;
852    }
853    
854    /**
855     * Explore the queue path if it is not empty
856     * @param page The root page to explore
857     * @param queuePath The path
858     * @return The child page, or given page if queue path is empty
859     */
860    public Page exploreQueuePath(Page page, String queuePath)
861    {
862        if (StringUtils.isNotEmpty(queuePath))
863        {
864            return page.getChild(queuePath);
865        }
866        return page;
867    }
868    
869    private Cache<OdfRootPageCacheKey, Set<String>> _getOdfRootPagesCache()
870    {
871        return _cacheManager.get(__ODF_ROOT_PAGES_CACHE);
872    }
873    
874    private Cache<HasOdfRootPageCacheKey, Boolean> _getHasOdfRootPageCache()
875    {
876        return _cacheManager.get(__HAS_ODF_ROOT_CACHE);
877    }
878    
879    static class OdfRootPageCacheKey extends AbstractCacheKey
880    {
881        public OdfRootPageCacheKey(String workspaceName, String siteName, String sitemapName)
882        {
883            super(workspaceName, siteName, sitemapName);
884        }
885        
886        public static OdfRootPageCacheKey of(String workspaceName, String siteName, String sitemapName)
887        {
888            return new OdfRootPageCacheKey(workspaceName, siteName, sitemapName);
889        }
890    }
891    
892    static class HasOdfRootPageCacheKey extends AbstractCacheKey
893    {
894        public HasOdfRootPageCacheKey(String workspaceName, String siteName)
895        {
896            super(workspaceName, siteName);
897        }
898        
899        public static HasOdfRootPageCacheKey of(String workspaceName, String siteName)
900        {
901            return new HasOdfRootPageCacheKey(workspaceName, siteName);
902        }
903    }
904    
905    static class ProgramInRootCacheKey extends AbstractCacheKey
906    {
907        public ProgramInRootCacheKey(String rootPageId, String programId)
908        {
909            super(rootPageId, programId);
910        }
911        
912        public static ProgramInRootCacheKey of(String rootPageId, String programId)
913        {
914            return new ProgramInRootCacheKey(rootPageId, programId);
915        }
916    }
917}