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