IssueForm.java

/*
 * This software was designed and created by Jason Carroll.
 * Copyright (c) 2002, 2003, 2004 Jason Carroll.
 * The author can be reached at jcarroll@cowsultants.com
 * ITracker website: http://www.cowsultants.com
 * ITracker forums: http://www.cowsultants.com/phpBB/index.php
 *
 * This program is free software; you can redistribute it and/or modify
 * it only 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.
 */

package org.itracker.web.forms;

import bsh.EvalError;
import bsh.Interpreter;
import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import groovy.lang.Script;
import org.apache.commons.lang.StringUtils;
import org.apache.struts.action.ActionErrors;
import org.apache.struts.action.ActionMapping;
import org.apache.struts.action.ActionMessage;
import org.apache.struts.action.ActionMessages;
import org.apache.struts.upload.FormFile;
import org.itracker.IssueException;
import org.itracker.WorkflowException;
import org.itracker.core.resources.ITrackerResources;
import org.itracker.model.*;
import org.itracker.model.util.*;
import org.itracker.services.*;
import org.itracker.web.util.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.*;

/**
 * This form is by the struts actions to pass issue data.
 */
public class IssueForm extends ITrackerForm {

    /**
     *
     */
    private static final long serialVersionUID = 1L;

    private static final Logger log = LoggerFactory.getLogger(IssueForm.class);

    private Integer id = null;
    private String caller = null;
    private Integer projectId = null;
    private Integer creatorId = null;
    private Integer ownerId = null;
    private String description = null;
    private Integer severity = null;
    private Integer status = null;
    private Integer prevStatus = null;
    private String resolution = null;
    private Integer targetVersion = null;
    private Integer[] components = new Integer[0];
    private Integer[] versions = new Integer[0];
    private String attachmentDescription = null;
    transient private FormFile attachment = null;
    private String history = null;
    // lets try to put Integer,String here:
    private HashMap<String, String> customFields = new HashMap<>();
    private IssueRelation.Type relationType = null;
    private Integer relatedIssueId = null;

    /**
     * The most general way to run scripts. All matching of event and fields
     * are embedded within. As a result, optionValues parameter will
     * contain updated values and form will contain new default values
     * if appropriate.
     *
     * @param projectScriptModels is a list of scripts.
     * @param event               is an event type.
     * @param currentValues       values mapped to field-ids
     * @param optionValues        is a map of current values to fields by field-Id.
     * @param currentErrors       is a container for errors.
     */
    public void processFieldScripts(List<ProjectScript> projectScriptModels, int event, Map<Integer, String> currentValues, Map<Integer, List<NameValuePair>> optionValues, ActionMessages currentErrors) throws WorkflowException {
        if ((!isWorkflowScriptsAllowed()) || projectScriptModels == null || projectScriptModels.size() == 0)
            return;
        log.debug("Processing " + projectScriptModels.size() + " field scripts for project " + projectScriptModels.get(0).getProject().getId());

        List<ProjectScript> scriptsToRun = new ArrayList<>(projectScriptModels.size());
        for (ProjectScript model : projectScriptModels) {
            if (model.getScript().getEvent() == event) {
                scriptsToRun.add(model);
            }
        }
        // order the scripts by priority
        Collections.sort(scriptsToRun, ProjectScript.PRIORITY_COMPARATOR);

        if (log.isDebugEnabled()) {
            log.debug(scriptsToRun.size() + " eligible scripts found for event " + event);
        }

        String currentValue;
        for (ProjectScript currentScript : scriptsToRun) {
            try {
                currentValue = currentValues.get((currentScript.getFieldType() == Configuration.Type.customfield?
                        currentScript.getFieldId():currentScript.getFieldType().getLegacyCode()));
                log.debug("Running script " + currentScript.getScript().getId()
                        + " with priority " + currentScript.getPriority());

                log.debug("Before script current value for field " + IssueUtilities.getFieldName(currentScript.getFieldId())
                        + " (" + currentScript.getFieldId() + ") is "
                        + currentValue + "'");

                List<NameValuePair> options;
                if (currentScript.getFieldType() == Configuration.Type.customfield) {
                    options = optionValues.get(currentScript.getFieldId());
                    if (null == options) {
                        options = Collections.emptyList();
                        optionValues.put(currentScript.getFieldId(), options);
                    }
                } else {
                    if (!optionValues.containsKey(currentScript.getFieldType().getLegacyCode())){
                        options = Collections.emptyList();
                        optionValues.put(currentScript.getFieldType().getLegacyCode(), options);
                    } else {
                        options = optionValues.get(currentScript.getFieldType().getLegacyCode());
                    }
                }

                currentValue = processFieldScript(currentScript, event,
                        currentValue,
                        options, currentErrors);
                currentValues.put( (currentScript.getFieldType() == Configuration.Type.customfield?
                        currentScript.getFieldId():currentScript.getFieldType().getLegacyCode()),
                        currentValue );


                log.debug("After script current value for field " + IssueUtilities.getFieldName(currentScript.getFieldId())
                        + " (" + currentScript.getFieldId() + ") is "
                        + currentValue + "'");

            } catch (WorkflowException we) {
                log.error("Error processing script ", we);
                currentErrors.add(ActionMessages.GLOBAL_MESSAGE, new ActionMessage("itracker.web.error.system.message", we.getMessage(), "Workflow"));
            }
        }


        // apply new values
        for (ProjectScript script: projectScriptModels) {
            if (script.getScript().getEvent() == event) {
                final String val;
                switch (script.getFieldType()) {
                    case status:
                        val = currentValues.get(Configuration.Type.status.getLegacyCode());
                        try {
                            setStatus(Integer.valueOf(val));
                        } catch (RuntimeException re) {/* OK */}
                        break;
                    case severity:
                        val = currentValues.get(Configuration.Type.severity.getLegacyCode());
                        try {
                            setSeverity(Integer.valueOf(val));
                        } catch (RuntimeException re) {/* OK */}
                        break;
                    case resolution:
                        val = currentValues.get(Configuration.Type.resolution.getLegacyCode());
                        setResolution(val);
                        break;
                    case customfield:
                        getCustomFields().put(String.valueOf(script.getFieldId()), currentValues.get(script.getFieldId()));
                        break;
                    default:
                        log.warn("unsupported field type in script: " + script.getFieldType() + " in project " + script.getProject().getName());
                        break;
                }
            }
        }
    }


