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