org.headsupdev.agile.storage.resource.ResourceManagerImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.headsupdev.agile.storage.resource.ResourceManagerImpl.java

Source

/*
 * HeadsUp Agile
 * Copyright 2009-2012 Heads Up Development Ltd.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.headsupdev.agile.storage.resource;

import org.headsupdev.agile.api.Manager;
import org.headsupdev.agile.api.Project;
import org.headsupdev.agile.api.User;
import org.headsupdev.agile.api.logging.Logger;
import org.headsupdev.agile.storage.HibernateStorage;
import org.headsupdev.agile.storage.StoredProject;
import org.headsupdev.agile.storage.issues.*;
import org.headsupdev.support.java.DateUtil;
import org.hibernate.Criteria;
import org.hibernate.ObjectNotFoundException;
import org.hibernate.Session;
import org.hibernate.criterion.Restrictions;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Utility methods for working with DurationWorked calculations.
 *
 * @author Andrew Williams
 * @version $Id$
 * @since 1.0
 */
public class ResourceManagerImpl {
    private static Logger log = Manager.getLogger(ResourceManagerImpl.class.getName());

    /**
     * this will always return a duration object. it will be the most appropriate 'estimated time' remaining
     * for the issue in question on the dayInQuestion.
     *
     * @param issue
     * @param dayInQuestion
     * @return
     */
    public Duration lastEstimateForDay(Issue issue, Date dayInQuestion) {

        Date endOfDayInQuestion = DateUtil.getEndOfDate(Calendar.getInstance(), dayInQuestion);
        Date lastEstimateDate = null; // used to know where our current estimate was based on
        Duration estimate = null;

        for (DurationWorked worked : issue.getTimeWorked()) {
            Date workedDay = worked.getDay();
            if (workedDay == null || worked.getUpdatedRequired() == null) {
                continue;
            }

            // for this Worked object to be considered valid it must be worked before the day in question
            if (workedDay.before(endOfDayInQuestion)) {
                if (lastEstimateDate == null || !workedDay.before(lastEstimateDate)) {
                    estimate = worked.getUpdatedRequired();
                    lastEstimateDate = workedDay;
                }
            }
        }

        if (estimate == null) {
            // if the issue was created before the dayInQuestion then we report the original issue time estimate
            // or if we want to backtrack the originalTimeEstimate
            if (issue.getIncludeInInitialEstimates() || issue.getCreated().before(endOfDayInQuestion)) {
                if (issue.getTimeRequired() != null
                        && (issue.getTimeWorked() == null || issue.getTimeWorked().size() == 0)) {
                    estimate = issue.getTimeRequired();
                } else if (issue.getTimeEstimate() != null) {
                    estimate = issue.getTimeEstimate();
                }
            } else {
                // otherwise we report 0 hours for this issue vs dayInQuestion
                estimate = new Duration(0);
            }
        }

        return estimate;
    }

    public Duration lastEstimateForIssue(Issue issue) {
        Duration estimate = issue.getTimeEstimate();

        Date lastEstimateDate = null;
        for (DurationWorked worked : issue.getTimeWorked()) {
            if (worked.getDay() == null || worked.getUpdatedRequired() == null) {
                continue;
            }

            if (lastEstimateDate == null
                    || (worked.getDay() != null && !worked.getDay().before(lastEstimateDate))) {
                estimate = worked.getUpdatedRequired();
                lastEstimateDate = worked.getDay();
            }
        }

        return estimate;
    }

    public Duration totalWorkedForDay(Issue issue, Date date) {
        Calendar cal = GregorianCalendar.getInstance();
        cal.setTime(date);

        double total = 0d;
        Calendar cal2 = GregorianCalendar.getInstance();

        for (DurationWorked worked : issue.getTimeWorked()) {
            if (worked.getDay() == null || worked.getUpdatedRequired() == null) {
                continue;
            }

            cal2.setTime(worked.getDay());
            if (cal.get(Calendar.DATE) == cal2.get(Calendar.DATE)
                    && cal.get(Calendar.MONTH) == cal2.get(Calendar.MONTH)
                    && cal.get(Calendar.YEAR) == cal2.get(Calendar.YEAR)) {
                if (worked.getWorked() != null) {
                    total += worked.getWorked().getHours();
                }
            }
        }

        return new Duration(total);
    }

