package org.syncany.gui.wizard;
import java.lang.reflect.Constructor;
import java.net.URI;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Text;
import org.syncany.gui.util.DesktopUtil;
import org.syncany.gui.util.I18n;
import org.syncany.gui.util.WidgetDecorator;
import org.syncany.plugins.transfer.StorageException;
import org.syncany.plugins.transfer.TransferSettings;
import org.syncany.plugins.transfer.oauth.OAuth;
import org.syncany.plugins.transfer.oauth.OAuthGenerator;
import org.syncany.plugins.transfer.oauth.OAuthGenerator.WithExtractor;
import org.syncany.plugins.transfer.oauth.OAuthGenerator.WithInterceptor;
import org.syncany.plugins.transfer.oauth.OAuthTokenFinish;
import org.syncany.plugins.transfer.oauth.OAuthTokenWebListener;
/**
* @author Christian Roth <christian.roth@port17.de>
*/
class PluginSettingsPanelOAuthHelper {
private enum StatusCode {
IDLE, RUNNING, FINISHED, SUCCESS
}
private static final Logger logger = Logger.getLogger(PluginSettingsPanelOAuthHelper.class.getName());
private static final String STRING_BUTTON_CONNECTING = I18n.getText("org.syncany.gui.wizard.PluginSettingsPanel.oauth.button.connecting");
private static final String STRING_BUTTON_AUTHORIZE = I18n.getText("org.syncany.gui.wizard.PluginSettingsPanel.oauth.button.authorize");
private static final String STRING_BUTTON_WAITING = I18n.getText("org.syncany.gui.wizard.PluginSettingsPanel.oauth.button.waiting");
private static final String STRING_BUTTON_ERROR = I18n.getText("org.syncany.gui.wizard.PluginSettingsPanel.oauth.button.error");
private static final String STRING_BUTTON_OK = I18n.getText("org.syncany.gui.wizard.PluginSettingsPanel.oauth.button.ok");
private final OAuthTokenWebListener webListener;
private final ButtonSelectionAdapter buttonSelectionAdapter;
private final OAuthGenerator generator;
private final Consumer<String> warningHandler;
private final Button authorizeButton;
private final Text tokenText;
private Future<OAuthTokenFinish> futureTokenFinish;
private StatusCode statusCode = StatusCode.IDLE;
private URI redirectUri;
private URI authUri;
private Thread tokenArrivalThread;
public static class Builder {
private final OAuth settings;
private final OAuthGenerator generator;
private Button button;
private Text text;
private Consumer<String> warningHandler;
private <T extends TransferSettings> Builder(T transferSettings) throws UnsupportedOperationException {
settings = transferSettings.getClass().getAnnotation(OAuth.class);
if (settings == null) {
throw new UnsupportedOperationException("Tried to create OAuth helper for non-OAuth class");
}
try {
Constructor<? extends OAuthGenerator> generatorConstructor = settings.value().getDeclaredConstructor(transferSettings.getClass());
generator = generatorConstructor.newInstance(transferSettings);
}
catch (Exception e) {
throw new RuntimeException("Unable to create generator", e);
}
}
public Builder withButton(Button button) {
this.button = button;
return this;
}
public Builder withText(Text text) {
this.text = text;
return this;
}
public Builder withWarningHandler(Consumer<String> warningHandler) {
this.warningHandler = warningHandler;
return this;
}
public PluginSettingsPanelOAuthHelper build() {
OAuthTokenWebListener.Builder tokenListerBuilder = OAuthTokenWebListener.forMode(settings.mode());
if (settings.callbackPort() != OAuth.RANDOM_PORT) {
tokenListerBuilder.setPort(settings.callbackPort());
}
if (!settings.callbackId().equals(OAuth.PLUGIN_ID)) {
tokenListerBuilder.setId(settings.callbackId());
}
// Non standard plugin?
if (generator instanceof WithInterceptor) {
tokenListerBuilder.setTokenInterceptor(((WithInterceptor) generator).getInterceptor());
}
if (generator instanceof WithExtractor) {
tokenListerBuilder.setTokenExtractor(((WithExtractor) generator).getExtractor());
}
return new PluginSettingsPanelOAuthHelper(tokenListerBuilder.build(), generator, warningHandler, button, text);
}
}
private PluginSettingsPanelOAuthHelper(OAuthTokenWebListener webListener, OAuthGenerator generator, Consumer<String> warningHandler, Button button, Text text) {
this.webListener = webListener;
this.generator = generator;
this.warningHandler = warningHandler;
this.authorizeButton = button;
this.tokenText = text;
this.buttonSelectionAdapter = new ButtonSelectionAdapter();
disableButton();
tokenText.setEditable(false);
}
/**
* Create a helper for a specific OAuth plugin. The helper manages buttons and token collection including validation.
*
* @param transferSettings The OAuth enabled plugin
* @throws UnsupportedOperationException If the plugin is no OAuth plugin (not annotated with {@link OAuth}.
*/
static <T extends TransferSettings> Builder forSettings(T transferSettings) throws UnsupportedOperationException {
return new Builder(transferSettings);
}
/**
* Stop all running listeners and reset the button to initial state.
*
* @param clearGui Define if the GUI elements should be reset to their initial state (will cause an Exception if
* the elements are already disposed)
*/
public void reset(boolean clearGui) {
if (statusCode == StatusCode.IDLE) {
return;
}
logger.log(Level.INFO, "Resetting the OAuth process");
if (tokenArrivalThread != null) {
tokenArrivalThread.interrupt();
tokenArrivalThread = null;
}
else {
webListener.stop();
}
redirectUri = null;
authUri = null;
if (clearGui) {
disableButton();
setButtonText(STRING_BUTTON_CONNECTING);
setTokenText("");
markTextAsSuccess(false);
}
statusCode = StatusCode.IDLE;
}
/**
* This method makes the GUI ready to perfom the OAuth process. It starts the webserver, adds the button event and
* changes button texts. Implicitly calls {@link #reset(boolean clearGui)} in GUI clearance mode.
*/
public void start() {
reset(true);
redirectUri = webListener.start();
statusCode = StatusCode.RUNNING;
asyncRetrieveOAuthUrlAndEnableAuthButton();
logger.log(Level.INFO, "OAuth process enabled");
}
public boolean isFinished() {
return statusCode == StatusCode.FINISHED;
}
public boolean isSuccess() {
return statusCode == StatusCode.SUCCESS;
}
private void asyncRetrieveOAuthUrlAndEnableAuthButton() {
new Thread(new Runnable() {
@Override
public void run() {
try {
authUri = generator.generateAuthUrl(redirectUri);
futureTokenFinish = webListener.getToken();
enableButton();
setButtonText(STRING_BUTTON_AUTHORIZE);
asyncWaitForTokenArrival();
}
catch (Exception e) {
triggerError(I18n.getText("org.syncany.gui.wizard.PluginSettingsPanel.oauth.errorCannotRetrieveOAuthURL", e.getMessage()));
setButtonText(STRING_BUTTON_ERROR);
logger.log(Level.SEVERE, "Unable to retrieve auth url", e);
}
}
}, "GetOAuthUrl").start();
}
private void asyncWaitForTokenArrival() {
tokenArrivalThread = new Thread(new Runnable() {
@Override
public void run() {
boolean startOver = false;
try {
OAuthTokenFinish tokenResponse = futureTokenFinish.get(); // we dont need a timeout here
statusCode = StatusCode.FINISHED;
if (tokenResponse != null) {
generator.checkToken(tokenResponse.getToken(), tokenResponse.getCsrfState());
setTokenText(tokenResponse.getToken());
setButtonText(STRING_BUTTON_OK);
markTextAsSuccess(true);
statusCode = StatusCode.SUCCESS;
}
else {
logger.log(Level.WARNING, "Invalid token received, maybe user cancled process.");
triggerError(I18n.getText("org.syncany.gui.wizard.PluginSettingsPanel.errorExceptionOAuthToken"));
startOver = true;
}
}
catch (InterruptedException e) {
// no big deal since that is intended when called #reset
logger.log(Level.INFO, "Thread was interrupted, maybe some called reset", e);
}
catch (ExecutionException | StorageException e) {
logger.log(Level.SEVERE, "Exception while waiting for OAuth token", e);
triggerError(I18n.getText("org.syncany.gui.wizard.PluginSettingsPanel.errorExceptionOAuthToken"));
}
finally {
logger.log(Level.INFO, "Finally stopping weblistener");
webListener.stop();
if (startOver) {
logger.log(Level.INFO, "Reenabling OAuth process");
start();
}
}
}
}, "WaitTokenArrival");
tokenArrivalThread.start();
}
private void disableButton() {
Display.getDefault().asyncExec(new Runnable() {
@Override
public void run() {
authorizeButton.setEnabled(false);
authorizeButton.removeSelectionListener(buttonSelectionAdapter);
}
});
}
private void enableButton() {
Display.getDefault().asyncExec(new Runnable() {
@Override
public void run() {
authorizeButton.setEnabled(true);
authorizeButton.addSelectionListener(buttonSelectionAdapter);
}
});
}
private void setButtonText(final String text) {
Display.getDefault().asyncExec(new Runnable() {
@Override
public void run() {
authorizeButton.setText(text);
}
});
}
private void setTokenText(final String text) {
Display.getDefault().asyncExec(new Runnable() {
@Override
public void run() {
tokenText.setText(text);
}
});
}
private void markTextAsSuccess(final boolean success) {
Display.getDefault().asyncExec(new Runnable() {
@Override
public void run() {
if (success) {
WidgetDecorator.markAsValid(tokenText);
}
else {
WidgetDecorator.normal(tokenText);
}
}
});
}
private void triggerError(String warning) {
Display.getDefault().asyncExec(new Runnable() {
@Override
public void run() {
WidgetDecorator.markAsInvalid(tokenText);
}
});
if (warningHandler != null) {
warningHandler.accept(warning);
}
}
private class ButtonSelectionAdapter extends SelectionAdapter {
@Override
public void widgetSelected(SelectionEvent e) {
DesktopUtil.launch(authUri.toString());
disableButton();
setButtonText(STRING_BUTTON_WAITING);
}
}
/* Java 7 sadly does not support java.util.function.Consumer<T> from Java 8, backporting */
public interface Consumer<T> {
void accept(T t);
}
}