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