package ch.cyberduck.core.threading;
/*
* Copyright (c) 2006 David Kocher. All rights reserved.
* http://cyberduck.ch/
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 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 General Public License for more details.
*
* Bug fixes, suggestions and comments should be sent to:
* dkocher@cyberduck.ch
*/
import ch.cyberduck.core.*;
import ch.cyberduck.core.i18n.Locale;
//import ch.cyberduck.ui.growl.Growl;
import org.apache.log4j.Logger;
//import com.enterprisedt.net.ftp.FTPNullReplyException;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.text.MessageFormat;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
/**
* @version $Id: RepeatableBackgroundAction.java 5625 2009-12-19 21:39:15Z dkocher $
*/
public abstract class RepeatableBackgroundAction extends AbstractBackgroundAction implements ErrorListener, TranscriptListener {
private static Logger log = Logger.getLogger(RepeatableBackgroundAction.class);
/**
* @param exception
* @see ch.cyberduck.core.ErrorListener
*/
public void error(final BackgroundException exception) {
// Do not report an error when the action was canceled intentionally
Throwable cause = exception.getCause();
if(cause instanceof ConnectionCanceledException) {
log.warn(cause.getMessage());
// Do not report as failed if instanceof ConnectionCanceledException
return;
}
final String description = null == exception.getPath() ? exception.getSession().getHost().getHostname() : exception.getPath().getName();
if(exceptions.size() < Preferences.instance().getInteger("growl.limit")) {
// Growl.instance().notify(exception.getMessage(), description);
}
exceptions.add(exception);
}
/**
* Contains the transcript of the session while this action was running
* @uml.property name="transcript"
*/
protected StringBuilder transcript;
/**
* Maximum transcript buffer
*/
private static final int TRANSCRIPT_MAX_LENGTH =
Preferences.instance().getInteger("transcript.length");
/**
* @param request
* @param message @see ch.cyberduck.core.TranscriptListener
*/
public void log(boolean request, String message) {
if(transcript.length() > TRANSCRIPT_MAX_LENGTH) {
transcript = new StringBuilder();
}
transcript.append(message).append("\n");
}
public RepeatableBackgroundAction() {
this.exceptions = new Collection<BackgroundException>();
}
@Override
public boolean prepare() {
final Session session = this.getSession();
if(session != null) {
session.addErrorListener(this);
session.addTranscriptListener(this);
}
// Clear the transcript and exceptions
transcript = new StringBuilder();
return super.prepare();
}
/**
* To be overriden in concrete subclass
* @return The session if any
* @uml.property name="session"
* @uml.associationEnd readOnly="true"
*/
protected abstract Session getSession();
/**
* @uml.property name="repeatAttempts"
*/
private final int repeatAttempts = Preferences.instance().getInteger("connection.retry");
/**
* The number of times a new connection attempt should be made. Takes into
* account the number of times already tried.
*
* @return Greater than zero if a failed action should be repeated again
*/
public int retry() {
if(!this.isCanceled()) {
for(BackgroundException e : exceptions) {
final Throwable cause = e.getCause();
// Check for an exception we consider possibly temporary
if(cause instanceof SocketException
|| cause instanceof SocketTimeoutException
|| cause instanceof UnknownHostException) {
// || cause instanceof FTPNullReplyException) {
// The initial connection attempt does not count
return repeatAttempts - repeatCount;
}
}
}
return 0;
}
/**
* Contains all exceptions thrown while this action was running
* @uml.property name="exceptions"
* @uml.associationEnd multiplicity="(0 -1)" elementType="ch.cyberduck.core.threading.BackgroundException"
*/
protected List<BackgroundException> exceptions;
protected boolean hasFailed() {
return this.exceptions.size() > 0;
}
/**
* The number of times this action has been run
* @uml.property name="repeatCount"
*/
protected int repeatCount;
@Override
public void finish() {
while(this.hasFailed() && this.retry() > 0) {
log.info("Retry failed background action:" + this);
// This is a automated retry. Wait some time first.
this.pause();
if(!this.isCanceled()) {
repeatCount++;
exceptions.clear();
// Re-run the action with the previous lock used
this.run();
}
}
final Session session = this.getSession();
if(session != null) {
// It is important _not_ to do this in #cleanup as otherwise
// the listeners are still registered when the next BackgroundAction
// is already running
session.removeTranscriptListener(this);
session.removeErrorListener(this);
}
super.finish();
}
/**
* Idle this action for some time. Blocks the caller.
*/
public void pause() {
if(0 == Preferences.instance().getInteger("connection.retry.delay")) {
log.info("No pause between retry");
return;
}
final Timer wakeup = new Timer();
final CyclicBarrier wait = new CyclicBarrier(2);
wakeup.scheduleAtFixedRate(new TimerTask() {
/**
* The delay to wait before execution of the action in seconds
*/
private int delay = (int) Preferences.instance().getDouble("connection.retry.delay");
private final String pattern = Locale.localizedString("Retry again in {0} seconds ({1} more attempts)", "Status");
@Override
public void run() {
if(0 == delay || RepeatableBackgroundAction.this.isCanceled()) {
// Cancel the timer repetition
this.cancel();
return;
}
final Session session = getSession();
if(session != null) {
session.message(MessageFormat.format(pattern, delay--, RepeatableBackgroundAction.this.retry()));
}
}
@Override
public boolean cancel() {
try {
// Notifiy to return to caller from #pause()
wait.await();
}
catch(InterruptedException e) {
log.error(e.getMessage());
}
catch(BrokenBarrierException e) {
log.error(e.getMessage());
}
return super.cancel();
}
}, 0, 1000); // Schedule for immediate execusion with an interval of 1s
try {
// Wait for notify from wakeup timer
wait.await();
}
catch(InterruptedException e) {
log.error(e.getMessage());
}
catch(BrokenBarrierException e) {
log.error(e.getMessage());
}
}
@Override
public Object lock() {
return this.getSession();
}
@Override
public String toString() {
final Session session = this.getSession();
if(session != null) {
return session.getHost().getHostname();
}
return Locale.localizedString("Unknown");
}
}