001/* 002 * Copyright 2019 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.odf.content; 017 018import java.io.IOException; 019import java.util.ArrayList; 020import java.util.List; 021import java.util.Map; 022import java.util.Optional; 023import java.util.Set; 024import java.util.regex.Pattern; 025 026import org.apache.avalon.framework.activity.Initializable; 027import org.apache.avalon.framework.parameters.Parameters; 028import org.apache.avalon.framework.service.ServiceException; 029import org.apache.avalon.framework.service.ServiceManager; 030import org.apache.cocoon.ProcessingException; 031import org.apache.cocoon.environment.ObjectModelHelper; 032import org.apache.cocoon.environment.Request; 033import org.apache.cocoon.generation.ServiceableGenerator; 034import org.apache.cocoon.xml.AttributesImpl; 035import org.apache.cocoon.xml.SaxBuffer; 036import org.apache.cocoon.xml.XMLUtils; 037import org.apache.commons.lang.StringUtils; 038import org.apache.commons.lang3.LocaleUtils; 039import org.apache.commons.lang3.tuple.Triple; 040import org.xml.sax.SAXException; 041 042import org.ametys.cms.contenttype.ContentTypesHelper; 043import org.ametys.cms.repository.Content; 044import org.ametys.core.cache.AbstractCacheManager; 045import org.ametys.core.cache.Cache; 046import org.ametys.odf.EducationalPathHelper; 047import org.ametys.odf.NoLiveVersionException; 048import org.ametys.odf.ODFHelper; 049import org.ametys.odf.ProgramItem; 050import org.ametys.odf.course.Course; 051import org.ametys.odf.courselist.CourseList; 052import org.ametys.odf.coursepart.CoursePart; 053import org.ametys.odf.data.EducationalPath; 054import org.ametys.odf.enumeration.OdfReferenceTableEntry; 055import org.ametys.odf.enumeration.OdfReferenceTableHelper; 056import org.ametys.odf.program.AbstractProgram; 057import org.ametys.odf.program.Container; 058import org.ametys.odf.program.Program; 059import org.ametys.odf.program.SubProgram; 060import org.ametys.odf.skill.ODFSkillsHelper; 061import org.ametys.plugins.repository.AmetysObjectResolver; 062import org.ametys.plugins.repository.AmetysRepositoryException; 063import org.ametys.plugins.repository.jcr.DefaultAmetysObject; 064import org.ametys.runtime.i18n.I18nizableText; 065import org.ametys.runtime.model.View; 066import org.ametys.runtime.model.exception.BadItemTypeException; 067import org.ametys.runtime.model.type.DataContext; 068 069/** 070 * SAX the structure (ie. the child program items) of a {@link ProgramItem} 071 * 072 */ 073public class ProgramItemStructureGenerator extends ServiceableGenerator implements Initializable 074{ 075 // FIXME Accept all view names starting with "main" to be able to make the ODF and ODF orientation skins coexist (whereas they use different main views) 076 private static final Set<Pattern> __ALLOWED_VIEW_NAMES = Set.of(Pattern.compile("main[a-z\\-]*"), Pattern.compile("pdf")); 077 078 private static final String __REF_ITEM_CACHE_ID = ProgramItemStructureGenerator.class.getName() + "$refItems"; 079 private static final String __VIEW_CACHE_ID = ProgramItemStructureGenerator.class.getName() + "$view"; 080 private static final String __COMMON_ATTRIBUTES_CACHE_ID = ProgramItemStructureGenerator.class.getName() + "$commonAttributes"; 081 082 /** The ODF helper */ 083 protected ODFHelper _odfHelper; 084 /** Helper for ODF reference table */ 085 protected OdfReferenceTableHelper _odfReferenceTableHelper; 086 /** The content types helper */ 087 protected ContentTypesHelper _cTypesHelper; 088 /** The ODF skills helper */ 089 protected ODFSkillsHelper _odfSkillsHelper; 090 /** The Ametys object resolver */ 091 protected AmetysObjectResolver _resolver; 092 /** The cache manager */ 093 protected AbstractCacheManager _cacheManager; 094 095 private Cache<Triple<String, String, String>, SaxBuffer> _refItemCache; 096 private Cache<Content, SaxBuffer> _viewCache; 097 private Cache<ProgramItem, CommonAttributes> _commonAttrCache; 098 099 @Override 100 public void service(ServiceManager smanager) throws ServiceException 101 { 102 _odfHelper = (ODFHelper) smanager.lookup(ODFHelper.ROLE); 103 _odfReferenceTableHelper = (OdfReferenceTableHelper) smanager.lookup(OdfReferenceTableHelper.ROLE); 104 _cTypesHelper = (ContentTypesHelper) smanager.lookup(ContentTypesHelper.ROLE); 105 _odfSkillsHelper = (ODFSkillsHelper) smanager.lookup(ODFSkillsHelper.ROLE); 106 _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE); 107 _cacheManager = (AbstractCacheManager) smanager.lookup(AbstractCacheManager.ROLE); 108 } 109 110 @Override 111 public void setup(org.apache.cocoon.environment.SourceResolver res, Map objModel, String src, Parameters par) throws ProcessingException, SAXException, IOException 112 { 113 super.setup(res, objModel, src, par); 114 _refItemCache = _cacheManager.get(__REF_ITEM_CACHE_ID); 115 _viewCache = _cacheManager.get(__VIEW_CACHE_ID); 116 _commonAttrCache = _cacheManager.get(__COMMON_ATTRIBUTES_CACHE_ID); 117 } 118 119 @Override 120 public void recycle() 121 { 122 super.recycle(); 123 _refItemCache = null; 124 _viewCache = null; 125 _commonAttrCache = null; 126 } 127 128 public void initialize() throws Exception 129 { 130 if (!_cacheManager.hasCache(__REF_ITEM_CACHE_ID)) 131 { 132 _cacheManager.createRequestCache(__REF_ITEM_CACHE_ID, 133 new I18nizableText("plugin.odf", "PLUGINS_ODF_CACHE_PROGRAM_ITEM_STRUCTURE_REF_ITEMS_LABEL"), 134 new I18nizableText("plugin.odf", "PLUGINS_ODF_CACHE_PROGRAM_ITEM_STRUCTURE_REF_ITEMS_DESCRIPTION"), 135 false); 136 } 137 138 if (!_cacheManager.hasCache(__VIEW_CACHE_ID)) 139 { 140 _cacheManager.createRequestCache(__VIEW_CACHE_ID, 141 new I18nizableText("plugin.odf", "PLUGINS_ODF_CACHE_PROGRAM_ITEM_STRUCTURE_VIEW_LABEL"), 142 new I18nizableText("plugin.odf", "PLUGINS_ODF_CACHE_PROGRAM_ITEM_STRUCTURE_VIEW_DESCRIPTION"), 143 false); 144 } 145 146 if (!_cacheManager.hasCache(__COMMON_ATTRIBUTES_CACHE_ID)) 147 { 148 _cacheManager.createRequestCache(__COMMON_ATTRIBUTES_CACHE_ID, 149 new I18nizableText("plugin.odf", "PLUGINS_ODF_CACHE_PROGRAM_ITEM_STRUCTURE_COMMON_ATTRIBUTES_LABEL"), 150 new I18nizableText("plugin.odf", "PLUGINS_ODF_CACHE_PROGRAM_ITEM_STRUCTURE_COMMON_ATTRIBUTES_DESCRIPTION"), 151 false); 152 } 153 } 154 155 public void generate() throws IOException, SAXException, ProcessingException 156 { 157 Request request = ObjectModelHelper.getRequest(objectModel); 158 Content content = (Content) request.getAttribute(Content.class.getName()); 159 160 if (content == null) 161 { 162 String contentId = parameters.getParameter("contentId", null); 163 if (StringUtils.isBlank(contentId)) 164 { 165 throw new IllegalArgumentException("Content is missing in request attribute or parameters"); 166 } 167 content = _resolver.resolveById(contentId); 168 } 169 170 String viewName = parameters.getParameter("viewName", StringUtils.EMPTY); 171 String fallbackViewName = parameters.getParameter("fallbackViewName", StringUtils.EMPTY); 172 173 View view = _cTypesHelper.getViewWithFallback(viewName, fallbackViewName, content); 174 175 contentHandler.startDocument(); 176 177 if (view != null && __ALLOWED_VIEW_NAMES.stream().anyMatch(p -> p.matcher(view.getName()).matches())) 178 { 179 if (content instanceof ProgramItem programItem) 180 { 181 XMLUtils.startElement(contentHandler, "structure"); 182 183 List<ProgramItem> initialAncestorPath = _getInitialAncestorPath(request, programItem); 184 saxProgramItem(programItem, initialAncestorPath); 185 186 XMLUtils.endElement(contentHandler, "structure"); 187 } 188 else 189 { 190 getLogger().warn("Cannot get the structure of a non program item '" + content.getId() + "'"); 191 } 192 } 193 194 contentHandler.endDocument(); 195 } 196 197 /** 198 * Get the initial ancestor path from request or from root program item 199 * @param request the request 200 * @param rootProgramItem the root program item in saxed structure 201 * @return the initial ancestor path as a list of program items 202 */ 203 protected List<ProgramItem> _getInitialAncestorPath(Request request, ProgramItem rootProgramItem) 204 { 205 // First try to get ancestor path given by request 206 @SuppressWarnings("unchecked") 207 List<ProgramItem> ancestorPath = (List<ProgramItem>) request.getAttribute(EducationalPathHelper.PROGRAM_ITEM_ANCESTOR_PATH_REQUEST_ATTR); 208 209 if (ancestorPath == null) 210 { 211 if (rootProgramItem instanceof SubProgram subProgram) 212 { 213 List<EducationalPath> subProgramPaths = subProgram.getCurrentEducationalPaths(); 214 if (subProgramPaths != null && subProgramPaths.size() == 1) 215 { 216 // Init the ancestor paths from current educational path only if there is only one eligible educational path 217 ancestorPath = subProgramPaths.get(0).getProgramItems(_resolver); 218 } 219 } 220 else if (rootProgramItem instanceof Course course) 221 { 222 List<EducationalPath> coursePaths = course.getCurrentEducationalPaths(); 223 if (coursePaths != null && coursePaths.size() == 1) 224 { 225 // Init the ancestor paths from current educational path only if there is only one eligible educational path 226 ancestorPath = coursePaths.get(0).getProgramItems(_resolver); 227 } 228 } 229 } 230 231 // Ancestor path cannot be determine by context, initialize the ancestor path to a item itself 232 return ancestorPath == null || ancestorPath.isEmpty() ? List.of(rootProgramItem) : ancestorPath; 233 } 234 235 /** 236 * SAX a program item with its child program items 237 * @param programItem the program item 238 * @param ancestorPath The path of this program item in the saxed structure (computed from the initial saxed program item). Can be a partial path. 239 * @throws SAXException if an error occurs while saxing 240 */ 241 protected void saxProgramItem(ProgramItem programItem, List<ProgramItem> ancestorPath) throws SAXException 242 { 243 if (programItem instanceof Program) 244 { 245 saxProgram((Program) programItem); 246 } 247 else if (programItem instanceof SubProgram) 248 { 249 saxSubProgram((SubProgram) programItem, ancestorPath); 250 } 251 else if (programItem instanceof Container) 252 { 253 saxContainer((Container) programItem, ancestorPath); 254 } 255 else if (programItem instanceof CourseList) 256 { 257 saxCourseList((CourseList) programItem, ancestorPath); 258 } 259 else if (programItem instanceof Course) 260 { 261 saxCourse((Course) programItem, ancestorPath); 262 } 263 } 264 265 /** 266 * SAX the child program items 267 * @param programItem the program item 268 * @param ancestorPath The path of parent program item in the structure (starting from the initial saxed program item) 269 * @throws SAXException if an error occurs while saxing 270 */ 271 protected void saxChildProgramItems(ProgramItem programItem, List<ProgramItem> ancestorPath) throws SAXException 272 { 273 List<ProgramItem> childProgramItems = _odfHelper.getChildProgramItems(programItem); 274 for (ProgramItem childProgramItem : childProgramItems) 275 { 276 try 277 { 278 _odfHelper.switchToLiveVersionIfNeeded((DefaultAmetysObject) childProgramItem); 279 List<ProgramItem> childAncestorPath = new ArrayList<>(ancestorPath); 280 childAncestorPath.add(childProgramItem); 281 282 saxProgramItem(childProgramItem, childAncestorPath); 283 } 284 catch (NoLiveVersionException e) 285 { 286 // Just ignore the program item 287 } 288 } 289 } 290 291 /** 292 * SAX a program 293 * @param program the subprogram to SAX 294 * @throws SAXException if an error occurs 295 */ 296 protected void saxProgram(Program program) throws SAXException 297 { 298 AttributesImpl attrs = new AttributesImpl(); 299 _saxCommonAttributes(program, null, attrs); 300 301 XMLUtils.startElement(contentHandler, "program", attrs); 302 303 XMLUtils.startElement(contentHandler, "attributes"); 304 _saxStructureViewIfExists(program); 305 XMLUtils.endElement(contentHandler, "attributes"); 306 307 saxChildProgramItems(program, List.of(program)); 308 XMLUtils.endElement(contentHandler, "program"); 309 } 310 311 /** 312 * SAX a subprogram 313 * @param subProgram the subprogram to SAX 314 * @param ancestorPath The path of this program item in the saxed structure (computed from the initial saxed program item). Can be a partial path. 315 * @throws SAXException if an error occurs 316 */ 317 protected void saxSubProgram(SubProgram subProgram, List<ProgramItem> ancestorPath) throws SAXException 318 { 319 AttributesImpl attrs = new AttributesImpl(); 320 _saxCommonAttributes(subProgram, ancestorPath, attrs); 321 322 XMLUtils.startElement(contentHandler, "subprogram", attrs); 323 324 XMLUtils.startElement(contentHandler, "attributes"); 325 _saxReferenceTableItem(subProgram.getEcts(), AbstractProgram.ECTS, subProgram.getLanguage()); 326 _saxStructureViewIfExists(subProgram); 327 XMLUtils.endElement(contentHandler, "attributes"); 328 329 saxChildProgramItems(subProgram, ancestorPath); 330 331 XMLUtils.endElement(contentHandler, "subprogram"); 332 } 333 334 /** 335 * SAX a container 336 * @param container the container to SAX 337 * @param ancestorPath The path of this program item in the saxed structure (computed from the initial saxed program item). Can be a partial path. 338 * @throws SAXException if an error occurs while saxing 339 */ 340 protected void saxContainer(Container container, List<ProgramItem> ancestorPath) throws SAXException 341 { 342 AttributesImpl attrs = new AttributesImpl(); 343 _saxCommonAttributes(container, ancestorPath, attrs); 344 345 XMLUtils.startElement(contentHandler, "container", attrs); 346 347 XMLUtils.startElement(contentHandler, "attributes"); 348 _saxReferenceTableItem(container.getNature(), Container.NATURE, container.getLanguage()); 349 _saxReferenceTableItem(container.getPeriod(), Container.PERIOD, container.getLanguage()); 350 351 _saxStructureViewIfExists(container); 352 353 XMLUtils.endElement(contentHandler, "attributes"); 354 355 saxChildProgramItems(container, ancestorPath); 356 357 XMLUtils.endElement(contentHandler, "container"); 358 } 359 360 /** 361 * SAX a course list 362 * @param cl the course list to SAX 363 * @param ancestorPath The path of this program item in the saxed structure (computed from the initial saxed program item). Can be a partial path. 364 * @throws SAXException if an error occurs while saxing 365 */ 366 protected void saxCourseList(CourseList cl, List<ProgramItem> ancestorPath) throws SAXException 367 { 368 AttributesImpl attrs = new AttributesImpl(); 369 _saxCommonAttributes(cl, ancestorPath, attrs); 370 371 XMLUtils.startElement(contentHandler, "courselist", attrs); 372 373 XMLUtils.startElement(contentHandler, "attributes"); 374 _saxStructureViewIfExists(cl); 375 XMLUtils.endElement(contentHandler, "attributes"); 376 377 saxChildProgramItems(cl, ancestorPath); 378 379 XMLUtils.endElement(contentHandler, "courselist"); 380 } 381 382 /** 383 * SAX a course 384 * @param course the container to SAX 385 * @param ancestorPath The path of this program item in the saxed structure (computed from the initial saxed program item). Can be a partial path. 386 * @throws SAXException if an error occurs while saxing 387 */ 388 protected void saxCourse(Course course, List<ProgramItem> ancestorPath) throws SAXException 389 { 390 AttributesImpl attrs = new AttributesImpl(); 391 _saxCommonAttributes(course, ancestorPath, attrs); 392 393 XMLUtils.startElement(contentHandler, "course", attrs); 394 395 XMLUtils.startElement(contentHandler, "attributes"); 396 _saxReferenceTableItem(course.getCourseType(), Course.COURSE_TYPE, course.getLanguage()); 397 _saxStructureViewIfExists(course); 398 XMLUtils.endElement(contentHandler, "attributes"); 399 400 saxChildProgramItems(course, ancestorPath); 401 402 saxCourseParts(course); 403 404 XMLUtils.endElement(contentHandler, "course"); 405 } 406 407 /** 408 * SAX a course part 409 * @param course The course 410 * @throws SAXException if an error occurs 411 */ 412 protected void saxCourseParts(Course course) throws SAXException 413 { 414 List<CoursePart> courseParts = course.getCourseParts(); 415 416 double totalHours = 0; 417 List<CoursePart> liveCourseParts = new ArrayList<>(); 418 for (CoursePart coursePart : courseParts) 419 { 420 try 421 { 422 _odfHelper.switchToLiveVersionIfNeeded(coursePart); 423 totalHours += coursePart.getNumberOfHours(); 424 liveCourseParts.add(coursePart); 425 } 426 catch (NoLiveVersionException e) 427 { 428 getLogger().warn("Some hours of " + course.toString() + " are not added because the course part " + coursePart.toString() + " does not have a live version."); 429 } 430 } 431 432 AttributesImpl attrs = new AttributesImpl(); 433 attrs.addCDATAAttribute("totalHours", String.valueOf(totalHours)); 434 XMLUtils.startElement(contentHandler, "courseparts", attrs); 435 436 for (CoursePart coursePart : liveCourseParts) 437 { 438 saxCoursePart(coursePart); 439 } 440 441 XMLUtils.endElement(contentHandler, "courseparts"); 442 } 443 444 /** 445 * SAX a course part 446 * @param coursePart The course part to SAX 447 * @throws SAXException if an error occurs 448 */ 449 protected void saxCoursePart(CoursePart coursePart) throws SAXException 450 { 451 AttributesImpl attrs = new AttributesImpl(); 452 attrs.addCDATAAttribute("id", coursePart.getId()); 453 attrs.addCDATAAttribute("title", coursePart.getTitle()); 454 _addAttrIfNotEmpty(attrs, "code", coursePart.getCode()); 455 456 XMLUtils.startElement(contentHandler, "coursepart", attrs); 457 458 XMLUtils.startElement(contentHandler, "attributes"); 459 _saxReferenceTableItem(coursePart.getNature(), CoursePart.NATURE, coursePart.getLanguage()); 460 461 _saxStructureViewIfExists(coursePart); 462 463 XMLUtils.endElement(contentHandler, "attributes"); 464 465 XMLUtils.endElement(contentHandler, "coursepart"); 466 } 467 468 /** 469 * SAX the 'structure' view if exists 470 * @param content the content 471 * @throws SAXException if an error occurs 472 */ 473 protected void _saxStructureViewIfExists(Content content) throws SAXException 474 { 475 SaxBuffer buffer = _viewCache.get(content); 476 477 if (buffer != null) 478 { 479 buffer.toSAX(contentHandler); 480 return; 481 } 482 483 View view = _cTypesHelper.getView("structure", content.getTypes(), content.getMixinTypes()); 484 if (view != null) 485 { 486 try 487 { 488 buffer = new SaxBuffer(); 489 490 content.dataToSAX(buffer, view, DataContext.newInstance().withLocale(LocaleUtils.toLocale(content.getLanguage())).withEmptyValues(false)); 491 492 _viewCache.put(content, buffer); 493 buffer.toSAX(contentHandler); 494 } 495 catch (BadItemTypeException | AmetysRepositoryException e) 496 { 497 throw new SAXException("Fail to sax the 'structure' view for content " + content.getId(), e); 498 } 499 } 500 } 501 502 /** 503 * SAX the common attributes for program item 504 * @param programItem the program item 505 * @param ancestorPath The path of this program item in the structure (starting from the initial saxed program item) 506 * @param attrs the attributes 507 */ 508 protected void _saxCommonAttributes(ProgramItem programItem, List<ProgramItem> ancestorPath, AttributesImpl attrs) 509 { 510 CommonAttributes commonAttributes = _commonAttrCache.get(programItem, k -> { 511 return new CommonAttributes(programItem.getId(), ((Content) programItem).getTitle(), programItem.getCode(), programItem.getName(), _odfSkillsHelper.isExcluded(programItem)); 512 }); 513 514 attrs.addCDATAAttribute("title", commonAttributes.title()); 515 attrs.addCDATAAttribute("id", commonAttributes.id()); 516 attrs.addCDATAAttribute("code", commonAttributes.code()); 517 attrs.addCDATAAttribute("name", commonAttributes.name()); 518 boolean excludedFromSkills = commonAttributes.excludedFromSkills(); 519 if (excludedFromSkills) 520 { 521 attrs.addCDATAAttribute("excludedFromSkills", String.valueOf(excludedFromSkills)); 522 } 523 524 if (ancestorPath != null) 525 { 526 attrs.addCDATAAttribute("path", EducationalPath.of(ancestorPath.toArray(ProgramItem[]::new)).toString()); 527 } 528 } 529 530 private record CommonAttributes(String id, String title, String code, String name, boolean excludedFromSkills) { } 531 532 /** 533 * SAX the item of a reference table 534 * @param itemId the item id 535 * @param tagName the tag name 536 * @param lang the language to use 537 * @throws SAXException if an error occurs while saxing 538 */ 539 protected void _saxReferenceTableItem(String itemId, String tagName, String lang) throws SAXException 540 { 541 Triple<String, String, String> cacheKey = Triple.of(itemId, tagName, lang); 542 SaxBuffer buffer = _refItemCache.get(cacheKey); 543 544 if (buffer != null) 545 { 546 buffer.toSAX(contentHandler); 547 return; 548 } 549 550 buffer = new SaxBuffer(); 551 552 OdfReferenceTableEntry item = Optional.ofNullable(itemId) 553 .filter(StringUtils::isNotEmpty) 554 .map(_odfReferenceTableHelper::getItem) 555 .orElse(null); 556 557 if (item != null) 558 { 559 AttributesImpl attrs = new AttributesImpl(); 560 attrs.addCDATAAttribute("id", item.getId()); 561 _addAttrIfNotEmpty(attrs, "code", item.getCode()); 562 563 XMLUtils.createElement(buffer, tagName, attrs, item.getLabel(lang)); 564 565 _refItemCache.put(cacheKey, buffer); 566 buffer.toSAX(contentHandler); 567 } 568 } 569 570 /** 571 * Add an attribute if its not null or empty. 572 * @param attrs The attributes 573 * @param attrName The attribute name 574 * @param attrValue The attribute value 575 */ 576 protected void _addAttrIfNotEmpty(AttributesImpl attrs, String attrName, String attrValue) 577 { 578 if (StringUtils.isNotEmpty(attrValue)) 579 { 580 attrs.addCDATAAttribute(attrName, attrValue); 581 } 582 } 583 584}