    /**
     * Run provided BEANSHELL script against form instance, taking into account
     * incoming event type, field raised an event and current values.
     * As a result, a set of new current values is returned and if
     * appropriate, default values are changed in form.
     * TODO: should issue, project, user, services be available too?
     *
     * @param projectScript is a script to run.
     * @param event         is an event type.
     * @param currentValue  the current field value
     * @param optionValues  is a set of valid option-values.
     * @param currentErrors is a container for occured errors.
     * @return new changed currentValue.
     */
    public String processFieldScript(ProjectScript projectScript, int event, String currentValue, List<NameValuePair> optionValues, ActionMessages currentErrors) throws WorkflowException {
        if (projectScript == null) {
            throw new WorkflowException("ProjectScript was null.", WorkflowException.INVALID_ARGS);
        }
        if (currentErrors == null) {
            throw new WorkflowException("Errors was null.", WorkflowException.INVALID_ARGS);
        }

        if (!isWorkflowScriptsAllowed()) {
            return currentValue;
        }

        String result = currentValue;

        try {
            if (projectScript.getScript().getLanguage() != WorkflowScript.ScriptLanguage.Groovy) {
                result = processBeanShellScript(projectScript, currentValue, optionValues, currentErrors, event);
            } else {
                result = processGroovyScript(projectScript, currentValue, optionValues, currentErrors, event);
            }
            if (log.isDebugEnabled()) {
                log.debug("processFieldScript: Script returned current value of '" + optionValues + "' (" + (optionValues != null ? optionValues.getClass().getName() : "NULL") + ")");
            }
        } catch (EvalError evalError) {
            log.error("processFieldScript: eval failed: " + projectScript, evalError);
            currentErrors.add(ActionMessages.GLOBAL_MESSAGE,
                    new ActionMessage("itracker.web.error.invalidscriptdata", evalError.getMessage()));
        } catch (RuntimeException e) {
            log.warn("processFieldScript: Error processing field script: " + projectScript, e);
            currentErrors.add(ActionMessages.GLOBAL_MESSAGE,
                    new ActionMessage("itracker.web.error.system.message",
                            new Object[]{
                                    e.getMessage(),
                                    ITrackerResources.getString("itracker.web.attr.script") // Script
                            }));
        }
        if (log.isDebugEnabled()) {
            log.debug("processFieldScript: returning " + result + ", errors: " + currentErrors);
        }
        return result;
    }

    private String processGroovyScript(final ProjectScript projectScript,
                                       final String currentValue,
                                       final List<NameValuePair> optionValues,
                                       final ActionMessages currentErrors,
                                       final int event) {

        final Map<String,Object> ctx = new HashMap<>(8);
        ctx.put("currentValue", StringUtils.defaultString(currentValue));
        ctx.put("event", event);
        ctx.put("fieldId", (projectScript.getFieldType() == Configuration.Type.customfield ?
                projectScript.getFieldId() : projectScript.getFieldType().getLegacyCode()));

        ctx.put("optionValues", Collections.unmodifiableList(optionValues));
        ctx.put("currentErrors", currentErrors);
        ctx.put("currentForm", this);

        final Binding binding = new Binding(ctx);

        GroovyShell shell = new GroovyShell();
        Script script = shell.parse(projectScript.getScript().getScript(),
                        projectScript.getScript().getName());
        script.setBinding(binding);
        Object ret = script.run();
        if (!currentErrors.isEmpty()) {
            return currentValue;
        }
        return returnScriptResult(ret, ctx.get("currentValue"), currentValue);
    }

    private String processBeanShellScript(final ProjectScript projectScript,
                                          final String currentValue,
                                          final List<NameValuePair> optionValues,
                                          final ActionMessages currentErrors,
                                          final int event) throws EvalError {
        Interpreter bshInterpreter = new Interpreter();
        bshInterpreter.set("event", event);
        bshInterpreter.set("fieldId", (projectScript.getFieldType()== Configuration.Type.customfield?
            projectScript.getFieldId():projectScript.getFieldType().getLegacyCode()));
        bshInterpreter.set("currentValue", StringUtils.defaultString(currentValue));
        bshInterpreter.set("optionValues", optionValues);
        bshInterpreter.set("currentErrors", currentErrors);
        bshInterpreter.set("currentForm", this);

        Object obj = bshInterpreter.eval(projectScript.getScript().getScript());
        if (!currentErrors.isEmpty()) {
            return currentValue;
        }
        return returnScriptResult(obj, bshInterpreter.get("currentValue"), currentValue);
    }

    private static String returnScriptResult(Object returned, Object assigned, String currentValue) {
        if (! (returned instanceof CharSequence)) {
            log.debug("script did not return a value");
            returned = assigned;
        }
        if (returned instanceof CharSequence) {
            return String.valueOf(returned);
        }
        log.debug("failed to get value from script, returning previous value");
        return currentValue;
    }

