IssueServiceImpl.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.services.implementations;

import org.apache.log4j.Logger;
import org.itracker.IssueSearchException;
import org.itracker.ProjectException;
import org.itracker.core.resources.ITrackerResources;
import org.itracker.model.*;
import org.itracker.model.Notification.Role;
import org.itracker.model.Notification.Type;
import org.itracker.model.util.IssueUtilities;
import org.itracker.persistence.dao.*;
import org.itracker.services.ConfigurationService;
import org.itracker.services.IssueService;
import org.itracker.services.NotificationService;

import java.util.*;

/**
 * Issue related service layer. A bit "fat" at this time, because of being a* direct EJB porting. Going go get thinner over time** @author ricardo
 * */

//TODO: Cleanup this file, go through all issues, todos, etc.

public class IssueServiceImpl implements IssueService {

    private static final Logger logger = Logger
            .getLogger(IssueServiceImpl.class);
    private ConfigurationService configurationService;

    private CustomFieldDAO customFieldDAO;

    private ProjectDAO projectDAO;

    private IssueDAO issueDAO;

    private IssueHistoryDAO issueHistoryDAO;

    private IssueRelationDAO issueRelationDAO;

    private IssueAttachmentDAO issueAttachmentDAO;

    private ComponentDAO componentDAO;

    private IssueActivityDAO issueActivityDAO;

    private VersionDAO versionDAO;

    private NotificationService notificationService;
    private UserDAO userDAO;

    public IssueServiceImpl() {

    }

    public Issue getIssue(Integer issueId) {
        Issue issue = getIssueDAO().findByPrimaryKey(issueId);
        return issue;
    }

    /**
     * @deprecated don't use to expensive memory use!
     */
    public List<Issue> getAllIssues() {
        logger.warn("getAllIssues: use of deprecated API");
        if (logger.isDebugEnabled()) {
            logger
                    .debug("getAllIssues: stacktrace was",
                            new RuntimeException());
        }
        return getIssueDAO().findAll();
    }

    /**
     * Added implementation to make proper count of ALL issues, instead select
     * them in a list and return its size
     */
    public Long getNumberIssues() {
        return getIssueDAO().countAllIssues();
    }

    public List<Issue> getIssuesCreatedByUser(Integer userId) {
        return getIssuesCreatedByUser(userId, true);
    }

    public List<Issue> getIssuesCreatedByUser(Integer userId,
                                              boolean availableProjectsOnly) {
        final List<Issue> issues;

        if (availableProjectsOnly) {
            issues = getIssueDAO().findByCreatorInAvailableProjects(userId,
                    IssueUtilities.STATUS_CLOSED);
        } else {
            issues = getIssueDAO().findByCreator(userId,
                    IssueUtilities.STATUS_CLOSED);
        }
        return issues;
    }

    public List<Issue> getIssuesOwnedByUser(Integer userId) {

        return getIssuesOwnedByUser(userId, true);

    }

    public List<Issue> getIssuesOwnedByUser(Integer userId,
                                            boolean availableProjectsOnly) {
        final List<Issue> issues;

        if (availableProjectsOnly) {
            issues = getIssueDAO().findByOwnerInAvailableProjects(userId,
                    IssueUtilities.STATUS_RESOLVED);
        } else {
            issues = getIssueDAO().findByOwner(userId,
                    IssueUtilities.STATUS_RESOLVED);
        }
        return issues;
    }

    public List<Issue> getIssuesWatchedByUser(Integer userId) {
        return getIssuesWatchedByUser(userId, true);
    }

    /**
     * TODO move to {@link NotificationService}
     */
    public List<Issue> getIssuesWatchedByUser(Integer userId,
                                              boolean availableProjectsOnly) {
        final List<Issue> issues;

        if (availableProjectsOnly) {
            issues = getIssueDAO().findByNotificationInAvailableProjects(
                    userId, IssueUtilities.STATUS_CLOSED);
        } else {
            issues = getIssueDAO().findByNotification(userId,
                    IssueUtilities.STATUS_CLOSED);
        }
        return issues;
    }

    public List<Issue> getUnassignedIssues() {
        return getUnassignedIssues(true);
    }

    public List<Issue> getUnassignedIssues(boolean availableProjectsOnly) {
        final List<Issue> issues;

        if (availableProjectsOnly) {
            issues = getIssueDAO()
                    .findByStatusLessThanEqualToInAvailableProjects(
                            IssueUtilities.STATUS_UNASSIGNED);
        } else {
            issues = getIssueDAO().findByStatusLessThanEqualTo(
                    IssueUtilities.STATUS_UNASSIGNED);
        }
        return issues;
    }

    /**
     * Returns all issues with a status equal to the given status number
     *
     * @param status the status to compare
     * @return an array of IssueModels that match the criteria
     */

    public List<Issue> getIssuesWithStatus(int status) {
        List<Issue> issues = getIssueDAO().findByStatus(status);
        return issues;
    }

    /**
     * Returns all issues with a status less than the given status number
     *
     * @param status the status to compare
     * @return an array of IssueModels that match the criteria
     */

    public List<Issue> getIssuesWithStatusLessThan(int status) {
        List<Issue> issues = getIssueDAO().findByStatusLessThan(status);
        return issues;
    }

    /**
     * Returns all issues with a severity equal to the given severity number
     *
     * @param severity the severity to compare
     * @return an array of IssueModels that match the criteria
     */

    public List<Issue> getIssuesWithSeverity(int severity) {
        List<Issue> issues = getIssueDAO().findBySeverity(severity);
        return issues;

    }

    public List<Issue> getIssuesByProjectId(Integer projectId) {
        return getIssuesByProjectId(projectId, IssueUtilities.STATUS_END);
    }

    public List<Issue> getIssuesByProjectId(Integer projectId, int status) {
        List<Issue> issues = getIssueDAO().findByProjectAndLowerStatus(
                projectId, status);
        return issues;
    }