    public Date getMilestoneStartDate(Milestone milestone) {
        return getIssueSetStartDate(milestone.getIssues(), milestone.getStartDate(), milestone.getDueDate());
    }

    public Date getMilestoneGroupStartDate(MilestoneGroup group) {
        return getIssueSetStartDate(group.getIssues(), group.getStartDate(), group.getDueDate());
    }

    private Date getIssueSetStartDate(Set<Issue> issues, Date start, Date due) {
        if (start != null) {
            return start;
        }

        if (due == null) {
            due = new Date();
        }

        Calendar cal = GregorianCalendar.getInstance();
        cal.setTime(due);

        cal.add(Calendar.DATE, -14);
        Date first = cal.getTime();

        for (Issue issue : issues) {
            for (DurationWorked worked : issue.getTimeWorked()) {
                if (worked.getDay() == null) {
                    continue;
                }

                if (!first.before(worked.getDay())) {
                    first = worked.getDay();
                }
            }
        }

        return first;
    }

    public List<Date> getMilestoneDates(Milestone milestone, boolean includeDayBefore) {
        return getIssueSetDates(getMilestoneStartDate(milestone), milestone.getDueDate(), milestone.getProject(),
                includeDayBefore);
    }

    public List<Date> getMilestoneGroupDates(MilestoneGroup group, boolean includeDayBefore) {
        return getIssueSetDates(getMilestoneGroupStartDate(group), group.getDueDate(), group.getProject(),
                includeDayBefore);
    }

    private List<Date> getIssueSetDates(Date start, Date due, Project project, boolean includeDayBefore) {
        // some prep work to make sure we have valid dates for start and end of milestone
        List<Date> dates = new LinkedList<Date>();
        if (due == null || start == null) {
            return dates;
        }

        Calendar calendar = Calendar.getInstance();
        Date confirmedEnd = DateUtil.getEndOfDate(calendar, due);
        Date confirmedStart = DateUtil.getStartOfDate(calendar, start);

        final boolean ignoreWeekend = Boolean.parseBoolean(
                project.getConfigurationValue(StoredProject.CONFIGURATION_TIMETRACKING_IGNOREWEEKEND));

        Calendar cal = GregorianCalendar.getInstance();
        cal.setTime(confirmedStart);

        if (includeDayBefore) {
            // return the day before as a starting point for estimates - work can be logged after this, contributing to
            // a lower value at the end of the first day
            cal.add(Calendar.DATE, -1);
        }
        boolean estimateDay = Boolean
                .parseBoolean(project.getConfigurationValue(StoredProject.CONFIGURATION_TIMETRACKING_BURNDOWN));
        for (Date date = cal.getTime(); date.before(confirmedEnd); date = cal.getTime()) {
            if (ignoreWeekend && !estimateDay && (cal.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY
                    || cal.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY)) {
                cal.add(Calendar.DATE, 1);
                continue;
            }

            dates.add(date);

            cal.add(Calendar.DATE, 1);
            estimateDay = false;
        }

        return dates;
    }

    /**
     * this will return an array of durations that match to the dates on the milestone in date order.
     * index 0 will show the effort remaining at end of day 1 , etc.
     *
     * @param milestone
     * @return
     */
    public Duration[] getMilestoneEffortRequired(Milestone milestone) {
        if (milestone == null) {
            return null;
        }

        List<Date> milestoneDates = getMilestoneDates(milestone, false);
        return getIssueSetEffortRequired(milestone.getIssues(), milestoneDates);
    }

    /**
     * this will return an array of durations that match to the dates on the milestone group in date order.
     * index 0 will show the effort remaining at end of day 1 , etc.
     *
     * @param group
     * @return
     */
    public Duration[] getMilestoneGroupEffortRequired(MilestoneGroup group) {
        if (group == null) {
            return null;
        }

        List<Date> groupDates = getMilestoneGroupDates(group, false);
        return getIssueSetEffortRequired(group.getIssues(), groupDates);
    }