    public final Issue processFullEdit(Issue issue, Project project, User user,
                                              Map<Integer, Set<PermissionType>> userPermissions, Locale locale,
                                              IssueService issueService, ActionMessages errors) throws Exception {
        int previousStatus = issue.getStatus();
        boolean needReloadIssue;
        ActionMessages msg = new ActionMessages();
        issue = addAttachment(issue, project, user, getITrackerServices(), msg);

        if (!msg.isEmpty()) {
            // Validation of attachment failed
            errors.add(msg);
            return issue;
        }

        needReloadIssue = issueService.setIssueVersions(issue.getId(),
                new HashSet<>(Arrays.asList(getVersions())),
                user.getId());

        needReloadIssue = needReloadIssue | issueService.setIssueComponents(issue.getId(),
                new HashSet<>(Arrays.asList(getComponents())),
                user.getId());

        // reload issue for further updates
        if (needReloadIssue) {
            if (log.isDebugEnabled()) {
                log.debug("processFullEdit: updating issue from session: " + issue);
            }
            issue = issueService.getIssue(issue.getId());
        }

        Integer targetVersion = getTargetVersion();
        if (targetVersion != null && targetVersion != -1) {
            ProjectService projectService = ServletContextUtils.getItrackerServices()
                    .getProjectService();
            Version version = projectService.getProjectVersion(targetVersion);
            if (version == null) {
                throw new RuntimeException("No version with Id "
                        + targetVersion);
            }
            issue.setTargetVersion(version);
        }

        issue.setResolution(getResolution());
        issue.setSeverity(getSeverity());

        applyLimitedFields(issue, project, user, userPermissions, locale, issueService);

        Integer formStatus = getStatus();
        issue.setStatus(formStatus);
        if (formStatus != null) {
            if (log.isDebugEnabled()) {
                log.debug("processFullEdit: processing status: " + formStatus);
            }
            if (previousStatus != -1) {
                // Reopened the issue. Reset the resolution field.
                if ((previousStatus >= IssueUtilities.STATUS_ASSIGNED && previousStatus < IssueUtilities.STATUS_RESOLVED)
                        && (previousStatus >= IssueUtilities.STATUS_RESOLVED && previousStatus < IssueUtilities.STATUS_END)) {
                    issue.setResolution("");
                }

                if (previousStatus >= IssueUtilities.STATUS_CLOSED
                        && !UserUtilities.hasPermission(userPermissions, project
                        .getId(), UserUtilities.PERMISSION_CLOSE)) {
                    if (previousStatus < IssueUtilities.STATUS_CLOSED) {
                        issue.setStatus(previousStatus);
                    } else {
                        issue.setStatus(IssueUtilities.STATUS_RESOLVED);
                    }
                }

                if (issue.getStatus() < IssueUtilities.STATUS_NEW
                        || issue.getStatus() >= IssueUtilities.STATUS_END) {
                    issue.setStatus(previousStatus);
                }
            } else if (issue.getStatus() >= IssueUtilities.STATUS_CLOSED
                    && !UserUtilities.hasPermission(userPermissions, project
                    .getId(), PermissionType.ISSUE_CLOSE)) {
                issue.setStatus(IssueUtilities.STATUS_RESOLVED);
            }
        }

        if (issue.getStatus() < IssueUtilities.STATUS_NEW) {
            if (log.isDebugEnabled()) {
                log.debug("processFullEdit: status < STATUS_NEW: " + issue.getStatus());
            }
            issue.setStatus(IssueUtilities.STATUS_NEW);
            if (log.isDebugEnabled()) {
                log.debug("processFullEdit: updated to: " + issue.getStatus());
            }
        } else if (issue.getStatus() >= IssueUtilities.STATUS_END) {
            if (log.isDebugEnabled()) {
                log.debug("processFullEdit: status >= STATUS_END: " + issue.getStatus());
            }
            if (!UserUtilities.hasPermission(userPermissions, project.getId(),
                    PermissionType.ISSUE_CLOSE)) {
                issue.setStatus(IssueUtilities.STATUS_RESOLVED);
            } else {
                issue.setStatus(IssueUtilities.STATUS_CLOSED);
            }
            if (log.isDebugEnabled()) {
                log.debug("processFullEdit: status updated to: " + issue.getStatus());
            }
        }
        if (log.isDebugEnabled()) {
            log.debug("processFullEdit: updating issue " + issue);
        }
        return issueService.updateIssue(issue, user.getId());
    }

    public final void applyLimitedFields(Issue issue, Project project,
                                                User user, Map<Integer, Set<PermissionType>> userPermissionsMap,
                                                Locale locale,  IssueService issueService) throws Exception {

        issue.setDescription(getDescription());

        setIssueFields(issue, user, locale,  issueService);
        setOwner(issue, user, userPermissionsMap);
        addHistoryEntry(issue, user);
    }

    private void setIssueFields(Issue issue, User user, Locale locale,
                                       IssueService issueService) throws Exception {
        if (log.isDebugEnabled()) {
            log.debug("setIssueFields: called");
        }
        List<CustomField> projectCustomFields = issue.getProject()
                .getCustomFields();
        if (log.isDebugEnabled()) {
            log.debug("setIssueFields: got project custom fields: " + projectCustomFields);
        }

        if (projectCustomFields == null || projectCustomFields.size() == 0) {
            log.debug("setIssueFields: no custom fields, returning...");
            return;
        }


        // here you see some of the ugly side of Struts 1.3 - the forms... they
        // can only contain Strings and some simple objects types...
        HashMap<String, String> formCustomFields = getCustomFields();

        if (log.isDebugEnabled()) {
            log.debug("setIssueFields: got form custom fields: " + formCustomFields);
        }

        if (formCustomFields == null || formCustomFields.size() == 0) {
            log.debug("setIssueFields: no form custom fields, returning..");
            return;
        }

        ResourceBundle bundle = ITrackerResources.getBundle(locale);
        Iterator<CustomField> customFieldsIt = projectCustomFields.iterator();
        // declare iteration fields
        CustomField field;
        String fieldValue;
        IssueField issueField;
        try {
            if (log.isDebugEnabled()) {
                log.debug("setIssueFields: processing project fields");
            }
            // set values to issue-fields and add if needed
            while (customFieldsIt.hasNext()) {

                field = customFieldsIt.next();
                fieldValue = (String) formCustomFields.get(String.valueOf(field
                        .getId()));

                // remove the existing field for re-setting
                issueField = getIssueField(issue, field);


                if (fieldValue != null && fieldValue.trim().length() > 0) {
                    if (null == issueField) {
                        issueField = new IssueField(issue, field);
                        issue.getFields().add(issueField);
                    }

                    issueField.setValue(fieldValue, bundle);
                } else {
                    if (null != issueField) {
                        issue.getFields().remove(issueField);
                    }
                }
            }

            // set new issue fields for later saving
//			issue.setFields(issueFieldsList);

//			issueService.setIssueFields(issue.getId(), issueFieldsList);
        } catch (Exception e) {
            log.error("setIssueFields: failed to process custom fields", e);
            throw e;
        }
    }

