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