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