Progress: fix synchro issues + tests
[nikiroo-utils.git] / src / be / nikiroo / utils / Progress.java
CommitLineData
b3aad1f9 1package be.nikiroo.utils;
86057589
NR
2
3import java.util.ArrayList;
4import java.util.EventListener;
5import java.util.HashMap;
6import java.util.List;
7import java.util.Map;
8import java.util.Map.Entry;
88b36f83 9import java.util.Set;
86057589
NR
10
11/**
12 * Progress reporting system, possibly nested.
13 *
14 * @author niki
15 */
16public class Progress {
17 public interface ProgressListener extends EventListener {
18 /**
19 * A progression event.
20 *
21 * @param progress
da5bfa48
NR
22 * the {@link Progress} object that generated it, not
23 * necessarily the same as the one where the listener was
24 * attached (it could be a child {@link Progress} of this
25 * {@link Progress}).
86057589
NR
26 * @param name
27 * the first non-null name of the {@link Progress} step that
28 * generated this event
29 */
30 public void progress(Progress progress, String name);
31 }
32
0924c45d
NR
33 private Progress parent = null;
34 private Object lock = new Object();
86057589
NR
35 private String name;
36 private Map<Progress, Double> children;
37 private List<ProgressListener> listeners;
38 private int min;
39 private int max;
40 private int localProgress;
41 private int progress; // children included
42
43 /**
44 * Create a new default unnamed {@link Progress}, from 0 to 100.
45 */
46 public Progress() {
47 this(null);
48 }
49
50 /**
51 * Create a new default {@link Progress}, from 0 to 100.
52 *
53 * @param name
54 * the name of this {@link Progress} step
55 */
56 public Progress(String name) {
57 this(name, 0, 100);
58 }
59
60 /**
61 * Create a new unnamed {@link Progress}, from min to max.
62 *
63 * @param min
64 * the minimum progress value (and starting value) -- must be
65 * non-negative
66 * @param max
67 * the maximum progress value
68 */
69 public Progress(int min, int max) {
70 this(null, min, max);
71 }
72
73 /**
74 * Create a new {@link Progress}, from min to max.
75 *
76 * @param name
77 * the name of this {@link Progress} step
78 * @param min
79 * the minimum progress value (and starting value) -- must be
80 * non-negative
81 * @param max
82 * the maximum progress value
83 */
84 public Progress(String name, int min, int max) {
85 this.name = name;
86 this.children = new HashMap<Progress, Double>();
87 this.listeners = new ArrayList<Progress.ProgressListener>();
88 setMinMax(min, max);
89 setProgress(min);
90 }
91
92 /**
93 * The name of this {@link Progress} step.
94 *
95 * @return the name
96 */
97 public String getName() {
98 return name;
99 }
100
2998b78a
NR
101 /**
102 * The name of this {@link Progress} step.
103 *
104 * @param name
105 * the new name
106 */
107 public void setName(String name) {
108 this.name = name;
109 // will fire an action event:
110 setProgress(this.localProgress);
111 }
112
86057589
NR
113 /**
114 * The minimum progress value.
115 *
116 * @return the min
117 */
118 public int getMin() {
119 return min;
120 }
121
122 /**
123 * The minimum progress value.
124 *
125 * @param min
126 * the min to set
0924c45d
NR
127 *
128 *
129 * @throws Error
130 * if min &lt; 0 or if min &gt; max
86057589
NR
131 */
132 public void setMin(int min) {
133 if (min < 0) {
134 throw new Error("negative values not supported");
135 }
136
0924c45d
NR
137 synchronized (getLock()) {
138 if (min > max) {
139 throw new Error(
140 "The minimum progress value must be <= the maximum progress value");
141 }
86057589 142
0924c45d
NR
143 this.min = min;
144 }
86057589
NR
145 }
146
147 /**
148 * The maximum progress value.
149 *
150 * @return the max
151 */
152 public int getMax() {
153 return max;
154 }
155
156 /**
157 * The maximum progress value (must be >= the minimum progress value).
158 *
159 * @param max
160 * the max to set
0924c45d
NR
161 *
162 *
163 * @throws Error
164 * if max &lt; min
86057589
NR
165 */
166 public void setMax(int max) {
0924c45d
NR
167 synchronized (getLock()) {
168 if (max < min) {
169 throw new Error(
170 "The maximum progress value must be >= the minimum progress value");
171 }
86057589 172
0924c45d
NR
173 this.max = max;
174 }
86057589
NR
175 }
176
177 /**
178 * Set both the minimum and maximum progress values.
179 *
180 * @param min
181 * the min
182 * @param max
183 * the max
0924c45d
NR
184 *
185 * @throws Error
186 * if min &lt; 0 or if min &gt; max
86057589
NR
187 */
188 public void setMinMax(int min, int max) {
189 if (min < 0) {
190 throw new Error("negative values not supported");
191 }
192
193 if (min > max) {
194 throw new Error(
195 "The minimum progress value must be <= the maximum progress value");
196 }
197
0924c45d
NR
198 synchronized (getLock()) {
199 this.min = min;
200 this.max = max;
201 }
86057589
NR
202 }
203
204 /**
205 * Get the total progress value (including the optional children
206 * {@link Progress}) on a {@link Progress#getMin()} to
207 * {@link Progress#getMax()} scale.
208 *
209 * @return the progress the value
210 */
211 public int getProgress() {
212 return progress;
213 }
214
215 /**
216 * Set the local progress value (not including the optional children
217 * {@link Progress}), on a {@link Progress#getMin()} to
218 * {@link Progress#getMax()} scale.
219 *
220 * @param progress
221 * the progress to set
222 */
223 public void setProgress(int progress) {
0924c45d
NR
224 synchronized (getLock()) {
225 int diff = this.progress - this.localProgress;
226 this.localProgress = progress;
227 setTotalProgress(this, name, progress + diff);
228 }
86057589
NR
229 }
230
231 /**
232 * Check if the action corresponding to this {@link Progress} is done (i.e.,
233 * if its progress value is >= its max value).
234 *
235 * @return TRUE if it is
236 */
237 public boolean isDone() {
238 return progress >= max;
239 }
240
241 /**
242 * Get the total progress value (including the optional children
243 * {@link Progress}) on a 0.0 to 1.0 scale.
244 *
245 * @return the progress
246 */
247 public double getRelativeProgress() {
248 return (((double) progress) / (max - min));
249 }
250
88b36f83
NR
251 /**
252 * Return the list of direct children of this {@link Progress}.
253 *
254 * @return the children (who will think of them??)
255 */
256 public Set<Progress> getChildren() {
257 return children.keySet();
258 }
259
86057589
NR
260 /**
261 * Set the total progress value (including the optional children
262 * {@link Progress}), on a {@link Progress#getMin()} to
263 * {@link Progress#getMax()} scale.
264 *
88b36f83
NR
265 * @param pg
266 * the {@link Progress} to report as the progression emitter
86057589
NR
267 * @param name
268 * the current name (if it is NULL, the first non-null name in
88b36f83
NR
269 * the hierarchy will overwrite it) of the {@link Progress} who
270 * emitted this change
86057589
NR
271 * @param progress
272 * the progress to set
273 */
88b36f83 274 private void setTotalProgress(Progress pg, String name, int progress) {
0924c45d
NR
275 synchronized (getLock()) {
276 this.progress = progress;
86057589 277
0924c45d
NR
278 for (ProgressListener l : listeners) {
279 l.progress(pg, name);
280 }
86057589
NR
281 }
282 }
283
284 /**
285 * Add a {@link ProgressListener} that will trigger on progress changes.
da5bfa48
NR
286 * <p>
287 * Note: the {@link Progress} that will be reported will be the active
288 * progress, not necessarily the same as the current one (it could be a
289 * child {@link Progress} of this {@link Progress}).
86057589
NR
290 *
291 * @param l
292 * the listener
293 */
294 public void addProgressListener(ProgressListener l) {
295 this.listeners.add(l);
296 }
297
2998b78a
NR
298 /**
299 * Remove a {@link ProgressListener} that would trigger on progress changes.
300 *
301 * @param l
302 * the listener
303 *
304 * @return TRUE if it was found (and removed)
305 */
306 public boolean removeProgressListener(ProgressListener l) {
307 return this.listeners.remove(l);
308 }
309
86057589
NR
310 /**
311 * Add a child {@link Progress} of the given weight.
312 *
313 * @param progress
314 * the child {@link Progress} to add
315 * @param weight
316 * the weight (on a {@link Progress#getMin()} to
317 * {@link Progress#getMax()} scale) of this child
318 * {@link Progress} in relation to its parent
0924c45d
NR
319 *
320 * @throws Error
321 * if weight exceed {@link Progress#getMax()} or if progress
322 * already has a parent
86057589
NR
323 */
324 public void addProgress(Progress progress, double weight) {
325 if (weight < min || weight > max) {
0924c45d
NR
326 throw new Error(String.format(
327 "Progress object %s cannot have a weight of %f, "
328 + "it is outside of its parent (%s) range (%f)",
329 progress.name, weight, name, max));
330 }
331
332 if (progress.parent != null) {
333 throw new Error(String.format(
334 "Progress object %s cannot be added to %s, "
335 + "as it already has a parent (%s)", progress.name,
336 name, progress.parent.name));
86057589
NR
337 }
338
86057589 339 progress.addProgressListener(new ProgressListener() {
62c9ec78 340 public void progress(Progress pg, String name) {
0924c45d
NR
341 synchronized (getLock()) {
342 double total = ((double) localProgress) / (max - min);
343 for (Entry<Progress, Double> entry : children.entrySet()) {
344 total += (entry.getValue() / (max - min))
345 * entry.getKey().getRelativeProgress();
346 }
347
348 if (name == null) {
349 name = Progress.this.name;
350 }
351
352 setTotalProgress(pg, name,
353 (int) Math.round(total * (max - min)));
86057589 354 }
86057589
NR
355 }
356 });
357
358 this.children.put(progress, weight);
359 }
0924c45d
NR
360
361 /**
362 * The lock object to use (this one or the recursively-parent one).
363 *
364 * @return the lock object to use
365 */
366 private Object getLock() {
367 synchronized (lock) {
368 if (parent != null) {
369 return parent.getLock();
370 }
371
372 return lock;
373 }
374 }
86057589 375}