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