001/*
002 *  Copyright 2015 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.xslt;
017
018import java.util.List;
019
020import org.apache.avalon.framework.context.Context;
021import org.apache.avalon.framework.context.ContextException;
022import org.apache.avalon.framework.context.Contextualizable;
023import org.apache.avalon.framework.service.ServiceException;
024import org.apache.avalon.framework.service.ServiceManager;
025import org.apache.cocoon.components.ContextHelper;
026import org.apache.cocoon.environment.Request;
027import org.apache.commons.lang.StringUtils;
028import org.apache.commons.lang3.tuple.Pair;
029
030import org.ametys.cms.repository.Content;
031import org.ametys.odf.EducationalPathHelper;
032import org.ametys.odf.ProgramItem;
033import org.ametys.odf.course.Course;
034import org.ametys.odf.data.EducationalPath;
035import org.ametys.odf.program.AbstractProgram;
036import org.ametys.odf.program.Program;
037import org.ametys.odf.program.SubProgram;
038import org.ametys.odf.skill.ODFSkillsHelper;
039import org.ametys.plugins.odfweb.repository.CoursePage;
040import org.ametys.plugins.odfweb.repository.OdfPageHandler;
041import org.ametys.plugins.odfweb.repository.ProgramPage;
042import org.ametys.plugins.repository.AmetysObject;
043import org.ametys.web.WebConstants;
044import org.ametys.web.repository.page.Page;
045import org.ametys.web.repository.sitemap.Sitemap;
046import org.ametys.web.transformation.xslt.AmetysXSLTHelper;
047
048/**
049 * Helper component to be used from XSL stylesheets.
050 */
051public class OdfXSLTHelper extends org.ametys.odf.OdfXSLTHelper implements Contextualizable
052{
053    /** The ODF page handler */
054    protected static OdfPageHandler _odfPageHandler;
055    
056    /** The ODF skills helper */
057    protected static ODFSkillsHelper _odfSkillsHelper;
058    
059    /** The avalon context */
060    protected static Context _context;
061    
062    @Override
063    public void service(ServiceManager smanager) throws ServiceException
064    {
065        super.service(smanager);
066        _odfPageHandler = (OdfPageHandler) smanager.lookup(OdfPageHandler.ROLE);
067        _odfSkillsHelper = (ODFSkillsHelper) smanager.lookup(ODFSkillsHelper.ROLE);
068    }
069    
070    public void contextualize(Context context) throws ContextException
071    {
072        _context = context;
073    }
074    
075    /**
076     * Get the ODF root page, for a specific site, language.
077     * If there is many ODF root pages, the first page of the list is returned.
078     * @param siteName the desired site name.
079     * @param language the sitemap language to search in.
080     * @return the first ODF root page, or null if not found
081     */
082    public static String odfRootPage(String siteName, String language)
083    {
084        Page odfRootPage = _odfPageHandler.getOdfRootPage(siteName, language);
085        if (odfRootPage != null)
086        {
087            return odfRootPage.getId();
088        }
089        return null;
090    }
091    
092    /**
093     * Get the ODF root page, for a specific site, language and catalog.
094     * @param siteName the desired site name.
095     * @param language the sitemap language to search in.
096     * @param catalog The ODF catalog
097     * @return the ODF root page, or null if not found
098     */
099    public static String odfRootPage(String siteName, String language, String catalog)
100    {
101        Page odfRootPage = _odfPageHandler.getOdfRootPage(siteName, language, catalog);
102        if (odfRootPage != null)
103        {
104            return odfRootPage.getId();
105        }
106        return null;
107    }
108    
109    /**
110     * Get the PDF url of a program or a subprogram
111     * @param contentId The content id
112     * @param siteName The site name
113     * @return the PDF url or empty string if the content is not a {@link Program} or {@link SubProgram}
114     */
115    public static String odfPDFUrl (String contentId, String siteName)
116    {
117        StringBuilder sb = new StringBuilder();
118        
119        Content content = _ametysObjectResolver.resolveById(contentId);
120        if (content instanceof AbstractProgram)
121        {
122            sb.append(AmetysXSLTHelper.uriPrefix())
123                .append("/plugins/odf-web/")
124                .append(siteName)
125                .append("/_content/")
126                .append(content.getName())
127                .append(".pdf");
128        }
129        
130        return sb.toString();
131    }
132
133    /**
134     * Get the id of parent program from the current page
135     * @return the id of parent program or null if not found
136     */
137    public static String parentProgramId()
138    {
139        String pageId = AmetysXSLTHelper.pageId();
140        
141        if (StringUtils.isNotEmpty(pageId))
142        {
143            Page page = _ametysObjectResolver.resolveById(pageId);
144            
145            AmetysObject parent = page.getParent();
146            while (!(parent instanceof Sitemap))
147            {
148                if (parent instanceof ProgramPage)
149                {
150                    return ((ProgramPage) parent).getProgram().getId();
151                }
152                
153                parent = parent.getParent();
154            }
155        }
156        
157        return null;
158    }
159    
160    /**
161     * Get the ECTS of the current course for the current context if present
162     * @return the ECTS or 0 if not found
163     */
164    public static double getCurrentEcts()
165    {
166        Pair<ProgramItem, List<EducationalPath>> currentEducationalPaths = _getCurrentEducationalPaths();
167        if (currentEducationalPaths != null)
168        {
169            ProgramItem programItem = currentEducationalPaths.getLeft();
170            if (programItem instanceof Course course)
171            {
172                return course.getEcts(currentEducationalPaths.getRight());
173            }
174        }
175        
176        return 0;
177    }
178    
179    private static Pair<ProgramItem, List<EducationalPath>> _getCurrentEducationalPaths()
180    {
181        Request request = ContextHelper.getRequest(_context);
182        Page page = (Page) request.getAttribute(WebConstants.REQUEST_ATTR_PAGE);
183        
184        // First try to get current educational paths from course page if present
185        if (page != null)
186        {
187            if (page instanceof CoursePage coursePage)
188            {
189                Course course = coursePage.getContent();
190                return Pair.of(course, course.getCurrentEducationalPaths());
191            }
192            else if (page instanceof ProgramPage programPage)
193            {
194                AbstractProgram abstractProgram = programPage.getContent();
195                return Pair.of(abstractProgram, abstractProgram.getCurrentEducationalPaths());
196            }
197        }
198        
199        // Then try to get current educational paths from course content if present
200        Content content = (Content) request.getAttribute(Content.class.getName());
201        return _getCurrentEducationalPaths(content);
202    }
203    
204    private static Pair<ProgramItem, List<EducationalPath>> _getCurrentEducationalPaths(Content content)
205    {
206        Request request = ContextHelper.getRequest(_context);
207        if (content != null && (content instanceof Course || content instanceof AbstractProgram))
208        {
209            // First try to get educational paths from content
210            List<EducationalPath> currentEducationalPaths = content instanceof Course course ? course.getCurrentEducationalPaths() : ((AbstractProgram) content).getCurrentEducationalPaths();
211            if (currentEducationalPaths == null)
212            {
213                // If null try to get educational paths from request attributes
214                @SuppressWarnings("unchecked")
215                List<ProgramItem> pathFromRequest = (List<ProgramItem>) request.getAttribute(EducationalPathHelper.PROGRAM_ITEM_ANCESTOR_PATH_REQUEST_ATTR);
216                if (pathFromRequest != null)
217                {
218                    // In request the path may be a partial path
219                    currentEducationalPaths = _odfHelper.getEducationPathFromPath(pathFromRequest);
220                    
221                    // If ancestor is present in request attribute, filter paths that contains this ancestor
222                    ProgramItem ancestor = (ProgramItem) request.getAttribute(EducationalPathHelper.ROOT_PROGRAM_ITEM_REQUEST_ATTR);
223                    if (ancestor != null)
224                    {
225                        currentEducationalPaths = currentEducationalPaths.stream()
226                                .filter(p -> p.getProgramItemIds().contains(ancestor.getId()))
227                                .toList();
228                    }
229                }
230                else
231                {
232                    // Cannot determine current educational paths from context, returns all available education paths
233                    currentEducationalPaths = _odfHelper.getEducationalPaths((ProgramItem) content, true, true);
234                }
235            }
236            
237            return Pair.of((ProgramItem) content, currentEducationalPaths);
238        }
239        
240        return null;
241    }
242    
243    /**
244     * Get the ECTS of the current course for the current context if present
245     * @param defaultValue The default value
246     * @return the ECTS or 0 if not found
247     */
248    public static double getCurrentEcts(String defaultValue)
249    {
250        double currentEcts = getCurrentEcts();
251        return currentEcts != 0 ? currentEcts : (StringUtils.isNotEmpty(defaultValue) ? Double.valueOf(defaultValue) : 0);
252    }
253    
254    /**
255     * Determines if the values of ECTS is equals for the course's educational paths in the current context
256     * @param courseId The course id
257     * @return true if the values of ECTS is equals in the current context
258     */
259    public static boolean areECTSEqual(String courseId)
260    {
261        Course course = _ametysObjectResolver.resolveById(courseId);
262        return _areECTSEqual(course);
263    }
264    
265    /**
266     * Determines if the values of ECTS is equals for the current course's educational paths in the current context
267     * @return true if the values of ECTS is equals in the current context
268     */
269    public static boolean areECTSEqual()
270    {
271        Request request = ContextHelper.getRequest(_context);
272        Content content = (Content) request.getAttribute(Content.class.getName());
273        if (content != null && content instanceof Course course)
274        {
275            return _areECTSEqual(course);
276        }
277        return false;
278    }
279    
280    private static boolean _areECTSEqual(Course course)
281    {
282        Pair<ProgramItem, List<EducationalPath>> currentEducationalPaths = _getCurrentEducationalPaths(course);
283        if (currentEducationalPaths != null)
284        {
285            ProgramItem programItem = currentEducationalPaths.getLeft();
286            List<EducationalPath> paths = currentEducationalPaths.getRight();
287            return paths != null ? _odfHelper.isSameValueForPaths(programItem, Course.ECTS_BY_PATH, paths) : _odfHelper.isSameValueForAllPaths(programItem, Course.ECTS_BY_PATH);
288        }
289        else
290        {
291            return _odfHelper.isSameValueForAllPaths(course, Course.ECTS_BY_PATH);
292        }
293    }
294    
295    /**
296     * <code>true</code> if the program item is part of an program item (program, subprogram or container) that is excluded from skills
297     * @param programItemId the program item id
298     * @param programPageItemId the program item page id. If null or empty, program item is display with no context, consider that skills are available
299     * @return <code>true</code> if the program item has an excluded parent in it path from the page context
300     */
301    public static boolean areSkillsUnavailable(String programItemId, String programPageItemId)
302    {
303        if (StringUtils.isBlank(programItemId) || StringUtils.isBlank(programPageItemId))
304        {
305            // program part is displayed outside a page context, assuming that skills should be displayed
306            return false;
307        }
308        
309        ProgramItem programItem = _ametysObjectResolver.resolveById(programItemId);
310        if (programItem instanceof Program)
311        {
312            return _odfSkillsHelper.isExcluded(programItem);
313        }
314        
315        Page programItemPage = _ametysObjectResolver.resolveById(programPageItemId);
316        
317        ProgramPage closestProgramPage = _getClosestProgramPage(programItemPage);
318        AbstractProgram closestProgramOrSubprogram = closestProgramPage.getProgram();
319        
320        ProgramItem parent = _odfHelper.getParentProgramItem(programItem, closestProgramOrSubprogram);
321        while (parent != null && !(parent instanceof Program))
322        {
323            if (_odfSkillsHelper.isExcluded(parent))
324            {
325                // If the parent is excluded, the skills are unavailable
326                return true;
327            }
328            
329            // If the closest program parent is a subprogram, continue to its program parent
330            if (closestProgramOrSubprogram instanceof SubProgram && closestProgramOrSubprogram.equals(parent))
331            {
332                closestProgramOrSubprogram = ((ProgramPage) closestProgramPage.getParent()).getProgram();
333            }
334            parent = _odfHelper.getParentProgramItem(parent, closestProgramOrSubprogram);
335        }
336        
337        return parent != null ? _odfSkillsHelper.isExcluded(parent) : false;
338    }
339    
340    private static ProgramPage _getClosestProgramPage(Page page)
341    {
342        Page parentPage = page.getParent();
343        while (!(parentPage instanceof ProgramPage))
344        {
345            parentPage = parentPage.getParent();
346        }
347        
348        return (ProgramPage) parentPage;
349    }
350}