| /******************************************************************************* |
| * Copyright (c) 2008, 2012 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.common.utility.internal.synchronizers; |
| |
| import java.util.Vector; |
| import org.eclipse.jpt.common.utility.command.Command; |
| import org.eclipse.jpt.common.utility.internal.CompositeException; |
| import org.eclipse.jpt.common.utility.internal.StringTools; |
| import org.eclipse.jpt.common.utility.internal.SynchronizedObject; |
| import org.eclipse.jpt.common.utility.synchronizers.Synchronizer; |
| |
| /** |
| * This synchronizer will synchronize immediately and not return until the |
| * synchronization and any nested (recursive) synchronizations are complete. |
| * In some situations this implementation should be used sparingly, and for as |
| * short a time as possible, as it increases the probability of deadlocks. A |
| * deadlock can occur when {@link Synchronizer#synchronize()} is called from multiple |
| * threads and multiple resources are locked by the synchronization in varying |
| * orders. |
| * <p> |
| * As defined in the {@link Synchronizer} interface, {@link Synchronizer#start()} |
| * and {@link Synchronizer#stop()} |
| * should be called in the same thread, but it is not required. |
| * {@link Synchronizer#synchronize()} should |
| * always be called in the same thread (i.e. only recursively, beyond the |
| * initial call); although, this too is not required. |
| * This thread need not be the same thread that executes |
| * {@link Synchronizer#start()} and {@link Synchronizer#stop()}. |
| */ |
| public class SynchronousSynchronizer |
| implements Synchronizer |
| { |
| /** |
| * The client-supplied command that performs the synchronization. It may |
| * trigger further calls to {@link #synchronize()} (i.e. the |
| * synchronization may recurse). |
| */ |
| private final Command command; |
| |
| /** |
| * The synchronizer's current state. |
| */ |
| final SynchronizedObject<State> state; |
| |
| /** |
| * The synchronizer's initial state is {@link #STOPPED}. |
| */ |
| enum State { |
| STOPPED, |
| READY, |
| EXECUTING, |
| REPEAT, |
| STOPPING |
| } |
| |
| /** |
| * A list of the uncaught exceptions thrown by the command. |
| */ |
| final Vector<Throwable> exceptions = new Vector<Throwable>(); |
| |
| |
| // ********** construction ********** |
| |
| /** |
| * Construct a synchronous synchronizer that uses the specified command to |
| * perform the synchronization. |
| */ |
| public SynchronousSynchronizer(Command command) { |
| super(); |
| if (command == null) { |
| throw new NullPointerException(); |
| } |
| this.command = command; |
| // use the synchronizer as the mutex so it is freed up by the wait in #stop() |
| this.state = new SynchronizedObject<State>(State.STOPPED, this); |
| } |
| |
| |
| // ********** Synchronizer implementation ********** |
| |
| /** |
| * Set the synchronizer's {@link #state} to {@link State#READY READY} |
| * and execute the first synchronization. Throw an exception if the |
| * synchronizer is not {@link State#STOPPED STOPPED}. |
| */ |
| public synchronized void start() { |
| switch (this.state.getValue()) { |
| case STOPPED: |
| this.state.setValue(State.READY); |
| this.synchronize(); |
| break; |
| case READY: |
| case EXECUTING: |
| case REPEAT: |
| case STOPPING: |
| throw this.buildISE(); |
| } |
| } |
| |
| /** |
| * It's possible to come back here if the synchronization command recurses |
| * and triggers another synchronization. |
| */ |
| public void synchronize() { |
| if (this.beginSynchronization()) { |
| this.synchronize_(); |
| } |
| } |
| |
| /** |
| * A client has requested a synchronization. |
| * Return whether we can begin a new synchronization. |
| * If a synchronization is already under way, return <code>false</code>; |
| * but set the {@link #state} to {@link State#REPEAT REPEAT} |
| * so another synchronization will occur once the current |
| * synchronization is complete. |
| */ |
| private synchronized boolean beginSynchronization() { |
| switch (this.state.getValue()) { |
| case STOPPED: |
| // synchronization is not allowed |
| return false; |
| case READY: |
| // begin a new synchronization |
| this.state.setValue(State.EXECUTING); |
| return true; |
| case EXECUTING: |
| // set flag so a new synchronization will occur once the current one is finished |
| this.state.setValue(State.REPEAT); |
| return false; |
| case REPEAT: |
| // the "repeat" flag is already set |
| return false; |
| case STOPPING: |
| // no further synchronizations are allowed |
| return false; |
| } |
| throw this.buildISE(); |
| } |
| |
| /** |
| * This method should be called only once per set of "recursing" |
| * synchronizations. Any recursive call to {@link #synchronize()} will |
| * simply set the {@link #state} to {@link State#REPEAT REPEAT}, |
| * causing the command to execute again. |
| */ |
| private void synchronize_() { |
| do { |
| this.execute(); |
| } while (this.repeatSynchronization()); |
| } |
| |
| /** |
| * 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) { |
| this.exceptions.add(ex); |
| } |
| } |
| |
| /** |
| * By default, just execute the command. |
| */ |
| void execute_() { |
| this.command.execute(); |
| } |
| |
| /** |
| * The current synchronization has finished. |
| * Return whether we should begin another synchronization. |
| */ |
| private synchronized boolean repeatSynchronization() { |
| switch (this.state.getValue()) { |
| case STOPPED: |
| case READY: |
| throw this.buildISE(); |
| case EXECUTING: |
| // synchronization has finished and there are no outstanding requests for another; return to "ready" |
| this.state.setValue(State.READY); |
| return false; |
| case REPEAT: |
| // the "repeat" flag was set; clear it and start another synchronization |
| this.state.setValue(State.EXECUTING); |
| return true; |
| case STOPPING: |
| // a client has initiated a "stop"; mark the "stop" complete and perform no more synchronizations |
| this.state.setValue(State.STOPPED); |
| return false; |
| } |
| throw this.buildISE(); |
| } |
| |
| /** |
| * Set the flags so that no further synchronizations occur. If any uncaught |
| * exceptions were thrown while the synchronization was executing, |
| * wrap them in a composite exception and throw the composite exception. |
| */ |
| public synchronized void stop() { |
| switch (this.state.getValue()) { |
| case STOPPED: |
| throw this.buildISE(); |
| case READY: |
| // simply return to "stopped" state |
| this.state.setValue(State.STOPPED); |
| break; |
| case EXECUTING: |
| case REPEAT: |
| // set the "stopping" flag and wait until the synchronization has finished |
| this.state.setValue(State.STOPPING); |
| this.waitUntilStopped(); |
| break; |
| case STOPPING: |
| throw this.buildISE(); |
| } |
| |
| if (this.exceptions.size() > 0) { |
| Throwable[] temp = this.exceptions.toArray(new Throwable[this.exceptions.size()]); |
| this.exceptions.clear(); |
| throw new CompositeException(temp); |
| } |
| } |
| |
| /** |
| * This wait will free up the synchronizer's synchronized methods |
| * (since the synchronizer is the state's mutex). |
| */ |
| private void waitUntilStopped() { |
| try { |
| this.state.waitUntilValueIs(State.STOPPED); |
| } catch (InterruptedException ex) { |
| // the thread that called #stop() was interrupted while waiting |
| // for the synchronization to finish - ignore; |
| // 'state' is still set to 'STOPPING', so the #synchronize_() loop |
| // will still stop - we just won't wait around for it... |
| } |
| } |
| |
| private IllegalStateException buildISE() { |
| return new IllegalStateException("state: " + this.state); //$NON-NLS-1$ |
| } |
| |
| @Override |
| public String toString() { |
| return StringTools.buildToStringFor(this, this.state); |
| } |
| } |