/******************************************************************************* * Copyright (c) 2014, 2016 Red Hat Inc. 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: * Red Hat - Initial Contribution *******************************************************************************/ package org.eclipse.linuxtools.internal.docker.ui.wizards; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; import org.eclipse.core.databinding.AggregateValidationStatus; import org.eclipse.core.databinding.DataBindingContext; import org.eclipse.core.databinding.UpdateValueStrategy; import org.eclipse.core.databinding.beans.BeanProperties; import org.eclipse.core.databinding.beans.PojoProperties; import org.eclipse.core.databinding.observable.list.IObservableList; import org.eclipse.core.databinding.observable.value.IObservableValue; import org.eclipse.core.databinding.validation.IValidator; import org.eclipse.core.databinding.validation.ValidationStatus; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.jface.databinding.swt.ISWTObservableValue; import org.eclipse.jface.databinding.swt.WidgetProperties; import org.eclipse.jface.databinding.viewers.ObservableListContentProvider; import org.eclipse.jface.databinding.viewers.ViewerProperties; import org.eclipse.jface.databinding.wizard.WizardPageSupport; import org.eclipse.jface.layout.GridDataFactory; import org.eclipse.jface.layout.GridLayoutFactory; import org.eclipse.jface.viewers.CellLabelProvider; import org.eclipse.jface.viewers.ColumnLabelProvider; import org.eclipse.jface.viewers.StyledCellLabelProvider; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.viewers.TableViewerColumn; import org.eclipse.jface.wizard.WizardPage; import org.eclipse.linuxtools.docker.core.AbstractRegistry; import org.eclipse.linuxtools.docker.core.DockerException; import org.eclipse.linuxtools.docker.core.IDockerImageSearchResult; import org.eclipse.linuxtools.docker.core.IRegistry; import org.eclipse.linuxtools.docker.ui.Activator; import org.eclipse.linuxtools.internal.docker.ui.SWTImagesFactory; import org.eclipse.swt.SWT; import org.eclipse.swt.events.KeyAdapter; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.KeyListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.events.TraverseListener; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Group; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableColumn; import org.eclipse.swt.widgets.Text; /** * @author xcoulon * */ public class ImageSearchPage extends WizardPage { private final ImageSearchModel model; private final DataBindingContext ctx = new DataBindingContext(); private IRegistry registry; /** * Default constructor. * * @param model * the model for this page */ public ImageSearchPage(final ImageSearchModel model, final IRegistry reg) { super("ImageSearchPage", //$NON-NLS-1$ WizardMessages.getString("ImageSearchPage.title"), //$NON-NLS-1$ SWTImagesFactory.DESC_BANNER_REPOSITORY); this.model = model; this.registry = reg; } /** * @return the selected Docker Image in the result table */ public IDockerImageSearchResult getSelectedImage() { return model.getSelectedImage(); } @Override public void dispose() { ctx.dispose(); super.dispose(); } @Override public void createControl(final Composite parent) { final int COLUMNS = 3; final Composite container = new Composite(parent, SWT.NONE); GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).span(1, 1) .grab(true, false).applyTo(container); GridLayoutFactory.fillDefaults().numColumns(COLUMNS).margins(6, 6) .spacing(10, 2).applyTo(container); // search text final Label searchImageLabel = new Label(container, SWT.NONE); searchImageLabel.setText( WizardMessages.getString("ImageSearchPage.imageLabel")); //$NON-NLS-1$ GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER) .grab(false, false).applyTo(searchImageLabel); final Text searchImageText = new Text(container, SWT.BORDER | SWT.SEARCH | SWT.ICON_SEARCH | SWT.ICON_CANCEL); GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER) .grab(true, false).applyTo(searchImageText); searchImageText.addKeyListener(onKeyPressed()); searchImageText.addTraverseListener(onSearchImageTextTraverse()); final Button searchImageButton = new Button(container, SWT.NONE); searchImageButton .setText(WizardMessages.getString("ImageSearchPage.search")); //$NON-NLS-1$ GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER) .grab(false, false).applyTo(searchImageButton); searchImageButton.addSelectionListener(onSearchImageButtonSelected()); // result table final Label searchResultLabel = new Label(container, SWT.NONE); searchResultLabel.setText( WizardMessages.getString("ImageSearchPage.searchResultLabel")); //$NON-NLS-1$ GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER) .grab(true, false).span(COLUMNS, 1).applyTo(searchResultLabel); final Table table = new Table(container, SWT.BORDER | SWT.FULL_SELECTION | SWT.V_SCROLL | SWT.H_SCROLL); final TableViewer searchResultTableViewer = new TableViewer(table); table.setHeaderVisible(true); table.setLinesVisible(true); addTableViewerColum(searchResultTableViewer, WizardMessages.getString("ImageSearchPage.nameColumn"), //$NON-NLS-1$ SWT.NONE, SWT.RIGHT, 290, new ImageNameColumnLabelProvider()); addTableViewerColum(searchResultTableViewer, WizardMessages.getString("ImageSearchPage.starsColumn"), //$NON-NLS-1$ SWT.NONE, SWT.RIGHT, 70, new ImageStarsColumnLabelProvider()); addTableViewerColum(searchResultTableViewer, WizardMessages.getString("ImageSearchPage.officialColumn"), //$NON-NLS-1$ SWT.NONE, SWT.CENTER, 70, new ImageOfficialColumnLabelProvider()); addTableViewerColum(searchResultTableViewer, WizardMessages.getString("ImageSearchPage.automatedColumn"), //$NON-NLS-1$ SWT.NONE, SWT.CENTER, 70, new ImageAutomatedColumnLabelProvider()); GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL) .grab(true, true).span(COLUMNS, 1).hint(200, 100) .applyTo(table); // description text area final Group selectedImageDescriptionGroup = new Group(container, SWT.NONE); selectedImageDescriptionGroup.setText(WizardMessages.getString("ImageSearchPage.descriptionGroup")); //$NON-NLS-1$ GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER) .grab(true, true).span(COLUMNS, 1) .applyTo(selectedImageDescriptionGroup); GridLayoutFactory.fillDefaults().margins(6, 6).spacing(10, 2) .applyTo(selectedImageDescriptionGroup); final Label selectedImageDescription = new Label( selectedImageDescriptionGroup, SWT.WRAP); GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER) .grab(true, true).hint(200, 100) .applyTo(selectedImageDescription); // bind the search term final IObservableValue observableTermModel = BeanProperties .value(ImageSearchModel.class, ImageSearchModel.TERM) .observe(model); final ISWTObservableValue imageSearchTextObservable = WidgetProperties .text(SWT.Modify).observe(searchImageText); ctx.bindValue(imageSearchTextObservable, observableTermModel, new UpdateValueStrategy().setBeforeSetValidator( new SearchTermValidator(searchImageButton)), null); // observe the viewer content searchResultTableViewer .setContentProvider(new ObservableListContentProvider()); // observe the viewer content final IObservableList observableSearchResultModel = BeanProperties .list(ImageSearchModel.class, ImageSearchModel.IMAGE_SEARCH_RESULT) .observe(model); searchResultTableViewer.setInput(observableSearchResultModel); // observe the viewer selection ctx.bindValue( ViewerProperties.singleSelection() .observe(searchResultTableViewer), BeanProperties.value(ImageSearchModel.SELECTED_IMAGE) .observe(model)); // observe the viewer selection to update the description label final IObservableValue observableSelectedImageDescription = PojoProperties .value("description", String.class) // $NON-NLS-1$ .observeDetail(ViewerProperties.singleSelection() .observe(searchResultTableViewer)); ctx.bindValue(WidgetProperties.text().observe(selectedImageDescription), observableSelectedImageDescription); searchImageText.setFocus(); // attach the Databinding context status to this wizard page. WizardPageSupport.create(this, this.ctx); setControl(container); // trigger a search if an image name was provided if (model.getTerm() != null && !model.getTerm().isEmpty()) { searchImages(); } } private TableViewerColumn addTableViewerColum(final TableViewer tableViewer, final String title, final int style, final int alignment, final int width, final CellLabelProvider columnLabelProvider) { final TableViewerColumn viewerColumn = new TableViewerColumn( tableViewer, style); final TableColumn column = viewerColumn.getColumn(); if (title != null) { column.setText(title); } column.setAlignment(alignment); column.setWidth(width); viewerColumn.setLabelProvider(columnLabelProvider); return viewerColumn; } private TraverseListener onSearchImageTextTraverse() { return e -> { if (e.keyCode == SWT.CR) { e.doit = false; } }; } private KeyListener onKeyPressed() { return new KeyAdapter() { @Override public void keyReleased(final KeyEvent event) { final IStatus status = AggregateValidationStatus .getStatusMaxSeverity( ctx.getValidationStatusProviders()); final String searchTerm = ImageSearchPage.this.model.getTerm(); if (event.character == SWT.CR && searchTerm != null && !searchTerm.isEmpty() && status.isOK()) { searchImages(); } } }; } private SelectionListener onSearchImageButtonSelected() { return new SelectionAdapter() { @Override public void widgetSelected(final SelectionEvent event) { searchImages(); } }; } private void searchImages() { final String term = this.model.getTerm(); if (term.isEmpty()) { return; } try { final BlockingQueue<List<IDockerImageSearchResult>> searchResultQueue = new ArrayBlockingQueue<>( 1); ImageSearchPage.this.getContainer().run(true, true, monitor -> { monitor.beginTask(WizardMessages .getString("ImageSearchPage.searchTask"), 1); //$NON-NLS-1$ final List<IDockerImageSearchResult> searchResults; try { /* * The searchImages API that goes through the Docker * Daemon is faster than querying for the JSON * results over HTTP so if we're dealing with * DockerHub, we use the API. */ // TODO: remove cast once AbstractRegistry methods // are // part of the IRegistry interface if (((AbstractRegistry) registry) .isDockerHubRegistry()) { searchResults = ImageSearchPage.this.model .getSelectedConnection().searchImages(term); } else { searchResults = registry.getImages(term); } searchResultQueue.offer(searchResults); } catch (DockerException e) { Activator.log(e); searchResultQueue.offer( new ArrayList<IDockerImageSearchResult>()); } monitor.done(); }); List<IDockerImageSearchResult> res = searchResultQueue .poll(10, TimeUnit.SECONDS); final List<IDockerImageSearchResult> searchResult = (res == null) ? new ArrayList<>() : res; Display.getCurrent().asyncExec(() -> { ImageSearchPage.this.model.setImageSearchResult(searchResult); // refresh the wizard buttons getWizard().getContainer().updateButtons(); }); // display a warning in the title area if the search result is empty if (searchResult.isEmpty()) { this.setMessage( WizardMessages .getString("ImageSearchPage.noImageWarning"), //$NON-NLS-1$ WARNING); } else if (searchResult.size() == 1) { this.setMessage( WizardMessages .getString("ImageSearchPage.oneImageMatched"), //$NON-NLS-1$ INFORMATION); } else { this.setMessage( WizardMessages.getFormattedString( "ImageSearchPage.imagesMatched", //$NON-NLS-1$ Integer.toString(searchResult.size())), INFORMATION); } } catch (InvocationTargetException | InterruptedException e) { Activator.log(e); } } @Override public boolean isPageComplete() { return this.model.getSelectedImage() != null; } static class SearchTermValidator implements IValidator { private static final String REPOSITORY = "[a-z0-9]+([._-][a-z0-9]+)*"; private static final String NAME = "[a-z0-9]+([._-][a-z0-9]+)*"; private static final Pattern termPattern = Pattern .compile("(" + REPOSITORY + "/)?" + NAME); private final Button searchImageButton; public SearchTermValidator(final Button searchImageButton) { this.searchImageButton = searchImageButton; } @Override public IStatus validate(final Object value) { final String term = (String) value; if (term == null || term.isEmpty()) { this.searchImageButton.setEnabled(false); return ValidationStatus.info(WizardMessages .getString("ImageSearchPage.description")); //$NON-NLS-1$ } else if (termPattern.matcher(term).matches()) { this.searchImageButton.setEnabled(true); return Status.OK_STATUS; } else { this.searchImageButton.setEnabled(false); return ValidationStatus.error(WizardMessages.getFormattedString( "ImageSearchPage.term.invalidformat", //$NON-NLS-1$ termPattern.pattern())); } } } static class ImageNameColumnLabelProvider extends ColumnLabelProvider { @Override public String getText(final Object element) { if (element instanceof IDockerImageSearchResult) { return ((IDockerImageSearchResult) element).getName(); } return super.getText(element); } } static class ImageStarsColumnLabelProvider extends ColumnLabelProvider { @Override public String getText(final Object element) { if (element instanceof IDockerImageSearchResult) { return Integer.toString( ((IDockerImageSearchResult) element).getStarCount()); } return super.getText(element); } } static class ImageOfficialColumnLabelProvider extends IconColumnLabelProvider { @Override boolean doPaint(final Object element) { return element instanceof IDockerImageSearchResult && ((IDockerImageSearchResult) element).isOfficial(); } } static class ImageAutomatedColumnLabelProvider extends IconColumnLabelProvider { @Override boolean doPaint(final Object element) { return element instanceof IDockerImageSearchResult && ((IDockerImageSearchResult) element).isAutomated(); } } static abstract class IconColumnLabelProvider extends StyledCellLabelProvider { private static final Image ICON = SWTImagesFactory.DESC_RESOLVED .createImage(); @Override protected void measure(Event event, Object element) { if (!ICON.isDisposed()) { final Rectangle rectangle = ICON.getBounds(); event.setBounds(new Rectangle(event.x, event.y, rectangle.width + 200, rectangle.height)); } } @Override protected void paint(final Event event, final Object element) { final TableColumn column = ((TableViewerColumn) getColumn()) .getColumn(); final int columnWidth = column.getWidth(); if (doPaint(element)) { final Rectangle bounds = event.getBounds(); event.gc.drawImage(ICON, bounds.x + (columnWidth - ICON.getBounds().width) / 2, bounds.y + 1); } } @Override public void dispose() { super.dispose(); } abstract boolean doPaint(final Object element); } }