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