IssueUtilities.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.model.util;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.itracker.core.resources.ITrackerResources;
import org.itracker.model.*;

import java.net.MalformedURLException;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * Contains utilities used when displaying and processing issues.
 */
public class IssueUtilities {

    private static final Logger log = Logger.getLogger(IssueUtilities.class);
    public static final int FIELD_TYPE_SINGLE = 1;
    public static final int FIELD_TYPE_INDEXED = 2;
    public static final int FIELD_TYPE_MAP = 3;

    public static final int FIELD_ID = -1;
    public static final int FIELD_DESCRIPTION = -2;
    public static final int FIELD_STATUS = -3;
    public static final int FIELD_RESOLUTION = -4;
    public static final int FIELD_SEVERITY = -5;
    public static final int FIELD_CREATOR = -6;
    public static final int FIELD_CREATEDATE = -7;
    public static final int FIELD_OWNER = -8;
    public static final int FIELD_LASTMODIFIED = -9;
    public static final int FIELD_PROJECT = -10;
    public static final int FIELD_TARGET_VERSION = -11;
    public static final int FIELD_COMPONENTS = -12;
    public static final int FIELD_VERSIONS = -13;
    public static final int FIELD_ATTACHMENTDESCRIPTION = -14;
    public static final int FIELD_ATTACHMENTFILENAME = -15;
    public static final int FIELD_HISTORY = -16;

    protected static final int[] STANDARD_FIELDS = {FIELD_ID,
            FIELD_DESCRIPTION, FIELD_STATUS, FIELD_RESOLUTION, FIELD_SEVERITY,
            FIELD_CREATOR, FIELD_CREATEDATE, FIELD_OWNER, FIELD_LASTMODIFIED,
            FIELD_PROJECT, FIELD_TARGET_VERSION, FIELD_COMPONENTS,
            FIELD_VERSIONS, FIELD_ATTACHMENTDESCRIPTION,
            FIELD_ATTACHMENTFILENAME, FIELD_HISTORY};

    public static final int STATUS_NEW = 100;
    public static final int STATUS_UNASSIGNED = 200;
    public static final int STATUS_ASSIGNED = 300;
    public static final int STATUS_RESOLVED = 400;
    public static final int STATUS_CLOSED = 500;

    // This marks the end of all status numbers. You can NOT add a status above
    // this number or
    // they will not be found.
    public static final int STATUS_END = 600;

    public static final int HISTORY_STATUS_REMOVED = -1;
    public static final int HISTORY_STATUS_AVAILABLE = 1;

    /**
     * Defines a related issue. Sample text: related to
     */
    public static final int RELATION_TYPE_RELATED_P = 1;
    /**
     * Defines a related issue. Sample text: related to
     */
    public static final int RELATION_TYPE_RELATED_C = 2;
    /**
     * Defines a duplicate issue. Sample text: duplicates
     */
    public static final int RELATION_TYPE_DUPLICATE_P = 3;
    /**
     * Defines a duplicate issue. Sample text: duplicate of
     */
    public static final int RELATION_TYPE_DUPLICATE_C = 4;
    /**
     * Defines a cloned issue. Sample text: cloned to
     */
    public static final int RELATION_TYPE_CLONED_P = 5;
    /**
     * Defines a cloned issue. Sample text: cloned from
     */
    public static final int RELATION_TYPE_CLONED_C = 6;
    /**
     * Defines a split issue. Sample text: split to
     */
    public static final int RELATION_TYPE_SPLIT_P = 7;
    /**
     * Defines a split issue. Sample text: split from
     */
    public static final int RELATION_TYPE_SPLIT_C = 8;
    /**
     * Defines a dependent issue. Sample text: dependents
     */
    public static final int RELATION_TYPE_DEPENDENT_P = 9;
    /**
     * Defines a dependent issue. Sample text: depends on
     */
    public static final int RELATION_TYPE_DEPENDENT_C = 10;

    public static final int NUM_RELATION_TYPES = 10;