    public User getIssueCreator(Integer issueId) {
        Issue issue = getIssueDAO().findByPrimaryKey(issueId);
        User user = issue.getCreator();
        return user;

    }

    public User getIssueOwner(Integer issueId) {
        Issue issue = getIssueDAO().findByPrimaryKey(issueId);
        User user = issue.getOwner();

        return user;

    }

    public List<Component> getIssueComponents(Integer issueId) {
        Issue issue = getIssueDAO().findByPrimaryKey(issueId);
        List<Component> components = issue.getComponents();

        return components;
    }

    public List<Version> getIssueVersions(Integer issueId) {
        Issue issue = getIssueDAO().findByPrimaryKey(issueId);

        List<Version> versions = issue.getVersions();
        return versions;
    }

    public List<IssueAttachment> getIssueAttachments(Integer issueId) {
        Issue issue = getIssueDAO().findByPrimaryKey(issueId);

        List<IssueAttachment> attachments = issue.getAttachments();
        return attachments;
    }

    /**
     * Old implementation is left here, commented, because it checked for
     * history entry status. This feature was not finished, I think (RJST)
     */
    public List<IssueHistory> getIssueHistory(Integer issueId) {
        return getIssueDAO().findByPrimaryKey(issueId).getHistory();
    }

    public Issue createIssue(Issue issue, Integer projectId, Integer userId,
                             Integer createdById) throws ProjectException {
        Project project = getProjectDAO().findByPrimaryKey(projectId);
        User creator = getUserDAO().findByPrimaryKey(userId);

        if (project.getStatus() != Status.ACTIVE) {
            throw new ProjectException("Project is not active.");
        }

        IssueActivity activity = new IssueActivity(issue, creator,
                IssueActivityType.ISSUE_CREATED);
        activity.setDescription(ITrackerResources
                .getString("itracker.activity.system.createdfor")
                + " " + creator.getFirstName() + " " + creator.getLastName());

        activity.setIssue(issue);

        if (!(createdById == null || createdById.equals(userId))) {

            User createdBy = getUserDAO().findByPrimaryKey(createdById);
            activity.setUser(createdBy);

            Notification watchModel = new Notification();

            watchModel.setUser(createdBy);

            watchModel.setIssue(issue);

            watchModel.setRole(Notification.Role.CONTRIBUTER);

            issue.getNotifications().add(watchModel);

        }

        List<IssueActivity> activities = new ArrayList<IssueActivity>();
        activities.add(activity);
        issue.setActivities(activities);

        issue.setProject(project);

        issue.setCreator(creator);

        // save
        getIssueDAO().save(issue);

        return issue;
    }

    /**
     * Save a modified issue to the persistence layer
     *
     * @param issueDirty the changed, unsaved issue to update on persistency layer
     * @param userId     the user-id of the changer
     */
    public Issue updateIssue(final Issue issueDirty, final Integer userId)
            throws ProjectException {

        String existingTargetVersion = null;

        // detach the modified Issue form the Hibernate Session
        getIssueDAO().detach(issueDirty);
        // Retrieve the Issue from Hibernate Session and refresh it from
        // Hibernate Session to previous state.
        Issue persistedIssue = getIssueDAO().findByPrimaryKey(
                issueDirty.getId());

        getIssueDAO().refresh(persistedIssue);
        if (logger.isDebugEnabled()) {
            logger.debug("updateIssue: updating issue " + issueDirty
                    + "\n(from " + persistedIssue + ")");
        }

        User user = getUserDAO().findByPrimaryKey(userId);

        if (persistedIssue.getProject().getStatus() != Status.ACTIVE) {
            throw new ProjectException("Project "
                    + persistedIssue.getProject().getName() + " is not active.");
        }

        if (!persistedIssue.getDescription().equalsIgnoreCase(
                issueDirty.getDescription())) {

            if (logger.isDebugEnabled()) {
                logger.debug("updateIssue: updating description from "
                        + persistedIssue.getDescription());
            }
            IssueActivity activity = new IssueActivity();
            activity.setActivityType(IssueActivityType.DESCRIPTION_CHANGE);
            activity.setDescription(ITrackerResources
                    .getString("itracker.web.generic.from")
                    + ": " + persistedIssue.getDescription());
            activity.setUser(user);
            activity.setIssue(issueDirty);
            issueDirty.getActivities().add(activity);

        }

        if (persistedIssue.getResolution() != null
                && !persistedIssue.getResolution().equalsIgnoreCase(
                issueDirty.getResolution())) {

            IssueActivity activity = new IssueActivity();
            activity.setActivityType(IssueActivityType.RESOLUTION_CHANGE);
            activity.setDescription(ITrackerResources
                    .getString("itracker.web.generic.from")
                    + ": " + persistedIssue.getResolution());
            activity.setUser(user);
            activity.setIssue(issueDirty);
            issueDirty.getActivities().add(activity);
        }

        if (null == persistedIssue.getStatus()
                || !persistedIssue.getStatus().equals(issueDirty.getStatus())) {
            IssueActivity activity = new IssueActivity();
            activity.setActivityType(IssueActivityType.STATUS_CHANGE);
            activity.setDescription(IssueUtilities.getStatusName(persistedIssue
                    .getStatus())
                    + " "
                    + ITrackerResources.getString("itracker.web.generic.to")
                    + " "
                    + IssueUtilities.getStatusName(issueDirty.getStatus()));
            activity.setUser(user);
            activity.setIssue(issueDirty);
            issueDirty.getActivities().add(activity);
        }

        if (issueDirty.getSeverity() != null
                && !issueDirty.getSeverity().equals(
                persistedIssue.getSeverity())
                && issueDirty.getSeverity() != -1) {

            IssueActivity activity = new IssueActivity();
            activity.setActivityType(IssueActivityType.SEVERITY_CHANGE);
            // FIXME why does it state Critical to Critical when it should Major to Critical!?
            activity.setDescription(IssueUtilities
                    .getSeverityName(persistedIssue.getSeverity())
                    + " "
                    + ITrackerResources.getString("itracker.web.generic.to")
                    + " "
                    + IssueUtilities.getSeverityName(issueDirty.getSeverity()));

            activity.setUser(user);
            activity.setIssue(issueDirty);
            issueDirty.getActivities().add(activity);
        }

        if (persistedIssue.getTargetVersion() != null
                && issueDirty.getTargetVersion() != null
                && !persistedIssue.getTargetVersion().getId().equals(
                issueDirty.getTargetVersion().getId())) {
            existingTargetVersion = persistedIssue.getTargetVersion()
                    .getNumber();
            Version version = this.getVersionDAO().findByPrimaryKey(
                    issueDirty.getTargetVersion().getId());

            IssueActivity activity = new IssueActivity();
            activity.setActivityType(IssueActivityType.TARGETVERSION_CHANGE);
            String description = existingTargetVersion + " "
                    + ITrackerResources.getString("itracker.web.generic.to")
                    + " ";
            description += version.getNumber();
            activity.setDescription(description);
            activity.setUser(user);
            activity.setIssue(issueDirty);
            issueDirty.getActivities().add(activity);
        }

        // (re-)assign issue
        User newOwner = issueDirty.getOwner();
        issueDirty.setOwner(persistedIssue.getOwner());
        if (logger.isDebugEnabled()) {
            logger.debug("updateIssue: assigning from " + issueDirty.getOwner()
                    + " to " + newOwner);
        }
        assignIssue(issueDirty, newOwner, user, false);
        if (logger.isDebugEnabled()) {
            logger.debug("updateIssue: updated assignment: " + issueDirty);
        }

        if (logger.isDebugEnabled()) {
            logger.debug("updateIssue: merging issue " + issueDirty + " to "
                    + persistedIssue);
        }

        persistedIssue = getIssueDAO().merge(issueDirty);

        if (logger.isDebugEnabled()) {
            logger.debug("updateIssue: merged issue for saving: "
                    + persistedIssue);
        }
        getIssueDAO().saveOrUpdate(persistedIssue);
        if (logger.isDebugEnabled()) {
            logger.debug("updateIssue: saved issue: " + persistedIssue);
        }
        return persistedIssue;
    }