    private static IssueField getIssueField(Issue issue, CustomField field) {
        Iterator<IssueField> it = issue.getFields().iterator();
        IssueField issueField;
        while (it.hasNext()) {
            issueField = it.next();
            if (issueField.getCustomField().equals(field)) {
                return issueField;
            }
        }
        return null;

    }

    private void setOwner(Issue issue, User user,
                                 Map<Integer, Set<PermissionType>> userPermissionsMap) throws Exception {
        if (log.isDebugEnabled()) {
            log.debug("setOwner: called to " + getOwnerId());
        }
        Integer currentOwner = (issue.getOwner() == null) ? null : issue
                .getOwner().getId();

        Integer ownerId = getOwnerId();

        if (ownerId == null || ownerId.equals(currentOwner)) {
            if (log.isDebugEnabled()) {
                log.debug("setOwner: returning, existing owner is the same: " + issue.getOwner());
            }
            return;
        }

        if (UserUtilities.hasPermission(userPermissionsMap,
                UserUtilities.PERMISSION_ASSIGN_OTHERS)
                || (UserUtilities.hasPermission(userPermissionsMap,
                UserUtilities.PERMISSION_ASSIGN_SELF) && user.getId()
                .equals(ownerId))
                || (UserUtilities.hasPermission(userPermissionsMap,
                UserUtilities.PERMISSION_UNASSIGN_SELF)
                && user.getId().equals(currentOwner) && ownerId == -1)) {
            User newOwner = ServletContextUtils.getItrackerServices().getUserService().getUser(ownerId);
            if (log.isDebugEnabled()) {
                log.debug("setOwner: setting new owner " + newOwner + " to " + issue);
            }
            issue.setOwner(newOwner);
//			issueService.assignIssue(issue.getId(), ownerId, user.getId());
        }

    }

    private void addHistoryEntry(Issue issue, User user) throws Exception {
        try {
            String history = getHistory();

            if (history == null || history.equals("")) {
                if (log.isDebugEnabled()) {
                    log.debug("addHistoryEntry: skip history to " + issue);
                }
                return;
            }


            if (ProjectUtilities.hasOption(ProjectUtilities.OPTION_SURPRESS_HISTORY_HTML, issue.getProject().getOptions())) {
                history = HTMLUtilities.removeMarkup(history);
            } else if (ProjectUtilities.hasOption(ProjectUtilities.OPTION_LITERAL_HISTORY_HTML, issue.getProject().getOptions())) {
                history = HTMLUtilities.escapeTags(history);
            } else {
                history = HTMLUtilities.newlinesToBreaks(history);
            }


            if (log.isDebugEnabled()) {
                log.debug("addHistoryEntry: adding history to " + issue);
            }
            IssueHistory issueHistory = new IssueHistory(issue, user, history,
                    IssueUtilities.HISTORY_STATUS_AVAILABLE);

            issueHistory.setDescription(getHistory());
            issueHistory.setCreateDate(new Date());

            issueHistory.setLastModifiedDate(new Date());
            issue.getHistory().add(issueHistory);


//  TODO why do we need to updateIssue here, and can not later?
//			issueService.updateIssue(issue, user.getId());
        } catch (Exception e) {
            log.error("addHistoryEntry: failed to add", e);
            throw e;
        }
//		issueService.addIssueHistory(issueHistory);
        if (log.isDebugEnabled()) {
            log.debug("addHistoryEntry: added history for issue " + issue);
        }
    }

    public final Issue processLimitedEdit(Issue issue, Project project,
                                                 User user, Map<Integer, Set<PermissionType>> userPermissionsMap,
                                                 Locale locale, IssueService issueService, ActionMessages messages)
            throws Exception {
        ActionMessages msg = new ActionMessages();
        issue = addAttachment(issue, project, user, ServletContextUtils.getItrackerServices(), msg);

        if (!msg.isEmpty()) {
            messages.add(msg);
            return issue;
        }

        Integer formStatus = getStatus();

        if (formStatus != null) {

            if (issue.getStatus() >= IssueUtilities.STATUS_RESOLVED
                    && formStatus >= IssueUtilities.STATUS_CLOSED
                    && UserUtilities.hasPermission(userPermissionsMap,
                    UserUtilities.PERMISSION_CLOSE)) {

                issue.setStatus(formStatus);
            }

        }

        applyLimitedFields(issue, project, user, userPermissionsMap, locale, issueService);
        return issueService.updateIssue(issue, user.getId());

    }

