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.Content; 042import org.ametys.cms.repository.ModifiableContent; 043import org.ametys.core.util.DateUtils; 044import org.ametys.odf.ProgramItem; 045import org.ametys.odf.course.Course; 046import org.ametys.odf.courselist.CourseList; 047import org.ametys.odf.courselist.CourseList.ChoiceType; 048import org.ametys.odf.coursepart.CoursePart; 049import org.ametys.odf.enumeration.OdfReferenceTableEntry; 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.AmetysRepositoryException; 055import org.ametys.plugins.repository.UnknownAmetysObjectException; 056 057/** 058 * Pilotage report for Apogée. 059 */ 060public class ApogeeReport extends AbstractReport 061{ 062 private int _order; 063 064 @Override 065 protected void _launchByOrgUnit(String uaiCode, String catalog, String lang) throws Exception 066 { 067 _writeApogeeReport(uaiCode, catalog, lang); 068 } 069 070 @Override 071 protected String getType() 072 { 073 return "apogee"; 074 } 075 076 @Override 077 protected Set<String> getSupportedOutputFormats() 078 { 079 return Set.of(OUTPUT_FORMAT_XLS); 080 } 081 082 /** 083 * Create the Apogee report for one organization unit 084 * @param uaiCode The UAI code of the org unit 085 * @param catalog The catalog 086 * @param lang The language 087 */ 088 protected void _writeApogeeReport(String uaiCode, String catalog, String lang) 089 { 090 SAXTransformerFactory factory = (SAXTransformerFactory) TransformerFactory.newInstance(); 091 String fileName = _getReportFileName(catalog, lang, _reportHelper.getAccronymOrUaiCode(uaiCode)); 092 093 // Delete old files 094 File xmlFile = new File(_tmpFolder, fileName + ".xml"); 095 FileUtils.deleteQuietly(xmlFile); 096 097 // Write XML file 098 try (FileOutputStream fos = new FileOutputStream(xmlFile)) 099 { 100 TransformerHandler handler = factory.newTransformerHandler(); 101 102 // Prepare the transformation 103 Result result = new StreamResult(fos); 104 handler.setResult(result); 105 handler.startDocument(); 106 107 AttributesImpl attrs = new AttributesImpl(); 108 attrs.addCDATAAttribute("type", getType()); 109 attrs.addCDATAAttribute("date", _reportHelper.getReadableCurrentDate()); 110 XMLUtils.startElement(handler, "report", attrs); 111 112 // SAX tree 113 _generateReport(handler, uaiCode, lang, catalog); 114 115 XMLUtils.endElement(handler, "report"); 116 handler.endDocument(); 117 118 // Convert the report to configured output format 119 convertReport(_tmpFolder, fileName, xmlFile); 120 } 121 catch (Exception e) 122 { 123 getLogger().error("An error occured while generating 'Apogée' report for orgunit '{}'", uaiCode, e); 124 } 125 finally 126 { 127 FileUtils.deleteQuietly(xmlFile); 128 } 129 } 130 131 /** 132 * SAX the XML of the report 133 * @param handler the transformer handler 134 * @param uaiCode the uai code of the organization unit to process 135 * @param lang the lang of the programs 136 * @param catalog the catalog of the programs 137 * @throws SAXException if an error occurs while generating the SAX events 138 */ 139 private void _generateReport(TransformerHandler handler, String uaiCode, String lang, String catalog) throws SAXException 140 { 141 if (StringUtils.isEmpty(uaiCode)) 142 { 143 throw new IllegalArgumentException("Cannot process the Apogee report without the uai code.The processing of the Apogee report is aborted."); 144 } 145 146 _reportHelper.saxNaturesEnseignement(handler, getLogger()); 147 148 Map<Program, Object> contentsTree = _getStructure(uaiCode, lang, catalog); 149 if (contentsTree.size() > 0) 150 { 151 _order = 1; 152 _saxTree(handler, contentsTree); 153 } 154 } 155 156 /** 157 * Generate the data structure that will be used to create the report 158 * @param uaiCode the uai code of the organization unit 159 * @param lang the lang of programs 160 * @param catalog the catalog of programs 161 * @return The structure 162 */ 163 private Map<Program, Object> _getStructure(String uaiCode, String lang, String catalog) 164 { 165 OrgUnit rootOrgUnit = _odfHelper.getOrgUnitByUAICode(uaiCode); 166 if (rootOrgUnit == null) 167 { 168 throw new IllegalArgumentException("Unable to find any organization unit with the uai code : " + uaiCode + ".The processing of the Apogee report is aborted."); 169 } 170 171 Map<Program, Object> programTree = new TreeMap<>(new ProgramTitleComparator()); 172 173 // On ne récupère que les composantes, enfant direct du root org unit, et on ignore les départements. 174 if (rootOrgUnit.getParentOrgUnit() != null && rootOrgUnit.getParentOrgUnit().getParentOrgUnit() == null) 175 { 176 // Chercher les programmes concernés par la composante sélectionnée et ses enfants 177 List<Program> programs = _odfHelper.getProgramsFromOrgUnit(rootOrgUnit, catalog, lang); 178 for (Program program : programs) 179 { 180 Map<ProgramItem, Object> courses = _reportHelper.getCoursesFromContent(program); 181 if (courses != null) 182 { 183 programTree.put(program, courses); 184 } 185 } 186 } 187 188 return programTree; 189 } 190 191 /** 192 * Sax the information related to the courses of the tree 193 * @param handler the transformer handler 194 * @param programTree the program tree to sax 195 * @throws SAXException if an error occurs when SAXing 196 */ 197 @SuppressWarnings("unchecked") 198 private void _saxTree(TransformerHandler handler, Map<Program, Object> programTree) throws SAXException 199 { 200 for (Entry<Program, Object> programEntry : programTree.entrySet()) 201 { 202 if (programEntry.getValue() != null && programEntry.getValue() instanceof Map<?, ?>) 203 { 204 _saxCourseFromTree(handler, (Map<ProgramItem, Object>) programEntry.getValue(), programEntry.getKey()); 205 } 206 } 207 } 208 209 210 private void _saxCourseFromTree(TransformerHandler handler, Map<ProgramItem, Object> programTree, Program program) throws SAXException 211 { 212 _saxCourseFromTree(handler, programTree, program, null, null, null, null, null, null, 1, ""); 213 } 214 215 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 216 { 217 int courseListPosition = 0; 218 for (Entry<ProgramItem, Object> entry : tree.entrySet()) 219 { 220 ProgramItem child = entry.getKey(); 221 @SuppressWarnings("unchecked") 222 Map<ProgramItem, Object> subTree = (Map<ProgramItem, Object>) entry.getValue(); 223 224 if (child instanceof Course) 225 { 226 Course childCourse = (Course) child; 227 String path = courseHierarchy + " > " + childCourse.getTitle(); 228 _saxCourse(handler, program, subprogram, containerYear, containerSemester, list, listPosition, (Course) child, parentCourse, level, path); 229 230 if (subTree != null) 231 { 232 _saxCourseFromTree(handler, subTree, program, subprogram, containerYear, containerSemester, list, listPosition, childCourse, level + 1, path); 233 } 234 } 235 236 if (subTree != null) 237 { 238 if (child instanceof Program) 239 { 240 _saxCourseFromTree(handler, subTree, (Program) child, subprogram, containerYear, containerSemester, list, listPosition, parentCourse, level, courseHierarchy); 241 } 242 else if (child instanceof Container) 243 { 244 Container container = (Container) child; 245 String containerNature = _refTableHelper.getItemCode(container.getNature()); 246 247 if ("annee".equals(containerNature)) 248 { 249 _saxCourseFromTree(handler, subTree, program, subprogram, container, containerSemester, list, listPosition, parentCourse, level, courseHierarchy); 250 } 251 else if ("semestre".equals(containerNature)) 252 { 253 _saxCourseFromTree(handler, subTree, program, subprogram, containerYear, container, list, listPosition, parentCourse, level, courseHierarchy); 254 } 255 else 256 { 257 _saxCourseFromTree(handler, subTree, program, subprogram, containerYear, containerSemester, list, listPosition, parentCourse, level, courseHierarchy); 258 } 259 } 260 else if (child instanceof SubProgram) 261 { 262 _saxCourseFromTree(handler, subTree, program, (SubProgram) child, containerYear, containerSemester, list, listPosition, parentCourse, level, courseHierarchy); 263 } 264 else if (child instanceof CourseList) 265 { 266 courseListPosition++; 267 CourseList childCourseList = (CourseList) child; 268 String path = courseHierarchy.equals("") ? childCourseList.getTitle() : courseHierarchy + " > " + childCourseList.getTitle(); 269 _saxCourseFromTree(handler, subTree, program, subprogram, containerYear, containerSemester, (CourseList) child, courseListPosition, parentCourse, level, path); 270 } 271 } 272 } 273 } 274 275 private String _getHierarchy(Program program, SubProgram subprogram, Container containerYear, Container containerSemester, String courseHierarchy) 276 { 277 String hierarchy = program.getTitle(); 278 if (subprogram != null) 279 { 280 hierarchy += " > " + subprogram.getTitle(); 281 } 282 283 if (containerYear != null) 284 { 285 hierarchy += " > " + containerYear.getTitle(); 286 } 287 288 if (containerSemester != null) 289 { 290 hierarchy += " > " + containerSemester.getTitle(); 291 } 292 293 hierarchy += " > " + courseHierarchy; 294 295 return hierarchy; 296 } 297 298 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 299 { 300 if (course != null) 301 { 302 String hierarchy = _getHierarchy(program, subprogram, containerYear, containerSemester, courseHierarchy); 303 304 XMLUtils.startElement(handler, "course"); 305 306 // Ordre 307 XMLUtils.createElement(handler, "ordre", String.valueOf(_order)); 308 309 // Composante 310 _saxOrgUnits(handler, program); 311 312 // Formation 313 XMLUtils.createElement(handler, "formation", program.getTitle()); 314 XMLUtils.createElement(handler, "formationCode", program.getCode()); 315 316 _saxSubProgram(handler, subprogram); 317 318 _saxContainer(handler, containerYear, parentCourse); 319 320 _saxCourseList(handler, list, listPosition); 321 322 // A des fils 323 boolean aDesFils = course.hasCourseLists(); 324 XMLUtils.createElement(handler, "aDesFils", aDesFils ? "X" : ""); 325 326 long courseListsSize = course.getParentCourseLists().size(); 327 328 // Partagé 329 XMLUtils.createElement(handler, "partage", courseListsSize > 1 ? "X" : ""); 330 331 // Nb occurrences 332 XMLUtils.createElement(handler, "occurrences", _reportHelper.formatNumberToSax(courseListsSize)); 333 334 Container etape = _getEtapePorteuse(course, hierarchy); 335 336 String porte = _getPorte(etape, containerYear); 337 338 // Porté ("X" si l'ELP est porté par la formation (Etape porteuse=COD_ETP), vide sinon) 339 XMLUtils.createElement(handler, "porte", porte); 340 341 // Niveau 342 XMLUtils.createElement(handler, "niveau", "niv" + level); 343 344 // Date de création 345 XMLUtils.createElement(handler, "creationDate", DateUtils.zonedDateTimeToString(course.getCreationDate())); 346 347 // Code Apogée 348 XMLUtils.createElement(handler, "codeApogee", course.getValue("elpCode", false, StringUtils.EMPTY)); 349 350 // Nature de l'élément 351 XMLUtils.createElement(handler, "nature", _refTableHelper.getItemCode(course.getCourseType())); 352 353 // Libellé court 354 XMLUtils.createElement(handler, "libelleCourt", course.getValue("shortLabel", false, StringUtils.EMPTY)); 355 356 // Libellé 357 XMLUtils.createElement(handler, "libelle", course.getTitle()); 358 359 // Code Ametys (ELP) 360 XMLUtils.createElement(handler, "elpCode", course.getCode()); 361 362 // Lieu 363 _reportHelper.saxContentAttribute(handler, course, "campus", "campus"); 364 365 // Crédits ECTS 366 XMLUtils.createElement(handler, "ects", String.valueOf(course.getEcts())); 367 368 String teachingActivity = _refTableHelper.getItemCode(course.getTeachingActivity()); 369 String stage = teachingActivity.equals("SA") ? "X" : ""; 370 371 // Element stage 372 XMLUtils.createElement(handler, "stage", stage); 373 374 // Code semestre et type de période (pair, impair, an) 375 Content period = Optional.of("period") 376 .map(course::<ContentValue>getValue) 377 .flatMap(ContentValue::getContentIfExists) 378 .orElse(null); 379 if (period != null) 380 { 381 try 382 { 383 String periodCode = Optional.of("code") 384 .map(period::<String>getValue) 385 .orElse(StringUtils.EMPTY); 386 387 String periodTypeCode = Optional.ofNullable(period.<ContentValue>getValue("type")) 388 .flatMap(ContentValue::getContentIfExists) 389 .map(c -> c.<String>getValue("code")) 390 .orElse(StringUtils.EMPTY); 391 392 XMLUtils.createElement(handler, "periode", "s10".equals(periodCode) ? "s0" : periodCode); 393 XMLUtils.createElement(handler, "periodeType", periodTypeCode); 394 } 395 catch (UnknownAmetysObjectException e) 396 { 397 getLogger().error("Impossible de retrouver la période : {}", period, e); 398 } 399 } 400 401 OrgUnit orgUnit = _getOrgUnit(course, hierarchy); 402 403 // Code composante 404 XMLUtils.createElement(handler, "codeComposante", orgUnit != null ? orgUnit.getValue("codCmp", false, StringUtils.EMPTY) : StringUtils.EMPTY); 405 406 // Code CIP 407 XMLUtils.createElement(handler, "codeCIP", orgUnit != null ? orgUnit.getValue("codCipApogee", false, StringUtils.EMPTY) : StringUtils.EMPTY); 408 409 // Code ANU 410 long codeAnu = course.getValue("CodeAnu", false, 0L); 411 XMLUtils.createElement(handler, "CodeAnu", codeAnu > 0 ? String.valueOf(codeAnu) : StringUtils.EMPTY); 412 413 // Calcul des charges 414 XMLUtils.createElement(handler, "calculCharges", aDesFils ? "" : "X"); 415 416 // Heures d'enseignement 417 for (CoursePart coursePart : course.getCourseParts()) 418 { 419 AttributesImpl attr = new AttributesImpl(); 420 attr.addCDATAAttribute("nature", coursePart.getNature()); 421 XMLUtils.createElement(handler, "volumeHoraire", attr, String.valueOf(coursePart.getNumberOfHours())); 422 } 423 424 // Etape porteuse pour les feuilles de l'arbre 425 if (etape != null) 426 { 427 XMLUtils.createElement(handler, "etapePorteuse", etape.getValue("etpCode", false, "")); 428 XMLUtils.createElement(handler, "vetEtapePorteuse", etape.getValue("vrsEtpCode", false, "")); 429 } 430 431 // Discipline 432 String disciplineEnseignement = Optional.of("disciplineEnseignement") 433 .map(course::<ContentValue>getValue) 434 .flatMap(ContentValue::getContentIfExists) 435 .map(OdfReferenceTableEntry::new) 436 .map(entry -> { 437 String code = entry.getCode(); 438 return (StringUtils.isNotEmpty(code) ? "[" + code + "] " : StringUtils.EMPTY) + entry.getLabel(course.getLanguage()); 439 }) 440 .orElse(StringUtils.EMPTY); 441 XMLUtils.createElement(handler, "discipline", disciplineEnseignement); 442 443 saxAdditionalCourseData(handler, course); 444 445 XMLUtils.endElement(handler, "course"); 446 447 _order++; 448 } 449 } 450 451 /** 452 * Generates SAX events for additional data of a {@link Course}. 453 * @param handler The handler 454 * @param course The course to SAX 455 * @throws AmetysRepositoryException if an error occurs 456 * @throws SAXException if an error occurs 457 */ 458 protected void saxAdditionalCourseData(TransformerHandler handler, Course course) throws AmetysRepositoryException, SAXException 459 { 460 // Do nothing by default 461 } 462 463 private void _saxContainer(TransformerHandler handler, Container container, Course parentCourse) throws AmetysRepositoryException, SAXException 464 { 465 if (container != null) 466 { 467 // Année 468 XMLUtils.createElement(handler, "annee", container.getTitle()); 469 // COD_ETP 470 XMLUtils.createElement(handler, "COD_ETP", container.getValue("etpCode", false, StringUtils.EMPTY)); 471 // COD_VRS_ETP 472 XMLUtils.createElement(handler, "COD_VRS_ETP", container.getValue("vrsEtpCode", false, StringUtils.EMPTY)); 473 // Code Ametys ELP père 474 XMLUtils.createElement(handler, "codeELPPere", parentCourse != null ? parentCourse.getCode() : ""); 475 } 476 } 477 478 private void _saxSubProgram(TransformerHandler handler, SubProgram subprogram) throws AmetysRepositoryException, SAXException 479 { 480 if (subprogram != null) 481 { 482 // Parcours 483 XMLUtils.createElement(handler, "parcours", subprogram.getTitle()); 484 XMLUtils.createElement(handler, "parcoursCode", subprogram.getCode()); 485 } 486 } 487 488 private void _saxOrgUnits(TransformerHandler handler, Program program) throws SAXException 489 { 490 StringBuilder sb = new StringBuilder(); 491 List<String> orgUnits = program.getOrgUnits(); 492 for (String orgUnitId : orgUnits) 493 { 494 try 495 { 496 OrgUnit orgUnit = _resolver.resolveById(orgUnitId); 497 if (sb.length() > 0) 498 { 499 sb.append(", "); 500 } 501 sb.append(orgUnit.getTitle()); 502 sb.append(" ("); 503 sb.append(orgUnit.getUAICode()); 504 sb.append(")"); 505 } 506 catch (UnknownAmetysObjectException e) 507 { 508 getLogger().info("La composante référencée par la formation {} ({}) n'a pas été trouvée.", program.getTitle(), program.getCode()); 509 } 510 } 511 XMLUtils.createElement(handler, "orgUnit", sb.toString()); 512 } 513 514 private String _getPorte(Container etape, Container containerYear) 515 { 516 String porte = ""; 517 if (etape != null && containerYear != null) 518 { 519 String etpCode = containerYear.getValue("etpCode", false, StringUtils.EMPTY); 520 if (StringUtils.isNotEmpty(etpCode)) 521 { 522 porte = etpCode.equals(etape.getValue("etpCode", false, StringUtils.EMPTY)) ? "X" : StringUtils.EMPTY; 523 } 524 } 525 return porte; 526 } 527 528 private OrgUnit _getOrgUnit(Course course, String hierarchy) 529 { 530 OrgUnit orgUnit = null; 531 532 List<String> courseOrgUnits = course.getOrgUnits(); 533 if (!courseOrgUnits.isEmpty()) 534 { 535 try 536 { 537 orgUnit = _resolver.resolveById(courseOrgUnits.get(0)); 538 } 539 catch (UnknownAmetysObjectException e) 540 { 541 getLogger().info("La composante référencée par l'élément pédagogique {} ({}) n'a pas été trouvée.", hierarchy, course.getCode()); 542 } 543 544 if (courseOrgUnits.size() > 1) 545 { 546 getLogger().warn("L'élément pédagogique {} ({}) référence plus d'une composante.", hierarchy, course.getCode()); 547 } 548 } 549 550 return orgUnit; 551 } 552 553 private Container _getEtapePorteuse(Course course, String hierarchy) 554 { 555 return Optional.ofNullable((ContentValue) course.getValue("etapePorteuse")) 556 .flatMap(contentValue -> _getEtapePorteuseIfExists(contentValue, course, hierarchy)) 557 .map(Container.class::cast) 558 .orElse(null); 559 } 560 561 private Optional<ModifiableContent> _getEtapePorteuseIfExists(ContentValue etapePorteuse, Course course, String hierarchy) 562 { 563 try 564 { 565 return Optional.ofNullable(etapePorteuse.getContent()); 566 } 567 catch (UnknownAmetysObjectException e) 568 { 569 getLogger().info("L'année porteuse référencée par l'élément pédagogique {} ({}) n'a pas été trouvée.", hierarchy, course.getCode()); 570 return Optional.empty(); 571 } 572 } 573 574 private void _saxCourseList(TransformerHandler handler, CourseList list, Integer position) throws SAXException 575 { 576 if (list != null) 577 { 578 XMLUtils.createElement(handler, "list", "Lst" + position); 579 _saxChoiceList(handler, list); 580 } 581 } 582 583 private void _saxChoiceList(TransformerHandler handler, CourseList list) throws SAXException 584 { 585 // Type 586 ChoiceType typeList = list.getType(); 587 if (typeList != null) 588 { 589 if (typeList.name() != null && (typeList.name().equals(ChoiceType.CHOICE.toString()) || typeList.name().equals(ChoiceType.MANDATORY.toString()) || typeList.name().equals(ChoiceType.OPTIONAL.toString()))) 590 { 591 String typeListAsString = ""; 592 if (typeList.name().equals(ChoiceType.CHOICE.toString())) 593 { 594 typeListAsString = "X"; 595 } 596 else if (typeList.name().equals(ChoiceType.MANDATORY.toString())) 597 { 598 typeListAsString = "O"; 599 } 600 else if (typeList.name().equals(ChoiceType.OPTIONAL.toString())) 601 { 602 typeListAsString = "F"; 603 } 604 605 XMLUtils.createElement(handler, "typeList", typeListAsString); 606 } 607 608 // Min-Max (Ne remplir que pour le type "CHOICE" (ne mettre que le min)) 609 if (typeList.name() != null && typeList.name().equals(ChoiceType.CHOICE.toString())) 610 { 611 XMLUtils.createElement(handler, "minmax", _reportHelper.formatNumberToSax(list.getMinNumberOfCourses())); 612 } 613 } 614 } 615 616 class ProgramTitleComparator implements Comparator<Program> 617 { 618 @Override 619 public int compare(Program p1, Program p2) 620 { 621 622 return p1.getTitle().compareTo(p2.getTitle()); 623 } 624 } 625}