alvinalexander.com | career | drupal | java | mac | mysql | perl | scala | uml | unix  

What this is

This file is included in the DevDaily.com "Java Source Code Warehouse" project. The intent of this project is to help you "Learn Java by Example" TM.

Other links

The source code

/*
 *                 Sun Public License Notice
 *
 * The contents of this file are subject to the Sun Public License
 * Version 1.0 (the "License"). You may not use this file except in
 * compliance with the License. A copy of the License is available at
 * http://www.sun.com/
 *
 * The Original Code is NetBeans. The Initial Developer of the Original
 * Code is Sun Microsystems, Inc. Portions Copyright 1997-2003 Sun
 * Microsystems, Inc. All Rights Reserved.
 */

package org.netbeans.modules.tasklist.usertasks;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Date;
import java.util.Iterator;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.netbeans.modules.tasklist.client.Suggestion;
import org.netbeans.modules.tasklist.client.SuggestionPriority;
import org.netbeans.modules.tasklist.core.TLUtils;
import org.netbeans.modules.tasklist.core.Task;
import org.netbeans.modules.tasklist.core.TaskAnnotation;
import org.netbeans.modules.tasklist.core.TaskListener;
import org.openide.ErrorManager;
import org.openide.cookies.LineCookie;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.loaders.DataObject;
import org.openide.loaders.DataObjectNotFoundException;
import org.openide.text.Line;
import org.openide.text.Line.Set;


/**
 * Class which represents a task in the
 * tasklist.
 *
 * @author Tor Norbye
 * @author Trond Norbye
 */
public final class UserTask extends Task implements TaskListener, PropertyChangeListener {
    /**
     * Splits a duration value
     * 
     * @param minutes duration in minutes
     * @param hoursPerDay working hours per day
     * @return [0] - minutes, [1] - hours, [2] - days
     */
    public static int[] splitDuration(int minutes, int hoursPerDay) {
        int[] ret = new int[3];
        ret[0] = minutes % 60;
        minutes /= 60;
        ret[1] = minutes % hoursPerDay;
        minutes /= hoursPerDay;
        ret[2] = minutes;
        return ret;
    }
    
    private UserTaskList list;
    
    /** Used to create uid's. May be expensive to compute, so only do
     * it once. */
    private static String domain = null;
    
    /** Used to create uid's. Assign unique id's for this session. */
    private static int unique = 0; 

    private String uid;
    
    /** 
     * 0..100 - value in percents 
     */
    private float progress = 0.0f;
    
    /**
     * true means progress will be computed automatically as a weighted average 
     * of the subtasks. If a task has no children it means 0%
     */
    private boolean percentComputed = false;
    
    private String filename;
    private String basename;

    private TaskAnnotation annotation = null;
    
    private Date dueDate;
    private boolean dueAlarmSent;
    
    /** 
     * 1, 2, 3, ... 0 = no line information 
     * This information is read from a file and is kept here only to
     * know that the line number has changed in the line object.
     */
    private int linenumber;
    
    private String category;
    private long created;
    private long edited;

    /** Alias shown in the Suggestions view. */
    private Suggestion suggestion;
    
    /**
     * true means that the effort will be computed automatically as them sum of the
     * subtask efforts. If a task has no children it means 0
     */
    private boolean effortComputed = false;
    
    /** in minutes */
    private int effort = 60;
    
    /** Time spent on this task */
    private int spentTime = 0;
    private boolean spentTimeComputed;
    
    // ATTENTION: if you add new fields here do not forget to update copyFrom
    
    /**
     * Creates a task with the specified description
     *
     * @param desc description
     * @param list task list that this task belongs to
     */
    public UserTask(String desc, UserTaskList list) {
        this(desc, false, 3, null, 0, "", "", null, list); // NOI18N
    }
    
