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