    private Duration[] getIssueSetEffortRequired(Set<Issue> issues, List<Date> milestoneDates) {
        if (milestoneDates == null || milestoneDates.size() == 0) {
            return null;
        }
        // TODO fix getMilestoneDates to work correctly ....
        // manually add the start date to the milestone dates list as getMilestoneDates acts weird.
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(milestoneDates.get(0));
        calendar.add(Calendar.DATE, -1);

        List<Date> dates = new ArrayList<Date>();
        dates.add(calendar.getTime());
        for (Date date : milestoneDates) {
            dates.add(date);
        }

        Duration[] effortRequired = new Duration[dates.size()];
        // initialise the returnArray if there are no issues on milestone.
        if (issues == null || issues.size() == 0) {
            for (int i = 0; i < effortRequired.length; i++) {
                effortRequired[i] = new Duration(0);
            }
            return effortRequired;
        }

        // iterate over each day and calculate the effort remaining.
        int dayIndex = 0;
        for (Date date : dates) {
            double totalHoursForDay = 0;
            for (Issue issue : issues) {
                Duration lastEstimate = lastEstimateForDay(issue, date);
                if (lastEstimate != null) {
                    totalHoursForDay += lastEstimate.getHours();
                }
            }
            effortRequired[dayIndex] = new Duration(totalHoursForDay);

            dayIndex++;
        }

        return effortRequired;
    }

    public double getMilestoneCompleteness(Milestone milestone) {
        return getIssueListCompleteness(milestone.getIssues(), milestone.getProject());
    }

    public double getMilestoneGroupCompleteness(MilestoneGroup group) {
        return getIssueListCompleteness(group.getIssues(), group.getProject());
    }

    private double getIssueListCompleteness(Set<Issue> issues, Project project) {
        final boolean timeEnabled = Boolean
                .parseBoolean(project.getConfigurationValue(StoredProject.CONFIGURATION_TIMETRACKING_ENABLED));
        final boolean timeBurndown = Boolean
                .parseBoolean(project.getConfigurationValue(StoredProject.CONFIGURATION_TIMETRACKING_BURNDOWN));

        double done = 0;
        double total = 0;
        for (Issue issue : issues) {
            double issueHours = .25;
            if (timeEnabled && issue.getTimeEstimate() != null && issue.getTimeEstimate().getHours() > 0) {
                issueHours = issue.getTimeEstimate().getHours();
            }
            total += issueHours;

            if (issue.getStatus() >= Issue.STATUS_RESOLVED) {
                done += issueHours;
                continue;
            }

            if (!timeEnabled) {
                // add nothing to the done count for open issues...
                continue;
            }

            if (timeBurndown) {
                Duration left = lastEstimateForIssue(issue);
                if (left != null) {
                    double leftHours = left.getHours();
                    if (issue.getStatus() < Issue.STATUS_RESOLVED && leftHours < issueHours) {
                        leftHours = issueHours;
                    }

                    done += Math.max(issueHours - leftHours, 0);
                }
            } else {
                Duration worked = lastEstimateForIssue(issue);
                if (worked != null) {
                    done += Math.min(worked.getHours(), issueHours);
                }
            }
        }

        return done / total;
    }

    public Velocity getAverageVelocity() {
        if (!averageVelocity.equals(Double.NaN)) {
            return averageVelocity;
        }

        double velocities = 0.0;
        int velocityCount = 0;
        for (User user : Manager.getSecurityInstance().getRealUsers()) {
            if (!user.canLogin()) {
                continue;
            }

            Velocity velocity = getUserVelocity(user);
            if (!velocity.equals(Velocity.INVALID)) {
                velocities += velocity.getVelocity();
                velocityCount++;
            }
        }

        averageVelocity = new Velocity(velocities, (double) velocityCount);
        return averageVelocity;
    }

    // TODO we need to expire this once a week (or day)...
    private static Map<User, Velocity> userVelocities = new HashMap<User, Velocity>();
    private static Velocity averageVelocity = Velocity.INVALID;

    public Velocity getUserVelocity(User user) {
        if (userVelocities.get(user) != null) {
            return userVelocities.get(user);
        }

        List<DurationWorked> worked = getDurationWorkedForUser(user);
        Velocity velocity = calculateVelocity(worked, user, null, null);

        userVelocities.put(user, velocity);
        return velocity;
    }

