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}