/*
* SonarLint for Eclipse
* Copyright (C) 2015-2017 SonarSource SA
* sonarlint@sonarsource.com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonarlint.eclipse.ui.internal.bind;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.eclipse.core.databinding.beans.BeanProperties;
import org.eclipse.core.databinding.observable.list.WritableList;
import org.eclipse.core.databinding.property.value.IValueProperty;
import org.eclipse.jface.bindings.keys.IKeyLookup;
import org.eclipse.jface.bindings.keys.KeyLookupFactory;
import org.eclipse.jface.databinding.viewers.ViewerSupport;
import org.eclipse.jface.dialogs.IMessageProvider;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.CellEditor;
import org.eclipse.jface.viewers.CheckboxTableViewer;
import org.eclipse.jface.viewers.ColumnViewerEditor;
import org.eclipse.jface.viewers.ColumnViewerEditorActivationEvent;
import org.eclipse.jface.viewers.ColumnViewerEditorActivationStrategy;
import org.eclipse.jface.viewers.ComboViewer;
import org.eclipse.jface.viewers.EditingSupport;
import org.eclipse.jface.viewers.FocusCellHighlighter;
import org.eclipse.jface.viewers.FocusCellOwnerDrawHighlighter;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.TableViewerColumn;
import org.eclipse.jface.viewers.TableViewerEditor;
import org.eclipse.jface.viewers.TableViewerFocusCellManager;
import org.eclipse.jface.window.Window;
import org.eclipse.jface.wizard.WizardDialog;
import org.eclipse.jface.wizard.WizardPage;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
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.Link;
import org.eclipse.ui.forms.widgets.Form;
import org.eclipse.ui.forms.widgets.FormToolkit;
import org.eclipse.ui.part.PageBook;
import org.sonarlint.eclipse.core.SonarLintLogger;
import org.sonarlint.eclipse.core.internal.SonarLintCorePlugin;
import org.sonarlint.eclipse.core.internal.TriggerType;
import org.sonarlint.eclipse.core.internal.jobs.ProjectUpdateJob;
import org.sonarlint.eclipse.core.internal.resources.SonarLintProjectConfiguration;
import org.sonarlint.eclipse.core.internal.server.IServer;
import org.sonarlint.eclipse.core.internal.server.IServerLifecycleListener;
import org.sonarlint.eclipse.core.internal.utils.StringUtils;
import org.sonarlint.eclipse.core.resource.ISonarLintProject;
import org.sonarlint.eclipse.ui.internal.Messages;
import org.sonarlint.eclipse.ui.internal.SonarLintImages;
import org.sonarlint.eclipse.ui.internal.server.actions.JobUtils;
import org.sonarlint.eclipse.ui.internal.server.wizard.NewServerLocationWizard;
import org.sonarsource.sonarlint.core.client.api.connected.RemoteModule;
import org.sonarsource.sonarlint.core.client.api.util.TextSearchIndex;
public class BindProjectsPage extends WizardPage {
private final List<ISonarLintProject> projects;
private CheckboxTableViewer viewer;
private Form noServersPage;
private PageBook book;
private IServerLifecycleListener serverListener;
private IServer selectedServer;
private Composite serverDropDownPage;
private ComboViewer serverCombo;
private Button autoBindBtn;
private Button unassociateBtn;
private Button checkAll;
private Composite container;
private Link updateServerLink;
public BindProjectsPage(List<ISonarLintProject> projects) {
super("bindProjects", "Bind Eclipse projects to SonarQube projects", SonarLintImages.SONARWIZBAN_IMG);
setDescription(
"SonarQube is an Open Source platform to manage code quality. "
+ "Bind your Eclipse projects to some SonarQube projects in order to get the same issues in Eclipse and in SonarQube.");
this.projects = projects;
if (!projects.isEmpty()) {
selectedServer = SonarLintCorePlugin.getServersManager().getServer(SonarLintProjectConfiguration.read(projects.get(0).getScopeContext()).getServerId());
}
}
@Override
public void dispose() {
if (serverListener != null) {
SonarLintCorePlugin.getServersManager().removeServerLifecycleListener(serverListener);
}
}
@Override
public void createControl(Composite parent) {
container = new Composite(parent, SWT.NONE);
GridLayout layout = new GridLayout();
layout.numColumns = 1;
layout.marginHeight = 0;
layout.marginWidth = 5;
container.setLayout(layout);
book = new PageBook(container, SWT.NONE);
createNoServerForm(book);
createServerDropDown(book);
toggleServerPage();
createCheckUncheckAllCb();
viewer = CheckboxTableViewer.newCheckList(container, SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL | SWT.FULL_SELECTION);
viewer.getTable().setLayoutData(new GridData(GridData.FILL, GridData.FILL, true, true, 1, 3));
viewer.getTable().setHeaderVisible(true);
TableViewerColumn columnProject = new TableViewerColumn(viewer, SWT.LEFT);
columnProject.getColumn().setText("Eclipse Project");
columnProject.getColumn().setWidth(200);
TableViewerColumn columnSonarProject = new TableViewerColumn(viewer, SWT.LEFT);
columnSonarProject.getColumn().setText("SonarQube Project");
columnSonarProject.getColumn().setWidth(600);
columnSonarProject.setEditingSupport(new ProjectAssociationModelEditingSupport(viewer));
List<ProjectBindModel> list = new ArrayList<>();
for (ISonarLintProject project : projects) {
ProjectBindModel sonarProject = new ProjectBindModel(project);
list.add(sonarProject);
}
ColumnViewerEditorActivationStrategy activationSupport = createActivationSupport();
/*
* Without focus highlighter, keyboard events will not be delivered to
* ColumnViewerEditorActivationStragety#isEditorActivationEvent(...) (see above)
*/
FocusCellHighlighter focusCellHighlighter = new FocusCellOwnerDrawHighlighter(viewer);
TableViewerFocusCellManager focusCellManager = new TableViewerFocusCellManager(viewer, focusCellHighlighter);
TableViewerEditor.create(viewer, focusCellManager, activationSupport, ColumnViewerEditor.TABBING_VERTICAL
| ColumnViewerEditor.KEYBOARD_ACTIVATION);
ViewerSupport.bind(
viewer,
new WritableList(list, ProjectBindModel.class),
new IValueProperty[] {BeanProperties.value(ProjectBindModel.class, ProjectBindModel.PROPERTY_PROJECT_ECLIPSE_NAME),
BeanProperties.value(ProjectBindModel.class, ProjectBindModel.PROPERTY_PROJECT_SONAR_FULLNAME)});
Composite btnContainer = new Composite(container, SWT.NONE);
FillLayout btnLayout = new FillLayout();
btnContainer.setLayout(btnLayout);
viewer.addSelectionChangedListener(event -> updateState());
createUnassociateBtn(btnContainer);
createAutoBindBtn(btnContainer);
viewer.setAllChecked(true);
updateState();
setControl(container);
}
private void createAutoBindBtn(Composite btnContainer) {
autoBindBtn = new Button(btnContainer, SWT.PUSH);
autoBindBtn.setText("Auto bind selected projects");
autoBindBtn.addListener(SWT.Selection, event -> {
TextSearchIndex<RemoteModule> moduleIndex = selectedServer.getModuleIndex();
for (Object object : viewer.getCheckedElements()) {
ProjectBindModel bind = (ProjectBindModel) object;
Map<RemoteModule, Double> results = moduleIndex.search(bind.getEclipseName());
if (!results.isEmpty()) {
// Take first highest scoring
bind.associate(selectedServer.getId(), results.keySet().iterator().next().getKey());
} else {
bind.setAutoBindFailed(true);
}
}
});
}
private void createUnassociateBtn(Composite btnContainer) {
unassociateBtn = new Button(btnContainer, SWT.PUSH);
unassociateBtn.setText("Unbind selected projects");
unassociateBtn.setEnabled(viewer.getCheckedElements().length > 0);
unassociateBtn.addListener(SWT.Selection, event -> {
for (Object object : viewer.getCheckedElements()) {
ProjectBindModel bind = (ProjectBindModel) object;
bind.unassociate();
}
});
}
private void createCheckUncheckAllCb() {
checkAll = new Button(container, SWT.CHECK);
checkAll.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
viewer.setAllChecked(checkAll.getSelection());
updateState();
}
});
}
private void createServerDropDown(Composite parent) {
serverDropDownPage = new Composite(parent, SWT.NONE);
GridData layoutData = new GridData();
layoutData.horizontalSpan = 2;
serverDropDownPage.setLayoutData(layoutData);
GridLayout layout = new GridLayout();
layout.numColumns = 3;
layout.marginHeight = 0;
layout.marginWidth = 5;
serverDropDownPage.setLayout(layout);
Label labelField = new Label(serverDropDownPage, SWT.NONE);
labelField.setText("Select a SonarQube server: ");
serverCombo = new ComboViewer(serverDropDownPage, SWT.READ_ONLY);
serverCombo.setContentProvider(ArrayContentProvider.getInstance());
serverCombo.setLabelProvider(new LabelProvider() {
@Override
public String getText(Object element) {
IServer current = (IServer) element;
return current.getId();
}
});
updateServerLink = new Link(serverDropDownPage, SWT.NONE);
updateServerLink.setText("<a>Refresh project list</a>");
updateServerLink.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
updateProjectListOfSelectedServer();
}
});
serverListener = new ServerChangeListener();
SonarLintCorePlugin.getServersManager().addServerLifecycleListener(serverListener);
/* within the selection event, tell the object it was selected */
serverCombo.addSelectionChangedListener(event -> {
IStructuredSelection selection = (IStructuredSelection) event.getSelection();
selectedServer = (IServer) selection.getFirstElement();
serverCombo.refresh();
updateState();
});
}
private void updateProjectListOfSelectedServer() {
updateServerLink.setEnabled(false);
try {
final IServer server = (IServer) ((IStructuredSelection) serverCombo.getSelection()).getFirstElement();
getContainer().run(true, false, monitor -> {
server.updateModuleList(monitor);
Display.getDefault().asyncExec(this::updateState);
});
} catch (InvocationTargetException ex) {
SonarLintLogger.get().error("Unable to update project list", ex);
setMessage(ex.getCause().getMessage(), IMessageProvider.ERROR);
} catch (InterruptedException e1) {
// Job cancelled, ignore
} finally {
updateServerLink.setEnabled(true);
}
}
private void updateState() {
if (viewer == null) {
return;
}
updateServerLink.setVisible(selectedServer != null);
updateServerLink.setEnabled(selectedServer != null && !selectedServer.isUpdating());
boolean hasSelected = viewer.getCheckedElements().length > 0;
checkAll.setSelection(hasSelected);
checkAll.setGrayed(viewer.getCheckedElements().length < projects.size());
checkAll.setText(hasSelected ? "Unselect all" : "Select all");
unassociateBtn.setEnabled(hasSelected);
if (selectedServer != null && !selectedServer.isStorageUpdated()) {
setMessage("No data for the selected server", IMessageProvider.WARNING);
} else {
setMessage(null);
}
if (autoBindBtn != null) {
autoBindBtn.setEnabled(hasSelected && selectedServer != null && selectedServer.isStorageUpdated());
}
container.layout(true, true);
}
private void createNoServerForm(Composite parent) {
FormToolkit toolkit = new FormToolkit(parent.getDisplay());
noServersPage = toolkit.createForm(book);
GridData layoutData = new GridData();
layoutData.horizontalSpan = 2;
noServersPage.setLayoutData(layoutData);
Composite body = noServersPage.getBody();
GridLayout layout = new GridLayout(2, false);
body.setLayout(layout);
Link hlink = new Link(body, SWT.NONE);
hlink.setText(Messages.ServersView_noServers);
hlink.setBackground(book.getDisplay().getSystemColor(SWT.COLOR_LIST_BACKGROUND));
GridData gd = new GridData(SWT.LEFT, SWT.FILL, true, false);
hlink.setLayoutData(gd);
hlink.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
NewServerLocationWizard wizard = new NewServerLocationWizard();
WizardDialog wd = new WizardDialog(book.getShell(), wizard);
if (wd.open() == Window.OK) {
toggleServerPage();
}
}
});
}
private void toggleServerPage() {
List<IServer> servers = SonarLintCorePlugin.getServersManager().getServers();
if (servers.isEmpty()) {
book.showPage(noServersPage);
selectedServer = null;
} else {
book.showPage(serverDropDownPage);
serverCombo.setInput(servers.toArray());
if (!servers.contains(selectedServer)) {
selectedServer = servers.get(0);
}
serverCombo.setSelection(new StructuredSelection(selectedServer));
}
}
private ColumnViewerEditorActivationStrategy createActivationSupport() {
ColumnViewerEditorActivationStrategy activationSupport = new ColumnViewerEditorActivationStrategy(viewer) {
@Override
protected boolean isEditorActivationEvent(ColumnViewerEditorActivationEvent event) {
return event.eventType == ColumnViewerEditorActivationEvent.TRAVERSAL
|| event.eventType == ColumnViewerEditorActivationEvent.MOUSE_CLICK_SELECTION
|| event.eventType == ColumnViewerEditorActivationEvent.PROGRAMMATIC
|| (event.eventType == ColumnViewerEditorActivationEvent.KEY_PRESSED && event.keyCode == KeyLookupFactory
.getDefault().formalKeyLookup(IKeyLookup.F2_NAME));
}
};
activationSupport.setEnableEditorActivationWithKeyboard(true);
return activationSupport;
}
private final class ServerChangeListener implements IServerLifecycleListener {
@Override
public void serverRemoved(IServer server) {
updateServerPage();
}
@Override
public void serverChanged(IServer server) {
updateServerPage();
}
@Override
public void serverAdded(IServer server) {
updateServerPage();
}
private void updateServerPage() {
getContainer().getShell().getDisplay().asyncExec(() -> {
toggleServerPage();
getContainer().getShell().layout(true, true);
});
}
}
private class ProjectAssociationModelEditingSupport extends EditingSupport {
public ProjectAssociationModelEditingSupport(TableViewer viewer) {
super(viewer);
}
@Override
protected boolean canEdit(Object element) {
return selectedServer != null && element instanceof ProjectBindModel;
}
@Override
protected CellEditor getCellEditor(Object element) {
return new TextCellEditorWithContentProposal(viewer.getTable(), new SearchEngineProvider(selectedServer, BindProjectsPage.this), (ProjectBindModel) element);
}
@Override
protected Object getValue(Object element) {
return StringUtils.trimToEmpty(((ProjectBindModel) element).getSonarFullName());
}
@Override
protected void setValue(Object element, Object value) {
// Don't set value as the model was already updated in the text adapter
}
}
/**
* Update all Eclipse projects when a binding was provided:
*/
public boolean finish() {
final ProjectBindModel[] projectBindings = getProjects();
for (ProjectBindModel projectBinding : projectBindings) {
updateProjectBinding(projectBinding);
}
return true;
}
private static void updateProjectBinding(ProjectBindModel projectBinding) {
boolean changed = false;
ISonarLintProject project = projectBinding.getProject();
SonarLintProjectConfiguration projectConfig = SonarLintProjectConfiguration.read(project.getScopeContext());
String oldServerId = projectConfig.getServerId();
if (!Objects.equals(projectBinding.getServerId(), oldServerId)) {
projectConfig.setServerId(projectBinding.getServerId());
changed = true;
}
if (!Objects.equals(projectBinding.getModuleKey(), projectConfig.getModuleKey())) {
projectConfig.setModuleKey(projectBinding.getModuleKey());
changed = true;
}
if (changed) {
projectConfig.save();
updateProjectBinding(project, oldServerId);
}
}
private static void updateProjectBinding(ISonarLintProject project, String oldServerId) {
project.deleteAllMarkers(SonarLintCorePlugin.MARKER_ID);
project.deleteAllMarkers(SonarLintCorePlugin.MARKER_CHANGESET_ID);
SonarLintCorePlugin.clearIssueTracker(project);
JobUtils.scheduleAnalysisOfOpenFiles(project, TriggerType.BINDING_CHANGE);
if (project.isBound()) {
new ProjectUpdateJob(project).schedule();
}
JobUtils.notifyServerViewAfterBindingChange(project, oldServerId);
}
private ProjectBindModel[] getProjects() {
WritableList projectAssociations = (WritableList) viewer.getInput();
return (ProjectBindModel[]) projectAssociations.toArray(new ProjectBindModel[projectAssociations.size()]);
}
}