    /**
     * method needed to prepare request for edit_issue.jsp
     */
    public static void setupJspEnv(ActionMapping mapping,
                                         IssueForm issueForm, HttpServletRequest request, Issue issue,
                                         IssueService issueService, UserService userService,
                                         Map<Integer, Set<PermissionType>> userPermissions,
                                         Map<Integer, List<NameValuePair>> listOptions, ActionMessages errors)
            throws ServletException, IOException, WorkflowException {

        if (log.isDebugEnabled()) {
            log.debug("setupJspEnv: for issue " + issue);
        }

        NotificationService notificationService = ServletContextUtils
                .getItrackerServices().getNotificationService();
        String pageTitleKey = "itracker.web.editissue.title";
        String pageTitleArg = request.getParameter("id");
        Locale locale = LoginUtilities.getCurrentLocale(request);
        User um = LoginUtilities.getCurrentUser(request);
        List<NameValuePair> statuses = WorkflowUtilities.getListOptions(
                listOptions, IssueUtilities.FIELD_STATUS);
        String statusName = IssueUtilities.getStatusName(issue.getStatus(), locale);
        boolean hasFullEdit = UserUtilities.hasPermission(userPermissions,
                issue.getProject().getId(), UserUtilities.PERMISSION_EDIT_FULL);
        List<NameValuePair> resolutions = WorkflowUtilities.getListOptions(
                listOptions, IssueUtilities.FIELD_RESOLUTION);
        String severityName = IssueUtilities.getSeverityName(issue
                .getSeverity(), locale);
        List<NameValuePair> components = WorkflowUtilities.getListOptions(
                listOptions, IssueUtilities.FIELD_COMPONENTS);
        List<NameValuePair> versions = WorkflowUtilities.getListOptions(
                listOptions, IssueUtilities.FIELD_VERSIONS);
        List<NameValuePair> targetVersion = WorkflowUtilities.getListOptions(
                listOptions, IssueUtilities.FIELD_TARGET_VERSION);
        List<Component> issueComponents = issue.getComponents();
        Collections.sort(issueComponents);
        List<Version> issueVersions = issue.getVersions();
        Collections.sort(issueVersions, new Version.VersionComparator());
        /* Get project fields and put name and value in map */
        setupProjectFieldsMapJspEnv(issue.getProject().getCustomFields(), issue.getFields(), request);

        setupRelationsRequestEnv(issue.getRelations(), request);


        request.setAttribute("pageTitleKey", pageTitleKey);
        request.setAttribute("pageTitleArg", pageTitleArg);
        request.getSession().setAttribute(Constants.LIST_OPTIONS_KEY,
                listOptions);
        request.setAttribute("targetVersions", targetVersion);
        request.setAttribute("components", components);
        request.setAttribute("versions", versions);
        request.setAttribute("hasIssueNotification", notificationService
                .hasIssueNotification(issue, um.getId()));
        request.setAttribute("hasHardIssueNotification", IssueUtilities.hasHardNotification(issue, issue.getProject(), um.getId()));
        request.setAttribute("hasEditIssuePermission", UserUtilities
                .hasPermission(userPermissions, issue.getProject().getId(),
                        UserUtilities.PERMISSION_EDIT));
        request.setAttribute("canCreateIssue",
                issue.getProject().getStatus() == Status.ACTIVE
                        && UserUtilities.hasPermission(userPermissions, issue
                        .getProject().getId(),
                        UserUtilities.PERMISSION_CREATE));
        request.setAttribute("issueComponents", issueComponents);
        request.setAttribute("issueVersions",
                issueVersions == null ? new ArrayList<Version>()
                        : issueVersions);
        request.setAttribute("statuses", statuses);
        request.setAttribute("statusName", statusName);
        request.setAttribute("hasFullEdit", hasFullEdit);
        request.setAttribute("resolutions", resolutions);
        request.setAttribute("severityName", severityName);
        request.setAttribute("hasPredefinedResolutionsOption", ProjectUtilities
                .hasOption(ProjectUtilities.OPTION_PREDEFINED_RESOLUTIONS,
                        issue.getProject().getOptions()));
        request.setAttribute("issueOwnerName",
                (issue.getOwner() == null ? ITrackerResources.getString(
                        "itracker.web.generic.unassigned", locale)
                        : issue.getOwner().getFirstName() + " "
                        + issue.getOwner().getLastName()));
        request.setAttribute("isStatusResolved",
                issue.getStatus() >= IssueUtilities.STATUS_RESOLVED);


        request.setAttribute("fieldSeverity", WorkflowUtilities.getListOptions(
                listOptions, IssueUtilities.FIELD_SEVERITY));
        request.setAttribute("possibleOwners", WorkflowUtilities
                .getListOptions(listOptions, IssueUtilities.FIELD_OWNER));

        request.setAttribute("hasNoViewAttachmentOption", ProjectUtilities
                .hasOption(ProjectUtilities.OPTION_NO_ATTACHMENTS, issue
                        .getProject().getOptions()));

        if (log.isDebugEnabled()) {
            log.debug("setupJspEnv: options " + issue.getProject().getOptions() + ", hasNoViewAttachmentOption: " + request.getAttribute("hasNoViewAttachmentOption"));
        }

        setupNotificationsInRequest(request, issue, notificationService);

        // setup issue to request, as it's needed by the jsp.
        request.setAttribute(Constants.ISSUE_KEY, issue);
        request.setAttribute("issueForm", issueForm);
        request.setAttribute(Constants.PROJECT_KEY, issue.getProject());
        List<IssueHistory> issueHistory = issueService.getIssueHistory(issue
                .getId());
        Collections.sort(issueHistory, IssueHistory.CREATE_DATE_COMPARATOR);
        request.setAttribute("issueHistory", issueHistory);


    }

