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