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