    public Velocity getCurrentUserVelocity(User user) {
        //        if ( userVelocities.get( user ) != null )
        //        {
        //            return userVelocities.get( user );
        //        }

        Calendar cal = Calendar.getInstance();
        cal.add(Calendar.WEEK_OF_YEAR, -1);
        List<DurationWorked> worked = getDurationWorkedForUser(user, cal.getTime(), new Date());
        Velocity velocity = calculateVelocity(worked, user, cal.getTime(), new Date());

        //        userVelocities.put( user, velocity );
        return velocity;
    }

    public Velocity getUserVelocityInWeek(User user, Date week) {
        // TODO cache this
        //        if ( userVelocities.get( user ) != null )
        //        {
        //            return userVelocities.get( user );
        //        }

        Calendar cal = Calendar.getInstance();
        cal.setTime(week);
        cal.add(Calendar.WEEK_OF_YEAR, 1);
        // TODO midnight next week is not included now - right?
        cal.add(Calendar.MILLISECOND, -1);

        List<DurationWorked> worked = getDurationWorkedForUser(user, week, cal.getTime());
        Velocity velocity = calculateVelocity(worked, user, week, cal.getTime());

        //        userVelocities.put( user, velocity );
        return velocity;
    }

    public Velocity getVelocity(List<DurationWorked> worked, Milestone milestone) {
        return getVelocity(worked, milestone.getStartDate(), milestone.getDueDate());
    }

    public Velocity getVelocity(List<DurationWorked> worked, MilestoneGroup group) {
        return getVelocity(worked, group.getStartDate(), group.getDueDate());
    }

    public Velocity getVelocity(List<DurationWorked> worked, Date setStart, Date setDue) {
        Set<User> usersWorked = new HashSet<User>();

        // TODO fix issues around resources not working days they were allocated to...
        Date start = new Date();
        Date end = new Date(0);
        boolean calculateRange = true;
        if (setStart != null) {
            start = setStart;
            end = setDue;
            calculateRange = false;
        }

        for (DurationWorked duration : worked) {
            usersWorked.add(duration.getUser());

            if (calculateRange) {
                if (duration.getDay().before(start)) {
                    start = DateUtil.getStartOfDate(Calendar.getInstance(), duration.getDay());
                }
                if (duration.getDay().after(end)) {
                    end = DateUtil.getEndOfDate(Calendar.getInstance(), duration.getDay());
                }
            }
        }

        double velocities = 0.0;
        int velocityCount = 0;
        for (User user : usersWorked) {
            if (user.isHiddenInTimeTracking()) {
                continue;
            }

            Velocity vel = calculateVelocity(worked, user, start, end);
            if (!vel.equals(Velocity.INVALID)) {
                velocities += vel.getVelocity();
                velocityCount++;
            }
        }

        return new Velocity(velocities, (double) velocityCount);
    }

