/******************************************************************************* * 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.deployimage; import org.apache.commons.lang.StringUtils; import org.apache.commons.validator.routines.DomainValidator; import org.eclipse.core.databinding.DataBindingContext; import org.eclipse.core.databinding.beans.BeanProperties; import org.eclipse.core.databinding.observable.list.IObservableList; import org.eclipse.core.databinding.observable.value.IObservableValue; import org.eclipse.core.databinding.property.Properties; import org.eclipse.core.databinding.validation.MultiValidator; import org.eclipse.core.databinding.validation.ValidationStatus; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.jface.databinding.fieldassist.ControlDecorationSupport; import org.eclipse.jface.databinding.swt.WidgetProperties; import org.eclipse.jface.databinding.viewers.ObservableListContentProvider; import org.eclipse.jface.databinding.viewers.ObservableMapLabelProvider; import org.eclipse.jface.databinding.viewers.ViewerProperties; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.layout.GridDataFactory; import org.eclipse.jface.layout.GridLayoutFactory; import org.eclipse.jface.viewers.ColumnLabelProvider; import org.eclipse.jface.viewers.DoubleClickEvent; import org.eclipse.jface.viewers.IDoubleClickListener; import org.eclipse.jface.viewers.IElementComparer; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.window.Window; import org.eclipse.jface.wizard.IWizard; import org.eclipse.osgi.util.NLS; import org.eclipse.swt.SWT; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.Text; import org.jboss.tools.common.ui.databinding.ParametrizableWizardPageSupport; import org.jboss.tools.common.ui.databinding.ValueBindingBuilder; import org.jboss.tools.openshift.internal.common.ui.TableCellMouseAdapter; import org.jboss.tools.openshift.internal.common.ui.databinding.IsNotNull2BooleanConverter; import org.jboss.tools.openshift.internal.common.ui.databinding.TrimmingStringConverter; import org.jboss.tools.openshift.internal.common.ui.utils.TableViewerBuilder; import org.jboss.tools.openshift.internal.common.ui.utils.UIUtils; import org.jboss.tools.openshift.internal.common.ui.wizard.AbstractOpenShiftWizardPage; import org.jboss.tools.openshift.internal.ui.OpenShiftImages; import org.jboss.tools.openshift.internal.ui.OpenShiftUIMessages; import com.openshift.restclient.model.IServicePort; /** * Page to configure OpenShift services and routes * * @author jeff.cantrill * @author Jeff Maury * */ public class ServicesAndRoutingPage extends AbstractOpenShiftWizardPage { private static final String PAGE_NAME = "Services && Routing Settings Page"; private static final String PAGE_TITLE = "Services && Routing Settings"; private static final String PAGE_DESCRIPTION = ""; private static final int ROUTE_PORT_COLUMN_INDEX = 3; private IServiceAndRoutingPageModel model; TableViewer portsViewer; public ServicesAndRoutingPage(IWizard wizard, IServiceAndRoutingPageModel model) { super(PAGE_TITLE, PAGE_DESCRIPTION, PAGE_NAME, wizard); this.model = model; } @Override protected void doCreateControls(Composite parent, DataBindingContext dbc) { GridLayoutFactory.fillDefaults().margins(10, 10).applyTo(parent); createExposedPortsControl(parent, dbc); GridDataFactory .fillDefaults() .align(SWT.FILL, SWT.BEGINNING) .grab(true, false) .applyTo(new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL)); //routing Composite routingContainer = new Composite(parent, SWT.NONE); GridDataFactory.fillDefaults() .align(SWT.FILL, SWT.FILL) .grab(true, false) .applyTo(routingContainer); GridLayoutFactory.fillDefaults() .margins(6, 6) .numColumns(2) .applyTo(routingContainer); Button btnAddRoute = new Button(routingContainer, SWT.CHECK); btnAddRoute.setText("Add Route"); btnAddRoute.setToolTipText("Adding a route to the service will make the image accessible\noutside of the OpenShift cluster on all the available service ports. \nYou can target a specific port by editing the route later."); GridDataFactory.fillDefaults() .align(SWT.FILL, SWT.FILL).grab(false, false).span(2, 1).applyTo(btnAddRoute); final IObservableValue<Boolean> addRouteModelObservable = BeanProperties.value(IServiceAndRoutingPageModel.PROPERTY_ADD_ROUTE).observe(model); ValueBindingBuilder.bind(WidgetProperties.selection().observe(btnAddRoute)) .to(addRouteModelObservable) .in(dbc); Label labelRouteHostname = new Label(routingContainer, SWT.NONE); labelRouteHostname.setText("Hostname:"); GridDataFactory.fillDefaults() .align(SWT.FILL, SWT.CENTER) .applyTo(labelRouteHostname); Text textRouteHostname = new Text(routingContainer, SWT.BORDER); GridDataFactory.fillDefaults() .align(SWT.FILL, SWT.CENTER) .grab(true, false) .applyTo(textRouteHostname); ValueBindingBuilder .bind(WidgetProperties.enabled().observe(textRouteHostname)) .to(BeanProperties.value(IServiceAndRoutingPageModel.PROPERTY_ADD_ROUTE) .observe(model)) .in(dbc); final IObservableValue<String> routeHostnameObservable = WidgetProperties.text(SWT.Modify).observe(textRouteHostname); ValueBindingBuilder .bind(routeHostnameObservable) .converting(new TrimmingStringConverter()) .to(BeanProperties.value(IServiceAndRoutingPageModel.PROPERTY_ROUTE_HOSTNAME).observe(model)) .in(dbc); MultiValidator validator = new MultiValidator() { @Override protected IStatus validate() { IStatus status = ValidationStatus.ok(); boolean isAddRoute = addRouteModelObservable.getValue(); String hostName = routeHostnameObservable.getValue(); final IObservableList<IServicePort> portsObservable = BeanProperties.list( IServiceAndRoutingPageModel.PROPERTY_SERVICE_PORTS).observe(model); final IServicePort routingPort = (IServicePort) BeanProperties.value(IServiceAndRoutingPageModel.PROPERTY_ROUTING_PORT).observe(model).getValue(); if (isAddRoute) { if (StringUtils.isBlank(hostName)) { status = ValidationStatus .info(NLS.bind(OpenShiftUIMessages.EmptyHostNameErrorMessage, hostName)); } else if (!DomainValidator.getInstance(true).isValid(hostName)) { status = ValidationStatus .error(NLS.bind(OpenShiftUIMessages.InvalidHostNameErrorMessage, hostName)); } if (!status.matches(IStatus.ERROR) && isAddRoute && (portsObservable.size() > 1) && (routingPort == null)) { if (status.matches(IStatus.INFO)) { status = ValidationStatus.info(status.getMessage() + "\n " + OpenShiftUIMessages.RoundRobinRoutingMessage); } else { status = ValidationStatus.info(OpenShiftUIMessages.RoundRobinRoutingMessage); } } } return status; } }; dbc.addValidationStatusProvider(validator); ControlDecorationSupport.create(validator, SWT.LEFT | SWT.TOP); } private void createExposedPortsControl(Composite parent, DataBindingContext dbc) { Composite container = new Composite(parent, SWT.NONE); GridDataFactory.fillDefaults() .align(SWT.FILL, SWT.FILL).grab(true, false).applyTo(container); GridLayoutFactory.fillDefaults() .numColumns(2).margins(6, 6).applyTo(container); Label label = new Label(container, SWT.NONE); label.setText("Service Ports:"); label.setToolTipText("The exposed ports of the image."); GridDataFactory.fillDefaults() .align(SWT.FILL, SWT.FILL) .span(2,1) .applyTo(label); Composite tableContainer = new Composite(container, SWT.NONE); IObservableList<IServicePort> portsObservable = BeanProperties.list( IServiceAndRoutingPageModel.PROPERTY_SERVICE_PORTS).observe(model); portsViewer = createTable(tableContainer); ObservableListContentProvider contentProvider = new ObservableListContentProvider(); portsViewer.setContentProvider(contentProvider); ObservableMapLabelProvider labelProvider = new ObservableMapLabelProvider( Properties.observeEach(contentProvider.getKnownElements(), BeanProperties.values(ServicePortAdapter.NAME, ServicePortAdapter.PORT, ServicePortAdapter.TARGET_PORT, /* ROUTE_PORT_COLUMN_INDEX = 3 */ ServicePortAdapter.ROUTE_PORT))) { @Override public Image getColumnImage(Object element, int columnIndex) { if (columnIndex == ROUTE_PORT_COLUMN_INDEX) { Object selected = attributeMaps[columnIndex].get(element); return selected != null && (boolean)selected ? OpenShiftImages.CHECKED_IMG : OpenShiftImages.UNCHECKED_IMG; } return null; } @Override public String getColumnText(Object element, int columnIndex) { if (columnIndex != ROUTE_PORT_COLUMN_INDEX) { Object result = attributeMaps[columnIndex].get(element); return result == null ? "" : result.toString(); //$NON-NLS-1$ } return null; } }; portsViewer.setLabelProvider(labelProvider); GridDataFactory.fillDefaults() .span(1, 5).align(SWT.FILL, SWT.FILL).grab(true, true).applyTo(tableContainer); ValueBindingBuilder.bind(ViewerProperties.singleSelection().observe(portsViewer)) .to(BeanProperties.value(IServiceAndRoutingPageModel.PROPERTY_SELECTED_SERVICE_PORT).observe(model)) .in(dbc); portsViewer.setInput(portsObservable); dbc.addValidationStatusProvider(new MultiValidator() { @Override protected IStatus validate() { if(portsObservable.isEmpty()) { return ValidationStatus.error("At least 1 port is required when generating the service for the deployed image"); } return Status.OK_STATUS; } }); portsViewer.getTable().addMouseListener(onTableCellClicked()); Button btnEdit = new Button(container, SWT.PUSH); GridDataFactory.fillDefaults() .align(SWT.FILL, SWT.FILL).applyTo(btnEdit); btnEdit.setText("Edit..."); btnEdit.setToolTipText("Edit a port to be exposed by the service."); btnEdit.addSelectionListener(new EditHandler()); ValueBindingBuilder .bind(WidgetProperties.enabled().observe(btnEdit)) .notUpdatingParticipant() .to(BeanProperties.value(IServiceAndRoutingPageModel.PROPERTY_SELECTED_SERVICE_PORT).observe(model)) .converting(new IsNotNull2BooleanConverter()) .in(dbc); UIUtils.setDefaultButtonWidth(btnEdit); Button btnReset = new Button(container, SWT.PUSH); GridDataFactory.fillDefaults() .align(SWT.FILL, SWT.FILL).applyTo(btnReset); btnReset.setText("Reset"); btnReset.setToolTipText("Resets the list of ports to the exposed ports of the image."); btnReset.addSelectionListener(onReset()); UIUtils.setDefaultButtonWidth(btnReset); } private MouseListener onTableCellClicked() { return new TableCellMouseAdapter(ROUTE_PORT_COLUMN_INDEX) { @Override public void mouseUpCell(MouseEvent event) { IServicePort port = model.getSelectedServicePort(); ServicePortAdapter target = new ServicePortAdapter((ServicePortAdapter)port); target.setRoutePort(!target.isRoutePort()); target.setName(NLS.bind("{0}-tcp", target.getPort())); model.updateServicePort(port, target); model.setSelectedServicePort(target); Display.getDefault().asyncExec(() -> { if(portsViewer != null && portsViewer.getTable() != null && !portsViewer.getTable().isDisposed()) { portsViewer.refresh(); } }); } }; } class EditHandler extends SelectionAdapter implements IDoubleClickListener{ @Override public void doubleClick(DoubleClickEvent event) { handleEvent(); } @Override public void widgetSelected(SelectionEvent e) { handleEvent(); } public void handleEvent(){ String message = "Edit the port to be exposed by the service"; final IServicePort port = model.getSelectedServicePort(); final ServicePortAdapter target = new ServicePortAdapter((ServicePortAdapter)port); ServicePortDialog dialog = new ServicePortDialog(target, message, model.getServicePorts()); if(Window.OK == dialog.open()) { target.setName(NLS.bind("{0}-tcp", target.getPort())); model.updateServicePort(port, target); model.setSelectedServicePort(target); } } } protected TableViewer createTable(Composite tableContainer) { Table table = new Table(tableContainer, SWT.BORDER | SWT.FULL_SELECTION | SWT.V_SCROLL | SWT.H_SCROLL); table.setLinesVisible(true); table.setHeaderVisible(true); TableViewer viewer = new TableViewerBuilder(table, tableContainer) .column("Name").align(SWT.LEFT).weight(2).minWidth(50).buildColumn() .column("Service Port").align(SWT.LEFT).weight(1).minWidth(25).buildColumn() .column("Pod Port").align(SWT.LEFT).weight(1).minWidth(25).buildColumn() .column(new ColumnLabelProvider() { @Override public Image getImage(Object element) { boolean selected = ((ServicePortAdapter)element).isRoutePort(); return selected?OpenShiftImages.CHECKED_IMG:OpenShiftImages.UNCHECKED_IMG; } @Override public String getText(Object element) { return null; } }) .name("Used by route").align(SWT.LEFT).weight(1).buildColumn() .buildViewer(); viewer.addDoubleClickListener(new EditHandler()); /* * required because otherwise values are cached and causes the ObservableMapLabelProvider * not to be updated because remove are not propagated. */ viewer.setComparer(new IElementComparer() { @Override public int hashCode(Object element) { return System.identityHashCode(element); } @Override public boolean equals(Object a, Object b) { return a == b; } }); return viewer; } private SelectionListener onReset() { return new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { if(MessageDialog.openQuestion(getShell(), "Reset ports", "Are you sure you want to reset the serviced ports to those exposed by the image?")) { model.resetServicePorts(); } } }; } /** * Allow Finish for info statuses. */ @Override protected void setupWizardPageSupport(DataBindingContext dbc) { ParametrizableWizardPageSupport.create(IStatus.ERROR | IStatus.CANCEL, this, dbc); } }