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.CmsConstants; 044import org.ametys.cms.contenttype.ContentAttributeDefinition; 045import org.ametys.cms.contenttype.ContentType; 046import org.ametys.cms.data.ContentDataHelper; 047import org.ametys.cms.data.ContentValue; 048import org.ametys.cms.repository.Content; 049import org.ametys.core.cache.AbstractCacheManager; 050import org.ametys.core.cache.Cache; 051import org.ametys.core.util.DateUtils; 052import org.ametys.core.util.IgnoreRootHandler; 053import org.ametys.odf.NoLiveVersionException; 054import org.ametys.odf.ODFHelper; 055import org.ametys.odf.course.Course; 056import org.ametys.odf.courselist.CourseList; 057import org.ametys.odf.courselist.CourseList.ChoiceType; 058import org.ametys.odf.coursepart.CoursePart; 059import org.ametys.odf.orgunit.OrgUnit; 060import org.ametys.odf.orgunit.OrgUnitFactory; 061import org.ametys.odf.person.Person; 062import org.ametys.odf.person.PersonFactory; 063import org.ametys.odf.program.AbstractProgram; 064import org.ametys.odf.program.Container; 065import org.ametys.odf.program.Program; 066import org.ametys.odf.program.ProgramPart; 067import org.ametys.odf.program.SubProgram; 068import org.ametys.odf.program.TraversableProgramPart; 069import org.ametys.odf.translation.TranslationHelper; 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 ProcessingException if an error occurs 431 */ 432 protected void saxCourseList(CourseList courseList, String parentPath, Locale defaultLocale) throws SAXException, ProcessingException 433 { 434 AttributesImpl attrs = new AttributesImpl(); 435 attrs.addCDATAAttribute("title", courseList.getTitle()); 436 attrs.addCDATAAttribute("id", courseList.getId()); 437 _addAttrIfNotEmpty(attrs, "code", courseList.getCode()); 438 439 ChoiceType type = courseList.getType(); 440 if (type != null) 441 { 442 attrs.addCDATAAttribute("type", type.toString()); 443 } 444 445 if (ChoiceType.CHOICE.equals(type)) 446 { 447 attrs.addCDATAAttribute("min", String.valueOf(courseList.getMinNumberOfCourses())); 448 attrs.addCDATAAttribute("max", String.valueOf(courseList.getMaxNumberOfCourses())); 449 } 450 451 XMLUtils.startElement(contentHandler, "courseList", attrs); 452 453 for (Course course : courseList.getCourses()) 454 { 455 try 456 { 457 saxCourse(course, parentPath, defaultLocale); 458 } 459 catch (UnknownAmetysObjectException e) 460 { 461 // The content can reference a non-existing course (in live for example): ignore the exception. 462 } 463 } 464 XMLUtils.endElement(contentHandler, "courseList"); 465 } 466 467 /** 468 * SAX a course 469 * @param course the course to SAX 470 * @param parentPath the parent path 471 * @param defaultLocale The default locale 472 * @throws SAXException if an error occurs 473 * @throws ProcessingException if an error occurs 474 */ 475 protected void saxCourse(Course course, String parentPath, Locale defaultLocale) throws SAXException, ProcessingException 476 { 477 AttributesImpl attrs = new AttributesImpl(); 478 attrs.addCDATAAttribute("title", course.getTitle()); 479 attrs.addCDATAAttribute("code", course.getCode()); 480 attrs.addCDATAAttribute("id", course.getId()); 481 attrs.addCDATAAttribute("name", course.getName()); 482 483 if (parentPath != null) 484 { 485 attrs.addCDATAAttribute("path", parentPath + "/" + course.getName() + "-" + course.getCode()); 486 } 487 488 View view = _cTypesHelper.getView("courseList", course.getTypes(), course.getMixinTypes()); 489 490 XMLUtils.startElement(contentHandler, "course", attrs); 491 492 if (view != null) 493 { 494 XMLUtils.startElement(contentHandler, "metadata"); 495 course.dataToSAX(contentHandler, view, DataContext.newInstance().withEmptyValues(false)); 496 XMLUtils.endElement(contentHandler, "metadata"); 497 } 498 499 // Course list 500 for (CourseList childCl : course.getCourseLists()) 501 { 502 saxCourseList(childCl, parentPath, defaultLocale); 503 } 504 505 // Course parts 506 for (CoursePart coursePart : course.getCourseParts()) 507 { 508 saxCoursePart(coursePart, defaultLocale); 509 } 510 511 XMLUtils.endElement(contentHandler, "course"); 512 } 513 514 /** 515 * SAX the HTML content of a {@link Content} 516 * @param content the content 517 * @param handler the {@link ContentHandler} to send SAX events to. 518 * @param viewName the view name 519 * @throws SAXException If an error occurred saxing the content 520 * @throws IOException If an error occurred resolving the content 521 */ 522 protected void saxContent(Content content, ContentHandler handler, String viewName) throws SAXException, IOException 523 { 524 String format = parameters.getParameter("output-format", "html"); 525 if (StringUtils.isEmpty(format)) 526 { 527 format = "html"; 528 } 529 530 saxContent(content, handler, viewName, format, true, false); 531 } 532 533 /** 534 * SAX a {@link Content} to given format 535 * @param content the content 536 * @param handler the {@link ContentHandler} to send SAX events to. 537 * @param viewName the view name 538 * @param format the output format 539 * @param withContentRoot true to wrap content stream into a root content tag 540 * @param ignoreChildren true to not SAX sub contents 541 * @throws SAXException If an error occurred saxing the content 542 * @throws IOException If an error occurred resolving the content 543 */ 544 protected void saxContent(Content content, ContentHandler handler, String viewName, String format, boolean withContentRoot, boolean ignoreChildren) throws SAXException, IOException 545 { 546 Triple<String, String, String> cacheKey = Triple.of(content.getId(), viewName, format); 547 SaxBuffer buffer = _cache.get(cacheKey); 548 549 if (buffer != null) 550 { 551 buffer.toSAX(handler); 552 return; 553 } 554 555 buffer = new SaxBuffer(); 556 557 Request request = ObjectModelHelper.getRequest(objectModel); 558 559 SitemapSource src = null; 560 try 561 { 562 String uri = "cocoon://_content." + format + "?contentId=" + content.getId() + "&viewName=" + viewName + "&output-format=" + format; 563 if (ignoreChildren) 564 { 565 uri += "&ignoreChildren=true"; 566 } 567 568 if (request.getAttribute(ODFHelper.REQUEST_ATTRIBUTE_VALID_LABEL) != null) 569 { 570 uri += "&versionLabel=" + CmsConstants.LIVE_LABEL; 571 } 572 573 src = (SitemapSource) _srcResolver.resolveURI(uri); 574 575 if (withContentRoot) 576 { 577 AttributesImpl attrs = new AttributesImpl(); 578 attrs.addCDATAAttribute("id", content.getId()); 579 attrs.addCDATAAttribute("name", content.getName()); 580 attrs.addCDATAAttribute("title", content.getTitle()); 581 attrs.addCDATAAttribute("lastModifiedAt", DateUtils.dateToString(content.getLastModified())); 582 583 XMLUtils.startElement(buffer, "content", attrs); 584 } 585 586 src.toSAX(new IgnoreRootHandler(buffer)); 587 588 if (withContentRoot) 589 { 590 XMLUtils.endElement(buffer, "content"); 591 } 592 593 _cache.put(cacheKey, buffer); 594 buffer.toSAX(handler); 595 } 596 catch (UnknownAmetysObjectException e) 597 { 598 // The content may be archived 599 } 600 finally 601 { 602 _srcResolver.release(src); 603 } 604 } 605 606 /** 607 * Add an attribute if its not null or empty. 608 * @param attrs The attributes 609 * @param attrName The attribute name 610 * @param attrValue The attribute value 611 */ 612 protected void _addAttrIfNotEmpty(AttributesImpl attrs, String attrName, String attrValue) 613 { 614 if (StringUtils.isNotEmpty(attrValue)) 615 { 616 attrs.addCDATAAttribute(attrName, attrValue); 617 } 618 } 619 620 /** 621 * SAX a course part 622 * @param coursePart The course part to SAX 623 * @param defaultLocale The default locale 624 * @throws SAXException if an error occurs 625 * @throws ProcessingException if an error occurs 626 */ 627 protected void saxCoursePart(CoursePart coursePart, Locale defaultLocale) throws SAXException, ProcessingException 628 { 629 AttributesImpl attrs = new AttributesImpl(); 630 attrs.addCDATAAttribute("id", coursePart.getId()); 631 attrs.addCDATAAttribute("title", coursePart.getTitle()); 632 _addAttrIfNotEmpty(attrs, "code", coursePart.getCode()); 633 634 XMLUtils.startElement(contentHandler, "coursePart", attrs); 635 View view = _cTypesHelper.getView("sax", coursePart.getTypes(), coursePart.getMixinTypes()); 636 coursePart.dataToSAX(contentHandler, view, DataContext.newInstance().withEmptyValues(false).withLocale(new Locale(coursePart.getLanguage()))); 637 XMLUtils.endElement(contentHandler, "coursePart"); 638 } 639}