    /**
     *
     * Construct a new task with the given parameters.
     *
     * @param desc description
     * @param done true = the task is done
     * @param priority 1..5 (High..Low)
     * @param filename filename or null
     * @param linenumber line number
     * @param details details
     * @param category task's category ("" - no category)
     * @param parent parent task
     * @param list task list that this task belongs to
     */    
    public UserTask(String desc, boolean done, int priority,
             String filename, int linenumber, String details, 
             String category, UserTask parent, UserTaskList list) {
        super(desc, parent);
        
        assert priority >= 1 && priority <= 5 : "priority ?"; // NOI18N
        assert desc != null : "desc == null"; // NOI18N
        assert details != null : "details == null"; // NOI18N
        assert category != null : "category == null"; // NOI18N
        
        this.list = list;
        
        if (done)
            setDone(true);
        
        setPriority(SuggestionPriority.getPriority(priority));

        this.filename = filename;
        this.category = category;
        
        setLineNumber(linenumber);
        setDetails(details);

        created = System.currentTimeMillis();
        edited = created;
        annotation = null;

	if (domain == null) {
            try {
                InetAddress address = InetAddress.getLocalHost();
                domain = address.toString();
            } catch (UnknownHostException e) {
                domain = "unknown"; // NOI18N
            }
	}
        
        // XXX Later, come up with a better time stamp, e.g. use DateFormat
	String timestamp = Long.toString(System.currentTimeMillis());
        
	// uid = "nb" + timestamp + "." + (unique++) + "@" + domain; 
        uid = new StringBuffer(50).append("nb").append(timestamp). // NOI18N
            append('.').append(unique++).append('@').append(domain).toString();

        addPropertyChangeListener(this);
    }
    
    /**
     * Returns the list this task belongs to.
     *
     * @return task list
     */
    public UserTaskList getList() {
        return list;
    }
    
    /**
     * Returns how long it took to complete this task.
     *
     * @return duration in minutes >= 0
     */
    public int getSpentTime() {
        return spentTime;
    }
    
    /**
     * Changes the duration of this task
     *
     * @param duration new value in minutes
     */
    public void setSpentTime(int spentTime) {
        assert spentTime >= 0;
        if (spentTimeComputed)
            setSpentTimeComputed(false);
    
        setSpentTime_(spentTime);

        if (isProgressComputed() && !hasSubtasks())
            setProgress_(computeProgress());
    }
    
    /**
     * Setter for property spentTime. This method does not check the 
     * spentTimeComputed property and so it could be used from setSpentTimeComputed()
     *
     * @param spentTime New value of property spentTime in minutes.
     */
    private void setSpentTime_(int spentTime) {
        int old = this.spentTime;
        
        if (this.spentTime != spentTime) {
            this.spentTime = spentTime;
            if (!silentUpdate) {
                firePropertyChange("spentTime",  // NOI18N
                    new Integer(old), new Integer(spentTime));
                if (getParent() != null) {
                    UserTask p = (UserTask) getParent();
                    if (p.isSpentTimeComputed())
                        p.setSpentTime_(p.computeSpentTime());
                    if (p.isSpentTimeComputed())
                        p.setSpentTime_(p.computeSpentTime());
                }
            }
        }
    }
    
    /**
     * Sets whether the spent time of this task should be computed
     *
     * @param v true = the spent time will be computed
     */
    public void setSpentTimeComputed(boolean v) {
        if (this.spentTimeComputed != v) {
            this.spentTimeComputed = v;
            firePropertyChange("spentTimeComputed", Boolean.valueOf(!v),
                Boolean.valueOf(v));
            if (v) {
                setSpentTime_(computeSpentTime());
            }
        }
    }
    
    /**
     * Getter for property spentTimeComputed.
     *
     * @return true = the spent time will be computed as the sum of the 
     * subtask values
     */
    public boolean isSpentTimeComputed() {
        return spentTimeComputed;
    }
    
    /**
     * Computes "spentTime" property as the sum of the subtask times.
     * This method should only be called if spentTimeComputed == true
     *
     * This method is used in tests that is why it's package private
     *
     * @return spent time in minutes
     */
    int computeSpentTime() {
        assert spentTimeComputed;

        int sum = 0;
        Iterator it = subtasksIterator();        
        while (it.hasNext()) {
            UserTask child = (UserTask) it.next();
            sum += child.getSpentTime();
        }
        return sum;
    }

    /**
     * Start to work on this task
     */
    public void start() {
        UserTask started = UserTaskList.getStarted();
        if (started != null) {
            UserTaskList.start(null);
            started.firePropertyChange("started", Boolean.TRUE, Boolean.FALSE);
        }
        
        UserTaskList.start(this);
        firePropertyChange("started", Boolean.FALSE, Boolean.TRUE);
    }
    
    /**
     * Stops this task
     */
    public void stop() {
        assert isStarted();
        UserTaskList.start(null);
        firePropertyChange("started", Boolean.TRUE, Boolean.FALSE);
    }
    