    /**
     * Moves an issues from its current project to a new project.
     *
     * @param issue     an Issue of the issue to move
     * @param projectId the id of the target project
     * @param userId    the id of the user that is moving the issue
     * @return an Issue of the issue after it has been moved
     */

    public Issue moveIssue(Issue issue, Integer projectId, Integer userId) {

        if (logger.isDebugEnabled()) {
            logger.debug("moveIssue: " + issue + " to project#" + projectId
                    + ", user#" + userId);
        }

        Project project = getProjectDAO().findByPrimaryKey(projectId);
        User user = getUserDAO().findByPrimaryKey(userId);

        if (logger.isDebugEnabled()) {
            logger.debug("moveIssue: " + issue + " to project: " + project
                    + ", user: " + user);
        }

        IssueActivity activity = new IssueActivity();
        activity
                .setActivityType(org.itracker.model.IssueActivityType.ISSUE_MOVE);
        activity.setDescription(issue.getProject().getName() + " "
                + ITrackerResources.getString("itracker.web.generic.to") + " "
                + project.getName());
        activity.setUser(user);
        activity.setIssue(issue);
        issue.setProject(project);

        issue.getActivities().add(activity);

        if (logger.isDebugEnabled()) {
            logger.debug("moveIssue: updated issue: " + issue);
        }
        try {
            getIssueDAO().saveOrUpdate(issue);
        } catch (Exception e) {
            logger.error("moveIssue: failed to save issue: " + issue, e);
            return null;
        }
        if (logger.isDebugEnabled()) {
            logger.debug("moveIssue: saved move-issue to " + project);
        }
        return issue;

    }

    /**
     * this should not exist. adding an history entry should be adding the
     * history entry to the domain object and saving the object...
     */
    public boolean addIssueHistory(IssueHistory history) {
        getIssueHistoryDAO().saveOrUpdate(history);
        history.getIssue().getHistory().add(history);
        getIssueDAO().saveOrUpdate(history.getIssue());
        return true;
    }

    /**
     * TODO maybe it has no use at all. is it obsolete? when I'd set the
     * issue-fields on an issue and then save/update issue, would it be good
     * enough?
     */
    public boolean setIssueFields(Integer issueId, List<IssueField> fields) {
        Issue issue = getIssueDAO().findByPrimaryKey(issueId);

        setIssueFields(issue, fields, true);

        return true;
    }

    private boolean setIssueFields(Issue issue, List<IssueField> fields,
                                   boolean save) {

        List<IssueField> issueFields = issue.getFields();

        if (fields.size() > 0) {
            for (int i = 0; i < fields.size(); i++) {

                IssueField field = fields.get(i);
                if (issueFields.contains(field)) {
                    issueFields.remove(field);
                }

                CustomField customField = getCustomFieldDAO().findByPrimaryKey(
                        fields.get(i).getCustomField().getId());
                field.setCustomField(customField);
                field.setIssue(issue);

                issueFields.add(field);
            }
        }
        issue.setFields(issueFields);

        if (save) {
            logger.debug("setIssueFields: save was true");
            getIssueDAO().saveOrUpdate(issue);
        }
        return true;
    }

    public boolean setIssueComponents(Integer issueId,
                                      HashSet<Integer> componentIds, Integer userId) {

        Issue issue = getIssueDAO().findByPrimaryKey(issueId);
        List<Component> components = new ArrayList<Component>(componentIds
                .size());
        User user = userDAO.findByPrimaryKey(userId);
        Iterator<Integer> idIt = componentIds.iterator();
        while (idIt.hasNext()) {
            Integer id = (Integer) idIt.next();
            Component c = getComponentDAO().findById(id);
            components.add(c);
        }

        setIssueComponents(issue, components, user, true);
        return true;
    }

