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.helper; 017 018import java.time.LocalDate; 019import java.time.format.DateTimeFormatter; 020import java.util.ArrayList; 021import java.util.HashSet; 022import java.util.LinkedHashMap; 023import java.util.List; 024import java.util.Locale; 025import java.util.Map; 026import java.util.Optional; 027import java.util.Set; 028import java.util.stream.Collectors; 029import java.util.stream.Stream; 030 031import javax.xml.transform.sax.TransformerHandler; 032 033import org.apache.avalon.framework.component.Component; 034import org.apache.avalon.framework.service.ServiceException; 035import org.apache.avalon.framework.service.ServiceManager; 036import org.apache.avalon.framework.service.Serviceable; 037import org.apache.cocoon.xml.AttributesImpl; 038import org.apache.cocoon.xml.XMLUtils; 039import org.apache.commons.collections.MapUtils; 040import org.apache.commons.lang3.StringUtils; 041import org.slf4j.Logger; 042import org.xml.sax.SAXException; 043 044import org.ametys.cms.data.ContentDataHelper; 045import org.ametys.cms.data.ContentValue; 046import org.ametys.cms.data.type.ModelItemTypeConstants; 047import org.ametys.cms.repository.Content; 048import org.ametys.cms.repository.ContentTypeExpression; 049import org.ametys.cms.repository.LanguageExpression; 050import org.ametys.cms.repository.ModifiableDefaultContent; 051import org.ametys.odf.ODFHelper; 052import org.ametys.odf.ProgramItem; 053import org.ametys.odf.course.Course; 054import org.ametys.odf.enumeration.OdfReferenceTableEntry; 055import org.ametys.odf.enumeration.OdfReferenceTableHelper; 056import org.ametys.odf.orgunit.OrgUnit; 057import org.ametys.odf.orgunit.OrgUnitFactory; 058import org.ametys.odf.orgunit.RootOrgUnitProvider; 059import org.ametys.odf.program.AbstractProgram; 060import org.ametys.odf.program.Container; 061import org.ametys.odf.program.Program; 062import org.ametys.odf.program.ProgramFactory; 063import org.ametys.plugins.repository.AmetysObjectIterable; 064import org.ametys.plugins.repository.AmetysObjectIterator; 065import org.ametys.plugins.repository.AmetysObjectResolver; 066import org.ametys.plugins.repository.AmetysRepositoryException; 067import org.ametys.plugins.repository.UnknownAmetysObjectException; 068import org.ametys.plugins.repository.query.QueryHelper; 069import org.ametys.plugins.repository.query.expression.AndExpression; 070import org.ametys.plugins.repository.query.expression.Expression; 071import org.ametys.plugins.repository.query.expression.Expression.Operator; 072import org.ametys.runtime.config.Config; 073import org.ametys.plugins.repository.query.expression.OrExpression; 074import org.ametys.plugins.repository.query.expression.StringExpression; 075 076/** 077 * Helper for report creation. 078 */ 079public class ReportHelper implements Component, Serviceable 080{ 081 /** The avalon role */ 082 public static final String ROLE = ReportHelper.class.getName(); 083 084 private static final String __READABLE_DF = "dd/MM/yyyy"; 085 086 /** The Ametys object resolver */ 087 protected AmetysObjectResolver _resolver; 088 089 /** The root orgunit provider */ 090 protected RootOrgUnitProvider _rootOrgUnitProvider; 091 092 /** The ODF helper */ 093 protected ODFHelper _odfHelper; 094 095 /** The ODF enumeration helper */ 096 protected OdfReferenceTableHelper _refTableHelper; 097 098 @Override 099 public void service(ServiceManager manager) throws ServiceException 100 { 101 _rootOrgUnitProvider = (RootOrgUnitProvider) manager.lookup(RootOrgUnitProvider.ROLE); 102 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 103 _odfHelper = (ODFHelper) manager.lookup(ODFHelper.ROLE); 104 _refTableHelper = (OdfReferenceTableHelper) manager.lookup(OdfReferenceTableHelper.ROLE); 105 } 106 107 /** 108 * Get the current date to the following format : 'dd/MM/yyyy' 109 * @return The date as a {@link String} 110 */ 111 public String getReadableCurrentDate() 112 { 113 return LocalDate.now().format(DateTimeFormatter.ofPattern(__READABLE_DF)); 114 } 115 116 /** 117 * Get the uaiCodes of the organization units involved in the groups report 118 * @param orgUnitId The parent UAI code 119 * @return if the uai code given by the user is valid, the list will contain solely this one 120 * if it is invalid, the list will contain no element and a warning message will be displayed 121 * if it is null, the list will contain all existing uai codes 122 */ 123 public List<String> getUaiCodes(String orgUnitId) 124 { 125 List<String> uaiCodes = new ArrayList<> (); 126 127 if (StringUtils.isEmpty(orgUnitId)) 128 { 129 uaiCodes = _getDirectSubOrgUnitsUAICodes(_rootOrgUnitProvider.getRootId()); 130 } 131 else 132 { 133 OrgUnit orgUnit = _resolver.resolveById(orgUnitId); 134 // Valid uai code 135 uaiCodes.add(orgUnit.getUAICode()); 136 } 137 138 return uaiCodes; 139 } 140 141 /** 142 * Get the accronym if exists or UAI code of the orgunit given. 143 * @param orgUnit The orgUnit 144 * @return The accronym if it exists, otherwise the UAI code 145 */ 146 public String getAccronymOrUaiCode(OrgUnit orgUnit) 147 { 148 return Optional.of(orgUnit) 149 .map(OrgUnit::getAcronym) 150 .filter(StringUtils::isNotBlank) 151 .orElseGet(orgUnit::getUAICode); 152 } 153 154 /** 155 * Get the accronym if exists or UAI code of the orgunit given by the UAI code. 156 * @param uaiCode The UAI code of the orgUnit 157 * @return The accronym if it exists, otherwise the UAI code 158 */ 159 public String getAccronymOrUaiCode(String uaiCode) 160 { 161 return getRootOrgUnitsByUaiCode(uaiCode).stream() 162 .findFirst() 163 .map(this::getAccronymOrUaiCode) 164 .orElse(uaiCode); 165 } 166 167 /** 168 * Retrieve the direct children's uai codes 169 * @param orgUnitId the id of the parent org unit 170 * @return the direct children's uai codes of the root organization unit 171 */ 172 private List<String> _getDirectSubOrgUnitsUAICodes(String orgUnitId) 173 { 174 OrgUnit orgUnit = _resolver.resolveById(orgUnitId); 175 List<String> orgUnitsUAICodes = new ArrayList<>(); 176 177 List<String> subOrgUnitsId = orgUnit.getSubOrgUnits(); 178 for (String subOrgUnitId : subOrgUnitsId) 179 { 180 OrgUnit subOrgUnit = _resolver.resolveById(subOrgUnitId); 181 String subOrgUnitUAICode = subOrgUnit.getUAICode(); 182 183 if (subOrgUnitUAICode != null) 184 { 185 orgUnitsUAICodes.add(subOrgUnitUAICode); 186 } 187 } 188 189 return orgUnitsUAICodes; 190 } 191 192 /** 193 * Get Programs with the current catalog, language and selected orgUnit. 194 * @param orgUnit Selected orgunit 195 * @param lang Selected language 196 * @param catalog Selected catalog 197 * @return A List of Program in the catalog, language and selected orgUnit 198 */ 199 public List<Program> filterProgramsFromOrgUnits(OrgUnit orgUnit, String lang, String catalog) 200 { 201 List<Program> selectedPrograms = new ArrayList<>(); 202 203 List<Expression> programExpressions = new ArrayList<>(); 204 programExpressions.add(new ContentTypeExpression(Operator.EQ, ProgramFactory.PROGRAM_CONTENT_TYPE)); 205 programExpressions.add(new LanguageExpression(Operator.EQ, lang)); 206 programExpressions.add(new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalog)); 207 208 if (orgUnit != null) 209 { 210 // Retrouver l'arborescence sous la composante 211 List<String> orgUnits = getSubOrgUnits(orgUnit); 212 213 // Chercher les programmes concernés par la composante sélectionnée et ses enfants 214 Expression[] orgUnitsExpressions = new Expression[orgUnits.size()]; 215 for (int i = 0; i < orgUnits.size(); i++) 216 { 217 String orgUnitId = orgUnits.get(i); 218 orgUnitsExpressions[i] = new StringExpression(AbstractProgram.ORG_UNITS_REFERENCES, Operator.EQ, orgUnitId); 219 } 220 221 programExpressions.add(new OrExpression(orgUnitsExpressions)); 222 } 223 224 String programQuery = QueryHelper.getXPathQuery(null, ProgramFactory.PROGRAM_NODETYPE, new AndExpression(programExpressions.toArray(new Expression[0]))); 225 AmetysObjectIterable<Program> programs = _resolver.query(programQuery); 226 AmetysObjectIterator<Program> programsIterator = programs.iterator(); 227 228 while (programsIterator.hasNext()) 229 { 230 selectedPrograms.add(programsIterator.next()); 231 } 232 233 return selectedPrograms; 234 } 235 236 /** 237 * Retrieves an organization unit with its uai code. 238 * @param uaiCode The UAI code 239 * @return the root organization units corresponding to this uai code 240 */ 241 public AmetysObjectIterable<OrgUnit> getRootOrgUnitsByUaiCode(String uaiCode) 242 { 243 // Find the root organization unit corresponding to this uai code 244 Expression orgUnitExpression = new AndExpression( 245 new ContentTypeExpression(Operator.EQ, OrgUnitFactory.ORGUNIT_CONTENT_TYPE), 246 new StringExpression(OrgUnit.CODE_UAI, Operator.EQ, uaiCode) 247 ); 248 String orgUnitQuery = QueryHelper.getXPathQuery(null, OrgUnitFactory.ORGUNIT_NODETYPE, orgUnitExpression); 249 AmetysObjectIterable<OrgUnit> rootOrgUnits = _resolver.query(orgUnitQuery); 250 251 return rootOrgUnits; 252 } 253 254 /** 255 * Get the ids of the organization units beneath the organization unit with the given id 256 * @param orgUnitId the id of the parent organization unit 257 * @return the list of child organization units ids 258 */ 259 public List<String> getSubOrgUnits(String orgUnitId) 260 { 261 OrgUnit orgUnit = _resolver.resolveById(orgUnitId); 262 return orgUnit == null ? null : getSubOrgUnits(orgUnit); 263 } 264 265 /** 266 * Get the ids of all the sub org units 267 * @param orgUnit the organization unit 268 * @return the list of child organization units ids 269 */ 270 public List<String> getSubOrgUnits(OrgUnit orgUnit) 271 { 272 List<String> orgUnits = new ArrayList<>(); 273 orgUnits.add(orgUnit.getId()); 274 275 for (String child : orgUnit.getSubOrgUnits()) 276 { 277 orgUnits.addAll(getSubOrgUnits(child)); 278 } 279 280 return orgUnits; 281 } 282 283 /** 284 * Format the given long 285 * @param number the long 286 * @return string representation of this long 287 */ 288 public String formatNumberToSax(Long number) 289 { 290 return number > 0 ? String.valueOf(number) : ""; 291 } 292 293 /** 294 * Get the programs' iterator of all programs contained in the organization unit with the given id 295 * @param orgUnitId the id of the organization unit 296 * @param lang the lang of the programs 297 * @param catalog the catalog of the programs 298 * @return the programs iterator 299 */ 300 public AmetysObjectIterable<Program> getProgramsByOrgUnitId(String orgUnitId, String lang, String catalog) 301 { 302 Expression programExpression = new AndExpression( 303 new ContentTypeExpression(Operator.EQ, ProgramFactory.PROGRAM_CONTENT_TYPE), 304 new LanguageExpression(Operator.EQ, lang), 305 new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalog), 306 new StringExpression(AbstractProgram.ORG_UNITS_REFERENCES, Operator.EQ, orgUnitId) 307 ); 308 String programQuery = QueryHelper.getXPathQuery(null, ProgramFactory.PROGRAM_NODETYPE, programExpression); 309 return _resolver.query(programQuery); 310 } 311 312 /** 313 * Get the list of courses underneath the given ametys object 314 * @param programItem The program item to gather the courses from 315 * @return the map representation of the tree of ametys objects 316 */ 317 public Map<ProgramItem, Object> getCoursesFromContent(ProgramItem programItem) 318 { 319 Map<ProgramItem, Object> contentTree = new LinkedHashMap<>(); 320 for (ProgramItem childProgramItem : _odfHelper.getChildProgramItems(programItem)) 321 { 322 Map<ProgramItem, Object> childTree = new LinkedHashMap<>(); 323 324 if (childProgramItem instanceof Course) 325 { 326 contentTree.put(childProgramItem, getCoursesFromContent(childProgramItem)); 327 } 328 else 329 { 330 childTree = getCoursesFromContent(childProgramItem); 331 } 332 333 if (MapUtils.isNotEmpty(childTree)) 334 { 335 contentTree.put(childProgramItem, childTree); 336 } 337 } 338 339 return contentTree.size() == 0 ? null : contentTree; 340 } 341 342 /** 343 * Get code VRSVDI 344 * @param content the content 345 * @return the codeVRSVDI if it's set, otherwise the second part of the content code 346 */ 347 public String getCodeVRSVDI(ModifiableDefaultContent content) 348 { 349 String defaultCode = StringUtils.substringAfter(((ProgramItem) content).getCode(), "-"); 350 return content.getValue("codeVRSVDI", false, defaultCode); 351 } 352 353 /** 354 * Get code DIP 355 * @param content the content 356 * @return the codeDIP if it's set, otherwise the first part of the content code 357 */ 358 public String getCodeDIP(ModifiableDefaultContent content) 359 { 360 String defaultCode = StringUtils.substringBefore(((ProgramItem) content).getCode(), "-"); 361 return content.getValue("codeDIP", false, defaultCode); 362 } 363 364 /** 365 * Generates SAX events for a multiple enumerated attribute. The attribute must be of type content or string 366 * @param handler The handler 367 * @param content The content 368 * @param attributeName The attribute name 369 * @param tagName The name of the tag 370 * @throws SAXException if an error occurs 371 */ 372 public void saxContentAttribute(TransformerHandler handler, ModifiableDefaultContent content, String attributeName, String tagName) throws SAXException 373 { 374 Locale lang = new Locale(content.getLanguage()); 375 376 if (!ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID.equals(content.getType(attributeName).getId())) 377 { 378 throw new IllegalArgumentException("The attribute '" + attributeName + "' should be of type '" + ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID + "'."); 379 } 380 381 Object value = content.getValue(attributeName); 382 if (value != null) 383 { 384 Stream<ContentValue> values = content.isMultiple(attributeName) 385 ? Stream.of((ContentValue[]) value) 386 : Stream.of((ContentValue) value); 387 388 String tagValue = 389 values.map(ContentValue::getContentIfExists) 390 .filter(Optional::isPresent) 391 .map(Optional::get) 392 .map(c -> c.getTitle(lang)) 393 .filter(StringUtils::isNotBlank) 394 .collect(Collectors.joining(", ")); 395 396 XMLUtils.createElement(handler, tagName, tagValue); 397 } 398 } 399 400 /** 401 * Convert a duration in minutes to a string representing the duration in hours. 402 * @param duree in minutes 403 * @return the duration in hours 404 */ 405 public String minute2hour(int duree) 406 { 407 int h = duree / 60; 408 int m = duree % 60; 409 return String.format("%dh%02d", h, m); 410 } 411 412 /** 413 * Sax the "natures d'enseignement" from the reference table. 414 * @param handler The transformer handler 415 * @param logger The logger 416 * @throws SAXException if an error occurs 417 */ 418 public void saxNaturesEnseignement(TransformerHandler handler, Logger logger) throws SAXException 419 { 420 String lang = Config.getInstance().getValue("odf.programs.lang"); 421 422 Map<String, List<OdfReferenceTableEntry>> itemsByCategory = 423 _refTableHelper.getItems(OdfReferenceTableHelper.ENSEIGNEMENT_NATURE) 424 .stream() 425 .collect(Collectors.groupingBy(item -> ContentDataHelper.getContentIdFromContentData(item.getContent(), "category"))); 426 427 XMLUtils.startElement(handler, "natureEnseignement"); 428 for (String categoryId : itemsByCategory.keySet()) 429 { 430 Content category = null; 431 try 432 { 433 category = Optional.ofNullable(categoryId) 434 .filter(StringUtils::isNotBlank) 435 .map(_resolver::<Content>resolveById) 436 .orElse(null); 437 } 438 catch (UnknownAmetysObjectException e) 439 { 440 if (StringUtils.isNotEmpty(categoryId)) 441 { 442 logger.warn("There is no content matching with the ID {}.", categoryId); 443 } 444 } 445 446 AttributesImpl attr = new AttributesImpl(); 447 attr.addCDATAAttribute("code", category == null ? StringUtils.EMPTY : category.getValue("code", false, StringUtils.EMPTY)); 448 attr.addCDATAAttribute("order", category == null ? String.valueOf(Long.MAX_VALUE) : String.valueOf(category.getValue("order", false, Long.MAX_VALUE))); 449 XMLUtils.startElement(handler, "category", attr); 450 for (OdfReferenceTableEntry item : itemsByCategory.get(categoryId)) 451 { 452 attr = new AttributesImpl(); 453 attr.addCDATAAttribute("id", item.getId()); 454 attr.addCDATAAttribute("code", item.getCode()); 455 attr.addCDATAAttribute("order", String.valueOf(item.getOrder())); 456 XMLUtils.createElement(handler, "item", attr, item.getLabel(lang)); 457 } 458 XMLUtils.endElement(handler, "category"); 459 } 460 XMLUtils.endElement(handler, "natureEnseignement"); 461 } 462 463 /** 464 * Get the steps which can hold this program item. 465 * @param programItem The program item 466 * @return The list of steps linked to the programItem 467 */ 468 public Set<Container> getSteps(ProgramItem programItem) 469 { 470 Set<Container> containers = new HashSet<>(); 471 472 // Search if the current element is a container and is of type year 473 if (programItem instanceof Container) 474 { 475 Container container = (Container) programItem; 476 if (_refTableHelper.getItemCode(container.getNature()).equals("annee")) 477 { 478 containers.add(container); 479 } 480 } 481 482 // In all other cases, search in the parent elements 483 if (containers.isEmpty()) 484 { 485 for (ProgramItem child : _odfHelper.getParentProgramItems(programItem)) 486 { 487 containers.addAll(getSteps(child)); 488 } 489 } 490 491 return containers; 492 } 493 494 /** 495 * Get the potential steps holder (step or field "etapePorteuse" in courses) of the {@link ProgramItem}. 496 * @param programItem The program item 497 * @param logger The logger 498 * @param logPrefix The log prefix 499 * @return The list of potential steps holder linked to the programItem. It there are several, there is no defined step holder. 500 */ 501 public Set<Container> getStepsHolders(ProgramItem programItem, Logger logger, String logPrefix) 502 { 503 Set<Container> containers = new HashSet<>(); 504 505 // Search if the current element is a course and has a step holder 506 if (programItem instanceof Course) 507 { 508 Course course = (Course) programItem; 509 ContentValue etapePorteuse = course.getValue("etapePorteuse"); 510 if (etapePorteuse != null) 511 { 512 logger.info("[{}] L'ELP {} ({}) contient une étape porteuse.", logPrefix, course.getTitle(), course.getId()); 513 try 514 { 515 containers.add((Container) etapePorteuse.getContent()); 516 } 517 catch (AmetysRepositoryException e) 518 { 519 logger.warn("[{}] L'étape porteuse {} référencée par l'ELP {} ({}) n'a pas été trouvée. Vérifiez qu'elle n'a pas été supprimée.", logPrefix, etapePorteuse.getContentId(), course.getTitle(), course.getId()); 520 } 521 } 522 } 523 // Search if the current element is a container and is of type year 524 else if (programItem instanceof Container) 525 { 526 Container container = (Container) programItem; 527 if (_refTableHelper.getItemCode(container.getNature()).equals("annee")) 528 { 529 containers.add(container); 530 } 531 } 532 533 // In all other cases, search in the parent elements 534 if (containers.isEmpty()) 535 { 536 for (ProgramItem child : _odfHelper.getParentProgramItems(programItem)) 537 { 538 containers.addAll(getStepsHolders(child, logger, logPrefix)); 539 } 540 } 541 542 return containers; 543 } 544}