/** * Copyright (c) 2010, 2013 Darmstadt University of Technology. * 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: * Olav Lenz - initial API and implementation */ package org.eclipse.recommenders.internal.coordinates.rcp; import static com.google.common.base.Optional.*; import static com.google.common.collect.Iterables.*; import static com.google.common.collect.Sets.newHashSet; import static java.text.MessageFormat.format; import static java.util.concurrent.TimeUnit.SECONDS; import static org.apache.commons.io.IOUtils.LINE_SEPARATOR; import static org.eclipse.recommenders.coordinates.DependencyInfo.*; import static org.eclipse.recommenders.rcp.SharedImages.Images.*; import static org.eclipse.recommenders.utils.Checks.cast; import java.io.IOException; import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Set; import javax.inject.Inject; 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.action.Action; import org.eclipse.jface.action.IAction; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.layout.TableColumnLayout; import org.eclipse.jface.viewers.ArrayContentProvider; import org.eclipse.jface.viewers.CellEditor; import org.eclipse.jface.viewers.CellLabelProvider; import org.eclipse.jface.viewers.ColumnViewerToolTipSupport; import org.eclipse.jface.viewers.ColumnWeightData; import org.eclipse.jface.viewers.ComboBoxViewerCellEditor; import org.eclipse.jface.viewers.EditingSupport; import org.eclipse.jface.viewers.IStructuredContentProvider; import org.eclipse.jface.viewers.ITableLabelProvider; import org.eclipse.jface.viewers.LabelProvider; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.viewers.TableViewerColumn; import org.eclipse.jface.viewers.Viewer; import org.eclipse.jface.viewers.ViewerCell; import org.eclipse.jface.viewers.ViewerFilter; import org.eclipse.jface.window.ToolTip; import org.eclipse.recommenders.coordinates.DependencyInfo; import org.eclipse.recommenders.coordinates.IDependencyListener; import org.eclipse.recommenders.coordinates.IProjectCoordinateAdvisor; import org.eclipse.recommenders.coordinates.ProjectCoordinate; import org.eclipse.recommenders.coordinates.rcp.CoordinateEvents.AdvisorConfigurationChangedEvent; import org.eclipse.recommenders.coordinates.rcp.CoordinateEvents.ProjectCoordinateChangeEvent; import org.eclipse.recommenders.coordinates.rcp.EclipseProjectCoordinateAdvisorService; import org.eclipse.recommenders.internal.coordinates.rcp.l10n.Messages; import org.eclipse.recommenders.rcp.SharedImages; import org.eclipse.recommenders.rcp.utils.TableSortConfigurator; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.CCombo; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableColumn; import org.eclipse.ui.part.ViewPart; import org.eclipse.ui.progress.UIJob; import com.google.common.base.Optional; import com.google.common.base.Supplier; import com.google.common.collect.ListMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimaps; import com.google.common.collect.Sets; import com.google.common.eventbus.EventBus; import com.google.common.eventbus.Subscribe; public class ProjectCoordinatesView extends ViewPart { private static final int COLUMN_LOCATION = 0; private static final int COLUMN_COORDINATE = 1; private TableViewer tableViewer; private ContentProvider contentProvider; private final IDependencyListener dependencyListener; private final EclipseProjectCoordinateAdvisorService pcAdvisorsService; private final ManualProjectCoordinateAdvisor manualPcAdvisor; private Table table; private TableViewerColumn locationColumn; private TableViewerColumn coordinateColumn; private final EventBus bus; private final SharedImages images; private static final Comparator<Object> COMPARE_COORDINATE = new Comparator<Object>() { @Override public int compare(Object o1, Object o2) { if (o1 instanceof Entry && o2 instanceof Entry) { Entry<DependencyInfo, Collection<Optional<ProjectCoordinate>>> firstElement = cast(o1); Entry<DependencyInfo, Collection<Optional<ProjectCoordinate>>> secondElement = cast(o2); Optional<ProjectCoordinate> pc1 = findFirstMatchingCoordinate(firstElement.getValue()); Optional<ProjectCoordinate> pc2 = findFirstMatchingCoordinate(secondElement.getValue()); if (pc1.isPresent() && pc2.isPresent()) { return pc1.get().toString().compareTo(pc2.get().toString()); } else if (pc1.isPresent() && !pc2.isPresent()) { return -1; } else if (!pc1.isPresent() && pc2.isPresent()) { return 1; } else { return 0; } } return 0; } }; private static final Comparator<Object> COMPARE_LOCATION = new Comparator<Object>() { @Override public int compare(Object o1, Object o2) { if (o1 instanceof Entry && o2 instanceof Entry) { Entry<DependencyInfo, Collection<Optional<ProjectCoordinate>>> firstElement = cast(o1); Entry<DependencyInfo, Collection<Optional<ProjectCoordinate>>> secondElement = cast(o2); int compareScore = secondElement.getKey().getType().compareTo(firstElement.getKey().getType()); if (compareScore == 0) { return firstElement.getKey().getFile().getName() .compareToIgnoreCase(secondElement.getKey().getFile().getName()); } return compareScore; } return 0; } }; @Inject public ProjectCoordinatesView(IDependencyListener dependencyListener, final EclipseProjectCoordinateAdvisorService pcAdvisorService, final ManualProjectCoordinateAdvisor manualProjectCoordinateAdvisor, EventBus bus, SharedImages images) { this.dependencyListener = dependencyListener; this.pcAdvisorsService = pcAdvisorService; manualPcAdvisor = manualProjectCoordinateAdvisor; this.bus = bus; bus.register(this); this.images = images; } @Override public void createPartControl(final Composite parent) { Composite composite = new Composite(parent, SWT.NONE); TableColumnLayout tableLayout = new TableColumnLayout(); composite.setLayout(tableLayout); tableViewer = new TableViewer(composite, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL | SWT.FULL_SELECTION); contentProvider = new ContentProvider(); tableViewer.setContentProvider(contentProvider); tableViewer.setInput(getViewSite()); ColumnViewerToolTipSupport.enableFor(tableViewer, ToolTip.NO_RECREATE); locationColumn = new TableViewerColumn(tableViewer, SWT.NONE); TableColumn tableColumn = locationColumn.getColumn(); tableColumn.setText(Messages.COLUMN_LABEL_LOCATION); tableLayout.setColumnData(tableColumn, new ColumnWeightData(1, ColumnWeightData.MINIMUM_WIDTH, true)); coordinateColumn = new TableViewerColumn(tableViewer, SWT.NONE); coordinateColumn.setEditingSupport(new ProjectCoordinateEditing(tableViewer)); tableColumn = coordinateColumn.getColumn(); tableColumn.setText(Messages.COLUMN_LABEL_COORDINATE); tableLayout.setColumnData(tableColumn, new ColumnWeightData(1, ColumnWeightData.MINIMUM_WIDTH, true)); table = tableViewer.getTable(); table.setHeaderVisible(true); table.setLinesVisible(true); Action refreshAction = new Action() { @Override public void run() { refreshTableUI(); } }; TableSortConfigurator.newConfigurator(tableViewer, refreshAction) .add(locationColumn.getColumn(), COMPARE_LOCATION).add(coordinateColumn.getColumn(), COMPARE_COORDINATE) .initialize(locationColumn.getColumn(), SWT.UP).configure(); addFilterFunctionality(); addClearCacheButton(); addRefreshButton(); refreshData(); } private void addFilterFunctionality() { final ViewerFilter manualAssignedFilter = new ViewerFilter() { @Override public boolean select(Viewer viewer, Object parentElement, Object element) { if (element instanceof Entry) { Collection<Optional<ProjectCoordinate>> value = extractProjectCoordinates(element); return isManualMapping(value); } return false; } private boolean isManualMapping(Collection<Optional<ProjectCoordinate>> pcs) { int indexOfManualMapping = pcAdvisorsService.getAdvisors().indexOf(manualPcAdvisor); Optional<ProjectCoordinate> opc = get(pcs, indexOfManualMapping); return opc.isPresent(); } }; final ViewerFilter conflictingCoordinatesFilter = new ViewerFilter() { @Override public boolean select(Viewer viewer, Object parentElement, Object element) { if (element instanceof Entry) { Collection<Optional<ProjectCoordinate>> value = extractProjectCoordinates(element); return newHashSet(presentInstances(value)).size() > 1; } return false; } }; final ViewerFilter missingCoordinatesFilter = new ViewerFilter() { @Override public boolean select(Viewer viewer, Object parentElement, Object element) { if (element instanceof Entry) { Collection<Optional<ProjectCoordinate>> value = extractProjectCoordinates(element); return isEmpty(presentInstances(value)); } return true; } }; IAction showAll = new Action(Messages.MENUITEM_SHOW_ALL, Action.AS_RADIO_BUTTON) { @Override public void run() { refreshTableUI(); } }; IAction showMissingCoord = new TableFilterAction(Messages.MENUITEM_SHOW_MISSING_COORDINATES_ONLY, Action.AS_RADIO_BUTTON, missingCoordinatesFilter); IAction showConflictingCoord = new TableFilterAction(Messages.MENUITEM_SHOW_CONFLICTING_COORDINATES_ONLY, Action.AS_RADIO_BUTTON, conflictingCoordinatesFilter); IAction showManualAssignedCoord = new TableFilterAction( Messages.MENUITEM_SHOW_MANUALLY_ASSIGNED_COORDINATES_ONLY, Action.AS_RADIO_BUTTON, manualAssignedFilter); MenuManager showMenu = new MenuManager(Messages.MENUITEM_SHOW); showMenu.add(showAll); showMenu.add(showMissingCoord); showMenu.add(showConflictingCoord); showMenu.add(showManualAssignedCoord); getViewSite().getActionBars().getMenuManager().add(showMenu); showAll.setChecked(true); } private void addClearCacheButton() { Action clearCache = new Action() { @Override public void run() { clearProjectCoordianteCache(); refreshData(); } }; clearCache.setText(Messages.MENUITEM_CLEAR_CACHE); clearCache.setImageDescriptor(images.getDescriptor(ELCL_CLEAR)); getViewSite().getActionBars().getMenuManager().add(clearCache); } private void clearProjectCoordianteCache() { pcAdvisorsService.clearCache(); } private void addRefreshButton() { IAction refreshAction = new Action() { @Override public void run() { refreshData(); } }; refreshAction.setToolTipText(Messages.TOOLBAR_TOOLTIP_REFRESH); refreshAction.setImageDescriptor(images.getDescriptor(ELCL_REFRESH)); getViewSite().getActionBars().getToolBarManager().add(refreshAction); } class ProjectCoordinateEditing extends EditingSupport { private String formerValue; private final ComboBoxViewerCellEditor editor; public ProjectCoordinateEditing(TableViewer viewer) { super(viewer); editor = new ComboBoxViewerCellEditor(viewer.getTable()); editor.setLabelProvider(new LabelProvider()); editor.setContentProvider(ArrayContentProvider.getInstance()); } @Override protected CellEditor getCellEditor(Object element) { if (element instanceof Entry) { Set<String> values = Sets.newHashSet(); Collection<Optional<ProjectCoordinate>> value = extractProjectCoordinates(element); for (ProjectCoordinate pc : presentInstances(value)) { values.add(pc.toString()); } editor.setInput(values); } return editor; } @Override protected boolean canEdit(Object element) { return true; } @Override protected Object getValue(Object element) { if (element instanceof Entry) { Collection<Optional<ProjectCoordinate>> pcs = extractProjectCoordinates(element); Optional<ProjectCoordinate> optionalFirstMatchingCoordinate = findFirstMatchingCoordinate(pcs); if (optionalFirstMatchingCoordinate.isPresent()) { formerValue = optionalFirstMatchingCoordinate.get().toString(); } else { formerValue = ""; //$NON-NLS-1$ } return formerValue; } return null; } @Override protected void setValue(Object element, Object value) { if (value == null) { if (editor.getControl() instanceof CCombo) { value = ((CCombo) editor.getControl()).getText(); } } if (Objects.equals(value, formerValue)) { return; } if (element instanceof Entry) { DependencyInfo dependencyInfo = extractDependencyInfo(element); if ("".equals(value)) { //$NON-NLS-1$ manualPcAdvisor.removeManualMapping(dependencyInfo); bus.post(new ProjectCoordinateChangeEvent(dependencyInfo)); } else { try { ProjectCoordinate valueOf = ProjectCoordinate.valueOf((String) value); manualPcAdvisor.setManualMapping(dependencyInfo, valueOf); bus.post(new ProjectCoordinateChangeEvent(dependencyInfo)); } catch (Exception e) { MessageDialog.openError(table.getShell(), Messages.DIALOG_TITLE_INVALID_COORDINATE_FORMAT, format(Messages.DIALOG_MESSAGE_INVALID_COORDINATE_FORMAT, value)); return; } } } /* * It is needed to make a total refresh (resolve all dependencies again) because the modification of the * data model isn't possible here (Entry is Immutable) */ refreshData(); } } class ContentProvider implements IStructuredContentProvider { private ListMultimap<DependencyInfo, Optional<ProjectCoordinate>> data; private List<IProjectCoordinateAdvisor> strategies = Lists.newArrayList(); public ContentProvider() { Map<DependencyInfo, Collection<Optional<ProjectCoordinate>>> map = Maps.newHashMap(); data = Multimaps.newListMultimap(map, new Supplier<List<Optional<ProjectCoordinate>>>() { @Override public List<Optional<ProjectCoordinate>> get() { return Lists.newArrayList(); } }); } public void setData(final Set<DependencyInfo> dependencyInfos) { data.clear(); new ResolvingDependenciesJob(Messages.JOB_RESOLVING_DEPENDENCIES, dependencyInfos).schedule(); } private final class ResolvingDependenciesJob extends Job { private final Set<DependencyInfo> dependencyInfos; public ResolvingDependenciesJob(String name, final Set<DependencyInfo> dependencyInfos) { super(name); this.dependencyInfos = dependencyInfos; } @Override protected IStatus run(IProgressMonitor monitor) { monitor.beginTask(Messages.TASK_ASSIGNING_PROJECT_COORDINATES, dependencyInfos.size()); strategies = pcAdvisorsService.getAdvisors(); for (DependencyInfo dependencyInfo : dependencyInfos) { monitor.subTask( format(Messages.TASK_ASSIGNING_PROJECT_COORDINATE_TO, dependencyInfo.getFile().getName())); for (IProjectCoordinateAdvisor strategy : strategies) { data.put(dependencyInfo, strategy.suggest(dependencyInfo)); } // Put the cached value as last element. data.put(dependencyInfo, pcAdvisorsService.suggest(dependencyInfo)); monitor.worked(1); } refreshUI(); return Status.OK_STATUS; } private void refreshUI() { Display.getDefault().asyncExec(new Runnable() { @Override public void run() { refreshTableUI(); } }); } } public List<IProjectCoordinateAdvisor> getStrategies() { return strategies; } @Override public void dispose() { // unused in this case } @Override public void inputChanged(final Viewer viewer, final Object oldInput, final Object newInput) { // unused in this case } @Override public Object[] getElements(final Object inputElement) { return data.asMap().entrySet().toArray(); } } class TableFilterAction extends Action { private final ViewerFilter filter; public TableFilterAction(String text, int style, ViewerFilter filter) { super(text, style); this.filter = filter; } @Override public void run() { if (isChecked()) { if (!isFilterAlreadyAdded()) { tableViewer.addFilter(filter); } } else { tableViewer.removeFilter(filter); } refreshTableUI(); } private boolean isFilterAlreadyAdded() { for (ViewerFilter viewerFilter : tableViewer.getFilters()) { if (viewerFilter.equals(filter)) { return true; } } return false; } } private void refreshData() { new UIJob(Messages.JOB_REFRESHING_PROJECT_COORDINATES_VIEW) { { schedule(); } @Override public IStatus runInUIThread(IProgressMonitor monitor) { contentProvider.setData(dependencyListener.getDependencies()); refreshTableUI(); return Status.OK_STATUS; } }; } private void refreshTableUI() { if (table.isDisposed()) { // https://bugs.eclipse.org/bugs/show_bug.cgi?id=464840 // We might be too late and the view may have been closed already. return; } tableViewer.setLabelProvider(new ViewLabelProvider()); locationColumn.setLabelProvider(new LocationTooltip()); coordinateColumn.setLabelProvider(new CoordinateTooltip()); tableViewer.refresh(); } class ViewLabelProvider extends LabelProvider implements ITableLabelProvider { @Override public String getColumnText(final Object obj, final int index) { if (obj instanceof Entry) { Entry<DependencyInfo, Collection<Optional<ProjectCoordinate>>> entry = cast(obj); DependencyInfo dependencyInfo = entry.getKey(); switch (index) { case COLUMN_LOCATION: String name = dependencyInfo.getFile().getName(); switch (dependencyInfo.getType()) { case JRE: return dependencyInfo.getHint(EXECUTION_ENVIRONMENT).or(name); case PROJECT: return dependencyInfo.getHint(PROJECT_NAME).or(name); default: return name; } case COLUMN_COORDINATE: // The last element contains the cached value Optional<ProjectCoordinate> pc = getLast(entry.getValue(), Optional.<ProjectCoordinate>absent()); if (pc.isPresent()) { return pc.get().toString(); } default: return ""; //$NON-NLS-1$ } } return ""; //$NON-NLS-1$ } @Override public Image getColumnImage(final Object obj, final int index) { if (obj instanceof Entry) { DependencyInfo dependencyInfo = extractDependencyInfo(obj); switch (index) { case COLUMN_LOCATION: return getImageForDependencyTyp(dependencyInfo); default: return null; } } return null; } private Image getImageForDependencyTyp(final DependencyInfo dependencyInfo) { switch (dependencyInfo.getType()) { case JRE: return images.getImage(OBJ_JRE); case JAR: return images.getImage(OBJ_JAR); case PROJECT: return images.getImage(OBJ_JAVA_PROJECT); default: return null; } } } abstract class ToolTipProvider extends CellLabelProvider { @Override public void update(final ViewerCell cell) { cell.setText(cell.getText()); } @Override public String getToolTipText(final Object element) { if (element instanceof Entry) { Entry<DependencyInfo, Collection<Optional<ProjectCoordinate>>> entry = cast(element); return generateTooltip(entry); } return ""; //$NON-NLS-1$ } protected abstract String generateTooltip(Entry<DependencyInfo, Collection<Optional<ProjectCoordinate>>> entry); @Override public Point getToolTipShift(final Object object) { return new Point(5, 5); } @Override public int getToolTipDisplayDelayTime(final Object object) { return 100; } @Override public int getToolTipTimeDisplayed(final Object object) { return (int) SECONDS.toMillis(10); } } class LocationTooltip extends ToolTipProvider { @Override protected String generateTooltip(final Entry<DependencyInfo, Collection<Optional<ProjectCoordinate>>> entry) { DependencyInfo dependencyInfo = entry.getKey(); StringBuilder sb = new StringBuilder(); sb.append(format(Messages.TABLE_CELL_TOOLTIP_LOCATION, dependencyInfo.getFile().getAbsolutePath())); sb.append(LINE_SEPARATOR); sb.append(format(Messages.TABLE_CELL_TOOLTIP_TYPE, dependencyInfo.getType().toString())); Map<String, String> hints = dependencyInfo.getHints(); if (hints != null && !hints.isEmpty()) { sb.append(LINE_SEPARATOR); sb.append(Messages.TABLE_CELL_TOOLTIP_HINTS); for (Entry<String, String> hint : hints.entrySet()) { sb.append(LINE_SEPARATOR); sb.append(" "); //$NON-NLS-1$ sb.append(format(Messages.TABLE_CELL_TOOLTIP_KEY_VALUE, hint.getKey(), hint.getValue())); } } return sb.toString(); } } class CoordinateTooltip extends ToolTipProvider { private String getDisplayName(IProjectCoordinateAdvisor advisor) { AdvisorDescriptor descriptor = pcAdvisorsService.getDescriptor(advisor); return descriptor.getName(); } @Override protected String generateTooltip(final Entry<DependencyInfo, Collection<Optional<ProjectCoordinate>>> entry) { DependencyInfo dependencyInfo = entry.getKey(); StringBuilder sb = new StringBuilder(); List<IProjectCoordinateAdvisor> advisors = contentProvider.getStrategies(); List<Optional<ProjectCoordinate>> coordinates = Lists.newArrayList(entry.getValue()); for (int i = 0; i < advisors.size(); i++) { IProjectCoordinateAdvisor advisor = advisors.get(i); Optional<ProjectCoordinate> coordinate = coordinates.get(i); if (i != 0) { sb.append(LINE_SEPARATOR); } Optional<ProjectCoordinate> optionalCoordinate = advisor.suggest(dependencyInfo); final String value; if (optionalCoordinate.isPresent()) { value = optionalCoordinate.get().toString(); } else { if (coordinate.isPresent()) { value = coordinate.get().toString(); } else { value = Messages.TABLE_CELL_TOOLTIP_UNKNOWN_COORDINATE; } } sb.append(format(Messages.TABLE_CELL_TOOLTIP_KEY_VALUE, getDisplayName(advisor), value)); } return sb.toString(); } } @Override public void setFocus() { tableViewer.getControl().setFocus(); } @Subscribe public void onEvent(AdvisorConfigurationChangedEvent e) throws IOException { refreshData(); } private static Optional<ProjectCoordinate> findFirstMatchingCoordinate( Collection<Optional<ProjectCoordinate>> pcs) { return fromNullable(getFirst(presentInstances(pcs), null)); } private DependencyInfo extractDependencyInfo(final Object obj) { Entry<DependencyInfo, Collection<Optional<ProjectCoordinate>>> entry = cast(obj); return entry.getKey(); } private Collection<Optional<ProjectCoordinate>> extractProjectCoordinates(final Object obj) { Entry<DependencyInfo, Collection<Optional<ProjectCoordinate>>> entry = cast(obj); return entry.getValue(); } }