    private boolean setIssueComponents(Issue issue, List<Component> components,
                                       User user, boolean save) {

        if (issue.getComponents() == null) {
            if (logger.isInfoEnabled()) {
                logger.info("setIssueComponents: components was null");
            }
            issue.setComponents(new ArrayList<Component>(components.size()));
        }
        if (components.isEmpty() && !issue.getComponents().isEmpty()) {
            addComponentsModifiedActivity(issue, user, new StringBuilder(
                    ITrackerResources.getString("itracker.web.generic.all"))
                    .append(" ").append(
                            ITrackerResources
                                    .getString("itracker.web.generic.removed"))
                    .toString());
            issue.getComponents().clear();
        } else {
            Collections.sort(issue.getComponents(), Component.NAME_COMPARATOR);

            for (Iterator<Component> iterator = issue.getComponents()
                    .iterator(); iterator.hasNext(); ) {
                Component component = (Component) iterator.next();
                if (components.contains(component)) {
                    components.remove(component);
                } else {
                    addComponentsModifiedActivity(issue, user,
                            new StringBuilder(ITrackerResources
                                    .getString("itracker.web.generic.removed"))
                                    .append(": ").append(component.getName())
                                    .toString());
                    iterator.remove();
                }
            }
            Collections.sort(components, Component.NAME_COMPARATOR);
            for (Iterator<Component> iterator = components.iterator(); iterator
                    .hasNext(); ) {

                Component component = iterator.next();
                if (!issue.getComponents().contains(component)) {
                    addComponentsModifiedActivity(issue, user,
                            new StringBuilder(ITrackerResources
                                    .getString("itracker.web.generic.added"))
                                    .append(": ").append(component.getName())
                                    .toString());
                    issue.getComponents().add(component);
                }
            }
        }

        if (save) {
            if (logger.isDebugEnabled()) {
                logger.debug("setIssueComponents: save was true");
            }
            getIssueDAO().saveOrUpdate(issue);
        }
        return true;

    }

    /**
     * used by setIssueComponents for adding change activities
     */
    private void addComponentsModifiedActivity(Issue issue, User user,
                                               String description) {
        IssueActivity activity = new IssueActivity();
        activity
                .setActivityType(org.itracker.model.IssueActivityType.COMPONENTS_MODIFIED);
        activity.setDescription(description);
        activity.setIssue(issue);
        activity.setUser(user);
        issue.getActivities().add(activity);
    }

    private boolean setIssueVersions(Issue issue, List<Version> versions,
                                     User user, boolean save) {

        if (issue.getVersions() == null) {
            if (logger.isInfoEnabled()) {
                logger.info("setIssueVersions: versions were null!");
            }
            issue.setVersions(new ArrayList<Version>());
        }

        if (versions.isEmpty() && !issue.getVersions().isEmpty()) {

            addVersionsModifiedActivity(issue, user, new StringBuilder(
                    ITrackerResources.getString("itracker.web.generic.all"))
                    .append(" ").append(
                            ITrackerResources
                                    .getString("itracker.web.generic.removed"))
                    .toString());
            issue.getVersions().clear();
        } else {

            Collections.sort(issue.getVersions(), Version.VERSION_COMPARATOR);

            StringBuilder changesBuf = new StringBuilder();
            for (Iterator<Version> iterator = issue.getVersions().iterator(); iterator
                    .hasNext(); ) {

                Version version = iterator.next();
                if (versions.contains(version)) {
                    versions.remove(version);
                } else {
                    if (changesBuf.length() > 0) {
                        changesBuf.append(", ");
                    }
                    changesBuf.append(version.getNumber());
                    iterator.remove();
                }
            }

            if (changesBuf.length() > 0) {
                addVersionsModifiedActivity(issue, user, new StringBuilder(
                        ITrackerResources
                                .getString("itracker.web.generic.removed"))
                        .append(": ").append(changesBuf).toString());
            }

            changesBuf = new StringBuilder();

            Collections.sort(versions, Version.VERSION_COMPARATOR);
            for (Iterator<Version> iterator = versions.iterator(); iterator
                    .hasNext(); ) {

                Version version = iterator.next();
                if (changesBuf.length() > 0) {
                    changesBuf.append(", ");
                }
                changesBuf.append(version.getNumber());
                issue.getVersions().add(version);
            }
            if (changesBuf.length() > 0) {
                addVersionsModifiedActivity(issue, user, new StringBuilder(
                        ITrackerResources
                                .getString("itracker.web.generic.added"))
                        .append(": ").append(changesBuf).toString());
            }
        }
        if (save) {
            if (logger.isDebugEnabled()) {
                logger.debug("setIssueVersions: updating issue: " + issue);
            }
            getIssueDAO().saveOrUpdate(issue);
        }

        return true;
    }

    /**
     * used by setIssueComponents for adding change activities
     */
    private void addVersionsModifiedActivity(Issue issue, User user,
                                             String description) {
        IssueActivity activity = new IssueActivity();
        activity
                .setActivityType(org.itracker.model.IssueActivityType.TARGETVERSION_CHANGE);
        activity.setDescription(description);
        activity.setIssue(issue);
        activity.setUser(user);
        issue.getActivities().add(activity);
    }

    public boolean setIssueVersions(Integer issueId,
                                    HashSet<Integer> versionIds, Integer userId) {

        Issue issue = getIssueDAO().findByPrimaryKey(issueId);
        User user = userDAO.findByPrimaryKey(userId);
        // load versions from ids
        ArrayList<Version> versions = new ArrayList<Version>(versionIds.size());
        Iterator<Integer> versionsIdIt = versionIds.iterator();
        while (versionsIdIt.hasNext()) {
            Integer id = versionsIdIt.next();
            versions.add(getVersionDAO().findByPrimaryKey(id));
        }

        return setIssueVersions(issue, versions, user, true);
    }

