001/*
002 *  Copyright 2020 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.export.pdf;
017
018import java.io.IOException;
019import java.net.MalformedURLException;
020import java.util.HashMap;
021import java.util.LinkedHashSet;
022import java.util.List;
023import java.util.Map;
024import java.util.Set;
025import java.util.stream.Collectors;
026
027import org.apache.avalon.framework.context.Context;
028import org.apache.avalon.framework.context.ContextException;
029import org.apache.avalon.framework.context.Contextualizable;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.cocoon.ProcessingException;
033import org.apache.cocoon.components.ContextHelper;
034import org.apache.cocoon.components.source.impl.SitemapSource;
035import org.apache.cocoon.environment.ObjectModelHelper;
036import org.apache.cocoon.environment.Request;
037import org.apache.cocoon.generation.ServiceableGenerator;
038import org.apache.cocoon.xml.AttributesImpl;
039import org.apache.cocoon.xml.XMLUtils;
040import org.apache.commons.lang3.StringUtils;
041import org.xml.sax.SAXException;
042
043import org.ametys.cms.CmsConstants;
044import org.ametys.cms.repository.Content;
045import org.ametys.cms.repository.DefaultContent;
046import org.ametys.core.util.IgnoreRootHandler;
047import org.ametys.odf.EducationalPathHelper;
048import org.ametys.odf.NoLiveVersionException;
049import org.ametys.odf.ODFHelper;
050import org.ametys.odf.ProgramItem;
051import org.ametys.odf.catalog.Catalog;
052import org.ametys.odf.catalog.CatalogsManager;
053import org.ametys.odf.course.Course;
054import org.ametys.odf.program.SubProgram;
055import org.ametys.odf.schedulable.ArchiveEducationalBookletSchedulable;
056import org.ametys.odf.schedulable.EducationalBookletSchedulable;
057import org.ametys.plugins.repository.AmetysObjectResolver;
058import org.ametys.plugins.repository.UnknownAmetysObjectException;
059import org.ametys.plugins.repository.jcr.DefaultAmetysObject;
060
061
062/**
063 * Generator producing the SAX of subprogram and its courses for the educational booklet 
064 */ 
065public class EducationalBookletGenerator extends ServiceableGenerator implements Contextualizable
066{
067    private static final String __EXPORT_MODE = "educational-booklet";
068    
069    /** The Ametys object resolver */
070    protected AmetysObjectResolver _resolver;
071    
072    /** The ODF helper */
073    protected ODFHelper _odfHelper;
074
075    /** The catalog manager */
076    protected CatalogsManager _catalogManager;
077
078    /** The avalon context */
079    protected Context _context;
080    
081    @Override
082    public void service(ServiceManager sManager) throws ServiceException
083    {
084        super.service(sManager);
085        _resolver = (AmetysObjectResolver) sManager.lookup(AmetysObjectResolver.ROLE);
086        _odfHelper = (ODFHelper) sManager.lookup(ODFHelper.ROLE);
087        _catalogManager = (CatalogsManager) sManager.lookup(CatalogsManager.ROLE);
088    }
089    
090    public void contextualize(Context context) throws ContextException
091    {
092        _context = context;
093    }
094    
095    public void generate() throws IOException, SAXException, ProcessingException
096    {
097        Request request = ObjectModelHelper.getRequest(objectModel);
098        
099        contentHandler.startDocument();
100
101        AttributesImpl attrs = new AttributesImpl();
102        
103        String archiveDateAsString = (String) request.getAttribute(ArchiveEducationalBookletSchedulable.PARAM_ARCHIVE_DATE);
104        if (StringUtils.isNotBlank(archiveDateAsString))
105        {
106            attrs.addCDATAAttribute("isValidated", "true");
107            attrs.addCDATAAttribute("archiveDate", archiveDateAsString);
108        }
109        else
110        {
111            attrs.addCDATAAttribute("isValidated", "false");
112        }
113
114        XMLUtils.startElement(contentHandler, "booklet", attrs);
115        
116        String programItemId = (String) request.getAttribute(EducationalBookletSchedulable.PARAM_PROGRAM_ITEM_ID);
117        ProgramItem programItem = _resolver.resolveById(programItemId);
118        
119        _saxCatalog(programItem);
120        
121        try
122        {
123            _odfHelper.switchToLiveVersion((DefaultContent) programItem);
124            _saxProgramItem(programItem, "programItem", List.of(programItem), null);
125            
126            boolean includeSubPrograms = (boolean) request.getAttribute(EducationalBookletSchedulable.PARAM_INCLUDE_SUBPROGRAMS);
127            
128            if (includeSubPrograms)
129            {
130                for (SubProgram subProgram : _getChildSubPrograms(programItem))
131                {
132                    _saxProgramItem(subProgram, "subprogram", List.of(programItem, subProgram), programItem);
133                }
134            }
135            
136            for (Course course : _getChildCourses(programItem))
137            {
138                _saxProgramItem(course, "course", List.of(course), programItem);
139            }
140        }
141        catch (NoLiveVersionException e) 
142        {
143            throw new IllegalArgumentException("The program item with id " + programItemId + " has no live version.");
144        }
145        
146        XMLUtils.endElement(contentHandler, "booklet");
147        contentHandler.endDocument();
148    }
149    
150    /**
151     * Sax the catalog of program item
152     * @param programItem the program item
153     * @throws SAXException if an error occurred
154     */
155    protected void _saxCatalog(ProgramItem programItem) throws SAXException
156    {
157        String catalogName = programItem.getCatalog();
158        Catalog catalog = _catalogManager.getCatalog(catalogName);
159        
160        if (catalog != null)
161        {
162            AttributesImpl attrs = new AttributesImpl();
163            attrs.addCDATAAttribute("name", catalogName);
164            XMLUtils.createElement(contentHandler, "catalog", attrs, catalog.getTitle());
165        }
166    }
167    
168    /**
169     * Get all child courses from the program item
170     * @param programItem the program item
171     * @return the set of child courses
172     * @throws MalformedURLException if an error occurred
173     * @throws IOException if an error occurred
174     * @throws SAXException if an error occurred
175     */
176    protected Set<Course> _getChildCourses(ProgramItem programItem) throws MalformedURLException, IOException, SAXException
177    {
178        Set<Course> courses = new LinkedHashSet<>();
179        for (ProgramItem childProgramItem : _odfHelper.getChildProgramItems(programItem))
180        {
181            try
182            {
183                _odfHelper.switchToLiveVersion((DefaultAmetysObject) childProgramItem);
184                if (childProgramItem instanceof Course)
185                {
186                    courses.add((Course) childProgramItem);
187                }
188                courses.addAll(_getChildCourses(childProgramItem));
189            }
190            catch (NoLiveVersionException e) 
191            {
192                getLogger().warn("Cannot add the course to the educational booklet : Live label is required but there is no Live version for content " + childProgramItem.getId());
193            }
194        }
195        
196        return courses;
197    }
198    
199    /**
200     * Get the direct child {@link SubProgram}s of a {@link ProgramItem}
201     * @param programItem the program item
202     * @return the subprograms
203     */
204    protected Set<SubProgram> _getChildSubPrograms(ProgramItem programItem)
205    {
206        return _odfHelper.getChildProgramItems(programItem)
207            .stream()
208            .filter(SubProgram.class::isInstance)
209            .map(SubProgram.class::cast)
210            .collect(Collectors.toSet());
211    }
212    
213    /**
214     * Sax a {@link ProgramItem} as fo
215     * @param programItem the program item
216     * @param tagName the xml tag name
217     * @param ancestorPath The path of this program item (computed from the initial program item). Can be a partial path.
218     * @param rootProgramItem The root program item ancestor for this booklet
219     * @throws MalformedURLException if an error occurred
220     * @throws IOException if an error occurred
221     * @throws SAXException if an error occurred
222     */
223    protected void _saxProgramItem(ProgramItem programItem, String tagName, List<ProgramItem> ancestorPath, ProgramItem rootProgramItem) throws MalformedURLException, IOException, SAXException
224    {
225        AttributesImpl attrs = new AttributesImpl();
226        attrs.addCDATAAttribute("id", programItem.getId());
227        attrs.addCDATAAttribute("name", programItem.getName());
228        attrs.addCDATAAttribute("title", ((Content) programItem).getTitle());
229        attrs.addCDATAAttribute("code", programItem.getCode());
230        
231        XMLUtils.startElement(contentHandler, tagName, attrs);
232        _saxContentAsFo((Content) programItem, ancestorPath, rootProgramItem);
233        XMLUtils.endElement(contentHandler, tagName);
234    }
235    
236    /**
237     * Sax content as fo
238     * @param content the content
239     * @param ancestorPath The path of this program item (computed from the initial program item). Can be a partial path.
240     * @param rootAncestor The root program item ancestor for this booklet
241     * @throws MalformedURLException if an error occurred
242     * @throws IOException if an error occurred
243     * @throws SAXException if an error occurred
244     */
245    protected void _saxContentAsFo(Content content, List<ProgramItem> ancestorPath, ProgramItem rootAncestor) throws MalformedURLException, IOException, SAXException
246    {
247        XMLUtils.startElement(contentHandler, "fo");
248        SitemapSource src = null;      
249        
250        Request request = ContextHelper.getRequest(_context);
251        
252        try
253        {
254            request.setAttribute(EducationalPathHelper.PROGRAM_ITEM_ANCESTOR_PATH_REQUEST_ATTR, ancestorPath);
255            if (rootAncestor != null)
256            {
257                request.setAttribute(EducationalPathHelper.ROOT_PROGRAM_ITEM_REQUEST_ATTR, rootAncestor);
258            }
259            
260            Map<String, Object> pdfParameters = new HashMap<>();
261            pdfParameters.put("versionLabel", CmsConstants.LIVE_LABEL);
262            pdfParameters.put("exportMode", __EXPORT_MODE);
263            
264            String uri = "cocoon://_plugins/odf/_content/" + content.getName() + ".fo";
265            src = (SitemapSource) resolver.resolveURI(uri, null, pdfParameters);
266            src.toSAX(new IgnoreRootHandler(contentHandler));
267        }
268        catch (UnknownAmetysObjectException e)
269        {
270            // The content may be archived
271        }
272        finally
273        {
274            resolver.release(src);
275            
276            request.removeAttribute(EducationalPathHelper.PROGRAM_ITEM_ANCESTOR_PATH_REQUEST_ATTR);
277            request.removeAttribute(EducationalPathHelper.ROOT_PROGRAM_ITEM_REQUEST_ATTR);
278        }
279        
280        XMLUtils.endElement(contentHandler, "fo");
281    }
282}