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.workflow; 017 018import java.io.IOException; 019import java.time.ZoneId; 020import java.util.ArrayList; 021import java.util.Collections; 022import java.util.Comparator; 023import java.util.Date; 024import java.util.HashMap; 025import java.util.HashSet; 026import java.util.Iterator; 027import java.util.List; 028import java.util.Map; 029import java.util.Set; 030 031import javax.jcr.Node; 032import javax.jcr.RepositoryException; 033import javax.jcr.Session; 034 035import org.apache.avalon.framework.service.ServiceException; 036import org.apache.avalon.framework.service.ServiceManager; 037import org.apache.cocoon.ProcessingException; 038import org.apache.cocoon.environment.ObjectModelHelper; 039import org.apache.cocoon.environment.Request; 040import org.apache.cocoon.generation.Generator; 041import org.apache.cocoon.generation.ServiceableGenerator; 042import org.apache.cocoon.xml.AttributesImpl; 043import org.apache.cocoon.xml.XMLUtils; 044import org.apache.commons.lang.StringUtils; 045import org.xml.sax.SAXException; 046 047import org.ametys.cms.repository.Content; 048import org.ametys.cms.repository.WorkflowAwareContent; 049import org.ametys.core.user.User; 050import org.ametys.core.user.UserIdentity; 051import org.ametys.core.user.UserManager; 052import org.ametys.core.util.DateUtils; 053import org.ametys.plugins.repository.AmetysObjectResolver; 054import org.ametys.plugins.repository.version.VersionAwareAmetysObject; 055import org.ametys.plugins.workflow.store.AbstractJackrabbitWorkflowStore; 056import org.ametys.plugins.workflow.store.AmetysStep; 057import org.ametys.plugins.workflow.support.WorkflowProvider; 058import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow; 059import org.ametys.runtime.i18n.I18nizableText; 060 061import com.opensymphony.workflow.loader.ActionDescriptor; 062import com.opensymphony.workflow.loader.StepDescriptor; 063import com.opensymphony.workflow.loader.WorkflowDescriptor; 064import com.opensymphony.workflow.spi.SimpleStep; 065import com.opensymphony.workflow.spi.Step; 066 067/** 068 * {@link Generator} for exposing content history and content versions. 069 */ 070public class HistoryGenerator extends ServiceableGenerator 071{ 072 073 private static final I18nizableText __MESSAGE_NO_STEP = new I18nizableText("plugin.cms", "WORKFLOW_UNKNOWN_STEP"); 074 075 private static final I18nizableText __MESSAGE_NO_ACTION = new I18nizableText("plugin.cms", "WORKFLOW_UNKNOWN_ACTION"); 076 077 private Map<UserIdentity, String> _identityToFullNameCache; 078 private WorkflowProvider _workflowProvider; 079 private UserManager _userManager; 080 private AmetysObjectResolver _resolver; 081 082 @Override 083 public void service(ServiceManager serviceManager) throws ServiceException 084 { 085 super.service(serviceManager); 086 _workflowProvider = (WorkflowProvider) serviceManager.lookup(WorkflowProvider.ROLE); 087 _userManager = (UserManager) serviceManager.lookup(UserManager.ROLE); 088 _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE); 089 } 090 091 @Override 092 public void generate() throws IOException, SAXException, ProcessingException 093 { 094 _identityToFullNameCache = new HashMap<>(); 095 096 Request request = ObjectModelHelper.getRequest(objectModel); 097 098 String id = request.getParameter("contentId"); 099 Content content = _resolver.resolveById(id); 100 101 assert content instanceof VersionAwareAmetysObject; 102 103 contentHandler.startDocument(); 104 XMLUtils.startElement(contentHandler, "History"); 105 106 try 107 { 108 List<VersionInformation> versionsInformation = _resolveVersionInformations((VersionAwareAmetysObject) content); 109 110 _saxVersionInformation(versionsInformation); 111 112 AttributesImpl attrs = new AttributesImpl(); 113 attrs.addCDATAAttribute("contentIsWorkflowAware", Boolean.toString(content instanceof WorkflowAwareContent)); 114 115 XMLUtils.startElement(contentHandler, "Workflow", attrs); 116 _saxContentWorkflowHistory(content, versionsInformation); 117 XMLUtils.endElement(contentHandler, "Workflow"); 118 } 119 catch (RepositoryException e) 120 { 121 throw new ProcessingException("Unable to access version history", e); 122 } 123 124 XMLUtils.endElement(contentHandler, "History"); 125 contentHandler.endDocument(); 126 } 127 128 private List<VersionInformation> _resolveVersionInformations(VersionAwareAmetysObject content) throws RepositoryException 129 { 130 List<VersionInformation> versionsInformation = new ArrayList<>(); 131 132 for (String revision : content.getAllRevisions()) 133 { 134 VersionInformation versionInformation = new VersionInformation(revision, content.getRevisionTimestamp(revision)); 135 136 for (String label : content.getLabels(revision)) 137 { 138 versionInformation.addLabel(label); 139 } 140 141 versionsInformation.add(versionInformation); 142 } 143 144 // Sort by date descendant 145 Collections.sort(versionsInformation, new Comparator<VersionInformation>() 146 { 147 public int compare(VersionInformation o1, VersionInformation o2) 148 { 149 try 150 { 151 return -o1.getCreatedAt().compareTo(o2.getCreatedAt()); 152 } 153 catch (RepositoryException e) 154 { 155 throw new RuntimeException("Unable to retrieve a creation date", e); 156 } 157 } 158 }); 159 160 // Set the version name 161 int count = versionsInformation.size(); 162 for (VersionInformation versionInformation : versionsInformation) 163 { 164 versionInformation.setVersionName(String.valueOf(count--)); 165 } 166 167 return versionsInformation; 168 } 169 170 private void _saxVersionInformation(List<VersionInformation> versionsInformation) throws SAXException, RepositoryException 171 { 172 XMLUtils.startElement(contentHandler, "Versions"); 173 174 for (VersionInformation info : versionsInformation) 175 { 176 AttributesImpl atts = new AttributesImpl(); 177 178 if (info.getVersionRawName() != null) 179 { 180 atts.addCDATAAttribute("rawName", info.getVersionRawName()); 181 } 182 183 if (info.getVersionName() != null) 184 { 185 atts.addCDATAAttribute("name", info.getVersionName()); 186 } 187 188 if (info.getCreatedAt() != null) 189 { 190 atts.addCDATAAttribute("createdAt", DateUtils.getISODateTimeFormatter().format(info.getCreatedAt().toInstant().atZone(ZoneId.systemDefault()))); 191 } 192 193 XMLUtils.createElement(contentHandler, "Version", atts); 194 } 195 196 XMLUtils.endElement(contentHandler, "Versions"); 197 } 198 199 @SuppressWarnings("unchecked") 200 private void _saxContentWorkflowHistory(Content content, List<VersionInformation> versionsInformation) throws SAXException, ProcessingException, RepositoryException 201 { 202 if (content instanceof WorkflowAwareContent) 203 { 204 WorkflowAwareContent workflowAwareContent = (WorkflowAwareContent) content; 205 206 long workflowId = workflowAwareContent.getWorkflowId(); 207 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(workflowAwareContent); 208 209 String workflowName = workflow.getWorkflowName(workflowId); 210 211 if (workflowName == null) 212 { 213 throw new ProcessingException("Unknown workflow name for workflow instance id: " + workflowId); 214 } 215 216 WorkflowDescriptor workflowDescriptor = workflow.getWorkflowDescriptor(workflowName); 217 218 if (workflowDescriptor == null) 219 { 220 throw new ProcessingException("No workflow description for workflow name: " + workflowName); 221 } 222 223 List<Step> allSteps = new ArrayList(workflow.getCurrentSteps(workflowId)); 224 allSteps.addAll(workflow.getHistorySteps(workflowId)); 225 226 // Sort by start date descendant (only relevant when there is no split and nor join!) 227 Collections.sort(allSteps, new Comparator<Step>() 228 { 229 public int compare(Step s1, Step s2) 230 { 231 return -s1.getStartDate().compareTo(s2.getStartDate()); 232 } 233 }); 234 235 Date startDate = null; 236 237 if (!allSteps.isEmpty()) 238 { 239 // Use start date of the first step for the start date 240 // of the unstored "real first step" 241 startDate = allSteps.get(allSteps.size() - 1).getStartDate(); 242 } 243 244 // Use a custom step for the unstored "real first step" 245 int initialActionId = (int) _getInitialActionId(workflow, workflowAwareContent); 246 allSteps.add(new SimpleStep(0, 0, 0, initialActionId, null, content.getCreationDate(), startDate, null, "", new long[0], UserIdentity.userIdentityToString(content.getCreator()))); 247 248 Iterator<Step> itStep = allSteps.iterator(); 249 // We got at least one 250 Step step = itStep.next(); 251 252 do 253 { 254 Step previousStep = null; 255 256 if (itStep.hasNext()) 257 { 258 previousStep = itStep.next(); 259 } 260 261 _saxStep(workflowDescriptor, versionsInformation, step, previousStep); 262 263 step = previousStep; 264 } while (itStep.hasNext()); 265 } 266 } 267 268 private long _getInitialActionId(AmetysObjectWorkflow workflow, WorkflowAwareContent waContent) 269 { 270 try 271 { 272 Session session = waContent.getNode().getSession(); 273 AbstractJackrabbitWorkflowStore workflowStore = (AbstractJackrabbitWorkflowStore) workflow.getConfiguration().getWorkflowStore(); 274 Node workflowEntryNode = workflowStore.getEntryNode(session, waContent.getWorkflowId()); 275 return workflowEntryNode.getProperty("ametys-internal:initialActionId").getLong(); 276 } 277 catch (Exception e) 278 { 279 getLogger().error("Unable to retrieves initial action id for workflow aware content : " + waContent.getId(), e); 280 return 0; 281 } 282 } 283 284 private void _saxStep(WorkflowDescriptor workflowDescriptor, List<VersionInformation> versionsInformation, Step step, Step previousStep) throws SAXException, RepositoryException 285 { 286 AttributesImpl stepAttrs = new AttributesImpl(); 287 288 // We want the caller of the action responsible for being in the step 289 String caller = previousStep != null ? previousStep.getCaller() : null; 290 int stepId = step.getStepId(); 291 // We want the id of the action responsible for being in the step 292 int actionId = previousStep != null ? previousStep.getActionId() : 0; 293 294 // Get the action start and finish dates (here, it acts as a marker to distinguish 295 // old and new workflow step structure). 296 Date actionStartDate = null; 297 Date actionFinishDate = null; 298 if (step instanceof AmetysStep) 299 { 300 actionStartDate = (Date) ((AmetysStep) step).getProperty("actionStartDate"); 301 actionFinishDate = (Date) ((AmetysStep) step).getProperty("actionFinishDate"); 302 } 303 304 Date startDate = null; 305 Date finishDate = null; 306 if (actionFinishDate != null) 307 { 308 // New format 309 startDate = step.getStartDate() != null ? step.getStartDate() : null; 310 finishDate = step.getFinishDate() != null ? step.getFinishDate() : null; 311 } 312 else 313 { 314 startDate = previousStep != null ? previousStep.getFinishDate() : null; 315 finishDate = step.getStartDate(); 316 } 317 318 if (stepId != 0) 319 { 320 stepAttrs.addCDATAAttribute("id", String.valueOf(stepId)); 321 } 322 323 if (step.getFinishDate() == null) 324 { 325 stepAttrs.addCDATAAttribute("current", Boolean.TRUE.toString()); 326 } 327 328 XMLUtils.startElement(contentHandler, "Step", stepAttrs); 329 330 _saxStepDescription(workflowDescriptor, stepId); 331 332 _saxCaller(UserIdentity.stringToUserIdentity(caller)); 333 334 _saxAction(actionId, workflowDescriptor); 335 336 _saxComments(previousStep); 337 338 _saxValidation(step, startDate, finishDate, actionStartDate, actionFinishDate); 339 340 List<VersionInformation> versionsInsideStep = _computeVersionsBetween(versionsInformation, startDate, finishDate); 341 342 for (VersionInformation versionInformation : versionsInsideStep) 343 { 344 AttributesImpl versionAttrs = new AttributesImpl(); 345 346 for (String label : versionInformation.getLabels()) 347 { 348 versionAttrs.addCDATAAttribute(label, label); 349 } 350 if (StringUtils.isNotEmpty(versionInformation.getVersionRawName())) 351 { 352 versionAttrs.addCDATAAttribute("name", versionInformation.getVersionRawName()); 353 } 354 XMLUtils.createElement(contentHandler, "Version", versionAttrs, versionInformation.getVersionName()); 355 } 356 357 XMLUtils.endElement(contentHandler, "Step"); 358 } 359 360 private void _saxStepDescription(WorkflowDescriptor workflowDescriptor, int stepId) throws SAXException 361 { 362 if (stepId != 0) 363 { 364 StepDescriptor stepDescriptor = workflowDescriptor.getStep(stepId); 365 366 I18nizableText workflowStepName = stepDescriptor == null ? __MESSAGE_NO_STEP : new I18nizableText("application", stepDescriptor.getName()); 367 368 workflowStepName.toSAX(contentHandler, "StepName"); 369 370 String[] icons = new String[]{"-small", "-medium", "-large"}; 371 for (String icon : icons) 372 { 373 if ("application".equals(workflowStepName.getCatalogue())) 374 { 375 XMLUtils.createElement(contentHandler, "StepIcon" + icon, "/plugins/cms/resources_workflow/" + workflowStepName.getKey() + icon + ".png"); 376 } 377 else 378 { 379 String pluginName = workflowStepName.getCatalogue().substring("plugin.".length()); 380 XMLUtils.createElement(contentHandler, "StepIcon" + icon, "/plugins/" + pluginName + "/resources/img/workflow/" + workflowStepName.getKey() + icon + ".png"); 381 } 382 } 383 } 384 else 385 { 386 XMLUtils.createElement(contentHandler, "StepName", "?"); 387 XMLUtils.createElement(contentHandler, "StepIcon-small", "/plugins/cms/resources/img/history/workflow/step_0_16.png"); 388 XMLUtils.createElement(contentHandler, "StepIcon-medium", "/plugins/cms/resources/img/history/workflow/step_0_32.png"); 389 XMLUtils.createElement(contentHandler, "StepIcon-large", "/plugins/cms/resources/img/history/workflow/step_0_48.png"); 390 } 391 } 392 393 private void _saxCaller(UserIdentity caller) throws SAXException 394 { 395 if (caller != null) 396 { 397 AttributesImpl authorAttrs = new AttributesImpl(); 398 authorAttrs.addCDATAAttribute("login", caller.getLogin()); 399 authorAttrs.addCDATAAttribute("population", caller.getPopulationId()); 400 XMLUtils.createElement(contentHandler, "Author", authorAttrs, _resolveFullName(caller)); 401 } 402 } 403 404 private void _saxAction(int actionId, WorkflowDescriptor workflowDescriptor) throws SAXException 405 { 406 if (actionId != 0) 407 { 408 ActionDescriptor actionDescriptor = workflowDescriptor.getAction(actionId); 409 410 I18nizableText actionLabel = actionDescriptor == null ? __MESSAGE_NO_ACTION : new I18nizableText("application", actionDescriptor.getName()); 411 412 XMLUtils.createElement(contentHandler, "ActionId", String.valueOf(actionId)); 413 actionLabel.toSAX(contentHandler, "ActionName"); 414 } 415 else 416 { 417 new I18nizableText("plugin.cms", "WORKFLOW_ACTION_CREATE").toSAX(contentHandler, "ActionName"); 418 } 419 } 420 421 private void _saxComments(Step previousStep) throws SAXException 422 { 423 if (previousStep != null && previousStep instanceof AmetysStep) 424 { 425 String comments = (String) ((AmetysStep) previousStep).getProperty("comment"); 426 if (comments != null) 427 { 428 XMLUtils.createElement(contentHandler, "Comment", comments.replaceAll("\r?\n", "<br/>")); 429 } 430 } 431 } 432 433 private void _saxValidation(Step step, Date startDate, Date finishDate, Date actionStartDate, Date actionFinishDate) throws SAXException 434 { 435 Boolean validation = false; 436 if (step instanceof AmetysStep) 437 { 438 validation = (Boolean) ((AmetysStep) step).getProperty("validation"); 439 } 440 boolean valid = validation == null ? false : validation; 441 XMLUtils.createElement(contentHandler, "Validation", String.valueOf(valid)); 442 443 if (startDate != null) 444 { 445 XMLUtils.createElement(contentHandler, "Start", DateUtils.dateToString(startDate)); 446 } 447 448 if (finishDate != null) 449 { 450 XMLUtils.createElement(contentHandler, "Finish", DateUtils.dateToString(finishDate)); 451 } 452 453 if (actionStartDate != null) 454 { 455 XMLUtils.createElement(contentHandler, "NewActionStart", DateUtils.dateToString(actionStartDate)); 456 } 457 458 if (actionFinishDate != null) 459 { 460 XMLUtils.createElement(contentHandler, "OldActionFinish", DateUtils.dateToString(actionFinishDate)); 461 } 462 463 } 464 465 private List<VersionInformation> _computeVersionsBetween(List<VersionInformation> versionsInformation, Date startDate, Date finishDate) throws RepositoryException 466 { 467 List<VersionInformation> versionsInsideStep = new ArrayList<>(); 468 469 for (VersionInformation versionInformation : versionsInformation) 470 { 471 Date versionCreationDate = versionInformation.getCreatedAt(); 472 473 if (startDate != null) 474 { 475 if (versionCreationDate.after(startDate)) 476 { 477 if (finishDate == null || versionCreationDate.before(finishDate)) 478 { 479 versionsInsideStep.add(versionInformation); 480 } 481 } 482 } 483 else 484 { 485 if (finishDate == null || versionCreationDate.before(finishDate)) 486 { 487 versionsInsideStep.add(versionInformation); 488 } 489 } 490 } 491 492 // If there is no version created inside the step, 493 // retrieve the last one just before the step 494 if (versionsInsideStep.isEmpty()) 495 { 496 VersionInformation lastVersionBeforeStep = null; 497 498 for (VersionInformation versionInformation : versionsInformation) 499 { 500 Date versionCreationDate = versionInformation.getCreatedAt(); 501 502 if (startDate != null && versionCreationDate.before(startDate)) 503 { 504 lastVersionBeforeStep = versionInformation; 505 break; 506 } 507 else if (startDate == null) 508 { 509 // Use the first version 510 lastVersionBeforeStep = versionInformation; 511 } 512 } 513 514 if (lastVersionBeforeStep != null) 515 { 516 versionsInsideStep.add(lastVersionBeforeStep); 517 } 518 } 519 520 // if there is still no version for this step, then it is version 1 (case of migrations) 521 if (versionsInsideStep.isEmpty()) 522 { 523 versionsInsideStep.add(new VersionInformation("1")); 524 } 525 526 return versionsInsideStep; 527 } 528 529 private String _resolveFullName(UserIdentity userIdentity) 530 { 531 String fullName = _identityToFullNameCache.get(userIdentity); 532 533 if (fullName == null) 534 { 535 User user = _userManager.getUser(userIdentity.getLogin(), userIdentity.getPopulationId()); 536 537 if (user != null) 538 { 539 fullName = user.getFullName(); 540 // Cache result 541 _identityToFullNameCache.put(userIdentity, fullName); 542 } 543 } 544 545 if (fullName == null) 546 { 547 // Not found 548 return ""; 549 } 550 else 551 { 552 return fullName; 553 } 554 } 555 556 @Override 557 public void recycle() 558 { 559 super.recycle(); 560 _identityToFullNameCache = null; 561 } 562 563 private static class VersionInformation 564 { 565 private String _rawName; 566 private String _name; 567 private Date _creationDate; 568 private Set<String> _labels = new HashSet<>(); 569 570 /** 571 * Creates a {@link VersionInformation}. 572 * @param rawName the revision name. 573 * @param creationDate the revision creation date. 574 * @throws RepositoryException if an error occurs. 575 */ 576 public VersionInformation(String rawName, Date creationDate)throws RepositoryException 577 { 578 _creationDate = creationDate; 579 _rawName = rawName; 580 } 581 582 /** 583 * Creates a named {@link VersionInformation}. 584 * @param name the name to use. 585 */ 586 public VersionInformation(String name) 587 { 588 _name = name; 589 } 590 591 /** 592 * Set the version name 593 * @param name The name 594 */ 595 public void setVersionName (String name) 596 { 597 _name = name; 598 } 599 600 /** 601 * Retrieves the version name. 602 * @return the version name. 603 */ 604 public String getVersionName() 605 { 606 return _name; 607 } 608 609 /** 610 * Retrieves the version raw name. 611 * @return the version raw name. 612 */ 613 public String getVersionRawName() 614 { 615 return _rawName; 616 } 617 618 /** 619 * Retrieves the creation date. 620 * @return the creation date. 621 * @throws RepositoryException if an error occurs. 622 */ 623 public Date getCreatedAt() throws RepositoryException 624 { 625 return _creationDate; 626 } 627 628 /** 629 * Retrieves the labels associated with this version. 630 * @return the labels. 631 */ 632 public Set<String> getLabels() 633 { 634 return _labels; 635 } 636 637 /** 638 * Add a label to this version. 639 * @param label the label. 640 */ 641 public void addLabel(String label) 642 { 643 _labels.add(label); 644 } 645 } 646}