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.cms.content; 017 018import java.io.IOException; 019import java.text.DateFormat; 020import java.text.SimpleDateFormat; 021import java.time.LocalDate; 022import java.time.format.DateTimeFormatter; 023import java.util.Collections; 024import java.util.Comparator; 025import java.util.Date; 026import java.util.List; 027import java.util.Locale; 028 029import javax.jcr.RepositoryException; 030 031import org.apache.avalon.framework.service.ServiceException; 032import org.apache.avalon.framework.service.ServiceManager; 033import org.apache.cocoon.ProcessingException; 034import org.apache.cocoon.environment.ObjectModelHelper; 035import org.apache.cocoon.environment.Request; 036import org.apache.cocoon.generation.Generator; 037import org.apache.cocoon.generation.ServiceableGenerator; 038import org.apache.cocoon.i18n.I18nUtils; 039import org.apache.cocoon.xml.AttributesImpl; 040import org.apache.cocoon.xml.XMLUtils; 041import org.apache.commons.lang.ArrayUtils; 042import org.apache.commons.lang.StringUtils; 043import org.xml.sax.SAXException; 044 045import org.ametys.cms.contenttype.ContentType; 046import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 047import org.ametys.cms.contenttype.ContentTypesHelper; 048import org.ametys.cms.contenttype.MetadataManager; 049import org.ametys.cms.contenttype.MetadataSet; 050import org.ametys.cms.languages.Language; 051import org.ametys.cms.languages.LanguagesManager; 052import org.ametys.cms.repository.Content; 053import org.ametys.cms.repository.WorkflowAwareContent; 054import org.ametys.cms.repository.comment.Comment; 055import org.ametys.cms.repository.comment.CommentableContent; 056import org.ametys.core.user.UserIdentity; 057import org.ametys.core.util.DateUtils; 058import org.ametys.plugins.repository.AmetysRepositoryException; 059import org.ametys.plugins.repository.dublincore.DublinCoreAwareAmetysObject; 060import org.ametys.plugins.repository.jcr.JCRAmetysObject; 061import org.ametys.plugins.repository.version.VersionAwareAmetysObject; 062import org.ametys.plugins.workflow.store.AmetysStep; 063import org.ametys.plugins.workflow.support.WorkflowHelper; 064import org.ametys.plugins.workflow.support.WorkflowProvider; 065import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow; 066import org.ametys.runtime.i18n.I18nizableText; 067import org.ametys.runtime.parameter.ParameterHelper; 068 069import com.opensymphony.workflow.WorkflowException; 070import com.opensymphony.workflow.spi.Step; 071 072/** 073 * {@link Generator} for rendering raw content data. 074 */ 075public class ContentGenerator extends ServiceableGenerator 076{ 077 /** The display date format. */ 078 protected static final DateFormat _DC_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); 079 080 /** Content type extension point. */ 081 protected ContentTypeExtensionPoint _contentTypeExtensionPoint; 082 /** Metadata manager. */ 083 protected MetadataManager _metadataManager; 084 /** The language manager */ 085 protected LanguagesManager _languageManager; 086 /** The workflow provider */ 087 protected WorkflowProvider _workflowProvider; 088 /** The workflow helper */ 089 protected WorkflowHelper _worklflowHelper; 090 /** Helper for content types */ 091 protected ContentTypesHelper _cTypesHelper; 092 /** The content helper */ 093 protected ContentHelper _contentHelper; 094 095 @Override 096 public void service(ServiceManager serviceManager) throws ServiceException 097 { 098 super.service(serviceManager); 099 _contentHelper = (ContentHelper) serviceManager.lookup(ContentHelper.ROLE); 100 _contentTypeExtensionPoint = (ContentTypeExtensionPoint) serviceManager.lookup(ContentTypeExtensionPoint.ROLE); 101 _metadataManager = (MetadataManager) serviceManager.lookup(MetadataManager.ROLE); 102 _languageManager = (LanguagesManager) serviceManager.lookup(LanguagesManager.ROLE); 103 _workflowProvider = (WorkflowProvider) serviceManager.lookup(WorkflowProvider.ROLE); 104 _worklflowHelper = (WorkflowHelper) serviceManager.lookup(WorkflowHelper.ROLE); 105 _cTypesHelper = (ContentTypesHelper) serviceManager.lookup(ContentTypesHelper.ROLE); 106 } 107 108 public void generate() throws IOException, SAXException, ProcessingException 109 { 110 contentHandler.startDocument(); 111 _generateContent(); 112 contentHandler.endDocument(); 113 } 114 115 /** 116 * Generate the content (with the start/end document) 117 * @throws SAXException if an error occurs while SAXing 118 * @throws IOException if an error occurs 119 * @throws ProcessingException if an error occurs 120 */ 121 protected void _generateContent() throws SAXException, IOException, ProcessingException 122 { 123 Request request = ObjectModelHelper.getRequest(objectModel); 124 Content content = (Content) request.getAttribute(Content.class.getName()); 125 126 // SAX the content 127 _saxContent(content, getDefaultLocale(request)); 128 } 129 130 /** 131 * Get the default locale to use to sax localized values 132 * @param request the request 133 * @return the default locale 134 */ 135 protected Locale getDefaultLocale(Request request) 136 { 137 String lang = parameters.getParameter("lang", request.getParameter("lang")); 138 return StringUtils.isNotEmpty(lang) ? new Locale(lang) : I18nUtils.findLocale(objectModel, "locale", null, Locale.getDefault(), true); 139 } 140 141 /** 142 * SAX the content 143 * @param content The content to SAX 144 * @param defaultLocale The default locale to use to sax localized values if the content's language is null. 145 * @throws SAXException if an error occurs while SAXing 146 * @throws IOException if an error occurs 147 * @throws ProcessingException if an error occurs 148 */ 149 protected void _saxContent (Content content, Locale defaultLocale) throws SAXException, IOException, ProcessingException 150 { 151 AttributesImpl attrs = new AttributesImpl(); 152 attrs.addCDATAAttribute("id", content.getId()); 153 if (content instanceof JCRAmetysObject) 154 { 155 try 156 { 157 attrs.addCDATAAttribute("uuid", ((JCRAmetysObject) content).getNode().getIdentifier()); 158 } 159 catch (RepositoryException e) 160 { 161 throw new ProcessingException("Unable to get jcr UUID for content '" + content.getId() + "'", e); 162 } 163 } 164 attrs.addCDATAAttribute("id", content.getId()); 165 attrs.addCDATAAttribute("name", content.getName()); 166 attrs.addCDATAAttribute("title", content.getTitle(defaultLocale)); 167 if (content.getLanguage() != null) 168 { 169 attrs.addCDATAAttribute("language", content.getLanguage()); 170 } 171 attrs.addCDATAAttribute("createdAt", ParameterHelper.valueToString(content.getCreationDate())); 172 attrs.addCDATAAttribute("creator", UserIdentity.userIdentityToString(content.getCreator())); 173 attrs.addCDATAAttribute("lastModifiedAt", ParameterHelper.valueToString(content.getLastModified())); 174 Date lastValidatedAt = content.getLastValidationDate(); 175 if (lastValidatedAt != null) 176 { 177 attrs.addCDATAAttribute("lastValidatedAt", ParameterHelper.valueToString(lastValidatedAt)); 178 } 179 attrs.addCDATAAttribute("lastContributor", UserIdentity.userIdentityToString(content.getLastContributor())); 180 attrs.addCDATAAttribute("commentable", Boolean.toString(content instanceof CommentableContent)); 181 182 _addAttributeIfNotNull (attrs, "iconGlyph", _cTypesHelper.getIconGlyph(content)); 183 _addAttributeIfNotNull (attrs, "iconDecorator", _cTypesHelper.getIconDecorator(content)); 184 185 _addAttributeIfNotNull (attrs, "smallIcon", _cTypesHelper.getSmallIcon(content)); 186 _addAttributeIfNotNull (attrs, "mediumIcon", _cTypesHelper.getMediumIcon(content)); 187 _addAttributeIfNotNull (attrs, "largeIcon", _cTypesHelper.getLargeIcon(content)); 188 189 XMLUtils.startElement(contentHandler, "content", attrs); 190 191 MetadataSet metadataSet = _getMetadataSet(content); 192 XMLUtils.startElement(contentHandler, "metadata"); 193 _saxMetadata(content, metadataSet, defaultLocale); 194 XMLUtils.endElement(contentHandler, "metadata"); 195 196 if (metadataSet.isEdition()) 197 { 198 XMLUtils.startElement(contentHandler, "comments"); 199 _saxMetadataComments(content, metadataSet, defaultLocale); 200 XMLUtils.endElement(contentHandler, "comments"); 201 } 202 203 String[] cTypes = (String[]) ArrayUtils.addAll(content.getTypes(), content.getMixinTypes()); 204 for (String cTypeId : cTypes) 205 { 206 ContentType cType = _contentTypeExtensionPoint.getExtension(cTypeId); 207 cType.saxContentTypeAdditionalData(contentHandler, content); 208 } 209 210 // FIXME CMS-3057 211 Request request = ObjectModelHelper.getRequest(objectModel); 212 boolean displayWorkflow = !"true".equals(request.getParameter("ignore-workflow")); 213 214 if (displayWorkflow) 215 { 216 _saxWorkflowStep(content); 217 } 218 219 _saxLanguage (content); 220 221 _saxDublinCoreMetadata(content); 222 223 _saxContentComments(content); 224 225 _saxOtherData(content); 226 227 XMLUtils.endElement(contentHandler, "content"); 228 } 229 230 /** 231 * Add attribute if value is not null 232 * @param attrs The attributes 233 * @param name The name of attribute 234 * @param value The value 235 */ 236 protected void _addAttributeIfNotNull (AttributesImpl attrs, String name, String value) 237 { 238 if (value != null) 239 { 240 attrs.addCDATAAttribute(name, value); 241 } 242 } 243 244 /** 245 * SAX the comments of the content 246 * @param content The content to consider. Cannot be null. 247 * @throws SAXException if an error occurs while SAXing. 248 */ 249 protected void _saxContentComments(Content content) throws SAXException 250 { 251 if (content instanceof CommentableContent) 252 { 253 CommentableContent cContent = (CommentableContent) content; 254 255 List<Comment> comments = cContent.getComments(false, true); 256 if (comments.size() > 0) 257 { 258 XMLUtils.startElement(contentHandler, "comments"); 259 260 for (Comment comment : comments) 261 { 262 AttributesImpl attrs = new AttributesImpl(); 263 264 attrs.addCDATAAttribute("id", comment.getId()); 265 266 attrs.addCDATAAttribute("creation-date", ParameterHelper.valueToString(comment.getCreationDate())); 267 268 if (!StringUtils.isBlank(comment.getAuthorName())) 269 { 270 attrs.addCDATAAttribute("author-name", comment.getAuthorName()); 271 } 272 273 if (!comment.isEmailHidden() && !StringUtils.isBlank(comment.getAuthorEmail())) 274 { 275 attrs.addCDATAAttribute("author-email", comment.getAuthorEmail()); 276 } 277 278 if (!StringUtils.isBlank(comment.getAuthorURL())) 279 { 280 attrs.addCDATAAttribute("author-url", comment.getAuthorURL()); 281 } 282 283 XMLUtils.startElement(contentHandler, "comment", attrs); 284 285 if (comment.getContent() != null) 286 { 287 String[] contents = comment.getContent().split("\r?\n"); 288 for (String c : contents) 289 { 290 XMLUtils.createElement(contentHandler, "p", c); 291 } 292 } 293 294 XMLUtils.endElement(contentHandler, "comment"); 295 } 296 297 XMLUtils.endElement(contentHandler, "comments"); 298 } 299 } 300 } 301 302 /** 303 * SAX the content language 304 * @param content The content 305 * @throws SAXException if an error occurs while SAXing. 306 */ 307 protected void _saxLanguage (Content content) throws SAXException 308 { 309 String code = content.getLanguage(); 310 if (code != null) 311 { 312 Language language = _languageManager.getLanguage(code); 313 314 AttributesImpl atts = new AttributesImpl(); 315 atts.addCDATAAttribute("code", code); 316 317 if (language != null) 318 { 319 atts.addCDATAAttribute("icon-small", language.getSmallIcon()); 320 atts.addCDATAAttribute("icon-medium", language.getMediumIcon()); 321 atts.addCDATAAttribute("icon-large", language.getLargeIcon()); 322 } 323 324 XMLUtils.startElement(contentHandler, "content-language", atts); 325 if (language != null) 326 { 327 language.getLabel().toSAX(contentHandler); 328 } 329 XMLUtils.endElement(contentHandler, "content-language"); 330 } 331 } 332 333 /** 334 * SAX the workflow step if the content is a <code>WorkflowAwareContent</code> 335 * @param content The content 336 * @throws SAXException if an error occurs while SAXing. 337 */ 338 protected void _saxWorkflowStep (Content content) throws SAXException 339 { 340 if (content instanceof WorkflowAwareContent) 341 { 342 WorkflowAwareContent waContent = (WorkflowAwareContent) content; 343 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent); 344 345 try 346 { 347 long workflowId = waContent.getWorkflowId(); 348 String workflowName = workflow.getWorkflowName(workflowId); 349 350 Step currentStep = _getCurrentStep(waContent, workflow); 351 352 int currentStepId = currentStep.getStepId(); 353 354 I18nizableText workflowStepName = new I18nizableText("application", _worklflowHelper.getStepName(workflowName, currentStepId)); 355 356 AttributesImpl atts = new AttributesImpl(); 357 atts.addAttribute("", "id", "id", "CDATA", String.valueOf(currentStepId)); 358 if ("application".equals(workflowStepName.getCatalogue())) 359 { 360 atts.addAttribute("", "icon-small", "icon-small", "CDATA", "/plugins/cms/resources_workflow/" + workflowStepName.getKey() + "-small.png"); 361 atts.addAttribute("", "icon-medium", "icon-medium", "CDATA", "/plugins/cms/resources_workflow/" + workflowStepName.getKey() + "-medium.png"); 362 atts.addAttribute("", "icon-large", "icon-large", "CDATA", "/plugins/cms/resources_workflow/" + workflowStepName.getKey() + "-large.png"); 363 } 364 else 365 { 366 String pluginName = workflowStepName.getCatalogue().substring("plugin.".length()); 367 atts.addAttribute("", "icon-small", "icon-small", "CDATA", "/plugins/" + pluginName + "/resources/img/workflow/" + workflowStepName.getKey() + "-small.png"); 368 atts.addAttribute("", "icon-medium", "icon-medium", "CDATA", "/plugins/" + pluginName + "/resources/img/workflow/" + workflowStepName.getKey() + "-medium.png"); 369 atts.addAttribute("", "icon-large", "icon-large", "CDATA", "/plugins/" + pluginName + "/resources/img/workflow/" + workflowStepName.getKey() + "-large.png"); 370 } 371 372 XMLUtils.startElement(contentHandler, "workflow-step", atts); 373 workflowStepName.toSAX(contentHandler); 374 XMLUtils.endElement(contentHandler, "workflow-step"); 375 } 376 catch (AmetysRepositoryException e) 377 { 378 // Current step id was not positioned 379 } 380 catch (WorkflowException e) 381 { 382 // Ignore, just don't SAX the workflow step. 383 } 384 } 385 } 386 387 /** 388 * SAX content metadata. 389 * @param content the content. 390 * @param metadataSet the metadata set. 391 * @param defaultLocale The default locale to use to sax localized values if the content's language is null. 392 * @throws SAXException if an error occurs while SAXing. 393 * @throws IOException if an error occurs. 394 * @throws ProcessingException if an error occurs. 395 */ 396 protected void _saxMetadata(Content content, MetadataSet metadataSet, Locale defaultLocale) throws SAXException, ProcessingException, IOException 397 { 398 _metadataManager.saxMetadata(contentHandler, content, metadataSet, defaultLocale); 399 } 400 401 /** 402 * SAX metadata comments. 403 * @param content the content. 404 * @param metadataSet the metadata set. 405 * @param defaultLocale The default locale to use to sax localized values if the content's language is null. 406 * @throws SAXException if an error occurs while SAXing. 407 * @throws IOException if an error occurs. 408 * @throws ProcessingException if an error occurs. 409 */ 410 protected void _saxMetadataComments(Content content, MetadataSet metadataSet, Locale defaultLocale) throws SAXException, ProcessingException, IOException 411 { 412 _metadataManager.saxMetadataComments(contentHandler, content, metadataSet, defaultLocale); 413 } 414 415 /** 416 * SAX content Dublin Core metadata. 417 * @param dcObject the Dublin Core object. 418 * @throws SAXException if an error occurs while SAXing. 419 */ 420 protected void _saxDublinCoreMetadata(DublinCoreAwareAmetysObject dcObject) throws SAXException 421 { 422 XMLUtils.startElement(contentHandler, "dublin-core-metadata"); 423 _saxIfNotNull("title", dcObject.getDCTitle()); 424 _saxIfNotNull("creator", dcObject.getDCCreator()); 425 _saxIfNotNull("subject", dcObject.getDCSubject()); 426 _saxIfNotNull("description", dcObject.getDCDescription()); 427 _saxIfNotNull("publisher", dcObject.getDCPublisher()); 428 _saxIfNotNull("contributor", dcObject.getDCContributor()); 429 _saxIfNotNull("date", dcObject.getDCDate()); 430 _saxIfNotNull("type", dcObject.getDCType()); 431 _saxIfNotNull("format", dcObject.getDCFormat()); 432 _saxIfNotNull("identifier", dcObject.getDCIdentifier()); 433 _saxIfNotNull("source", dcObject.getDCSource()); 434 _saxIfNotNull("language", dcObject.getDCLanguage()); 435 _saxIfNotNull("relation", dcObject.getDCRelation()); 436 _saxIfNotNull("coverage", dcObject.getDCCoverage()); 437 _saxIfNotNull("rights", dcObject.getDCRights()); 438 XMLUtils.endElement(contentHandler, "dublin-core-metadata"); 439 } 440 441 /** 442 * SAX string Dublin Core metadata. 443 * @param name the metadata name. 444 * @param value the metadata value. 445 * @throws SAXException if an error occurs while SAXing. 446 */ 447 protected void _saxIfNotNull(String name, String value) throws SAXException 448 { 449 if (value != null) 450 { 451 XMLUtils.createElement(contentHandler, name, value); 452 } 453 } 454 455 /** 456 * SAX string Dublin Core metadata. 457 * @param name the metadata name. 458 * @param values the metadata values. 459 * @throws SAXException if an error occurs while SAXing. 460 */ 461 protected void _saxIfNotNull(String name, String[] values) throws SAXException 462 { 463 if (values != null) 464 { 465 for (String value : values) 466 { 467 XMLUtils.createElement(contentHandler, name, value); 468 } 469 } 470 } 471 472 /** 473 * SAX date Dublin Core metadata. 474 * @param name the metadata name. 475 * @param value the metadata value. 476 * @throws SAXException if an error occurs while SAXing. 477 */ 478 protected void _saxIfNotNull(String name, Date value) throws SAXException 479 { 480 if (value != null) 481 { 482 LocalDate ld = DateUtils.asLocalDate(value); 483 XMLUtils.createElement(contentHandler, name, ld.format(DateTimeFormatter.ISO_LOCAL_DATE)); 484 } 485 } 486 487 /** 488 * SAX any other data needed by the view.<p> 489 * Default implementation does nothing. 490 * @param content the content. 491 * @throws SAXException if an error occurs while SAXing. 492 * @throws ProcessingException if an error occurs. 493 */ 494 protected void _saxOtherData(Content content) throws SAXException, ProcessingException 495 { 496 // No other data to SAX 497 } 498 499 /** 500 * Retrieves the metadata set to be used when SAX'ing metadata and metadata comments. 501 * @param content The content to consider. Cannot be null. 502 * @return The retrieved metadata set 503 * @throws ProcessingException If the metadata set could not be retrieved 504 */ 505 protected MetadataSet _getMetadataSet(Content content) throws ProcessingException 506 { 507 boolean isEditionMetadataSet = parameters.getParameterAsBoolean("isEditionMetadataSet", false); 508 String metadataSetName = parameters.getParameter("metadataSetName", ""); 509 if (StringUtils.isBlank(metadataSetName)) 510 { 511 metadataSetName = "main"; 512 } 513 514 MetadataSet metadataSet = null; 515 516 if (isEditionMetadataSet) 517 { 518 metadataSet = _cTypesHelper.getMetadataSetForEdition(metadataSetName, content.getTypes(), content.getMixinTypes()); 519 } 520 else 521 { 522 metadataSet = _cTypesHelper.getMetadataSetForView(metadataSetName, content.getTypes(), content.getMixinTypes()); 523 } 524 525 if (metadataSet == null) 526 { 527 throw new ProcessingException(String.format("Unknown metadata set '%s' of type '%s' for content type(s) '%s'", 528 metadataSetName, isEditionMetadataSet ? "edition" : "view", StringUtils.join(content.getTypes(), ','))); 529 } 530 531 return metadataSet; 532 } 533 534 /** 535 * Get a content's step, wherever it works on the base version or not. 536 * @param content the content. 537 * @param workflow The workflow impl to use 538 * @return the content's workflow step. 539 * @throws WorkflowException if an error occurs. 540 */ 541 protected Step _getCurrentStep(WorkflowAwareContent content, AmetysObjectWorkflow workflow) throws WorkflowException 542 { 543 long workflowId = content.getWorkflowId(); 544 545 Step currentStep = (Step) workflow.getCurrentSteps(workflowId).get(0); 546 547 if (content instanceof VersionAwareAmetysObject) 548 { 549 VersionAwareAmetysObject vaContent = (VersionAwareAmetysObject) content; 550 String currentRevision = vaContent.getRevision(); 551 552 if (currentRevision != null) 553 { 554 555 String[] allRevisions = vaContent.getAllRevisions(); 556 int currentRevIndex = ArrayUtils.indexOf(allRevisions, currentRevision); 557 558 if (currentRevIndex > -1 && currentRevIndex < (allRevisions.length - 1)) 559 { 560 String nextRevision = allRevisions[currentRevIndex + 1]; 561 562 Date currentRevTimestamp = vaContent.getRevisionTimestamp(); 563 Date nextRevTimestamp = vaContent.getRevisionTimestamp(nextRevision); 564 565 // Get all steps between the two revisions. 566 List<Step> steps = _worklflowHelper.getStepsBetween(workflow, workflowId, currentRevTimestamp, nextRevTimestamp); 567 568 // In the old workflow structure 569 // We take the second, which is current revision's last step. 570 if (steps.size() > 0 && steps.get(0) instanceof AmetysStep) 571 { 572 AmetysStep amStep = (AmetysStep) steps.get(0); 573 if (amStep.getProperty("actionFinishDate") != null) 574 { 575 // New workflow structure detected: cut the first workflow step 576 // in the list, as it belongs to the next version. 577 steps = steps.subList(1, steps.size()); 578 } 579 } 580 581 // Order by step descendant. 582 Collections.sort(steps, new Comparator<Step>() 583 { 584 public int compare(Step step1, Step step2) 585 { 586 return -new Long(step1.getId()).compareTo(step2.getId()); 587 } 588 }); 589 590 // The first step in the list is the current version's last workflow step. 591 if (steps.size() > 0) 592 { 593 currentStep = steps.get(0); 594 } 595 } 596 } 597 } 598 return currentStep; 599 } 600 601}