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