001/* 002 * Copyright 2010 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.program.generators; 017 018import java.io.IOException; 019import java.util.HashMap; 020import java.util.LinkedHashSet; 021import java.util.List; 022import java.util.Locale; 023import java.util.Map; 024import java.util.Set; 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.components.source.impl.SitemapSource; 032import org.apache.cocoon.environment.ObjectModelHelper; 033import org.apache.cocoon.environment.Request; 034import org.apache.cocoon.generation.Generator; 035import org.apache.cocoon.xml.AttributesImpl; 036import org.apache.cocoon.xml.SaxBuffer; 037import org.apache.cocoon.xml.XMLUtils; 038import org.apache.commons.lang.StringUtils; 039import org.apache.commons.lang3.tuple.Triple; 040import org.apache.excalibur.source.SourceResolver; 041import org.xml.sax.ContentHandler; 042import org.xml.sax.SAXException; 043 044import org.ametys.cms.CmsConstants; 045import org.ametys.cms.content.ContentHelper; 046import org.ametys.cms.contenttype.ContentAttributeDefinition; 047import org.ametys.cms.contenttype.ContentType; 048import org.ametys.cms.data.ContentDataHelper; 049import org.ametys.cms.data.ContentValue; 050import org.ametys.cms.repository.Content; 051import org.ametys.core.cache.AbstractCacheManager; 052import org.ametys.core.cache.Cache; 053import org.ametys.core.util.DateUtils; 054import org.ametys.core.util.IgnoreRootHandler; 055import org.ametys.odf.NoLiveVersionException; 056import org.ametys.odf.ODFHelper; 057import org.ametys.odf.course.Course; 058import org.ametys.odf.courselist.CourseList; 059import org.ametys.odf.courselist.CourseList.ChoiceType; 060import org.ametys.odf.coursepart.CoursePart; 061import org.ametys.odf.orgunit.OrgUnit; 062import org.ametys.odf.orgunit.OrgUnitFactory; 063import org.ametys.odf.person.Person; 064import org.ametys.odf.person.PersonFactory; 065import org.ametys.odf.program.AbstractProgram; 066import org.ametys.odf.program.Container; 067import org.ametys.odf.program.Program; 068import org.ametys.odf.program.ProgramPart; 069import org.ametys.odf.program.SubProgram; 070import org.ametys.odf.program.TraversableProgramPart; 071import org.ametys.odf.translation.TranslationHelper; 072import org.ametys.plugins.repository.UnknownAmetysObjectException; 073import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder; 074import org.ametys.plugins.repository.data.holder.group.ModelAwareComposite; 075import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeater; 076import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeaterEntry; 077import org.ametys.plugins.repository.jcr.DefaultAmetysObject; 078import org.ametys.plugins.repository.model.RepeaterDefinition; 079import org.ametys.runtime.i18n.I18nizableText; 080import org.ametys.runtime.model.ModelItem; 081import org.ametys.runtime.model.ModelItemContainer; 082import org.ametys.runtime.model.View; 083import org.ametys.runtime.model.type.DataContext; 084 085/** 086 * {@link Generator} for rendering raw content data for a {@link Program}. 087 */ 088public class ProgramContentGenerator extends ODFContentGenerator implements Initializable 089{ 090 private static final String __CACHE_ID = ProgramContentGenerator.class.getName() + "$linkedContents"; 091 092 /** The source resolver */ 093 protected SourceResolver _srcResolver; 094 095 /** The ODF helper */ 096 protected ODFHelper _odfHelper; 097 098 /** The content helper */ 099 protected ContentHelper _contentHelper; 100 101 private AbstractCacheManager _cacheManager; 102 private Cache<Triple<String, String, String>, SaxBuffer> _cache; 103 104 @Override 105 public void service(ServiceManager serviceManager) throws ServiceException 106 { 107 super.service(serviceManager); 108 _srcResolver = (SourceResolver) serviceManager.lookup(SourceResolver.ROLE); 109 _odfHelper = (ODFHelper) serviceManager.lookup(ODFHelper.ROLE); 110 _cacheManager = (AbstractCacheManager) serviceManager.lookup(AbstractCacheManager.ROLE); 111 _contentHelper = (ContentHelper) serviceManager.lookup(ContentHelper.ROLE); 112 } 113 114 public void initialize() throws Exception 115 { 116 if (!_cacheManager.hasCache(__CACHE_ID)) 117 { 118 _cacheManager.createRequestCache(__CACHE_ID, 119 new I18nizableText("plugin.odf", "PLUGINS_ODF_CACHE_PROGRAMCONTENTGENERATOR_LABEL"), 120 new I18nizableText("plugin.odf", "PLUGINS_ODF_CACHE_PROGRAMCONTENTGENERATOR_DESCRIPTION"), 121 false); 122 } 123 } 124 125 @Override 126 public void setup(org.apache.cocoon.environment.SourceResolver res, Map objModel, String src, Parameters par) throws ProcessingException, SAXException, IOException 127 { 128 super.setup(res, objModel, src, par); 129 _cache = _cacheManager.get(__CACHE_ID); 130 } 131 132 @Override 133 public void recycle() 134 { 135 super.recycle(); 136 _cache = null; 137 } 138 139 @Override 140 protected void _saxOtherData(Content content, Locale defaultLocale) throws SAXException, ProcessingException, IOException 141 { 142 super._saxOtherData(content, defaultLocale); 143 144 boolean isEdition = parameters.getParameterAsBoolean("isEdition", false); 145 if (!isEdition) 146 { 147 if (content instanceof AbstractProgram) 148 { 149 AbstractProgram program = (AbstractProgram) content; 150 151 // Contacts 152 saxPersons(program); 153 154 // Child containers, subprograms or course lists 155 saxChildProgramPart(program, defaultLocale); 156 157 // OrgUnits 158 saxOrgUnits(program); 159 160 // Translations 161 saxTranslation(program); 162 } 163 } 164 165 Request request = ObjectModelHelper.getRequest(objectModel); 166 request.setAttribute(Content.class.getName(), content); 167 } 168 169 /** 170 * SAX the referenced {@link Person}s 171 * @param content The content to analyze 172 * @throws SAXException if an error occurs while saxing 173 */ 174 protected void saxPersons(Content content) throws SAXException 175 { 176 saxLinkedContents(content, "persons", PersonFactory.PERSON_CONTENT_TYPE, "abstract"); 177 } 178 179 /** 180 * SAX the referenced {@link OrgUnit}s 181 * @param content The content to analyze 182 * @throws SAXException if an error occurs while saxing 183 */ 184 protected void saxOrgUnits(Content content) throws SAXException 185 { 186 saxLinkedContents(content, "orgunits", OrgUnitFactory.ORGUNIT_CONTENT_TYPE, "link"); 187 } 188 189 /** 190 * SAX the referenced {@link ProgramPart}s 191 * @param program The program or subprogram 192 * @param defaultLocale The default locale 193 * @throws SAXException if an error occurs while saxing 194 * @throws IOException if an error occurs 195 * @throws ProcessingException if an error occurs 196 */ 197 protected void saxChildProgramPart(AbstractProgram program, Locale defaultLocale) throws SAXException, ProcessingException, IOException 198 { 199 ContentValue[] children = program.getValue(TraversableProgramPart.CHILD_PROGRAM_PARTS, false, new ContentValue[0]); 200 for (ContentValue child : children) 201 { 202 try 203 { 204 Content content = child.getContent(); 205 _odfHelper.switchToLiveVersionIfNeeded((DefaultAmetysObject) content); 206 207 if (content instanceof SubProgram) 208 { 209 saxSubProgram((SubProgram) content, program.getContextPath()); 210 } 211 else if (content instanceof Container) 212 { 213 saxContainer ((Container) content, program.getContextPath(), defaultLocale); 214 } 215 else if (content instanceof CourseList) 216 { 217 saxCourseList((CourseList) content, program.getContextPath(), defaultLocale); 218 } 219 } 220 catch (UnknownAmetysObjectException | NoLiveVersionException e) 221 { 222 // The content can reference a non-existing child: ignore the exception. 223 // Or the content has no live version: ignore too 224 } 225 } 226 } 227 228 /** 229 * SAX the existing translation 230 * @param content The content 231 * @throws SAXException if an error occurs while saxing 232 */ 233 protected void saxTranslation(Content content) throws SAXException 234 { 235 Map<String, String> translations = TranslationHelper.getTranslations(content); 236 if (!translations.isEmpty()) 237 { 238 saxTranslations(translations); 239 } 240 } 241 242 /** 243 * SAX the referenced content types. 244 * @param content The content to analyze 245 * @param tagName The root tagName 246 * @param linkedContentType The content type to search 247 * @param viewName The view to parse the found contents 248 * @throws SAXException if an error occurs while saxing 249 */ 250 protected void saxLinkedContents(Content content, String tagName, String linkedContentType, String viewName) throws SAXException 251 { 252 Set<String> contentIds = getLinkedContents(content, linkedContentType); 253 254 XMLUtils.startElement(contentHandler, tagName); 255 for (String id : contentIds) 256 { 257 if (StringUtils.isNotEmpty(id)) 258 { 259 try 260 { 261 Content subContent = _resolver.resolveById(id); 262 saxContent(subContent, contentHandler, viewName); 263 } 264 catch (IOException e) 265 { 266 throw new SAXException(e); 267 } 268 catch (UnknownAmetysObjectException e) 269 { 270 // The program can reference a non-existing person: ignore the exception. 271 } 272 } 273 } 274 275 XMLUtils.endElement(contentHandler, tagName); 276 } 277 278 /** 279 * Get the linked contents of the defined content type. 280 * @param content The content to analyze 281 * @param linkedContentType The content type to search 282 * @return A {@link Set} of content ids 283 */ 284 protected Set<String> getLinkedContents(Content content, String linkedContentType) 285 { 286 Set<String> contentIds = new LinkedHashSet<>(); 287 288 for (String cTypeId : content.getTypes()) 289 { 290 ContentType cType = _contentTypeExtensionPoint.getExtension(cTypeId); 291 contentIds.addAll(_getContentIds(content, cType, linkedContentType)); 292 } 293 294 return contentIds; 295 } 296 297 private Set<String> _getContentIds(ModelAwareDataHolder dataHolder, ModelItemContainer modelItemContainer, String contentType) 298 { 299 Set<String> contentIds = new LinkedHashSet<>(); 300 301 for (ModelItem modelItem : modelItemContainer.getModelItems()) 302 { 303 if (modelItem instanceof ContentAttributeDefinition) 304 { 305 if (contentType.equals(((ContentAttributeDefinition) modelItem).getContentTypeId())) 306 { 307 String modelItemName = modelItem.getName(); 308 if (dataHolder.isMultiple(modelItemName)) 309 { 310 List<String> values = ContentDataHelper.getContentIdsListFromMultipleContentData(dataHolder, modelItemName); 311 contentIds.addAll(values); 312 } 313 else 314 { 315 contentIds.add(ContentDataHelper.getContentIdFromContentData(dataHolder, modelItemName)); 316 } 317 } 318 } 319 else if (modelItem instanceof ModelItemContainer) 320 { 321 322 if (dataHolder.hasValue(modelItem.getName())) 323 { 324 if (modelItem instanceof RepeaterDefinition) 325 { 326 ModelAwareRepeater repeater = dataHolder.getRepeater(modelItem.getName()); 327 for (ModelAwareRepeaterEntry entry : repeater.getEntries()) 328 { 329 contentIds.addAll(_getContentIds(entry, (ModelItemContainer) modelItem, contentType)); 330 } 331 } 332 else 333 { 334 ModelAwareComposite composite = dataHolder.getComposite(modelItem.getName()); 335 contentIds.addAll(_getContentIds(composite, (ModelItemContainer) modelItem, contentType)); 336 } 337 } 338 } 339 } 340 341 return contentIds; 342 } 343 344 /** 345 * SAX a container 346 * @param container the container to SAX 347 * @param parentPath the parent path 348 * @param defaultLocale The default locale 349 * @throws SAXException if an error occurs 350 * @throws IOException if an error occurs 351 * @throws ProcessingException if an error occurs 352 */ 353 protected void saxContainer(Container container, String parentPath, Locale defaultLocale) throws SAXException, ProcessingException, IOException 354 { 355 AttributesImpl attrs = new AttributesImpl(); 356 attrs.addCDATAAttribute("title", container.getTitle()); 357 attrs.addCDATAAttribute("id", container.getId()); 358 _addAttrIfNotEmpty(attrs, "code", container.getCode()); 359 _addAttrIfNotEmpty(attrs, "nature", container.getNature()); 360 double ects = container.getEcts(); 361 if (ects > 0) 362 { 363 attrs.addCDATAAttribute("ects", String.valueOf(ects)); 364 } 365 366 XMLUtils.startElement(contentHandler, "container", attrs); 367 368 // Children 369 ContentValue[] children = container.getValue(TraversableProgramPart.CHILD_PROGRAM_PARTS, false, new ContentValue[0]); 370 for (ContentValue child : children) 371 { 372 try 373 { 374 Content content = child.getContent(); 375 if (content instanceof SubProgram) 376 { 377 saxSubProgram((SubProgram) content, parentPath); 378 } 379 else if (content instanceof Container) 380 { 381 saxContainer ((Container) content, parentPath, defaultLocale); 382 } 383 else if (content instanceof CourseList) 384 { 385 saxCourseList((CourseList) content, parentPath, defaultLocale); 386 } 387 } 388 catch (UnknownAmetysObjectException e) 389 { 390 // The content can reference a non-existing child (in live for example): ignore the exception. 391 } 392 } 393 394 XMLUtils.endElement(contentHandler, "container"); 395 } 396 397 /** 398 * SAX a sub program 399 * @param subProgram the sub program to SAX 400 * @param parentPath the parent path 401 * @throws SAXException if an error occurs 402 */ 403 protected void saxSubProgram(SubProgram subProgram, String parentPath) throws SAXException 404 { 405 AttributesImpl attrs = new AttributesImpl(); 406 attrs.addCDATAAttribute("title", subProgram.getTitle()); 407 attrs.addCDATAAttribute("id", subProgram.getId()); 408 _addAttrIfNotEmpty(attrs, "code", subProgram.getCode()); 409 _addAttrIfNotEmpty(attrs, "ects", subProgram.getEcts()); 410 411 if (parentPath != null) 412 { 413 attrs.addCDATAAttribute("path", parentPath + "/" + subProgram.getName()); 414 } 415 XMLUtils.startElement(contentHandler, "subprogram", attrs); 416 417 try 418 { 419 // SAX the "parcours" view of a subprogram 420 saxContent(subProgram, contentHandler, "parcours", "xml", false, true); 421 } 422 catch (IOException e) 423 { 424 throw new SAXException(e); 425 } 426 427 XMLUtils.endElement(contentHandler, "subprogram"); 428 } 429 430 /** 431 * SAX a course list 432 * @param courseList The course list to SAX 433 * @param parentPath the parent path 434 * @param defaultLocale The default locale 435 * @throws SAXException if an error occurs 436 * @throws ProcessingException if an error occurs 437 */ 438 protected void saxCourseList(CourseList courseList, String parentPath, Locale defaultLocale) throws SAXException, ProcessingException 439 { 440 AttributesImpl attrs = new AttributesImpl(); 441 attrs.addCDATAAttribute("title", courseList.getTitle()); 442 attrs.addCDATAAttribute("id", courseList.getId()); 443 _addAttrIfNotEmpty(attrs, "code", courseList.getCode()); 444 445 ChoiceType type = courseList.getType(); 446 if (type != null) 447 { 448 attrs.addCDATAAttribute("type", type.toString()); 449 } 450 451 if (ChoiceType.CHOICE.equals(type)) 452 { 453 attrs.addCDATAAttribute("min", String.valueOf(courseList.getMinNumberOfCourses())); 454 attrs.addCDATAAttribute("max", String.valueOf(courseList.getMaxNumberOfCourses())); 455 } 456 457 XMLUtils.startElement(contentHandler, "courseList", attrs); 458 459 for (Course course : courseList.getCourses()) 460 { 461 try 462 { 463 saxCourse(course, parentPath, defaultLocale); 464 } 465 catch (UnknownAmetysObjectException e) 466 { 467 // The content can reference a non-existing course (in live for example): ignore the exception. 468 } 469 } 470 XMLUtils.endElement(contentHandler, "courseList"); 471 } 472 473 /** 474 * SAX a course 475 * @param course the course to SAX 476 * @param parentPath the parent path 477 * @param defaultLocale The default locale 478 * @throws SAXException if an error occurs 479 * @throws ProcessingException if an error occurs 480 */ 481 protected void saxCourse(Course course, String parentPath, Locale defaultLocale) throws SAXException, ProcessingException 482 { 483 AttributesImpl attrs = new AttributesImpl(); 484 attrs.addCDATAAttribute("title", course.getTitle()); 485 attrs.addCDATAAttribute("code", course.getCode()); 486 attrs.addCDATAAttribute("id", course.getId()); 487 attrs.addCDATAAttribute("name", course.getName()); 488 489 if (parentPath != null) 490 { 491 attrs.addCDATAAttribute("path", parentPath + "/" + course.getName() + "-" + course.getCode()); 492 } 493 494 View view = _cTypesHelper.getView("courseList", course.getTypes(), course.getMixinTypes()); 495 496 XMLUtils.startElement(contentHandler, "course", attrs); 497 498 if (view != null) 499 { 500 XMLUtils.startElement(contentHandler, "metadata"); 501 course.dataToSAX(contentHandler, view, DataContext.newInstance().withEmptyValues(false)); 502 XMLUtils.endElement(contentHandler, "metadata"); 503 } 504 505 // Course list 506 for (CourseList childCl : course.getCourseLists()) 507 { 508 saxCourseList(childCl, parentPath, defaultLocale); 509 } 510 511 // Course parts 512 for (CoursePart coursePart : course.getCourseParts()) 513 { 514 saxCoursePart(coursePart, defaultLocale); 515 } 516 517 XMLUtils.endElement(contentHandler, "course"); 518 } 519 520 /** 521 * SAX the HTML content of a {@link Content} 522 * @param content the content 523 * @param handler the {@link ContentHandler} to send SAX events to. 524 * @param viewName the view name 525 * @throws SAXException If an error occurred saxing the content 526 * @throws IOException If an error occurred resolving the content 527 */ 528 protected void saxContent(Content content, ContentHandler handler, String viewName) throws SAXException, IOException 529 { 530 String format = parameters.getParameter("output-format", "html"); 531 if (StringUtils.isEmpty(format)) 532 { 533 format = "html"; 534 } 535 536 saxContent(content, handler, viewName, format, true, false); 537 } 538 539 /** 540 * SAX a {@link Content} to given format 541 * @param content the content 542 * @param handler the {@link ContentHandler} to send SAX events to. 543 * @param viewName the view name 544 * @param format the output format 545 * @param withContentRoot true to wrap content stream into a root content tag 546 * @param ignoreChildren true to not SAX sub contents 547 * @throws SAXException If an error occurred saxing the content 548 * @throws IOException If an error occurred resolving the content 549 */ 550 protected void saxContent(Content content, ContentHandler handler, String viewName, String format, boolean withContentRoot, boolean ignoreChildren) throws SAXException, IOException 551 { 552 Triple<String, String, String> cacheKey = Triple.of(content.getId(), viewName, format); 553 SaxBuffer buffer = _cache.get(cacheKey); 554 555 if (buffer != null) 556 { 557 buffer.toSAX(handler); 558 return; 559 } 560 561 buffer = new SaxBuffer(); 562 563 Request request = ObjectModelHelper.getRequest(objectModel); 564 565 SitemapSource src = null; 566 try 567 { 568 Map<String, String> params = new HashMap<>(); 569 if (ignoreChildren) 570 { 571 params.put("ignoreChildren", "true"); 572 } 573 574 if (request.getAttribute(ODFHelper.REQUEST_ATTRIBUTE_VALID_LABEL) != null) 575 { 576 params.put("versionLabel", CmsConstants.LIVE_LABEL); 577 } 578 579 String uri = _contentHelper.getContentHtmlViewUrl(content, viewName, params); 580 src = (SitemapSource) _srcResolver.resolveURI(uri); 581 582 if (withContentRoot) 583 { 584 AttributesImpl attrs = new AttributesImpl(); 585 attrs.addCDATAAttribute("id", content.getId()); 586 attrs.addCDATAAttribute("name", content.getName()); 587 attrs.addCDATAAttribute("title", content.getTitle()); 588 attrs.addCDATAAttribute("lastModifiedAt", DateUtils.zonedDateTimeToString(content.getLastModified())); 589 590 XMLUtils.startElement(buffer, "content", attrs); 591 } 592 593 src.toSAX(new IgnoreRootHandler(buffer)); 594 595 if (withContentRoot) 596 { 597 XMLUtils.endElement(buffer, "content"); 598 } 599 600 _cache.put(cacheKey, buffer); 601 buffer.toSAX(handler); 602 } 603 catch (UnknownAmetysObjectException e) 604 { 605 // The content may be archived 606 } 607 finally 608 { 609 _srcResolver.release(src); 610 } 611 } 612 613 /** 614 * Add an attribute if its not null or empty. 615 * @param attrs The attributes 616 * @param attrName The attribute name 617 * @param attrValue The attribute value 618 */ 619 protected void _addAttrIfNotEmpty(AttributesImpl attrs, String attrName, String attrValue) 620 { 621 if (StringUtils.isNotEmpty(attrValue)) 622 { 623 attrs.addCDATAAttribute(attrName, attrValue); 624 } 625 } 626 627 /** 628 * SAX a course part 629 * @param coursePart The course part to SAX 630 * @param defaultLocale The default locale 631 * @throws SAXException if an error occurs 632 * @throws ProcessingException if an error occurs 633 */ 634 protected void saxCoursePart(CoursePart coursePart, Locale defaultLocale) throws SAXException, ProcessingException 635 { 636 AttributesImpl attrs = new AttributesImpl(); 637 attrs.addCDATAAttribute("id", coursePart.getId()); 638 attrs.addCDATAAttribute("title", coursePart.getTitle()); 639 _addAttrIfNotEmpty(attrs, "code", coursePart.getCode()); 640 641 XMLUtils.startElement(contentHandler, "coursePart", attrs); 642 View view = _cTypesHelper.getView("sax", coursePart.getTypes(), coursePart.getMixinTypes()); 643 coursePart.dataToSAX(contentHandler, view, DataContext.newInstance().withEmptyValues(false).withLocale(new Locale(coursePart.getLanguage()))); 644 XMLUtils.endElement(contentHandler, "coursePart"); 645 } 646}