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