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.runtime.config.Config;
054import org.ametys.web.WebConstants;
055import org.ametys.web.repository.page.Page;
056import org.ametys.web.repository.sitemap.Sitemap;
057import org.ametys.web.transformation.xslt.AmetysXSLTHelper;
058
059/**
060 * Helper component to be used from XSL stylesheets.
061 */
062public class OdfXSLTHelper extends org.ametys.odf.OdfXSLTHelper implements Contextualizable
063{
064    /** The ODF page handler */
065    protected static OdfPageHandler _odfPageHandler;
066    
067    /** The ODF skills helper */
068    protected static ODFSkillsHelper _odfSkillsHelper;
069    
070    /** The avalon context */
071    protected static Context _context;
072    
073    /**
074     * Enum result to determine if a path is part of the current context
075     */
076    public static enum EducationaPathContextResult
077    {
078        /** If the path does not belong to the current context */
079        FALSE,
080        /** If the path does belong to the current context but we can't exactly determined the context */
081        TRUE,
082        /** If the path does belong to the current context */
083        TRUE_EXACTLY
084    }
085    
086    @Override
087    public void service(ServiceManager smanager) throws ServiceException
088    {
089        super.service(smanager);
090        _odfPageHandler = (OdfPageHandler) smanager.lookup(OdfPageHandler.ROLE);
091        _odfSkillsHelper = (ODFSkillsHelper) smanager.lookup(ODFSkillsHelper.ROLE);
092    }
093    
094    public void contextualize(Context context) throws ContextException
095    {
096        _context = context;
097    }
098    
099    /**
100     * Get the ODF root page, for a specific site, language.
101     * If there is many ODF root pages, the first page of the list is returned.
102     * @param siteName the desired site name.
103     * @param language the sitemap language to search in.
104     * @return the first ODF root page, or null if not found
105     */
106    public static String odfRootPage(String siteName, String language)
107    {
108        Page odfRootPage = _odfPageHandler.getOdfRootPage(siteName, language);
109        if (odfRootPage != null)
110        {
111            return odfRootPage.getId();
112        }
113        return null;
114    }
115    
116    /**
117     * Get the ODF root page, for a specific site, language and catalog.
118     * @param siteName the desired site name.
119     * @param language the sitemap language to search in.
120     * @param catalog The ODF catalog
121     * @return the ODF root page, or null if not found
122     */
123    public static String odfRootPage(String siteName, String language, String catalog)
124    {
125        Page odfRootPage = _odfPageHandler.getOdfRootPage(siteName, language, catalog);
126        if (odfRootPage != null)
127        {
128            return odfRootPage.getId();
129        }
130        return null;
131    }
132    
133    /**
134     * Get the PDF url of a program or a subprogram
135     * @param contentId The content id
136     * @param siteName The site name
137     * @return the PDF url or empty string if the content is not a {@link Program} or {@link SubProgram}
138     */
139    public static String odfPDFUrl (String contentId, String siteName)
140    {
141        StringBuilder sb = new StringBuilder();
142        
143        Content content = _ametysObjectResolver.resolveById(contentId);
144        if (content instanceof AbstractProgram)
145        {
146            sb.append(AmetysXSLTHelper.uriPrefix())
147                .append("/plugins/odf-web/")
148                .append(siteName)
149                .append("/_content/")
150                .append(content.getName())
151                .append(".pdf");
152        }
153        
154        return sb.toString();
155    }
156
157    /**
158     * Get the id of parent program from the current page
159     * @return the id of parent program or null if not found
160     */
161    public static String parentProgramId()
162    {
163        String pageId = AmetysXSLTHelper.pageId();
164        
165        if (StringUtils.isNotEmpty(pageId))
166        {
167            Page page = _ametysObjectResolver.resolveById(pageId);
168            
169            AmetysObject parent = page.getParent();
170            while (!(parent instanceof Sitemap))
171            {
172                if (parent instanceof ProgramPage)
173                {
174                    return ((ProgramPage) parent).getProgram().getId();
175                }
176                
177                parent = parent.getParent();
178            }
179        }
180        
181        return null;
182    }
183    
184    /**
185     * Get the ECTS of the current course for the current context if present
186     * @return the ECTS or 0 if not found
187     */
188    public static double getCurrentEcts()
189    {
190        Pair<ProgramItem, List<EducationalPath>> currentEducationalPaths = _getCurrentEducationalPaths();
191        if (currentEducationalPaths != null)
192        {
193            ProgramItem programItem = currentEducationalPaths.getLeft();
194            if (programItem instanceof Course course)
195            {
196                return course.getEcts(currentEducationalPaths.getRight());
197            }
198        }
199        
200        return 0;
201    }
202    
203    /**
204     * Get the Skills of the current course for the current context if present
205     * @return the MicroSkills by MacroSkills or null if not found
206     */
207    public static NodeList getCurrentSkills()
208    {
209        List<Element> result = new ArrayList<>();
210        
211        // If skills not enabled, return empty results
212        boolean isSkillsEnabled = Config.getInstance().getValue("odf.skills.enabled");
213        if (!isSkillsEnabled)
214        {
215            return new AmetysNodeList(result);
216        }
217        
218        Pair<ProgramItem, List<EducationalPath>> currentEducationalPaths = _getCurrentEducationalPaths();
219        if (currentEducationalPaths != null)
220        {
221            ProgramItem programItem = currentEducationalPaths.getLeft();
222            List<EducationalPath> educationalPath = currentEducationalPaths.getRight();
223            if (programItem instanceof Course course && !educationalPath.isEmpty())
224            {
225                EducationalPath programEducationalPath = educationalPath.getFirst();
226                String programId = programEducationalPath.getProgramItemIds().get(0);
227                ContentValue[] skillsValues = course.getAcquiredSkills(programId);
228
229                Map<String, Element> createdMacroSkillNode = new HashMap<>();
230
231                for (ContentValue skillValue : skillsValues)
232                {
233                    Optional<ModifiableContent> optionalContent = skillValue.getContentIfExists();
234                    if (optionalContent.isPresent())
235                    {
236                        Content skillContent = optionalContent.get();
237
238                        ContentValue parentMacroSkillValue = skillContent.getValue("parentMacroSkill");
239                        if (parentMacroSkillValue != null)
240                        {
241                            Optional<ModifiableContent> optionalParentMacroSkill = parentMacroSkillValue.getContentIfExists();
242                            if (optionalParentMacroSkill.isPresent())
243                            {
244                                ModifiableContent parentMacroSkill = optionalParentMacroSkill.get();
245                                
246                                String parentMacroSkillId = parentMacroSkill.getId();
247                                Map<String, String> macroSkillMap = new HashMap<>();
248                                if (!createdMacroSkillNode.containsKey(parentMacroSkillId))
249                                {
250                                    macroSkillMap.put("id", parentMacroSkillId);
251                                    macroSkillMap.put("title", parentMacroSkill.getTitle());
252                                    macroSkillMap.put("type", "MACROSKILL");
253                                    macroSkillMap.put("parentProgram", programId);
254                                    
255                                    Element parentNode = new StringElement("skill", macroSkillMap);
256                                    
257                                    createdMacroSkillNode.put(parentMacroSkillId, parentNode);
258                                    
259                                    result.add(parentNode);
260                                }
261                                
262                                Map<String, String> skillMap = new HashMap<>();
263                                skillMap.put("id", skillContent.getId());
264                                skillMap.put("title", skillContent.getTitle());
265                                skillMap.put("type", "MICROSKILL");
266                                skillMap.put("parent", parentMacroSkillId);
267                                
268                                Element node = new StringElement("skill", skillMap);
269                                result.add(node);
270                            }
271                        }
272                    }
273                }
274            }
275        }
276
277        return new AmetysNodeList(result);
278    }
279    
280    private static Pair<ProgramItem, List<EducationalPath>> _getCurrentEducationalPaths()
281    {
282        Request request = ContextHelper.getRequest(_context);
283        Page page = (Page) request.getAttribute(WebConstants.REQUEST_ATTR_PAGE);
284        
285        // First try to get current educational paths from course page if present
286        if (page != null)
287        {
288            if (page instanceof CoursePage coursePage)
289            {
290                Course course = coursePage.getContent();
291                return Pair.of(course, course.getCurrentEducationalPaths());
292            }
293            else if (page instanceof ProgramPage programPage)
294            {
295                AbstractProgram abstractProgram = programPage.getContent();
296                return Pair.of(abstractProgram, abstractProgram.getCurrentEducationalPaths());
297            }
298        }
299        
300        // Then try to get current educational paths from course content if present
301        Content content = (Content) request.getAttribute(Content.class.getName());
302        return _getCurrentEducationalPaths(content);
303    }
304    
305    private static Pair<ProgramItem, List<EducationalPath>> _getCurrentEducationalPaths(Content content)
306    {
307        Request request = ContextHelper.getRequest(_context);
308        if (content != null && (content instanceof Course || content instanceof AbstractProgram))
309        {
310            // First try to get educational paths from content
311            List<EducationalPath> currentEducationalPaths = content instanceof Course course ? course.getCurrentEducationalPaths() : ((AbstractProgram) content).getCurrentEducationalPaths();
312            if (currentEducationalPaths == null)
313            {
314                // If null try to get educational paths from request attributes
315                @SuppressWarnings("unchecked")
316                List<ProgramItem> pathFromRequest = (List<ProgramItem>) request.getAttribute(EducationalPathHelper.PROGRAM_ITEM_ANCESTOR_PATH_REQUEST_ATTR);
317                if (pathFromRequest != null)
318                {
319                    // In request the path may be a partial path
320                    currentEducationalPaths = _odfHelper.getEducationPathFromPath(pathFromRequest);
321                    
322                    // If ancestor is present in request attribute, filter paths that contains this ancestor
323                    ProgramItem ancestor = (ProgramItem) request.getAttribute(EducationalPathHelper.ROOT_PROGRAM_ITEM_REQUEST_ATTR);
324                    if (ancestor != null)
325                    {
326                        currentEducationalPaths = currentEducationalPaths.stream()
327                                .filter(p -> p.getProgramItemIds().contains(ancestor.getId()))
328                                .toList();
329                    }
330                }
331                else
332                {
333                    // Cannot determine current educational paths from context, returns all available education paths
334                    currentEducationalPaths = _odfHelper.getEducationalPaths((ProgramItem) content, true, true);
335                }
336            }
337            
338            return Pair.of((ProgramItem) content, currentEducationalPaths);
339        }
340        
341        return null;
342    }
343    
344    /**
345     * Determines if a given {@link EducationalPath} is part of the current {@link EducationalPath}s<br>
346     * - TRUE_EXACTLY if the education path belongs to the current educational path and the current educational path is unique
347     * - TRUE if the education path belongs to the current educational paths
348     * - FALSE if the education path does not belong to the current educational paths
349     * @param educationPath the education path as string
350     * @return the status of the entry path belonging to the current context or not
351     */
352    public static String isPartOfCurrentEducationalPaths(String educationPath)
353    {
354        return _isPartOfCurrentEducationalPaths(educationPath).name();
355    }
356    
357    private static EducationaPathContextResult _isPartOfCurrentEducationalPaths(String educationPath)
358    {
359        EducationalPath educationalPath = EducationalPath.of(educationPath.split(EducationalPath.PATH_SEGMENT_SEPARATOR));
360        
361        List<EducationalPath> currentEducationalPaths = _getCurrentEducationalPaths().getRight();
362        if (currentEducationalPaths.contains(educationalPath))
363        {
364            return currentEducationalPaths.size() > 1
365                    ? EducationaPathContextResult.TRUE // Cannot determine the current context precisely
366                    : EducationaPathContextResult.TRUE_EXACTLY; // Only one educational path possible
367        }
368        
369        return EducationaPathContextResult.FALSE; // no part of current education paths
370    }
371    
372    /**
373     * Extract educational path of main structure that the given education path belongs to. The path stops to the last parent container of type year or the last subprogram if the year does not exist.
374     * @param educationalPath the educational path as string
375     * @return the readable education path of main structure
376     */
377    public static String getMainStructureEducationalPathAsString(String educationalPath)
378    {
379        EducationalPath educationPath = EducationalPath.of(educationalPath.split(EducationalPath.PATH_SEGMENT_SEPARATOR));
380        return educationPath != null ? _odfHelper.getEducationalPathAsString(educationPath, pi -> ((Content) pi).getTitle(), " > ", pi -> _filterProgramItemInPath(pi)) : null;
381    }
382    
383    private static boolean _filterProgramItemInPath(ProgramItem p)
384    {
385        return p instanceof AbstractProgram || _odfHelper.isContainerOfTypeYear((Content) p);
386    }
387    
388    /**
389     * Get the ECTS of the current course for the current context if present
390     * @param defaultValue The default value
391     * @return the ECTS or 0 if not found
392     */
393    public static double getCurrentEcts(String defaultValue)
394    {
395        double currentEcts = getCurrentEcts();
396        return currentEcts != 0 ? currentEcts : (StringUtils.isNotEmpty(defaultValue) ? Double.valueOf(defaultValue) : 0);
397    }
398    
399    /**
400     * Determines if the values of ECTS is equals for the course's educational paths in the current context
401     * @param courseId The course id
402     * @return true if the values of ECTS is equals in the current context
403     */
404    public static boolean areECTSEqual(String courseId)
405    {
406        Course course = _ametysObjectResolver.resolveById(courseId);
407        return _areECTSEqual(course);
408    }
409    
410    /**
411     * Determines if the values of ECTS is equals for the current course's educational paths in the current context
412     * @return true if the values of ECTS is equals in the current context
413     */
414    public static boolean areECTSEqual()
415    {
416        Request request = ContextHelper.getRequest(_context);
417        Content content = (Content) request.getAttribute(Content.class.getName());
418        if (content != null && content instanceof Course course)
419        {
420            return _areECTSEqual(course);
421        }
422        return false;
423    }
424    
425    private static boolean _areECTSEqual(Course course)
426    {
427        Pair<ProgramItem, List<EducationalPath>> currentEducationalPaths = _getCurrentEducationalPaths(course);
428        if (currentEducationalPaths != null)
429        {
430            ProgramItem programItem = currentEducationalPaths.getLeft();
431            List<EducationalPath> paths = currentEducationalPaths.getRight();
432            return paths != null ? _odfHelper.isSameValueForPaths(programItem, Course.ECTS_BY_PATH, paths) : _odfHelper.isSameValueForAllPaths(programItem, Course.ECTS_BY_PATH);
433        }
434        else
435        {
436            return _odfHelper.isSameValueForAllPaths(course, Course.ECTS_BY_PATH);
437        }
438    }
439    
440    /**
441     * <code>true</code> if the program item is part of an program item (program, subprogram or container) that is excluded from skills
442     * @param programItemId the program item id
443     * @param programPageItemId the program item page id. If null or empty, program item is display with no context, consider that skills are available
444     * @return <code>true</code> if the program item has an excluded parent in it path from the page context
445     */
446    public static boolean areSkillsUnavailable(String programItemId, String programPageItemId)
447    {
448        if (StringUtils.isBlank(programItemId) || StringUtils.isBlank(programPageItemId))
449        {
450            // program part is displayed outside a page context, assuming that skills should be displayed
451            return false;
452        }
453        
454        ProgramItem programItem = _ametysObjectResolver.resolveById(programItemId);
455        if (programItem instanceof Program)
456        {
457            return _odfSkillsHelper.isExcluded(programItem);
458        }
459        
460        Page programItemPage = _ametysObjectResolver.resolveById(programPageItemId);
461        
462        ProgramPage closestProgramPage = _getClosestProgramPage(programItemPage);
463        AbstractProgram closestProgramOrSubprogram = closestProgramPage.getProgram();
464        
465        ProgramItem parent = _odfHelper.getParentProgramItem(programItem, closestProgramOrSubprogram);
466        while (parent != null && !(parent instanceof Program))
467        {
468            if (_odfSkillsHelper.isExcluded(parent))
469            {
470                // If the parent is excluded, the skills are unavailable
471                return true;
472            }
473            
474            // If the closest program parent is a subprogram, continue to its program parent
475            if (closestProgramOrSubprogram instanceof SubProgram && closestProgramOrSubprogram.equals(parent))
476            {
477                closestProgramOrSubprogram = ((ProgramPage) closestProgramPage.getParent()).getProgram();
478            }
479            parent = _odfHelper.getParentProgramItem(parent, closestProgramOrSubprogram);
480        }
481        
482        return parent != null ? _odfSkillsHelper.isExcluded(parent) : false;
483    }
484    
485    private static ProgramPage _getClosestProgramPage(Page page)
486    {
487        Page parentPage = page.getParent();
488        while (!(parentPage instanceof ProgramPage))
489        {
490            parentPage = parentPage.getParent();
491        }
492        
493        return (ProgramPage) parentPage;
494    }
495}