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