    public IssueRelation getIssueRelation(Integer relationId) {

        IssueRelation issueRelation = getIssueRelationDAO().findByPrimaryKey(
                relationId);

        return issueRelation;

    }

    /**
     * add a relation between two issues.
     * <p/>
     * TODO: There is no relation saved to database yet?
     */
    public boolean addIssueRelation(Integer issueId, Integer relatedIssueId,
                                    IssueRelation.Type relationType, Integer userId) {

        User user = getUserDAO().findByPrimaryKey(userId);

        if (null == user) {
            throw new IllegalArgumentException("Invalid user-id: " + userId);
        }

        if (issueId != null && relatedIssueId != null) {

            IssueRelation.Type matchingRelationType = IssueUtilities
                    .getMatchingRelationType(relationType);

            // if(matchingRelationType < 0) {

            // throw new CreateException("Unable to find matching relation type

            // for type: " + relationType);

            // }

            Issue issue = getIssueDAO().findByPrimaryKey(issueId);

            Issue relatedIssue = getIssueDAO().findByPrimaryKey(relatedIssueId);

            IssueRelation relationA = new IssueRelation();

            relationA.setRelationType(relationType);

            // relationA.setMatchingRelationId(relationBId);

            relationA.setIssue(issue);

            relationA.setRelatedIssue(relatedIssue);

            // set to 0 first, later reassign to relationB.id
            relationA.setMatchingRelationId(0);

            relationA.setLastModifiedDate(new java.sql.Timestamp(new Date()
                    .getTime()));

            getIssueRelationDAO().saveOrUpdate(relationA);

            IssueRelation relationB = new IssueRelation();

            relationB.setRelationType(matchingRelationType);

            // relationB.setMatchingRelationId(relationAId);

            relationB.setIssue(relatedIssue);

            relationB.setRelatedIssue(issue);

            relationB.setMatchingRelationId(relationA.getId());

            relationB.setLastModifiedDate(new java.sql.Timestamp(new Date()
                    .getTime()));

            getIssueRelationDAO().saveOrUpdate(relationB);

            relationA.setMatchingRelationId(relationB.getId());
            getIssueRelationDAO().saveOrUpdate(relationA);

            IssueActivity activity = new IssueActivity();
            activity
                    .setActivityType(org.itracker.model.IssueActivityType.RELATION_ADDED);
            activity.setDescription(ITrackerResources.getString(
                    "itracker.activity.relation.add", new Object[]{
                    IssueUtilities.getRelationName(relationType),
                    relatedIssueId}));

            activity.setIssue(issue);
            issue.getActivities().add(activity);
            // need to set user here
            activity.setUser(user);
            // need to save here
            getIssueDAO().saveOrUpdate(issue);

            activity = new IssueActivity();
            activity
                    .setActivityType(org.itracker.model.IssueActivityType.RELATION_ADDED);
            activity.setDescription(ITrackerResources.getString(
                    "itracker.activity.relation.add", new Object[]{
                    IssueUtilities
                            .getRelationName(matchingRelationType),
                    issueId}));
            activity.setIssue(relatedIssue);
            activity.setUser(user);
            relatedIssue.getActivities().add(activity);
            getIssueDAO().saveOrUpdate(relatedIssue);
            return true;

        }

        return false;

    }

    public void removeIssueRelation(Integer relationId, Integer userId) {
        IssueRelation issueRelation = getIssueRelationDAO().findByPrimaryKey(
                relationId);
        Integer issueId = issueRelation.getIssue().getId();

        Integer relatedIssueId = issueRelation.getRelatedIssue().getId();

        Integer matchingRelationId = issueRelation.getMatchingRelationId();

        if (matchingRelationId != null) {
            IssueActivity activity = new IssueActivity();
            activity
                    .setActivityType(org.itracker.model.IssueActivityType.RELATION_REMOVED);
            activity.setDescription(ITrackerResources.getString(
                    "itracker.activity.relation.removed", issueId.toString()));
            // FIXME need to fix the commented code and save
            // activity.setIssue(relatedIssueId);
            // activity.setUser(userId);
            // IssueRelationDAO.remove(matchingRelationId);
        }

        IssueActivity activity = new IssueActivity();
        activity
                .setActivityType(org.itracker.model.IssueActivityType.RELATION_REMOVED);
        activity.setDescription(ITrackerResources
                .getString("itracker.activity.relation.removed", relatedIssueId
                        .toString()));
        // activity.setIssue(issueId);
        // activity.setUser(userId);
        // irHome.remove(relationId);
        // need to save

        getIssueRelationDAO().delete(issueRelation);
    }

    public boolean assignIssue(Integer issueId, Integer userId) {
        return assignIssue(issueId, userId, userId);
    }

    /**
     * only use for updating issue from actions..
     */
    public boolean assignIssue(Integer issueId, Integer userId,
                               Integer assignedByUserId) {

        return assignIssue(getIssueDAO().findByPrimaryKey(issueId),
                getUserDAO().findByPrimaryKey(userId), getUserDAO()
                .findByPrimaryKey(assignedByUserId), true);
    }

