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