blob: 75d6fea7fa4c8c8d6f9db9ca690e49f680c4dad2 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2008, 2009 Oracle. All rights reserved.
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v1.0, which accompanies this distribution
* and is available at http://www.eclipse.org/legal/epl-v10.html.
*
* Contributors:
* Oracle - initial API and implementation
******************************************************************************/
package org.eclipse.jpt.utility.internal.synchronizers;
import java.util.Vector;
import org.eclipse.jpt.utility.Command;
import org.eclipse.jpt.utility.internal.CompositeException;
import org.eclipse.jpt.utility.internal.StringTools;
import org.eclipse.jpt.utility.internal.SynchronizedBoolean;
/**
* This synchronizer will perform synchronizations in a separate thread,
* allowing calls to {@link Synchronizer#synchronize()} to return immediately.
* <p>
* <strong>NB:</strong> The client-supplied command should handle any exceptions
* appropriately (e.g. log the exception and return gracefully so the thread
* can continue the synchronization process).
*/
public class AsynchronousSynchronizer
implements Synchronizer
{
/**
* This flag is shared with the synchronization thread. Setting it to true
* will trigger the synchronization to begin or, if the synchronization is
* currently executing, to execute again, once the current execution is
* complete.
*/
final SynchronizedBoolean synchronizeFlag = new SynchronizedBoolean(false);
/**
* The runnable passed to the synchronization thread each time it is built.
*/
private final Runnable runnable;
/**
* Optional, client-supplied name for the synchronization thread.
* If null, allow the JDK to assign a name.
*/
private final String threadName;
/**
* The synchronization is performed on this thread. A new thread is built
* for every start/stop cycle (since a thread cannot be started more than
* once).
*/
private Thread thread;
/**
* A list of the uncaught exceptions thrown by the command.
*/
final Vector<Throwable> exceptions = new Vector<Throwable>();
// ********** construction **********
/**
* Construct an asynchronous synchronizer that uses the specified command to
* perform the synchronization. Allow the generated thread(s) to be assigned
* JDK-generated names.
*/
public AsynchronousSynchronizer(Command command) {
this(command, null);
}
/**
* Construct an asynchronous synchronizer that uses the specified command to
* perform the synchronization. Assign the generated thread(s) the specified
* name.
*/
public AsynchronousSynchronizer(Command command, String threadName) {
super();
this.runnable = this.buildRunnable(command);
this.threadName = threadName;
}
Runnable buildRunnable(Command command) {
return new RunnableSynchronization(command);
}
// ********** Synchronizer implementation **********
/**
* Build and start the synchronization thread, but postpone the first
* synchronization until requested, i.e. via a call to
* {@link #synchronize()}.
* <p>
* Note: We don't clear the "synchronize" flag here; so if the flag has
* been set <em>before</em> getting here, the first synchronization will
* start promptly (albeit, asynchronously).
* The "synchronize" flag will be set if:<ul>
* <li>{@link #synchronize()} was called after the synchronizer was
* constructed but before {@link #start()} was called; or
* <li>{@link #synchronize()} was called after {@link #stop()} was called
* but before {@link #start()} was called (to restart the synchronizer); or
* <li>{@link #stop()} was called when there was an outstanding request
* for a synchronization (i.e. the "synchronization" flag was set at
* the time {@link #stop()} was called)
* </ul>
*/
public synchronized void start() {
if (this.thread != null) {
throw new IllegalStateException("The Synchronizer was not stopped."); //$NON-NLS-1$
}
this.thread = this.buildThread();
this.thread.start();
}
private Thread buildThread() {
Thread t = new Thread(this.runnable);
if (this.threadName != null) {
t.setName(this.threadName);
}
return t;
}
/**
* Set the "synchronize" flag so the synchronization thread will either<ul>
* <li>if the thread is quiesced, start a synchronization immediately, or
* <li>if the thread is currently executing a synchronization, execute another
* synchronization once the current synchronization is complete
* </ul>
*/
public void synchronize() {
this.synchronizeFlag.setTrue();
}
/**
* Interrupt the synchronization thread so that it stops executing at the
* end of the current synchronization. Suspend the current thread until
* the synchronization thread is finished executing. If any uncaught
* exceptions were thrown while the synchronization thread was executing,
* wrap them in a composite exception and throw the composite exception.
*/
public synchronized void stop() {
if (this.thread == null) {
throw new IllegalStateException("The Synchronizer was not started."); //$NON-NLS-1$
}
this.thread.interrupt();
try {
this.thread.join();
} catch (InterruptedException ex) {
// the thread that called #stop() was interrupted while waiting to
// join the synchronization thread - ignore;
// 'thread' is still "interrupted", so its #run() loop will still stop
// after its current execution - we just won't wait around for it...
}
this.thread = null;
if (this.exceptions.size() > 0) {
Throwable[] temp = this.exceptions.toArray(new Throwable[this.exceptions.size()]);
this.exceptions.clear();
throw new CompositeException(temp);
}
}
@Override
public String toString() {
return StringTools.buildToStringFor(this, this.thread);
}
// ********** synchronization thread runnable **********
/**
* This implementation of {@link Runnable} will execute a client-supplied command.
* It will wait until a shared "synchronize" flag is set to execute the
* command. Once the the comand is executed, the thread will quiesce until
* the flag is set again. If the flag was set during the execution of the
* command (either recursively by the command itself or by another thread),
* the command will be re-executed immediately. Stop the thread by calling
* {@link Thread#interrupt()}.
*/
class RunnableSynchronization
implements Runnable
{
/** The client-supplied command that executes on this thread. */
private final Command command;
RunnableSynchronization(Command command) {
super();
if (command == null) {
throw new NullPointerException();
}
this.command = command;
}
/**
* Loop while this thread has not been interrupted by another thread.
* In each loop: Wait until the "synchronize" flag is set,
* then clear it and execute the command. If the
* "synchronize" flag was set <em>during</em> the synchronization,
* there will be no "wait" before beginning the next synchronization
* (thus the call to {@link Thread#isInterrupted()} before each cycle).
* <p>
* If this thread is interrupted <em>during</em> the synchronization, the
* call to {@link Thread#interrupted()} will stop the loop. If this thread is
* interrupted during the call to {@link SynchronizedBoolean#waitToSetFalse()},
* we will catch the {@link InterruptedException} and stop the loop.
*/
public void run() {
while ( ! Thread.interrupted()) {
try {
AsynchronousSynchronizer.this.synchronizeFlag.waitToSetFalse();
} catch (InterruptedException ex) {
// we were interrupted while waiting, must be Quittin' Time
return;
}
this.execute();
}
}
/**
* Execute the client-supplied command. Do not allow any unhandled
* exceptions to kill the thread. Store them up for later pain.
*/
private void execute() {
try {
this.execute_();
} catch (Throwable ex) {
AsynchronousSynchronizer.this.exceptions.add(ex);
}
}
/**
* Execute the client-supplied command.
*/
void execute_() {
this.command.execute();
}
}
}