    /**
     * Only for use
     *
     * @param save save issue and send notification
     */
    private boolean assignIssue(Issue issue, User user, User assignedByUser,
                                final boolean save) {

        if (issue.getOwner() == user
                || (null != issue.getOwner() && issue.getOwner().equals(user))) {
            // nothing to do.
            if (logger.isDebugEnabled()) {
                logger.debug("assignIssue: attempted to reassign " + issue
                        + " to current owner " + user);
            }
            return false;
        }

        if (null == user) {
            if (logger.isInfoEnabled()) {
                logger.info("assignIssue: call to unasign " + issue);
            }

            return unassignIssue(issue, assignedByUser, save);
        }

        if (logger.isInfoEnabled()) {
            logger.info("assignIssue: assigning " + issue + " to " + user);
        }

        User currOwner = issue.getOwner();

        if (!user.equals(currOwner)) {
            if (currOwner != null
                    && !notificationService.hasIssueNotification(issue,
                    currOwner.getId(), Role.IP)) {
                // Notification notification = new Notification();
                Notification notification = new Notification(currOwner, issue,
                        Role.IP);
                if (save) {
                    notificationService.addIssueNotification(notification);
                } else {
                    issue.getNotifications().add(notification);
                }
            }

            IssueActivity activity = new IssueActivity();
            activity
                    .setActivityType(org.itracker.model.IssueActivityType.OWNER_CHANGE);
            activity.setDescription((currOwner == null ? "["
                    + ITrackerResources
                    .getString("itracker.web.generic.unassigned") + "]"
                    : currOwner.getLogin())
                    + " "
                    + ITrackerResources.getString("itracker.web.generic.to")
                    + " " + user.getLogin());
            activity.setUser(assignedByUser);
            activity.setIssue(issue);
            issue.getActivities().add(activity);

            issue.setOwner(user);

            if (logger.isDebugEnabled()) {
                logger.debug("assignIssue: current status: "
                        + issue.getStatus());
            }
            if (issue.getStatus() < IssueUtilities.STATUS_ASSIGNED) {
                issue.setStatus(IssueUtilities.STATUS_ASSIGNED);
                if (logger.isDebugEnabled()) {
                    logger.debug("assignIssue: new status set to "
                            + issue.getStatus());
                }
            }

            // send assignment notification
            if (save) {
                if (logger.isDebugEnabled()) {
                    logger.debug("assignIssue: saving re-assigned issue");
                }
                getIssueDAO().saveOrUpdate(issue);
                notificationService.sendNotification(issue, Type.ASSIGNED,
                        getConfigurationService().getSystemBaseURL());

            }
        }
        return true;

    }

    /**
     * @param save save issue and send notification
     */
    private boolean unassignIssue(Issue issue, User unassignedByUser,
                                  boolean save) {
        if (logger.isDebugEnabled()) {
            logger.debug("unassignIssue: " + issue);
        }
        if (issue.getOwner() != null) {

            if (logger.isDebugEnabled()) {
                logger.debug("unassignIssue: unassigning from "
                        + issue.getOwner());
            }
            if (!notificationService.hasIssueNotification(issue, issue
                    .getOwner().getId(), Role.CONTRIBUTER)) {
                // Notification notification = new Notification();
                Notification notification = new Notification(issue.getOwner(),
                        issue, Role.CONTRIBUTER);
                if (save) {
                    notificationService.addIssueNotification(notification);
                } else {
                    issue.getNotifications().add(notification);
                }
            }
            IssueActivity activity = new IssueActivity(issue, unassignedByUser,
                    IssueActivityType.OWNER_CHANGE);
            activity
                    .setDescription(issue.getOwner().getLogin()
                            + " "
                            + ITrackerResources
                            .getString("itracker.web.generic.to")
                            + " ["
                            + ITrackerResources
                            .getString("itracker.web.generic.unassigned")
                            + "]");

            issue.setOwner(null);

            if (issue.getStatus() >= IssueUtilities.STATUS_ASSIGNED) {
                issue.setStatus(IssueUtilities.STATUS_UNASSIGNED);
            }
            if (save) {
                if (logger.isDebugEnabled()) {
                    logger.debug("unassignIssue: saving unassigned issue..");
                }
                getIssueDAO().saveOrUpdate(issue);
                notificationService.sendNotification(issue, Type.ASSIGNED,
                        getConfigurationService().getSystemBaseURL());
            }
        }

        return true;
    }

    /**
     * System-Update an issue, adds the action to the issue and updates the
     * issue
     */
    public Issue systemUpdateIssue(Issue updateissue, Integer userId)
            throws ProjectException {

        IssueActivity activity = new IssueActivity();
        activity.setActivityType(IssueActivityType.SYSTEM_UPDATE);
        activity.setDescription(ITrackerResources
                .getString("itracker.activity.system.status"));
        ArrayList<IssueActivity> activities = new ArrayList<IssueActivity>();

        activity.setIssue(updateissue);
        activity.setUser(getUserDAO().findByPrimaryKey(userId));
        updateissue.getActivities().add(activity);

        Issue updated = updateIssue(updateissue, userId);
        updated.getActivities().addAll(activities);
        getIssueDAO().saveOrUpdate(updated);

        return updated;
    }

    /*
      * public boolean addIssueActivity(IssueActivityModel model) {
      *
      * Issue issue = ifHome.findByPrimaryKey(model.getIssueId());
      *
      * User user = ufHome.findByPrimaryKey(model.getUserId());
      *
      * //return addIssueActivity(model, issue, user); return
      * addIssueActivity(null, issue, user); }
      */

    /*
      * public boolean addIssueActivity(IssueActivityModel model, Issue issue) {
      *
      * User user = ufHome.findByPrimaryKey(model.getUserId());
      *
      * return true;//addIssueActivity(model, issue, user); }
      */

    /**
     * I think this entire method is useless - RJST TODO move to
     * {@link NotificationService}
     */
    /*
      * public boolean addIssueActivity(IssueActivityBean model, Issue issue,
      * User user) {
      *
      * IssueActivityBean activity = new IssueActivityBean();
      *
      * //activity.setModel(model);
      *
      * activity.setIssue(issue);
      *
      * activity.setUser(user);
      *
      * return true; }
      */
    public void updateIssueActivityNotification(Integer issueId,
                                                boolean notificationSent) {

        if (issueId == null) {

            return;

        }

        Collection<IssueActivity> activity = getIssueActivityDAO()
                .findByIssueId(issueId);

        for (Iterator<IssueActivity> iter = activity.iterator(); iter.hasNext(); ) {

            ((IssueActivity) iter.next()).setNotificationSent(notificationSent);

        }

    }

