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.orgunit.OrgUnit; 049import org.ametys.odf.program.Container; 050import org.ametys.odf.program.Program; 051import org.ametys.odf.program.SubProgram; 052import org.ametys.plugins.repository.AmetysObjectIterable; 053import org.ametys.plugins.repository.AmetysObjectIterator; 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 = catalog + "-" + lang + "-" + _reportHelper.getAccronymOrUaiCode(uaiCode) + "-" + "[" + getType() + "-" + _currentFormattedDate + "]"; 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 AmetysObjectIterable<OrgUnit> orgUnits = _reportHelper.getRootOrgUnitsByUaiCode(uaiCode); 166 AmetysObjectIterator<OrgUnit> orgUnitsIterator = orgUnits.iterator(); 167 if (!orgUnitsIterator.hasNext()) 168 { 169 throw new IllegalArgumentException("Unable to find any organization unit with the uai code : " + uaiCode + ".The processing of the Apogee report is aborted."); 170 } 171 172 Map<Program, Object> programTree = new TreeMap<>(new ProgramTitleComparator()); 173 174 OrgUnit rootOrgUnit = orgUnitsIterator.next(); 175 // On ne récupère que les composantes, enfant direct du root org unit, et on ignore les départements. 176 if (rootOrgUnit.getParentOrgUnit() != null && rootOrgUnit.getParentOrgUnit().getParentOrgUnit() == null) 177 { 178 List<String> orgUnitIds = _reportHelper.getSubOrgUnits(rootOrgUnit); 179 180 // Chercher les programmes concernés par la composante sélectionnée et ses enfants 181 for (String orgUnitId : orgUnitIds) 182 { 183 AmetysObjectIterable<Program> programs = _reportHelper.getProgramsByOrgUnitId(orgUnitId, lang, catalog); 184 AmetysObjectIterator<Program> programsIterator = programs.iterator(); 185 while (programsIterator.hasNext()) 186 { 187 Program program = programsIterator.next(); 188 Map<ProgramItem, Object> courses = _reportHelper.getCoursesFromContent(program); 189 if (courses != null) 190 { 191 programTree.put(program, courses); 192 } 193 } 194 } 195 } 196 197 return programTree; 198 } 199 200 /** 201 * Sax the information related to the courses of the tree 202 * @param handler the transformer handler 203 * @param programTree the program tree to sax 204 * @throws SAXException if an error occurs when SAXing 205 */ 206 @SuppressWarnings("unchecked") 207 private void _saxTree(TransformerHandler handler, Map<Program, Object> programTree) throws SAXException 208 { 209 for (Entry<Program, Object> programEntry : programTree.entrySet()) 210 { 211 if (programEntry.getValue() != null && programEntry.getValue() instanceof Map<?, ?>) 212 { 213 _saxCourseFromTree(handler, (Map<ProgramItem, Object>) programEntry.getValue(), programEntry.getKey()); 214 } 215 } 216 } 217 218 219 private void _saxCourseFromTree(TransformerHandler handler, Map<ProgramItem, Object> programTree, Program program) throws SAXException 220 { 221 _saxCourseFromTree(handler, programTree, program, null, null, null, null, null, null, 1, ""); 222 } 223 224 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 225 { 226 int courseListPosition = 0; 227 for (Entry<ProgramItem, Object> entry : tree.entrySet()) 228 { 229 ProgramItem child = entry.getKey(); 230 @SuppressWarnings("unchecked") 231 Map<ProgramItem, Object> subTree = (Map<ProgramItem, Object>) entry.getValue(); 232 233 if (child instanceof Course) 234 { 235 Course childCourse = (Course) child; 236 String path = courseHierarchy + " > " + childCourse.getTitle(); 237 _saxCourse(handler, program, subprogram, containerYear, containerSemester, list, listPosition, (Course) child, parentCourse, level, path); 238 239 if (subTree != null) 240 { 241 _saxCourseFromTree(handler, subTree, program, subprogram, containerYear, containerSemester, list, listPosition, childCourse, level + 1, path); 242 } 243 } 244 245 if (subTree != null) 246 { 247 if (child instanceof Program) 248 { 249 _saxCourseFromTree(handler, subTree, (Program) child, subprogram, containerYear, containerSemester, list, listPosition, parentCourse, level, courseHierarchy); 250 } 251 else if (child instanceof Container) 252 { 253 Container container = (Container) child; 254 String containerNature = _refTableHelper.getItemCode(container.getNature()); 255 256 if ("annee".equals(containerNature)) 257 { 258 _saxCourseFromTree(handler, subTree, program, subprogram, container, containerSemester, list, listPosition, parentCourse, level, courseHierarchy); 259 } 260 else if ("semestre".equals(containerNature)) 261 { 262 _saxCourseFromTree(handler, subTree, program, subprogram, containerYear, container, list, listPosition, parentCourse, level, courseHierarchy); 263 } 264 else 265 { 266 _saxCourseFromTree(handler, subTree, program, subprogram, containerYear, containerSemester, list, listPosition, parentCourse, level, courseHierarchy); 267 } 268 } 269 else if (child instanceof SubProgram) 270 { 271 _saxCourseFromTree(handler, subTree, program, (SubProgram) child, containerYear, containerSemester, list, listPosition, parentCourse, level, courseHierarchy); 272 } 273 else if (child instanceof CourseList) 274 { 275 courseListPosition++; 276 CourseList childCourseList = (CourseList) child; 277 String path = courseHierarchy.equals("") ? childCourseList.getTitle() : courseHierarchy + " > " + childCourseList.getTitle(); 278 _saxCourseFromTree(handler, subTree, program, subprogram, containerYear, containerSemester, (CourseList) child, courseListPosition, parentCourse, level, path); 279 } 280 } 281 } 282 } 283 284 private String _getHierarchy(Program program, SubProgram subprogram, Container containerYear, Container containerSemester, String courseHierarchy) 285 { 286 String hierarchy = program.getTitle(); 287 if (subprogram != null) 288 { 289 hierarchy += " > " + subprogram.getTitle(); 290 } 291 292 if (containerYear != null) 293 { 294 hierarchy += " > " + containerYear.getTitle(); 295 } 296 297 if (containerSemester != null) 298 { 299 hierarchy += " > " + containerSemester.getTitle(); 300 } 301 302 hierarchy += " > " + courseHierarchy; 303 304 return hierarchy; 305 } 306 307 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 308 { 309 if (course != null) 310 { 311 String hierarchy = _getHierarchy(program, subprogram, containerYear, containerSemester, courseHierarchy); 312 313 XMLUtils.startElement(handler, "course"); 314 315 // Ordre 316 XMLUtils.createElement(handler, "ordre", String.valueOf(_order)); 317 318 // Composante 319 _saxOrgUnits(handler, program); 320 321 // Formation 322 XMLUtils.createElement(handler, "formation", program.getTitle()); 323 XMLUtils.createElement(handler, "formationCode", program.getCode()); 324 325 _saxSubProgram(handler, subprogram); 326 327 _saxContainer(handler, containerYear, parentCourse); 328 329 _saxCourseList(handler, list, listPosition); 330 331 // A des fils 332 boolean aDesFils = course.hasCourseLists(); 333 XMLUtils.createElement(handler, "aDesFils", aDesFils ? "X" : ""); 334 335 long courseListsSize = course.getParentCourseLists().size(); 336 337 // Partagé 338 XMLUtils.createElement(handler, "partage", courseListsSize > 1 ? "X" : ""); 339 340 // Nb occurences 341 XMLUtils.createElement(handler, "occurences", _reportHelper.formatNumberToSax(courseListsSize)); 342 343 Container etape = _getEtapePorteuse(course, hierarchy); 344 345 String porte = _getPorte(etape, containerYear); 346 347 // Porté ("X" si l'ELP est porté par la formation (Etape porteuse=COD_ETP), vide sinon) 348 XMLUtils.createElement(handler, "porte", porte); 349 350 // Niveau 351 XMLUtils.createElement(handler, "niveau", "niv" + level); 352 353 // Date de création 354 XMLUtils.createElement(handler, "creationDate", DateUtils.dateToString(course.getCreationDate())); 355 356 // Code Apogée 357 XMLUtils.createElement(handler, "codeApogee", course.getValue("elpCode", false, StringUtils.EMPTY)); 358 359 // Nature de l'élément 360 XMLUtils.createElement(handler, "nature", _refTableHelper.getItemCode(course.getCourseType())); 361 362 // Libellé court 363 XMLUtils.createElement(handler, "libelleCourt", course.getValue("shortLabel", false, StringUtils.EMPTY)); 364 365 // Libellé 366 XMLUtils.createElement(handler, "libelle", course.getTitle()); 367 368 // Code Ametys (ELP) 369 XMLUtils.createElement(handler, "elpCode", course.getCode()); 370 371 // Lieu 372 _reportHelper.saxContentAttribute(handler, course, "campus", "campus"); 373 374 // Crédits ECTS 375 XMLUtils.createElement(handler, "ects", String.valueOf(course.getEcts())); 376 377 String teachingActivity = _refTableHelper.getItemCode(course.getTeachingActivity()); 378 String stage = teachingActivity.equals("SA") ? "X" : ""; 379 380 // Element stage 381 XMLUtils.createElement(handler, "stage", stage); 382 383 // Code semestre 384 String periode = _refTableHelper.getItemCode(course.getTeachingTerm()); 385 XMLUtils.createElement(handler, "periode", "s10".equals(periode) ? "s0" : periode); 386 387 OrgUnit orgUnit = _getOrgUnit(course, hierarchy); 388 389 // Code composante 390 XMLUtils.createElement(handler, "codeComposante", orgUnit != null ? orgUnit.getValue("codCmp", false, StringUtils.EMPTY) : StringUtils.EMPTY); 391 392 // Code CIP 393 XMLUtils.createElement(handler, "codeCIP", orgUnit != null ? orgUnit.getValue("codCipApogee", false, StringUtils.EMPTY) : StringUtils.EMPTY); 394 395 // Code ANU 396 long codeAnu = course.getValue("CodeAnu", false, 0L); 397 XMLUtils.createElement(handler, "CodeAnu", codeAnu > 0 ? String.valueOf(codeAnu) : StringUtils.EMPTY); 398 399 // Calcul des charges 400 XMLUtils.createElement(handler, "calculCharges", aDesFils ? "" : "X"); 401 402 // Heures d'enseignement 403 for (CoursePart coursePart : course.getCourseParts()) 404 { 405 AttributesImpl attr = new AttributesImpl(); 406 attr.addCDATAAttribute("nature", coursePart.getNature()); 407 XMLUtils.createElement(handler, "volumeHoraire", attr, String.valueOf(coursePart.getNumberOfHours())); 408 } 409 410 // Etape porteuse pour les feuilles de l'arbre 411 if (etape != null) 412 { 413 XMLUtils.createElement(handler, "etapePorteuse", etape.getValue("etpCode", false, "")); 414 XMLUtils.createElement(handler, "vetEtapePorteuse", etape.getValue("vrsEtpCode", false, "")); 415 } 416 417 // Discipline 418 // FIXME ODF-2161: discipline attribute is not yet part of the model and may never be 419// XMLUtils.createElement(handler, "discipline", ContentDataHelper.getContentIdFromContentData(course, "discipline")); 420 421 saxAdditionalCourseData(handler, course); 422 423 XMLUtils.endElement(handler, "course"); 424 425 _order++; 426 } 427 } 428 429 /** 430 * Generates SAX events for additional data of a {@link Course}. 431 * @param handler The handler 432 * @param course The course to SAX 433 * @throws AmetysRepositoryException if an error occurs 434 * @throws SAXException if an error occurs 435 */ 436 protected void saxAdditionalCourseData(TransformerHandler handler, Course course) throws AmetysRepositoryException, SAXException 437 { 438 // Do nothing by default 439 } 440 441 private void _saxContainer(TransformerHandler handler, Container container, Course parentCourse) throws AmetysRepositoryException, SAXException 442 { 443 if (container != null) 444 { 445 // Année 446 XMLUtils.createElement(handler, "annee", container.getTitle()); 447 // COD_ETP 448 XMLUtils.createElement(handler, "COD_ETP", container.getValue("etpCode", false, StringUtils.EMPTY)); 449 // COD_VRS_ETP 450 XMLUtils.createElement(handler, "COD_VRS_ETP", container.getValue("vrsEtpCode", false, StringUtils.EMPTY)); 451 // Code Ametys ELP père 452 XMLUtils.createElement(handler, "codeELPPere", parentCourse != null ? parentCourse.getCode() : ""); 453 } 454 } 455 456 private void _saxSubProgram(TransformerHandler handler, SubProgram subprogram) throws AmetysRepositoryException, SAXException 457 { 458 if (subprogram != null) 459 { 460 // Parcours 461 XMLUtils.createElement(handler, "parcours", subprogram.getTitle()); 462 XMLUtils.createElement(handler, "parcoursCode", subprogram.getCode()); 463 } 464 } 465 466 private void _saxOrgUnits(TransformerHandler handler, Program program) throws SAXException 467 { 468 StringBuilder sb = new StringBuilder(); 469 List<String> orgUnits = program.getOrgUnits(); 470 for (String orgUnitId : orgUnits) 471 { 472 try 473 { 474 OrgUnit orgUnit = _resolver.resolveById(orgUnitId); 475 if (sb.length() > 0) 476 { 477 sb.append(", "); 478 } 479 sb.append(orgUnit.getTitle()); 480 sb.append(" ("); 481 sb.append(orgUnit.getUAICode()); 482 sb.append(")"); 483 } 484 catch (UnknownAmetysObjectException e) 485 { 486 getLogger().info("La composante référencée par la formation {} ({}) n'a pas été trouvée.", program.getTitle(), program.getCode()); 487 } 488 } 489 XMLUtils.createElement(handler, "orgUnit", sb.toString()); 490 } 491 492 private String _getPorte(Container etape, Container containerYear) 493 { 494 String porte = ""; 495 if (etape != null && containerYear != null) 496 { 497 String etpCode = containerYear.getValue("etpCode", false, StringUtils.EMPTY); 498 if (StringUtils.isNotEmpty(etpCode)) 499 { 500 porte = etpCode.equals(etape.getValue("etpCode", false, StringUtils.EMPTY)) ? "X" : StringUtils.EMPTY; 501 } 502 } 503 return porte; 504 } 505 506 private OrgUnit _getOrgUnit(Course course, String hierarchy) 507 { 508 OrgUnit orgUnit = null; 509 510 List<String> courseOrgUnits = course.getOrgUnits(); 511 if (!courseOrgUnits.isEmpty()) 512 { 513 try 514 { 515 orgUnit = _resolver.resolveById(courseOrgUnits.get(0)); 516 } 517 catch (UnknownAmetysObjectException e) 518 { 519 getLogger().info("La composante référencée par l'élément pédagogique {} ({}) n'a pas été trouvée.", hierarchy, course.getCode()); 520 } 521 522 if (courseOrgUnits.size() > 1) 523 { 524 getLogger().warn("L'élément pédagogique {} ({}) référence plus d'une composante.", hierarchy, course.getCode()); 525 } 526 } 527 528 return orgUnit; 529 } 530 531 private Container _getEtapePorteuse(Course course, String hierarchy) 532 { 533 return Optional.ofNullable((ContentValue) course.getValue("etapePorteuse")) 534 .flatMap(contentValue -> _getEtapePorteuseIfExists(contentValue, course, hierarchy)) 535 .map(Container.class::cast) 536 .orElse(null); 537 } 538 539 private Optional<ModifiableContent> _getEtapePorteuseIfExists(ContentValue etapePorteuse, Course course, String hierarchy) 540 { 541 try 542 { 543 return Optional.ofNullable(etapePorteuse.getContent()); 544 } 545 catch (UnknownAmetysObjectException e) 546 { 547 getLogger().info("L'étape porteuse référencée par l'élément pédagogique {} ({}) n'a pas été trouvée.", hierarchy, course.getCode()); 548 return Optional.empty(); 549 } 550 } 551 552 private void _saxCourseList(TransformerHandler handler, CourseList list, Integer position) throws SAXException 553 { 554 if (list != null) 555 { 556 XMLUtils.createElement(handler, "list", "Lst" + position); 557 _saxChoiceList(handler, list); 558 } 559 } 560 561 private void _saxChoiceList(TransformerHandler handler, CourseList list) throws SAXException 562 { 563 // Type 564 ChoiceType typeList = list.getType(); 565 if (typeList != null) 566 { 567 if (typeList.name() != null && (typeList.name().equals(ChoiceType.CHOICE.toString()) || typeList.name().equals(ChoiceType.MANDATORY.toString()) || typeList.name().equals(ChoiceType.OPTIONAL.toString()))) 568 { 569 String typeListAsString = ""; 570 if (typeList.name().equals(ChoiceType.CHOICE.toString())) 571 { 572 typeListAsString = "X"; 573 } 574 else if (typeList.name().equals(ChoiceType.MANDATORY.toString())) 575 { 576 typeListAsString = "O"; 577 } 578 else if (typeList.name().equals(ChoiceType.OPTIONAL.toString())) 579 { 580 typeListAsString = "F"; 581 } 582 583 XMLUtils.createElement(handler, "typeList", typeListAsString); 584 } 585 586 // Min-Max (Ne remplir que pour le type "CHOICE" (ne mettre que le min)) 587 if (typeList.name() != null && typeList.name().equals(ChoiceType.CHOICE.toString())) 588 { 589 XMLUtils.createElement(handler, "minmax", _reportHelper.formatNumberToSax(list.getMinNumberOfCourses())); 590 } 591 } 592 } 593 594 class ProgramTitleComparator implements Comparator<Program> 595 { 596 @Override 597 public int compare(Program p1, Program p2) 598 { 599 600 return p1.getTitle().compareTo(p2.getTitle()); 601 } 602 } 603}