/* $Id: ToDoList.java 17741 2010-01-10 03:50:08Z bobtarling $
*******************************************************************************
* Copyright (c) 2010 Contributors - see below
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Bob Tarling
*******************************************************************************
*
* Some portions of this file were previously release using the BSD License:
*/
// $Id: ToDoList.java 17741 2010-01-10 03:50:08Z bobtarling $
// Copyright (c) 1996-2008 The Regents of the University of California. All
// Rights Reserved. Permission to use, copy, modify, and distribute this
// software and its documentation without fee, and without a written
// agreement is hereby granted, provided that the above copyright notice
// and this paragraph appear in all copies. This software program and
// documentation are copyrighted by The Regents of the University of
// California. The software program and documentation are supplied "AS
// IS", without any accompanying services from The Regents. The Regents
// does not warrant that the operation of the program will be
// uninterrupted or error-free. The end-user understands that the program
// was developed for research purposes and is advised not to rely
// exclusively on the program for any reason. IN NO EVENT SHALL THE
// UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
// SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS,
// ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
// THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE. THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY
// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
// PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
// CALIFORNIA HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT,
// UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
package org.argouml.cognitive;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Observable;
import java.util.Set;
import javax.swing.event.EventListenerList;
import org.apache.log4j.Logger;
import org.argouml.i18n.Translator;
import org.argouml.model.InvalidElementException;
/**
* Implements a list of ToDoItem's.
* <p>
*
* It spawns a "sweeper" thread that periodically goes through the list and
* eliminates ToDoItem's that are no longer valid.
* <p>
*
* One difficulty designers face is keeping track of all the myriad details of
* their task. It is all too easy to skip a step in the design process, leave
* part of the design unspecified, or make a mistake that requires revision.
* ArgoUML provides the designer with a "to do" list user interface that
* presents action items in an organized form. These items can be suggestions
* from critics, reminders to finish steps in the process model, or personal
* notes entered by the designer. The choice control at the top of the "to do"
* list pane allow the designer to organize items in different ways: by
* priority, by decision supported, by offending design element, etc.
* <p>
*
* Items are shown under all applicable headings.
* <p>
*
* This class is dependent on Designer.
* <p>
*
* @see Designer#inform
* @author Jason Robbins
*/
public class ToDoList extends Observable implements Runnable {
/**
* Logger.
*/
private static final Logger LOG = Logger.getLogger(ToDoList.class);
/**
* Number of seconds thread should sleep between passes
*/
private static final int SLEEP_SECONDS = 3;
/**
* Pending ToDoItems for the designer to consider.
*/
private List<ToDoItem> items;
private Set<ToDoItem> itemSet;
/**
* These are computed when needed.
*/
// TODO: Offenders need to be more strongly typed. - tfm 20070630
private volatile ListSet allOffenders;
/**
* These are computed when needed.
*/
private volatile ListSet<Poster> allPosters;
/**
* ToDoItems that the designer has explicitly indicated that (s)he considers
* resolved.
* <p>
* TODO: generalize into a design rationale logging facility.
*/
private Set<ResolvedCritic> resolvedItems;
/**
* A Thread that keeps checking if the items on the list are still valid.
*/
private Thread validityChecker;
/**
* The designer, used in determining if a ToDoItem is still valid.
*/
private Designer designer;
private EventListenerList listenerList;
private static int longestToDoList;
private static int numNotValid;
/**
* state variable for whether the validity checking thread is paused
* (waiting).
*/
private boolean isPaused;
private Object pausedMutex = new Object();
/**
* Creates a new todolist. The only ToDoList is owned by the Designer.
*/
ToDoList() {
items = Collections.synchronizedList(new ArrayList<ToDoItem>(100));
itemSet = Collections.synchronizedSet(new HashSet<ToDoItem>(100));
resolvedItems =
Collections.synchronizedSet(new LinkedHashSet<ResolvedCritic>(100));
listenerList = new EventListenerList();
longestToDoList = 0;
numNotValid = 0;
}
/**
* Start a Thread to delete old items from the ToDoList.
*
* @param d the designer
*/
public synchronized void spawnValidityChecker(Designer d) {
designer = d;
validityChecker = new Thread(this, "Argo-ToDoValidityCheckingThread");
validityChecker.setDaemon(true);
validityChecker.setPriority(Thread.MIN_PRIORITY);
setPaused(false);
validityChecker.start();
}
/**
* Entry point for validity checker thread. Periodically check to see if
* items on the list are still valid.
*/
public void run() {
final List<ToDoItem> removes = new ArrayList<ToDoItem>();
while (true) {
// the validity checking thread should wait if disabled.
synchronized (pausedMutex) {
while (isPaused) {
try {
pausedMutex.wait();
} catch (InterruptedException ignore) {
LOG.error("InterruptedException!!!", ignore);
}
}
}
forceValidityCheck(removes);
removes.clear();
try {
Thread.sleep(SLEEP_SECONDS * 1000);
} catch (InterruptedException ignore) {
LOG.error("InterruptedException!!!", ignore);
}
}
}
/**
* Check each ToDoItem on the list to see if it is still valid. If not, then
* remove that item. This is called automatically by the
* ValidityCheckingThread, and it can be called by the user pressing a
* button via forceValidityCheck().
*/
public void forceValidityCheck() {
final List<ToDoItem> removes = new ArrayList<ToDoItem>();
forceValidityCheck(removes);
}
/**
* Check each ToDoItem on the list to see if it is still valid. If not, then
* remove that item. This is called automatically by the
* ValidityCheckingThread, and it can be called by the user pressing a
* button via forceValidityCheck().
* <p>
*
* <em>Warning: Fragile code!</em> No method that this method calls can
* synchronized the Designer, otherwise there will be deadlock.
*
* @param removes a list containing the items to be removed
*/
private synchronized void forceValidityCheck(
final List<ToDoItem> removes) {
synchronized (items) {
for (ToDoItem item : items) {
boolean valid;
try {
valid = item.stillValid(designer);
} catch (InvalidElementException ex) {
// If element has been deleted, it's no longer valid
valid = false;
} catch (Exception ex) {
valid = false;
StringBuffer buf = new StringBuffer(
"Exception raised in ToDo list cleaning");
buf.append("\n");
buf.append(item.toString());
LOG.error(buf.toString(), ex);
}
if (!valid) {
numNotValid++;
removes.add(item);
}
}
}
for (ToDoItem item : removes) {
removeE(item);
// History.TheHistory.addItemResolution(item,
// "no longer valid");
// ((ToDoItem)item).resolve("no longer valid");
// notifyObservers("removeElement", item);
}
recomputeAllOffenders();
recomputeAllPosters();
fireToDoItemsRemoved(removes);
}
/**
* Pause the validity checking thread.
*/
public void pause() {
synchronized (pausedMutex) {
isPaused = true;
}
}
/**
* Resume the validity checking thread.
*/
public void resume() {
synchronized (pausedMutex) {
isPaused = false;
pausedMutex.notifyAll();
}
}
/**
* @return true is paused
*/
public boolean isPaused() {
synchronized (pausedMutex) {
return isPaused;
}
}
/**
* sets the pause state.
*
* @param paused if set to false, calls resume() also to start working
*/
public void setPaused(boolean paused) {
if (paused) {
pause();
} else {
resume();
}
}
// //////////////////////////////////////////////////////////////
// Notifications and Updates
/**
* @param action the action
* @param arg the argument
*/
public void notifyObservers(String action, Object arg) {
setChanged();
List<Object> l = new ArrayList<Object>(2);
l.add(action);
l.add(arg);
super.notifyObservers(l);
}
/*
* @see java.util.Observable#notifyObservers(java.lang.Object)
*/
public void notifyObservers(Object o) {
setChanged();
super.notifyObservers(o);
}
/*
* @see Observable#notifyObservers()
*/
public void notifyObservers() {
setChanged();
super.notifyObservers();
}
/**
* Returns the List of the ToDoItems. It is <em>mandatory</em> that
* code iterating over this list synchronize access to the list as described
* in {@link Collections#synchronizedList(List)}.
* <pre>
* List<ToDoItem> list = toDoList.getToDoItemList();
* ...
* synchronized(list) {
* for (ToDoItem item : list ) { // Must be in synchronized block
* ....
* }
* </pre>
* @see Collections#synchronizedList(List)
* @return the List of ToDo items.
*/
public List<ToDoItem> getToDoItemList() {
return items;
}
/**
* Returns the set of ResolvedCritics. It is <em>mandatory</em> that
* code iterating over this set synchronize access to the set as described
* in {@link Collections#synchronizedSet(Set)}.
* <pre>
* Set<ResolvedCritic> set = toDoList.getResolvedItems();
* ...
* synchronized(set) {
* for (ResolvedCritic item : set ) { // Must be in synchronized block
* ....
* }
* </pre>
* @see Collections#synchronizedSet(Set)
* @return the resolved items
*/
public Set<ResolvedCritic> getResolvedItems() {
return resolvedItems;
}
/**
* @return the set of offenders
*
* TODO: The return value needs to be more strongly typed. - tfm - 20070630
*/
public ListSet getOffenders() {
// Extra care to be taken since allOffenders can be reset while
// this method is running.
ListSet all = allOffenders;
if (all == null) {
int size = items.size();
all = new ListSet(size * 2);
synchronized (items) {
for (ToDoItem item : items) {
all.addAll(item.getOffenders());
}
}
allOffenders = all;
}
return all;
}
private void addOffenders(ListSet newoffs) {
if (allOffenders != null) {
allOffenders.addAll(newoffs);
}
}
/**
* @return the set of all the posters
*/
public ListSet<Poster> getPosters() {
// Extra care to be taken since allPosters can be reset while
// this method is running.
ListSet<Poster> all = allPosters;
if (all == null) {
all = new ListSet<Poster>();
synchronized (items) {
for (ToDoItem item : items) {
all.add(item.getPoster());
}
}
allPosters = all;
}
return all;
}
private void addPosters(Poster newp) {
if (allPosters != null) {
allPosters.add(newp);
}
}
/**
* @return the list of Decisions (empty by default).
*/
public static List<Decision> getDecisionList() {
return new ArrayList<Decision>();
}
/**
* @return The list of Goals (empty by default).
*/
public static List<Goal> getGoalList() {
return new ArrayList<Goal>();
}
/*
* TODO: needs documenting, why synchronized?
*/
private void addE(ToDoItem item) {
/* skip any identical items already on the list */
if (itemSet.contains(item)) {
return;
}
if (item.getPoster() instanceof Critic) {
ResolvedCritic rc;
try {
rc = new ResolvedCritic((Critic) item.getPoster(), item
.getOffenders(), false);
Iterator<ResolvedCritic> elems = resolvedItems.iterator();
// cat.debug("Checking for inhibitors " + rc);
while (elems.hasNext()) {
if (elems.next().equals(rc)) {
LOG.debug("ToDoItem not added because it was resolved");
return;
}
}
} catch (UnresolvableException ure) {
}
}
items.add(item);
itemSet.add(item);
longestToDoList = Math.max(longestToDoList, items.size());
addOffenders(item.getOffenders());
addPosters(item.getPoster());
// if (item.getPoster() instanceof Designer)
// History.TheHistory.addItem(item, "note: ");
// else
// History.TheHistory.addItemCritique(item);
notifyObservers("addElement", item);
fireToDoItemAdded(item);
}
/**
* @param item the todo item to be added
*/
public void addElement(ToDoItem item) {
addE(item);
}
/**
* @param list the todo items to be removed
*/
public void removeAll(ToDoList list) {
List<ToDoItem> itemList = list.getToDoItemList();
synchronized (itemList) {
for (ToDoItem item : itemList) {
removeE(item);
}
recomputeAllOffenders();
recomputeAllPosters();
fireToDoItemsRemoved(itemList);
}
}
/**
* @param item the todo item to be removed
* @return <code>true</code> if the argument was a component of this list;
* <code>false</code> otherwise
*/
private boolean removeE(ToDoItem item) {
itemSet.remove(item);
return items.remove(item);
}
/**
* @param item the todo item to be removed
* @return <code>true</code> if the argument was a component of this list;
* <code>false</code> otherwise
*/
public boolean removeElement(ToDoItem item) {
boolean res = removeE(item);
recomputeAllOffenders();
recomputeAllPosters();
fireToDoItemRemoved(item);
notifyObservers("removeElement", item);
return res;
}
/**
* @param item the todo item to be resolved
* @return <code>true</code> if the argument was a component of this list;
* <code>false</code> otherwise
*/
public boolean resolve(ToDoItem item) {
boolean res = removeE(item);
fireToDoItemRemoved(item);
return res;
}
/**
* @param item the todo item
* @param reason the reason TODO: Use it!
* @return <code>true</code> if the argument was a component of this list;
* <code>false</code> otherwise
* @throws UnresolvableException unable to resolve
*/
public boolean explicitlyResolve(ToDoItem item, String reason)
throws UnresolvableException {
if (item.getPoster() instanceof Designer) {
boolean res = resolve(item);
// History.TheHistory.addItemResolution(item, reason);
return res;
}
if (!(item.getPoster() instanceof Critic)) {
throw new UnresolvableException(Translator.localize(
"misc.todo-unresolvable", new Object[] {item.getPoster()
.getClass()}));
}
ResolvedCritic rc = new ResolvedCritic((Critic) item.getPoster(), item
.getOffenders());
boolean res = resolve(item);
if (res) {
res = addResolvedCritic(rc);
}
return res;
}
/**
* Add the given resolved critic to the list of resolved critics.
*
* @param rc the resolved critic
* @return <code>true</code> if successfully added; <code>false</code>
* otherwise
*/
public boolean addResolvedCritic(ResolvedCritic rc) {
return resolvedItems.add(rc);
}
/**
* Remove all todo items.
*/
public void removeAllElements() {
LOG.debug("removing all todo items");
List<ToDoItem> oldItems = new ArrayList<ToDoItem>(items);
items.clear();
itemSet.clear();
recomputeAllOffenders();
recomputeAllPosters();
notifyObservers("removeAllElements");
fireToDoItemsRemoved(oldItems);
}
/**
* @param offender the offender
* @return A List of todo items for this offender.
* <p>
* Note: the previous implementation returned an internal static
* (global) list which could be modified at any point, requiring the
* caller to copy the list before using it (negating the value of
* caching the static copy). The current implementation returns a
* private copy which will not change, so callers don't need to copy
* it.
*/
public List<ToDoItem> elementListForOffender(Object offender) {
List<ToDoItem> offenderItems = new ArrayList<ToDoItem>();
synchronized (items) {
for (ToDoItem item : items) {
if (item.getOffenders().contains(offender)) {
offenderItems.add(item);
}
}
}
return offenderItems;
}
/**
* @return the number of todo items
*/
public int size() {
return items.size();
}
/**
* @param index 0-based index to retrieve ToDoItem from
* @return the ToDoItem at the given index
*/
public ToDoItem get(int index) {
return items.get(index);
}
/**
* Re-compute all offenders.
*/
private void recomputeAllOffenders() {
allOffenders = null;
}
/**
* Reset all posters.
*/
private void recomputeAllPosters() {
allPosters = null;
}
// //////////////////////////////////////////////////////////////
// event related stuff
/**
* @param l the listener to be added
*/
public void addToDoListListener(ToDoListListener l) {
// EventListenerList.add() is synchronized, so we don't need to
// synchronize ourselves
listenerList.add(ToDoListListener.class, l);
}
/**
* @param l the listener to be removed
*/
public void removeToDoListListener(ToDoListListener l) {
// EventListenerList.remove() is synchronized, so we don't need to
// synchronize ourselves
listenerList.remove(ToDoListListener.class, l);
}
/**
* @param item the todo item
*/
void fireToDoItemChanged(ToDoItem item) {
Object[] listeners = listenerList.getListenerList();
ToDoListEvent e = null;
// Process the listeners last to first, notifying
// those that are interested in this event
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == ToDoListListener.class) {
// Lazily create the event:
if (e == null) {
List<ToDoItem> its = new ArrayList<ToDoItem>();
its.add(item);
e = new ToDoListEvent(its);
}
((ToDoListListener) listeners[i + 1]).toDoItemsChanged(e);
}
}
}
/**
* @param item the todo item
*/
private void fireToDoItemAdded(ToDoItem item) {
List<ToDoItem> l = new ArrayList<ToDoItem>();
l.add(item);
fireToDoItemsAdded(l);
}
/**
* @param theItems the todo items
*/
private void fireToDoItemsAdded(List<ToDoItem> theItems) {
if (theItems.size() > 0) {
// Guaranteed to return a non-null array
final Object[] listeners = listenerList.getListenerList();
ToDoListEvent e = null;
// Process the listeners last to first, notifying
// those that are interested in this event
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == ToDoListListener.class) {
// Lazily create the event:
if (e == null) {
e = new ToDoListEvent(theItems);
}
((ToDoListListener) listeners[i + 1]).toDoItemsAdded(e);
}
}
}
}
/**
* @param item the todo item
*/
private void fireToDoItemRemoved(ToDoItem item) {
List<ToDoItem> l = new ArrayList<ToDoItem>();
l.add(item);
fireToDoItemsRemoved(l);
}
/**
* @param theItems the todo items
*/
private void fireToDoItemsRemoved(final List<ToDoItem> theItems) {
if (theItems.size() > 0) {
// Guaranteed to return a non-null array
final Object[] listeners = listenerList.getListenerList();
ToDoListEvent e = null;
// Process the listeners last to first, notifying
// those that are interested in this event
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == ToDoListListener.class) {
// Lazily create the event:
if (e == null) {
e = new ToDoListEvent(theItems);
}
((ToDoListListener) listeners[i + 1]).toDoItemsRemoved(e);
}
}
}
}
@Override
public String toString() {
StringBuffer res = new StringBuffer(100);
res.append(getClass().getName()).append(" {\n");
List<ToDoItem> itemList = getToDoItemList();
synchronized (itemList) {
for (ToDoItem item : itemList) {
res.append(" ").append(item.toString()).append("\n");
}
}
res.append(" }");
return res.toString();
}
}