    /**
     * Adds an attachment to an issue
     *
     * @param attachment The attachment data
     * @param data       The byte data
     */
    public boolean addIssueAttachment(IssueAttachment attachment, byte[] data) {
        Issue issue = attachment.getIssue();

        attachment.setFileName("attachment_issue_" + issue.getId() + "_"
                + attachment.getOriginalFileName());
        attachment.setFileData((data == null ? new byte[0] : data));

        // TODO: translate activity for adding attachments
        // IssueActivity activityAdd = new IssueActivity(issue,
        //         attachment.getUser(), IssueActivityType.ATTACHMENT_ADDED);
        // activityAdd.setDescription(attachment.getOriginalFileName());
        // issue.getActivities().add(activityAdd);

        if (logger.isDebugEnabled()) {
            logger.debug("addIssueAttachment: adding attachment " + attachment);
        }
        // add attachment to issue
        issue.getAttachments().add(attachment);
        if (logger.isDebugEnabled()) {
            logger.debug("addIssueAttachment: saving updated issue " + issue);
        }
        this.getIssueDAO().saveOrUpdate(issue);
        return true;
    }

    public boolean setIssueAttachmentData(Integer attachmentId, byte[] data) {

        if (attachmentId != null && data != null) {

            IssueAttachment attachment = getIssueAttachmentDAO()
                    .findByPrimaryKey(attachmentId);

            attachment.setFileData(data);

            return true;

        }

        return false;

    }

    public boolean setIssueAttachmentData(String fileName, byte[] data) {

        if (fileName != null && data != null) {

            IssueAttachment attachment = getIssueAttachmentDAO()
                    .findByFileName(fileName);

            attachment.setFileData(data);

            return true;

        }

        return false;

    }

    /**
     * Removes a attachement (deletes it)
     *
     * @param attachmentId the id of the <code>IssueAttachmentBean</code>
     */
    public boolean removeIssueAttachment(Integer attachmentId) {

        IssueAttachment attachementBean = this.getIssueAttachmentDAO()
                .findByPrimaryKey(attachmentId);

        getIssueAttachmentDAO().delete(attachementBean);

        return true;
    }

    public Integer removeIssueHistoryEntry(Integer entryId, Integer userId) {

        IssueHistory history = getIssueHistoryDAO().findByPrimaryKey(entryId);

        if (history != null) {

            history.setStatus(IssueUtilities.HISTORY_STATUS_REMOVED);

            IssueActivity activity = new IssueActivity();
            activity
                    .setActivityType(org.itracker.model.IssueActivityType.REMOVE_HISTORY);
            activity.setDescription(ITrackerResources
                    .getString("itracker.web.generic.entry")
                    + " "
                    + entryId
                    + " "
                    + ITrackerResources
                    .getString("itracker.web.generic.removed") + ".");

            getIssueHistoryDAO().delete(history);

            return history.getIssue().getId();

        }

        return Integer.valueOf(-1);

    }

    public Project getIssueProject(Integer issueId) {
        Issue issue = getIssueDAO().findByPrimaryKey(issueId);
        Project project = issue.getProject();

        return project;
    }

    public HashSet<Integer> getIssueComponentIds(Integer issueId) {

        HashSet<Integer> componentIds = new HashSet<Integer>();
        Issue issue = getIssueDAO().findByPrimaryKey(issueId);
        Collection<Component> components = issue.getComponents();

        for (Iterator<Component> iterator = components.iterator(); iterator
                .hasNext(); ) {
            componentIds.add(((Component) iterator.next()).getId());
        }

        return componentIds;

    }

    public HashSet<Integer> getIssueVersionIds(Integer issueId) {

        HashSet<Integer> versionIds = new HashSet<Integer>();

        Issue issue = getIssueDAO().findByPrimaryKey(issueId);

        Collection<Version> versions = issue.getVersions();

        for (Iterator<Version> iterator = versions.iterator(); iterator
                .hasNext(); ) {

            versionIds.add(((Version) iterator.next()).getId());

        }

        return versionIds;

    }

    public List<IssueActivity> getIssueActivity(Integer issueId) {

        int i = 0;

        Collection<IssueActivity> activity = getIssueActivityDAO()
                .findByIssueId(issueId);

        IssueActivity[] activityArray = new IssueActivity[activity.size()];

        for (Iterator<IssueActivity> iterator = activity.iterator(); iterator
                .hasNext(); i++) {

            activityArray[i] = ((IssueActivity) iterator.next());

        }

        return Arrays.asList(activityArray);

    }

    /**
     * TODO move to {@link NotificationService} ?
     */
    public List<IssueActivity> getIssueActivity(Integer issueId,
                                                boolean notificationSent) {

        int i = 0;

        Collection<IssueActivity> activity = getIssueActivityDAO()
                .findByIssueIdAndNotification(issueId, notificationSent);

        IssueActivity[] activityArray = new IssueActivity[activity.size()];

        for (Iterator<IssueActivity> iterator = activity.iterator(); iterator
                .hasNext(); i++) {

            activityArray[i] = ((IssueActivity) iterator.next());

        }

        return Arrays.asList(activityArray);

    }

    public Long getAllIssueAttachmentCount() {
        return getIssueAttachmentDAO().countAll().longValue();
    }

    public List<IssueAttachment> getAllIssueAttachments() {
        logger.warn("getAllIssueAttachments: use of deprecated API");
        if (logger.isDebugEnabled()) {
            logger.debug("getAllIssueAttachments: stacktrace was",
                    new RuntimeException());
        }

        List<IssueAttachment> attachments = getIssueAttachmentDAO().findAll();

        return attachments;
    }

    public IssueAttachment getIssueAttachment(Integer attachmentId) {
        IssueAttachment attachment = getIssueAttachmentDAO().findByPrimaryKey(
                attachmentId);

        return attachment;

    }