    private Velocity calculateVelocity(List<DurationWorked> workedList, User user, Date start, Date end) {
        if (user.isHiddenInTimeTracking()) {
            return Velocity.INVALID;
        }

        double estimatedHoursWorked = 0.0;
        double daysWorked = 0.0;
        Set<Date> daysSeen = new HashSet<Date>();
        Calendar cal = Calendar.getInstance();

        Set<Issue> relevantIssues = new HashSet<Issue>();
        for (DurationWorked duration : workedList) {
            try {
                if (duration.getIssue() != null && !relevantIssues.contains(duration.getIssue())) {
                    relevantIssues.add(duration.getIssue());
                }
            } catch (ObjectNotFoundException e) {
                // ignore - TODO find a better way of handling this...
            }
        }

        for (Issue issue : relevantIssues) {
            if (issue.getTimeEstimate() == null || issue.getTimeEstimate().getHours() == 0) {
                continue;
            }

            double estimate = issue.getTimeEstimate().getHours();
            double hoursWorked = 0;
            double totalEstimated = 0;

            List<DurationWorked> listWorked = new ArrayList<DurationWorked>(issue.getTimeWorked());
            Collections.sort(listWorked, new Comparator<DurationWorked>() {
                public int compare(DurationWorked d1, DurationWorked d2) {
                    Date date1 = d1.getDay();
                    Date date2 = d2.getDay();

                    if (date1 == null || date2 == null) {
                        if (date1 == null) {
                            if (date2 == null) {
                                return 0;
                            } else {
                                return 1;
                            }
                        } else {
                            return -1;
                        }
                    }

                    if (date1.equals(date2)) {
                        double d1hours = 0;
                        if (d1.getUpdatedRequired() != null) {
                            d1hours = d1.getUpdatedRequired().getHours();
                        }
                        double d2hours = 0;
                        if (d2.getUpdatedRequired() != null) {
                            d2hours = d2.getUpdatedRequired().getHours();
                        }
                        return Double.compare(d1hours, d2hours);
                    }
                    return date1.compareTo(date2);
                }
            });

            for (DurationWorked worked : listWorked) {
                if ((start != null && start.after(worked.getDay()))
                        || (end != null && end.before(worked.getDay()))) {
                    continue;
                }
                // ignore empty work but respect it's estimate
                if (worked.getWorked() == null || worked.getWorked().getHours() == 0) {
                    if (worked.getUpdatedRequired() != null) {
                        estimate = worked.getUpdatedRequired().getHours();
                    }
                    continue;
                }

                // don't count work not in the list but respect new estimates
                if (!workedList.contains(worked)) {
                    if (worked.getUpdatedRequired() != null) {
                        estimate = worked.getUpdatedRequired().getHours();
                    }
                    continue;
                }

                if (worked.getUser().equals(user) && worked.getDay() != null) {
                    hoursWorked += worked.getWorked().getHours();
                    if (worked.getUpdatedRequired() != null) {
                        totalEstimated += estimate - worked.getUpdatedRequired().getHours();
                    }

                    // check if this is a new day
                    cal.setTime(DateUtil.getStartOfDate(cal, worked.getDay()));
                    if (!daysSeen.contains(cal.getTime())) {
                        daysWorked += 1.0;
                        daysSeen.add(cal.getTime());
                    }
                }

                if (worked.getUpdatedRequired() != null) {
                    estimate = Math.min(estimate, worked.getUpdatedRequired().getHours());
                }
            }

            if (hoursWorked == 0) {
                continue;
            }

            estimatedHoursWorked += totalEstimated;
        }

        Velocity velocity = Velocity.INVALID;
        if (daysWorked > 0) {
            velocity = new Velocity(estimatedHoursWorked, daysWorked);
        }
        log.debug(velocity.toString());
        return velocity;
    }

    public Double getUserHoursLogged(User user) {
        // TODO cache
        //        if ( userVelocities.get( user ) != null )
        //        {
        //            return userVelocities.get( user );
        //        }

        Double logged = calculateHoursLogged(getDurationWorkedForUser(user), user);

        //        userVelocities.put( user, logged );
        return logged;
    }

    public Double getUserHoursLoggedInWeek(User user, Date week) {
        // TODO cache this
        //        if ( userVelocities.get( user ) != null )
        //        {
        //            return userVelocities.get( user );
        //        }

        Calendar cal = Calendar.getInstance();
        cal.setTime(week);
        cal.add(Calendar.WEEK_OF_YEAR, 1);
        // TODO midnight next week is not included now - right?
        cal.add(Calendar.MILLISECOND, -1);
        Double logged = calculateHoursLogged(getDurationWorkedForUser(user, week, cal.getTime()), user);

        //        userVelocities.put( user, velocity );
        return logged;
    }

    private Double calculateHoursLogged(List<DurationWorked> workedList, User user) {
        double total = 0;
        int daysWorked = 0;
        Set<Date> daysSeen = new HashSet<Date>();
        Calendar cal = Calendar.getInstance();

        for (DurationWorked worked : workedList) {
            if (worked.getUser().equals(user) && worked.getWorked() != null) {
                total += worked.getWorked().getHours();
                if (worked.getDay() == null) {
                    continue;
                }

                // check if this is a new day
                cal.setTime(worked.getDay());
                cal.set(Calendar.HOUR_OF_DAY, 0);
                cal.set(Calendar.MINUTE, 0);
                cal.set(Calendar.SECOND, 0);
                cal.set(Calendar.MILLISECOND, 0);
                if (!daysSeen.contains(cal.getTime())) {
                    daysWorked++;
                    daysSeen.add(cal.getTime());
                }
            }
        }

        return total / daysWorked;
    }