    private static List<Configuration> resolutions = new ArrayList<Configuration>();
    private static List<Configuration> severities = new ArrayList<Configuration>();
    private static List<Configuration> statuses = new ArrayList<Configuration>();
    private static List<CustomField> customFields = new ArrayList<CustomField>();
    private static final Logger logger = Logger.getLogger(IssueUtilities.class);

    public IssueUtilities() {
    }

    public static String getOwnerName(User owner, Locale locale) {
        return (null != owner? owner.getFullName():
                ITrackerResources.getString("itracker.web.generic.unassigned", locale));
    }

    public static int getFieldType(Integer fieldId) {
        if (fieldId != null) {
            if (fieldId > 0) {
                return FIELD_TYPE_MAP;
            }

        }

        return FIELD_TYPE_SINGLE;
    }

    public static String getFieldName(Integer fieldId) {
        if (fieldId == null) {
            return "";
        }

        if (fieldId > 0) {
            return "customFields";
        }

        switch (fieldId) {
            case FIELD_ID:
                return "id";
            case FIELD_DESCRIPTION:
                return "description";
            case FIELD_STATUS:
                return "status";
            case FIELD_RESOLUTION:
                return "resolution";
            case FIELD_SEVERITY:
                return "severity";
            case FIELD_CREATOR:
                return "creatorId";
            case FIELD_CREATEDATE:
                return "createdate";
            case FIELD_OWNER:
                return "ownerId";
            case FIELD_LASTMODIFIED:
                return "lastmodified";
            case FIELD_PROJECT:
                return "projectId";
            case FIELD_TARGET_VERSION:
                return "targetVersion";
            case FIELD_COMPONENTS:
                return "components";
            case FIELD_VERSIONS:
                return "versions";
            case FIELD_ATTACHMENTDESCRIPTION:
                return "attachmentDescription";
            case FIELD_ATTACHMENTFILENAME:
                return "attachment";
            case FIELD_HISTORY:
                return "history";
            default:
                return "";
        }
    }

    public static String getFieldName(Integer fieldId,
                                      List<CustomField> customFields, Locale locale) {

        if (fieldId < 0) {
            return ITrackerResources.getString(getStandardFieldKey(fieldId), locale);
        } else {
            return CustomFieldUtilities.getCustomFieldName(fieldId,
                    locale);
        }

    }

    public static String getStandardFieldKey(int fieldId) {
        switch (fieldId) {
            case FIELD_ID:
                return "itracker.web.attr.id";
            case FIELD_DESCRIPTION:
                return "itracker.web.attr.description";
            case FIELD_STATUS:
                return "itracker.web.attr.status";
            case FIELD_RESOLUTION:
                return "itracker.web.attr.resolution";
            case FIELD_SEVERITY:
                return "itracker.web.attr.severity";
            case FIELD_CREATOR:
                return "itracker.web.attr.creator";
            case FIELD_CREATEDATE:
                return "itracker.web.attr.createdate";
            case FIELD_OWNER:
                return "itracker.web.attr.owner";
            case FIELD_LASTMODIFIED:
                return "itracker.web.attr.lastmodified";
            case FIELD_PROJECT:
                return "itracker.web.attr.project";
            case FIELD_TARGET_VERSION:
                return "itracker.web.attr.target";
            case FIELD_COMPONENTS:
                return "itracker.web.attr.components";
            case FIELD_VERSIONS:
                return "itracker.web.attr.versions";
            case FIELD_ATTACHMENTDESCRIPTION:
                return "itracker.web.attr.attachmentdescription";
            case FIELD_ATTACHMENTFILENAME:
                return "itracker.web.attr.attachmentfilename";
            case FIELD_HISTORY:
                return "itracker.web.attr.detaileddescription";
            default:
                return "itracker.web.generic.unknown";
        }
    }

