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}