001/*
002 *  Copyright 2018 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.plugins.odfpilotage.report.impl;
017
018import java.io.File;
019import java.io.FileOutputStream;
020import java.util.Comparator;
021import java.util.List;
022import java.util.Map;
023import java.util.Map.Entry;
024import java.util.Optional;
025import java.util.Set;
026import java.util.TreeMap;
027
028import javax.xml.transform.Result;
029import javax.xml.transform.TransformerFactory;
030import javax.xml.transform.sax.SAXTransformerFactory;
031import javax.xml.transform.sax.TransformerHandler;
032import javax.xml.transform.stream.StreamResult;
033
034import org.apache.cocoon.xml.AttributesImpl;
035import org.apache.cocoon.xml.XMLUtils;
036import org.apache.commons.io.FileUtils;
037import org.apache.commons.lang3.StringUtils;
038import org.xml.sax.SAXException;
039
040import org.ametys.cms.data.ContentValue;
041import org.ametys.cms.repository.ModifiableContent;
042import org.ametys.core.util.DateUtils;
043import org.ametys.odf.ProgramItem;
044import org.ametys.odf.course.Course;
045import org.ametys.odf.courselist.CourseList;
046import org.ametys.odf.courselist.CourseList.ChoiceType;
047import org.ametys.odf.coursepart.CoursePart;
048import org.ametys.odf.orgunit.OrgUnit;
049import org.ametys.odf.program.Container;
050import org.ametys.odf.program.Program;
051import org.ametys.odf.program.SubProgram;
052import org.ametys.plugins.repository.AmetysObjectIterable;
053import org.ametys.plugins.repository.AmetysObjectIterator;
054import org.ametys.plugins.repository.AmetysRepositoryException;
055import org.ametys.plugins.repository.UnknownAmetysObjectException;
056
057/**
058 * Pilotage report for Apogée.
059 */
060public class ApogeeReport extends AbstractReport
061{
062    private int _order;
063    
064    @Override
065    protected void _launchByOrgUnit(String uaiCode, String catalog, String lang) throws Exception
066    {
067        _writeApogeeReport(uaiCode, catalog, lang);
068    }
069
070    @Override
071    protected String getType()
072    {
073        return "apogee";
074    }
075
076    @Override
077    protected Set<String> getSupportedOutputFormats()
078    {
079        return Set.of(OUTPUT_FORMAT_XLS);
080    }
081    
082    /**
083     * Create the Apogee report for one organization unit 
084     * @param uaiCode The UAI code of the org unit
085     * @param catalog The catalog
086     * @param lang The language
087     */
088    protected void _writeApogeeReport(String uaiCode, String catalog, String lang)
089    {
090        SAXTransformerFactory factory = (SAXTransformerFactory) TransformerFactory.newInstance();
091        String fileName = catalog + "-" + lang + "-" + _reportHelper.getAccronymOrUaiCode(uaiCode) + "-" + "[" + getType() + "-" + _currentFormattedDate + "]";
092        
093        // Delete old files
094        File xmlFile = new File(_tmpFolder, fileName + ".xml");
095        FileUtils.deleteQuietly(xmlFile);
096        
097        // Write XML file
098        try (FileOutputStream fos = new FileOutputStream(xmlFile))
099        {
100            TransformerHandler handler = factory.newTransformerHandler();
101            
102            // Prepare the transformation
103            Result result = new StreamResult(fos);
104            handler.setResult(result);
105            handler.startDocument();
106        
107            AttributesImpl attrs = new AttributesImpl();
108            attrs.addCDATAAttribute("type", getType());
109            attrs.addCDATAAttribute("date", _reportHelper.getReadableCurrentDate());
110            XMLUtils.startElement(handler, "report", attrs);
111            
112            // SAX tree
113            _generateReport(handler, uaiCode, lang, catalog);
114            
115            XMLUtils.endElement(handler, "report");
116            handler.endDocument();
117
118            // Convert the report to configured output format
119            convertReport(_tmpFolder, fileName, xmlFile);
120        }
121        catch (Exception e)
122        {
123            getLogger().error("An error occured while generating 'Apogée' report for orgunit '{}'", uaiCode, e);
124        }
125        finally
126        {
127            FileUtils.deleteQuietly(xmlFile);
128        }
129    }
130    
131    /**
132     * SAX the XML of the report
133     * @param handler the transformer handler 
134     * @param uaiCode the uai code of the organization unit to process
135     * @param lang the lang of the programs
136     * @param catalog the catalog of the programs
137     * @throws SAXException if an error occurs while generating the SAX events
138     */
139    private void _generateReport(TransformerHandler handler, String uaiCode, String lang, String catalog) throws SAXException
140    {
141        if (StringUtils.isEmpty(uaiCode))
142        {
143            throw new IllegalArgumentException("Cannot process the Apogee report without the uai code.The processing of the Apogee report is aborted.");
144        }
145        
146        _reportHelper.saxNaturesEnseignement(handler, getLogger());
147        
148        Map<Program, Object> contentsTree = _getStructure(uaiCode, lang, catalog);
149        if (contentsTree.size() > 0)
150        {
151            _order = 1;
152            _saxTree(handler, contentsTree);
153        }
154    }
155
156    /**
157     * Generate the data structure that will be used to create the report
158     * @param uaiCode the uai code of the organization unit
159     * @param lang the lang of programs
160     * @param catalog the catalog of programs
161     * @return The structure
162     */
163    private Map<Program, Object> _getStructure(String uaiCode, String lang, String catalog)
164    {
165        AmetysObjectIterable<OrgUnit> orgUnits = _reportHelper.getRootOrgUnitsByUaiCode(uaiCode);
166        AmetysObjectIterator<OrgUnit> orgUnitsIterator = orgUnits.iterator();
167        if (!orgUnitsIterator.hasNext())
168        {
169            throw new IllegalArgumentException("Unable to find any organization unit with the uai code : " + uaiCode + ".The processing of the Apogee report is aborted.");
170        }
171        
172        Map<Program, Object> programTree = new TreeMap<>(new ProgramTitleComparator());
173        
174        OrgUnit rootOrgUnit = orgUnitsIterator.next();
175        // On ne récupère que les composantes, enfant direct du root org unit, et on ignore les départements.
176        if (rootOrgUnit.getParentOrgUnit() != null && rootOrgUnit.getParentOrgUnit().getParentOrgUnit() == null)
177        {
178            List<String> orgUnitIds = _reportHelper.getSubOrgUnits(rootOrgUnit);
179            
180            // Chercher les programmes concernés par la composante sélectionnée et ses enfants
181            for (String orgUnitId : orgUnitIds)
182            {
183                AmetysObjectIterable<Program> programs = _reportHelper.getProgramsByOrgUnitId(orgUnitId, lang, catalog);
184                AmetysObjectIterator<Program> programsIterator = programs.iterator();
185                while (programsIterator.hasNext())
186                {
187                    Program program = programsIterator.next();
188                    Map<ProgramItem, Object> courses = _reportHelper.getCoursesFromContent(program);
189                    if (courses != null)
190                    {
191                        programTree.put(program, courses);
192                    }
193                }
194            }
195        }
196        
197        return programTree;
198    }
199
200    /**
201     * Sax the information related to the courses of the tree
202     * @param handler the transformer handler
203     * @param programTree the program tree to sax
204     * @throws SAXException if an error occurs when SAXing
205     */
206    @SuppressWarnings("unchecked")
207    private void _saxTree(TransformerHandler handler, Map<Program, Object> programTree) throws SAXException
208    {
209        for (Entry<Program, Object> programEntry : programTree.entrySet())
210        {
211            if (programEntry.getValue() != null && programEntry.getValue() instanceof Map<?, ?>)
212            {
213                _saxCourseFromTree(handler, (Map<ProgramItem, Object>) programEntry.getValue(), programEntry.getKey());
214            }
215        }
216    }
217
218
219    private void _saxCourseFromTree(TransformerHandler handler, Map<ProgramItem, Object> programTree, Program program) throws SAXException
220    {
221        _saxCourseFromTree(handler, programTree, program, null, null, null, null, null, null, 1, "");
222    }
223
224    private void _saxCourseFromTree(TransformerHandler handler, Map<ProgramItem, Object> tree, Program program, SubProgram subprogram, Container containerYear, Container containerSemester, CourseList list, Integer listPosition, Course parentCourse, int level, String courseHierarchy) throws SAXException
225    {
226        int courseListPosition = 0;
227        for (Entry<ProgramItem, Object> entry : tree.entrySet())
228        {
229            ProgramItem child = entry.getKey();
230            @SuppressWarnings("unchecked")
231            Map<ProgramItem, Object> subTree = (Map<ProgramItem, Object>) entry.getValue();
232            
233            if (child instanceof Course)
234            {
235                Course childCourse = (Course) child;
236                String path = courseHierarchy +  " > " + childCourse.getTitle();
237                _saxCourse(handler, program, subprogram, containerYear, containerSemester, list, listPosition, (Course) child, parentCourse, level, path);
238                
239                if (subTree != null)
240                {
241                    _saxCourseFromTree(handler, subTree, program, subprogram, containerYear, containerSemester, list, listPosition, childCourse, level + 1, path);
242                }
243            }
244            
245            if (subTree != null)
246            {
247                if (child instanceof Program)
248                {
249                    _saxCourseFromTree(handler, subTree, (Program) child, subprogram, containerYear, containerSemester, list, listPosition, parentCourse, level, courseHierarchy);
250                }
251                else if (child instanceof Container)
252                {
253                    Container container = (Container) child;
254                    String containerNature = _refTableHelper.getItemCode(container.getNature());
255                    
256                    if ("annee".equals(containerNature))
257                    {
258                        _saxCourseFromTree(handler, subTree, program, subprogram, container, containerSemester, list, listPosition, parentCourse, level, courseHierarchy);
259                    }
260                    else if ("semestre".equals(containerNature))
261                    {
262                        _saxCourseFromTree(handler, subTree, program, subprogram, containerYear, container, list, listPosition, parentCourse, level, courseHierarchy);
263                    }
264                    else
265                    {
266                        _saxCourseFromTree(handler, subTree, program, subprogram, containerYear, containerSemester, list, listPosition, parentCourse, level, courseHierarchy);
267                    }
268                }
269                else if (child instanceof SubProgram)
270                {
271                    _saxCourseFromTree(handler, subTree, program, (SubProgram) child, containerYear, containerSemester, list, listPosition, parentCourse, level, courseHierarchy);
272                }
273                else if (child instanceof CourseList)
274                {
275                    courseListPosition++;
276                    CourseList childCourseList = (CourseList) child;
277                    String path = courseHierarchy.equals("") ? childCourseList.getTitle() : courseHierarchy +  " > " + childCourseList.getTitle();
278                    _saxCourseFromTree(handler, subTree, program, subprogram, containerYear, containerSemester, (CourseList) child, courseListPosition, parentCourse, level, path);
279                }
280            }
281        }
282    }
283    
284    private String _getHierarchy(Program program, SubProgram subprogram, Container containerYear, Container containerSemester, String courseHierarchy)
285    {
286        String hierarchy = program.getTitle();
287        if (subprogram != null)
288        {
289            hierarchy += " > " + subprogram.getTitle();
290        }
291        
292        if (containerYear != null)
293        {
294            hierarchy += " > " + containerYear.getTitle();
295        }
296        
297        if (containerSemester != null)
298        {
299            hierarchy += " > " + containerSemester.getTitle();
300        }
301            
302        hierarchy += " > " + courseHierarchy;
303        
304        return hierarchy;
305    }
306
307    private void _saxCourse(TransformerHandler handler, Program program, SubProgram subprogram, Container containerYear, Container containerSemester, CourseList list, Integer listPosition, Course course, Course parentCourse, int level, String courseHierarchy) throws SAXException
308    {
309        if (course != null)
310        {
311            String hierarchy = _getHierarchy(program, subprogram, containerYear, containerSemester, courseHierarchy);
312            
313            XMLUtils.startElement(handler, "course");
314            
315            // Ordre
316            XMLUtils.createElement(handler, "ordre", String.valueOf(_order));
317            
318            // Composante
319            _saxOrgUnits(handler, program);
320            
321            // Formation
322            XMLUtils.createElement(handler, "formation", program.getTitle());
323            XMLUtils.createElement(handler, "formationCode", program.getCode());
324            
325            _saxSubProgram(handler, subprogram);
326            
327            _saxContainer(handler, containerYear, parentCourse);
328            
329            _saxCourseList(handler, list, listPosition);
330            
331            // A des fils
332            boolean aDesFils = course.hasCourseLists();
333            XMLUtils.createElement(handler, "aDesFils", aDesFils ? "X" : "");
334
335            long courseListsSize = course.getParentCourseLists().size();
336            
337            // Partagé
338            XMLUtils.createElement(handler, "partage", courseListsSize > 1 ? "X" : "");
339
340            // Nb occurences
341            XMLUtils.createElement(handler, "occurences", _reportHelper.formatNumberToSax(courseListsSize));
342            
343            Container etape = _getEtapePorteuse(course, hierarchy);
344            
345            String porte = _getPorte(etape, containerYear);
346           
347            // Porté ("X" si l'ELP est porté par la formation (Etape porteuse=COD_ETP), vide sinon)
348            XMLUtils.createElement(handler, "porte", porte);
349            
350            // Niveau
351            XMLUtils.createElement(handler, "niveau", "niv" + level);
352
353            // Date de création
354            XMLUtils.createElement(handler, "creationDate", DateUtils.dateToString(course.getCreationDate()));
355
356            // Code Apogée
357            XMLUtils.createElement(handler, "codeApogee", course.getValue("elpCode", false, StringUtils.EMPTY));
358            
359            // Nature de l'élément
360            XMLUtils.createElement(handler, "nature", _refTableHelper.getItemCode(course.getCourseType()));
361
362            // Libellé court
363            XMLUtils.createElement(handler, "libelleCourt", course.getValue("shortLabel", false, StringUtils.EMPTY));
364            
365            // Libellé
366            XMLUtils.createElement(handler, "libelle", course.getTitle());
367            
368            // Code Ametys (ELP)
369            XMLUtils.createElement(handler, "elpCode", course.getCode());
370            
371            // Lieu
372            _reportHelper.saxContentAttribute(handler, course, "campus", "campus");
373
374            // Crédits ECTS
375            XMLUtils.createElement(handler, "ects", String.valueOf(course.getEcts()));
376            
377            String teachingActivity = _refTableHelper.getItemCode(course.getTeachingActivity());
378            String stage = teachingActivity.equals("SA") ? "X" : "";
379            
380            // Element stage
381            XMLUtils.createElement(handler, "stage", stage);
382            
383            // Code semestre
384            String periode = _refTableHelper.getItemCode(course.getTeachingTerm());
385            XMLUtils.createElement(handler, "periode", "s10".equals(periode) ? "s0" : periode);
386            
387            OrgUnit orgUnit = _getOrgUnit(course, hierarchy);
388
389            // Code composante
390            XMLUtils.createElement(handler, "codeComposante", orgUnit != null ? orgUnit.getValue("codCmp", false, StringUtils.EMPTY) : StringUtils.EMPTY);
391            
392            // Code CIP
393            XMLUtils.createElement(handler, "codeCIP", orgUnit != null ? orgUnit.getValue("codCipApogee", false, StringUtils.EMPTY) : StringUtils.EMPTY);
394            
395            // Code ANU
396            long codeAnu = course.getValue("CodeAnu", false, 0L);
397            XMLUtils.createElement(handler, "CodeAnu", codeAnu > 0 ? String.valueOf(codeAnu) : StringUtils.EMPTY);
398            
399            // Calcul des charges 
400            XMLUtils.createElement(handler, "calculCharges", aDesFils ? "" : "X");
401            
402            // Heures d'enseignement
403            for (CoursePart coursePart : course.getCourseParts())
404            {
405                AttributesImpl attr = new AttributesImpl();
406                attr.addCDATAAttribute("nature", coursePart.getNature());
407                XMLUtils.createElement(handler, "volumeHoraire", attr, String.valueOf(coursePart.getNumberOfHours()));
408            }
409
410            // Etape porteuse pour les feuilles de l'arbre
411            if (etape != null)
412            {
413                XMLUtils.createElement(handler, "etapePorteuse", etape.getValue("etpCode", false, ""));
414                XMLUtils.createElement(handler, "vetEtapePorteuse", etape.getValue("vrsEtpCode", false, ""));
415            }
416            
417            // Discipline
418            // FIXME ODF-2161: discipline attribute is not yet part of the model and may never be
419//            XMLUtils.createElement(handler, "discipline", ContentDataHelper.getContentIdFromContentData(course, "discipline"));
420            
421            saxAdditionalCourseData(handler, course);
422            
423            XMLUtils.endElement(handler, "course");
424            
425            _order++;
426        }
427    }
428    
429    /**
430     * Generates SAX events for additional data of a {@link Course}.
431     * @param handler The handler
432     * @param course The course to SAX
433     * @throws AmetysRepositoryException if an error occurs
434     * @throws SAXException if an error occurs
435     */
436    protected void saxAdditionalCourseData(TransformerHandler handler, Course course) throws AmetysRepositoryException, SAXException
437    {
438        // Do nothing by default
439    }
440    
441    private void _saxContainer(TransformerHandler handler, Container container, Course parentCourse) throws AmetysRepositoryException, SAXException
442    {
443        if (container != null)
444        {
445            // Année
446            XMLUtils.createElement(handler, "annee", container.getTitle());
447            // COD_ETP
448            XMLUtils.createElement(handler, "COD_ETP", container.getValue("etpCode", false, StringUtils.EMPTY));
449            // COD_VRS_ETP
450            XMLUtils.createElement(handler, "COD_VRS_ETP", container.getValue("vrsEtpCode", false, StringUtils.EMPTY));
451            // Code Ametys ELP père 
452            XMLUtils.createElement(handler, "codeELPPere", parentCourse != null ? parentCourse.getCode() : "");
453        }
454    }
455    
456    private void _saxSubProgram(TransformerHandler handler, SubProgram subprogram) throws AmetysRepositoryException, SAXException
457    {
458        if (subprogram != null)
459        {
460            // Parcours 
461            XMLUtils.createElement(handler, "parcours", subprogram.getTitle());
462            XMLUtils.createElement(handler, "parcoursCode", subprogram.getCode());
463        }
464    }
465    
466    private void _saxOrgUnits(TransformerHandler handler, Program program) throws SAXException
467    {
468        StringBuilder sb = new StringBuilder();
469        List<String> orgUnits = program.getOrgUnits();
470        for (String orgUnitId : orgUnits)
471        {
472            try
473            {
474                OrgUnit orgUnit = _resolver.resolveById(orgUnitId);
475                if (sb.length() > 0)
476                {
477                    sb.append(", ");
478                }
479                sb.append(orgUnit.getTitle());
480                sb.append(" (");
481                sb.append(orgUnit.getUAICode());
482                sb.append(")");
483            }
484            catch (UnknownAmetysObjectException e)
485            {
486                getLogger().info("La composante référencée par la formation {} ({}) n'a pas été trouvée.", program.getTitle(), program.getCode());
487            }
488        }
489        XMLUtils.createElement(handler, "orgUnit", sb.toString());
490    }
491    
492    private String _getPorte(Container etape, Container containerYear)
493    {
494        String porte = "";
495        if (etape != null && containerYear != null)
496        {
497            String etpCode = containerYear.getValue("etpCode", false, StringUtils.EMPTY);
498            if (StringUtils.isNotEmpty(etpCode))
499            {
500                porte = etpCode.equals(etape.getValue("etpCode", false, StringUtils.EMPTY)) ? "X" : StringUtils.EMPTY;
501            }
502        }
503        return porte;
504    }
505    
506    private OrgUnit _getOrgUnit(Course course, String hierarchy)
507    {
508        OrgUnit orgUnit = null;
509        
510        List<String> courseOrgUnits = course.getOrgUnits();
511        if (!courseOrgUnits.isEmpty())
512        {
513            try
514            {
515                orgUnit = _resolver.resolveById(courseOrgUnits.get(0));
516            }
517            catch (UnknownAmetysObjectException e)
518            {
519                getLogger().info("La composante référencée par l'élément pédagogique {} ({}) n'a pas été trouvée.", hierarchy, course.getCode());
520            }
521            
522            if (courseOrgUnits.size() > 1)
523            {
524                getLogger().warn("L'élément pédagogique {} ({}) référence plus d'une composante.", hierarchy, course.getCode());
525            }
526        }
527        
528        return orgUnit;
529    }
530    
531    private Container _getEtapePorteuse(Course course, String hierarchy)
532    {
533        return Optional.ofNullable((ContentValue) course.getValue("etapePorteuse"))
534                .flatMap(contentValue -> _getEtapePorteuseIfExists(contentValue, course, hierarchy))
535                .map(Container.class::cast)
536                .orElse(null);
537    }
538    
539    private Optional<ModifiableContent> _getEtapePorteuseIfExists(ContentValue etapePorteuse, Course course, String hierarchy)
540    {
541        try
542        {
543            return Optional.ofNullable(etapePorteuse.getContent());
544        }
545        catch (UnknownAmetysObjectException e)
546        {
547            getLogger().info("L'étape porteuse référencée par l'élément pédagogique {} ({}) n'a pas été trouvée.", hierarchy, course.getCode());
548            return Optional.empty();
549        }
550    }
551    
552    private void _saxCourseList(TransformerHandler handler, CourseList list, Integer position) throws SAXException
553    {
554        if (list != null)
555        {
556            XMLUtils.createElement(handler, "list", "Lst" + position);
557            _saxChoiceList(handler, list);
558        }
559    }
560    
561    private void _saxChoiceList(TransformerHandler handler, CourseList list) throws SAXException
562    {
563        // Type
564        ChoiceType typeList = list.getType();
565        if (typeList != null)
566        {
567            if (typeList.name() != null && (typeList.name().equals(ChoiceType.CHOICE.toString()) || typeList.name().equals(ChoiceType.MANDATORY.toString()) || typeList.name().equals(ChoiceType.OPTIONAL.toString())))
568            {
569                String typeListAsString = "";
570                if (typeList.name().equals(ChoiceType.CHOICE.toString()))
571                {
572                    typeListAsString = "X";
573                }
574                else if (typeList.name().equals(ChoiceType.MANDATORY.toString()))
575                {
576                    typeListAsString = "O";
577                }
578                else if (typeList.name().equals(ChoiceType.OPTIONAL.toString()))
579                {
580                    typeListAsString = "F";
581                }
582                
583                XMLUtils.createElement(handler, "typeList", typeListAsString);
584            }
585            
586            // Min-Max (Ne remplir que pour le type "CHOICE" (ne mettre que le min))
587            if (typeList.name() != null && typeList.name().equals(ChoiceType.CHOICE.toString()))
588            {
589                XMLUtils.createElement(handler, "minmax", _reportHelper.formatNumberToSax(list.getMinNumberOfCourses()));
590            }
591        }
592    }
593    
594    class ProgramTitleComparator implements Comparator<Program>
595    {
596        @Override
597        public int compare(Program p1, Program p2)
598        {
599            
600            return p1.getTitle().compareTo(p2.getTitle());
601        }
602    }
603}