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