/******************************************************************************* * Imixs Workflow * Copyright (C) 2001, 2011 Imixs Software Solutions GmbH, * http://www.imixs.com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You can receive a copy of the GNU General Public * License at http://www.gnu.org/licenses/gpl.html * * Project: * http://www.imixs.org * http://java.net/projects/imixs-workflow * * Contributors: * Imixs Software Solutions GmbH - initial API and implementation * Ralph Soika - Software Developer *******************************************************************************/ package org.imixs.marty.workflow; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.ResourceBundle; import java.util.StringTokenizer; import java.util.logging.Level; import java.util.logging.Logger; import javax.ejb.EJB; import javax.enterprise.context.Conversation; import javax.enterprise.context.ConversationScoped; import javax.enterprise.event.Event; import javax.enterprise.event.ObserverException; import javax.faces.component.UIViewRoot; import javax.faces.context.ExternalContext; import javax.faces.context.FacesContext; import javax.faces.event.ActionEvent; import javax.inject.Inject; import javax.inject.Named; import javax.servlet.http.HttpServletRequest; import org.imixs.marty.ejb.WorkitemService; import org.imixs.marty.model.ProcessController; import org.imixs.marty.util.ErrorHandler; import org.imixs.marty.util.ValidationException; import org.imixs.workflow.ItemCollection; import org.imixs.workflow.ItemCollectionComparator; import org.imixs.workflow.WorkflowKernel; import org.imixs.workflow.engine.WorkflowService; import org.imixs.workflow.exceptions.AccessDeniedException; import org.imixs.workflow.exceptions.ModelException; import org.imixs.workflow.exceptions.PluginException; import org.imixs.workflow.exceptions.ProcessingErrorException; import org.imixs.workflow.exceptions.QueryException; import org.imixs.workflow.faces.util.LoginController; /** * The marty WorkflowController extends the * org.imixs.workflow.jee.faces.workitem.WorkflowController and provides * additional functionality to manage workitems. * * The String property 'project' is used to assign the workItem to a project * entity. The property contains the $UniqueId of the corresponding project. If * the 'project' property is not set a new workItem can not be processed or a * new workItem can not be created. * * The marty WorkflowController provides an editor selector which allows to * split parts of a form in separate sections (see formpanel.xhmtl, * tabpanel.xhmtl) * * @author rsoika * @version 2.0 */ @Named @ConversationScoped public class WorkflowController extends org.imixs.workflow.faces.workitem.WorkflowController implements Serializable { public static final String DEFAULT_EDITOR_ID = "form_panel_simple#basic"; public static final String DEFAULT_ACTION_RESULT = "/pages/workitems/workitem"; @Inject private Conversation conversation; /* Services */ @EJB protected WorkitemService workitemService; @Inject protected ProcessController processController; @Inject protected LoginController loginController = null; @Inject protected Event<WorkflowEvent> events; private static final long serialVersionUID = 1L; private static Logger logger = Logger.getLogger(WorkflowController.class.getName()); private List<ItemCollection> versions = null; private List<EditorSection> editorSections = null; private String action = null; // optional page result private String deepLinkId = null; // deep link UniqueId private String defaultActionResult = null; public WorkflowController() { super(); } /** * Defines an optinal Action Result used by the process method */ public String getAction() { return action; } public void setAction(String action) { this.action = action; } public String getDefaultActionResult() { if (defaultActionResult == null) { defaultActionResult = DEFAULT_ACTION_RESULT; } return defaultActionResult; } public void setDefaultActionResult(String defultActionResult) { this.defaultActionResult = defultActionResult; } /** * The method loads a new wokitem by a uniqueID. If no id is provided the * method did not change the current workitem reference. If the uniqueId is * invalid the workitem will be set to null (see setUnqiueId) * * This method is used for the DeepLink Feature used by workitem.xhml. * * * @param aUniqueID */ public void setDeepLinkId(String adeepLinkId) { this.deepLinkId = adeepLinkId; // if Id is provided try to load the corresponding workitem. if (deepLinkId != null && !deepLinkId.isEmpty()) { this.load(deepLinkId); // finally we destroy the deepLinkId to avoid a reload on the next // postback deepLinkId = null; // ! } } public String getDeepLinkId() { return deepLinkId; } /** * The method sets the workitem and fires a WORKITEM_CHANGED event. */ @Override public void setWorkitem(ItemCollection newWorkitem) { // we may not call reset() here, because a conversation context can // still exist. events.fire(new WorkflowEvent(newWorkitem, WorkflowEvent.WORKITEM_CHANGED)); super.setWorkitem(newWorkitem); versions = null; editorSections = null; } /** * Loads a workitem by its ID. The method starts a new conversation context. */ @Override public void load(String uniqueID) { super.load(uniqueID); if (conversation.isTransient()) { conversation.setTimeout(((HttpServletRequest)FacesContext.getCurrentInstance().getExternalContext() .getRequest()).getSession().getMaxInactiveInterval()*1000); conversation.begin(); logger.fine("start new conversation, id=" + conversation.getId()); } } /** * This ActionListener method creates a new empty workitem. An existing * workitem and optional conversation context will be reset. The method * starts a new conversation context. Finally the method fires the * WorkfowEvent WORKITEM_CREATED. */ @Override public void create(ActionEvent event) { super.create(); if (conversation.isTransient()) { conversation.setTimeout(((HttpServletRequest)FacesContext.getCurrentInstance().getExternalContext() .getRequest()).getSession().getMaxInactiveInterval()*1000); conversation.begin(); logger.fine("start new conversation, id=" + conversation.getId()); } // fire event events.fire(new WorkflowEvent(getWorkitem(), WorkflowEvent.WORKITEM_CREATED)); } /** * This method creates a new empty workitem. An existing workitem and * optional conversation context will be reset. * * The method assigns the initial values '$ModelVersion', '$ProcessID' and * '$UniqueIDRef' to the new workitem. The method creates the empty field * '$workitemID' and the field 'namowner' which is assigned to the current * user. This data can be used in case that a workitem is not processed but * saved (e.g. by the dmsController). * * The method starts a new conversation context. Finally the method fires * the WorkfowEvent WORKITEM_CREATED. * * @param modelVersion * - model version * @param processID * - processID * @param processRef * - uniqueid ref */ public void create(String modelVersion, int processID, String processRef) { super.create(); // set model information.. getWorkitem().replaceItemValue("$ModelVersion", modelVersion); getWorkitem().replaceItemValue("$ProcessID", processID); // set default owner getWorkitem().replaceItemValue("namowner", loginController.getUserPrincipal()); // set empty $workitemid getWorkitem().replaceItemValue("$workitemid", ""); // assign process.. if (processRef != null) { getWorkitem().replaceItemValue("$UniqueIDRef", processRef); // find process ItemCollection process = processController.getProcessById(processRef); if (process != null) { getWorkitem().replaceItemValue("txtProcessName", process.getItemValueString("txtName")); getWorkitem().replaceItemValue("txtProcessRef", process.getItemValueString(WorkflowKernel.UNIQUEID)); } else { logger.warning("[create] - unable to find process entity '" + processRef + "'!"); } } // start now the new conversation if (conversation.isTransient()) { conversation.setTimeout(((HttpServletRequest)FacesContext.getCurrentInstance().getExternalContext() .getRequest()).getSession().getMaxInactiveInterval()*1000); conversation.begin(); logger.fine("start new conversation, id=" + conversation.getId()); } // fire event events.fire(new WorkflowEvent(getWorkitem(), WorkflowEvent.WORKITEM_CREATED)); } /** * This method overwrites the default init() and fires a WorkflowEvent. * * @throws ModelException * */ @Override public String init(String action) throws ModelException { String actionResult = super.init(action); // fire event events.fire(new WorkflowEvent(getWorkitem(), WorkflowEvent.WORKITEM_INITIALIZED)); return actionResult; } /** * The action method processes the current workItem and fires the * WorkflowEvents WORKITEM_BEFORE_PROCESS and WORKITEM_AFTER_PROCESS. The * Method also catches PluginExceptions and adds the corresponding Faces * Error Message into the FacesContext. In case of an exception the * WorkflowEvent WORKITEM_AFTER_PROCESS will not be fired. <br> * The action result returned by the workflow engine may contain a $uniqueid * to redirect the user and load that new workitem. If no action result is * defined the method redirects to the default action with the current * workitem id. * * The method appends faces-redirect=true to the action result in case no * faces-redirect is defined. * * * <code> * /pages/workitems/workitem?id=23452345-2452435234&faces-redirect=true * </code> * * In case the processing was successful, the current conversation will be * closed. In Case of an Exception (e.g PluginException) the conversation * will not be closed, so that the current workitem data is still available. * @throws ModelException * */ @Override public String process() throws AccessDeniedException, ProcessingErrorException, ModelException { String actionResult = null; long lTotal = System.currentTimeMillis(); // process workItem and catch exceptions try { // fire event long l1 = System.currentTimeMillis(); events.fire(new WorkflowEvent(getWorkitem(), WorkflowEvent.WORKITEM_BEFORE_PROCESS)); logger.finest("[process] fire WORKITEM_BEFORE_PROCESS event: ' in " + (System.currentTimeMillis() - l1) + "ms"); // process workitem actionResult = super.process(); // reset versions and editor sections versions = null; editorSections = null; // ! Do not call setWorkitem here because this fires a // WORKITEM_CHANGED event ! // fire event long l2 = System.currentTimeMillis(); events.fire(new WorkflowEvent(getWorkitem(), WorkflowEvent.WORKITEM_AFTER_PROCESS)); logger.finest("[process] fire WORKITEM_AFTER_PROCESS event: ' in " + (System.currentTimeMillis() - l2) + "ms"); // if a action was set by the workflowController, then this // action will be the action result String if (action != null && !action.isEmpty()) { actionResult = action; // reset action setAction(null); } // compute the Action result... if (actionResult == null || actionResult.isEmpty()) { // construct default action result if no actionResult was // defined actionResult = getDefaultActionResult() + "?id=" + getWorkitem().getUniqueID() + "&faces-redirect=true"; } // test if 'faces-redirect' is included in actionResult if (actionResult.contains("/") && !actionResult.contains("faces-redirect=")) { // append faces-redirect=true if (!actionResult.contains("?")) { actionResult = actionResult + "?"; } actionResult = actionResult + "faces-redirect=true"; } logger.fine("action result=" + actionResult); if (logger.isLoggable(Level.FINEST)) { logger.finest( "[process] '" + getWorkitem().getItemValueString(WorkflowService.UNIQUEID) + "' completed in " + (System.currentTimeMillis() - lTotal) + "ms"); } // close conversation reset(); } catch (ObserverException oe) { actionResult = null; // test if we can handle the exception... if (oe.getCause() instanceof PluginException) { // add error message into current form ErrorHandler.addErrorMessage((PluginException) oe.getCause()); } else { if (oe.getCause() instanceof ValidationException) { // add error message into current form ErrorHandler.addErrorMessage((ValidationException) oe.getCause()); } else { // throw unknown exception throw oe; } } } catch (PluginException pe) { actionResult = null; // add a new FacesMessage into the FacesContext ErrorHandler.handlePluginException(pe); } return actionResult; } /** * The method saves the current workItem and fires the WorkflowEvents * WORKITEM_BEFORE_SAVE and WORKITEM_AFTER_SAVE. * * NOTE: the super class changes the behavior of save(action) and process a * workItem instead of saving. This may conflict in future cases. If so we * should decide if we simply add a new method here called 'saveAsDraft()' * which would more precisely describe the behavior of this method. */ @Override public void save() throws AccessDeniedException { logger.fine("save workitem..."); // fire event events.fire(new WorkflowEvent(getWorkitem(), WorkflowEvent.WORKITEM_BEFORE_SAVE)); super.save(); // fire event events.fire(new WorkflowEvent(getWorkitem(), WorkflowEvent.WORKITEM_AFTER_SAVE)); } /** * Reset the current workitem. The method closes the existing conversation * context. */ @Override public void reset() { super.reset(); if (!conversation.isTransient()) { logger.fine("close conversation, id=" + conversation.getId()); conversation.end(); } } /** * This method reset the worktiem, closes the current conversation and * navigation to the home page. * * @return */ public String close() { this.reset(); return "pages/home?faces-redirect=true"; } /** * Loades a new wokitem by a uniqueID * * @param aUniqueID */ public void setUniqueId(String uniqueID) { this.load(uniqueID); } /** * Returns the current uniqueid of the workitem or null if no workitem is * set. * * @return uniqueid of current workitem or null */ public String getUniqueId() { if (this.getWorkitem() == null) { return null; } else { return this.getWorkitem().getItemValueString(WorkflowKernel.UNIQUEID); } } /** * returns the workflowEditorID for the current workItem. If no attribute * with the name "txtWorkflowEditorid" is available then the method return * the DEFAULT_EDITOR_ID. * * Additional the method tests if the txtWorkflowEditorid contains the * character '#'. This character indicates additional form-section * informations. The Method cuts this information and provides an Array of * EditoSection Objects by the property EditorSections * * @see getEditorSections * * * @return */ public String getEditor() { String sEditor = DEFAULT_EDITOR_ID; if (getWorkitem() != null) { String currentEditor = getWorkitem().getItemValueString("txtWorkflowEditorid"); if (!currentEditor.isEmpty()) sEditor = currentEditor; } // test if # is provides to indicate optional section // informations if (sEditor.indexOf('#') > -1) sEditor = sEditor.substring(0, sEditor.indexOf('#')); return sEditor; } /** * returns an array list with EditorSection Objects. Each EditorSection * object contains the url and the name of one section. EditorSections can * be provided by the workitem property 'txtWorkflowEditorid' marked with * the '#' character and separated with charater '|'. * * e.g.: form_tab#basic_project|sub_timesheet[owner,manager] * * This example provides the editor sections 'basic_project' and * 'sub_timesheet'. The optional marker after the second section in [] * defines the user membership to access this action. In this example the * second section is only visible if the current user is member of the * project owner or manager list. * * The following example illustrates how to iterate over the section array * from a JSF fragment: * * <code> * <ui:repeat value="#{workitemMB.editorSections}" var="section"> * .... * <ui:include src="/pages/workitems/forms/#{section.url}.xhtml" /> * </code> * * * The array of EditorSections also contains information about the name for * a section. This name is read from the resouce bundle 'bundle.forms'. The * '/' character will be replaced with '_'. So for example the section url * myforms/sub_timesheet will result in resoure bundle lookup for the name * 'myforms_sub_timersheet' * * @return */ public List<EditorSection> getEditorSections() { if (editorSections == null) { // compute editorSections editorSections = new ArrayList<EditorSection>(); UIViewRoot viewRoot = FacesContext.getCurrentInstance().getViewRoot(); Locale locale = viewRoot.getLocale(); String sEditor = DEFAULT_EDITOR_ID; if (getWorkitem() != null) { String currentEditor = getWorkitem().getItemValueString("txtWorkflowEditorid"); if (!currentEditor.isEmpty()) sEditor = currentEditor; } if (sEditor.indexOf('#') > -1) { String liste = sEditor.substring(sEditor.indexOf('#') + 1); StringTokenizer st = new StringTokenizer(liste, "|"); while (st.hasMoreTokens()) { try { String sURL = st.nextToken(); // if the URL contains a [] section test the defined // user // permissions if (sURL.indexOf('[') > -1 || sURL.indexOf(']') > -1) { boolean bPermissionGranted = false; // yes - cut the permissions String sPermissions = sURL.substring(sURL.indexOf('[') + 1, sURL.indexOf(']')); // cut the permissions from the URL sURL = sURL.substring(0, sURL.indexOf('[')); StringTokenizer stPermission = new StringTokenizer(sPermissions, ","); while (stPermission.hasMoreTokens()) { String aPermission = stPermission.nextToken(); // test for user role ExternalContext ectx = FacesContext.getCurrentInstance().getExternalContext(); if (ectx.isUserInRole(aPermission)) { bPermissionGranted = true; break; } // test if user is project member String sProjectUniqueID = this.getWorkitem().getItemValueString("$UniqueIDRef"); if ("manager".equalsIgnoreCase(aPermission) && processController.isManagerOf(sProjectUniqueID)) { bPermissionGranted = true; break; } if ("team".equalsIgnoreCase(aPermission) && this.processController.isTeamMemberOf(sProjectUniqueID)) { bPermissionGranted = true; break; } } // if not permission is granted - skip this section if (!bPermissionGranted) continue; } String sName = null; // compute name from ressource Bundle.... try { ResourceBundle rb = null; if (locale != null) rb = ResourceBundle.getBundle("bundle.app", locale); else rb = ResourceBundle.getBundle("bundle.app"); String sResouceURL = sURL.replace('/', '_'); sName = rb.getString(sResouceURL); } catch (java.util.MissingResourceException eb) { sName = ""; logger.warning(eb.getMessage()); } EditorSection aSection = new EditorSection(sURL, sName); editorSections.add(aSection); } catch (Exception est) { logger.severe("[getEditorSections] can not parse EditorSections : '" + sEditor + "'"); logger.severe(est.getMessage()); } } } } return editorSections; } /** * returns a List with all Versions of the current Workitem * * @return */ public List<ItemCollection> getVersions() { if (versions == null) loadVersionWorkItemList(); return versions; } /** * this method loads all versions to the current workitem. Idependent from * the type property! The method returns an empty list if no version exist * (only the main version) * * @see org.imixs.WorkitemService.business.WorkitemServiceBean */ private void loadVersionWorkItemList() { versions = new ArrayList<ItemCollection>(); if (this.isNewWorkitem() || null == getWorkitem()) return; List<ItemCollection> col = null; String sRefID = getWorkitem().getItemValueString("$workitemId"); // String refQuery = "SELECT entity FROM Entity entity " + " JOIN entity.textItems AS t" // + " WHERE entity.type IN ('workitem', 'workitemarchive', 'workitemversion') " // + " AND t.itemName = '$workitemid'" + " AND t.itemValue = '" + sRefID + "' " // + " ORDER BY entity.modified ASC"; String refQuery="( (type:\"workitem\" OR type:\"workitemarchive\" OR type:\"workitemversion\") AND $workitemid:\""+sRefID + "\")"; try { col = this.getWorkflowService().getDocumentService().find(refQuery, 999,0); // sort by $modified Collections.sort(col, new ItemCollectionComparator("$modified", true)); // Only return version list if more than one version was found! if (col.size() > 1) { for (ItemCollection aworkitem : col) { versions.add(aworkitem); } } } catch (QueryException e) { logger.warning("loadVersionWorkItemList - invalid query: " + e.getMessage()); } } }