001/*
002 *  Copyright 2019 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.odf.content;
017
018import java.io.IOException;
019import java.util.ArrayList;
020import java.util.List;
021import java.util.Map;
022import java.util.Optional;
023import java.util.Set;
024
025import org.apache.avalon.framework.activity.Initializable;
026import org.apache.avalon.framework.parameters.Parameters;
027import org.apache.avalon.framework.service.ServiceException;
028import org.apache.avalon.framework.service.ServiceManager;
029import org.apache.cocoon.ProcessingException;
030import org.apache.cocoon.environment.ObjectModelHelper;
031import org.apache.cocoon.environment.Request;
032import org.apache.cocoon.generation.ServiceableGenerator;
033import org.apache.cocoon.xml.AttributesImpl;
034import org.apache.cocoon.xml.SaxBuffer;
035import org.apache.cocoon.xml.XMLUtils;
036import org.apache.commons.lang.StringUtils;
037import org.apache.commons.lang3.LocaleUtils;
038import org.apache.commons.lang3.tuple.Triple;
039import org.xml.sax.SAXException;
040
041import org.ametys.cms.contenttype.ContentTypesHelper;
042import org.ametys.cms.repository.Content;
043import org.ametys.core.cache.AbstractCacheManager;
044import org.ametys.core.cache.Cache;
045import org.ametys.odf.EducationalPathHelper;
046import org.ametys.odf.NoLiveVersionException;
047import org.ametys.odf.ODFHelper;
048import org.ametys.odf.ProgramItem;
049import org.ametys.odf.course.Course;
050import org.ametys.odf.courselist.CourseList;
051import org.ametys.odf.coursepart.CoursePart;
052import org.ametys.odf.data.EducationalPath;
053import org.ametys.odf.enumeration.OdfReferenceTableEntry;
054import org.ametys.odf.enumeration.OdfReferenceTableHelper;
055import org.ametys.odf.program.AbstractProgram;
056import org.ametys.odf.program.Container;
057import org.ametys.odf.program.Program;
058import org.ametys.odf.program.SubProgram;
059import org.ametys.odf.skill.ODFSkillsHelper;
060import org.ametys.plugins.repository.AmetysObjectResolver;
061import org.ametys.plugins.repository.AmetysRepositoryException;
062import org.ametys.plugins.repository.jcr.DefaultAmetysObject;
063import org.ametys.runtime.i18n.I18nizableText;
064import org.ametys.runtime.model.View;
065import org.ametys.runtime.model.exception.BadItemTypeException;
066import org.ametys.runtime.model.type.DataContext;
067
068/**
069 * SAX the structure (ie. the child program items) of a {@link ProgramItem}
070 *
071 */
072public class ProgramItemStructureGenerator extends ServiceableGenerator implements Initializable
073{
074    private static final Set<String> __ALLOWED_VIEW_NAMES = Set.of("main", "pdf");
075    
076    private static final String __REF_ITEM_CACHE_ID = ProgramItemStructureGenerator.class.getName() + "$refItems";
077    private static final String __VIEW_CACHE_ID = ProgramItemStructureGenerator.class.getName() + "$view";
078    private static final String __COMMON_ATTRIBUTES_CACHE_ID = ProgramItemStructureGenerator.class.getName() + "$commonAttributes";
079    
080    /** The ODF helper */
081    protected ODFHelper _odfHelper;
082    /** Helper for ODF reference table */
083    protected OdfReferenceTableHelper _odfReferenceTableHelper;
084    /** The content types helper */
085    protected ContentTypesHelper _cTypesHelper;
086    /** The ODF skills helper */
087    protected ODFSkillsHelper _odfSkillsHelper;
088    /** The Ametys object resolver */
089    protected AmetysObjectResolver _resolver;
090    /** The cache manager */
091    protected AbstractCacheManager _cacheManager;
092    
093    private Cache<Triple<String, String, String>, SaxBuffer> _refItemCache;
094    private Cache<Content, SaxBuffer> _viewCache;
095    private Cache<ProgramItem, CommonAttributes> _commonAttrCache;
096    
097    @Override
098    public void service(ServiceManager smanager) throws ServiceException
099    {
100        _odfHelper = (ODFHelper) smanager.lookup(ODFHelper.ROLE);
101        _odfReferenceTableHelper = (OdfReferenceTableHelper) smanager.lookup(OdfReferenceTableHelper.ROLE);
102        _cTypesHelper = (ContentTypesHelper) smanager.lookup(ContentTypesHelper.ROLE);
103        _odfSkillsHelper = (ODFSkillsHelper) smanager.lookup(ODFSkillsHelper.ROLE);
104        _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
105        _cacheManager = (AbstractCacheManager) smanager.lookup(AbstractCacheManager.ROLE);
106    }
107    
108    @Override
109    public void setup(org.apache.cocoon.environment.SourceResolver res, Map objModel, String src, Parameters par) throws ProcessingException, SAXException, IOException
110    {
111        super.setup(res, objModel, src, par);
112        _refItemCache = _cacheManager.get(__REF_ITEM_CACHE_ID);
113        _viewCache = _cacheManager.get(__VIEW_CACHE_ID);
114        _commonAttrCache = _cacheManager.get(__COMMON_ATTRIBUTES_CACHE_ID);
115    }
116    
117    @Override
118    public void recycle()
119    {
120        super.recycle();
121        _refItemCache = null;
122        _viewCache = null;
123        _commonAttrCache = null;
124    }
125    
126    public void initialize() throws Exception
127    {
128        if (!_cacheManager.hasCache(__REF_ITEM_CACHE_ID))
129        {
130            _cacheManager.createRequestCache(__REF_ITEM_CACHE_ID,
131                                      new I18nizableText("plugin.odf", "PLUGINS_ODF_CACHE_PROGRAM_ITEM_STRUCTURE_REF_ITEMS_LABEL"),
132                                      new I18nizableText("plugin.odf", "PLUGINS_ODF_CACHE_PROGRAM_ITEM_STRUCTURE_REF_ITEMS_DESCRIPTION"),
133                                      false);
134        }
135        
136        if (!_cacheManager.hasCache(__VIEW_CACHE_ID))
137        {
138            _cacheManager.createRequestCache(__VIEW_CACHE_ID,
139                                      new I18nizableText("plugin.odf", "PLUGINS_ODF_CACHE_PROGRAM_ITEM_STRUCTURE_VIEW_LABEL"),
140                                      new I18nizableText("plugin.odf", "PLUGINS_ODF_CACHE_PROGRAM_ITEM_STRUCTURE_VIEW_DESCRIPTION"),
141                                      false);
142        }
143        
144        if (!_cacheManager.hasCache(__COMMON_ATTRIBUTES_CACHE_ID))
145        {
146            _cacheManager.createRequestCache(__COMMON_ATTRIBUTES_CACHE_ID,
147                                      new I18nizableText("plugin.odf", "PLUGINS_ODF_CACHE_PROGRAM_ITEM_STRUCTURE_COMMON_ATTRIBUTES_LABEL"),
148                                      new I18nizableText("plugin.odf", "PLUGINS_ODF_CACHE_PROGRAM_ITEM_STRUCTURE_COMMON_ATTRIBUTES_DESCRIPTION"),
149                                      false);
150        }
151    }
152    
153    public void generate() throws IOException, SAXException, ProcessingException
154    {
155        Request request = ObjectModelHelper.getRequest(objectModel);
156        Content content = (Content) request.getAttribute(Content.class.getName());
157        
158        if (content == null)
159        {
160            String contentId = parameters.getParameter("contentId", null);
161            if (StringUtils.isBlank(contentId))
162            {
163                throw new IllegalArgumentException("Content is missing in request attribute or parameters");
164            }
165            content = _resolver.resolveById(contentId);
166        }
167
168        String viewName = parameters.getParameter("viewName", StringUtils.EMPTY);
169        String fallbackViewName = parameters.getParameter("fallbackViewName", StringUtils.EMPTY);
170        
171        View view = _cTypesHelper.getViewWithFallback(viewName, fallbackViewName, content);
172        
173        contentHandler.startDocument();
174        
175        if (view != null && __ALLOWED_VIEW_NAMES.contains(view.getName()))
176        {
177            if (content instanceof ProgramItem programItem)
178            {
179                XMLUtils.startElement(contentHandler, "structure");
180                
181                List<ProgramItem> initialAncestorPath = _getInitialAncestorPath(request, programItem);
182                saxProgramItem(programItem, initialAncestorPath);
183                
184                XMLUtils.endElement(contentHandler, "structure");
185            }
186            else
187            {
188                getLogger().warn("Cannot get the structure of a non program item '" + content.getId() + "'");
189            }
190        }
191        
192        contentHandler.endDocument();
193    }
194    
195    /**
196     * Get the initial ancestor path from request or from root program item
197     * @param request the request
198     * @param rootProgramItem the root program item in saxed structure
199     * @return the initial ancestor path as a list of program items
200     */
201    protected List<ProgramItem> _getInitialAncestorPath(Request request, ProgramItem rootProgramItem)
202    {
203        // First try to get ancestor path given by request
204        @SuppressWarnings("unchecked")
205        List<ProgramItem> ancestorPath = (List<ProgramItem>) request.getAttribute(EducationalPathHelper.PROGRAM_ITEM_ANCESTOR_PATH_REQUEST_ATTR);
206        
207        if (ancestorPath == null)
208        {
209            if (rootProgramItem instanceof SubProgram subProgram)
210            {
211                List<EducationalPath> subProgramPaths = subProgram.getCurrentEducationalPaths();
212                if (subProgramPaths != null && subProgramPaths.size() == 1)
213                {
214                    // Init the ancestor paths from current educational path only if there is only one eligible educational path
215                    ancestorPath = subProgramPaths.get(0).getProgramItems(_resolver);
216                }
217            }
218            else if (rootProgramItem instanceof Course course)
219            {
220                List<EducationalPath> coursePaths = course.getCurrentEducationalPaths();
221                if (coursePaths != null && coursePaths.size() == 1)
222                {
223                    // Init the ancestor paths from current educational path only if there is only one eligible educational path
224                    ancestorPath = coursePaths.get(0).getProgramItems(_resolver);
225                }
226            }
227        }
228        
229        // Ancestor path cannot be determine by context, initialize the ancestor path to a item itself
230        return ancestorPath == null || ancestorPath.isEmpty() ? List.of(rootProgramItem) : ancestorPath;
231    }
232        
233    /**
234     * SAX a program item with its child program items
235     * @param programItem the program item
236     * @param ancestorPath The path of this program item in the saxed structure (computed from the initial saxed program item). Can be a partial path.
237     * @throws SAXException if an error occurs while saxing
238     */
239    protected void saxProgramItem(ProgramItem programItem, List<ProgramItem> ancestorPath) throws SAXException
240    {
241        if (programItem instanceof Program)
242        {
243            saxProgram((Program) programItem);
244        }
245        else if (programItem instanceof SubProgram)
246        {
247            saxSubProgram((SubProgram) programItem, ancestorPath);
248        }
249        else if (programItem instanceof Container)
250        {
251            saxContainer((Container) programItem, ancestorPath);
252        }
253        else if (programItem instanceof CourseList)
254        {
255            saxCourseList((CourseList) programItem, ancestorPath);
256        }
257        else if (programItem instanceof Course)
258        {
259            saxCourse((Course) programItem, ancestorPath);
260        }
261    }
262    
263    /**
264     * SAX the child program items
265     * @param programItem the program item
266     * @param ancestorPath The path of parent program item in the structure (starting from the initial saxed program item)
267     * @throws SAXException if an error occurs while saxing
268     */
269    protected void saxChildProgramItems(ProgramItem programItem, List<ProgramItem> ancestorPath) throws SAXException
270    {
271        List<ProgramItem> childProgramItems = _odfHelper.getChildProgramItems(programItem);
272        for (ProgramItem childProgramItem : childProgramItems)
273        {
274            try
275            {
276                _odfHelper.switchToLiveVersionIfNeeded((DefaultAmetysObject) childProgramItem);
277                List<ProgramItem> childAncestorPath = new ArrayList<>(ancestorPath);
278                childAncestorPath.add(childProgramItem);
279                
280                saxProgramItem(childProgramItem, childAncestorPath);
281            }
282            catch (NoLiveVersionException e)
283            {
284                // Just ignore the program item
285            }
286        }
287    }
288        
289    /**
290     * SAX a program
291     * @param program the subprogram to SAX
292     * @throws SAXException if an error occurs
293     */
294    protected void saxProgram(Program program) throws SAXException
295    {
296        AttributesImpl attrs = new AttributesImpl();
297        _saxCommonAttributes(program, null, attrs);
298        
299        XMLUtils.startElement(contentHandler, "program", attrs);
300        
301        XMLUtils.startElement(contentHandler, "attributes");
302        _saxStructureViewIfExists(program);
303        XMLUtils.endElement(contentHandler, "attributes");
304        
305        saxChildProgramItems(program, List.of(program));
306        XMLUtils.endElement(contentHandler, "program");
307    }
308    
309    /**
310     * SAX a subprogram
311     * @param subProgram the subprogram to SAX
312     * @param ancestorPath The path of this program item in the saxed structure (computed from the initial saxed program item). Can be a partial path.
313     * @throws SAXException if an error occurs
314     */
315    protected void saxSubProgram(SubProgram subProgram, List<ProgramItem> ancestorPath) throws SAXException
316    {
317        AttributesImpl attrs = new AttributesImpl();
318        _saxCommonAttributes(subProgram, ancestorPath, attrs);
319        
320        XMLUtils.startElement(contentHandler, "subprogram", attrs);
321        
322        XMLUtils.startElement(contentHandler, "attributes");
323        _saxReferenceTableItem(subProgram.getEcts(), AbstractProgram.ECTS, subProgram.getLanguage());
324        _saxStructureViewIfExists(subProgram);
325        XMLUtils.endElement(contentHandler, "attributes");
326        
327        saxChildProgramItems(subProgram, ancestorPath);
328        
329        XMLUtils.endElement(contentHandler, "subprogram");
330    }
331    
332    /**
333     * SAX a container
334     * @param container the container to SAX
335     * @param ancestorPath The path of this program item in the saxed structure (computed from the initial saxed program item). Can be a partial path.
336     * @throws SAXException if an error occurs while saxing
337     */
338    protected void saxContainer(Container container, List<ProgramItem> ancestorPath) throws SAXException
339    {
340        AttributesImpl attrs = new AttributesImpl();
341        _saxCommonAttributes(container, ancestorPath, attrs);
342        
343        XMLUtils.startElement(contentHandler, "container", attrs);
344        
345        XMLUtils.startElement(contentHandler, "attributes");
346        _saxReferenceTableItem(container.getNature(), Container.NATURE, container.getLanguage());
347        _saxReferenceTableItem(container.getPeriod(), Container.PERIOD, container.getLanguage());
348        
349        _saxStructureViewIfExists(container);
350        
351        XMLUtils.endElement(contentHandler, "attributes");
352        
353        saxChildProgramItems(container, ancestorPath);
354        
355        XMLUtils.endElement(contentHandler, "container");
356    }
357    
358    /**
359     * SAX a course list
360     * @param cl the course list to SAX
361     * @param ancestorPath The path of this program item in the saxed structure (computed from the initial saxed program item). Can be a partial path.
362     * @throws SAXException if an error occurs while saxing
363     */
364    protected void saxCourseList(CourseList cl, List<ProgramItem> ancestorPath) throws SAXException
365    {
366        AttributesImpl attrs = new AttributesImpl();
367        _saxCommonAttributes(cl, ancestorPath, attrs);
368        
369        XMLUtils.startElement(contentHandler, "courselist", attrs);
370        
371        XMLUtils.startElement(contentHandler, "attributes");
372        _saxStructureViewIfExists(cl);
373        XMLUtils.endElement(contentHandler, "attributes");
374        
375        saxChildProgramItems(cl, ancestorPath);
376        
377        XMLUtils.endElement(contentHandler, "courselist");
378    }
379    
380    /**
381     * SAX a course
382     * @param course the container to SAX
383     * @param ancestorPath The path of this program item in the saxed structure (computed from the initial saxed program item). Can be a partial path.
384     * @throws SAXException if an error occurs while saxing
385     */
386    protected void saxCourse(Course course, List<ProgramItem> ancestorPath) throws SAXException
387    {
388        AttributesImpl attrs = new AttributesImpl();
389        _saxCommonAttributes(course, ancestorPath, attrs);
390        
391        XMLUtils.startElement(contentHandler, "course", attrs);
392        
393        XMLUtils.startElement(contentHandler, "attributes");
394        _saxReferenceTableItem(course.getCourseType(), Course.COURSE_TYPE, course.getLanguage());
395        _saxStructureViewIfExists(course);
396        XMLUtils.endElement(contentHandler, "attributes");
397        
398        saxChildProgramItems(course, ancestorPath);
399        
400        saxCourseParts(course);
401        
402        XMLUtils.endElement(contentHandler, "course");
403    }
404    
405    /**
406     * SAX a course part
407     * @param course The course
408     * @throws SAXException if an error occurs
409     */
410    protected void saxCourseParts(Course course) throws SAXException
411    {
412        List<CoursePart> courseParts = course.getCourseParts();
413        
414        double totalHours = 0;
415        List<CoursePart> liveCourseParts = new ArrayList<>();
416        for (CoursePart coursePart : courseParts)
417        {
418            try
419            {
420                _odfHelper.switchToLiveVersionIfNeeded(coursePart);
421                totalHours += coursePart.getNumberOfHours();
422                liveCourseParts.add(coursePart);
423            }
424            catch (NoLiveVersionException e)
425            {
426                getLogger().warn("Some hours of " + course.toString() + " are not added because the course part " + coursePart.toString() + " does not have a live version.");
427            }
428        }
429        
430        AttributesImpl attrs = new AttributesImpl();
431        attrs.addCDATAAttribute("totalHours", String.valueOf(totalHours));
432        XMLUtils.startElement(contentHandler, "courseparts", attrs);
433
434        for (CoursePart coursePart : liveCourseParts)
435        {
436            saxCoursePart(coursePart);
437        }
438        
439        XMLUtils.endElement(contentHandler, "courseparts");
440    }
441    
442    /**
443     * SAX a course part
444     * @param coursePart The course part to SAX
445     * @throws SAXException if an error occurs
446     */
447    protected void saxCoursePart(CoursePart coursePart) throws SAXException
448    {
449        AttributesImpl attrs = new AttributesImpl();
450        attrs.addCDATAAttribute("id", coursePart.getId());
451        attrs.addCDATAAttribute("title", coursePart.getTitle());
452        _addAttrIfNotEmpty(attrs, "code", coursePart.getCode());
453
454        XMLUtils.startElement(contentHandler, "coursepart", attrs);
455        
456        XMLUtils.startElement(contentHandler, "attributes");
457        _saxReferenceTableItem(coursePart.getNature(), CoursePart.NATURE, coursePart.getLanguage());
458        
459        _saxStructureViewIfExists(coursePart);
460        
461        XMLUtils.endElement(contentHandler, "attributes");
462        
463        XMLUtils.endElement(contentHandler, "coursepart");
464    }
465    
466    /**
467     * SAX the 'structure' view if exists
468     * @param content the content
469     * @throws SAXException if an error occurs
470     */
471    protected void _saxStructureViewIfExists(Content content) throws SAXException
472    {
473        SaxBuffer buffer = _viewCache.get(content);
474        
475        if (buffer != null)
476        {
477            buffer.toSAX(contentHandler);
478            return;
479        }
480        
481        View view = _cTypesHelper.getView("structure", content.getTypes(), content.getMixinTypes());
482        if (view != null)
483        {
484            try
485            {
486                buffer = new SaxBuffer();
487                
488                content.dataToSAX(buffer, view, DataContext.newInstance().withLocale(LocaleUtils.toLocale(content.getLanguage())).withEmptyValues(false));
489                
490                _viewCache.put(content, buffer);
491                buffer.toSAX(contentHandler);
492            }
493            catch (BadItemTypeException | AmetysRepositoryException e)
494            {
495                throw new SAXException("Fail to sax the 'structure' view for content " + content.getId(), e);
496            }
497        }
498    }
499    
500    /**
501     * SAX the common attributes for program item
502     * @param programItem the program item
503     * @param ancestorPath The path of this program item in the structure (starting from the initial saxed program item)
504     * @param attrs the attributes
505     */
506    protected void _saxCommonAttributes(ProgramItem programItem, List<ProgramItem> ancestorPath, AttributesImpl attrs)
507    {
508        CommonAttributes commonAttributes = _commonAttrCache.get(programItem, k -> {
509            return new CommonAttributes(programItem.getId(), ((Content) programItem).getTitle(), programItem.getCode(), programItem.getName(), _odfSkillsHelper.isExcluded(programItem));
510        });
511        
512        attrs.addCDATAAttribute("title", commonAttributes.title());
513        attrs.addCDATAAttribute("id", commonAttributes.id());
514        attrs.addCDATAAttribute("code", commonAttributes.code());
515        attrs.addCDATAAttribute("name", commonAttributes.name());
516        boolean excludedFromSkills = commonAttributes.excludedFromSkills();
517        if (excludedFromSkills)
518        {
519            attrs.addCDATAAttribute("excludedFromSkills", String.valueOf(excludedFromSkills));
520        }
521        
522        if (ancestorPath != null)
523        {
524            attrs.addCDATAAttribute("path", EducationalPath.of(ancestorPath.toArray(ProgramItem[]::new)).toString());
525        }
526    }
527    
528    private record CommonAttributes(String id, String title, String code, String name, boolean excludedFromSkills) {};
529    
530    /**
531     * SAX the item of a reference table
532     * @param itemId the item id
533     * @param tagName the tag name
534     * @param lang the language to use
535     * @throws SAXException if an error occurs while saxing
536     */
537    protected void _saxReferenceTableItem(String itemId, String tagName, String lang) throws SAXException
538    {
539        Triple<String, String, String> cacheKey = Triple.of(itemId, tagName, lang);
540        SaxBuffer buffer = _refItemCache.get(cacheKey);
541        
542        if (buffer != null)
543        {
544            buffer.toSAX(contentHandler);
545            return;
546        }
547        
548        buffer = new SaxBuffer();
549        
550        OdfReferenceTableEntry item = Optional.ofNullable(itemId)
551                                              .filter(StringUtils::isNotEmpty)
552                                              .map(_odfReferenceTableHelper::getItem)
553                                              .orElse(null);
554        
555        if (item != null)
556        {
557            AttributesImpl attrs = new AttributesImpl();
558            attrs.addCDATAAttribute("id", item.getId());
559            _addAttrIfNotEmpty(attrs, "code", item.getCode());
560            
561            XMLUtils.createElement(buffer, tagName, attrs, item.getLabel(lang));
562            
563            _refItemCache.put(cacheKey, buffer);
564            buffer.toSAX(contentHandler);
565        }
566    }
567    
568    /**
569     * Add an attribute if its not null or empty.
570     * @param attrs The attributes
571     * @param attrName The attribute name
572     * @param attrValue The attribute value
573     */
574    protected void _addAttrIfNotEmpty(AttributesImpl attrs, String attrName, String attrValue)
575    {
576        if (StringUtils.isNotEmpty(attrValue))
577        {
578            attrs.addCDATAAttribute(attrName, attrValue);
579        }
580    }
581
582}