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.plugins.odfpilotage.report.impl;
017
018import java.io.File;
019import java.io.FileOutputStream;
020import java.util.Collections;
021import java.util.Comparator;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.Map.Entry;
026import java.util.Objects;
027import java.util.Optional;
028import java.util.Set;
029import java.util.TreeMap;
030import java.util.stream.Collectors;
031
032import javax.xml.transform.Result;
033import javax.xml.transform.TransformerFactory;
034import javax.xml.transform.sax.SAXTransformerFactory;
035import javax.xml.transform.sax.TransformerHandler;
036import javax.xml.transform.stream.StreamResult;
037
038import org.apache.avalon.framework.service.ServiceException;
039import org.apache.avalon.framework.service.ServiceManager;
040import org.apache.cocoon.xml.AttributesImpl;
041import org.apache.cocoon.xml.XMLUtils;
042import org.apache.commons.io.FileUtils;
043import org.apache.commons.lang3.StringUtils;
044import org.xml.sax.SAXException;
045
046import org.ametys.cms.data.ContentValue;
047import org.ametys.core.util.DateUtils;
048import org.ametys.odf.ProgramItem;
049import org.ametys.odf.course.Course;
050import org.ametys.odf.courselist.CourseList;
051import org.ametys.odf.courselist.CourseList.ChoiceType;
052import org.ametys.odf.coursepart.CoursePart;
053import org.ametys.odf.enumeration.OdfReferenceTableEntry;
054import org.ametys.odf.orgunit.OrgUnit;
055import org.ametys.odf.program.Container;
056import org.ametys.odf.program.Program;
057import org.ametys.odf.program.SubProgram;
058import org.ametys.plugins.odfpilotage.cost.CostComputationComponent;
059import org.ametys.plugins.odfpilotage.cost.entity.CostComputationData;
060import org.ametys.plugins.odfpilotage.cost.entity.CoursePartCostData;
061import org.ametys.plugins.odfpilotage.helper.ReportUtils;
062import org.ametys.plugins.repository.AmetysObjectIterable;
063import org.ametys.plugins.repository.AmetysObjectIterator;
064import org.ametys.plugins.repository.AmetysRepositoryException;
065import org.ametys.plugins.repository.UnknownAmetysObjectException;
066
067/**
068 * Pilotage report for cost model
069 */
070public class CoutMaquettesReport extends AbstractReport
071{
072    private static final String __ETAPE_PORTEUSE = "etapePorteuse";
073    private static final String __VALUE_YEAR = "annee";
074    
075    /** CalculerEffectifComponent */
076    protected CostComputationComponent _costComputationComponent;
077    
078    private int _order;
079
080
081    @Override
082    public void service(ServiceManager manager) throws ServiceException
083    {
084        super.service(manager);
085        _costComputationComponent = (CostComputationComponent) manager.lookup(CostComputationComponent.ROLE);
086    }
087    
088    @Override
089    protected String getType()
090    {
091        return "coutmaquettes";
092    }
093
094    @Override
095    protected Set<String> getSupportedOutputFormats()
096    {
097        return Set.of(OUTPUT_FORMAT_XLS);
098    }
099    
100    @Override
101    protected void _launchByOrgUnit(String uaiCode, String catalog, String lang) throws Exception
102    {
103        AmetysObjectIterable<OrgUnit> orgUnits = _reportHelper.getRootOrgUnitsByUaiCode(uaiCode);
104        AmetysObjectIterator<OrgUnit> orgUnitsIterator = orgUnits.iterator();
105    
106        List<Program> selectedPrograms = _reportHelper.filterProgramsFromOrgUnits(orgUnitsIterator.next(), lang, catalog);
107        CostComputationData costData = _costComputationComponent.computeCostsOnPrograms(selectedPrograms);
108
109        _writeCoutMaquettesReport(uaiCode, catalog, lang, costData);
110    }
111    
112    /**
113     * Create the groups' report for one organization unit 
114     * @param uaiCode the reference to the organization unit whose formations are going to be saxed
115     * @param catalog the catalog
116     * @param lang The language code
117     * @param costData object containing informations about the formation
118     */
119    private void _writeCoutMaquettesReport(String uaiCode, String catalog, String lang, CostComputationData costData)
120    {
121        SAXTransformerFactory factory = (SAXTransformerFactory) TransformerFactory.newInstance();
122        String fileName = _getReportFileName(catalog, lang, _reportHelper.getAccronymOrUaiCode(uaiCode));
123        
124        // Delete old files
125        File xmlFile = new File(_tmpFolder, fileName + ".xml");
126        FileUtils.deleteQuietly(xmlFile);
127        
128        // Write XML file
129        try (FileOutputStream fos = new FileOutputStream(xmlFile))
130        {
131            TransformerHandler handler = factory.newTransformerHandler();
132            
133            // Prepare the transformation
134            Result result = new StreamResult(fos);
135            handler.setResult(result);
136            handler.startDocument();
137        
138            AttributesImpl attrs = new AttributesImpl();
139            attrs.addCDATAAttribute("type", getType());
140            attrs.addCDATAAttribute("date", _reportHelper.getReadableCurrentDate());
141            XMLUtils.startElement(handler, "report", attrs);
142            
143            _generateReport(handler, costData, uaiCode, lang, catalog);
144            
145            XMLUtils.endElement(handler, "report");
146            handler.endDocument();
147
148            // Convert the report to configured output format
149            convertReport(_tmpFolder, fileName, xmlFile);
150        }
151        catch (Exception e)
152        {
153            getLogger().error("An error occured while generating 'Coût des maquettes' report for orgunit '{}'", uaiCode, e);
154        }
155        finally
156        {
157            FileUtils.deleteQuietly(xmlFile);
158        }
159    }
160    
161    private void _generateReport(TransformerHandler handler, CostComputationData costData, String uaiCode, String lang, String catalog)
162    {
163        if (StringUtils.isEmpty(uaiCode))
164        {
165            throw new IllegalArgumentException("Cannot process the Apogee report without the uai code.The processing of the Apogee report is aborted.");
166        }
167        
168        try
169        {
170            Map<Program, Object> contentsTree = _getStructure(uaiCode, lang, catalog);
171            if (contentsTree.size() > 0)
172            {
173                _order = 1;
174                _saxTree(handler, contentsTree);
175            }
176            
177            _writeColumns(handler, costData);
178            _writeLines(handler, costData);
179        }
180        catch (Exception e)
181        {
182            getLogger().error("An error occurred while processing the apogee report. Its generation has been aborted.", e);
183        }
184    }
185    
186    /**
187     * Sax the information related to the courses of the tree
188     * @param handler the transformer handler
189     * @param programTree the program tree to sax
190     * @throws SAXException if an error occurs when SAXing
191     */
192    @SuppressWarnings("unchecked")
193    private void _saxTree(TransformerHandler handler, Map<Program, Object> programTree) throws SAXException
194    {
195        for (Entry<Program, Object> programEntry : programTree.entrySet())
196        {
197            if (programEntry.getValue() != null && programEntry.getValue() instanceof Map<?, ?>)
198            {
199                _saxCourseFromTree(handler, (Map<ProgramItem, Object>) programEntry.getValue(), programEntry.getKey());
200            }
201        }
202    }
203
204    private void _saxCourseFromTree(TransformerHandler handler, Map<ProgramItem, Object> programTree, Program program) throws SAXException
205    {
206        _saxCourseFromTree(handler, programTree, program, null, null, null, null, null, null, 1, "");
207    }
208
209    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
210    {
211        int courseListPosition = 0;
212        for (Entry<ProgramItem, Object> entry : tree.entrySet())
213        {
214            ProgramItem child = entry.getKey();
215            @SuppressWarnings("unchecked")
216            Map<ProgramItem, Object> subTree = (Map<ProgramItem, Object>) entry.getValue();
217            
218            if (child instanceof Course)
219            {
220                Course childCourse = (Course) child;
221                String path = courseHierarchy +  " > " + childCourse.getTitle();
222                _saxCourse(handler, program, subprogram, containerYear, containerSemester, list, listPosition, (Course) child, parentCourse, level, subTree == null ? true : false, path);
223                
224                if (subTree != null)
225                {
226                    _saxCourseFromTree(handler, subTree, program, subprogram, containerYear, containerSemester, list, listPosition, childCourse, level + 1, path);
227                }
228            }
229            
230            if (subTree != null)
231            {
232                if (child instanceof Program)
233                {
234                    _saxCourseFromTree(handler, subTree, (Program) child, subprogram, containerYear, containerSemester, list, listPosition, parentCourse, level, courseHierarchy);
235                }
236                else if (child instanceof Container)
237                {
238                    Container container = (Container) child;
239                    String containerNature = _refTableHelper.getItemCode(container.getNature());
240                    
241                    if ("annee".equals(containerNature))
242                    {
243                        _saxCourseFromTree(handler, subTree, program, subprogram, container, containerSemester, list, listPosition, parentCourse, level, courseHierarchy);
244                    }
245                    else if ("semestre".equals(containerNature))
246                    {
247                        _saxCourseFromTree(handler, subTree, program, subprogram, containerYear, container, list, listPosition, parentCourse, level, courseHierarchy);
248                    }
249                    else
250                    {
251                        _saxCourseFromTree(handler, subTree, program, subprogram, containerYear, containerSemester, list, listPosition, parentCourse, level, courseHierarchy);
252                    }
253                }
254                else if (child instanceof SubProgram)
255                {
256                    _saxCourseFromTree(handler, subTree, program, (SubProgram) child, containerYear, containerSemester, list, listPosition, parentCourse, level, courseHierarchy);
257                }
258                else if (child instanceof CourseList)
259                {
260                    courseListPosition++;
261                    CourseList childCourseList = (CourseList) child;
262                    String path = courseHierarchy.equals("") ? childCourseList.getTitle() : courseHierarchy +  " > " + childCourseList.getTitle();
263                    _saxCourseFromTree(handler, subTree, program, subprogram, containerYear, containerSemester, (CourseList) child, courseListPosition, parentCourse, level, path);
264                }
265            }
266        }
267    }
268    
269    private String _getHierarchy(Program program, SubProgram subprogram, Container containerYear, Container containerSemester, String courseHierarchy)
270    {
271        String hierarchy = program.getTitle();
272        if (subprogram != null)
273        {
274            hierarchy += " > " + subprogram.getTitle();
275        }
276        
277        if (containerYear != null)
278        {
279            hierarchy += " > " + containerYear.getTitle();
280        }
281        
282        if (containerSemester != null)
283        {
284            hierarchy += " > " + containerSemester.getTitle();
285        }
286            
287        hierarchy += " > " + courseHierarchy;
288        
289        return hierarchy;
290    }
291
292    private void _saxCourse(TransformerHandler handler, Program program, SubProgram subprogram, Container containerYear, Container containerSemester, CourseList list, Integer listPosition, Course course, Course parentCourse, int level, boolean lastLevel, String courseHierarchy) throws SAXException
293    {
294        if (course != null)
295        {
296            String hierarchy = _getHierarchy(program, subprogram, containerYear, containerSemester, courseHierarchy);
297            
298            XMLUtils.startElement(handler, "course");
299            
300            // Ordre
301            XMLUtils.createElement(handler, "ordre", String.valueOf(_order));
302            
303            // Composante
304            _saxOrgUnits(handler, program);
305            
306            // Formation
307            XMLUtils.createElement(handler, "formation", program.getTitle());
308            XMLUtils.createElement(handler, "formationCode", program.getCode());
309            
310            _saxSubProgram(handler, subprogram);
311            
312            _saxContainer(handler, containerYear, parentCourse);
313            
314            _saxCourseList(handler, list, listPosition);
315            
316            // A des fils
317            boolean aDesFils = course.hasCourseLists();
318            XMLUtils.createElement(handler, "aDesFils", aDesFils ? "X" : "");
319
320            long courseListsSize = course.getParentCourseLists().size();
321            
322            // Partagé
323            XMLUtils.createElement(handler, "partage", courseListsSize > 1 ? "X" : "");
324
325            // Nb occurrences
326            XMLUtils.createElement(handler, "occurrences", _reportHelper.formatNumberToSax(courseListsSize));
327            
328            Container etape = _getEtapePorteuse(course, hierarchy);
329            
330            String porte = _getPorte(etape, containerYear);
331           
332            // Porté ("X" si l'ELP est porté par la formation (Etape porteuse=COD_ETP), vide sinon)
333            XMLUtils.createElement(handler, "porte", porte);
334            
335            // Niveau
336            XMLUtils.createElement(handler, "niveau", "niv" + level);
337
338            // Date de création
339            XMLUtils.createElement(handler, "creationDate", DateUtils.dateToString(course.getCreationDate()));
340
341            // Code Apogée
342            XMLUtils.createElement(handler, "codeApogee", course.getValue("elpCode", false, StringUtils.EMPTY));
343            
344            // Nature de l'élément
345            XMLUtils.createElement(handler, "nature", _refTableHelper.getItemCode(course.getCourseType()));
346
347            // Libellé court
348            XMLUtils.createElement(handler, "libelleCourt", course.getValue("shortLabel", false, StringUtils.EMPTY));
349            
350            // Libellé
351            XMLUtils.createElement(handler, "libelle", course.getTitle());
352            
353            // Code Ametys (ELP)
354            XMLUtils.createElement(handler, "elpCode", course.getCode());
355            
356            // Lieu
357            _reportHelper.saxContentAttribute(handler, course, "campus", "campus");
358
359            // Crédits ECTS
360            XMLUtils.createElement(handler, "ects", String.valueOf(course.getEcts()));
361            
362            String teachingActivity = _refTableHelper.getItemCode(course.getTeachingActivity());
363            String stage = teachingActivity.equals("SA") ? "X" : "";
364            
365            // Element stage
366            XMLUtils.createElement(handler, "stage", stage);
367            
368            // Code semestre
369            String periode = _refTableHelper.getItemCode(course.getTeachingTerm());
370            XMLUtils.createElement(handler, "periode", "s10".equals(periode) ? "s0" : periode);
371            
372            OrgUnit orgUnit = _getOrgUnit(course, hierarchy);
373
374            // Code composante
375            String codeComposante = Optional.ofNullable(orgUnit)
376                .map(o -> o.<String>getValue("codCmp"))
377                .orElse(StringUtils.EMPTY);
378            XMLUtils.createElement(handler, "codeComposante", codeComposante);
379            
380            // Code CIP
381            String codeCIP = Optional.ofNullable(orgUnit)
382                .map(o -> o.<String>getValue("codCipApogee"))
383                .orElse(StringUtils.EMPTY);
384            XMLUtils.createElement(handler, "codeCIP", codeCIP);
385            
386            // Code ANU
387            long codeAnu = course.getValue("CodeAnu", false, 0L);
388            XMLUtils.createElement(handler, "CodeAnu", codeAnu > 0 ? String.valueOf(codeAnu) : StringUtils.EMPTY);
389            
390            // Calcul des charges 
391            XMLUtils.createElement(handler, "calculCharges", aDesFils ? "" : "X");
392
393            // Discipline
394            String disciplineEnseignement = Optional.of("disciplineEnseignement")
395                    .map(course::<ContentValue>getValue)
396                    .flatMap(ContentValue::getContentIfExists)
397                    .map(OdfReferenceTableEntry::new)
398                    .map(entry -> {
399                        String code = entry.getCode();
400                        return (StringUtils.isNotEmpty(code) ? "[" + code + "] " : StringUtils.EMPTY) + entry.getLabel(course.getLanguage());
401                    })
402                    .orElse(StringUtils.EMPTY);
403            XMLUtils.createElement(handler, "discipline", disciplineEnseignement);
404            
405            // Etape porteuse
406            String etapePorteuse = lastLevel ? _getEtapeTitle(etape) : StringUtils.EMPTY;
407            XMLUtils.createElement(handler, "etapePorteuse", etapePorteuse);
408            
409            if (lastLevel)
410            {
411                // Heures d'enseignement
412                XMLUtils.startElement(handler, "courseParts");
413                for (CoursePart coursePart : course.getCourseParts())
414                {
415                    AttributesImpl attr = new AttributesImpl();
416                    attr.addCDATAAttribute("id", coursePart.getId());
417                    XMLUtils.createElement(handler, "coursePart", attr);
418                }
419                XMLUtils.endElement(handler, "courseParts");
420            }
421            
422            saxAdditionalCourseData(handler, course);
423            
424            XMLUtils.endElement(handler, "course");
425            
426            _order++;
427        }
428    }
429    
430    private String _getEtapeTitle(Container etape)
431    {
432        if (etape == null)
433        {
434            return StringUtils.EMPTY;
435        }
436        
437        StringBuilder etapeTitle = new StringBuilder();
438        etapeTitle.append("[");
439        etapeTitle.append(etape.getCode());
440        etapeTitle.append("] ");
441        etapeTitle.append(etape.getTitle());
442        
443        String etpCode = etape.getValue("etpCode");
444        if (StringUtils.isNotBlank(etpCode))
445        {
446            etapeTitle.append(" (");
447            etapeTitle.append(etpCode);
448            String vrsEtpCode = etape.getValue("vrsEtpCode");
449            if (StringUtils.isNotBlank(vrsEtpCode))
450            {
451                etapeTitle.append("-");
452                etapeTitle.append(vrsEtpCode);
453            }
454            etapeTitle.append(")");
455        }
456        
457        return etapeTitle.toString();
458    }
459
460    /**
461     * Sax a additional data of a {@link Course}.
462     * @param handler The handler
463     * @param course The course to SAX
464     * @throws AmetysRepositoryException if an error occurs
465     * @throws SAXException if an error occurs
466     */
467    protected void saxAdditionalCourseData(TransformerHandler handler, Course course) throws AmetysRepositoryException, SAXException
468    {
469        // Do nothing by default
470    }
471    
472    private void _saxContainer(TransformerHandler handler, Container container, Course parentCourse) throws AmetysRepositoryException, SAXException
473    {
474        if (container != null)
475        {
476            // Année
477            XMLUtils.createElement(handler, "annee", container.getTitle());
478            // COD_ETP
479            XMLUtils.createElement(handler, "COD_ETP", container.getValue("etpCode", false, StringUtils.EMPTY));
480            // COD_VRS_ETP
481            XMLUtils.createElement(handler, "COD_VRS_ETP", container.getValue("vrsEtpCode", false, StringUtils.EMPTY));
482            // Code Ametys ELP père 
483            XMLUtils.createElement(handler, "codeELPPere", parentCourse != null ? parentCourse.getCode() : "");
484        }
485    }
486    
487    private void _saxSubProgram(TransformerHandler handler, SubProgram subprogram) throws AmetysRepositoryException, SAXException
488    {
489        if (subprogram != null)
490        {
491            // Parcours 
492            XMLUtils.createElement(handler, "parcours", subprogram.getTitle());
493            XMLUtils.createElement(handler, "parcoursCode", subprogram.getCode());
494        }
495    }
496    
497    private void _saxOrgUnits(TransformerHandler handler, Program program) throws SAXException
498    {
499        StringBuilder sb = new StringBuilder();
500        List<String> orgUnits = program.getOrgUnits();
501        for (String orgUnitId : orgUnits)
502        {
503            try
504            {
505                OrgUnit orgUnit = _resolver.resolveById(orgUnitId);
506                if (sb.length() > 0)
507                {
508                    sb.append(", ");
509                }
510                sb.append(orgUnit.getTitle());
511                sb.append(" (");
512                sb.append(orgUnit.getUAICode());
513                sb.append(")");
514            }
515            catch (UnknownAmetysObjectException e)
516            {
517                getLogger().info("La composante référencée par la formation {} ({}) n'a pas été trouvée.", program.getTitle(), program.getCode());
518            }
519        }
520        XMLUtils.createElement(handler, "orgUnit", sb.toString());
521    }
522    
523    private String _getPorte(Container etape, Container containerYear)
524    {
525        String porte = "";
526        if (etape != null && containerYear != null)
527        {
528            String etpCode = containerYear.getValue("etpCode", false, StringUtils.EMPTY);
529            if (StringUtils.isNotEmpty(etpCode))
530            {
531                porte = etpCode.equals(etape.getValue("etpCode", false, StringUtils.EMPTY)) ? "X" : "";
532            }
533        }
534        return porte;
535    }
536    
537    private OrgUnit _getOrgUnit(Course course, String hierarchy)
538    {
539        OrgUnit orgUnit = null;
540        
541        List<String> courseOrgUnits = course.getOrgUnits();
542        if (!courseOrgUnits.isEmpty())
543        {
544            try
545            {
546                orgUnit = _resolver.resolveById(courseOrgUnits.get(0));
547            }
548            catch (UnknownAmetysObjectException e)
549            {
550                getLogger().info("La composante référencée par l'élément pédagogique {} ({}) n'a pas été trouvée.", hierarchy, course.getCode());
551            }
552            
553            if (courseOrgUnits.size() > 1)
554            {
555                getLogger().warn("L'élément pédagogique {} ({}) référence plus d'une composante.", hierarchy, course.getCode());
556            }
557        }
558        
559        return orgUnit;
560    }
561    
562    private Container _getEtapePorteuse(Course course, String hierarchy)
563    {
564        // Use a very useful component from ODF OSE
565        Set<Container> stepsHolder = _getStepsHolder(course);
566        switch (stepsHolder.size())
567        {
568            case 0:
569                getLogger().info("L'élément pédagogique {} ({}) n'est rattaché à aucune étape.", hierarchy, course.getCode());
570                break;
571            case 1:
572                return stepsHolder.stream().findFirst().get();
573            default:
574                getLogger().info("Impossible de définir une étape porteuse unique sur l'élément pédagogique {} ({}).", hierarchy, course.getCode());
575                break;
576        }
577        
578        return null;
579    }
580    
581    /**
582     * Find steps holders
583     * @param programItem the item we want to explore
584     * @return a list of steps holders
585     */
586    protected Set<Container> _getStepsHolder(ProgramItem programItem)
587    {
588        // Search if the current element is a course and has a step holder
589        if (programItem instanceof Course)
590        {
591            Course course = (Course) programItem;
592            ContentValue stepHolder = course.getValue(__ETAPE_PORTEUSE);
593            if (stepHolder != null)
594            {
595                try
596                {
597                    return Collections.singleton((Container) stepHolder.getContent());
598                }
599                catch (AmetysRepositoryException e)
600                {
601                    if (getLogger().isWarnEnabled())
602                    {
603                        getLogger().warn("L'étape porteuse {} référencée par l'ELP {} ({}) n'a pas été trouvée. Vérifiez qu'elle n'a pas été supprimée.", stepHolder.getContentId(), course.getTitle(), course.getId());
604                    }
605                }
606            }
607        }
608        // Search if the current element is a container and is of type year
609        else if (programItem instanceof Container)
610        {
611            Container container = (Container) programItem;
612            if (_refTableHelper.getItemCode(container.getNature()).equals(__VALUE_YEAR))
613            {
614                return Collections.singleton(container);
615            }
616        }
617        
618        // In all other cases, search in the parent elements
619        return _odfHelper.getParentProgramItems(programItem)
620            .stream()
621            .map(child -> _getStepsHolder(child))
622            .flatMap(Set::stream)
623            .collect(Collectors.toSet());
624    }
625    
626    private void _saxCourseList(TransformerHandler handler, CourseList list, Integer position) throws SAXException
627    {
628        if (list != null)
629        {
630            XMLUtils.createElement(handler, "list", "Lst" + position);
631            _saxChoiceList(handler, list);
632        }
633    }
634    
635    private void _saxChoiceList(TransformerHandler handler, CourseList list) throws SAXException
636    {
637        // Type
638        ChoiceType typeList = list.getType();
639        if (typeList != null)
640        {
641            if (typeList.name() != null && (typeList.name().equals(ChoiceType.CHOICE.toString()) || typeList.name().equals(ChoiceType.MANDATORY.toString()) || typeList.name().equals(ChoiceType.OPTIONAL.toString())))
642            {
643                String typeListAsString = "";
644                if (typeList.name().equals(ChoiceType.CHOICE.toString()))
645                {
646                    typeListAsString = "X";
647                }
648                else if (typeList.name().equals(ChoiceType.MANDATORY.toString()))
649                {
650                    typeListAsString = "O";
651                }
652                else if (typeList.name().equals(ChoiceType.OPTIONAL.toString()))
653                {
654                    typeListAsString = "F";
655                }
656                
657                XMLUtils.createElement(handler, "typeList", typeListAsString);
658            }
659            
660            // Min-Max (Ne remplir que pour le type "CHOICE" (ne mettre que le min))
661            if (typeList.name() != null && typeList.name().equals(ChoiceType.CHOICE.toString()))
662            {
663                XMLUtils.createElement(handler, "minmax", _reportHelper.formatNumberToSax(list.getMinNumberOfCourses()));
664            }
665        }
666    }
667    
668    class ProgramTitleComparator implements Comparator<Program>
669    {
670        @Override
671        public int compare(Program p1, Program p2)
672        {
673            
674            return p1.getTitle().compareTo(p2.getTitle());
675        }
676    }
677    
678    /**
679     * Generate the data structure that will be used to create the report
680     * @param uaiCode the uai code of the organization unit
681     * @param lang the lang of programs
682     * @param catalog the catalog of programs
683     * @return The structure
684     */
685    private Map<Program, Object> _getStructure(String uaiCode, String lang, String catalog)
686    {
687        AmetysObjectIterable<OrgUnit> orgUnits = _reportHelper.getRootOrgUnitsByUaiCode(uaiCode);
688        AmetysObjectIterator<OrgUnit> orgUnitsIterator = orgUnits.iterator();
689        if (!orgUnitsIterator.hasNext())
690        {
691            throw new IllegalArgumentException("Unable to find any organization unit with the uai code : " + uaiCode + ".The processing of the Apogee report is aborted.");
692        }
693        
694        Map<Program, Object> programTree = new TreeMap<>(new ProgramTitleComparator());
695        
696        OrgUnit rootOrgUnit = orgUnitsIterator.next();
697        // On ne récupère que les composantes, enfant direct du root org unit, et on ignore les départements.
698        if (rootOrgUnit.getParentOrgUnit() != null && rootOrgUnit.getParentOrgUnit().getParentOrgUnit() == null)
699        {
700            List<String> orgUnitIds = _reportHelper.getSubOrgUnits(rootOrgUnit);
701            
702            // Chercher les programmes concernés par la composante sélectionnée et ses enfants
703            for (String orgUnitId : orgUnitIds)
704            {
705                AmetysObjectIterable<Program> programs = _reportHelper.getProgramsByOrgUnitId(orgUnitId, lang, catalog);
706                AmetysObjectIterator<Program> programsIterator = programs.iterator();
707                while (programsIterator.hasNext())
708                {
709                    Program program = programsIterator.next();
710                    Map<ProgramItem, Object> courses = _reportHelper.getCoursesFromContent(program);
711                    if (courses != null)
712                    {
713                        programTree.put(program, courses);
714                    }
715                }
716            }
717        }
718        
719        return programTree;
720    }
721    
722    /**
723     * Write lines content of the report
724     * @param handler the transformer handler
725     * @param costData informations about the capacity 
726     * @throws SAXException to handle XMLUtils exceptions
727     */
728    private void _writeLines(TransformerHandler handler, CostComputationData costData) throws SAXException
729    {
730        // Remove null values before wirting it (the null values have been stored to avoid the same operation twice)
731        costData.getCoursePartCostData().values().removeIf(Objects::isNull);
732
733        XMLUtils.startElement(handler, "courseParts");
734        
735        // Write a course part by line
736        for (Entry<CoursePart, CoursePartCostData> entry : costData.getCoursePartCostData().entrySet())
737        {
738            AttributesImpl attr = new AttributesImpl();
739            attr.addCDATAAttribute("id", entry.getKey().getId());
740            XMLUtils.startElement(handler, "coursePart", attr);
741            Map<String, String> calculatedCoursePart = this.getValues(costData, entry.getKey(), entry.getValue());
742            for (String columnName : calculatedCoursePart.keySet())
743            {
744                XMLUtils.createElement(handler, columnName, calculatedCoursePart.get(columnName));
745            }
746            
747            // Ventilation des effectifs
748            Map<Container, Double> ventilationEffectif = costData.getEffectiveByStep(entry.getKey().getId());
749            if (ventilationEffectif != null)
750            {
751                for (Container etape : ventilationEffectif.keySet())
752                {
753                    AttributesImpl attrs = new AttributesImpl();
754                    attrs.addCDATAAttribute("id", etape.getId());
755                    XMLUtils.createElement(handler, "effectif", attrs, ReportUtils.FORMAT_2_DIGITS.format(ventilationEffectif.get(etape)));
756                }
757            }
758            
759            // Ventilation des eqTD
760            Map<Container, Double> ventilationEqTD = costData.getEqTDByStep(entry.getKey().getId());
761            if (ventilationEqTD != null)
762            {
763                for (Container etape : ventilationEqTD.keySet())
764                {
765                    AttributesImpl attrs = new AttributesImpl();
766                    attrs.addCDATAAttribute("id", etape.getId());
767                    XMLUtils.createElement(handler, "eqTD", attrs, ReportUtils.FORMAT_2_DIGITS.format(ventilationEqTD.get(etape)));
768                }
769            }
770            
771            XMLUtils.endElement(handler, "coursePart");
772        }
773        
774        XMLUtils.endElement(handler, "courseParts");
775    }
776    
777    private void _writeColumns(TransformerHandler handler, CostComputationData costData) throws SAXException
778    {
779        // Columns
780        XMLUtils.startElement(handler, "columns");
781        
782        // Ecriture des colonnes par ordre alphabétique sur le titre
783        Map<String, String> sortedYearsToDisplay = _getYearsToDisplay(costData);
784        
785        for (Map.Entry<String, String> entry : sortedYearsToDisplay.entrySet())
786        {
787            _writeColumn(handler, entry);
788        }
789        
790        XMLUtils.endElement(handler, "columns");
791    }
792    
793    private void _writeColumn(TransformerHandler handler, Map.Entry<String, String> entry) throws SAXException
794    {
795        AttributesImpl attrs = new AttributesImpl();
796        attrs.addCDATAAttribute("id", entry.getKey());
797        XMLUtils.createElement(handler, "column", attrs, entry.getValue());
798    }
799    
800    private Map<String, String> _getYearsToDisplay(CostComputationData costData)
801    {
802        return costData.getAllEffectivesByStep()
803                .keySet()
804                .stream()
805                .collect(Collectors.toMap(Container::getId, Container::getCode));
806    }
807    
808    /**
809     * Create a map of values to sax
810     * @param costData informations about the formation
811     * @param coursePart the coursePart
812     * @param coursePartCostData the coursePart cost data
813     * @return the map of values to sax
814     */
815    private Map<String, String> getValues(CostComputationData costData, CoursePart coursePart, CoursePartCostData coursePartCostData)
816    {
817        Map<String, String> values = new HashMap<>();
818        Double enteredEffective = costData.getEnteredEffective(coursePart.getId()).isPresent() ? costData.getEnteredEffective(coursePart.getId()).get() : 0d;
819        Course courseHolder = coursePart.getCourseHolder();
820        
821        // *** Course part ***
822        values.put("titre", coursePart.getTitle());
823        values.put("code", coursePart.getCode());
824        values.put("nature", _refTableHelper.getItemCode(coursePart.getNature()));
825        values.put("volumeHoraire", ReportUtils.FORMAT_2_DIGITS.format(coursePart.getNumberOfHours()));
826        values.put("elpPorteur", courseHolder.getCode());
827        
828        // *** Norme et + ***
829        values.put("effectifCalcule", ReportUtils.FORMAT_2_DIGITS.format(costData.getGlobalComputedEffective(coursePart.getId()).get()));
830        values.put("effectifPrevisionnel", ReportUtils.FORMAT_2_DIGITS.format(enteredEffective));
831        values.put("effectifMax", ReportUtils.FORMAT_2_DIGITS.format(coursePartCostData.getEffectiveMax()));
832        values.put("effectifMinSup", ReportUtils.FORMAT_2_DIGITS.format(coursePartCostData.getEffectiveMinSup()));
833        values.put("norme", coursePartCostData.getNormLabel());
834        values.put("groupesCalcules", ReportUtils.FORMAT_2_DIGITS.format(coursePartCostData.getCalculatedGroups()));
835        values.put("groupesAOuvrir", ReportUtils.FORMAT_2_DIGITS.format(coursePartCostData.getGroupsToOpen()));
836        values.put("eqTDTotal", ReportUtils.FORMAT_2_DIGITS.format(costData.getEqTD(coursePart.getId()).getGlobalEqTD()));
837       
838        values.values().removeIf(Objects::isNull);
839        
840        return values;
841    }
842}