/*******************************************************************************
* Copyright (c) 2015 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.portforwading;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import org.apache.commons.lang.StringUtils;
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.ValueChangeEvent;
import org.eclipse.core.databinding.validation.MultiValidator;
import org.eclipse.core.databinding.validation.ValidationStatus;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.databinding.swt.WidgetProperties;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.layout.TableColumnLayout;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.CellLabelProvider;
import org.eclipse.jface.viewers.ColumnWeightData;
import org.eclipse.jface.viewers.StyledCellLabelProvider;
import org.eclipse.jface.viewers.StyledString;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.TableViewerColumn;
import org.eclipse.jface.viewers.ViewerCell;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.TextStyle;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Table;
import org.jboss.tools.common.ui.WizardUtils;
import org.jboss.tools.common.ui.databinding.DataBindingUtils;
import org.jboss.tools.common.ui.databinding.ValueBindingBuilder;
import org.jboss.tools.foundation.core.plugin.log.IPluginLog;
import org.jboss.tools.openshift.internal.common.ui.wizard.AbstractOpenShiftWizardPage;
import org.jboss.tools.openshift.internal.core.portforwarding.PortForwardingUtils;
import org.jboss.tools.openshift.internal.ui.OpenShiftUIActivator;
import com.openshift.restclient.OpenShiftException;
import com.openshift.restclient.capability.resources.IPortForwardable;
/**
* @author jeff.cantrill
*/
public class PortForwardingWizardPage extends AbstractOpenShiftWizardPage {
private static final IPluginLog LOG = OpenShiftUIActivator.getDefault().getLogger();
private final PortForwardingWizardModel wizardModel;
/**
* Constructor.
* @param wizardModel the wizard model
* @param portForwardingWizard the parent wizard
*/
public PortForwardingWizardPage(final PortForwardingWizardModel wizardModel, final PortForwardingWizard portForwardingWizard) {
super("Port forwarding", null,
"PortForwardingWizardPage", portForwardingWizard);
this.wizardModel = wizardModel;
setDescription(NLS.bind("Port forwarding for the {0} pod.", wizardModel.getPodName()));
}
@Override
protected void doCreateControls(Composite parent, DataBindingContext dbc) {
GridLayoutFactory.fillDefaults().margins(6, 6).applyTo(parent);
Composite container = new Composite(parent, SWT.NONE);
GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).grab(true, true).applyTo(container);
GridLayoutFactory.fillDefaults().numColumns(2).applyTo(container);
Composite tableContainer = new Composite(container, SWT.NONE);
final TableViewer viewer = createTable(tableContainer, dbc);
GridDataFactory.fillDefaults().span(1, 3).align(SWT.FILL, SWT.FILL).grab(true, true).applyTo(tableContainer);
final Button startButton = new Button(container, SWT.PUSH);
startButton.setText("Start All");
GridDataFactory.fillDefaults().hint(110, SWT.DEFAULT).align(SWT.FILL, SWT.TOP).applyTo(startButton);
startButton.addSelectionListener(onStartPortForwarding(viewer));
final Button stopButton = new Button(container, SWT.PUSH);
stopButton.setText("Stop All");
GridDataFactory.fillDefaults().hint(110, SWT.DEFAULT).align(SWT.FILL, SWT.TOP).applyTo(stopButton);
stopButton.addSelectionListener(onStopPortForwarding(viewer));
final Button findFreesPortButton = new Button(container, SWT.CHECK);
findFreesPortButton.setText("Find free local ports for remote ports");
GridDataFactory.fillDefaults().span(2, 1).align(SWT.FILL, SWT.CENTER).grab(false, false)
.applyTo(findFreesPortButton);
final IObservableValue findFreePortsButtonObservable = BeanProperties.value(
PortForwardingWizardModel.PROPERTY_USE_FREE_PORTS).observe(wizardModel);
final IObservableValue findFreePortsButtonSelection = WidgetProperties.selection().observe(findFreesPortButton);
dbc.bindValue(findFreePortsButtonSelection, findFreePortsButtonObservable);
DataBindingUtils.addDisposableValueChangeListener(
new IValueChangeListener() {
@Override
public void handleValueChange(ValueChangeEvent event) {
refreshViewerInput(viewer);
}
}, findFreePortsButtonObservable, viewer.getTable());
// enabling/disabling controls
IObservableValue portForwardingStartedObservable = BeanProperties.value(
PortForwardingWizardModel.PROPERTY_PORT_FORWARDING).observe(wizardModel);
IObservableValue portForwardingAllowedObservable = BeanProperties.value(
PortForwardingWizardModel.PROPERTY_PORT_FORWARDING_ALLOWED).observe(wizardModel);
IObservableValue freePortSearchAllowedObservable = BeanProperties.value(
PortForwardingWizardModel.PROPERTY_FREE_PORT_SEARCH_ALLOWED).observe(wizardModel);
ValueBindingBuilder.bind(WidgetProperties.enabled().observe(startButton))
.notUpdating(portForwardingAllowedObservable).in(dbc);
ValueBindingBuilder.bind(WidgetProperties.enabled().observe(stopButton))
.notUpdating(portForwardingStartedObservable).in(dbc);
ValueBindingBuilder.bind(WidgetProperties.enabled().observe(findFreesPortButton))
.notUpdating(freePortSearchAllowedObservable).in(dbc);
}
private SelectionListener onStartPortForwarding(final TableViewer viewer) {
return new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
if(!wizardModel.checkPortForwardingAllowed()) {
//This is rather a testing case, it is not very probable at normal usage.
viewer.refresh(true);
MessageDialog.openWarning(getShell(), "Warning", "Some ports are in use now.");
return;
}
try {
WizardUtils.runInWizard(new Job("Starting port forwarding...") {
@Override
protected IStatus run(IProgressMonitor monitor) {
wizardModel.startPortForwarding();
refreshViewerInput(viewer);
return Status.OK_STATUS;
}
}, getContainer(), getDataBindingContext());
} catch (OpenShiftException | InvocationTargetException | InterruptedException e1) {
LOG.logError(e1);
MessageDialog.openError(getShell(), "Error starting port forwarding", e1.getMessage());
}
}
};
}
private SelectionListener onStopPortForwarding(final TableViewer viewer) {
return new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
try {
WizardUtils.runInWizard(new Job("Stopping port forwarding...") {
@Override
protected IStatus run(IProgressMonitor monitor) {
try {
wizardModel.stopPortForwarding();
} catch (IOException e) {
Display.getDefault()
.syncExec(() -> MessageDialog.openError(Display.getDefault().getActiveShell(),
"Error", "Failed to close console inputstream while stopping port-forwarding: "
+ e.getMessage()));
OpenShiftUIActivator.getDefault().getLogger().logError(
"Failed to close console inputstream while stopping port-forwarding", e);
}
refreshViewerInput(viewer);
if(!wizardModel.isPortForwardingAllowed()) {
//Ports remain in use after a reasonable wait.
//Lets give UI a break and then repeat waiting.
Display.getDefault().asyncExec(new Runnable() {
@Override
public void run() {
waitForPortsToGetFree(viewer);
}
});
}
return Status.OK_STATUS;
}
}, getContainer(), getDataBindingContext());
} catch (OpenShiftException | InvocationTargetException | InterruptedException e1) {
LOG.logError(e1);
MessageDialog.openError(getShell(), "Error stopping port forwarding", e1.getMessage());
}
}
};
}
private void waitForPortsToGetFree(final TableViewer viewer) {
//Try, if ports got free while this task was in wait, one poll is fast.
if(wizardModel.waitForPortsToGetFree(0)) {
return;
}
try {
WizardUtils.runInWizard(new Job("Waiting for ports to get free...") {
@Override
protected IStatus run(IProgressMonitor monitor) {
if(!wizardModel.waitForPortsToGetFree(5)) {
Display.getDefault()
.asyncExec(() -> MessageDialog.openWarning(Display.getDefault().getActiveShell(),
"Warning", "Ports remain in use. Try free ports."));
}
refreshViewerInput(viewer);
return Status.OK_STATUS;
}
}, getContainer(), getDataBindingContext());
} catch (InvocationTargetException | InterruptedException e1) {
LOG.logError(e1);
MessageDialog.openError(getShell(), "Error while waiting for ports to get free", e1.getMessage());
}
}
protected TableViewer createTable(Composite tableContainer, DataBindingContext dbc) {
Table table = new Table(tableContainer, SWT.BORDER | SWT.FULL_SELECTION | SWT.V_SCROLL | SWT.H_SCROLL);
table.setLinesVisible(true);
table.setHeaderVisible(true);
TableColumnLayout tableLayout = new TableColumnLayout();
tableContainer.setLayout(tableLayout);
TableViewer viewer = new TableViewer(table);
viewer.setContentProvider(new ArrayContentProvider());
createTableColumn("Name", 1, new CellLabelProvider() {
@Override
public void update(ViewerCell cell) {
IPortForwardable.PortPair port = (IPortForwardable.PortPair) cell.getElement();
cell.setText(StringUtils.defaultIfBlank(port.getName(), ""));
}
}, viewer, tableLayout);
createTableColumn("Local Port", 2, new StyledCellLabelProvider() {
@Override
public void update(ViewerCell cell) {
IPortForwardable.PortPair port = (IPortForwardable.PortPair) cell.getElement();
int local = port.getLocalPort();
if(wizardModel.getPortForwarding() || !PortForwardingUtils.isPortInUse(local)) {
cell.setText(Integer.toString(local));
cell.setStyleRanges(new StyleRange[0]);
} else {
String text = Integer.toString(local) + " (in use)";
StyledString styledString = new StyledString();
styledString.append(text, usedPortStyler);
cell.setText(styledString.toString());
cell.setStyleRanges(styledString.getStyleRanges());
}
super.update(cell);
}
}, viewer, tableLayout);
createTableColumn("Remote Port", 2, new CellLabelProvider() {
@Override
public void update(ViewerCell cell) {
IPortForwardable.PortPair port = (IPortForwardable.PortPair) cell.getElement();
cell.setText(Integer.toString(port.getRemotePort()));
}
}, viewer, tableLayout);
createTableColumn("Status", 1, new CellLabelProvider() {
@Override
public void update(ViewerCell cell) {
final boolean started = wizardModel.getPortForwarding();
cell.setText(started ? "Started" : "Stopped");
}
}, viewer, tableLayout);
IObservableValue forwardablePortsModelObservable =
BeanProperties.value(PortForwardingWizardModel.PROPERTY_FORWARDABLE_PORTS).observe(wizardModel);
final ForwardablePortListValidator validator =
new ForwardablePortListValidator(forwardablePortsModelObservable);
dbc.addValidationStatusProvider(validator);
viewer.setInput(wizardModel.getForwardablePorts());
//
return viewer;
}
StyledString.Styler usedPortStyler = new StyledString.Styler() {
@Override
public void applyStyles(TextStyle textStyle) {
textStyle.foreground = Display.getDefault().getSystemColor(SWT.COLOR_RED);
}
};
private void createTableColumn(String name, int weight, CellLabelProvider cellLabelProvider, TableViewer viewer,
TableColumnLayout layout) {
TableViewerColumn column = new TableViewerColumn(viewer, SWT.LEFT);
column.getColumn().setText(name);
column.setLabelProvider(cellLabelProvider);
layout.setColumnData(column.getColumn(), new ColumnWeightData(weight, true));
}
private void refreshViewerInput(final TableViewer viewer) {
getShell().getDisplay().syncExec(new Runnable() {
@Override
public void run() {
try {
viewer.setInput(wizardModel.getForwardablePorts());
} catch (Exception e) {
LOG.logError("Failed to refresh table content with list of ports for selected pod", e);
}
}
});
}
class ForwardablePortListValidator extends MultiValidator {
private final IObservableValue viewerObservable;
public ForwardablePortListValidator(IObservableValue viewerObservable) {
this.viewerObservable = viewerObservable;
}
@Override
protected IStatus validate() {
@SuppressWarnings("unchecked")
final Collection<IPortForwardable.PortPair> ports = (Collection<IPortForwardable.PortPair>) viewerObservable.getValue();
if(ports == null || ports.isEmpty()) {
return ValidationStatus.error(
NLS.bind("There are no available ports to forward to {0}.\nYour pod may not be running or does not expose any ports.",
wizardModel.getPodName()));
}
return Status.OK_STATUS;
}
}
@Override
public boolean isPageComplete() {
// enable OK button even when binding validation errors exist
return true;
}
@Override
public void dispose() {
if(!wizardModel.getPortForwarding()) {
wizardModel.setUseFreePorts(false);
}
super.dispose();
}
}