001/* 002 * Copyright 2015 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.odfweb.xslt; 017 018import java.util.ArrayList; 019import java.util.HashMap; 020import java.util.List; 021import java.util.Map; 022import java.util.Optional; 023 024import org.apache.avalon.framework.context.Context; 025import org.apache.avalon.framework.context.ContextException; 026import org.apache.avalon.framework.context.Contextualizable; 027import org.apache.avalon.framework.service.ServiceException; 028import org.apache.avalon.framework.service.ServiceManager; 029import org.apache.cocoon.components.ContextHelper; 030import org.apache.cocoon.environment.Request; 031import org.apache.commons.lang.StringUtils; 032import org.apache.commons.lang3.tuple.Pair; 033import org.w3c.dom.Element; 034import org.w3c.dom.NodeList; 035 036import org.ametys.cms.data.ContentValue; 037import org.ametys.cms.repository.Content; 038import org.ametys.cms.repository.ModifiableContent; 039import org.ametys.core.util.dom.AmetysNodeList; 040import org.ametys.core.util.dom.MapElement; 041import org.ametys.core.util.dom.StringElement; 042import org.ametys.odf.EducationalPathHelper; 043import org.ametys.odf.ProgramItem; 044import org.ametys.odf.catalog.Catalog; 045import org.ametys.odf.catalog.CatalogsManager; 046import org.ametys.odf.course.Course; 047import org.ametys.odf.data.EducationalPath; 048import org.ametys.odf.program.AbstractProgram; 049import org.ametys.odf.program.Program; 050import org.ametys.odf.program.SubProgram; 051import org.ametys.odf.skill.ODFSkillsHelper; 052import org.ametys.plugins.odfweb.repository.CoursePage; 053import org.ametys.plugins.odfweb.repository.OdfPageHandler; 054import org.ametys.plugins.odfweb.repository.OdfPageResolver; 055import org.ametys.plugins.odfweb.repository.ProgramPage; 056import org.ametys.plugins.repository.AmetysObject; 057import org.ametys.runtime.config.Config; 058import org.ametys.web.WebConstants; 059import org.ametys.web.repository.page.Page; 060import org.ametys.web.repository.sitemap.Sitemap; 061import org.ametys.web.transformation.xslt.AmetysXSLTHelper; 062 063/** 064 * Helper component to be used from XSL stylesheets. 065 */ 066public class OdfXSLTHelper extends org.ametys.odf.OdfXSLTHelper implements Contextualizable 067{ 068 /** The ODF page handler */ 069 protected static OdfPageHandler _odfPageHandler; 070 071 /** The ODF skills helper */ 072 protected static ODFSkillsHelper _odfSkillsHelper; 073 074 /** The ODF page resolver */ 075 protected static OdfPageResolver _odfPageResolver; 076 077 /** The catalogs manager */ 078 protected static CatalogsManager _catalogsManager; 079 080 /** The avalon context */ 081 protected static Context _context; 082 083 /** 084 * Enum result to determine if a path is part of the current context 085 */ 086 public static enum EducationaPathContextResult 087 { 088 /** If the path does not belong to the current context */ 089 FALSE, 090 /** If the path does belong to the current context but we can't exactly determined the context */ 091 TRUE, 092 /** If the path does belong to the current context */ 093 TRUE_EXACTLY 094 } 095 096 @Override 097 public void service(ServiceManager smanager) throws ServiceException 098 { 099 super.service(smanager); 100 _odfPageHandler = (OdfPageHandler) smanager.lookup(OdfPageHandler.ROLE); 101 _odfSkillsHelper = (ODFSkillsHelper) smanager.lookup(ODFSkillsHelper.ROLE); 102 _odfPageResolver = (OdfPageResolver) smanager.lookup(OdfPageResolver.ROLE); 103 _catalogsManager = (CatalogsManager) smanager.lookup(CatalogsManager.ROLE); 104 } 105 106 public void contextualize(Context context) throws ContextException 107 { 108 _context = context; 109 } 110 111 /** 112 * Get the ODF root page, for a specific site, language. 113 * If there is many ODF root pages, the first page of the list is returned. 114 * @param siteName the desired site name. 115 * @param language the sitemap language to search in. 116 * @return the first ODF root page, or null if not found 117 */ 118 public static String odfRootPage(String siteName, String language) 119 { 120 Page odfRootPage = _odfPageHandler.getOdfRootPage(siteName, language); 121 if (odfRootPage != null) 122 { 123 return odfRootPage.getId(); 124 } 125 return null; 126 } 127 128 /** 129 * Get the ODF root page, for a specific site, language and catalog. 130 * @param siteName the desired site name. 131 * @param language the sitemap language to search in. 132 * @param catalog The ODF catalog 133 * @return the ODF root page, or null if not found 134 */ 135 public static String odfRootPage(String siteName, String language, String catalog) 136 { 137 Page odfRootPage = _odfPageHandler.getOdfRootPage(siteName, language, catalog); 138 if (odfRootPage != null) 139 { 140 return odfRootPage.getId(); 141 } 142 return null; 143 } 144 145 /** 146 * Get the PDF url of a program or a subprogram 147 * @param contentId The content id 148 * @param siteName The site name 149 * @return the PDF url or empty string if the content is not a {@link Program} or {@link SubProgram} 150 */ 151 public static String odfPDFUrl (String contentId, String siteName) 152 { 153 StringBuilder sb = new StringBuilder(); 154 155 Content content = _ametysObjectResolver.resolveById(contentId); 156 if (content instanceof AbstractProgram) 157 { 158 sb.append(AmetysXSLTHelper.uriPrefix()) 159 .append("/plugins/odf-web/") 160 .append(siteName) 161 .append("/_content/") 162 .append(content.getName()) 163 .append(".pdf"); 164 } 165 166 return sb.toString(); 167 } 168 169 /** 170 * Get the id of parent program from the current page 171 * @return the id of parent program or null if not found 172 */ 173 public static String parentProgramId() 174 { 175 String pageId = AmetysXSLTHelper.pageId(); 176 177 if (StringUtils.isNotEmpty(pageId)) 178 { 179 Page page = _ametysObjectResolver.resolveById(pageId); 180 181 AmetysObject parent = page.getParent(); 182 while (!(parent instanceof Sitemap)) 183 { 184 if (parent instanceof ProgramPage) 185 { 186 return ((ProgramPage) parent).getProgram().getId(); 187 } 188 189 parent = parent.getParent(); 190 } 191 } 192 193 return null; 194 } 195 196 /** 197 * Get the ECTS of the current course for the current context if present 198 * @return the ECTS or 0 if not found 199 */ 200 public static double getCurrentEcts() 201 { 202 Pair<ProgramItem, List<EducationalPath>> currentEducationalPaths = _getCurrentEducationalPaths(); 203 if (currentEducationalPaths != null) 204 { 205 ProgramItem programItem = currentEducationalPaths.getLeft(); 206 if (programItem instanceof Course course) 207 { 208 return course.getEcts(currentEducationalPaths.getRight()); 209 } 210 } 211 212 return 0; 213 } 214 215 /** 216 * Get the Skills of the current course for the current context if present 217 * @return the MicroSkills by MacroSkills or null if not found 218 */ 219 public static NodeList getCurrentSkills() 220 { 221 List<Element> result = new ArrayList<>(); 222 223 // If skills not enabled, return empty results 224 boolean isSkillsEnabled = Config.getInstance().getValue("odf.skills.enabled"); 225 if (!isSkillsEnabled) 226 { 227 return new AmetysNodeList(result); 228 } 229 230 Pair<ProgramItem, List<EducationalPath>> currentEducationalPaths = _getCurrentEducationalPaths(); 231 if (currentEducationalPaths != null) 232 { 233 ProgramItem programItem = currentEducationalPaths.getLeft(); 234 List<EducationalPath> educationalPath = currentEducationalPaths.getRight(); 235 if (programItem instanceof Course course && !educationalPath.isEmpty()) 236 { 237 EducationalPath programEducationalPath = educationalPath.getFirst(); 238 String programId = programEducationalPath.getProgramItemIds().get(0); 239 ContentValue[] skillsValues = course.getAcquiredSkills(programId); 240 241 Map<String, Element> createdMacroSkillNode = new HashMap<>(); 242 243 for (ContentValue skillValue : skillsValues) 244 { 245 Optional<ModifiableContent> optionalContent = skillValue.getContentIfExists(); 246 if (optionalContent.isPresent()) 247 { 248 Content skillContent = optionalContent.get(); 249 250 ContentValue parentMacroSkillValue = skillContent.getValue("parentMacroSkill"); 251 if (parentMacroSkillValue != null) 252 { 253 Optional<ModifiableContent> optionalParentMacroSkill = parentMacroSkillValue.getContentIfExists(); 254 if (optionalParentMacroSkill.isPresent()) 255 { 256 ModifiableContent parentMacroSkill = optionalParentMacroSkill.get(); 257 258 String parentMacroSkillId = parentMacroSkill.getId(); 259 Map<String, String> macroSkillMap = new HashMap<>(); 260 if (!createdMacroSkillNode.containsKey(parentMacroSkillId)) 261 { 262 macroSkillMap.put("id", parentMacroSkillId); 263 macroSkillMap.put("title", parentMacroSkill.getTitle()); 264 macroSkillMap.put("type", "MACROSKILL"); 265 macroSkillMap.put("parentProgram", programId); 266 267 Element parentNode = new StringElement("skill", macroSkillMap); 268 269 createdMacroSkillNode.put(parentMacroSkillId, parentNode); 270 271 result.add(parentNode); 272 } 273 274 Map<String, String> skillMap = new HashMap<>(); 275 skillMap.put("id", skillContent.getId()); 276 skillMap.put("title", skillContent.getTitle()); 277 skillMap.put("type", "MICROSKILL"); 278 skillMap.put("parent", parentMacroSkillId); 279 280 Element node = new StringElement("skill", skillMap); 281 result.add(node); 282 } 283 } 284 } 285 } 286 } 287 } 288 289 return new AmetysNodeList(result); 290 } 291 292 private static Pair<ProgramItem, List<EducationalPath>> _getCurrentEducationalPaths() 293 { 294 Request request = ContextHelper.getRequest(_context); 295 Page page = (Page) request.getAttribute(WebConstants.REQUEST_ATTR_PAGE); 296 297 // First try to get current educational paths from course page if present 298 if (page != null) 299 { 300 if (page instanceof CoursePage coursePage) 301 { 302 Course course = coursePage.getContent(); 303 return Pair.of(course, course.getCurrentEducationalPaths()); 304 } 305 else if (page instanceof ProgramPage programPage) 306 { 307 AbstractProgram abstractProgram = programPage.getContent(); 308 return Pair.of(abstractProgram, abstractProgram.getCurrentEducationalPaths()); 309 } 310 } 311 312 // Then try to get current educational paths from course content if present 313 Content content = (Content) request.getAttribute(Content.class.getName()); 314 return _getCurrentEducationalPaths(content); 315 } 316 317 private static Pair<ProgramItem, List<EducationalPath>> _getCurrentEducationalPaths(Content content) 318 { 319 Request request = ContextHelper.getRequest(_context); 320 if (content != null && (content instanceof Course || content instanceof AbstractProgram)) 321 { 322 // First try to get educational paths from content 323 List<EducationalPath> currentEducationalPaths = content instanceof Course course ? course.getCurrentEducationalPaths() : ((AbstractProgram) content).getCurrentEducationalPaths(); 324 if (currentEducationalPaths == null) 325 { 326 // If null try to get educational paths from request attributes 327 @SuppressWarnings("unchecked") 328 List<ProgramItem> pathFromRequest = (List<ProgramItem>) request.getAttribute(EducationalPathHelper.PROGRAM_ITEM_ANCESTOR_PATH_REQUEST_ATTR); 329 if (pathFromRequest != null) 330 { 331 // In request the path may be a partial path 332 currentEducationalPaths = _odfHelper.getEducationPathFromPath(pathFromRequest); 333 334 // If ancestor is present in request attribute, filter paths that contains this ancestor 335 ProgramItem ancestor = (ProgramItem) request.getAttribute(EducationalPathHelper.ROOT_PROGRAM_ITEM_REQUEST_ATTR); 336 if (ancestor != null) 337 { 338 currentEducationalPaths = currentEducationalPaths.stream() 339 .filter(p -> p.getProgramItemIds().contains(ancestor.getId())) 340 .toList(); 341 } 342 } 343 else 344 { 345 // Cannot determine current educational paths from context, returns all available education paths 346 currentEducationalPaths = _odfHelper.getEducationalPaths((ProgramItem) content, true, true); 347 } 348 } 349 350 return Pair.of((ProgramItem) content, currentEducationalPaths); 351 } 352 353 return null; 354 } 355 356 /** 357 * Determines if a given {@link EducationalPath} is part of the current {@link EducationalPath}s<br> 358 * - TRUE_EXACTLY if the education path belongs to the current educational path and the current educational path is unique 359 * - TRUE if the education path belongs to the current educational paths 360 * - FALSE if the education path does not belong to the current educational paths 361 * @param educationPath the education path as string 362 * @return the status of the entry path belonging to the current context or not 363 */ 364 public static String isPartOfCurrentEducationalPaths(String educationPath) 365 { 366 return _isPartOfCurrentEducationalPaths(educationPath).name(); 367 } 368 369 private static EducationaPathContextResult _isPartOfCurrentEducationalPaths(String educationPath) 370 { 371 EducationalPath educationalPath = EducationalPath.of(educationPath.split(EducationalPath.PATH_SEGMENT_SEPARATOR)); 372 373 List<EducationalPath> currentEducationalPaths = _getCurrentEducationalPaths().getRight(); 374 if (currentEducationalPaths.contains(educationalPath)) 375 { 376 return currentEducationalPaths.size() > 1 377 ? EducationaPathContextResult.TRUE // Cannot determine the current context precisely 378 : EducationaPathContextResult.TRUE_EXACTLY; // Only one educational path possible 379 } 380 381 return EducationaPathContextResult.FALSE; // no part of current education paths 382 } 383 384 /** 385 * Extract educational path of main structure that the given education path belongs to. The path stops to the last parent container of type year or the last subprogram if the year does not exist. 386 * @param educationalPath the educational path as string 387 * @return the readable education path of main structure 388 */ 389 public static String getMainStructureEducationalPathAsString(String educationalPath) 390 { 391 EducationalPath educationPath = EducationalPath.of(educationalPath.split(EducationalPath.PATH_SEGMENT_SEPARATOR)); 392 return educationPath != null ? _odfHelper.getEducationalPathAsString(educationPath, pi -> ((Content) pi).getTitle(), " > ", pi -> _filterProgramItemInPath(pi)) : null; 393 } 394 395 private static boolean _filterProgramItemInPath(ProgramItem p) 396 { 397 return p instanceof AbstractProgram || _odfHelper.isContainerOfTypeYear((Content) p); 398 } 399 400 /** 401 * Get the ECTS of the current course for the current context if present 402 * @param defaultValue The default value 403 * @return the ECTS or 0 if not found 404 */ 405 public static double getCurrentEcts(String defaultValue) 406 { 407 double currentEcts = getCurrentEcts(); 408 return currentEcts != 0 ? currentEcts : (StringUtils.isNotEmpty(defaultValue) ? Double.valueOf(defaultValue) : 0); 409 } 410 411 /** 412 * Determines if the values of ECTS is equals for the course's educational paths in the current context 413 * @param courseId The course id 414 * @return true if the values of ECTS is equals in the current context 415 */ 416 public static boolean areECTSEqual(String courseId) 417 { 418 Course course = _ametysObjectResolver.resolveById(courseId); 419 return _areECTSEqual(course); 420 } 421 422 /** 423 * Determines if the values of ECTS is equals for the current course's educational paths in the current context 424 * @return true if the values of ECTS is equals in the current context 425 */ 426 public static boolean areECTSEqual() 427 { 428 Request request = ContextHelper.getRequest(_context); 429 Content content = (Content) request.getAttribute(Content.class.getName()); 430 if (content != null && content instanceof Course course) 431 { 432 return _areECTSEqual(course); 433 } 434 return false; 435 } 436 437 private static boolean _areECTSEqual(Course course) 438 { 439 Pair<ProgramItem, List<EducationalPath>> currentEducationalPaths = _getCurrentEducationalPaths(course); 440 if (currentEducationalPaths != null) 441 { 442 ProgramItem programItem = currentEducationalPaths.getLeft(); 443 List<EducationalPath> paths = currentEducationalPaths.getRight(); 444 return paths != null ? _odfHelper.isSameValueForPaths(programItem, Course.ECTS_BY_PATH, paths) : _odfHelper.isSameValueForAllPaths(programItem, Course.ECTS_BY_PATH); 445 } 446 else 447 { 448 return _odfHelper.isSameValueForAllPaths(course, Course.ECTS_BY_PATH); 449 } 450 } 451 452 /** 453 * <code>true</code> if the program item is part of an program item (program, subprogram or container) that is excluded from skills 454 * @param programItemId the program item id 455 * @param programPageItemId the program item page id. If null or empty, program item is display with no context, consider that skills are available 456 * @return <code>true</code> if the program item has an excluded parent in it path from the page context 457 */ 458 public static boolean areSkillsUnavailable(String programItemId, String programPageItemId) 459 { 460 if (StringUtils.isBlank(programItemId) || StringUtils.isBlank(programPageItemId)) 461 { 462 // program part is displayed outside a page context, assuming that skills should be displayed 463 return false; 464 } 465 466 ProgramItem programItem = _ametysObjectResolver.resolveById(programItemId); 467 if (programItem instanceof Program) 468 { 469 return _odfSkillsHelper.isExcluded(programItem); 470 } 471 472 Page programItemPage = _ametysObjectResolver.resolveById(programPageItemId); 473 474 ProgramPage closestProgramPage = _getClosestProgramPage(programItemPage); 475 AbstractProgram closestProgramOrSubprogram = closestProgramPage.getProgram(); 476 477 ProgramItem parent = _odfHelper.getParentProgramItem(programItem, closestProgramOrSubprogram); 478 while (parent != null && !(parent instanceof Program)) 479 { 480 if (_odfSkillsHelper.isExcluded(parent)) 481 { 482 // If the parent is excluded, the skills are unavailable 483 return true; 484 } 485 486 // If the closest program parent is a subprogram, continue to its program parent 487 if (closestProgramOrSubprogram instanceof SubProgram && closestProgramOrSubprogram.equals(parent)) 488 { 489 closestProgramOrSubprogram = ((ProgramPage) closestProgramPage.getParent()).getProgram(); 490 } 491 parent = _odfHelper.getParentProgramItem(parent, closestProgramOrSubprogram); 492 } 493 494 return parent != null ? _odfSkillsHelper.isExcluded(parent) : false; 495 } 496 497 private static ProgramPage _getClosestProgramPage(Page page) 498 { 499 Page parentPage = page.getParent(); 500 while (!(parentPage instanceof ProgramPage)) 501 { 502 parentPage = parentPage.getParent(); 503 } 504 505 return (ProgramPage) parentPage; 506 } 507 508 /** 509 * Get the pages of program items with the same code. 510 * @param programItemId The program item identifier 511 * @param lang The language 512 * @param siteName The site name 513 * @return the list of pages holding program items with the same code (one by catalog max) 514 */ 515 public static NodeList getProgramItemPages(String programItemId, String lang, String siteName) 516 { 517 ProgramItem currentProgramItem = _ametysObjectResolver.resolveById(programItemId); 518 519 List<Element> pages = new ArrayList<>(); 520 521 // Get all existing contents with this code 522 for (Content content : _odfHelper.getProgramItems(List.of(), currentProgramItem.getCode(), null, lang)) 523 { 524 if (content instanceof ProgramItem programItem) 525 { 526 // Get the catalog of the program item 527 Catalog catalog = _catalogsManager.getCatalog(programItem.getCatalog()); 528 529 // Retrieve all pages referencing this content in the given sitename and language 530 _odfPageResolver.getReferencingPages(programItem, siteName, lang) 531 .stream() 532 // Retain only the first one 533 .findFirst() 534 // Build the values to return 535 .map(Page::getId) 536 .map(pageId -> 537 new MapElement( 538 "page", 539 Map.of("isCurrentCatalog", String.valueOf(programItem.getCatalog().equals(currentProgramItem.getCatalog()))), 540 Map.of( 541 "pageId", pageId, 542 "catalog", catalog.getTitle() 543 ) 544 ) 545 ) 546 // Only add it if exists 547 .ifPresent(pages::add); 548 } 549 } 550 551 return new AmetysNodeList(pages); 552 } 553}