package com.psddev.cms.tool.page; import com.google.common.collect.ImmutableMap; import com.psddev.cms.db.Content; import com.psddev.cms.db.Draft; import com.psddev.cms.db.Workflow; import com.psddev.cms.db.WorkflowLog; import com.psddev.cms.db.WorkflowState; import com.psddev.cms.db.WorkflowTransition; import com.psddev.cms.tool.PageServlet; import com.psddev.cms.tool.Search; import com.psddev.cms.tool.SearchResultSelection; import com.psddev.cms.tool.ToolPageContext; import com.psddev.dari.db.ObjectType; import com.psddev.dari.db.Query; import com.psddev.dari.db.State; import com.psddev.dari.util.ObjectUtils; import com.psddev.dari.util.RoutingFilter; import com.psddev.dari.util.StringUtils; import com.psddev.dari.util.TypeReference; import com.psddev.dari.util.UrlBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.jsp.PageContext; import java.io.IOException; import java.io.Writer; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; @RoutingFilter.Path(application = "cms", value = BulkWorkflow.PATH) public class BulkWorkflow extends PageServlet { public static final String PATH = "bulkWorkflow"; public static final String TARGET = "_top"; private static final Logger LOGGER = LoggerFactory.getLogger(BulkWorkflow.class); private static final String DEFAULT_ERROR_MESSAGE = "An error has occurred."; @Override protected String getPermissionId() { return null; } @Override protected void doService(ToolPageContext page) throws IOException, ServletException { execute(new Context(page)); } public void execute(ToolPageContext page, Search search, SearchResultSelection selection, WidgetState widgetState) throws IOException, ServletException { Context context = new Context(page, selection, search); context.setWidgetState(widgetState); execute(context); } private void execute(Context page) throws IOException, ServletException { if (WidgetState.BUTTON.equals(page.getWidgetState()) && !page.hasAnyTransitions()) { return; } switch (page.getWidgetState()) { // BUTTON state is used to display a button to invoke the bulk workflow detail page case BUTTON: page.writeStart("div", "class", "searchResult-action-simple"); page.writeStart("a", "class", "button", "target", TARGET, "href", getActionUrl(page, null, null, null, WidgetState.DETAIL)); page.writeHtml(page.localize( BulkWorkflow.class, ImmutableMap.of("extra", page.getSelection() != null ? "Selected" : ""), "action.bulkWorfklow")); page.writeEnd(); page.writeEnd(); break; // CONFIRM state is shown to the ToolUser before action is taken case CONFIRM: page.writeHeader(); writeConfirmStateHtml(page); page.writeFooter(); break; // DETAIL state shows all available bulk workflow transition options for the provided Search or SearchResultSelection. // Transitions for which the ToolUser does not have permission are not displayed. case DETAIL: default: page.writeHeader(); if (page.isFormPost()) { Search search = page.getSearch(); List<String> originalVisibilities = new ArrayList<>(); // Ensure that the Search object only returns results with the specified workflow state. // Store original visibilities to present a new DETAIL view after action is taken. if (search != null) { originalVisibilities.addAll(search.getVisibilities()); if (search.getVisibilities() != null) { search.getVisibilities().clear(); } else { search.setVisibilities(new ArrayList<>()); } search.getVisibilities().add("w." + page.param(String.class, Context.WORKFLOW_STATE_PARAMETER)); } Map<String, Integer> messageMap = new LinkedHashMap<>(); int successCount = 0; for (Object item : page.itemsQuery().selectAll()) { State itemState = State.getInstance(item); // Skip selected items or query results that aren't in the specified workflow state. if (!ObjectUtils.equals(page.param(String.class, Context.WORKFLOW_STATE_PARAMETER), itemState.as(Workflow.Data.class).getCurrentState())) { continue; } if (page.tryWorkflowOnly(item)) { successCount ++; } } // Build user notification String from errors' localized messages. // Stack repeat errors and track the counts of each type. if (page.getErrors().size() > 0) { for (Throwable throwable : page.getErrors()) { String message = throwable.getLocalizedMessage() != null ? throwable.getLocalizedMessage() : DEFAULT_ERROR_MESSAGE; int messageCount = ObjectUtils.to(int.class, messageMap.get(message)); messageMap.put(message, messageCount + 1); LOGGER.warn("Bulk Workflow Error: ", throwable); } } List<String> errorMessages = new ArrayList<>(); // Display error notifications. if (messageMap.size() > 0) { for (Map.Entry<String, Integer> entry : messageMap.entrySet()) { errorMessages.add(entry.getKey() + (entry.getValue() > 1 ? " (" + entry.getValue() + ")" : "")); } page.writeStart("div", "class", "message message-error"); page.writeHtml(StringUtils.join(errorMessages, "<br>")); page.writeEnd(); // end .message-error } // Display success notification. if (successCount > 0) { page.writeStart("div", "class", "message message-success"); page.writeHtml(page.localize( BulkWorkflow.class, ImmutableMap.of("count", successCount), "message.success")); String returnUrl = page.param(String.class, "returnUrl"); if (!ObjectUtils.isBlank(returnUrl)) { page.writeStart("a", "href", returnUrl); page.writeHtml(page.localize(BulkWorkflow.class, "action.returnToSearch")); page.writeEnd(); } page.writeEnd(); // end .message-success } // Trigger the Context to rebuild its internal data structure by re-setting the SearchResultSelection or Search. if (page.getSelection() != null) { page.setSelection(page.getSelection()); } else { Search originalSearch = page.getSearch(); // Reset the original visibilities that were set on the Search object. if (originalSearch != null) { originalSearch.setVisibilities(originalVisibilities); } page.setSearch(originalSearch); } } writeDetailStateHtml(page); page.writeFooter(); break; } } public static enum WidgetState { BUTTON, DETAIL, CONFIRM, DEFAULT } /** * Writes the WidgetState.DETAIL view of the BulkWorkflow widget. This view displays options for available workflow transitions * based on the SearchResultSelection or Search and the user's bulk workflow and individual workflow transition permissions. * @param page an instance of Context * @throws IOException * @throws ServletException */ private void writeDetailStateHtml(Context page) throws IOException, ServletException { page.writeStart("div", "class", "widget"); page.writeStart("h1", "class", "icon icon-object-workflow"); page.writeHtml("Workflow Options"); page.writeEnd(); if (!page.hasAnyTransitions()) { page.writeStart("p"); page.writeHtml(page.localize( BulkWorkflow.class, page.getSelection() != null ? "message.noTransitionsForSelection" : "message.noTransitionsForSearch")); page.writeEnd(); } for (Workflow workflow : page.workflows()) { for (ObjectType workflowType : page.workflowTypes(workflow)) { for (WorkflowState workflowState : page.workflowStates(workflow, workflowType)) { Set<WorkflowTransition> availableTransitions = page.getAvailableTransitions(workflow, workflowType, workflowState); if (availableTransitions.size() == 0) { continue; } page.writeStart("h3"); page.writeHtml(page.getWorkflowStateCount(workflowType, workflowState.getName()) + " " + workflowType.getDisplayName() + " "); page.writeStart("span", "class", "visibilityLabel"); page.writeHtml(workflowState.getDisplayName()); page.writeEnd(); // end .visibilityLabel page.writeEnd(); for (WorkflowTransition transition : availableTransitions) { page.writeStart("a", "class", "button", "target", TARGET, "href", getActionUrl(page, workflow, workflowType, workflowState, WidgetState.CONFIRM, "action-workflow", transition.getName())); page.writeHtml(transition.getDisplayName()); page.writeEnd(); } } } } page.writeEnd(); // end .-widget } /** * Writes the WidgetState.CONFIRM view of the BulkWorkflow widget. This view displays a summary of the requested workflow transition * and prompts the user for confirmation to proceed. * @param page an instance of Context * @throws IOException * @throws ServletException */ private void writeConfirmStateHtml(Context page) throws IOException, ServletException { page.writeStart("div", "class", "widget"); page.writeStart("h1"); page.writeHtml(page.localize(BulkWorkflow.class, "title.confirm")); page.writeEnd(); ObjectType transitionSourceType = ObjectType.getInstance(page.param(UUID.class, Context.TYPE_ID_PARAMETER)); String workflowStateName = page.param(String.class, Context.WORKFLOW_STATE_PARAMETER); WorkflowState workflowState = null; Workflow workflow = Query.from(Workflow.class).where("_id = ?", page.param(String.class, Context.WORKFLOW_ID_PARAMETER)).first(); for (WorkflowState workflowStateValue : workflow.getStates()) { if (ObjectUtils.equals(workflowStateValue.getName(), workflowStateName)) { workflowState = workflowStateValue; break; } } WorkflowTransition workflowTransition = page.getWorkflowTransition(workflow, transitionSourceType, workflowState, page.param(String.class, "action-workflow")); if (workflowTransition == null) { return; } page.writeStart("form", "method", "post", "action", getActionUrl(page, workflow, transitionSourceType, workflowState, WidgetState.DETAIL)); page.writeStart("table", "class", "table-striped"); page.writeStart("tr"); page.writeStart("td"); page.writeHtml(page.localize(BulkWorkflow.class, "label.type")); page.writeEnd(); page.writeStart("td").writeHtml(transitionSourceType.getDisplayName()).writeEnd(); page.writeEnd(); // end row page.writeStart("tr"); page.writeStart("td"); page.writeHtml(page.localize(BulkWorkflow.class, "label.count")); page.writeEnd(); page.writeStart("td").writeHtml(page.getWorkflowStateCount(transitionSourceType, workflowStateName)).writeEnd(); page.writeEnd(); // end row page.writeStart("tr"); page.writeStart("td"); page.writeHtml(page.localize(BulkWorkflow.class, "label.currentState")); page.writeEnd(); page.writeStart("td").writeHtml(workflowTransition.getSource().getDisplayName()).writeEnd(); page.writeEnd(); // end row page.writeStart("tr"); page.writeStart("td"); page.writeHtml(page.localize(BulkWorkflow.class, "label.newState")); page.writeEnd(); page.writeStart("td").writeHtml(workflowTransition.getTarget().getDisplayName()).writeEnd(); page.writeEnd(); // end row page.writeEnd(); // end table WorkflowLog log = new WorkflowLog(); page.writeStart("div", "class", "widget-publishingWorkflowLog"); page.writeElement("input", "type", "hidden", "name", "workflowLogId", "value", log.getId()); page.writeFormFields(log); page.writeEnd(); page.writeStart("button", "name", "action-workflow", "value", workflowTransition.getName()); page.writeHtml(page.localize( BulkWorkflow.class, ImmutableMap.of("name", workflowTransition.getDisplayName()), "action.confirm")); page.writeEnd(); page.writeEnd(); page.writeEnd(); } /** * Helper method for generating a stateful BulkWorkflow servlet URL for forms and anchors. * @param page an instance of Context * @param workflow A Workflow for which to display or effect transitions. * @param workflowType An ObjectType for which the workflow is specified. * @param workflowState A WorkflowState of the specified workflow. * @param widgetState A state of the BulkWorkflow servlet widget to render. * @param params Additional query parameters to attach to the returned URL. * @return the requested URL */ private String getActionUrl(Context page, Workflow workflow, ObjectType workflowType, WorkflowState workflowState, WidgetState widgetState, Object... params) { UrlBuilder urlBuilder = new UrlBuilder(page.getRequest()) .absolutePath(page.cmsUrl(PATH)); urlBuilder.parameter("action-workflow", null); if (page.getSearch() != null) { // Search uses current page parameters urlBuilder.currentParameters(); } // SearchResultSelection uses an ID parameter urlBuilder.parameter(Context.SELECTION_ID_PARAMETER, page.getSelection() != null ? page.getSelection().getId() : null); urlBuilder.parameter(Context.WORKFLOW_ID_PARAMETER, workflow != null ? workflow.getId() : null); urlBuilder.parameter(Context.TYPE_ID_PARAMETER, workflowType != null ? workflowType.getId() : null); urlBuilder.parameter(Context.WORKFLOW_STATE_PARAMETER, workflowState != null ? workflowState.getName() : null); urlBuilder.parameter(Context.WIDGET_STATE_PARAMETER, widgetState); for (int i = 0; i < params.length / 2; i++) { urlBuilder.parameter(params[i], params[i + 1]); } return urlBuilder.toString(); } /** * A private extension of ToolPageContext for use only with the BulkWorkflow servlet widget. */ private static class Context extends ToolPageContext { public static final String WIDGET_STATE_PARAMETER = "bulkWorkflowState"; public static final String SELECTION_ID_PARAMETER = "selectionId"; public static final String TYPE_ID_PARAMETER = "sourceTypeId"; public static final String WORKFLOW_ID_PARAMETER = "workflowId"; public static final String WORKFLOW_STATE_PARAMETER = "sourceWorkflowState"; public static final String SEARCH_PARAMETER = "search"; private Search search; private SearchResultSelection selection; private WidgetState widgetState; private Map<Workflow, Map<ObjectType, Map<WorkflowState, Map<String, WorkflowTransition>>>> availableTransitionsMap = new LinkedHashMap<>(); private Map<ObjectType, Map<String, Integer>> workflowStateCounts = new LinkedHashMap<>(); private boolean hasAnyTransitions = false; public Context(PageContext pageContext) { this(pageContext.getServletContext(), (HttpServletRequest) pageContext.getRequest(), (HttpServletResponse) pageContext.getResponse(), pageContext.getOut(), null, null); } public Context(ToolPageContext page) { this(page.getServletContext(), page.getRequest(), page.getResponse(), page.getDelegate(), null, null); } public Context(ToolPageContext page, SearchResultSelection selection, Search search) { this(page.getServletContext(), page.getRequest(), page.getResponse(), page.getDelegate(), selection, search); } public Context(ServletContext servletContext, HttpServletRequest request, HttpServletResponse response, Writer delegate, SearchResultSelection selection, Search search) { super(servletContext, request, response); setDelegate(delegate); String selectionId = param(String.class, SELECTION_ID_PARAMETER); this.widgetState = ObjectUtils.firstNonNull(param(WidgetState.class, WIDGET_STATE_PARAMETER), WidgetState.DETAIL); if (selection != null) { setSelection(selection); } else if (!ObjectUtils.isBlank(selectionId)) { LOGGER.debug("Found " + SELECTION_ID_PARAMETER + " query parameter with value: " + selectionId); SearchResultSelection queriedSelection = (SearchResultSelection) Query.fromAll().where("_id = ?", selectionId).first(); if (queriedSelection == null) { try { throw new IllegalArgumentException(this.localize( BulkWorkflow.class, ImmutableMap.of("id", selectionId), "error.noSelectionExists")); } catch (IOException exception) { throw new IllegalArgumentException("No Collection/SearchResultSelection exists for id " + selectionId); } } setSelection(queriedSelection); } else if (search != null) { setSearch(search); } else { Search searchFromJson = searchFromJson(); if (searchFromJson == null) { LOGGER.debug("Could not obtain Search object from JSON query parameter"); searchFromJson = new Search(); } setSearch(searchFromJson); } } public Search getSearch() { return search; } public void setSearch(Search search) { this.search = search; buildTransitionMap(search); } public SearchResultSelection getSelection() { return selection; } public void setSelection(SearchResultSelection selection) { this.selection = selection; buildTransitionMap(selection); } public WidgetState getWidgetState() { return widgetState; } public void setWidgetState(WidgetState widgetState) { this.widgetState = widgetState; } // Utility methods for navigating the availableTransitionsMap public Set<Workflow> workflows() { return availableTransitionsMap.keySet(); } private Map<ObjectType, Map<WorkflowState, Map<String, WorkflowTransition>>> getTypeMap(Workflow workflow) { Map<ObjectType, Map<WorkflowState, Map<String, WorkflowTransition>>> typeMap = availableTransitionsMap.get(workflow); if (typeMap == null) { return new HashMap<>(); } return typeMap; } public Set<ObjectType> workflowTypes(Workflow workflow) { return getTypeMap(workflow).keySet(); } private Map<WorkflowState, Map<String, WorkflowTransition>> getStateMap(Workflow workflow, ObjectType workflowType) { Map<WorkflowState, Map<String, WorkflowTransition>> stateMap = getTypeMap(workflow).get(workflowType); if (stateMap == null) { return new HashMap<>(); } return stateMap; } public Set<WorkflowState> workflowStates(Workflow workflow, ObjectType workflowType) { return getStateMap(workflow, workflowType).keySet(); } private Map<String, WorkflowTransition> getTransitionMap(Workflow workflow, ObjectType workflowType, WorkflowState workflowState) { Map<String, WorkflowTransition> transitionMap = getStateMap(workflow, workflowType).get(workflowState); if (transitionMap == null) { return new HashMap<>(); } return transitionMap; } public Set<WorkflowTransition> getAvailableTransitions(Workflow workflow, ObjectType workflowType, WorkflowState workflowState) { return new HashSet<>(getTransitionMap(workflow, workflowType, workflowState).values()); } public WorkflowTransition getWorkflowTransition(Workflow workflow, ObjectType workflowType, WorkflowState workflowState, String transitionName) { return getTransitionMap(workflow, workflowType, workflowState).get(transitionName); } public int getWorkflowStateCount(ObjectType workflowType, String workflowStateName) { Map<String, Integer> countMap = workflowStateCounts.get(workflowType); if (countMap == null) { return 0; } return ObjectUtils.to(int.class, countMap.get(workflowStateName)); } /** * Produces a Search object from JSON and prevents errors when the same query parameter name is used for non-JSON Search representation. * @return Search if a query parameter specifies valid Search JSON, null otherwise. */ public Search searchFromJson() { Search search = null; String searchParam = param(String.class, SEARCH_PARAMETER); if (searchParam != null) { try { Map<String, Object> searchJson = ObjectUtils.to(new TypeReference<Map<String, Object>>() { }, ObjectUtils.fromJson(searchParam)); search = new Search(); search.getState().setValues(searchJson); } catch (Exception ignore) { // Ignore. Search will be constructed below using ToolPageContext } } return search; } public boolean hasAnyTransitions() { return hasAnyTransitions; } // Produces a Query for objects to be bulk workflow transitioned. public Query itemsQuery() { if (getSearch() != null) { return getSearch().toQuery(getSite()); } else if (getSelection() != null) { return getSelection().createItemsQuery(); } throw new IllegalStateException("No Search or SearchResultsSelection populated. Cannot create items Query."); } /** * Clears and sets the internal workflowStateCounts, availableTransitionsMap, and hasAnyTransitions state variables using the * SearchResultSelection provided. * @param selection a SearchResultSelection representing the objects to be analyzed for Workflow transition availability. */ private void buildTransitionMap(SearchResultSelection selection) { workflowStateCounts.clear(); availableTransitionsMap.clear(); hasAnyTransitions = false; if (selection == null) { return; } Set<ObjectType> itemTypes = new HashSet<>(); for (Object item : selection.createItemsQuery().selectAll()) { State itemState = State.getInstance(item); ObjectType itemType = itemState.getType(); if (itemType == null) { continue; } if (workflowStateCounts.get(itemType) == null) { workflowStateCounts.put(itemType, new LinkedHashMap<>()); } String itemWorkflowCurrentState = itemState.as(Workflow.Data.class).getCurrentState(); if (!ObjectUtils.isBlank(itemWorkflowCurrentState)) { int count = ObjectUtils.to(int.class, workflowStateCounts.get(itemType).get(itemWorkflowCurrentState)); workflowStateCounts.get(itemType).put(itemWorkflowCurrentState, count + 1); } itemTypes.add(itemType); } if (itemTypes.size() == 0) { return; } for (Workflow workflow : Query.from(Workflow.class).where("contentTypes = ?", itemTypes).selectAll()) { for (ObjectType workflowType : workflow.getContentTypes()) { Map<ObjectType, Map<WorkflowState, Map<String, WorkflowTransition>>> stateTransitionsByType = availableTransitionsMap.get(workflow); if (stateTransitionsByType == null) { stateTransitionsByType = new LinkedHashMap<>(); availableTransitionsMap.put(workflow, stateTransitionsByType); } for (WorkflowState workflowState : workflow.getStates()) { Map<WorkflowState, Map<String, WorkflowTransition>> stateTransitions = stateTransitionsByType.get(workflowType); if (stateTransitions == null) { stateTransitions = new LinkedHashMap<>(); stateTransitionsByType.put(workflowType, stateTransitions); } Map<String, Integer> counts = workflowStateCounts.get(workflowType); if (counts != null && ObjectUtils.to(int.class, counts.get(workflowState.getName())) > 0) { Map<String, WorkflowTransition> transitionsFrom = new HashMap<>(); for (Map.Entry<String, WorkflowTransition> entry : workflow.getTransitionsFrom(workflowState.getName()).entrySet()) { String typePermissionId = "type/" + workflowType.getId(); if (hasPermission(typePermissionId + "/bulkWorkflow") && hasPermission(typePermissionId + "/" + entry.getKey())) { transitionsFrom.put(entry.getKey(), entry.getValue()); } } if (transitionsFrom.size() > 0) { hasAnyTransitions = true; } stateTransitions.put(workflowState, transitionsFrom); } } } } } /** * Clears and sets the internal workflowStateCounts, availableTransitionsMap, and hasAnyTransitions state variables using the * Search provided. * @param search a Search representing the objects to be analyzed for Workflow transition availability. */ private void buildTransitionMap(Search search) { workflowStateCounts.clear(); availableTransitionsMap.clear(); hasAnyTransitions = false; if (search == null) { return; } Query<?> query = search.toQuery(getSite()); ObjectType selectedType = search.getSelectedType(); if (selectedType == null) { return; } String typePermissionId = "type/" + selectedType.getId(); workflowStateCounts.put(selectedType, new HashMap<>()); // check that a single visibility (workflow) has been refined if (search.getVisibilities() == null || search.getVisibilities().size() == 0) { return; } Set<String> refinedStates = new HashSet<>(); for (String currentState : search.getVisibilities()) { if (currentState.startsWith("w.")) { refinedStates.add(currentState.substring(2)); } refinedStates.add(currentState); } if (refinedStates.size() == 0) { return; } for (Workflow workflow : Query.from(Workflow.class).where("contentTypes = ?", selectedType).selectAll()) { Map<ObjectType, Map<WorkflowState, Map<String, WorkflowTransition>>> stateTransitionsByType = availableTransitionsMap.get(workflow); if (stateTransitionsByType == null) { stateTransitionsByType = new LinkedHashMap<>(); availableTransitionsMap.put(workflow, stateTransitionsByType); } for (WorkflowState workflowState : workflow.getStates()) { // skip workflow states that are not part of the current Search refinement if (!refinedStates.contains(workflowState.getName()) && !refinedStates.contains("w")) { continue; } Map<WorkflowState, Map<String, WorkflowTransition>> stateTransitions = stateTransitionsByType.get(selectedType); if (stateTransitions == null) { stateTransitions = new LinkedHashMap<>(); stateTransitionsByType.put(selectedType, stateTransitions); } long stateCount = Query.fromQuery(query).where("cms.workflow.currentState = ?", workflowState.getName()).noCache().count(); workflowStateCounts.get(selectedType).put(workflowState.getName(), ObjectUtils.to(Integer.class, stateCount)); if (stateCount > 0) { Map<String, WorkflowTransition> transitionsFrom = new HashMap<>(); for (Map.Entry<String, WorkflowTransition> entry : workflow.getTransitionsFrom(workflowState.getName()).entrySet()) { if (hasPermission(typePermissionId + "/bulkWorkflow") && hasPermission(typePermissionId + "/" + entry.getKey())) { transitionsFrom.put(entry.getKey(), entry.getValue()); } } if (transitionsFrom.size() > 0) { hasAnyTransitions = true; } stateTransitions.put(workflowState, transitionsFrom); } } } } /** * Tries to apply a workflow action to the given {@code object} if the * user has asked for it in the current request. * * @param object Can't be {@code null}. * @param {@code true} if the application of a workflow action is tried. */ public boolean tryWorkflowOnly(Object object) { if (!isFormPost()) { return false; } String action = param(String.class, "action-workflow"); if (ObjectUtils.isBlank(action)) { return false; } State state = State.getInstance(object); Draft draft = getOverlaidDraft(object); Workflow.Data workflowData = state.as(Workflow.Data.class); String oldWorkflowState = workflowData.getCurrentState(); try { state.beginWrites(); Workflow workflow = Workflow.findWorkflow(getSite(), state); if (workflow != null) { WorkflowTransition transition = workflow.getTransitions().get(action); if (transition != null) { if (!hasPermission("type/" + state.getTypeId() + "/bulkWorkflow") || !hasPermission("type/" + state.getTypeId() + "/" + transition.getName())) { throw new IllegalAccessException(this.localize( BulkWorkflow.class, ImmutableMap.of( "transitionName", transition.getDisplayName(), "typeDisplayName", state.getType().getDisplayName()), "error.transitionPermission")); } WorkflowLog log = new WorkflowLog(); UUID logId = log.getId(); state.as(Content.ObjectModification.class).setDraft(false); log.getState().setId(param(UUID.class, "workflowLogId")); updateUsingParameters(log); // keep unique ID log.getState().setId(logId); workflowData.changeState(transition, getUser(), log); if (draft == null) { publish(object); } else { draft.as(Workflow.Data.class).changeState(transition, getUser(), log); draft.update(Draft.findOldValues(object), object); publish(draft); } state.commitWrites(); } } return true; } catch (Exception error) { if (draft != null) { draft.as(Workflow.Data.class).revertState(oldWorkflowState); } workflowData.revertState(oldWorkflowState); getErrors().add(error); return false; } finally { state.endWrites(); } } } }