Java tutorial
/******************************************************************************* * Copyright (c) 2015-2016 Red Hat, Inc. * Distributed under license by Red Hat, Inc. All rights reserved. * This program is 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: * Red Hat, Inc. - initial API and implementation ******************************************************************************/ package org.jboss.tools.openshift.internal.ui.wizard.connection; import java.lang.reflect.InvocationTargetException; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.http.conn.ConnectTimeoutException; import org.eclipse.core.databinding.Binding; import org.eclipse.core.databinding.DataBindingContext; import org.eclipse.core.databinding.beans.BeanProperties; import org.eclipse.core.databinding.observable.value.IObservableValue; import org.eclipse.core.databinding.observable.value.IValueChangeListener; import org.eclipse.core.databinding.observable.value.WritableValue; import org.eclipse.core.databinding.validation.IValidator; import org.eclipse.core.databinding.validation.MultiValidator; import org.eclipse.core.databinding.validation.ValidationStatus; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.IJobChangeEvent; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.core.runtime.jobs.JobChangeAdapter; import org.eclipse.jface.databinding.fieldassist.ControlDecorationSupport; import org.eclipse.jface.databinding.fieldassist.ControlDecorationUpdater; import org.eclipse.jface.databinding.swt.WidgetProperties; import org.eclipse.jface.dialogs.Dialog; import org.eclipse.jface.dialogs.ErrorDialog; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.layout.GridDataFactory; import org.eclipse.jface.layout.GridLayoutFactory; import org.eclipse.jface.wizard.IWizard; import org.eclipse.osgi.util.NLS; import org.eclipse.swt.SWT; import org.eclipse.swt.browser.Browser; import org.eclipse.swt.browser.ProgressEvent; import org.eclipse.swt.browser.ProgressListener; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.graphics.Cursor; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.ProgressBar; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Text; import org.jboss.tools.common.ui.WizardUtils; import org.jboss.tools.common.ui.databinding.ValueBindingBuilder; import org.jboss.tools.openshift.common.core.connection.IConnection; import org.jboss.tools.openshift.common.core.connection.NewConnectionMarker; import org.jboss.tools.openshift.core.OpenShiftCoreUIIntegration; import org.jboss.tools.openshift.core.connection.Connection; import org.jboss.tools.openshift.internal.common.ui.connection.ConnectionWizardPageModel; import org.jboss.tools.openshift.internal.common.ui.connection.ConnectionWizardPageModel.IConnectionAuthenticationProvider; import org.jboss.tools.openshift.internal.common.ui.databinding.RequiredControlDecorationUpdater; import org.jboss.tools.openshift.internal.common.ui.databinding.RequiredStringValidator; import org.jboss.tools.openshift.internal.common.ui.databinding.TrimmingStringConverter; import org.jboss.tools.openshift.internal.common.ui.detailviews.BaseDetailsView; import org.jboss.tools.openshift.internal.common.ui.utils.DataBindingUtils; import org.jboss.tools.openshift.internal.common.ui.utils.StyledTextUtils; import org.jboss.tools.openshift.internal.ui.OpenShiftUIActivator; import com.openshift.restclient.ClientBuilder; import com.openshift.restclient.IClient; import com.openshift.restclient.authorization.IAuthorizationContext; import com.openshift.restclient.authorization.IAuthorizationDetails; /** * @author jeff.cantrill * @author Jeff Maury * @author Andre Dietisheim */ public class OAuthDetailView extends BaseDetailsView implements IConnectionEditorDetailView { private static final String MSG_TOKEN = "Enter a token or <a>retrieve</a> a new one."; /** * Helper class to handle Openshift token authentication response * */ public static class TokenExtractor { /** * Regular expression used to check if browser page is page that displays the OAuth token */ public static final Pattern TOKEN_PAGE_PATTERN = Pattern .compile(".*<h2>Your API token is<\\/h2>.*<code>(.*)<\\/code>.*", Pattern.DOTALL); private Matcher matcher; public TokenExtractor(String content) { matcher = TOKEN_PAGE_PATTERN.matcher(content); } public boolean isTokenPage() { return matcher.matches(); } public String getToken() { return matcher.group(1); } } private IObservableValue<String> tokenObservable; private Binding tokenBinding; private Text tokenText; private IValueChangeListener<?> changeListener; private IObservableValue<Boolean> rememberTokenObservable; private IObservableValue<String> authSchemeObservable; private IAuthorizationDetails authDetails; private ConnectionWizardPageModel pageModel; IObservableValue<String> urlObservable; private Button rememberTokenCheckbox; private Binding rememberTokenBinding; private MultiValidator connectionValidator; private IWizard wizard; public OAuthDetailView(IWizard wizard, ConnectionWizardPageModel pageModel, IValueChangeListener<?> changeListener, Object context, IObservableValue<String> authSchemeObservable) { this.wizard = wizard; this.pageModel = pageModel; this.urlObservable = BeanProperties.value(ConnectionWizardPageModel.PROPERTY_HOST).observe(pageModel); this.tokenObservable = new WritableValue<String>(null, String.class); this.rememberTokenObservable = new WritableValue<Boolean>(Boolean.FALSE, Boolean.class); this.connectionValidator = ConnectionValidatorFactory.createOAuthAuthenticationValidator(pageModel, tokenObservable, urlObservable); this.authSchemeObservable = authSchemeObservable; this.changeListener = changeListener; if (context instanceof IAuthorizationDetails) { this.authDetails = (IAuthorizationDetails) context; } } IObservableValue<Boolean> getRememberTokenObservable() { return rememberTokenObservable; } public final Text getTokenTextControl() { return tokenText; } @Override public Composite createControls(Composite parent, Object context, DataBindingContext dbc) { Composite composite = setControl(new Composite(parent, SWT.None)); GridLayoutFactory.fillDefaults().numColumns(2).spacing(10, 10).applyTo(composite); StyledText tokenRequestLink = StyledTextUtils.emulateLinkWidget(MSG_TOKEN, new StyledText(composite, SWT.WRAP)); GridDataFactory.fillDefaults().align(SWT.LEFT, SWT.CENTER).span(3, 1).applyTo(tokenRequestLink); if (authDetails != null) { authDetails.getRequestTokenLink(); } StyledTextUtils.emulateLinkAction(tokenRequestLink, r -> onRetrieveLinkClicked(tokenRequestLink.getShell(), dbc)); tokenRequestLink.setCursor(new Cursor(tokenRequestLink.getShell().getDisplay(), SWT.CURSOR_HAND)); //token Label authTypeLabel = new Label(composite, SWT.NONE); authTypeLabel.setText("Token"); GridDataFactory.fillDefaults().align(SWT.LEFT, SWT.CENTER).applyTo(authTypeLabel); this.tokenText = new Text(composite, SWT.BORDER); GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER).grab(true, false).applyTo(tokenText); ControlDecorationSupport.create(connectionValidator, SWT.LEFT | SWT.TOP, null, new ControlDecorationUpdater()); this.rememberTokenCheckbox = new Button(composite, SWT.CHECK); rememberTokenCheckbox.setText("&Save token (could trigger secure storage login)"); GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER).span(2, 1).grab(true, false) .applyTo(rememberTokenCheckbox); return composite; } @Override public void onVisible(IObservableValue detailsViewModel, DataBindingContext dbc) { dbc.addValidationStatusProvider(connectionValidator); bindWidgetsToInternalModel(dbc); this.rememberTokenBinding = ValueBindingBuilder .bind(WidgetProperties.selection().observe(rememberTokenCheckbox)).to(rememberTokenObservable) .in(dbc); } @Override public void onInVisible(IObservableValue detailsViewModel, DataBindingContext dbc) { dispose(); DataBindingUtils.dispose(rememberTokenBinding); dbc.removeValidationStatusProvider(connectionValidator); } @Override public void dispose() { DataBindingUtils.dispose(tokenBinding); } private void bindWidgetsToInternalModel(DataBindingContext dbc) { IValidator validator = new RequiredStringValidator("token"); this.tokenBinding = ValueBindingBuilder.bind(WidgetProperties.text(SWT.Modify).observe(tokenText)) .converting(new TrimmingStringConverter()).validatingAfterConvert(validator).to(tokenObservable) .validatingBeforeSet(validator).in(dbc); ControlDecorationSupport.create(tokenBinding, SWT.LEFT | SWT.TOP, null, new RequiredControlDecorationUpdater()); org.jboss.tools.common.ui.databinding.DataBindingUtils.addDisposableValueChangeListener(changeListener, tokenObservable, tokenText); } @Override public void setSelectedConnection(IConnection selectedConnection) { if (selectedConnection instanceof Connection) { Connection connection = (Connection) selectedConnection; tokenObservable.setValue(connection.getToken()); rememberTokenObservable.setValue(connection.isRememberToken()); } else if (selectedConnection instanceof NewConnectionMarker) { tokenObservable.setValue(null); rememberTokenObservable.setValue(Boolean.FALSE); } } @Override public IConnectionAuthenticationProvider getConnectionAuthenticationProvider() { return new BearTokenAuthenticationProvider(); } @Override public boolean isViewFor(Object object) { return object == this; } @Override public String toString() { return IAuthorizationContext.AUTHSCHEME_OAUTH; } private void onRetrieveLinkClicked(final Shell shell, final DataBindingContext dbc) { if (StringUtils.isBlank(pageModel.getHost())) { return; } Job job = new AuthDetailsJob(pageModel.getHost()); job.addJobChangeListener(new JobChangeAdapter() { @Override public void done(final IJobChangeEvent event) { if (event.getJob() instanceof AuthDetailsJob) { shell.getDisplay().asyncExec(new Runnable() { @Override public void run() { AuthDetailsJob job = (AuthDetailsJob) event.getJob(); final IAuthorizationDetails details = job.getDetails(); if (details != null) { //TODO fix this to handle other authschemes if (IAuthorizationContext.AUTHSCHEME_BASIC.equals(details.getScheme())) { MessageDialog.openError(shell, "Authorization Information", NLS.bind("This server utilizes {0} authorization protocol", details.getScheme())); authSchemeObservable.setValue(details.getScheme()); } else { OAuthDialog dialog = new OAuthDialog(shell, details.getRequestTokenLink()); dialog.open(); String token = dialog.getToken(); if (StringUtils.isNotBlank(token)) { tokenObservable.setValue(token); } } } } }); } } }); try { WizardUtils.runInWizard(job, wizard.getContainer(), dbc); } catch (InvocationTargetException | InterruptedException ex) { showErrorDialog(shell, ex); } } private void showErrorDialog(final Shell shell, final Throwable e) { Display.getDefault().asyncExec(new Runnable() { @Override public void run() { ErrorDialog.openError(shell, "Request Authentication Details Error", "Unable to retrieve authentication details", ValidationStatus.error(e.getMessage(), e)); } }); } private class AuthDetailsJob extends Job { private IAuthorizationDetails details; private String host; public AuthDetailsJob(String host) { super(NLS.bind("Retrieve authorization details from {0}...", host)); this.host = host; } public IAuthorizationDetails getDetails() { return details; } @Override protected IStatus run(IProgressMonitor monitor) { try { IClient client = new ClientBuilder(host).sslCertificateCallback( OpenShiftCoreUIIntegration.getInstance().getSSLCertificateCallback()).build(); details = client.getAuthorizationContext().getAuthorizationDetails(); return ValidationStatus.OK_STATUS; } catch (Exception e) { if (e.getCause() instanceof ConnectTimeoutException) { return new Status(IStatus.ERROR, OpenShiftUIActivator.PLUGIN_ID, "Timed out waiting for a response for authorization details.\nThis server might be unavailable or may not support OAuth.", e); } else { return new Status(IStatus.ERROR, OpenShiftUIActivator.PLUGIN_ID, "Unable to retrieve the authentication details", e); } } } } private class BearTokenAuthenticationProvider implements IConnectionAuthenticationProvider { @Override public IConnection update(IConnection conn) { Assert.isLegal(conn instanceof Connection); final Connection connection = (Connection) conn; // might be called from job, switch to display thread to access observables Display.getDefault().syncExec(new Runnable() { @Override public void run() { connection.setAuthScheme(IAuthorizationContext.AUTHSCHEME_OAUTH); connection.setToken(tokenObservable.getValue()); connection.setRememberToken(rememberTokenObservable.getValue()); } }); return connection; } } /* * Leaving for now as we may need this if we are ever able * to progammatically get the token */ private class OAuthDialog extends Dialog { private String loadingHtml; private String url; private Browser browser; private String token; OAuthDialog(Shell parentShell, String url) { super(parentShell); this.url = url; try { loadingHtml = IOUtils .toString(OpenShiftUIActivator.getDefault().getPluginFile("html/spinner.html")); } catch (Exception e) { loadingHtml = "Loading..."; } } @Override protected void createButtonsForButtonBar(Composite parent) { createButton(parent, IDialogConstants.OK_ID, "Close", true); } @Override protected boolean isResizable() { return true; } @Override protected Point getInitialSize() { return new Point(500, 700); } @Override protected Control createDialogArea(Composite parent) { Composite container = (Composite) super.createDialogArea(parent); container.setLayout(new GridLayout()); GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).grab(true, true).applyTo(container); browser = new Browser(container, SWT.BORDER); browser.setText(loadingHtml); GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).grab(true, true).applyTo(browser); final ProgressBar progressBar = new ProgressBar(container, SWT.NONE); GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).applyTo(progressBar); ProgressListener progressListener = new ProgressListener() { @Override public void changed(ProgressEvent event) { if (event.total <= 0) return; int ratio = event.current * 100 / event.total; progressBar.setSelection(ratio); ; } @Override public void completed(ProgressEvent event) { progressBar.setSelection(0); TokenExtractor extractor = new TokenExtractor(browser.getText()); if (extractor.isTokenPage()) { token = extractor.getToken(); } } }; browser.addProgressListener(progressListener); setURL(url); return container; } public void setURL(String url) { if (StringUtils.isNotBlank(url)) { this.url = url; browser.setUrl(url); } } /** * @return the token */ public String getToken() { return token; } } }