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