    public static NameValuePair[] getStandardFields(Locale locale) {
        NameValuePair[] fieldNames = new NameValuePair[STANDARD_FIELDS.length];
        for (int i = 0; i < STANDARD_FIELDS.length; i++) {
            fieldNames[i] = new NameValuePair(ITrackerResources.getString(
                    getStandardFieldKey(STANDARD_FIELDS[i]), locale), Integer
                    .toString(STANDARD_FIELDS[i]));
        }
        return fieldNames;
    }

    public static String getRelationName(IssueRelation.Type value) {
        return getRelationName(value, ITrackerResources.getLocale());
    }


    public static String getRelationName(IssueRelation.Type value, Locale locale) {
        return StringUtils.defaultIfBlank(ITrackerResources.getString(
                ITrackerResources.KEY_BASE_ISSUE_RELATION + value.getCode(), locale), value.name());
    }

    public static IssueRelation.Type getMatchingRelationType(IssueRelation.Type relationType) {
        switch (relationType) {
            case RELATED_P:
                return IssueRelation.Type.RELATED_C;
            case RELATED_C:
                return IssueRelation.Type.RELATED_P;
            case DUPLICATE_P:
                return IssueRelation.Type.DUPLICATE_C;
            case DUPLICATE_C:
                return IssueRelation.Type.DUPLICATE_P;
            case CLONED_P:
                return IssueRelation.Type.CLONED_C;
            case CLONED_C:
                return IssueRelation.Type.CLONED_P;
            case SPLIT_P:
                return IssueRelation.Type.SPLIT_C;
            case SPLIT_C:
                return IssueRelation.Type.SPLIT_P;
            case DEPENDENT_P:
                return IssueRelation.Type.DEPENDENT_C;
            case DEPENDENT_C:
            default:
                return IssueRelation.Type.DEPENDENT_P;
        }
    }

    public static String componentsToString(Issue issue) {
        StringBuilder value = new StringBuilder();
        if (issue != null && issue.getComponents().size() > 0) {
            for (int i = 0; i < issue.getComponents().size(); i++) {
                value.append(i != 0 ? ", " : "").append(issue.getComponents().get(i).getName());
            }
        }
        return value.toString();
    }

    public static String versionsToString(Issue issue) {
        StringBuilder value = new StringBuilder();
        if (issue != null && issue.getVersions().size() > 0) {
            for (int i = 0; i < issue.getVersions().size(); i++) {
                value.append(i != 0 ? ", " : "").append(issue.getVersions().get(i).getNumber());
            }
        }
        return value.toString();
    }

    public static String historyToString(Issue issue, SimpleDateFormat sdf) {
        StringBuilder value = new StringBuilder();
        if (issue != null && issue.getHistory().size() > 0 && sdf != null) {
            for (int i = 0; i < issue.getHistory().size(); i++) {
                value.append(i != 0 ? "," : "").append(issue.getHistory().get(i).getDescription()).append(",").append(issue.getHistory().get(i).getUser().getFirstName());
                value.append(" ").append(issue.getHistory().get(i).getUser().getLastName()).append(",").append(sdf.format(issue.getHistory().get(i)
                        .getLastModifiedDate()));
            }
        }
        return value.toString();
    }

    public static String getStatusName(Integer value) {
        return getStatusName(value, ITrackerResources.getLocale());
    }

    public static String getStatusName(Integer value, Locale locale) {
        return getStatusName(Integer.toString(value), locale);
    }

    public static String getStatusName(String value, Locale locale) {
        return StringUtils.defaultIfBlank(ITrackerResources.getString(ITrackerResources.KEY_BASE_STATUS
                + value, locale), value);
    }

    /**
     * getStatuses() needs to get implemented..
     */
    public static List<Configuration> getStatuses() {
        return statuses;
    }

    public static List<NameValuePair> getStatuses(Locale locale) {
        List<NameValuePair> statusStrings = new ArrayList<>(statuses.size());
        for (Configuration status : statuses) {
            statusStrings.add(new NameValuePair(getStatusName(status.getValue(), locale), status.getValue()));
        }
        return statusStrings;
    }

