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