/*
* sulky-modules - several general-purpose modules.
* Copyright (C) 2007-2015 Joern Huxhorn
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* Copyright 2007-2015 Joern Huxhorn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.huxhorn.sulky.tasks;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <p>This class is an abstract implementation of the ProgressingCallable interface.</p>
*
* <p>Extending classes only call setNumberOfSteps and setCurrentStep whenever
* necessary. The progress is automatically calculated.</p>
*
* <p>The registered PropertyChangeListeners are called from the calculating thread
* not from the event dispatch thread. This is perfectly ok since TaskManager
* transforms those changes into ResultListener calls that are guaranteed
* to be executed on the event dispatch thread if usingEventQueue is set to true.</p>
*
* <p>The constructors with initialSleepSteps and laterSleepSteps arguments are recommended for longer
* operations. setCurrentStep will sleep for 1ms every time initialSleepSteps number of steps have been
* processed. If laterSleepSteps is defined this value is used if more than 5*initialSleepSteps steps have
* been processed.</p>
*
* <p>It can be useful to use a smaller value for initialSleepSteps to support faster cancellation at the start of an
* operation, e.g. in case of an accidental start by the user, while using a larger value for laterSleepSteps
* after an initial warm-up-period for performance reason (switching threads is expensive).</p>
*
* <p>The sleep itself is necessary to allow cancellation from the executor. The InterruptedException should not
* be caught by the caller. If it is caught, e.g. to perform some cleanup, care should be taken to leave
* the call-method at the earliest possible time.</p>
*
* @param <T> the type of the result.
* @see java.util.concurrent.Callable
* @see de.huxhorn.sulky.tasks.ProgressingCallable
*/
public abstract class AbstractProgressingCallable<T>
implements ProgressingCallable<T>
{
private final Logger logger = LoggerFactory.getLogger(AbstractProgressingCallable.class);
private PropertyChangeSupport changeSupport;
private int progress;
private long numberOfSteps;
private long currentStep;
private long initialSleepSteps;
private long laterSleepSteps;
private long lastSleepStep;
private final ReentrantReadWriteLock rwLock;
public AbstractProgressingCallable()
{
this(0, 0);
}
public AbstractProgressingCallable(long sleepSteps)
{
this(sleepSteps, 0);
}
public AbstractProgressingCallable(long initialSleepSteps, long laterSleepSteps)
{
rwLock = new ReentrantReadWriteLock(true);
this.changeSupport = new PropertyChangeSupport(this);
this.progress = -1;
this.numberOfSteps = 0;
this.initialSleepSteps = initialSleepSteps;
this.laterSleepSteps = laterSleepSteps;
this.lastSleepStep = 0;
}
/**
* Sets the number of steps required to complete the task. A number <= 0 means that the number of steps are not
* (yet) known and will result in a progress of -1.
*
* @param numberOfSteps the number of steps to complete the task.
*/
protected void setNumberOfSteps(long numberOfSteps)
{
if(this.numberOfSteps != numberOfSteps)
{
this.numberOfSteps = numberOfSteps;
calculateProgress();
}
}
protected void setCurrentStep(long currentStep)
throws InterruptedException
{
if(this.currentStep != currentStep)
{
this.currentStep = currentStep;
calculateProgress();
if(currentStep != 0 && initialSleepSteps > 0)
{
if(laterSleepSteps > 0 && currentStep > initialSleepSteps * 5)
{
if(lastSleepStep + laterSleepSteps < currentStep)
{
lastSleepStep = currentStep;
if(Thread.interrupted())
{
throw new InterruptedException();
}
}
}
else if(lastSleepStep + initialSleepSteps < currentStep)
{
lastSleepStep = currentStep;
if(Thread.interrupted())
{
throw new InterruptedException();
}
}
}
}
}
private void calculateProgress()
{
int newProgress = -1;
if(numberOfSteps > 0)
{
newProgress = (int) (((double) currentStep / (double) numberOfSteps) * 100);
}
setProgress(newProgress);
}
/**
* Fires the PropertyChangeEvent if required - as defined in the interface.
*
* @param progress the new progress.
*/
private void setProgress(int progress)
{
ReentrantReadWriteLock.WriteLock lock = rwLock.writeLock();
lock.lock();
try
{
if(this.progress != progress)
{
if(logger.isDebugEnabled()) logger.debug("New progress: {}", progress);
Object oldValue = this.progress;
this.progress = progress;
Object newValue = this.progress;
changeSupport.firePropertyChange(PROGRESS_PROPERTY_NAME, oldValue, newValue);
}
}
finally
{
lock.unlock();
}
}
/**
* Returns values between 0 and 100, or a negative value indicating an unknown progress.
*
* @return the progress of the operation.
*/
public int getProgress()
{
ReentrantReadWriteLock.ReadLock lock = rwLock.readLock();
lock.lock();
try
{
return progress;
}
finally
{
lock.unlock();
}
}
public void addPropertyChangeListener(PropertyChangeListener listener)
{
changeSupport.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener)
{
changeSupport.removePropertyChangeListener(listener);
}
}