    public static void setStatuses(List<Configuration> value) {
        statuses = (value == null ? new ArrayList<Configuration>() : value);
    }

    public static int getNumberStatuses() {
        return statuses.size();
    }

    public static String getSeverityName(Integer value) {
        return StringUtils.defaultIfBlank(getSeverityName(value, ITrackerResources.getLocale()), String.valueOf(value));
    }

    public static String getSeverityName(Integer value, Locale locale) {
        return StringUtils.defaultIfBlank(getSeverityName(Integer.toString(value), locale), String.valueOf(value));
    }

    public static String getSeverityName(String value, Locale locale) {
        return StringUtils.defaultIfBlank(ITrackerResources.getString(ITrackerResources.KEY_BASE_SEVERITY
                + value, locale), String.valueOf(value));
    }

    /**
     * Returns the list of the defined issue severities in the system. The array
     * returned is a cached list set from the setSeverities method. The actual
     * values are stored in the database and and can be obtained from the
     * ConfigurationService bean.
     *
     * @param locale the locale to return the severities as
     */
    public static List<NameValuePair> getSeverities(Locale locale) {
        List<NameValuePair> severityStrings = new ArrayList<>();

        for (Configuration severity : severities) {
            NameValuePair nvp = new NameValuePair(getSeverityName(severity.getValue(), locale),
                    severity.getValue());
            severityStrings.add(nvp);
        }
        return severityStrings;
    }

    public static void setSeverities(List<Configuration> value) {
        severities = (value == null ? new ArrayList<Configuration>() : value);
    }

    public static int getNumberSeverities() {
        return severities.size();
    }

    /**
     * Compares the severity of two issues. The int returned will be negative if
     * the the severity of issue A is less than the severity of issue B,
     * positive if issue A is a higher severity than issue B, or 0 if the two
     * issues have the same severity.
     *
     * @param issueA IssueModel A
     * @param issueB IssueModel B
     */
    public static int compareSeverity(Issue issueA, Issue issueB) {
        if (issueA == null && issueB == null) {
            return 0;
        } else if (issueA == null) {
            return -1;
        } else if (issueB == null) {
            return 1;
        } else {
            int issueAIndex = Integer.MAX_VALUE;
            int issueBIndex = Integer.MAX_VALUE;
            for (int i = 0; i < severities.size(); i++) {
                if (severities.get(i) != null) {
                    if (severities.get(i).getValue().equalsIgnoreCase(
                            Integer.toString(issueA.getSeverity()))) {
                        issueAIndex = i;
                    }
                    if (severities.get(i).getValue().equalsIgnoreCase(
                            Integer.toString(issueB.getSeverity()))) {
                        issueBIndex = i;
                    }
                }
            }
            if (issueAIndex > issueBIndex) {
                return -1;
            } else if (issueAIndex < issueBIndex) {
                return 1;
            }
        }

        return 0;
    }

    public static String getResolutionName(int value) {
        return getResolutionName(value, ITrackerResources.getLocale());
    }

    public static String getResolutionName(int value, Locale locale) {
        return getResolutionName(Integer.toString(value), locale);
    }

    public static String getResolutionName(String value, Locale locale) {
        return ITrackerResources.getString(
                ITrackerResources.KEY_BASE_RESOLUTION + value, locale);
    }

    public static String checkResolutionName(String value, Locale locale)
            throws MissingResourceException {
        return ITrackerResources.getCheckForKey(
                ITrackerResources.KEY_BASE_RESOLUTION + value, locale);
    }

    /**
     * Returns the list of predefined resolutions in the system. The array
     * returned is a cached list set from the setResolutions method. The actual
     * values are stored in the database and and can be obtained from the
     * ConfigurationService bean.
     *
     * @param locale the locale to return the resolutions as
     */
    public static List<NameValuePair> getResolutions(Locale locale) {
        final List<NameValuePair> resolutionStrings = new ArrayList<>(resolutions.size());
        for (Configuration resolution : resolutions) {
            resolutionStrings.add(new NameValuePair(
                    getResolutionName(resolution.getValue(), locale),
                    resolution.getValue()));
        }
        return resolutionStrings;
    }

