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.ArrayList;
021import java.util.HashMap;
022import java.util.LinkedHashSet;
023import java.util.List;
024import java.util.Map;
025import java.util.Optional;
026import java.util.Set;
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.apache.commons.lang3.tuple.Pair;
039import org.xml.sax.SAXException;
040
041import org.ametys.cms.data.ContentValue;
042import org.ametys.cms.repository.Content;
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.enumeration.OdfReferenceTableHelper;
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.AmetysObjectIterable;
055import org.ametys.plugins.repository.AmetysObjectIterator;
056import org.ametys.plugins.repository.UnknownAmetysObjectException;
057
058import com.google.common.collect.ImmutableMap;
059
060/**
061 * Class to generate the volume horaire report.
062 */
063public class VolumeHoraireReport extends AbstractReport
064{
065    private static final Map<ChoiceType, String> __COURSELIST_TYPE_2_LABEL = ImmutableMap.of(
066        ChoiceType.MANDATORY, "Obligatoire",
067        ChoiceType.OPTIONAL, "Facultatif",
068        ChoiceType.CHOICE, "A choix"
069    );
070
071    private Map<String, Map<String, String>> _calculatedElps;
072    private String _natureSemester;
073    private String _natureYear;
074    private String _natureUE;
075    
076    @Override
077    protected String getType()
078    {
079        return "volumehoraire";
080    }
081    
082    @Override
083    protected Set<String> getSupportedOutputFormats()
084    {
085        return Set.of(OUTPUT_FORMAT_XLS);
086    }
087    
088    @Override
089    protected void _launchByOrgUnit(String uaiCode, String catalog, String lang) throws Exception
090    {
091        _natureYear = Optional.of(_refTableHelper.getItemFromCode(OdfReferenceTableHelper.CONTAINER_NATURE, "annee")).map(OdfReferenceTableEntry::getId).orElse(null);
092        _natureSemester = Optional.of(_refTableHelper.getItemFromCode(OdfReferenceTableHelper.CONTAINER_NATURE, "semestre")).map(OdfReferenceTableEntry::getId).orElse(null);
093        _natureUE = Optional.ofNullable(_refTableHelper.getItemFromCode(OdfReferenceTableHelper.COURSE_NATURE, "UE")).map(OdfReferenceTableEntry::getId).orElse(null);
094        
095        _calculatedElps = new HashMap<>();
096
097        // Get the selected orgunit
098        AmetysObjectIterable<OrgUnit> orgUnits = _reportHelper.getRootOrgUnitsByUaiCode(uaiCode);
099        AmetysObjectIterator<OrgUnit> orgUnitsIterator = orgUnits.iterator();
100        if (orgUnitsIterator.hasNext())
101        {
102            OrgUnit orgUnit = orgUnitsIterator.next();
103            List<Program> selectedPrograms = _reportHelper.filterProgramsFromOrgUnits(orgUnit, lang, catalog);
104            
105            // Initialize data
106            _volumeHoraire(selectedPrograms);
107            
108            // Generate the XML file corresponding to the organization with the current uai code
109            _writeReportsVolumeHoraire(uaiCode, catalog, lang, selectedPrograms);
110        }
111
112        _calculatedElps = null;
113    }
114
115    /**
116     * Processing of the hourly volume for each UE.
117     * @param selectedPrograms The programs to explore
118     */
119    private void _volumeHoraire(List<Program> selectedPrograms)
120    {
121        // Descendre au niveau des courses de type UE
122        Set<Course> courses = _getUEsFromPrograms(selectedPrograms);
123        
124        // Calculer pour chaque ELP
125        for (Course course : courses)
126        {
127            String coursePrefix = course.getTitle();
128            
129            // Calcul des volumes pour l'UE
130            getLogger().info("[{}] Calcul des volumes horaires...", coursePrefix);
131            Map<String, Pair<Double, Double>> volumesByNature = _calculVolumeByEnseignement(course, 1);
132            
133            Map<String, String> ueData = new HashMap<>();
134            
135            // Période et type de période
136            ContentValue periodValue = course.getValue("period");
137            if (periodValue != null)
138            {
139                try
140                {
141                    Content period = periodValue.getContent();
142                    String periodType = Optional.ofNullable(period.<ContentValue>getValue("type"))
143                        .flatMap(ContentValue::getContentIfExists)
144                        .map(OdfReferenceTableEntry::new)
145                        .map(OdfReferenceTableEntry::getCode)
146                        .orElse(StringUtils.EMPTY);
147                    
148                    ueData.put("periode", period.getTitle());
149                    ueData.put("typePeriode", periodType);
150                }
151                catch (UnknownAmetysObjectException e)
152                {
153                    getLogger().error("Impossible de retrouver la période : {}", periodValue, e);
154                }
155            }
156            
157            ueData.put("codeAmetys", course.getCode());
158            ueData.put("codeELP", course.getValue("elpCode", false, StringUtils.EMPTY));
159            ueData.put("shortLabel", course.getValue("shortLabel", false, StringUtils.EMPTY));
160            ueData.put("title", course.getTitle());
161            Double ects = course.getValue("ects");
162            ueData.put("ects", ects == null ? StringUtils.EMPTY : String.valueOf(ects));
163
164            // Mutualisation
165            Set<Container> steps = _reportHelper.getSteps(course);
166            ueData.put("isShared", steps.size() > 1 ? "X" : StringUtils.EMPTY);
167            
168            Set<Container> stepsHolder = _reportHelper.getStepsHolders(course, getLogger(), coursePrefix);
169            if (stepsHolder.size() == 1)
170            {
171                String stepHolder = stepsHolder.stream()
172                    .findFirst()
173                    .map(this::_getStepCode)
174                    .orElse(StringUtils.EMPTY);
175                ueData.put("stepHolder", stepHolder);
176            }
177            
178            for (String nature : volumesByNature.keySet())
179            {
180                Pair<Double, Double> volumes = volumesByNature.get(nature);
181                ueData.put("nbHours#" + nature + "#average", String.valueOf(volumes.getLeft()));
182                ueData.put("nbHours#" + nature + "#total", String.valueOf(volumes.getRight()));
183            }
184            
185            _calculatedElps.put(course.getId(), ueData);
186        }
187    }
188    
189    private String _getStepCode(Container container)
190    {
191        StringBuilder stepCode = new StringBuilder(container.getValue("code"));
192        String etpCode = container.getValue("etpCode");
193        if (StringUtils.isNotBlank(etpCode))
194        {
195            stepCode.append(" (");
196            stepCode.append(etpCode);
197            stepCode.append(")");
198        }
199        return stepCode.toString();
200    }
201    
202    private Set<Course> _getUEsFromPrograms(List<Program> selectedPrograms)
203    {
204        Set<Course> courses = new LinkedHashSet<>();
205        
206        if (_natureUE != null)
207        {
208            selectedPrograms.forEach(program -> courses.addAll(_getCoursesForProgramItem(program)));
209        }
210        
211        return courses;
212    }
213    
214    private List<Course> _getCoursesForProgramItem(ProgramItem programItem) 
215    {
216        List<Course> courses = new ArrayList<>();
217        
218        if (programItem instanceof Course && _natureUE.equals(((Course) programItem).getCourseType()))
219        {
220            courses.add((Course) programItem);
221        }
222        else
223        {
224            _odfHelper.getChildProgramItems(programItem).forEach(child -> courses.addAll(_getCoursesForProgramItem(child)));
225        }
226        
227        return courses;
228    }
229
230    private Map<String, Pair<Double, Double>> _calculVolumeByEnseignement(CourseList courseList, float initialWeight, Map<String, Pair<Double, Double>> volumesByNature)
231    {
232        Map<String, Pair<Double, Double>> volumes = volumesByNature;
233        
234        ChoiceType courseListType = courseList.getType();
235        if (courseListType == null)
236        {
237            getLogger().error("The list '{}' ({}) doesn't have a valid type.", courseList.getTitle(), courseList.getCode());
238        }
239        else if (!courseListType.equals(ChoiceType.OPTIONAL) && courseList.hasCourses())
240        {
241            List<Course> courses = courseList.getCourses();
242            
243            float weight = initialWeight;
244            if (courseListType.equals(ChoiceType.CHOICE))
245            {
246                weight *= (float) courseList.getMinNumberOfCourses() / (float) courses.size();
247            }
248            
249            if (weight > 0)
250            {
251                for (Course course : courses)
252                {
253                    Map<String, Pair<Double, Double>> courseVolumes = _calculVolumeByEnseignement(course, weight);
254                    for (String nature : courseVolumes.keySet())
255                    {
256                        Pair<Double, Double> courseVolume = courseVolumes.getOrDefault(nature, Pair.of(0.0, 0.0));
257                        Pair<Double, Double> volume = volumes.getOrDefault(nature, Pair.of(0.0, 0.0));
258                        volumes.put(nature, Pair.of(courseVolume.getLeft() + volume.getLeft(), courseVolume.getRight() + volume.getRight()));
259                    }
260                }
261            }
262        }
263        
264        return volumes;
265    }
266    
267    private Map<String, Pair<Double, Double>> _calculVolumeByEnseignement(Course course, float weight)
268    {
269        Map<String, Pair<Double, Double>> volumesByNature = new HashMap<>();
270        
271        if (course.hasCourseLists())
272        {
273            // Parcours de toutes les UEs en dessous
274            for (CourseList courseList : course.getCourseLists())
275            {
276                volumesByNature = _calculVolumeByEnseignement(courseList, weight, volumesByNature);
277            }
278        }
279        else
280        {
281            // Calcul
282            Map<String, Double> volumes = new HashMap<>();
283            for (CoursePart coursePart : course.getCourseParts())
284            {
285                String nature = coursePart.getNature();
286                double nbHours = coursePart.getNumberOfHours();
287                volumes.put(nature, volumes.getOrDefault(nature, 0.0) + nbHours);
288            }
289            
290            for (String nature : volumes.keySet())
291            {
292                Double nbHours = volumes.get(nature);
293                volumesByNature.put(nature, Pair.of(nbHours * weight, nbHours));
294            }
295        }
296        
297        return volumesByNature;
298    }
299
300    /**
301     * Write the report.
302     * @param uaiCode The UAI code of the orgunit
303     * @param catalog The catalog
304     * @param lang The language
305     * @param selectedPrograms The programs
306     */
307    private void _writeReportsVolumeHoraire(String uaiCode, String catalog, String lang, List<Program> selectedPrograms)
308    {
309        SAXTransformerFactory factory = (SAXTransformerFactory) TransformerFactory.newInstance();
310        String fileName = _getReportFileName(catalog, lang, _reportHelper.getAccronymOrUaiCode(uaiCode));
311
312        // Delete old files
313        File xmlFile = new File(_tmpFolder, fileName + ".xml");
314        FileUtils.deleteQuietly(xmlFile);
315        
316        // Write XML file
317        try (FileOutputStream fos = new FileOutputStream(xmlFile))
318        {
319            TransformerHandler handler = factory.newTransformerHandler();
320            
321            // Prepare the transformation
322            Result result = new StreamResult(fos);
323            handler.setResult(result);
324            handler.startDocument();
325        
326            AttributesImpl attrs = new AttributesImpl();
327            attrs.addCDATAAttribute("type", getType());
328            attrs.addCDATAAttribute("date", _reportHelper.getReadableCurrentDate());
329            XMLUtils.startElement(handler, "report", attrs);
330            
331            // SAX dynamic informations
332            _reportHelper.saxNaturesEnseignement(handler, getLogger());
333
334            XMLUtils.startElement(handler, "lines");
335            for (Program program : selectedPrograms)
336            {
337                _saxUEsForProgram(handler, program);
338            }
339            XMLUtils.endElement(handler, "lines");
340            
341            XMLUtils.endElement(handler, "report");
342            handler.endDocument();
343            
344            // Convert the report to configured output format
345            convertReport(_tmpFolder, fileName, xmlFile);
346        }
347        catch (Exception e)
348        {
349            getLogger().error("An error occured while generating 'Volume horaire' report for orgunit '{}'", uaiCode, e);
350        }
351        finally
352        {
353            FileUtils.deleteQuietly(xmlFile);
354        }
355    }
356    
357    private void _saxUEsForProgram(TransformerHandler handler, Program program) throws SAXException
358    {
359        _saxUEsWithStructure(handler, program, new HashMap<>());
360    }
361    
362    private void _saxUEsWithStructure(TransformerHandler handler, ProgramItem programItem, Map<String, String> structureData) throws SAXException
363    {
364        Map<String, String> currentStructureData = new HashMap<>(structureData);
365        String title = ((Content) programItem).getTitle();
366        
367        if (programItem instanceof Program)
368        {
369            Program program = (Program) programItem;
370            
371            currentStructureData.put("program", title);
372            String degree = _refTableHelper.getItemLabel(program.getDegree(), program.getLanguage());
373            currentStructureData.put("degree", degree); 
374        }
375        else if (programItem instanceof SubProgram)
376        {
377            currentStructureData.put("parcours", title);
378        }
379        else if (programItem instanceof Container)
380        {
381            Container container = (Container) programItem;
382            
383            String containerNature = container.getNature();
384            if (containerNature.equals(_natureSemester))
385            {
386                currentStructureData.put("semestre", title);
387            }
388            else if (containerNature.equals(_natureYear))
389            {
390                currentStructureData.put("annee", title);
391                currentStructureData.put("etpCode", container.getValue("etpCode", false, StringUtils.EMPTY));
392            }
393        }
394        else if (programItem instanceof CourseList)
395        {
396            if (!currentStructureData.containsKey("listType"))
397            {
398                ChoiceType courseListType = ((CourseList) programItem).getType();
399                if (courseListType == null)
400                {
401                    getLogger().error("The course list '{}' hasn't a type.", title);
402                }
403                else
404                {
405                    String courseListTypeTraduction = __COURSELIST_TYPE_2_LABEL.get(courseListType);
406                    if (courseListTypeTraduction == null)
407                    {
408                        getLogger().error("Invalid course list type '{}' for '{}'.", courseListType, title);
409                    }
410                    else
411                    {
412                        currentStructureData.put("listType", courseListTypeTraduction);
413                    }
414                }
415            }
416        }
417        else if (programItem instanceof Course)
418        {
419            if (!currentStructureData.containsKey("CodeAnu"))
420            {
421                String codeAnu = Optional.ofNullable(((Course) programItem).getValue("CodeAnu"))
422                                         .map(codeAsLong -> String.valueOf(codeAsLong))
423                                         .orElse(StringUtils.EMPTY);
424                currentStructureData.put("CodeAnu", codeAnu);
425            }
426            
427            Map<String, String> ueData = _calculatedElps.get(programItem.getId());
428            if (ueData != null)
429            {
430                _saxUE(handler, currentStructureData, ueData);
431            }
432        }
433        
434        for (ProgramItem child : _odfHelper.getChildProgramItems(programItem))
435        {
436            _saxUEsWithStructure(handler, child, currentStructureData);
437        }
438    }
439    
440    private void _saxUE(TransformerHandler handler, Map<String, String> structureData, Map<String, String> ueData) throws SAXException
441    {
442        XMLUtils.startElement(handler, "line");
443        
444        // Sax structure data
445        for (Map.Entry<String, String> cell : structureData.entrySet())
446        {
447            XMLUtils.createElement(handler, cell.getKey(), cell.getValue());
448        }
449        
450        // Sax UE data
451        for (Map.Entry<String, String> cell : ueData.entrySet())
452        {
453            String key = cell.getKey();
454            AttributesImpl cellAttrs = new AttributesImpl();
455            if (key.startsWith("nbHours#"))
456            {
457                String[] tokens = key.split("#");
458                key = tokens[0];
459                cellAttrs.addCDATAAttribute("nature", tokens[1]);
460                cellAttrs.addCDATAAttribute("type", tokens[2]);
461            }
462            XMLUtils.createElement(handler, key, cellAttrs, cell.getValue());
463        }
464        
465        XMLUtils.endElement(handler, "line");
466    }
467}