    /**
     * Get project fields and put name and value in map
     * TODO: simplify this code, it's not readable, unsave yet.
     */
    public static final void setupProjectFieldsMapJspEnv(List<CustomField> projectFields, Collection<IssueField> issueFields, HttpServletRequest request) {
        Map<CustomField, String> projectFieldsMap = new HashMap<CustomField, String>();

        if (projectFields != null && projectFields.size() > 0) {
            Collections.sort(projectFields, CustomField.ID_COMPARATOR);

            HashMap<String, String> fieldValues = new HashMap<String, String>();
            Iterator<IssueField> issueFieldsIt = issueFields.iterator();
            while (issueFieldsIt.hasNext()) {
                IssueField issueField = issueFieldsIt.next();

                if (issueField.getCustomField() != null
                        && issueField.getCustomField().getId() > 0) {
                    if (issueField.getCustomField().getFieldType() == CustomField.Type.DATE) {
                        Locale locale = LoginUtilities.getCurrentLocale(request);
                        String value = issueField.getValue(locale);
                        fieldValues.put(issueField.getCustomField().getId()
                                .toString(), value);
                    } else {
                        fieldValues.put(issueField.getCustomField().getId()
                                .toString(), issueField
                                .getStringValue());
                    }
                }
            }
            Iterator<CustomField> fieldsIt = projectFields.iterator();
            CustomField field;
            while (fieldsIt.hasNext()) {

                field = fieldsIt.next();

                String fieldValue = fieldValues.get(String.valueOf(field
                        .getId()));
                if (null == fieldValue) {
                    fieldValue = "";
                };
                projectFieldsMap.put(field, fieldValue);

            }

            request.setAttribute("projectFieldsMap", projectFieldsMap);
        }
    }

    protected static void setupRelationsRequestEnv(List<IssueRelation> relations, HttpServletRequest request) {
        Collections.sort(relations, IssueRelation.LAST_MODIFIED_DATE_COMPARATOR);
        request.setAttribute("issueRelations", relations);

    }

    public static void setupNotificationsInRequest(
            HttpServletRequest request, Issue issue,
            NotificationService notificationService) {
        List<Notification> notifications = notificationService
                .getIssueNotifications(issue);

        Collections.sort(notifications, Notification.TYPE_COMPARATOR);

        request.setAttribute("notifications", notifications);
        Map<User, Set<Notification.Role>> notificationMap = NotificationUtilities
                .mappedRoles(notifications);
        request.setAttribute("notificationMap", notificationMap);
        request.setAttribute("notifiedUsers", notificationMap.keySet());
    }

    /**
     * Adds an attachment to issue.
     *
     * @return updated issue
     */
    public Issue addAttachment(Issue issue, Project project, User user,
                                       ITrackerServices services, ActionMessages messages) {


        FormFile file = getAttachment();

        if (file == null || file.getFileName().trim().length() < 1) {
            log.info("addAttachment: skipping file " + file);
            return issue;
        }

        if (ProjectUtilities.hasOption(ProjectUtilities.OPTION_NO_ATTACHMENTS,
                project.getOptions())) {
            messages.add(ActionMessages.GLOBAL_MESSAGE, new ActionMessage("itracker.web.error.validate.attachment.disabled", project.getName()));
            return issue;
        }

        String origFileName = file.getFileName();
        String contentType = file.getContentType();
        int fileSize = file.getFileSize();

        String attachmentDescription = getAttachmentDescription();

        if (null == contentType || 0 >= contentType.length()) {
            log.info("addAttachment: got no mime-type, using default plain-text");
            contentType = "text/plain";
        }

        if (log.isDebugEnabled()) {
            log.debug("addAttachment: adding file, name: " + origFileName
                    + " of type " + file.getContentType() + ", description: "
                    + getAttachmentDescription() + ". filesize: " + fileSize);
        }
        ActionMessages validation = AttachmentUtilities.validate(file, services);
        if (validation.isEmpty()) {

//		if (AttachmentUtilities.checkFile(file, getITrackerServices())) {
            int lastSlash = Math.max(origFileName.lastIndexOf('/'),
                    origFileName.lastIndexOf('\\'));
            if (lastSlash > -1) {
                origFileName = origFileName.substring(lastSlash + 1);
            }

            IssueAttachment attachmentModel = new IssueAttachment(issue,
                    origFileName, contentType, attachmentDescription, fileSize,
                    user);

            attachmentModel.setIssue(issue);
//			issue.getAttachments().add(attachmentModel);
            byte[] fileData;
            try {
                fileData = file.getFileData();
            } catch (IOException e) {
                log.error("addAttachment: failed to get file data", e);
                messages.add(ActionMessages.GLOBAL_MESSAGE, new ActionMessage("itracker.web.error.system"));
                return issue;
            }
            if (services.getIssueService()
                    .addIssueAttachment(attachmentModel, fileData)) {
                return services.getIssueService().getIssue(issue.getId());
            }


        } else {
            if (log.isDebugEnabled()) {
                log.debug("addAttachment: failed to validate: " + origFileName + ", " + validation);
            }
            messages.add(validation);
        }
        return issue;
    }