    /**
     * Is this task currently running?
     *
     * @return true = this is the currently running task
     */
    public boolean isStarted() {
        return UserTaskList.getStarted() == this;
    }
    
    /** 
     * Return true iff the task has an associated file position 
     *
     * @return true the task has an associated file position
     */
    public boolean hasAssociatedFilePos() {
	return ((linenumber > 0) && (filename != null) && (filename.length() > 0))
            || (getLine() != null);
    }
    
    /**
     * ???
     * @return TODO
     */
    public boolean isDueAlarmSent() {
        return dueAlarmSent;
    }
    
    /**
     * ???
     * @param flag TODO
     */
    public void setDueAlarmSent(boolean flag) {
        boolean old = this.dueAlarmSent;
        dueAlarmSent = flag;
        firePropertyChange("dueAlarmSent", Boolean.valueOf(old),  // NOI18N
            Boolean.valueOf(dueAlarmSent));
    }
    
    /**
     * Get the "Deadline" for this task
     * @return the "deadline"
     */
    public Date getDueDate() {
        return dueDate;
    }
    
    /**
     * Set the "Deadline" for this task
     *
     * @param d the "deadline"
     */
    public void setDueDate(Date d) {      
        Date old = this.dueDate;
        if (d != null) {
            if (!d.equals(dueDate)) {
                dueAlarmSent = false;
            }
        } else {
            if (dueDate != null) {
                dueAlarmSent = false;
            }
        }
        dueDate = d;
        firePropertyChange("dueDate", old, dueDate); // NOI18N
    }
    
    /**
     * get the "deadline" for this task
     *
     * @return "deadline" for this task, Long.MAX_VALUE == no due time
     */
    public long getDueTime() {
        long ret = Long.MAX_VALUE;
        if (dueDate != null) {
            ret = dueDate.getTime(); // getNextDueDate();
        }
        return ret;
    }

