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.Content;
042import org.ametys.cms.repository.ModifiableContent;
043import org.ametys.core.util.DateUtils;
044import org.ametys.odf.ProgramItem;
045import org.ametys.odf.course.Course;
046import org.ametys.odf.courselist.CourseList;
047import org.ametys.odf.courselist.CourseList.ChoiceType;
048import org.ametys.odf.coursepart.CoursePart;
049import org.ametys.odf.enumeration.OdfReferenceTableEntry;
050import org.ametys.odf.orgunit.OrgUnit;
051import org.ametys.odf.program.Container;
052import org.ametys.odf.program.Program;
053import org.ametys.odf.program.SubProgram;
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 = _getReportFileName(catalog, lang, _reportHelper.getAccronymOrUaiCode(uaiCode));
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        OrgUnit rootOrgUnit = _odfHelper.getOrgUnitByUAICode(uaiCode);
166        if (rootOrgUnit == null)
167        {
168            throw new IllegalArgumentException("Unable to find any organization unit with the uai code : " + uaiCode + ".The processing of the Apogee report is aborted.");
169        }
170        
171        Map<Program, Object> programTree = new TreeMap<>(new ProgramTitleComparator());
172        
173        // On ne récupère que les composantes, enfant direct du root org unit, et on ignore les départements.
174        if (rootOrgUnit.getParentOrgUnit() != null && rootOrgUnit.getParentOrgUnit().getParentOrgUnit() == null)
175        {
176            // Chercher les programmes concernés par la composante sélectionnée et ses enfants
177            List<Program> programs = _odfHelper.getProgramsFromOrgUnit(rootOrgUnit, catalog, lang);
178            for (Program program : programs)
179            {
180                Map<ProgramItem, Object> courses = _reportHelper.getCoursesFromContent(program);
181                if (courses != null)
182                {
183                    programTree.put(program, courses);
184                }
185            }
186        }
187        
188        return programTree;
189    }
190
191    /**
192     * Sax the information related to the courses of the tree
193     * @param handler the transformer handler
194     * @param programTree the program tree to sax
195     * @throws SAXException if an error occurs when SAXing
196     */
197    @SuppressWarnings("unchecked")
198    private void _saxTree(TransformerHandler handler, Map<Program, Object> programTree) throws SAXException
199    {
200        for (Entry<Program, Object> programEntry : programTree.entrySet())
201        {
202            if (programEntry.getValue() != null && programEntry.getValue() instanceof Map<?, ?>)
203            {
204                _saxCourseFromTree(handler, (Map<ProgramItem, Object>) programEntry.getValue(), programEntry.getKey());
205            }
206        }
207    }
208
209
210    private void _saxCourseFromTree(TransformerHandler handler, Map<ProgramItem, Object> programTree, Program program) throws SAXException
211    {
212        _saxCourseFromTree(handler, programTree, program, null, null, null, null, null, null, 1, "");
213    }
214
215    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
216    {
217        int courseListPosition = 0;
218        for (Entry<ProgramItem, Object> entry : tree.entrySet())
219        {
220            ProgramItem child = entry.getKey();
221            @SuppressWarnings("unchecked")
222            Map<ProgramItem, Object> subTree = (Map<ProgramItem, Object>) entry.getValue();
223            
224            if (child instanceof Course)
225            {
226                Course childCourse = (Course) child;
227                String path = courseHierarchy +  " > " + childCourse.getTitle();
228                _saxCourse(handler, program, subprogram, containerYear, containerSemester, list, listPosition, (Course) child, parentCourse, level, path);
229                
230                if (subTree != null)
231                {
232                    _saxCourseFromTree(handler, subTree, program, subprogram, containerYear, containerSemester, list, listPosition, childCourse, level + 1, path);
233                }
234            }
235            
236            if (subTree != null)
237            {
238                if (child instanceof Program)
239                {
240                    _saxCourseFromTree(handler, subTree, (Program) child, subprogram, containerYear, containerSemester, list, listPosition, parentCourse, level, courseHierarchy);
241                }
242                else if (child instanceof Container)
243                {
244                    Container container = (Container) child;
245                    String containerNature = _refTableHelper.getItemCode(container.getNature());
246                    
247                    if ("annee".equals(containerNature))
248                    {
249                        _saxCourseFromTree(handler, subTree, program, subprogram, container, containerSemester, list, listPosition, parentCourse, level, courseHierarchy);
250                    }
251                    else if ("semestre".equals(containerNature))
252                    {
253                        _saxCourseFromTree(handler, subTree, program, subprogram, containerYear, container, list, listPosition, parentCourse, level, courseHierarchy);
254                    }
255                    else
256                    {
257                        _saxCourseFromTree(handler, subTree, program, subprogram, containerYear, containerSemester, list, listPosition, parentCourse, level, courseHierarchy);
258                    }
259                }
260                else if (child instanceof SubProgram)
261                {
262                    _saxCourseFromTree(handler, subTree, program, (SubProgram) child, containerYear, containerSemester, list, listPosition, parentCourse, level, courseHierarchy);
263                }
264                else if (child instanceof CourseList)
265                {
266                    courseListPosition++;
267                    CourseList childCourseList = (CourseList) child;
268                    String path = courseHierarchy.equals("") ? childCourseList.getTitle() : courseHierarchy +  " > " + childCourseList.getTitle();
269                    _saxCourseFromTree(handler, subTree, program, subprogram, containerYear, containerSemester, (CourseList) child, courseListPosition, parentCourse, level, path);
270                }
271            }
272        }
273    }
274    
275    private String _getHierarchy(Program program, SubProgram subprogram, Container containerYear, Container containerSemester, String courseHierarchy)
276    {
277        String hierarchy = program.getTitle();
278        if (subprogram != null)
279        {
280            hierarchy += " > " + subprogram.getTitle();
281        }
282        
283        if (containerYear != null)
284        {
285            hierarchy += " > " + containerYear.getTitle();
286        }
287        
288        if (containerSemester != null)
289        {
290            hierarchy += " > " + containerSemester.getTitle();
291        }
292            
293        hierarchy += " > " + courseHierarchy;
294        
295        return hierarchy;
296    }
297
298    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
299    {
300        if (course != null)
301        {
302            String hierarchy = _getHierarchy(program, subprogram, containerYear, containerSemester, courseHierarchy);
303            
304            XMLUtils.startElement(handler, "course");
305            
306            // Ordre
307            XMLUtils.createElement(handler, "ordre", String.valueOf(_order));
308            
309            // Composante
310            _saxOrgUnits(handler, program);
311            
312            // Formation
313            XMLUtils.createElement(handler, "formation", program.getTitle());
314            XMLUtils.createElement(handler, "formationCode", program.getCode());
315            
316            _saxSubProgram(handler, subprogram);
317            
318            _saxContainer(handler, containerYear, parentCourse);
319            
320            _saxCourseList(handler, list, listPosition);
321            
322            // A des fils
323            boolean aDesFils = course.hasCourseLists();
324            XMLUtils.createElement(handler, "aDesFils", aDesFils ? "X" : "");
325
326            long courseListsSize = course.getParentCourseLists().size();
327            
328            // Partagé
329            XMLUtils.createElement(handler, "partage", courseListsSize > 1 ? "X" : "");
330
331            // Nb occurrences
332            XMLUtils.createElement(handler, "occurrences", _reportHelper.formatNumberToSax(courseListsSize));
333            
334            Container etape = _getEtapePorteuse(course, hierarchy);
335            
336            String porte = _getPorte(etape, containerYear);
337           
338            // Porté ("X" si l'ELP est porté par la formation (Etape porteuse=COD_ETP), vide sinon)
339            XMLUtils.createElement(handler, "porte", porte);
340            
341            // Niveau
342            XMLUtils.createElement(handler, "niveau", "niv" + level);
343
344            // Date de création
345            XMLUtils.createElement(handler, "creationDate", DateUtils.zonedDateTimeToString(course.getCreationDate()));
346
347            // Code Apogée
348            XMLUtils.createElement(handler, "codeApogee", course.getValue("elpCode", false, StringUtils.EMPTY));
349            
350            // Nature de l'élément
351            XMLUtils.createElement(handler, "nature", _refTableHelper.getItemCode(course.getCourseType()));
352
353            // Libellé court
354            XMLUtils.createElement(handler, "libelleCourt", course.getValue("shortLabel", false, StringUtils.EMPTY));
355            
356            // Libellé
357            XMLUtils.createElement(handler, "libelle", course.getTitle());
358            
359            // Code Ametys (ELP)
360            XMLUtils.createElement(handler, "elpCode", course.getCode());
361            
362            // Lieu
363            _reportHelper.saxContentAttribute(handler, course, "campus", "campus");
364
365            // Crédits ECTS
366            XMLUtils.createElement(handler, "ects", String.valueOf(course.getEcts()));
367            
368            String teachingActivity = _refTableHelper.getItemCode(course.getTeachingActivity());
369            String stage = teachingActivity.equals("SA") ? "X" : "";
370            
371            // Element stage
372            XMLUtils.createElement(handler, "stage", stage);
373            
374            // Code semestre et type de période (pair, impair, an)
375            Content period = Optional.of("period")
376                    .map(course::<ContentValue>getValue)
377                    .flatMap(ContentValue::getContentIfExists)
378                    .orElse(null);
379            if (period != null)
380            {
381                try
382                {
383                    String periodCode = Optional.of("code")
384                        .map(period::<String>getValue)
385                        .orElse(StringUtils.EMPTY);
386                    
387                    String periodTypeCode = Optional.ofNullable(period.<ContentValue>getValue("type"))
388                        .flatMap(ContentValue::getContentIfExists)
389                        .map(c -> c.<String>getValue("code"))
390                        .orElse(StringUtils.EMPTY);
391
392                    XMLUtils.createElement(handler, "periode", "s10".equals(periodCode) ? "s0" : periodCode);
393                    XMLUtils.createElement(handler, "periodeType", periodTypeCode);
394                }
395                catch (UnknownAmetysObjectException e)
396                {
397                    getLogger().error("Impossible de retrouver la période : {}", period, e);
398                }
399            }
400            
401            OrgUnit orgUnit = _getOrgUnit(course, hierarchy);
402            
403            // Code composante
404            XMLUtils.createElement(handler, "codeComposante", orgUnit != null ? orgUnit.getValue("codCmp", false, StringUtils.EMPTY) : StringUtils.EMPTY);
405            
406            // Code CIP
407            XMLUtils.createElement(handler, "codeCIP", orgUnit != null ? orgUnit.getValue("codCipApogee", false, StringUtils.EMPTY) : StringUtils.EMPTY);
408            
409            // Code ANU
410            long codeAnu = course.getValue("CodeAnu", false, 0L);
411            XMLUtils.createElement(handler, "CodeAnu", codeAnu > 0 ? String.valueOf(codeAnu) : StringUtils.EMPTY);
412            
413            // Calcul des charges 
414            XMLUtils.createElement(handler, "calculCharges", aDesFils ? "" : "X");
415            
416            // Heures d'enseignement
417            for (CoursePart coursePart : course.getCourseParts())
418            {
419                AttributesImpl attr = new AttributesImpl();
420                attr.addCDATAAttribute("nature", coursePart.getNature());
421                XMLUtils.createElement(handler, "volumeHoraire", attr, String.valueOf(coursePart.getNumberOfHours()));
422            }
423
424            // Etape porteuse pour les feuilles de l'arbre
425            if (etape != null)
426            {
427                XMLUtils.createElement(handler, "etapePorteuse", etape.getValue("etpCode", false, ""));
428                XMLUtils.createElement(handler, "vetEtapePorteuse", etape.getValue("vrsEtpCode", false, ""));
429            }
430            
431            // Discipline
432            String disciplineEnseignement = Optional.of("disciplineEnseignement")
433                    .map(course::<ContentValue>getValue)
434                    .flatMap(ContentValue::getContentIfExists)
435                    .map(OdfReferenceTableEntry::new)
436                    .map(entry -> {
437                        String code = entry.getCode();
438                        return (StringUtils.isNotEmpty(code) ? "[" + code + "] " : StringUtils.EMPTY) + entry.getLabel(course.getLanguage());
439                    })
440                    .orElse(StringUtils.EMPTY);
441            XMLUtils.createElement(handler, "discipline", disciplineEnseignement);
442            
443            saxAdditionalCourseData(handler, course);
444            
445            XMLUtils.endElement(handler, "course");
446            
447            _order++;
448        }
449    }
450    
451    /**
452     * Generates SAX events for additional data of a {@link Course}.
453     * @param handler The handler
454     * @param course The course to SAX
455     * @throws AmetysRepositoryException if an error occurs
456     * @throws SAXException if an error occurs
457     */
458    protected void saxAdditionalCourseData(TransformerHandler handler, Course course) throws AmetysRepositoryException, SAXException
459    {
460        // Do nothing by default
461    }
462    
463    private void _saxContainer(TransformerHandler handler, Container container, Course parentCourse) throws AmetysRepositoryException, SAXException
464    {
465        if (container != null)
466        {
467            // Année
468            XMLUtils.createElement(handler, "annee", container.getTitle());
469            // COD_ETP
470            XMLUtils.createElement(handler, "COD_ETP", container.getValue("etpCode", false, StringUtils.EMPTY));
471            // COD_VRS_ETP
472            XMLUtils.createElement(handler, "COD_VRS_ETP", container.getValue("vrsEtpCode", false, StringUtils.EMPTY));
473            // Code Ametys ELP père 
474            XMLUtils.createElement(handler, "codeELPPere", parentCourse != null ? parentCourse.getCode() : "");
475        }
476    }
477    
478    private void _saxSubProgram(TransformerHandler handler, SubProgram subprogram) throws AmetysRepositoryException, SAXException
479    {
480        if (subprogram != null)
481        {
482            // Parcours 
483            XMLUtils.createElement(handler, "parcours", subprogram.getTitle());
484            XMLUtils.createElement(handler, "parcoursCode", subprogram.getCode());
485        }
486    }
487    
488    private void _saxOrgUnits(TransformerHandler handler, Program program) throws SAXException
489    {
490        StringBuilder sb = new StringBuilder();
491        List<String> orgUnits = program.getOrgUnits();
492        for (String orgUnitId : orgUnits)
493        {
494            try
495            {
496                OrgUnit orgUnit = _resolver.resolveById(orgUnitId);
497                if (sb.length() > 0)
498                {
499                    sb.append(", ");
500                }
501                sb.append(orgUnit.getTitle());
502                sb.append(" (");
503                sb.append(orgUnit.getUAICode());
504                sb.append(")");
505            }
506            catch (UnknownAmetysObjectException e)
507            {
508                getLogger().info("La composante référencée par la formation {} ({}) n'a pas été trouvée.", program.getTitle(), program.getCode());
509            }
510        }
511        XMLUtils.createElement(handler, "orgUnit", sb.toString());
512    }
513    
514    private String _getPorte(Container etape, Container containerYear)
515    {
516        String porte = "";
517        if (etape != null && containerYear != null)
518        {
519            String etpCode = containerYear.getValue("etpCode", false, StringUtils.EMPTY);
520            if (StringUtils.isNotEmpty(etpCode))
521            {
522                porte = etpCode.equals(etape.getValue("etpCode", false, StringUtils.EMPTY)) ? "X" : StringUtils.EMPTY;
523            }
524        }
525        return porte;
526    }
527    
528    private OrgUnit _getOrgUnit(Course course, String hierarchy)
529    {
530        OrgUnit orgUnit = null;
531        
532        List<String> courseOrgUnits = course.getOrgUnits();
533        if (!courseOrgUnits.isEmpty())
534        {
535            try
536            {
537                orgUnit = _resolver.resolveById(courseOrgUnits.get(0));
538            }
539            catch (UnknownAmetysObjectException e)
540            {
541                getLogger().info("La composante référencée par l'élément pédagogique {} ({}) n'a pas été trouvée.", hierarchy, course.getCode());
542            }
543            
544            if (courseOrgUnits.size() > 1)
545            {
546                getLogger().warn("L'élément pédagogique {} ({}) référence plus d'une composante.", hierarchy, course.getCode());
547            }
548        }
549        
550        return orgUnit;
551    }
552    
553    private Container _getEtapePorteuse(Course course, String hierarchy)
554    {
555        return Optional.ofNullable((ContentValue) course.getValue("etapePorteuse"))
556                .flatMap(contentValue -> _getEtapePorteuseIfExists(contentValue, course, hierarchy))
557                .map(Container.class::cast)
558                .orElse(null);
559    }
560    
561    private Optional<ModifiableContent> _getEtapePorteuseIfExists(ContentValue etapePorteuse, Course course, String hierarchy)
562    {
563        try
564        {
565            return Optional.ofNullable(etapePorteuse.getContent());
566        }
567        catch (UnknownAmetysObjectException e)
568        {
569            getLogger().info("L'année porteuse référencée par l'élément pédagogique {} ({}) n'a pas été trouvée.", hierarchy, course.getCode());
570            return Optional.empty();
571        }
572    }
573    
574    private void _saxCourseList(TransformerHandler handler, CourseList list, Integer position) throws SAXException
575    {
576        if (list != null)
577        {
578            XMLUtils.createElement(handler, "list", "Lst" + position);
579            _saxChoiceList(handler, list);
580        }
581    }
582    
583    private void _saxChoiceList(TransformerHandler handler, CourseList list) throws SAXException
584    {
585        // Type
586        ChoiceType typeList = list.getType();
587        if (typeList != null)
588        {
589            if (typeList.name() != null && (typeList.name().equals(ChoiceType.CHOICE.toString()) || typeList.name().equals(ChoiceType.MANDATORY.toString()) || typeList.name().equals(ChoiceType.OPTIONAL.toString())))
590            {
591                String typeListAsString = "";
592                if (typeList.name().equals(ChoiceType.CHOICE.toString()))
593                {
594                    typeListAsString = "X";
595                }
596                else if (typeList.name().equals(ChoiceType.MANDATORY.toString()))
597                {
598                    typeListAsString = "O";
599                }
600                else if (typeList.name().equals(ChoiceType.OPTIONAL.toString()))
601                {
602                    typeListAsString = "F";
603                }
604                
605                XMLUtils.createElement(handler, "typeList", typeListAsString);
606            }
607            
608            // Min-Max (Ne remplir que pour le type "CHOICE" (ne mettre que le min))
609            if (typeList.name() != null && typeList.name().equals(ChoiceType.CHOICE.toString()))
610            {
611                XMLUtils.createElement(handler, "minmax", _reportHelper.formatNumberToSax(list.getMinNumberOfCourses()));
612            }
613        }
614    }
615    
616    class ProgramTitleComparator implements Comparator<Program>
617    {
618        @Override
619        public int compare(Program p1, Program p2)
620        {
621            
622            return p1.getTitle().compareTo(p2.getTitle());
623        }
624    }
625}