    public final void setupIssueForm(Issue issue,
                                            final Map<Integer, List<NameValuePair>> listOptions,
                                            HttpServletRequest request, ActionMessages errors)
            throws WorkflowException {
        HttpSession session = request.getSession(true);

        IssueService issueService = ServletContextUtils.getItrackerServices()
                .getIssueService();
        Locale locale = (Locale) session.getAttribute(Constants.LOCALE_KEY);
        setId(issue.getId());
        setProjectId(issue.getProject().getId());
        setPrevStatus(issue.getStatus());
        setCaller(request.getParameter("caller"));

        setDescription(HTMLUtilities.handleQuotes(issue
                .getDescription()));
        setStatus(issue.getStatus());

        if (!ProjectUtilities.hasOption(
                ProjectUtilities.OPTION_PREDEFINED_RESOLUTIONS, issue
                .getProject().getOptions())) {
            // TODO What happens here, validation?
            try {
                issue.setResolution(IssueUtilities.checkResolutionName(issue
                        .getResolution(), locale));
            } catch (MissingResourceException | NumberFormatException mre) {
                log.error(mre.getMessage());
            }
        }

        setResolution(HTMLUtilities.handleQuotes(issue
                .getResolution()));
        setSeverity(issue.getSeverity());

        setTargetVersion(issue.getTargetVersion() == null ? -1
                : issue.getTargetVersion().getId());

        setOwnerId((issue.getOwner() == null ? -1 : issue.getOwner()
                .getId()));

        List<IssueField> fields = issue.getFields();
        HashMap<String, String> customFields = new HashMap<String, String>();
        for (IssueField field : fields) {
            customFields.put(field.getCustomField().getId().toString(),
                    field.getValue(locale));
        }

        setCustomFields(customFields);

        HashSet<Integer> selectedComponents = issueService
                .getIssueComponentIds(issue.getId());
        if (selectedComponents != null) {
            Integer[] componentIds;
            ArrayList<Integer> components = new ArrayList<>(
                    selectedComponents);
            componentIds = components.toArray(new Integer[components.size()]);
            setComponents(componentIds);
        }

        HashSet<Integer> selectedVersions = issueService
                .getIssueVersionIds(issue.getId());
        if (selectedVersions != null) {
            Integer[] versionIds;
            ArrayList<Integer> versions = new ArrayList<>(
                    selectedVersions);
            versionIds = versions.toArray(new Integer[versions.size()]);
            setVersions(versionIds);
        }

        invokeProjectScripts(issue.getProject(), WorkflowUtilities.EVENT_FIELD_ONPOPULATE, listOptions, errors);

    }

    public void invokeProjectScripts(Project project, int event, final Map<Integer, List<NameValuePair>> options, ActionMessages errors)
            throws WorkflowException {
        final Map<Integer, String> values = new HashMap<>(options.size());
        for (CustomField field: project.getCustomFields()) {
            values.put(field.getId()
                    , getCustomFields().get(String.valueOf(field.getId())));
        }
        values.put(Configuration.Type.status.getLegacyCode(),
                String.valueOf(getStatus()));
        values.put(Configuration.Type.severity.getLegacyCode(),
                String.valueOf(getSeverity()));
        values.put(Configuration.Type.resolution.getLegacyCode(),
                getResolution());

        processFieldScripts(project.getScripts(),
                event, values, options, errors);

    }

    public  Map<Integer, List<NameValuePair>> invokeProjectScripts(Project project, int event, ActionMessages errors)
            throws WorkflowException {

        final Map<Integer, List<NameValuePair>> options = EditIssueActionUtil.mappedFieldOptions(project.getCustomFields()) ;
        invokeProjectScripts(project, event, options, errors);
        return options;
    }

    public FormFile getAttachment() {
        return attachment;
    }

    public void setAttachment(FormFile attachment) {
        this.attachment = attachment;
    }

    public String getAttachmentDescription() {
        return attachmentDescription;
    }

    public void setAttachmentDescription(String attachmentDescription) {
        this.attachmentDescription = attachmentDescription;
    }

    public String getCaller() {
        return caller;
    }

    public void setCaller(String caller) {
        this.caller = caller;
    }

    public Integer[] getComponents() {
        if (null == components)
            return null;
        return components.clone();
    }

    public void setComponents(Integer[] components) {
        if (null == components)
            this.components = null;
        else
            this.components = components.clone();
    }

    public Integer getCreatorId() {
        return creatorId;
    }

    public void setCreatorId(Integer creatorId) {
        this.creatorId = creatorId;
    }

    // let's try to put Integer,String here:
    public HashMap<String, String> getCustomFields() {
        return customFields;
    }

