001/*
002 *  jDTAUS Core RI Task Monitor
003 *  Copyright (C) 2005 Christian Schulte
004 *  <cs@schulte.it>
005 *
006 *  This library is free software; you can redistribute it and/or
007 *  modify it under the terms of the GNU Lesser General Public
008 *  License as published by the Free Software Foundation; either
009 *  version 2.1 of the License, or any later version.
010 *
011 *  This library is distributed in the hope that it will be useful,
012 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
013 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014 *  Lesser General Public License for more details.
015 *
016 *  You should have received a copy of the GNU Lesser General Public
017 *  License along with this library; if not, write to the Free Software
018 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
019 *
020 */
021package org.jdtaus.core.monitor.ri;
022
023import java.util.Date;
024import java.util.HashMap;
025import java.util.Iterator;
026import java.util.Locale;
027import java.util.Map;
028import javax.swing.event.EventListenerList;
029import org.jdtaus.core.container.ContainerFactory;
030import org.jdtaus.core.logging.spi.Logger;
031import org.jdtaus.core.monitor.Task;
032import org.jdtaus.core.monitor.TaskEvent;
033import org.jdtaus.core.monitor.TaskListener;
034import org.jdtaus.core.monitor.spi.TaskMonitor;
035import org.jdtaus.core.text.Message;
036
037/**
038 * jDTAUS Core SPI {@code TaskMonitor} reference implementation.
039 * <p>The reference implementation uses a thread checking the state of all tasks
040 * in the system periodically which is started upon initialization and runs
041 * endlessly. Monitoring is controlled by property {@code pollIntervalMillis}
042 * specifying the milliseconds of one period. Each time a period ends, tasks
043 * are checked for state changes and corresponding events are fired. Property
044 * {@code pollIntervalMillis} defaults to {@code 250ms}.</p>
045 *
046 * <p><b>Note:</b><br/>
047 * {@code TaskEvent}s of type {@code STARTED} and {@code ENDED} are fired by the
048 * thread executing the task's operation. Since tasks are monitored
049 * asynchronously, {@code TaskEvent}s of type {@code CHANGED_STATE} are fired by
050 * the monitor thread, not by the thread executing the task's operation. Make
051 * sure {@code TaskListener} implementations are prepared for being notified
052 * by a different thread than the one executing a task's operation.</p>
053 *
054 * @author <a href="mailto:cs@schulte.it">Christian Schulte</a>
055 * @version $JDTAUS: DefaultTaskMonitor.java 8641 2012-09-27 06:45:17Z schulte $
056 *
057 * @see org.jdtaus.core.container.Container
058 */
059public class DefaultTaskMonitor implements TaskMonitor
060{
061    //--Constructors------------------------------------------------------------
062
063// <editor-fold defaultstate="collapsed" desc=" Generated Code ">//GEN-BEGIN:jdtausConstructors
064    // This section is managed by jdtaus-container-mojo.
065
066    /** Standard implementation constructor <code>org.jdtaus.core.monitor.ri.DefaultTaskMonitor</code>. */
067    public DefaultTaskMonitor()
068    {
069        super();
070    }
071
072// </editor-fold>//GEN-END:jdtausConstructors
073
074    //------------------------------------------------------------Constructors--
075    //--Dependencies------------------------------------------------------------
076
077// <editor-fold defaultstate="collapsed" desc=" Generated Code ">//GEN-BEGIN:jdtausDependencies
078    // This section is managed by jdtaus-container-mojo.
079
080    /**
081     * Gets the configured <code>Logger</code> implementation.
082     *
083     * @return The configured <code>Logger</code> implementation.
084     */
085    private Logger getLogger()
086    {
087        return (Logger) ContainerFactory.getContainer().
088            getDependency( this, "Logger" );
089
090    }
091
092    /**
093     * Gets the configured <code>TaskListener</code> implementation.
094     *
095     * @return The configured <code>TaskListener</code> implementation.
096     */
097    private TaskListener[] getTaskListener()
098    {
099        return (TaskListener[]) ContainerFactory.getContainer().
100            getDependency( this, "TaskListener" );
101
102    }
103
104    /**
105     * Gets the configured <code>Locale</code> implementation.
106     *
107     * @return The configured <code>Locale</code> implementation.
108     */
109    private Locale getLocale()
110    {
111        return (Locale) ContainerFactory.getContainer().
112            getDependency( this, "Locale" );
113
114    }
115
116// </editor-fold>//GEN-END:jdtausDependencies
117
118    //------------------------------------------------------------Dependencies--
119    //--Properties--------------------------------------------------------------
120
121// <editor-fold defaultstate="collapsed" desc=" Generated Code ">//GEN-BEGIN:jdtausProperties
122    // This section is managed by jdtaus-container-mojo.
123
124    /**
125     * Gets the value of property <code>defaultPollIntervalMillis</code>.
126     *
127     * @return Default number of milliseconds per poll interval.
128     */
129    private java.lang.Long getDefaultPollIntervalMillis()
130    {
131        return (java.lang.Long) ContainerFactory.getContainer().
132            getProperty( this, "defaultPollIntervalMillis" );
133
134    }
135
136// </editor-fold>//GEN-END:jdtausProperties
137
138    //--------------------------------------------------------------Properties--
139    //--TaskEventSource---------------------------------------------------------
140
141    public void addTaskListener( final TaskListener listener )
142    {
143        if ( listener == null )
144        {
145            throw new NullPointerException( "listener" );
146        }
147
148        this.taskListeners.add( TaskListener.class, listener );
149    }
150
151    public void removeTaskListener( final TaskListener listener )
152    {
153        if ( listener == null )
154        {
155            throw new NullPointerException( "listener" );
156        }
157
158        this.taskListeners.remove( TaskListener.class, listener );
159    }
160
161    public TaskListener[] getTaskListeners()
162    {
163        return (TaskListener[]) this.taskListeners.getListeners(
164            TaskListener.class );
165
166    }
167
168    //---------------------------------------------------------TaskEventSource--
169    //--TaskMonitor-------------------------------------------------------------
170
171    public void monitor( final Task task )
172    {
173        if ( task == null )
174        {
175            throw new NullPointerException( "task" );
176        }
177
178        synchronized ( this.stateMap )
179        {
180            this.checkMonitorThread();
181            this.createTaskState( task );
182            this.fireTaskEvent( new TaskEvent( task, TaskEvent.STARTED ) );
183        }
184    }
185
186    public void finish( final Task task )
187    {
188        if ( task == null )
189        {
190            throw new NullPointerException( "task" );
191        }
192
193        synchronized ( this.stateMap )
194        {
195            if ( this.changedState( task ) )
196            {
197                this.fireTaskEvent( new TaskEvent( task,
198                                                   TaskEvent.CHANGED_STATE ) );
199
200            }
201
202            this.removeTaskState( task );
203            this.fireTaskEvent( new TaskEvent( task, TaskEvent.ENDED ) );
204        }
205    }
206
207    //-------------------------------------------------------------TaskMonitor--
208    //--DefaultTaskMonitor------------------------------------------------------
209
210    /** List of {@code TaskListener}s. */
211    private final EventListenerList taskListeners = new EventListenerList();
212
213    /** The thread monitoring tasks. */
214    private MonitorThread monitorThread;
215
216    /** Maps {@code Task}s to corresponding {@code TaskState} instances. */
217    private final Map stateMap = new HashMap( 1000 );
218
219    /** Number of milliseconds per poll interval. */
220    private Long pollIntervalMillis;
221
222    /**
223     * Creates a new {@code DefaultTaskMonitor} instance taking the
224     * milliseconds of one period.
225     *
226     * @param pollIntervalMillis the number of milliseconds per poll interval.
227     */
228    public DefaultTaskMonitor( final long pollIntervalMillis )
229    {
230        if ( pollIntervalMillis > 0L )
231        {
232            this.pollIntervalMillis = new Long( pollIntervalMillis );
233        }
234    }
235
236    /**
237     * Gets the value of property {@code pollIntervalMillis}.
238     *
239     * @return the number of milliseconds per poll interval.
240     */
241    private long getPollIntervalMillis()
242    {
243        if ( this.pollIntervalMillis == null )
244        {
245            this.pollIntervalMillis = this.getDefaultPollIntervalMillis();
246        }
247
248        return this.pollIntervalMillis.longValue();
249    }
250
251    /** Caches the state of a task. */
252    private static final class TaskState
253    {
254
255        boolean indeterminate;
256
257        boolean cancelable;
258
259        boolean cancelled;
260
261        int minimum;
262
263        int maximum;
264
265        int progress;
266
267        Message progressDescription;
268
269    }
270
271    /** Thread monitoring all currently running {@code Task}s for changes. */
272    private class MonitorThread extends Thread
273    {
274
275        /** Milliseconds per poll interval. */
276        private final long pollIntervalMillis;
277
278        /** Creates a new {@code MonitorThread} instance. */
279        private MonitorThread( final long pollIntervalMillis )
280        {
281            super( "DefaultTaskMonitor" );
282            this.pollIntervalMillis = pollIntervalMillis;
283        }
284
285        /** {@inheritDoc} */
286        public void run()
287        {
288            while ( true )
289            {
290                try
291                {
292                    Thread.sleep( this.pollIntervalMillis );
293                    this.checkTasks();
294                }
295                catch ( InterruptedException e )
296                {
297                    this.checkTasks();
298                }
299            }
300        }
301
302        public void start()
303        {
304            super.start();
305            getLogger().debug( getThreadStartedMessage(
306                getLocale(), new Long( this.pollIntervalMillis ) ) );
307
308        }
309
310        /**
311         * Checks the state of all currently running tasks for changes and
312         * fires corresponding events.
313         */
314        private void checkTasks()
315        {
316            synchronized ( DefaultTaskMonitor.this.stateMap )
317            {
318                for ( Iterator it = DefaultTaskMonitor.this.stateMap.keySet().
319                    iterator(); it.hasNext(); )
320                {
321                    final Task task = (Task) it.next();
322                    if ( changedState( task ) )
323                    {
324                        fireTaskEvent( new TaskEvent(
325                            task, TaskEvent.CHANGED_STATE ) );
326
327                    }
328                }
329            }
330        }
331
332    }
333
334    /**
335     * Gets the monitor thread.
336     *
337     * @return the monitor thread.
338     */
339    private synchronized void checkMonitorThread()
340    {
341        if ( this.monitorThread == null )
342        {
343            this.monitorThread =
344            new MonitorThread( this.getPollIntervalMillis() );
345
346            this.monitorThread.start();
347        }
348
349        if ( !this.monitorThread.isAlive() )
350        { // Monitoring died for some reason; start a new thread.
351            this.getLogger().warn( this.getThreadDiedMessage(
352                this.getLocale() ) );
353
354            this.monitorThread =
355            new MonitorThread( this.getPollIntervalMillis() );
356
357            this.monitorThread.start();
358        }
359    }
360
361    /**
362     * Notifies all registered {@code TaskListener}s about {@code TaskEvent}s.
363     *
364     * @param e The event to be provided to the listeners.
365     */
366    private void fireTaskEvent( final TaskEvent e )
367    {
368        if ( e == null )
369        {
370            throw new NullPointerException( "e" );
371        }
372
373
374        final Object[] listeners = this.taskListeners.getListenerList();
375        for ( int i = listeners.length - 2; i >= 0; i -= 2 )
376        {
377            if ( listeners[i] == TaskListener.class )
378            {
379                ( (TaskListener) listeners[i + 1] ).onTaskEvent( e );
380            }
381        }
382
383        final TaskListener[] taskListener = this.getTaskListener();
384        for ( int i = taskListener.length - 1; i >= 0; i-- )
385        {
386            taskListener[i].onTaskEvent( e );
387        }
388    }
389
390    /**
391     * Caches the state of a {@code Task}.
392     *
393     * @param task the task to cache state for.
394     *
395     * @throws NullPointerException if {@code task} is {@code null}.
396     * @throws IllegalStateException if the cache already holds state for
397     * {@code task}.
398     */
399    private void createTaskState( final Task task )
400    {
401        final TaskState state = new TaskState();
402        state.cancelable = task.isCancelable();
403        state.indeterminate = task.isIndeterminate();
404        state.cancelled = state.cancelable
405                          ? task.isCancelled()
406                          : false;
407
408        state.progressDescription = task.getProgressDescription();
409
410        if ( state.indeterminate )
411        {
412            state.maximum = Integer.MIN_VALUE;
413            state.minimum = Integer.MIN_VALUE;
414            state.progress = Integer.MIN_VALUE;
415        }
416        else
417        {
418            state.maximum = task.getMaximum();
419            state.minimum = task.getMinimum();
420            state.progress = task.getProgress();
421        }
422
423        if ( this.stateMap.put( task, state ) != null )
424        {
425            throw new IllegalStateException( this.getTaskAlreadyStartedMessage(
426                this.getLocale(),
427                task.getDescription().getText( this.getLocale() ),
428                new Date( task.getTimestamp() ) ) );
429
430        }
431    }
432
433    /**
434     * Removes the cached state of a {@code Task}.
435     *
436     * @param task the task to remove the cached state of.
437     *
438     * @throws NullPointerException if {@code task} is {@code null}.
439     */
440    private void removeTaskState( final Task task )
441    {
442        if ( task == null )
443        {
444            throw new NullPointerException( "task" );
445        }
446
447        this.stateMap.remove( task );
448    }
449
450    /**
451     * Checks the state of a given task for changes.
452     *
453     * @param task the task to check for state changes.
454     *
455     * @return {@code true} if the state of {@code task} changed since the last
456     * time this method got called; {@code false} if the state did not change.
457     *
458     * @throws NullPointerException if {@code task} is {@code null}.
459     * @throws IllegalStateException if no cached state exists for {@code task}.
460     */
461    private boolean changedState( final Task task )
462    {
463        if ( task == null )
464        {
465            throw new NullPointerException( "task" );
466        }
467
468
469        boolean changedState = false;
470        final TaskState state = (TaskState) this.stateMap.get( task );
471
472        if ( state == null )
473        {
474            throw new IllegalStateException();
475        }
476
477        if ( state.indeterminate )
478        {
479            state.indeterminate = task.isIndeterminate();
480            if ( !state.indeterminate )
481            {
482                state.minimum = task.getMinimum();
483                state.maximum = task.getMaximum();
484                state.progress = task.getProgress();
485                changedState = true;
486            }
487        }
488        else
489        {
490            state.indeterminate = task.isIndeterminate();
491            if ( state.indeterminate )
492            {
493                changedState = true;
494            }
495            else
496            {
497                if ( state.minimum != task.getMinimum() )
498                {
499                    state.minimum = task.getMinimum();
500                    changedState = true;
501                }
502                if ( state.maximum != task.getMaximum() )
503                {
504                    state.maximum = task.getMaximum();
505                    changedState = true;
506                }
507                if ( state.progress != task.getProgress() )
508                {
509                    state.progress = task.getProgress();
510                    changedState = true;
511                }
512            }
513        }
514
515        if ( state.cancelable )
516        {
517            state.cancelable = task.isCancelable();
518            if ( !state.cancelable )
519            {
520                changedState = true;
521            }
522            else
523            {
524                if ( state.cancelled != task.isCancelled() )
525                {
526                    state.cancelled = task.isCancelled();
527                    changedState = true;
528                }
529            }
530        }
531        else
532        {
533            state.cancelable = task.isCancelable();
534            if ( !state.cancelable )
535            {
536                state.cancelled = false;
537                changedState = true;
538            }
539        }
540
541        if ( state.progressDescription != task.getProgressDescription() )
542        {
543            state.progressDescription = task.getProgressDescription();
544            changedState = true;
545        }
546        else if ( state.progressDescription != null &&
547                  !state.progressDescription.getText( this.getLocale() ).
548            equals( task.getProgressDescription().getText(
549            this.getLocale() ) ) )
550        {
551            changedState = true;
552        }
553
554        return changedState;
555    }
556
557    //------------------------------------------------------DefaultTaskMonitor--
558    //--Messages----------------------------------------------------------------
559
560// <editor-fold defaultstate="collapsed" desc=" Generated Code ">//GEN-BEGIN:jdtausMessages
561    // This section is managed by jdtaus-container-mojo.
562
563    /**
564     * Gets the text of message <code>threadStarted</code>.
565     * <blockquote><pre>Neuen Thread gestartet. Abtastperiode {0,number}ms.</pre></blockquote>
566     * <blockquote><pre>New thread started. Period {0,number}ms.</pre></blockquote>
567     *
568     * @param locale The locale of the message instance to return.
569     * @param periodMillis Period of the started thread.
570     *
571     * @return Information about a started thread.
572     */
573    private String getThreadStartedMessage( final Locale locale,
574            final java.lang.Number periodMillis )
575    {
576        return ContainerFactory.getContainer().
577            getMessage( this, "threadStarted", locale,
578                new Object[]
579                {
580                    periodMillis
581                });
582
583    }
584
585    /**
586     * Gets the text of message <code>threadDied</code>.
587     * <blockquote><pre>Thread abnorm beendet. Konsultieren Sie die Protokolle für mögliche Ursachen.</pre></blockquote>
588     * <blockquote><pre>The monitor thread terminated abnormally. Consult the logs for any causes.</pre></blockquote>
589     *
590     * @param locale The locale of the message instance to return.
591     *
592     * @return Information about a dead thread.
593     */
594    private String getThreadDiedMessage( final Locale locale )
595    {
596        return ContainerFactory.getContainer().
597            getMessage( this, "threadDied", locale, null );
598
599    }
600
601    /**
602     * Gets the text of message <code>taskAlreadyStarted</code>.
603     * <blockquote><pre>Ein Vorgang mit Beschreibung {0} wurde bereits um {1,time,long} gestartet.</pre></blockquote>
604     * <blockquote><pre>A task with description {0} already has been started at {1,time,long}.</pre></blockquote>
605     *
606     * @param locale The locale of the message instance to return.
607     * @param taskDescription Description of the already running task.
608     * @param startTime Time the already running task got started.
609     *
610     * @return Information about an already running task.
611     */
612    private String getTaskAlreadyStartedMessage( final Locale locale,
613            final java.lang.String taskDescription,
614            final java.util.Date startTime )
615    {
616        return ContainerFactory.getContainer().
617            getMessage( this, "taskAlreadyStarted", locale,
618                new Object[]
619                {
620                    taskDescription,
621                    startTime
622                });
623
624    }
625
626// </editor-fold>//GEN-END:jdtausMessages
627
628    //----------------------------------------------------------------Messages--
629}