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