    public byte[] getIssueAttachmentData(Integer attachmentId) {

        byte[] data;

        IssueAttachment attachment = getIssueAttachmentDAO().findByPrimaryKey(
                attachmentId);

        data = attachment.getFileData();

        return data;

    }

    public int getIssueAttachmentCount(Integer issueId) {

        int i = 0;

        Issue issue = getIssueDAO().findByPrimaryKey(issueId);

        Collection<IssueAttachment> attachments = issue.getAttachments();

        i = attachments.size();

        return i;

    }

    /**
     * Returns the latest issue history entry for a particular issue.
     *
     * @param issueId the id of the issue to return the history entry for.
     * @return the latest IssueHistory, or null if no entries could be found
     */
    public IssueHistory getLastIssueHistory(Integer issueId) {

        List<IssueHistory> history = getIssueHistoryDAO()
                .findByIssueId(issueId);

        if (null != history && history.size() > 0) {
            // sort ascending by id
            Collections.sort(history, AbstractEntity.ID_COMPARATOR);
            // return last entry in list
            return history.get(history.size() - 1);
        }

        return null;

    }

    public int getOpenIssueCountByProjectId(Integer projectId) {

        Collection<Issue> issues = getIssueDAO().findByProjectAndLowerStatus(
                projectId, IssueUtilities.STATUS_RESOLVED);

        return issues.size();

    }

    public int getResolvedIssueCountByProjectId(Integer projectId) {

        Collection<Issue> issues = getIssueDAO().findByProjectAndHigherStatus(
                projectId, IssueUtilities.STATUS_RESOLVED);

        return issues.size();

    }

    public int getTotalIssueCountByProjectId(Integer projectId) {

        Collection<Issue> issues = getIssueDAO().findByProject(projectId);

        return issues.size();

    }

    public Date getLatestIssueDateByProjectId(Integer projectId) {

        return getIssueDAO().latestModificationDate(projectId);

    }

    public List<Issue> getNextIssues(Integer issueId) {
        return getIssueDAO().findNextIssues(issueId);
    }
    public List<Issue> getPreviousIssues(Integer issueId) {
        return getIssueDAO().findPreviousIssues(issueId);
    }

    public boolean canViewIssue(Integer issueId, User user) {

        Issue issue = getIssue(issueId);

        Map<Integer, Set<PermissionType>> permissions = getUserDAO()
                .getUsersMapOfProjectsAndPermissionTypes(user);

        return IssueUtilities.canViewIssue(issue, user.getId(), permissions);

    }

    public boolean canViewIssue(Issue issue, User user) {

        Map<Integer, Set<PermissionType>> permissions = getUserDAO()
                .getUsersMapOfProjectsAndPermissionTypes(user);

        return IssueUtilities.canViewIssue(issue, user.getId(), permissions);

    }

    private UserDAO getUserDAO() {
        return userDAO;
    }

    private IssueDAO getIssueDAO() {
        return issueDAO;
    }

    private ProjectDAO getProjectDAO() {
        return projectDAO;
    }

    private IssueActivityDAO getIssueActivityDAO() {
        return issueActivityDAO;
    }

    private VersionDAO getVersionDAO() {
        return this.versionDAO;
    }

    private ComponentDAO getComponentDAO() {
        return this.componentDAO;
    }

    private CustomFieldDAO getCustomFieldDAO() {
        return customFieldDAO;
    }

    private IssueHistoryDAO getIssueHistoryDAO() {
        return issueHistoryDAO;
    }

    private IssueRelationDAO getIssueRelationDAO() {
        return issueRelationDAO;
    }

    private IssueAttachmentDAO getIssueAttachmentDAO() {
        return issueAttachmentDAO;
    }

    /**
     * get total size of all attachments in database
     */
    public Long getAllIssueAttachmentSize() {

        return getIssueAttachmentDAO().totalAttachmentsSize().longValue() / 1024;

    }

    public List<Issue> searchIssues(IssueSearchQuery queryModel, User user,
                                    Map<Integer, Set<PermissionType>> userPermissions)
            throws IssueSearchException {
        return getIssueDAO().query(queryModel, user, userPermissions);
    }

    public Long totalSystemIssuesAttachmentSize() {
        return getIssueAttachmentDAO().totalAttachmentsSize();
    }

    public ConfigurationService getConfigurationService() {
        return configurationService;
    }

    public void setUserDAO(UserDAO userDAO) {
        this.userDAO = userDAO;
    }

    public void setConfigurationService(ConfigurationService configurationService) {
        this.configurationService = configurationService;
    }

    public void setCustomFieldDAO(CustomFieldDAO customFieldDAO) {
        this.customFieldDAO = customFieldDAO;
    }

    public void setProjectDAO(ProjectDAO projectDAO) {
        this.projectDAO = projectDAO;
    }

    public void setIssueDAO(IssueDAO issueDAO) {
        this.issueDAO = issueDAO;
    }

    public void setIssueHistoryDAO(IssueHistoryDAO issueHistoryDAO) {
        this.issueHistoryDAO = issueHistoryDAO;
    }

    public void setIssueRelationDAO(IssueRelationDAO issueRelationDAO) {
        this.issueRelationDAO = issueRelationDAO;
    }

    public void setIssueAttachmentDAO(IssueAttachmentDAO issueAttachmentDAO) {
        this.issueAttachmentDAO = issueAttachmentDAO;
    }

    public void setComponentDAO(ComponentDAO componentDAO) {
        this.componentDAO = componentDAO;
    }

    public void setIssueActivityDAO(IssueActivityDAO issueActivityDAO) {
        this.issueActivityDAO = issueActivityDAO;
    }

    public void setVersionDAO(VersionDAO versionDAO) {
        this.versionDAO = versionDAO;
    }

    public void setNotificationService(NotificationService notificationService) {
        this.notificationService = notificationService;
    }
}