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