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