JAL-4312 defend against ConcurrentModificationException
[jalview.git] / src / jalview / workers / AlignCalcManager.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.workers;
22
23 import jalview.api.AlignCalcManagerI;
24 import jalview.api.AlignCalcWorkerI;
25 import jalview.datamodel.AlignmentAnnotation;
26
27 import java.util.ArrayList;
28 import java.util.Collection;
29 import java.util.Collections;
30 import java.util.HashSet;
31 import java.util.Hashtable;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.Set;
35
36 public class AlignCalcManager implements AlignCalcManagerI
37 {
38   /*
39    * list of registered workers
40    */
41   private volatile List<AlignCalcWorkerI> restartable;
42
43   /*
44    * types of worker _not_ to run (for example, because they have
45    * previously thrown errors)
46    */
47   private volatile List<Class<? extends AlignCalcWorkerI>> blackList;
48
49   /*
50    * global record of calculations in progress
51    */
52   private volatile List<AlignCalcWorkerI> inProgress;
53
54   /*
55    * record of calculations pending or in progress in the current context
56    */
57   private volatile Map<Class<? extends AlignCalcWorkerI>, List<AlignCalcWorkerI>> updating;
58
59   /*
60    * workers that have run to completion so are candidates for visual-only 
61    * update of their results
62    */
63   private HashSet<AlignCalcWorkerI> canUpdate;
64
65   /**
66    * Constructor
67    */
68   public AlignCalcManager()
69   {
70     restartable = Collections
71             .synchronizedList(new ArrayList<AlignCalcWorkerI>());
72     blackList = Collections.synchronizedList(
73             new ArrayList<Class<? extends AlignCalcWorkerI>>());
74     inProgress = Collections
75             .synchronizedList(new ArrayList<AlignCalcWorkerI>());
76     updating = Collections.synchronizedMap(
77             new Hashtable<Class<? extends AlignCalcWorkerI>, List<AlignCalcWorkerI>>());
78     canUpdate = new HashSet<AlignCalcWorkerI>();
79   }
80
81   @Override
82   public void notifyStart(AlignCalcWorkerI worker)
83   {
84     synchronized (updating)
85     {
86       List<AlignCalcWorkerI> upd = updating.get(worker.getClass());
87       if (upd == null)
88       {
89         updating.put(worker.getClass(), upd = Collections
90                 .synchronizedList(new ArrayList<AlignCalcWorkerI>()));
91       }
92       synchronized (upd)
93       {
94         upd.add(worker);
95       }
96     }
97   }
98
99   /*
100    * (non-Javadoc)
101    * 
102    * @see jalview.api.AlignCalcManagerI#isPending(jalview.api.AlignCalcWorkerI)
103    */
104   @Override
105   public boolean isPending(AlignCalcWorkerI workingClass)
106   {
107     List<AlignCalcWorkerI> upd;
108     synchronized (updating)
109     {
110       upd = updating.get(workingClass.getClass());
111       if (upd == null)
112       {
113         return false;
114       }
115       synchronized (upd)
116       {
117         if (upd.size() > 1)
118         {
119           return true;
120         }
121       }
122       return false;
123     }
124   }
125
126   @Override
127   public boolean notifyWorking(AlignCalcWorkerI worker)
128   {
129     synchronized (inProgress)
130     {
131       if (inProgress.contains(worker))
132       {
133         return false; // worker is already working, so ask caller to wait around
134       }
135       else
136       {
137         inProgress.add(worker);
138       }
139     }
140     return true;
141   }
142
143   @Override
144   public void workerComplete(AlignCalcWorkerI worker)
145   {
146     synchronized (inProgress)
147     {
148       // jalview.bin.Console.errPrintln("Worker " + worker + " marked as complete.");
149       inProgress.remove(worker);
150       List<AlignCalcWorkerI> upd = updating.get(worker.getClass());
151       if (upd != null)
152       {
153         synchronized (upd)
154         {
155           upd.remove(worker);
156         }
157         canUpdate.add(worker);
158       }
159     }
160   }
161
162   @Override
163   public void disableWorker(AlignCalcWorkerI worker)
164   {
165     synchronized (blackList)
166     {
167       blackList.add(worker.getClass());
168     }
169   }
170
171   @Override
172   public boolean isDisabled(AlignCalcWorkerI worker)
173   {
174     synchronized (blackList)
175     {
176       return blackList.contains(worker.getClass());
177     }
178   }
179
180   @Override
181   public void startWorker(AlignCalcWorkerI worker)
182   {
183     if (!isDisabled(worker))
184     {
185       Thread tw = new Thread(worker);
186       tw.setName(worker.getClass().toString());
187       tw.start();
188     }
189   }
190
191   @Override
192   public boolean isWorking(AlignCalcWorkerI worker)
193   {
194     synchronized (inProgress)
195     {// jalview.bin.Console.errPrintln("isWorking : worker "+(worker!=null ?
196      // worker.getClass():"null")+ " "+hashCode());
197       return worker != null && inProgress.contains(worker);
198     }
199   }
200
201   @Override
202   public boolean isWorking()
203   {
204     boolean working=false;
205     synchronized (inProgress)
206     {
207       // jalview.bin.Console.errPrintln("isWorking "+hashCode());
208       working |= inProgress.size() > 0;
209     }
210     synchronized (updating)
211     {
212       Collection<List<AlignCalcWorkerI>> workersLists = updating.values();
213       synchronized (workersLists)
214       {
215         for (List<AlignCalcWorkerI> workers : workersLists)
216         {
217           if (workers!=null)
218           {
219             synchronized (workers) {
220               working |= workers.size() > 0;
221             }
222           }
223         }
224       }
225     }
226     return working;
227   }
228
229   @Override
230   public void registerWorker(AlignCalcWorkerI worker)
231   {
232     synchronized (restartable)
233     {
234       if (!restartable.contains(worker))
235       {
236         restartable.add(worker);
237       }
238       startWorker(worker);
239     }
240   }
241
242   @Override
243   public void restartWorkers()
244   {
245     synchronized (restartable)
246     {
247       for (AlignCalcWorkerI worker : restartable)
248       {
249         startWorker(worker);
250       }
251     }
252   }
253
254   @Override
255   public boolean workingInvolvedWith(
256           AlignmentAnnotation alignmentAnnotation)
257   {
258     synchronized (inProgress)
259     {
260       for (AlignCalcWorkerI worker : inProgress)
261       {
262         if (worker.involves(alignmentAnnotation))
263         {
264           return true;
265         }
266       }
267     }
268     synchronized (updating)
269     {
270       for (List<AlignCalcWorkerI> workers : updating.values())
271       {
272         for (AlignCalcWorkerI worker : workers)
273         {
274           if (worker.involves(alignmentAnnotation))
275           {
276             return true;
277           }
278         }
279       }
280     }
281     return false;
282   }
283
284   @Override
285   public void updateAnnotationFor(
286           Class<? extends AlignCalcWorkerI> workerClass)
287   {
288
289     AlignCalcWorkerI[] workers;
290     synchronized (canUpdate)
291     {
292       workers = canUpdate.toArray(new AlignCalcWorkerI[0]);
293     }
294     for (AlignCalcWorkerI worker : workers)
295     {
296       if (workerClass.equals(worker.getClass()))
297       {
298         worker.updateAnnotation();
299       }
300     }
301   }
302
303   @Override
304   public List<AlignCalcWorkerI> getRegisteredWorkersOfClass(
305           Class<? extends AlignCalcWorkerI> workerClass)
306   {
307     List<AlignCalcWorkerI> workingClass = new ArrayList<AlignCalcWorkerI>();
308     AlignCalcWorkerI[] workers;
309     synchronized (canUpdate)
310     {
311       workers = canUpdate.toArray(new AlignCalcWorkerI[0]);
312     }
313     for (AlignCalcWorkerI worker : workers)
314     {
315       if (workerClass.equals(worker.getClass()))
316       {
317         workingClass.add(worker);
318       }
319     }
320     return (workingClass.size() == 0) ? null : workingClass;
321   }
322
323   @Override
324   public void enableWorker(AlignCalcWorkerI worker)
325   {
326     synchronized (blackList)
327     {
328       blackList.remove(worker.getClass());
329     }
330   }
331
332   @Override
333   public void removeRegisteredWorkersOfClass(
334           Class<? extends AlignCalcWorkerI> typeToRemove)
335   {
336     List<AlignCalcWorkerI> removable = new ArrayList<AlignCalcWorkerI>();
337     Set<AlignCalcWorkerI> toremovannot = new HashSet<AlignCalcWorkerI>();
338     synchronized (restartable)
339     {
340       for (AlignCalcWorkerI worker : restartable)
341       {
342         if (typeToRemove.equals(worker.getClass()))
343         {
344           removable.add(worker);
345           toremovannot.add(worker);
346         }
347       }
348       restartable.removeAll(removable);
349     }
350     synchronized (canUpdate)
351     {
352       for (AlignCalcWorkerI worker : canUpdate)
353       {
354         if (typeToRemove.equals(worker.getClass()))
355         {
356           removable.add(worker);
357           toremovannot.add(worker);
358         }
359       }
360       canUpdate.removeAll(removable);
361     }
362     // TODO: finish testing this extension
363
364     /*
365      * synchronized (inProgress) { // need to kill or mark as dead any running
366      * threads... (inProgress.get(typeToRemove)); }
367      * 
368      * if (workers == null) { return; } for (AlignCalcWorkerI worker : workers)
369      * {
370      * 
371      * if (isPending(worker)) { worker.abortAndDestroy(); startWorker(worker); }
372      * else { jalview.bin.Console.errPrintln("Pending exists for " + workerClass); } }
373      */
374   }
375
376   /**
377    * Deletes the worker that update the given annotation, provided it is marked
378    * as deletable.
379    */
380   @Override
381   public void removeWorkerForAnnotation(AlignmentAnnotation ann)
382   {
383     /*
384      * first just find those to remove (to avoid
385      * ConcurrentModificationException)
386      */
387     List<AlignCalcWorkerI> toRemove = new ArrayList<AlignCalcWorkerI>();
388     for (AlignCalcWorkerI worker : restartable)
389     {
390       if (worker.involves(ann))
391       {
392         if (worker.isDeletable())
393         {
394           toRemove.add(worker);
395         }
396       }
397     }
398
399     /*
400      * remove all references to deleted workers so any references 
401      * they hold to annotation data can be garbage collected 
402      */
403     for (AlignCalcWorkerI worker : toRemove)
404     {
405       restartable.remove(worker);
406       blackList.remove(worker.getClass());
407       inProgress.remove(worker);
408       canUpdate.remove(worker);
409       synchronized (updating)
410       {
411         List<AlignCalcWorkerI> upd = updating.get(worker.getClass());
412         if (upd != null)
413         {
414           upd.remove(worker);
415         }
416       }
417     }
418   }
419 }