    // let's try to put Integer,String here:
    public void setCustomFields(HashMap<String, String> customFields) {
        this.customFields = customFields;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public String getHistory() {
        return history;
    }

    public void setHistory(String history) {
        this.history = history;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public Integer getOwnerId() {
        return ownerId;
    }

    public void setOwnerId(Integer ownerId) {
        this.ownerId = ownerId;
    }

    public Integer getPrevStatus() {
        return prevStatus;
    }

    public void setPrevStatus(Integer prevStatus) {
        this.prevStatus = prevStatus;
    }

    public Integer getProjectId() {
        return projectId;
    }

    public void setProjectId(Integer projectId) {
        this.projectId = projectId;
    }

    public Integer getRelatedIssueId() {
        return relatedIssueId;
    }

    public void setRelatedIssueId(Integer relatedIssueId) {
        this.relatedIssueId = relatedIssueId;
    }

    public IssueRelation.Type getRelationType() {
        return relationType;
    }

    public void setRelationType(IssueRelation.Type relationType) {
        this.relationType = relationType;
    }

    public String getResolution() {
        return resolution;
    }

    public void setResolution(String resolution) {
        this.resolution = resolution;
    }

    public Integer getSeverity() {
        return severity;
    }

    public void setSeverity(Integer severity) {
        this.severity = severity;
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }

    public Integer getTargetVersion() {
        return targetVersion;
    }

    public void setTargetVersion(Integer targetVersion) {
        this.targetVersion = targetVersion;
    }

    public Integer[] getVersions() {
        if (null == versions)
            return null;
        return versions.clone();
    }

    public void setVersions(Integer[] versions) {
        if (null == versions)
            this.versions = null;
        else
            this.versions = versions.clone();
    }

    /**
     * This methods adds in validation for custom fields. It makes sure the
     * datatype matches and also that all required fields have been populated.
     *
     * @param mapping the ActionMapping object
     * @param request the current HttpServletRequest object
     * @return an ActionErrors object containing any validation errors
     */
    public ActionErrors validate(ActionMapping mapping,
                                 HttpServletRequest request) {
        if (log.isDebugEnabled()) {
            log.debug("validate called: mapping: " + mapping + ", request: "
                    + request);
        }
        ActionErrors errors = super.validate(mapping, request);

        if (log.isDebugEnabled()) {
            log.debug("validate called: mapping: " + mapping + ", request: "
                    + request + ", errors: " + errors);
        }

        try {
            if (null != getId()) {
                Issue issue;
                try {
                    issue = getITrackerServices().getIssueService().getIssue(
                            getId());
                } catch (Exception e) {
                    return errors;
                }

                Locale locale = (Locale) request.getSession().getAttribute(
                        Constants.LOCALE_KEY);
                User currUser = (User) request.getSession().getAttribute(
                        Constants.USER_KEY);
                List<NameValuePair> ownersList = EditIssueActionUtil
                        .getAssignableIssueOwnersList(issue,
                                issue.getProject(), currUser, locale,
                                getITrackerServices().getUserService(),
                                RequestHelper.getUserPermissions(request
                                        .getSession()));

                setupJspEnv(mapping, this, request, issue,
                        getITrackerServices().getIssueService(),
                        getITrackerServices().getUserService(), RequestHelper
                        .getUserPermissions(request.getSession()),
                        EditIssueActionUtil.getListOptions(request, issue,
                                ownersList, RequestHelper
                                .getUserPermissions(request
                                        .getSession()), issue
                                .getProject(), currUser), errors);

                if (errors.isEmpty() && issue.getProject() == null) {
                    if (log.isDebugEnabled()) {
                        log.debug("validate: issue project is null: " + issue);
                    }
                    errors.add(ActionMessages.GLOBAL_MESSAGE,
                            new ActionMessage(
                                    "itracker.web.error.invalidproject"));
                } else if (errors.isEmpty()
                        && issue.getProject().getStatus() != Status.ACTIVE) {
                    if (log.isDebugEnabled()) {
                        log.debug("validate: issue project is not active: " + issue);
                    }
                    errors.add(ActionMessages.GLOBAL_MESSAGE,
                            new ActionMessage(
                                    "itracker.web.error.projectlocked"));
                } else if (errors.isEmpty() && !"editIssueForm".equals(mapping.getName())) {
                    if (log.isDebugEnabled()) {
                        log.debug("validate: validation had errors for " + issue + ": " + errors);
                    }

                    if (UserUtilities.hasPermission(RequestHelper.getUserPermissions(request.getSession()),
                            issue.getProject().getId(),
                            UserUtilities.PERMISSION_EDIT_FULL)) {
                        validateProjectFields(issue.getProject(), request, errors);
                    }


                    validateProjectScripts(issue.getProject(), errors);
                    validateAttachment(this.getAttachment(), getITrackerServices(), errors);
                }
            } else {
                EditIssueActionUtil.setupCreateIssue(request);
                HttpSession session = request.getSession();
                Project project = (Project) session
                        .getAttribute(Constants.PROJECT_KEY);
                if (log.isDebugEnabled()) {
                    log.debug("validate: validating create new issue for project: " + page);
                }
                validateProjectFields(project, request, errors);
                validateProjectScripts(project, errors);
                validateAttachment(this.getAttachment(), getITrackerServices(), errors);
            }
        } catch (Exception e) {
            e.printStackTrace();
            log.error("validate: unexpected exception", e);
            errors.add(ActionMessages.GLOBAL_MESSAGE, new ActionMessage(
                    "itracker.web.error.system"));
        }
        if (log.isDebugEnabled()) {
            log.debug("validate: returning errors: " + errors);
        }
        return errors;
    }

    private static void validateAttachment(FormFile attachment, ITrackerServices services, ActionMessages errors) {
        if (null != attachment) {
            ActionMessages msg = AttachmentUtilities.validate(attachment, services);
            if (!msg.isEmpty()) {
                if (log.isDebugEnabled()) {
                    log.debug("validateAttachment: failed to validate, " + msg);
                }
                errors.add(msg);
            }
        }
    }

    private static void validateProjectFields(Project project,
                                              HttpServletRequest request, ActionErrors errors) {

        List<CustomField> projectFields = project.getCustomFields();
        if (null != projectFields && projectFields.size() > 0) {

            Locale locale = LoginUtilities.getCurrentLocale(request);

            ResourceBundle bundle = ITrackerResources.getBundle(locale);
            for (CustomField customField : projectFields) {
                String fieldValue = request.getParameter("customFields("
                        + customField.getId() + ")");
                if (fieldValue != null && !fieldValue.equals("")) {

                    // Don't create an IssueField only so that we can call
                    // setValue to validate the value!
                    try {
                        customField.checkAssignable(fieldValue, locale, bundle);
                    } catch (IssueException ie) {
                        String label = CustomFieldUtilities.getCustomFieldName(
                                customField.getId(), locale);
                        errors.add(ActionMessages.GLOBAL_MESSAGE,
                                new ActionMessage(ie.getType(), label));
                    }
                } else if (customField.isRequired()) {
                    String label = CustomFieldUtilities.getCustomFieldName(
                            customField.getId(), locale);
                    errors.add(ActionMessages.GLOBAL_MESSAGE,
                            new ActionMessage(IssueException.TYPE_CF_REQ_FIELD,
                                    label));
                }
            }
        }
    }

    private void validateProjectScripts(Project project, ActionErrors errors)
            throws WorkflowException {

        invokeProjectScripts(project, WorkflowUtilities.EVENT_FIELD_ONVALIDATE, errors);

    }

    public static boolean isWorkflowScriptsAllowed() {
        Boolean val = ServletContextUtils.getItrackerServices().getConfigurationService().getBooleanProperty("allow_workflowscripts", true);
        if (log.isDebugEnabled()) {
            log.debug("isWorkflowScriptsAllowed: {}allowed by configuration 'allow_workflowscripts'", !val?"NOT ":"");
        }
        return val;
    }

}