    /**
     * Sets the cached list of predefined resolutions.
     */
    public static void setResolutions(List<Configuration> value) {
        resolutions = (value == null ? new ArrayList<Configuration>() : value);
    }

    public static String getActivityName(IssueActivityType type) {
        return getActivityName(type, ITrackerResources.getLocale());
    }

    public static String getActivityName(IssueActivityType type, Locale locale) {
        return StringUtils.defaultIfBlank(ITrackerResources.getString("itracker.activity."
                + String.valueOf(type.name()), locale), type.name());
    }

    /**
     * Returns the cached array of CustomFieldModels.
     *
     * @return an array of CustomFieldModels
     */
    public static List<CustomField> getCustomFields() {
        return (customFields == null ? new ArrayList<CustomField>()
                : customFields);
    }

    /**
     * Sets the cached array of CustomFieldModels.
     *
     */
    public static void setCustomFields(List<CustomField> value) {
        customFields = (value == null ? new ArrayList<CustomField>() : value);
    }

    /**
     * Returns the custom field with the supplied id. Any labels will be
     * localized to the system default locale.
     *
     * @param id the id of the field to return
     * @return the requested CustomField object, or a new field if not found
     */
    public static CustomField getCustomField(Integer id) {
        return getCustomField(id, ITrackerResources.getLocale());
    }

    /**
     * Returns the custom field with the supplied id value. Any labels will be
     * translated to the given locale.
     *
     * @param id     the id of the field to return
     * @param locale the locale to initialize any labels with
     * @return the requested CustomField object, or a new field if not found
     */
    public static CustomField getCustomField(Integer id, Locale locale) {
        CustomField retField = null;

        try {
            for (CustomField customField : customFields) {
                if (customField != null
                        && customField.getId() != null
                        && customField.getId().equals(id)) {
                    retField = (CustomField) customField.clone();
                    break;
                }
            }
        } catch (CloneNotSupportedException cnse) {
            logger.error("Error cloning CustomField: " + cnse.getMessage());
        }
        if (retField == null) {
            retField = new CustomField();
        }

        return retField;
    }

    /**
     * Returns the total number of defined custom fields
     */
    public static int getNumberCustomFields() {
        return customFields.size();
    }

    /**
     * Returns true if the user has permission to view the requested issue.
     *
     * @param issue       an IssueModel of the issue to check view permission for
     * @param user        a User for the user to check permission for
     * @param permissions a HashMap of the users permissions
     */
    public static boolean canViewIssue(Issue issue, User user,
                                       Map<Integer, Set<PermissionType>> permissions) {
        if (user == null) {
            if (log.isInfoEnabled()) {
                log
                        .info("canViewIssue: missing argument. user is null returning false");
            }
            return false;
        }
        return canViewIssue(issue, user.getId(), permissions);
    }


