001/*
002 *  Copyright 2011 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.ArrayList;
019import java.util.HashSet;
020import java.util.List;
021import java.util.Set;
022
023import org.apache.avalon.framework.activity.Initializable;
024import org.apache.avalon.framework.component.Component;
025import org.apache.avalon.framework.service.ServiceException;
026import org.apache.avalon.framework.service.ServiceManager;
027import org.apache.avalon.framework.service.Serviceable;
028import org.apache.commons.lang3.StringUtils;
029
030import org.ametys.cms.repository.Content;
031import org.ametys.core.cache.AbstractCacheManager;
032import org.ametys.core.cache.Cache;
033import org.ametys.odf.ODFHelper;
034import org.ametys.odf.ProgramItem;
035import org.ametys.odf.course.Course;
036import org.ametys.odf.courselist.CourseList;
037import org.ametys.odf.program.AbstractProgram;
038import org.ametys.odf.program.Program;
039import org.ametys.odf.program.ProgramPart;
040import org.ametys.odf.program.SubProgram;
041import org.ametys.plugins.core.impl.cache.AbstractCacheKey;
042import org.ametys.plugins.repository.AmetysObjectResolver;
043import org.ametys.plugins.repository.UnknownAmetysObjectException;
044import org.ametys.runtime.config.Config;
045import org.ametys.runtime.i18n.I18nizableText;
046import org.ametys.runtime.plugin.component.AbstractLogEnabled;
047import org.ametys.web.repository.page.Page;
048
049/**
050 * Resolves an ODF page path from the associated ODF content.
051 */
052public class OdfPageResolver extends AbstractLogEnabled implements Component, Serviceable, Initializable
053{
054    /** The avalon role. */
055    public static final String ROLE = OdfPageResolver.class.getName();
056    
057    private static final String __PATH_IN_SITEMAP_CACHE = OdfPageResolver.class.getName() + "$pathInSitemap";
058    private static final String __LEVELS_PATH_CACHE = OdfPageResolver.class.getName() + "$levelsPath";
059    
060    /** The ametys object resolver. */
061    protected AmetysObjectResolver _ametysResolver;
062    /** The odf page handler */
063    protected OdfPageHandler _odfPageHandler;
064    /** ODF helper */
065    protected ODFHelper _odfHelper;
066    /** The cache manager */
067    protected AbstractCacheManager _cacheManager;
068    
069    public void service(ServiceManager serviceManager) throws ServiceException
070    {
071        _ametysResolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
072        _odfPageHandler = (OdfPageHandler) serviceManager.lookup(OdfPageHandler.ROLE);
073        _odfHelper = (ODFHelper) serviceManager.lookup(ODFHelper.ROLE);
074        _cacheManager = (AbstractCacheManager) serviceManager.lookup(AbstractCacheManager.ROLE);
075    }
076    
077    public void initialize() throws Exception
078    {
079        _cacheManager.createRequestCache(__PATH_IN_SITEMAP_CACHE,
080                new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_PATH_IN_SITEMAP_LABEL"),
081                new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_PATH_IN_SITEMAP_DESCRIPTION"),
082                false);
083        _cacheManager.createRequestCache(__LEVELS_PATH_CACHE,
084                new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_LEVELS_PATH_LABEL"),
085                new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_LEVELS_PATH_DESCRIPTION"),
086                false);
087    }
088    
089    /**
090     * Get all referencing pages for this program item, in all sites and all sitemaps
091     * @param programItem The program item
092     * @return the referencing pages
093     */
094    public Set<Page> getReferencingPages(ProgramItem programItem)
095    {
096        return getReferencingPages(programItem, null, ((Content) programItem).getLanguage());
097    }
098    
099    /**
100     * Get all referencing pages for this program item
101     * @param programItem The program item
102     * @param siteName The site name. Can be null to search on all sites
103     * @param lang The sitemap language. Can be null to search on all sitemaps
104     * @return the referencing pages
105     */
106    public Set<Page> getReferencingPages(ProgramItem programItem, String siteName, String lang)
107    {
108        Set<Page> refPages = new HashSet<>();
109        
110        Set<Page> odfRootPages = _odfPageHandler.getOdfRootPages(siteName, lang);
111        
112        for (Page rootPage : odfRootPages)
113        {
114            if (programItem instanceof Program)
115            {
116                ProgramPage programPage = getProgramPage(rootPage, (Program) programItem);
117                if (programPage != null)
118                {
119                    refPages.add(programPage);
120                }
121            }
122            else if (programItem instanceof SubProgram)
123            {
124                Set<Program> parentPrograms = _odfHelper.getParentPrograms(programItem);
125                for (Program parentProgram : parentPrograms)
126                {
127                    ProgramPage subProgramPage = getSubProgramPage(rootPage, (SubProgram) programItem, parentProgram);
128                    if (subProgramPage != null)
129                    {
130                        refPages.add(subProgramPage);
131                    }
132                }
133            }
134            else if (programItem instanceof Course)
135            {
136                List<CourseList> parentCourseLists = ((Course) programItem).getParentCourseLists();
137                for (CourseList courseList : parentCourseLists)
138                {
139                    List<Course> parentCourses = courseList.getParentCourses();
140                    for (Course parentCourse : parentCourses)
141                    {
142                        CoursePage coursePage = getCoursePage(rootPage, (Course) programItem, parentCourse);
143                        if (coursePage != null)
144                        {
145                            refPages.add(coursePage);
146                        }
147                    }
148                    
149                    List<AbstractProgram> parentAbstractPrograms = getNearestAncestorAbstractPrograms(courseList);
150                    for (AbstractProgram parentAbstractProgram : parentAbstractPrograms)
151                    {
152                        CoursePage coursePage = getCoursePage(rootPage, (Course) programItem, parentAbstractProgram);
153                        if (coursePage != null)
154                        {
155                            refPages.add(coursePage);
156                        }
157                    }
158                }
159            }
160        }
161        
162        return refPages;
163    }
164    
165    /**
166     * Return the program page
167     * @param program the program
168     * @return the page program or null
169     */
170    public ProgramPage getProgramPage(Program program)
171    {
172        return getProgramPage(program, null);
173    }
174    
175    /**
176     * Return the program page
177     * @param program the program
178     * @param siteName The current site name. If the no ODF root page is present in this site, the default ODF site will be used instead.
179     * @return the page program or null
180     */
181    public ProgramPage getProgramPage(Program program, String siteName)
182    {
183        Page odfRootPage = getOdfRootPage(siteName, program.getLanguage(), program.getCatalog());
184        
185        if (odfRootPage == null)
186        {
187            return null;
188        }
189        
190        return getProgramPage(odfRootPage, program);
191    }
192    
193    /**
194     * Return the program page
195     * @param odfRootPage the odf root page
196     * @param program the program
197     * @return the page program or null
198     */
199    public ProgramPage getProgramPage (Page odfRootPage, Program program)
200    {
201        // E.g: program://_root?rootId=xxxx&programId=xxxx
202        String pageId =  "program://_root?rootId=" + odfRootPage.getId() + "&programId=" + program.getId();
203        try
204        {
205            return _ametysResolver.resolveById(pageId);
206        }
207        catch (UnknownAmetysObjectException e)
208        {
209            return null;
210        }
211    }
212    
213    /**
214     * Return the subprogram page
215     * @param subProgram the subprogram
216     * @param parentProgram The parent program
217     * @param siteName The current site name. If the no ODF root page is present in this site, the default ODF site will be used instead.
218     * @return the subprogram page or null
219     */
220    public ProgramPage getSubProgramPage(SubProgram subProgram, AbstractProgram parentProgram, String siteName)
221    {
222        Page odfRootPage = getOdfRootPage(siteName, subProgram.getLanguage(), subProgram.getCatalog());
223        
224        if (odfRootPage == null)
225        {
226            return null;
227        }
228        
229        return getSubProgramPage(odfRootPage, subProgram, parentProgram);
230    }
231    
232    /**
233     * Return the subprogram page
234     * @param odfRootPage the odf root page
235     * @param subProgram the subprogram
236     * @param parentAbstractProgram The parent program or subprogram
237     * @return the subprogram page or null
238     */
239    public ProgramPage getSubProgramPage (Page odfRootPage, SubProgram subProgram, AbstractProgram parentAbstractProgram)
240    {
241        AbstractProgram parent = getNearestAncestorAbstractProgram(subProgram, parentAbstractProgram);
242        Program parentProgram = getParentProgram(subProgram, parentAbstractProgram);
243        
244        if (parent == null || parentProgram == null)
245        {
246            // No page
247            return null;
248        }
249     
250        // Id is like program://path/to/subprogram?rootId=xxxx&programId=xxxx&parentId=xxxx
251        String pageId =  "program://" + getPathInProgram(parent, parentProgram) + "?rootId=" + odfRootPage.getId() + "&programId=" + subProgram.getId() + "&parentId=" + parentProgram.getId();
252        try
253        {
254            return _ametysResolver.resolveById(pageId);
255        }
256        catch (UnknownAmetysObjectException e)
257        {
258            return null;
259        }
260    }
261    
262    /**
263     * Return the course page
264     * @param course the course
265     * @param parentProgram the parent program or subprogram. Can be null.
266     * @param siteName The current site name. If the no ODF root page is present in this site, the default ODF site will be used instead.
267     * @return the course page or null if not found
268     */
269    public CoursePage getCoursePage(Course course, AbstractProgram parentProgram, String siteName)
270    {
271        String catalog = course.getCatalog();
272        Page odfRootPage = getOdfRootPage(siteName, course.getLanguage(), catalog);
273        
274        if (odfRootPage == null)
275        {
276            return null;
277        }
278        
279        return getCoursePage(odfRootPage, course, parentProgram);
280    }
281    
282    /**
283     * Return the course page
284     * @param odfRootPage the odf root page
285     * @param course the course
286     * @param parentAbstractProgram the parent program or subprogram. Can be null.
287     * @return the course page or null if not found
288     */
289    public CoursePage getCoursePage (Page odfRootPage, Course course, AbstractProgram parentAbstractProgram)
290    {
291        ProgramItem parent = null;
292        
293        Course parentCourse = getNearestAncestorCourse(course, parentAbstractProgram);
294        if (parentCourse != null)
295        {
296            parent = parentCourse;
297        }
298        else
299        {
300            parent = getNearestAncestorAbstractProgram(course, parentAbstractProgram);
301        }
302        Program parentProgram = getParentProgram(course, parentAbstractProgram);
303        
304        if (parent == null || parentProgram == null)
305        {
306            // No page
307            return null;
308        }
309     
310        // Test program restriction
311        if (!_odfPageHandler.isValidRestriction(odfRootPage, parentProgram))
312        {
313            return null;
314        }
315        
316        // Id is like course://path/from/program?rootId=xxx&courseId=xxx&programId=xxxx
317        String pageId =  "course://" + getPathInProgram(parent, parentProgram) + "?rootId=" + odfRootPage.getId() + "&courseId=" + course.getId() + "&programId=" + parentProgram.getId();
318        try
319        {
320            return _ametysResolver.resolveById(pageId);
321        }
322        catch (UnknownAmetysObjectException e)
323        {
324            return null;
325        }
326    }
327    
328    /**
329     * Return the course page
330     * @param course the course
331     * @param parentCourse the parent course. Can NOT be null.
332     * @param siteName The current site name. If the no ODF root page is present in this site, the default ODF site will be used instead.
333     * @return the course page or null if not found
334     */
335    public CoursePage getCoursePage (Course course, Course parentCourse, String siteName)
336    {
337        String catalog = course.getCatalog();
338        Page odfRootPage = getOdfRootPage(siteName, course.getLanguage(), catalog);
339        
340        if (odfRootPage == null)
341        {
342            return null;
343        }
344        
345        return getCoursePage(odfRootPage, course, parentCourse);
346    }
347    
348    /**
349     * Return the course page
350     * @param odfRootPage the odf root page
351     * @param course the course
352     * @param parentCourse the parent course. Can NOT be null.
353     * @return the course page or null if not found
354     */
355    public CoursePage getCoursePage (Page odfRootPage, Course course, Course parentCourse)
356    {
357        AbstractProgram parent = getNearestAncestorAbstractProgram(parentCourse, null);
358        Program parentProgram = getParentProgram(parentCourse, null);
359        
360        if (parent == null || parentProgram == null)
361        {
362            // No page
363            return null;
364        }
365     
366        // Test program restriction
367        if (!_odfPageHandler.isValidRestriction(odfRootPage, parentProgram))
368        {
369            return null;
370        }
371        
372        // Id is like course://path/from/program?rootId=xxx&courseId=xxx&programId=xxxx
373        String pageId =  "course://" + getPathInProgram(parentCourse, parentProgram) + "?rootId=" + odfRootPage.getId() + "&courseId=" + course.getId() + "&programId=" + parentProgram.getId();
374        try
375        {
376            return _ametysResolver.resolveById(pageId);
377        }
378        catch (UnknownAmetysObjectException e)
379        {
380            return null;
381        }
382    }
383    
384    /**
385     * Get the ODF root page, either in the given site if it exists, or in the default ODF site.
386     * @param siteName the desired site name.
387     * @param language the sitemap language to search in.
388     * @param catalog The ODF catalog
389     * @return the ODF root page, either in the given site if it exists, or in the default ODF site.
390     */
391    public Page getOdfRootPage(String siteName, String language, String catalog)
392    {
393        Page odfRootPage = null;
394        
395        if (StringUtils.isNotEmpty(siteName))
396        {
397            odfRootPage = _odfPageHandler.getOdfRootPage(siteName, language, catalog);
398        }
399        
400        if (odfRootPage == null)
401        {
402            String odfSiteName = Config.getInstance().getValue("odf.web.site.name");
403            odfRootPage = _odfPageHandler.getOdfRootPage(odfSiteName, language, catalog);
404        }
405        
406        return odfRootPage;
407    }
408
409    /**
410     * Get the path in sitemap of a ODF content into a {@link Program} or {@link SubProgram}<br>
411     * Be careful, this is the path in sitemap, to get the path of a item into a Program, use {@link ODFHelper#getPathInProgram} instead.
412     * @param programItem The program item
413     * @param parentProgram The parent root (sub)program. Can be null.
414     * @return the path in sitemap from the parent program or null if no found ODF path for those program item and parent program
415     */
416    public String getPathInProgram (ProgramItem programItem, AbstractProgram parentProgram)
417    {
418        Cache<PathInProgramCacheKey, String> cache = _cacheManager.get(__PATH_IN_SITEMAP_CACHE);
419        
420        return cache.get(PathInProgramCacheKey.of(programItem.getId(), parentProgram != null ? parentProgram.getId() : "__NOPARENT"), item -> {
421            
422            if (programItem instanceof Program || programItem.equals(parentProgram))
423            {
424                // The program item is already the program it self
425                return _odfPageHandler.getPageName(programItem);
426            }
427            
428            List<String> paths = new ArrayList<>();
429            
430            // Add the parent path in program if exists
431            ProgramItem parent = _odfHelper.getParentProgramItem(programItem, parentProgram);
432            if (parent != null)
433            {
434                String parentPath = getPathInProgram(parent, parentProgram);
435                if (parentPath != null)
436                {
437                    paths.add(parentPath);
438                }
439            }
440            
441            // Add the current page name (if it is an item with a page (only course, subprogram and program)
442            if (programItem instanceof AbstractProgram || programItem instanceof Course)
443            {
444                paths.add(_odfPageHandler.getPageName(programItem));
445            }
446            
447            // If the path is empty, return null
448            return paths.isEmpty() ? null : StringUtils.join(paths, "/");
449        });
450    }
451    
452    private static class PathInProgramCacheKey extends AbstractCacheKey
453    {
454        protected PathInProgramCacheKey(String programItemId, String parentProgramId)
455        {
456            super(programItemId, parentProgramId);
457        }
458        
459        public static PathInProgramCacheKey of(String programItemId, String parentProgramId)
460        {
461            return new PathInProgramCacheKey(programItemId, parentProgramId);
462        }
463    }
464    
465    /**
466     * Returns the first {@link Program} ancestor, ensuring that the given parent content 'parentProgram' is in the hierarchy, if not null.<br>
467     * If 'parentProgram' is null, the first {@link Program} ancestor will be returned regardless of parent hierarchy.<br>
468     * If 'parentProgram' is a {@link SubProgram}, the first {@link Program} ancestor from this {@link SubProgram} will be returned regardless of parent hierarchy
469     * @param programItem a {@link ProgramItem}
470     * @param parentProgram The parent program or subprogram. Can be null.
471     * @return the parent {@link Program} into this (sub)program or null if not found
472     */
473    public Program getParentProgram (ProgramItem programItem, AbstractProgram parentProgram)
474    {
475        AbstractProgram parent = parentProgram;
476        
477        ProgramItem parentItem = _odfHelper.getParentProgramItem(programItem, parentProgram);
478        while (parentItem != null && !(parentItem instanceof Program))
479        {
480            if (parent != null && parentItem.equals(parent))
481            {
482                // Once the desired abstract program parent is passed, the parent is null
483                parent = null;
484            }
485            parentItem = _odfHelper.getParentProgramItem(parentItem, parent);
486        }
487        
488        return parentItem != null ? (Program) parentItem : null;
489    }
490    
491    /**
492     * Returns the nearest {@link AbstractProgram} ancestors.
493     * @param programPart a {@link ProgramPart}
494     * @return the nearest {@link AbstractProgram} ancestors containing this program part
495     */
496    public List<AbstractProgram> getNearestAncestorAbstractPrograms (ProgramPart programPart)
497    {
498        List<AbstractProgram> ancestors = new ArrayList<>();
499        
500        List<ProgramPart> parents = programPart.getProgramPartParents();
501        for (ProgramPart parent : parents)
502        {
503            if (parent instanceof AbstractProgram)
504            {
505                ancestors.add((AbstractProgram) parent);
506            }
507            else
508            {
509                ancestors.addAll(getNearestAncestorAbstractPrograms(parent));
510            }
511        }
512        
513        return ancestors;
514    }
515    
516    /**
517     * Returns the nearest {@link AbstractProgram} ancestor.
518     * @param programItem a {@link ProgramItem}
519     * @param parentProgram The parent program or subprogram
520     * @return the nearest {@link AbstractProgram} ancestor into this (sub)program or null if not found
521     */
522    public AbstractProgram getNearestAncestorAbstractProgram (ProgramItem programItem, AbstractProgram parentProgram)
523    {
524        ProgramItem parentItem = _odfHelper.getParentProgramItem(programItem, parentProgram);
525        while (parentItem != null && !(parentItem instanceof AbstractProgram))
526        {
527            parentItem = _odfHelper.getParentProgramItem(parentItem, parentProgram);
528        }
529        
530        return parentItem != null ? (AbstractProgram) parentItem : null;
531    }
532    
533    /**
534     * Returns the nearest {@link Course} ancestor.
535     * @param course a {@link Course}
536     * @param parentProgram The parent program or subprogram
537     * @return the nearest {@link Course} ancestor into this (sub)program or null if not found
538     */
539    public Course getNearestAncestorCourse (Course course, AbstractProgram parentProgram)
540    {
541        ProgramItem parentItem = _odfHelper.getParentProgramItem(course, parentProgram);
542        while (parentItem != null && !(parentItem instanceof Course) && !(parentItem instanceof AbstractProgram))
543        {
544            parentItem = _odfHelper.getParentProgramItem(parentItem, parentProgram);
545        }
546        
547        return parentItem != null && parentItem instanceof Course ? (Course) parentItem : null;
548    }
549    
550    /**
551     * Get the path of a {@link ProgramItem} page into the given {@link Program}
552     * @param siteName the site name
553     * @param language the language
554     * @param programItem the subprogram.
555     * @param parentProgram The parent program
556     * @return the page path or empty if no page matches
557     */
558    public String getProgramItemPagePath(String siteName, String language, ProgramItem programItem, Program parentProgram)
559    {
560        String catalog = programItem.getCatalog();
561        
562        // Get the path with the language, path to odf root page, and levels, finishing by a /
563        Cache<ODFLevelsPathCacheKey, String> cache = _cacheManager.get(__LEVELS_PATH_CACHE);
564        String rootPath = cache.get(ODFLevelsPathCacheKey.of(siteName, language, catalog, parentProgram.getId()), key -> _getLevelsPath(siteName, language, programItem.getCatalog(), parentProgram));
565        
566        if (rootPath != null)
567        {
568            // Add the path from the parent program to the program item
569            String pathInProgram = getPathInProgram(programItem, parentProgram);
570            if (pathInProgram != null)
571            {
572                return rootPath + pathInProgram;
573            }
574        }
575        
576        return StringUtils.EMPTY;
577    }
578    
579    private String _getLevelsPath(String siteName, String language, String catalog, Program parentProgram)
580    {
581        // Get the ODF root page for given site, language and catalog
582        Page rootPage = _odfPageHandler.getOdfRootPage(siteName, language, catalog);
583        
584        StringBuilder sb = new StringBuilder()
585            // Sitemap (language)
586            .append(rootPage.getSitemapName()).append('/')
587            // Path from sitemap to ODF root page
588            .append(rootPage.getPathInSitemap()).append('/');
589        
590        // Add level1 if defined
591        String level1 = _odfPageHandler.getLevel1PageName(rootPage, parentProgram);
592        if (StringUtils.isNotEmpty(level1))
593        {
594            sb.append(level1).append('/');
595        }
596        
597        // Add level2 if defined
598        String level2 = _odfPageHandler.getLevel2PageName(rootPage, parentProgram);
599        if (StringUtils.isNotEmpty(level1))
600        {
601            sb.append(level2).append('/');
602        }
603        
604        return sb.toString();
605    }
606    
607    private static class ODFLevelsPathCacheKey extends AbstractCacheKey
608    {
609        protected ODFLevelsPathCacheKey(String site, String lang, String catalog, String parentProgramId)
610        {
611            super(site, lang, catalog, parentProgramId);
612        }
613        
614        public static ODFLevelsPathCacheKey of(String site, String lang, String catalog, String parentProgramId)
615        {
616            return new ODFLevelsPathCacheKey(site, lang, catalog, parentProgramId);
617        }
618    }
619}