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