    /**
     * Returns true if the user has permission to view the requested issue.
     *
     * @param issue       an IssueModel of the issue to check view permission for
     * @param userId      the userId of the user to check permission for
     * @param permissions a HashMap of the users permissions
     */
    public static boolean canViewIssue(Issue issue, Integer userId,
                                       Map<Integer, Set<PermissionType>> permissions) {

        if (issue == null || userId == null || permissions == null) {
            if (log.isInfoEnabled()) {
                log.info("canViewIssue: missing argument. issue: " + issue
                        + ", userid: " + userId + ", permissions: "
                        + permissions);
            }
            return false;
        }

        if (UserUtilities.hasPermission(permissions,
                issue.getProject().getId(), PermissionType.ISSUE_VIEW_ALL)) {
            if (log.isDebugEnabled()) {
                log.debug("canViewIssue: issue: " + issue + ", user: " + userId
                        + ", permission: " + PermissionType.ISSUE_VIEW_ALL);
            }
            return true;
        }

        boolean isUsersIssue = false;
        // I think owner & creator should always be able to view the issue
        // otherwise it makes no sense of creating the issue itself.
        // So put these checks before checking permissions for the whole project.
        if (issue.getCreator().getId().equals(userId)) {
            if (log.isDebugEnabled()) {
                log.debug("canViewIssue: issue: " + issue + ", user: " + userId
                        + ", permission: is creator");
            }
            isUsersIssue = true;
        }

        if (issue.getOwner() != null) {
            if (issue.getOwner().getId().equals(userId)) {

                if (log.isDebugEnabled()) {
                    log.debug("canViewIssue: issue: " + issue + ", user: "
                            + userId + ", permission: is owner");
                }
                isUsersIssue = true;
            }
        }

        // TODO should be checking for
        // UserUtilities.hasPermission(permissions, issue.getProject()
        //             .getId(), PermissionType.ISSUE_VIEW_USERS)
        if (isUsersIssue) {
            if (log.isDebugEnabled()) {
                log.debug("canViewIssue: issue: " + issue + ", user: "
                        + userId + ", permission: isUsersIssue");
            }
            return true;
        }


        if (log.isDebugEnabled()) {
            log.debug("canViewIssue: issue: " + issue + ", user: " + userId
                    + ", permission: none matched");
        }
        return false;
    }

    /**
     * Returns true if the user has permission to edit the requested issue.
     *
     * @param issue       an IssueModel of the issue to check edit permission for
     * @param userId      the userId of the user to check permission for
     * @param permissions a HashMap of the users permissions
     */
    @Deprecated
    public static boolean canEditIssue(Issue issue, Integer userId,
                                       Map<Integer, Set<PermissionType>> permissions) {
        if (issue == null || userId == null || permissions == null) {
            if (log.isInfoEnabled()) {
                log.info("canEditIssue: missing argument. issue: " + issue
                        + ", userid: " + userId + ", permissions: "
                        + permissions);
            }
            return false;
        }


        if (UserUtilities.hasPermission(permissions,
                issue.getProject().getId(), PermissionType.ISSUE_EDIT_ALL)) {

            if (log.isDebugEnabled()) {
                log.debug("canEditIssue: user " + userId
                        + " has permission to edit issue " + issue.getId()
                        + ":" + PermissionType.ISSUE_EDIT_ALL);
            }
            return true;
        }
        if (!UserUtilities.hasPermission(permissions, issue.getProject()
                .getId(), PermissionType.ISSUE_EDIT_USERS)) {
            if (log.isDebugEnabled()) {
                log.debug("canEditIssue: user " + userId
                        + " has not permission  to edit issue " + issue.getId()
                        + ":" + PermissionType.ISSUE_EDIT_USERS);
            }
            return false;
        }

        if (issue.getCreator().getId().equals(userId)) {
            if (log.isDebugEnabled()) {
                log.debug("canEditIssue: user " + userId
                        + " is creator of issue " + issue.getId() + ":");
            }
            return true;
        }
        if (issue.getOwner() != null) {
            if (issue.getOwner().getId().equals(userId)) {
                if (log.isDebugEnabled()) {
                    log.debug("canEditIssue: user " + userId
                            + " is owner of issue " + issue.getId() + ":");
                }
                return true;
            }
        }

        if (log.isDebugEnabled()) {
            log.debug("canEditIssue: user " + userId
                    + " could not match permission, denied");
        }
        return false;
    }

