org.itracker.model.util.IssueUtilities.java Source code

Java tutorial

Introduction

Here is the source code for org.itracker.model.util.IssueUtilities.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.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;
    }
}