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