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}