org.itracker.web.forms.IssueForm.java Source code

Java tutorial

Introduction

Here is the source code for org.itracker.web.forms.IssueForm.java

Source

/*
 * 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;
    }

}