    public List<DurationWorked> getDurationWorkedForUser(User user) {
        Session session = ((HibernateStorage) Manager.getStorageInstance()).getHibernateSession();

        Criteria c = session.createCriteria(DurationWorked.class);
        c.add(Restrictions.eq("user", user));
        c.add(Restrictions.gt("worked.time", 0));

        return c.list();
    }

    public List<DurationWorked> getDurationWorkedForUser(User user, Date start, Date end) {
        List<DurationWorked> workedList = new ArrayList<DurationWorked>();
        Session session = ((HibernateStorage) Manager.getStorageInstance()).getHibernateSession();

        Criteria c = session.createCriteria(DurationWorked.class);
        c.add(Restrictions.eq("user", user));
        c.add(Restrictions.gt("worked.time", 0));
        c.add(Restrictions.between("day", start, end));

        for (DurationWorked worked : (List<DurationWorked>) c.list()) {
            if (worked.getDay().before(start) || worked.getDay().after(end)) {
                continue;
            }

            workedList.add(worked);
        }
        return workedList;
    }

    /**
     * This will sum together all duration logged against a user between start and end.
     * Depending on the types of Duration logged against this user, the smallest unit will be
     * represented in the return type.
     * <p/>
     * For example if there are two logged times, 1 hour and 1 day, the time unit
     * for the Duration returned will be composed of hours
     *
     * @return
     */
    public Duration getLoggedTimeForUser(User user, Date start, Date end) {

        List<DurationWorked> workedList = getDurationWorkedForUser(user, start, end);
        double hoursLogged = 0;
        for (DurationWorked worked : workedList) {
            hoursLogged += worked.getWorked().getHours();
        }

        return new Duration(hoursLogged);
    }

    /**
     * Determine if an issue is going to miss it's target amount of work.
     * This is based on how much work has been logged and the initial estimate of the issue.
     *
     * @param issue The issue to check
     * @return True if the issue is over the estimated amount of work required to complete.
     */
    public boolean isIssueMissingEstimate(Issue issue) {
        final boolean burndown = Boolean.parseBoolean(
                issue.getProject().getConfigurationValue(StoredProject.CONFIGURATION_TIMETRACKING_BURNDOWN));

        if (burndown) {
            if (issue.getTimeEstimate() != null && issue.getTimeEstimate().getHours() > 0
                    && issue.getTimeWorked() != null) {
                double remain = getWorkRemainingForIssue(issue);

                return remain < 0;
            }
        }

        return false;
    }

    /**
     * Determine if an issue is going to miss it's target amount of work by a significant margin.
     * This is based on how much work has been logged and the initial estimate of the issue.
     * The margin is defined as 50% over the estimated amount of work.
     *
     * @param issue The issue to check
     * @return True if the issue is over the estimated amount of work required to complete.
     */
    public boolean isIssueSeriouslyMissingEstimate(Issue issue) {
        final double AMOUNT_OVER_TO_BE_SERIOUS = .5;
        final boolean burndown = Boolean.parseBoolean(
                issue.getProject().getConfigurationValue(StoredProject.CONFIGURATION_TIMETRACKING_BURNDOWN));

        if (burndown) {
            if (issue.getTimeEstimate() != null && issue.getTimeEstimate().getHours() > 0
                    && issue.getTimeWorked() != null) {
                double estimate = getEstimateMultiplier(issue) * issue.getTimeEstimate().getHours();
                double remain = getWorkRemainingForIssue(issue);

                return remain < (estimate * AMOUNT_OVER_TO_BE_SERIOUS) * -1;
            }
        }

        return false;
    }

    protected double getWorkRemainingForIssue(Issue issue) {
        double estimate = getEstimateMultiplier(issue) * issue.getTimeEstimate().getHours();
        double remain = estimate;
        for (DurationWorked worked : issue.getTimeWorked()) {
            if (worked.getWorked() != null) {
                remain -= worked.getWorked().getHours();
            }
        }

        if (issue.getTimeRequired() == null) {
            remain -= estimate;
        } else {
            remain -= getEstimateMultiplier(issue) * issue.getTimeRequired().getHours();
        }

        return remain;
    }

    protected double getEstimateMultiplier(Issue issue) {
        return 1;
    }
}