    /**
     * Returns true if the user can be assigned to this issue.
     *
     * @param issue       an IssueModel of the issue to check assign permission for
     * @param userId      the userId of the user to check permission for
     * @param permissions a HashMap of the users permissions
     */
    @Deprecated
    public static boolean canBeAssignedIssue(Issue issue, Integer userId,
                                             Map<Integer, Set<PermissionType>> permissions) {
        if (issue == null || userId == null || permissions == null) {
            return false;
        }

        if (UserUtilities.hasPermission(permissions,
                issue.getProject().getId(), PermissionType.ISSUE_EDIT_ALL)) {
            return true;
        }
        if (UserUtilities.hasPermission(permissions,
                issue.getProject().getId(), PermissionType.ISSUE_EDIT_USERS)) {
            if (issue.getCreator().getId().equals(userId)) {
                return true;
            } else if (UserUtilities.hasPermission(permissions, issue
                    .getProject().getId(), PermissionType.ISSUE_ASSIGNABLE)) {
                return true;
            } else if (issue.getOwner().getId() != null
                    && issue.getOwner().getId().equals(userId)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Returns true if the user can unassign themselves from the issue.
     *
     * @param issue       an IssueModel of the issue to check assign permission for
     * @param userId      the userId of the user to check permission for
     * @param permissions a HashMap of the users permissions
     */
    public static boolean canUnassignIssue(Issue issue, Integer userId,
                                           Map<Integer, Set<PermissionType>> permissions) {
        if (issue == null || userId == null || permissions == null) {
            return false;
        }

        if (UserUtilities.hasPermission(permissions,
                issue.getProject().getId(), PermissionType.ISSUE_ASSIGN_OTHERS)) {
            return true;
        }
        return issue.getOwner() != null
                && userId.equals(issue.getOwner().getId())
                && UserUtilities.hasPermission(permissions, issue.getProject()
                .getId(), PermissionType.ISSUE_UNASSIGN_SELF);

    }

    public static boolean hasIssueRelation(Issue issue, Integer relatedIssueId) {
        if (issue != null) {
            List<IssueRelation> relations = issue.getRelations();
            for (IssueRelation relation : relations) {
                if (relation.getRelatedIssue().getId().equals(
                        relatedIssueId)) {
                    return true;
                }
            }
        }
        return false;
    }

    public static boolean hasIssueNotification(Issue issue, Integer userId) {
        return hasIssueNotification(issue, issue.getProject(), userId);
    }

    public static boolean hasHardNotification(Issue issue, Project project, Integer userId) {
        if (issue == null || userId == null) {
            return false;
        }


        if ((issue.getOwner() != null && issue.getOwner().getId().equals(userId))
                || issue.getCreator().getId().equals(userId)) {
            return true;
        }

        if (project != null && project.getOwners() != null) {
            for (User user : project.getOwners()) {
                if (user.getId().equals(userId)) {
                    return true;
                }
            }
        }
        Collection<Notification> notifications = issue.getNotifications();
        if (notifications != null) {
            for (Notification notification : notifications) {
                if (notification.getUser().getId().equals(userId) && notification.getRole() != Notification.Role.IP) {
                    return true;
                }
            }
        }
        return false;
    }
    /**
     * Evaluate if a certain user is notified on issue change.
     * <p/>
     * FIXME: Does not work for admin of unassigned-issue-projects owner, see portalhome.do
     */
    public static boolean hasIssueNotification(Issue issue, Project project,
                                               Integer userId) {
        if (issue == null || userId == null) {
            return false;
        }

        if (hasHardNotification(issue, project, userId)) {
            return true;
        }

        Collection<Notification> notifications = issue.getNotifications();
        if (notifications != null) {
            for (Notification notification : notifications) {
                if (notification.getUser().getId().equals(userId)) {
                    return true;
                }
            }
        }

        return false;
    }

    public static URL getIssueURL(Issue issue, String baseURL) throws MalformedURLException {
        return getIssueURL(issue, new URL(baseURL + (StringUtils.endsWith(baseURL, "/") ? "" : "/")
        ));
    }

    public static URL getIssueURL(Issue issue, URL base) {
        try {
            if (null != base && null != issue)
                return new URL(base, "module-projects/view_issue.do?id=" + issue.getId());
        } catch (MalformedURLException e) {
            log.error("could not create URL", e);
        }
        return null;
    }
}