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