blob: b1f07d41882b5f9b2f565a4385edd6a1f47224dc [file] [log] [blame]
/*
* Copyright (c) 2015, 2016 Eike Stepper (Berlin, Germany) and others.
* 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:
* Eike Stepper - initial API and implementation
*/
package org.eclipse.oomph.setup.ui.synchronizer;
import org.eclipse.oomph.internal.setup.SetupProperties;
import org.eclipse.oomph.internal.ui.UIPropertyTester;
import org.eclipse.oomph.setup.SetupTask;
import org.eclipse.oomph.setup.internal.core.SetupContext;
import org.eclipse.oomph.setup.internal.sync.DataProvider.Location;
import org.eclipse.oomph.setup.internal.sync.DataProvider.NotCurrentException;
import org.eclipse.oomph.setup.internal.sync.LocalDataProvider;
import org.eclipse.oomph.setup.internal.sync.RemoteDataProvider;
import org.eclipse.oomph.setup.internal.sync.SetupSyncPlugin;
import org.eclipse.oomph.setup.internal.sync.Synchronization;
import org.eclipse.oomph.setup.internal.sync.Synchronizer;
import org.eclipse.oomph.setup.internal.sync.SynchronizerAdapter;
import org.eclipse.oomph.setup.internal.sync.SynchronizerJob;
import org.eclipse.oomph.setup.internal.sync.SynchronizerListener;
import org.eclipse.oomph.setup.sync.SyncAction;
import org.eclipse.oomph.setup.sync.SyncActionType;
import org.eclipse.oomph.setup.sync.SyncPolicy;
import org.eclipse.oomph.setup.ui.SetupPropertyTester;
import org.eclipse.oomph.setup.ui.SetupUIPlugin;
import org.eclipse.oomph.ui.UIUtil;
import org.eclipse.oomph.util.LockFile;
import org.eclipse.oomph.util.PropertiesUtil;
import org.eclipse.oomph.util.PropertyFile;
import org.eclipse.emf.common.util.EMap;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.dialogs.ProgressMonitorDialog;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.userstorage.IStorage;
import org.eclipse.userstorage.IStorageService;
import org.eclipse.userstorage.StorageFactory;
import org.eclipse.userstorage.oauth.EclipseOAuthCredentialsProvider;
import org.eclipse.userstorage.spi.ICredentialsProvider;
import org.eclipse.userstorage.spi.StorageCache;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @author Eike Stepper
*/
public final class SynchronizerManager
{
public static final File SYNC_FOLDER = SetupSyncPlugin.INSTANCE.getUserLocation().toFile();
public static final SynchronizerManager INSTANCE = new SynchronizerManager();
public static final boolean ENABLED = !PropertiesUtil.isProperty(SetupProperties.PROP_SETUP_SYNC_SKIP);
private static final File USER_SETUP = new File(SetupContext.USER_SETUP_LOCATION_URI.toFileString());
private static final LockFile USER_SETUP_LOCK = new LockFile(new File(USER_SETUP.getParentFile(), USER_SETUP.getName() + ".lock"));
private static final PropertyFile CONFIG = new PropertyFile(SetupSyncPlugin.INSTANCE.getUserLocation().append("sync.properties").toFile());
private static final String CONFIG_SYNC_ENABLED = "sync.enabled";
private static final String CONFIG_CONNECTION_OFFERED = "connection.offered";
/**
* If set to <code>true</code> the {@link #CONFIG_CONNECTION_OFFERED} property is cleared before each access,
* which makes it easier to test/debug the opt-in workflow.
* <p>
* <b>Should never be committed with a <code>true</code> value!</b>
*/
private static final boolean DEBUG_CONNECTION_OFFERED = false;
private final IStorage storage;
private final RemoteDataProvider remoteDataProvider;
private Boolean connectionOffered;
private SynchronizerManager()
{
StorageCache cache = new RemoteDataProvider.SyncStorageCache(SYNC_FOLDER);
storage = StorageFactory.DEFAULT.create(RemoteDataProvider.APPLICATION_TOKEN, cache);
ICredentialsProvider credentialProvider = createCredentialProvider();
if (credentialProvider != null)
{
storage.setCredentialsProvider(credentialProvider);
}
remoteDataProvider = new RemoteDataProvider(storage);
}
public IStorage getStorage()
{
return storage;
}
public RemoteDataProvider getRemoteDataProvider()
{
return remoteDataProvider;
}
public boolean isSyncEnabled()
{
try
{
return Boolean.parseBoolean(CONFIG.getProperty(CONFIG_SYNC_ENABLED, "false"));
}
catch (Throwable ex)
{
SetupUIPlugin.INSTANCE.log(ex);
return false;
}
}
public boolean setSyncEnabled(boolean enabled)
{
boolean changed;
if (enabled)
{
changed = CONFIG.compareAndSetProperty(CONFIG_SYNC_ENABLED, "true", "false", null);
}
else
{
changed = CONFIG.compareAndSetProperty(CONFIG_SYNC_ENABLED, "false", "true");
}
if (changed)
{
try
{
UIPropertyTester.requestEvaluation(SetupPropertyTester.PREFIX + SetupPropertyTester.SYNC_ENABLED, false);
}
catch (Exception ex)
{
SetupUIPlugin.INSTANCE.log(ex);
}
}
return changed;
}
public Synchronizer createSynchronizer(File userSetup, File syncFolder)
{
LocalDataProvider localDataProvider = new LocalDataProvider(userSetup);
Synchronizer synchronizer = new Synchronizer(localDataProvider, remoteDataProvider, syncFolder);
synchronizer.setLockFile(USER_SETUP_LOCK);
synchronizer.addListener(new SkipHandler());
return synchronizer;
}
public SynchronizationController startSynchronization(boolean withProgressDialog, boolean withCredentialsPrompt, boolean deferLocal)
{
SynchronizationController controller = new SynchronizationController();
if (controller.start(withProgressDialog, withCredentialsPrompt, deferLocal))
{
return controller;
}
return null;
}
public Synchronization synchronize(boolean withProgressDialog, boolean withCredentialsPrompt, boolean deferLocal)
{
SynchronizationController synchronizationController = startSynchronization(withProgressDialog, withCredentialsPrompt, deferLocal);
if (synchronizationController != null)
{
return synchronizationController.await();
}
return null;
}
public Impact performSynchronization(Synchronization synchronization, boolean interactive, boolean remoteModifications)
{
try
{
EMap<String, SyncPolicy> policies = synchronization.getRemotePolicies();
Map<String, SyncAction> actions = synchronization.getActions();
Map<String, SyncAction> includedActions = new HashMap<String, SyncAction>();
for (Iterator<Map.Entry<String, SyncAction>> it = actions.entrySet().iterator(); it.hasNext();)
{
Map.Entry<String, SyncAction> entry = it.next();
String syncID = entry.getKey();
SyncAction syncAction = entry.getValue();
SyncPolicy policy = policies.get(syncID);
if (policy == SyncPolicy.EXCLUDE)
{
it.remove();
continue;
}
if (policy == null && !interactive)
{
it.remove();
continue;
}
SyncActionType type = syncAction.getComputedType();
switch (type)
{
case SET_LOCAL:
case REMOVE_LOCAL:
if (!remoteModifications)
{
// Ignore LOCAL -> REMOTE actions.
it.remove();
continue;
}
break;
case CONFLICT:
if (!interactive)
{
// Ignore interactive actions.
it.remove();
continue;
}
break;
case EXCLUDE:
case NONE:
// Should not occur.
it.remove();
continue;
}
if (policy == SyncPolicy.INCLUDE && type != SyncActionType.CONFLICT)
{
it.remove();
includedActions.put(syncID, syncAction);
}
}
if (!actions.isEmpty() || !includedActions.isEmpty())
{
if (!actions.isEmpty())
{
SynchronizerDialog dialog = new SynchronizerDialog(UIUtil.getShell(), null, synchronization);
if (dialog.open() != SynchronizerDialog.OK)
{
return null;
}
}
actions.putAll(includedActions);
try
{
synchronization.commit();
}
catch (NotCurrentException ex)
{
SetupUIPlugin.INSTANCE.log(ex, IStatus.INFO);
}
catch (IOException ex)
{
SetupUIPlugin.INSTANCE.log(ex, IStatus.WARNING);
}
return getSkipHandler(synchronization);
}
}
finally
{
synchronization.dispose();
}
return null;
}
public Impact performFullSynchronization()
{
offerFirstTimeConnect(UIUtil.getShell());
Synchronization synchronization = synchronize(true, true, false);
if (synchronization != null)
{
return performSynchronization(synchronization, true, true);
}
return null;
}
/**
* Returns <code>true</code> if offered the first time and offer was accepted, <code>false</code> otherwise.
*/
public boolean offerFirstTimeConnect(Shell shell)
{
if (!ENABLED)
{
return false;
}
if (isSyncEnabled())
{
return false;
}
IStorageService service = storage.getService();
if (service == null)
{
return false;
}
if (DEBUG_CONNECTION_OFFERED)
{
connectionOffered = null;
CONFIG.removeProperty(CONFIG_CONNECTION_OFFERED);
}
if (connectionOffered == null)
{
String property = CONFIG.getProperty(CONFIG_CONNECTION_OFFERED, null);
connectionOffered = property != null;
}
if (connectionOffered)
{
return false;
}
Boolean answer = connect(shell, service);
if (answer == null)
{
return false;
}
setSyncEnabled(answer);
return answer;
}
private Boolean connect(final Shell shell, final IStorageService service)
{
final Boolean[] result = { null };
try
{
shell.getDisplay().syncExec(new Runnable()
{
public void run()
{
OptInDialog dialog = new OptInDialog(shell, service);
dialog.open();
result[0] = dialog.getAnswer();
}
});
return result[0];
}
catch (Throwable ex)
{
SetupUIPlugin.INSTANCE.log(ex);
return false;
}
finally
{
if (result[0] != null)
{
CONFIG.setProperty(CONFIG_CONNECTION_OFFERED, new Date().toString());
connectionOffered = true;
}
}
}
private static SkipHandler getSkipHandler(Synchronization synchronization)
{
for (SynchronizerListener listener : synchronization.getSynchronizer().getListeners())
{
if (listener instanceof SkipHandler)
{
return (SkipHandler)listener;
}
}
return null;
}
private static ICredentialsProvider createCredentialProvider()
{
return new EclipseOAuthCredentialsProvider(new OAuthConstants());
}
/**
* @author Eike Stepper
*/
public static interface Impact
{
public boolean hasLocalImpact();
public boolean hasRemoteImpact();
}
/**
* @author Eike Stepper
*/
private static final class SkipHandler extends SynchronizerAdapter implements Impact
{
private static final String CONFIG_SKIPPED_LOCAL = "skipped.local";
private static final String CONFIG_SKIPPED_REMOTE = "skipped.remote";
private static final String ID_SEPARATOR = " ";
private final Set<Location> skippedLocations = new HashSet<Location>();
private final Set<String> skippedLocal = new HashSet<String>();
private final Set<String> skippedRemote = new HashSet<String>();
private final Set<String> computedLocal = new HashSet<String>();
private final Set<String> computedRemote = new HashSet<String>();
private boolean localImpact;
private boolean remoteImpact;
public SkipHandler()
{
}
public boolean hasLocalImpact()
{
return localImpact;
}
public boolean hasRemoteImpact()
{
return remoteImpact;
}
@Override
public void tasksCollected(Synchronization synchronization, Location location, Map<String, SetupTask> oldTasks, Map<String, SetupTask> newTasks)
{
Set<String> skippedIDs = getSkippedIDs(location);
oldTasks.keySet().removeAll(skippedIDs);
}
@Override
public void actionsComputed(Synchronization synchronization, Map<String, SyncAction> actions)
{
computedLocal.clear();
computedRemote.clear();
analyzeImpact(actions, computedLocal, computedRemote);
}
@Override
public void commitFinished(Synchronization synchronization, Throwable t)
{
if (t != null)
{
return;
}
Set<String> committedLocal = new HashSet<String>();
Set<String> committedRemote = new HashSet<String>();
Map<String, SyncAction> actions = synchronization.getActions();
analyzeImpact(actions, committedLocal, committedRemote);
localImpact = !committedRemote.isEmpty();
remoteImpact = !committedLocal.isEmpty();
computedLocal.removeAll(committedLocal);
computedLocal.removeAll(committedRemote);
computedRemote.removeAll(committedLocal);
computedRemote.removeAll(committedRemote);
setSkippedIDs(Location.LOCAL, computedLocal);
setSkippedIDs(Location.REMOTE, computedRemote);
}
private Set<String> getSkippedIDs(Location location)
{
Set<String> skippedIDs = location.pick(skippedLocal, skippedRemote);
if (skippedLocations.add(location))
{
String key = location.pick(CONFIG_SKIPPED_LOCAL, CONFIG_SKIPPED_REMOTE);
String property = CONFIG.getProperty(key, null);
if (property != null)
{
StringTokenizer tokenizer = new StringTokenizer(property, ID_SEPARATOR);
while (tokenizer.hasMoreTokens())
{
String id = tokenizer.nextToken();
skippedIDs.add(id);
}
}
}
return skippedIDs;
}
private void setSkippedIDs(Location location, Set<String> skippedIDs)
{
String key = location.pick(CONFIG_SKIPPED_LOCAL, CONFIG_SKIPPED_REMOTE);
if (skippedIDs.isEmpty())
{
CONFIG.removeProperty(key);
}
else
{
List<String> list = new ArrayList<String>(skippedIDs);
Collections.sort(list);
StringBuilder builder = new StringBuilder();
for (String id : list)
{
if (builder.length() != 0)
{
builder.append(ID_SEPARATOR);
}
builder.append(id);
}
CONFIG.setProperty(key, builder.toString());
}
}
private static void analyzeImpact(Map<String, SyncAction> actions, Set<String> local, Set<String> remote)
{
for (Map.Entry<String, SyncAction> entry : actions.entrySet())
{
String id = entry.getKey();
SyncAction action = entry.getValue();
SyncActionType effectiveType = action.getEffectiveType();
switch (effectiveType)
{
case SET_LOCAL:
case REMOVE_LOCAL:
local.add(id);
break;
case SET_REMOTE:
case REMOVE_REMOTE:
remote.add(id);
break;
case CONFLICT:
local.add(id);
remote.add(id);
break;
}
}
}
}
/**
* @author Eike Stepper
*/
public static final class SynchronizationController
{
private boolean withProgressDialog;
private SynchronizerJob synchronizerJob;
public boolean start(boolean withProgressDialog, boolean withCredentialsPrompt, boolean deferLocal)
{
this.withProgressDialog = withProgressDialog;
if (ENABLED)
{
if (synchronizerJob == null && INSTANCE.isSyncEnabled())
{
IStorageService service = INSTANCE.getStorage().getService();
if (service == null)
{
return false;
}
Synchronizer synchronizer = INSTANCE.createSynchronizer(USER_SETUP, SYNC_FOLDER);
synchronizerJob = new SynchronizerJob(synchronizer, deferLocal);
synchronizerJob.setService(service);
if (!withCredentialsPrompt)
{
synchronizerJob.setCredentialsProvider(ICredentialsProvider.CANCEL);
}
synchronizerJob.schedule();
}
}
return synchronizerJob != null;
}
public void stop()
{
if (!ENABLED)
{
return;
}
if (synchronizerJob != null)
{
synchronizerJob.stopSynchronization();
synchronizerJob = null;
}
}
public Synchronization await()
{
if (!ENABLED)
{
return null;
}
if (synchronizerJob != null)
{
final Synchronization[] result = { synchronizerJob.getSynchronization() };
if (result[0] == null)
{
try
{
final AtomicBoolean canceled = new AtomicBoolean();
final IStorageService service = synchronizerJob.getService();
final String serviceLabel = service.getServiceLabel();
if (withProgressDialog)
{
final Semaphore authenticationSemaphore = service.getAuthenticationSemaphore();
authenticationSemaphore.acquire();
UIUtil.syncExec(new Runnable()
{
public void run()
{
try
{
Shell shell = UIUtil.getShell();
ProgressMonitorDialog dialog = new ProgressMonitorDialog(shell);
dialog.run(true, true, new IRunnableWithProgress()
{
public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException
{
authenticationSemaphore.release();
result[0] = await(serviceLabel, monitor);
}
});
}
catch (InvocationTargetException ex)
{
SetupUIPlugin.INSTANCE.log(ex);
}
catch (InterruptedException ex)
{
canceled.set(true);
}
}
});
}
else
{
final IProgressMonitor monitor = new NullProgressMonitor();
Job watchDog = new Job("Synchronizer Watch Dog")
{
@Override
protected IStatus run(IProgressMonitor monitor)
{
try
{
Thread.sleep(10000);
}
catch (Throwable ex)
{
//$FALL-THROUGH$
}
monitor.setCanceled(true);
return Status.OK_STATUS;
}
};
watchDog.setSystem(true);
watchDog.schedule();
result[0] = await(serviceLabel, monitor);
}
if (result[0] == null && !canceled.get())
{
Throwable exception = synchronizerJob.getException();
if (exception == null || exception instanceof OperationCanceledException)
{
return null;
}
SynchronizerManager.log(exception);
}
}
catch (Throwable ex)
{
SetupUIPlugin.INSTANCE.log(ex);
}
finally
{
synchronizerJob = null;
}
}
return result[0];
}
return null;
}
private Synchronization await(String serviceLabel, IProgressMonitor monitor)
{
monitor.beginTask("Requesting data from " + serviceLabel + "...", IProgressMonitor.UNKNOWN);
try
{
return synchronizerJob.awaitSynchronization(monitor);
}
finally
{
monitor.done();
}
}
}
public static void log(Throwable throwable)
{
final IStorage storage = SynchronizerManager.INSTANCE.getStorage();
IStorageService service = storage.getService();
String serviceLabel = service == null ? "Unknown" : service.getServiceLabel();
SetupUIPlugin.INSTANCE.log(
new Status(IStatus.WARNING, SetupUIPlugin.PLUGIN_ID, "Window -> Preferences -> Oomph -> Setup Tasks -> Preference Synchronizer -> 'Synchronize with "
+ serviceLabel + "' is enabled and the synchronization has failed for the following reason:", throwable));
}
/**
* @author Ed Merks
*/
public static final class Availability
{
public static final boolean AVAILABLE;
static
{
boolean available;
try
{
available = SynchronizerManager.INSTANCE != null;
}
catch (NoClassDefFoundError error)
{
available = false;
}
AVAILABLE = available;
}
private Availability()
{
}
}
}