001/*
002 *  Copyright 2014 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.ArrayList;
021import java.util.Arrays;
022import java.util.List;
023import java.util.Map;
024import java.util.stream.Collectors;
025
026import org.apache.avalon.framework.parameters.ParameterException;
027import org.apache.avalon.framework.service.ServiceException;
028import org.apache.avalon.framework.service.ServiceManager;
029import org.apache.cocoon.ProcessingException;
030import org.apache.cocoon.components.source.impl.SitemapSource;
031import org.apache.cocoon.environment.ObjectModelHelper;
032import org.apache.cocoon.generation.ServiceableGenerator;
033import org.apache.cocoon.xml.AttributesImpl;
034import org.apache.cocoon.xml.XMLUtils;
035import org.apache.commons.lang3.StringUtils;
036import org.xml.sax.SAXException;
037
038import org.ametys.cms.contenttype.ContentAttributeDefinition;
039import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
040import org.ametys.cms.repository.Content;
041import org.ametys.cms.repository.ContentTypeExpression;
042import org.ametys.cms.repository.LanguageExpression;
043import org.ametys.core.util.IgnoreRootHandler;
044import org.ametys.core.util.LambdaUtils;
045import org.ametys.odf.NoLiveVersionException;
046import org.ametys.odf.ODFHelper;
047import org.ametys.odf.ProgramItem;
048import org.ametys.odf.catalog.Catalog;
049import org.ametys.odf.catalog.CatalogsManager;
050import org.ametys.odf.enumeration.OdfReferenceTableEntry;
051import org.ametys.odf.enumeration.OdfReferenceTableHelper;
052import org.ametys.odf.orgunit.OrgUnit;
053import org.ametys.odf.program.AbstractProgram;
054import org.ametys.odf.program.Program;
055import org.ametys.odf.program.ProgramFactory;
056import org.ametys.odf.program.ProgramPart;
057import org.ametys.odf.program.SubProgram;
058import org.ametys.odf.program.SubProgramFactory;
059import org.ametys.odf.schedulable.CatalogPDFExportSchedulable;
060import org.ametys.plugins.repository.AmetysObjectIterable;
061import org.ametys.plugins.repository.AmetysObjectResolver;
062import org.ametys.plugins.repository.AmetysRepositoryException;
063import org.ametys.plugins.repository.CollectionIterable;
064import org.ametys.plugins.repository.UnknownAmetysObjectException;
065import org.ametys.plugins.repository.query.QueryHelper;
066import org.ametys.plugins.repository.query.SortCriteria;
067import org.ametys.plugins.repository.query.expression.AndExpression;
068import org.ametys.plugins.repository.query.expression.Expression;
069import org.ametys.plugins.repository.query.expression.Expression.Operator;
070import org.ametys.plugins.repository.query.expression.OrExpression;
071import org.ametys.plugins.repository.query.expression.StringExpression;
072import org.ametys.runtime.model.View;
073
074/**
075 * Generator producing the SAX events for the catalogue summary 
076 */ 
077public class FOProgramsGenerator extends ServiceableGenerator
078{
079    private static final String __EXPORT_MODE = "catalog";
080    
081    /** The Ametys object resolver */
082    protected AmetysObjectResolver _resolver;
083    /** The content type extension point */
084    protected ContentTypeExtensionPoint _ctypeEP;
085    /** The ODf helper */
086    protected ODFHelper _odfHelper;
087    /** The ODf enumeration helper */
088    protected OdfReferenceTableHelper _odfTableRefHelper;
089    /** The catalog manager */
090    protected CatalogsManager _catalogManager;
091    /** The query helper */
092    protected org.ametys.plugins.queriesdirectory.QueryHelper _queryHelper;
093    
094    @Override
095    public void service(ServiceManager sManager) throws ServiceException
096    {
097        super.service(sManager);
098        _resolver = (AmetysObjectResolver) sManager.lookup(AmetysObjectResolver.ROLE);
099        _ctypeEP = (ContentTypeExtensionPoint) sManager.lookup(ContentTypeExtensionPoint.ROLE);
100        _odfHelper = (ODFHelper) sManager.lookup(ODFHelper.ROLE);
101        _odfTableRefHelper = (OdfReferenceTableHelper) sManager.lookup(OdfReferenceTableHelper.ROLE);
102        _catalogManager = (CatalogsManager) sManager.lookup(CatalogsManager.ROLE);
103        _queryHelper = (org.ametys.plugins.queriesdirectory.QueryHelper) sManager.lookup(org.ametys.plugins.queriesdirectory.QueryHelper.ROLE);
104    }
105    
106    public void generate() throws IOException, SAXException, ProcessingException
107    {
108        contentHandler.startDocument();
109        XMLUtils.startElement(contentHandler, "programs");
110        
111        Catalog catalog = null;
112        if ("_default".equals(source))
113        {
114            catalog = _catalogManager.getDefaultCatalog();
115        }
116        else
117        {
118            catalog = _catalogManager.getCatalog(source);
119        }
120        
121        if (catalog == null)
122        {
123            throw new IllegalArgumentException ("Failed to generated PDF of unknown catalog '" + source + "'");
124        }
125
126        String lang;
127        try
128        {
129            lang = parameters.getParameter("lang");
130        }
131        catch (ParameterException e)
132        {
133            throw new IllegalArgumentException ("Missing lang parameter", e);
134        }
135        
136        // Catalog
137        AttributesImpl attrs = new AttributesImpl();
138        attrs.addCDATAAttribute("name", source);
139        XMLUtils.createElement(contentHandler, "catalog", attrs, catalog.getTitle());
140
141        Map parentContext = (Map) objectModel.get(ObjectModelHelper.PARENT_CONTEXT);
142        
143        boolean queryMode = CatalogPDFExportSchedulable.MODE_QUERY.equals(parentContext.get(CatalogPDFExportSchedulable.JOBDATAMAP_MODE_KEY));
144        
145        // Orgunits
146        List<OrgUnit> orgUnits = new ArrayList<>();
147        if (parentContext.containsKey(CatalogPDFExportSchedulable.JOBDATAMAP_ORGUNIT_KEY))
148        {
149            Object[] ouIds = (Object[]) parentContext.get(CatalogPDFExportSchedulable.JOBDATAMAP_ORGUNIT_KEY);
150            
151            orgUnits = Arrays.stream(ouIds)
152                .map(String.class::cast)
153                .filter(StringUtils::isNotEmpty)
154                .map(LambdaUtils.wrap(_resolver::<OrgUnit>resolveById))
155                .collect(Collectors.toList());
156        }
157        
158        for (OrgUnit orgUnit : orgUnits)
159        {
160            attrs.clear();
161            attrs.addCDATAAttribute("id", orgUnit.getId());
162            attrs.addCDATAAttribute("uaiCode", orgUnit.getUAICode());
163            XMLUtils.createElement(contentHandler, "orgunit", attrs, orgUnit.getTitle());
164        }
165        
166        // Degrees
167        List<OdfReferenceTableEntry> degrees = new ArrayList<>();
168        if (parentContext.containsKey(CatalogPDFExportSchedulable.JOBDATAMAP_DEGREE_KEY))
169        {
170            Object[] degreeIds = (Object[]) parentContext.get(CatalogPDFExportSchedulable.JOBDATAMAP_DEGREE_KEY);
171            
172            degrees = Arrays.stream(degreeIds)
173                .map(String.class::cast)
174                .filter(StringUtils::isNotEmpty)
175                .map(_odfTableRefHelper::getItem)
176                .collect(Collectors.toList());
177        }
178        
179        for (OdfReferenceTableEntry degree : degrees)
180        {
181            attrs.clear();
182            attrs.addCDATAAttribute("id", degree.getId());
183            attrs.addCDATAAttribute("code", degree.getCode());
184            attrs.addCDATAAttribute("order", String.valueOf(degree.getOrder()));
185            XMLUtils.createElement(contentHandler, "degree", attrs, degree.getLabel(lang));
186        }
187        
188        String queryId = (String) parentContext.get(CatalogPDFExportSchedulable.JOBDATAMAP_QUERY_KEY);
189        boolean includeSubPrograms = (Boolean) parentContext.get(CatalogPDFExportSchedulable.JOBDATAMAP_INCLUDE_SUBPROGRAMS);
190        
191        Map<String, ContentAttributeDefinition> tableRefAttributeDefs = _odfTableRefHelper.getTableRefAttributeDefinitions(ProgramFactory.PROGRAM_CONTENT_TYPE);
192        
193        AmetysObjectIterable<Program> programs = queryMode ? _getPrograms(catalog.getName(), lang, queryId) : _getPrograms(catalog.getName(), lang, orgUnits, degrees);
194        _saxPrograms(programs, includeSubPrograms);
195        
196        // SAX entries of table references
197        XMLUtils.startElement(contentHandler, "enumerated-metadata");
198        for (ContentAttributeDefinition attributeDef : tableRefAttributeDefs.values())
199        {
200            _odfTableRefHelper.saxItems(contentHandler, attributeDef, lang);
201        }
202        XMLUtils.endElement(contentHandler, "enumerated-metadata");
203        
204        XMLUtils.endElement(contentHandler, "programs");
205        contentHandler.endDocument();
206    }
207    
208    /**
209     * Get programs from catalog, lang, orgunit and degree. Orgunit and degree can be null.
210     * @param catalog The catalog
211     * @param lang The content language
212     * @param orgUnits The restricted orgunits. Can be empty to not filter by orgunits
213     * @param degrees The restricted degrees. Can be empty to not filter by degrees
214     * @return An iterable of programs corresponding to the query with catalog, lang, orgunit and degree.
215     */
216    protected AmetysObjectIterable<Program> _getPrograms(String catalog, String lang, List<OrgUnit> orgUnits, List<OdfReferenceTableEntry> degrees)
217    {
218        List<Expression> exprs = new ArrayList<>();
219        exprs.add(new ContentTypeExpression(Operator.EQ, ProgramFactory.PROGRAM_CONTENT_TYPE));
220        exprs.add(new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalog));
221        exprs.add(new LanguageExpression(Operator.EQ, lang));
222
223        if (!degrees.isEmpty())
224        {
225            Expression[] degreeExprs = degrees.stream()
226                .map(d -> new StringExpression(AbstractProgram.DEGREE, Operator.EQ, d.getId()))
227                .toArray(Expression[]::new);
228            
229            exprs.add(new OrExpression(degreeExprs));
230        }
231        
232        if (!orgUnits.isEmpty())
233        {
234            Expression[] ouExprs = orgUnits.stream()
235                .map(_odfHelper::getSubOrgUnitIds)
236                .flatMap(List::stream)
237                .distinct()
238                .map(orgunitId -> new StringExpression(ProgramItem.ORG_UNITS_REFERENCES, Operator.EQ, orgunitId))
239                .toArray(Expression[]::new);
240            
241            exprs.add(new OrExpression(ouExprs));
242        }
243        
244        Expression programsExpression = new AndExpression(exprs.toArray(Expression[]::new));
245        
246        SortCriteria sortCriteria = new SortCriteria();
247        sortCriteria.addCriterion(Content.ATTRIBUTE_TITLE, true, true);
248  
249        String programsQuery = QueryHelper.getXPathQuery(null, ProgramFactory.PROGRAM_NODETYPE, programsExpression, sortCriteria);
250        AmetysObjectIterable<Program> programs = _resolver.query(programsQuery);
251        
252        return programs;
253    }
254    
255    /**
256     * Get programs from catalog, lang and a query
257     * @param catalog The catalog
258     * @param lang The content language
259     * @param queryId The query id
260     * @return An iterable of programs corresponding to the query. Results are filtered 
261     * @throws ProcessingException if failed to execute query
262     */
263    protected AmetysObjectIterable<Program> _getPrograms(String catalog, String lang, String queryId) throws ProcessingException
264    {
265        try
266        {
267            AmetysObjectIterable<Content> contents = _queryHelper.executeQuery(queryId);
268            
269            List<Program> programs = contents.stream()
270                .filter(Program.class::isInstance)
271                .map(Program.class::cast)
272                .filter(p -> catalog.equals(p.getCatalog()))
273                .filter(p -> lang.equals(p.getLanguage()))
274                .toList();
275                
276            return new CollectionIterable<>(programs);
277        }
278        catch (Exception e)
279        {
280            throw new ProcessingException("Failed to execute query '" + queryId + "' to generate PDF catalog", e);
281        }
282    }
283    
284    /**
285     * Sax programs
286     * @param programs the programs to sax
287     * @param includeSubPrograms true to include subprograms
288     * @throws MalformedURLException if an error occurred
289     * @throws IOException if an error occurred
290     * @throws SAXException if an error occurred
291     */
292    protected void _saxPrograms(AmetysObjectIterable<Program> programs, boolean includeSubPrograms) throws MalformedURLException, IOException, SAXException
293    {
294        Map<String, ContentAttributeDefinition> tableRefAttributeDefs = _odfTableRefHelper.getTableRefAttributeDefinitions(ProgramFactory.PROGRAM_CONTENT_TYPE);
295        
296        for (AbstractProgram program : programs)
297        {
298            _saxAbstractProgram("program", program, tableRefAttributeDefs, includeSubPrograms);
299        }
300    }
301    
302    /**
303     * SAX a program or subprogram
304     * @param tagName the XML root tag
305     * @param program The abstract program to sax
306     * @param tableRefAttributeDefs The table reference attribute definitions
307     * @param includeSubprograms true to include subprograms
308     * @throws MalformedURLException if an error occurred
309     * @throws IOException if an error occurred
310     * @throws SAXException if an error occurred
311     */
312    protected void _saxAbstractProgram(String tagName, AbstractProgram program, Map<String, ContentAttributeDefinition> tableRefAttributeDefs, boolean includeSubprograms) throws MalformedURLException, IOException, SAXException
313    {
314        try
315        {
316            _odfHelper.switchToLiveVersionIfNeeded(program);
317            SitemapSource src = null;      
318            
319            try
320            {
321                AttributesImpl attrs = new AttributesImpl();
322                attrs.addCDATAAttribute("id", program.getId());
323                attrs.addCDATAAttribute("name", program.getName());
324                attrs.addCDATAAttribute("title", program.getTitle());
325                
326                XMLUtils.startElement(contentHandler, tagName, attrs);
327                
328                _saxTableRefAttributeValues(program, tableRefAttributeDefs);
329                
330                XMLUtils.startElement(contentHandler, "fo");
331                
332                String uri = "cocoon://_plugins/odf/_content/" + program.getName() + ".fo";
333                src = (SitemapSource) resolver.resolveURI(uri, null, Map.of("exportMode", __EXPORT_MODE));
334                src.toSAX(new IgnoreRootHandler(contentHandler));
335                
336                XMLUtils.endElement(contentHandler, "fo");
337                
338                if (includeSubprograms)
339                {
340                    Map<String, ContentAttributeDefinition> subProgramTableRefAttributeDefs = _odfTableRefHelper.getTableRefAttributeDefinitions(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE);
341                    List<ProgramPart> programPartChildren = program.getProgramPartChildren();
342                    for (ProgramPart programPart : programPartChildren)
343                    {
344                        if (programPart instanceof SubProgram)
345                        {
346                            _saxAbstractProgram("subprogram", (SubProgram) programPart, subProgramTableRefAttributeDefs, includeSubprograms);
347                        }
348                    }
349                }
350                XMLUtils.endElement(contentHandler, tagName);
351                
352            }
353            catch (UnknownAmetysObjectException e)
354            {
355                // The content may be archived
356            }
357            finally
358            {
359                resolver.release(src);
360            }
361            
362        }
363        catch (NoLiveVersionException e)
364        {
365            getLogger().info("No live version found for program item " + program.getTitle() + " (" + program.getCode() + "). The program item will not appear in the PDF export.", e);
366        }
367    }
368    
369    /**
370     * SAX enumerated values of an attribute 
371     * @param program The program
372     * @param tableRefAttributeDefs The table reference attribute definitions
373     * @throws AmetysRepositoryException if an error occurred
374     * @throws SAXException if an error occurred
375     * @throws IOException if an error occurred
376     */
377    protected void _saxTableRefAttributeValues(AbstractProgram program, Map<String, ContentAttributeDefinition> tableRefAttributeDefs) throws AmetysRepositoryException, SAXException, IOException
378    {
379        // Build a view containing all the reference tables attributes
380        View view = View.of(program.getModel(), tableRefAttributeDefs.keySet().toArray(new String[tableRefAttributeDefs.size()]));
381        
382        // Generate SAX events for the built view
383        XMLUtils.startElement(contentHandler, "metadata");
384        program.dataToSAX(contentHandler, view);
385        XMLUtils.endElement(contentHandler, "metadata");
386    }
387}