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