    /** 
     * Check if this task is due. A task is considered
     * due if its due time has already passed, or will
     * occur in the next 36 hours (that will usually
     * be roughly "today or tomorrow")
     * 

* @param when Date when we want to know if the date is due. * You'll probably want to pass in a date corresponding * to now. */ boolean isDue(Date when) { if (dueDate == null) { return false; } long due = dueDate.getTime(); long now = when.getTime(); return (due < (now + (36 * 60 * 60 * 1000))); // number of milliseconds in 36 hours: // 36 hours * 60 minutes * 60 * seconds * 1000 milliseconds } /** * The UID (Unique Identifier) for this item. See RFC 822 and RFC 2445. * * @return unique ID */ public String getUID() { return uid; } public void setUID(String nuid) { String old = this.uid; uid = nuid; firePropertyChange("UID", old, this.uid); // NOI18N } /** * Indicate if the task is done * * @return true iff percents complete equals 100 */ public boolean isDone() { return Math.abs(getPercentComplete() - 100.0f) < 1e-6; } /** * Sets the percent complete to either 0 or 100. * @param done Whether or not the task is done. */ public void setDone(boolean done) { if (done) setPercentComplete(100); else setPercentComplete(0); } /** * Returns the percentage complete for this task. * * @return 0..100 - value in percents */ public int getPercentComplete() { return Math.round(progress); } /** * Returns the percentage complete for this task. * * @return 0..100 - value in percents */ public float getProgress() { return progress; } /** * Computes "percent complete" property as an average of the subtasks * This method should only be called if percentComputed == true * * @return computed percentage */ private float computeProgress() { assert percentComputed; if (!hasSubtasks()) { float p = (((float) getSpentTime()) / getEffort()) * 100.0f; if (p > 99.0) p = 99; return p; } Iterator it = subtasksIterator(); int sum = 0; int full = 0; while (it.hasNext()) { UserTask child = (UserTask) it.next(); sum += child.getPercentComplete() * child.getEffort(); full += 100 * child.getEffort(); } if (full == sum) return 100; float p = ((float) sum) / full * 100.0f; return p; } /** * Returns true if progress of this task will be computed automatically * from the progress of subtasks * * @return true = automatically computed */ public boolean isProgressComputed() { return percentComputed; } /** * Sets whether the progress of this task should be computed * * @param v true = the progress will be computed */ public void setProgressComputed(boolean v) { if (this.percentComputed != v) { this.percentComputed = v; firePropertyChange("progressComputed", Boolean.valueOf(!v), Boolean.valueOf(v)); if (v) { setProgress_(computeProgress()); } } } /** * Sets the percentage complete for this task. Will also * update the done flag for this task. * * @param percent 0..100 - value in percents */ public void setPercentComplete(int percent) { setProgress(percent); } /** * Sets the percentage complete for this task. Will also * update the done flag for this task. * * @param percent 0..100 - value in percents */ public void setProgress(float progress) { assert progress >= 0 && progress <= 100; if (percentComputed) setProgressComputed(false); setProgress_(progress); if (isDone() && isStarted()) stop(); } /** * Sets the percentage complete for this task. This method does not * check the percentComputed flag and could be used from * setProgressComputed() * * @param percent 0..100 - value in percents */ private void setProgress_(float progress) { float old = this.progress; if (this.progress != progress) { this.progress = progress; if (!silentUpdate) { firePropertyChange("progress", // NOI18N new Float(old), new Float(progress)); if (getParent() != null) { UserTask p = (UserTask) getParent(); if (p.isProgressComputed()) p.setProgress_(p.computeProgress()); } } } } /** * Return the name of the file associated with this * task, or the empty string if none. * * @return filename, or empty string */ public java.lang.String getFilename() { if (filename == null) { return ""; // NOI18N } return filename; } /** * Set file to be associated with this task. * * @param filename Name of file to be associated with this task or null */ public void setFilename(java.lang.String filename) { String old = this.filename; this.filename = filename; basename = null; // Force recomputation firePropertyChange("filename", old, this.filename); if (filename == null) { setLine(null); } } /** * Return the name of the file associated with this * task, or the empty string if none. * * @return basename, or empty string */ public java.lang.String getFileBaseName() { if (basename == null) { // Compute basename from the filename if ((filename == null) || (filename == "")) { basename = ""; } else { int index = filename.lastIndexOf(File.separator); if (index == -1) { basename = filename; } else { basename = filename.substring(index+1); } } } return basename; } /** * Set file to be associated with this task from a basename. * * @param basename Name of file to be associated with this task */ public void setFileBaseName(java.lang.String basename) { this.basename = basename; // If we're given a basename, take the current directory // and replace the basename with it. Otherwise, set the // full filename to this basename. // XXX the below doesn't exactly check to see if it's // an absolute path; in Unix I would just check // basename[0] == File.separator, but on windows you can // have ugly paths like C:\... etc. Supporting relative // path setting doesn't seem very important so for now // I will assume all paths are absolute if they contain // the separator character. (Later I might consider : // to be indicative of a relative path that is really an // absolute path.) if (basename.indexOf(File.separator) != -1) { // Absolute path (with the caveat listed above) setFilename(basename); return; } else { // Just a basename if ((filename == null) || (filename == "")) { // Yuck - unknown directory. Oh well, what does // the user expect if entering a basename as a filename // -- perhaps that I use the current directory of // the current file, whatever that means? (node selection? // editor selection? filesystems explorer selection?) setFilename(basename); return; } else { // Current directory is int index = filename.lastIndexOf(File.separator); if (index == -1) { // Same problem as above: unknown dir setFilename(basename); return; } String newName = filename.substring(0, index) + File.separator + basename; setFilename(newName); return; } } } /** * Return line number associated with the task. * * @return Line number, or "0" if no particular line is * associated. Will always be 0 if there is no associated file. * @see getFilename */ public int getLineNumber() { Line line = getLine(); if(line != null) return line.getLineNumber() + 1; return linenumber; } /** * Set line number to be associated with task, if a file * has also been selected. * * @param linenumber New line number */ public void setLineNumber(int linenumber) { UTUtils.LOGGER.fine("linenumber " + linenumber); if (UTUtils.LOGGER.isLoggable(Level.FINER)) Thread.dumpStack(); int old = this.linenumber; this.linenumber = linenumber; if (filename != null && linenumber > 0) { setLine(getLineByFile(filename, linenumber)); } else { setLine(null); } firePropertyChange("lineNumber", new Integer(old), new Integer(this.linenumber)); } /** * Return the Line object for a particular line in a file */ private static Line getLineByFile(String filename, int lineno) { // It's gotta be a file FileObject[] fos = FileUtil.fromFile( new File(filename).getAbsoluteFile()); if ((fos == null) || (fos.length == 0)) { /* DialogDisplayer.getDefault().notify(new Message( NbBundle.getMessage(UserTask.class, "FileNotFound", // NOI18N filename))); */ return null; } DataObject dobj = null; try { for (int i = 0; i < fos.length; i++) { dobj = DataObject.find(fos[i]); break; } } catch (DataObjectNotFoundException e) { ErrorManager.getDefault().log( "No data object could be found for file objects " + fos); // NOI18N } if (dobj == null) return null; // Go to the given line try { LineCookie lc = (LineCookie)dobj.getCookie(LineCookie.class); if (lc != null) { Set ls = lc.getLineSet(); if (ls != null) { // I'm subtracting 1 because empirically I've discovered // that the editor highlights whatever line I ask for plus 1 Line l = ls.getCurrent(lineno-1); return l; } } } catch (Exception e) { ErrorManager.getDefault(). notify(ErrorManager.INFORMATIONAL, e); } return null; } /** * Return category of the task, or "" if no category * has been selected. * * @return Category name */ public java.lang.String getCategory() { if (category == null) { return ""; } return category; } /** * Set category associated with the task. * * @param category New category */ public void setCategory(java.lang.String category) { String old = this.category; this.category = category; firePropertyChange("category", old, this.category); } /** * Return the date when the item was created * * @return Date when the item was created */ public long getCreatedDate() { return created; } /** * Return any annotation associated with this task */ void setAnnotation(TaskAnnotation annotation) { this.annotation = annotation; } /** * Set annotation associated with this task */ TaskAnnotation getAnnotation() { return annotation; } /** * Set the date when the item was created */ public void setCreatedDate(long cr) { long old = this.created; created = cr; firePropertyChange("createdDate", new Long(old), new Long(this.created)); } /** * Get the date when the item was last edited. * If the item has not been edited since it was created, * this returns the date of creation. Note also that * adding subtasks or removing subtasks is not considered * an edit of this item. * * @return the date when the item was last edited. */ public long getLastEditedDate() { return edited; } /** * Set the date when the item was last edited */ public void setLastEditedDate(long ed) { edited = ed; } /** * @deprecated splitting model from the view */ /*public Node [] createNode() { if (hasSubtasks()) { return new Node [] {new UserTaskNode(this, new UserTaskNode.UserTaskChildren(this))}; } else { return new Node [] {new UserTaskNode(this)}; } }*/ /** * Create an identical copy of a task (a deep copy, e.g. the * list of subtasks will be cloned as well */ protected Object clone() { UserTask t = new UserTask("", list); t.copyFrom(this); return t; } /** * Copy all the fields in the given task into this object. * Should only be called on an object of the EXACT same type. * Thus, if you're implementing a subclass of Task, say * UserTask, you can implement copy assuming that the passed * in Task parameter is of type UserTask. When overriding, * remember to call super.copyFrom. *

* Make a deep copy - except when that doesn't make sense. * For example, you can share the same icon reference. * And in particular, the tasklist reference should be the same. * But the list of subitems should be unique. You get the idea. * * @param from another task */ protected void copyFrom(UserTask from) { super.copyFrom(from); progress = from.progress; percentComputed = from.percentComputed; filename = from.filename; basename = from.basename; linenumber = from.linenumber; category = from.category; created = from.created; edited = from.edited; suggestion = from.suggestion; effort = from.effort; effortComputed = from.effortComputed; spentTime = from.spentTime; spentTimeComputed = from.spentTimeComputed; // [PENDING] annotation - do we want to copy that? } /** * Reads line number from the line property and fires an * event if it was changed. */ void updateLineNumberRecursively() { Line line = getLine(); if (line != null) { if (line.getLineNumber() != linenumber) { int old = this.linenumber; this.linenumber = line.getLineNumber(); firePropertyChange("lineNumber", new Integer(old), new Integer(linenumber)); } } // process subtasks if (subtasks != null) { Iterator it = subtasks.iterator(); while (it.hasNext()) { ((UserTask) it.next()).updateLineNumberRecursively(); } } } /** * Some user tasks can be shown in the Suggestions view * (for example when they're overdue) and since UserTasks cannot * be included directly, we keep a reference to the alias here. */ Suggestion getSuggestion() { return suggestion; } /** * See getSuggestion for a description of this field. This method * sets the suggestion associated with this usertask. */ void setSuggestion(Suggestion s) { suggestion = s; } /** * Update the annotation associated with this task: show or remove * it depending on hasFilePos/getLine and getAnnotation */ void updateAnnotation() { if (getLine() != null) { if (getAnnotation() == null) { TaskAnnotation taskMarker = new TaskAnnotation(this, false); taskMarker.attach(getLine()); setAnnotation(taskMarker); } } else if (getAnnotation() != null) { TaskAnnotation taskMarker = getAnnotation(); taskMarker.detach(); setAnnotation(null); } } /** * Returns the remaining effort in minutes * * @return remaining effort in minutes >= 0 */ public int getRemainingEffort() { return Math.round(getEffort() * (1 - getProgress() / 100)); } /** * Getter for property effort. * * @return effort in minutes > 0 */ public int getEffort() { return effort; } /** * Getter for property effortComputed. * * @return true = effort will be computed as the sum of the subtask efforts */ public boolean isEffortComputed() { return effortComputed; } /** * Setter for property effortComputed. * * @param effortComputed New value of property effortComputed. */ public void setEffortComputed(boolean effortComputed) { if (this.effortComputed != effortComputed) { this.effortComputed = effortComputed; firePropertyChange("effortComputed", Boolean.valueOf(!effortComputed), Boolean.valueOf(effortComputed)); if (effortComputed) { setEffort_(computeEffort()); } } } /** * Computes "effort" property as the sum of the subtask efforts. * This method should only be called if effortComputed == true * * This method is used in tests that is why it's package private * * @return effort in minutes */ int computeEffort() { assert effortComputed; Iterator it = subtasksIterator(); int sum = 0; while (it.hasNext()) { UserTask child = (UserTask) it.next(); sum += child.getEffort(); } return sum; } /** * Setter for property effort. * * @param effort New value of property effort in minutes. */ public void setEffort(int effort) { assert effort >= 0; if (effortComputed) setEffortComputed(false); setEffort_(effort); if (isProgressComputed() && !hasSubtasks()) setProgress_(computeProgress()); } /** * Setter for property effort. This method does not check the * effortComputed property and so it could be used from setEffortComputed() * * @param effort New value of property effort in minutes. */ private void setEffort_(int effort) { int old = this.effort; if (this.effort != effort) { this.effort = effort; if (!silentUpdate) { firePropertyChange("effort", // NOI18N new Integer(old), new Integer(effort)); if (getParent() != null) { UserTask p = (UserTask) getParent(); if (p.isEffortComputed()) p.setEffort_(p.computeEffort()); if (p.isProgressComputed()) p.setProgress_(p.computeProgress()); } } } } // TaskListener implementation public void removedTask(Task pt, Task t, int index) { structureChanged(t); } public void structureChanged(Task t) { if (isProgressComputed()) { setProgress_(computeProgress()); } if (isEffortComputed()) { setEffort_(computeEffort()); } if (isSpentTimeComputed()) { setSpentTime_(computeSpentTime()); } list.markChanged(); } public void addedTask(Task t) { structureChanged(t); } public void warpedTask(Task t) { } public void selectedTask(Task t) { } /** * Generate a string summary of the task; only used * for debugging. DO NOT depend on this format for anything! * Use generate() instead. * * @return summary string */ public String toString() { return "UserTask[\"" + getSummary() + "]" + super.toString(); // NOI18N } public void propertyChange(PropertyChangeEvent evt) { edited = System.currentTimeMillis(); } public void setLine(Line line) { super.setLine(line); } /** * Deletes all completed subtasks of this task (recursively) */ public void purgeCompleted() { Iterator it = subtasksIterator(); while (it.hasNext()) { UserTask ut = (UserTask) it.next(); if (ut.isDone()) { this.removeSubtask(ut); it = subtasksIterator(); } else { ut.purgeCompleted(); } } } protected void firePropertyChange(String propertyName, Object oldValue, Object newValue) { super.firePropertyChange(propertyName, oldValue, newValue); list.markChanged(); } }

... this post is sponsored by my books ...

#1 New Release!

FP Best Seller

 

new blog posts

 

Copyright 1998-2021 Alvin Alexander, alvinalexander.com
All Rights Reserved.

A percentage of advertising revenue from
pages under the /java/jwarehouse URI on this website is
paid back to open source projects.