org.eclipse.ptp.internal.rdt.sync.git.ui.preferences.SyncGitPreferencePage.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.ptp.internal.rdt.sync.git.ui.preferences.SyncGitPreferencePage.java

Source

/*******************************************************************************
 * Copyright (c) 2013 Oak Ridge National Laboratory 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:
 *    John Eblen - initial implementation
 *******************************************************************************/
package org.eclipse.ptp.internal.rdt.sync.git.ui.preferences;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.preferences.IScopeContext;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.jface.dialogs.IMessageProvider;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.dialogs.ProgressMonitorDialog;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.preference.PreferencePage;
import org.eclipse.ptp.internal.rdt.sync.git.core.CommandRunner;
import org.eclipse.ptp.internal.rdt.sync.git.core.CommandRunner.CommandResults;
import org.eclipse.ptp.internal.rdt.sync.git.ui.Activator;
import org.eclipse.ptp.internal.rdt.sync.git.ui.messages.Messages;
import org.eclipse.ptp.rdt.sync.core.RecursiveSubMonitor;
import org.eclipse.ptp.rdt.sync.core.exceptions.RemoteExecutionException;
import org.eclipse.ptp.rdt.sync.core.exceptions.RemoteSyncException;
import org.eclipse.remote.core.IRemoteConnection;
import org.eclipse.remote.core.IRemoteServices;
import org.eclipse.remote.core.RemoteServices;
import org.eclipse.remote.core.exception.RemoteConnectionException;
import org.eclipse.remote.ui.IRemoteUIConnectionManager;
import org.eclipse.remote.ui.IRemoteUIConstants;
import org.eclipse.remote.ui.IRemoteUIFileManager;
import org.eclipse.remote.ui.IRemoteUIServices;
import org.eclipse.remote.ui.RemoteUIServices;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPreferencePage;
import org.osgi.service.prefs.BackingStoreException;
import org.osgi.service.prefs.Preferences;

public class SyncGitPreferencePage extends PreferencePage implements IWorkbenchPreferencePage {
    private static final String instanceScopeSyncNode = "org.eclipse.ptp.rdt.sync.core"; //$NON-NLS-1$
    private static final String GIT_LOCATION_NODE_NAME = "git-location"; //$NON-NLS-1$

    Map<Integer, IRemoteConnection> fComboIndexToRemoteConnectionMap = new HashMap<Integer, IRemoteConnection>();
    Map<IRemoteConnection, String> fConnectionNameToGitPathMap = new HashMap<IRemoteConnection, String>();
    IRemoteConnection fSelectedConnection = null;
    String pathWarningMessage = null;
    String pathErrorMessage = null;
    String gitWarningMessage = null;
    String gitErrorMessage = null;

    private Composite composite;
    private Combo fConnectionCombo;
    private Button fUseDefaultGitLocationCheckbox;
    private Text fGitLocationText;
    private Button fBrowseButton;
    private Button fGitValidateButton;

    @Override
    protected Control createContents(Composite parent) {
        composite = new Composite(parent, SWT.NONE);
        GridLayout layout = new GridLayout();
        layout.numColumns = 2;
        composite.setLayout(layout);

        fConnectionCombo = new Combo(composite, SWT.DROP_DOWN | SWT.READ_ONLY);
        GridData gd = new GridData();
        gd.horizontalSpan = 2;
        fConnectionCombo.setLayoutData(gd);
        fConnectionCombo.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                handleConnectionSelected();
                update();
            }
        });

        fUseDefaultGitLocationCheckbox = new Button(composite, SWT.CHECK);
        gd = new GridData();
        gd.horizontalSpan = 2;
        fUseDefaultGitLocationCheckbox.setLayoutData(gd);
        fUseDefaultGitLocationCheckbox.setText(Messages.SyncGitPreferencePage_0);
        fUseDefaultGitLocationCheckbox.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                handleCheckDefaultGitLocation();
                update();
            }
        });

        fGitLocationText = new Text(composite, SWT.SINGLE | SWT.BORDER);
        gd = new GridData(GridData.FILL_HORIZONTAL);
        fGitLocationText.setLayoutData(gd);
        fGitLocationText.addModifyListener(new ModifyListener() {
            @Override
            public void modifyText(ModifyEvent e) {
                update();
            }
        });

        fBrowseButton = new Button(composite, SWT.PUSH);
        fBrowseButton.setText(Messages.SyncGitPreferencePage_1);
        fBrowseButton.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                if (fSelectedConnection == null) {
                    return;
                }
                if (!fSelectedConnection.isOpen()) {
                    IRemoteUIConnectionManager mgr = getUIConnectionManager();
                    if (mgr != null) {
                        mgr.openConnectionWithProgress(fConnectionCombo.getShell(), null, fSelectedConnection);
                    }
                }
                if (!fSelectedConnection.isOpen()) {
                    return;
                }
                IRemoteUIServices remoteUIServices = RemoteUIServices
                        .getRemoteUIServices(fSelectedConnection.getRemoteServices());
                if (remoteUIServices == null) {
                    return;
                }
                IRemoteUIFileManager fileMgr = remoteUIServices.getUIFileManager();
                if (fileMgr == null) {
                    return;
                }
                fileMgr.setConnection(fSelectedConnection);
                String selectedPath = fileMgr.browseFile(fGitLocationText.getShell(),
                        Messages.SyncGitPreferencePage_3 + fSelectedConnection.getName() + ")", null, //$NON-NLS-1$
                        IRemoteUIConstants.NONE);
                if (selectedPath != null) {
                    fGitLocationText.setText(selectedPath);
                }
            }
        });

        fGitValidateButton = new Button(composite, SWT.PUSH);
        gd = new GridData();
        gd.horizontalSpan = 2;
        gd.horizontalAlignment = SWT.RIGHT;
        fGitValidateButton.setLayoutData(gd);
        fGitValidateButton.setText(Messages.SyncGitPreferencePage_15);
        fGitValidateButton.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                validateGit();
                update();
            }
        });

        this.populateConnectionCombo(fConnectionCombo);
        this.handleConnectionSelected();

        return composite;
    }

    @Override
    public void init(IWorkbench arg0) {
        // nothing to do
    }

    public void update() {
        this.validatePath();
        this.validatePage();
        getContainer().updateMessage();
        getContainer().updateButtons();
        updateApplyButton();
    }

    @Override
    public void performApply() {
        this.storeConnectionSettings();
        this.saveAllConnectionSettings();
    }

    @Override
    protected void performDefaults() {
        fConnectionNameToGitPathMap.clear();
        this.loadConnectionSettings();
    }

    @Override
    public boolean performOk() {
        this.performApply();
        return true;
    }

    @Override
    public String getMessage() {
        if (pathWarningMessage != null) {
            return pathWarningMessage;
        } else {
            return gitWarningMessage;
        }
    }

    @Override
    public int getMessageType() {
        // Currently all messages are warning messages.
        return IMessageProvider.WARNING;
    }

    @Override
    public String getErrorMessage() {
        if (pathErrorMessage != null) {
            return pathErrorMessage;
        } else {
            return gitErrorMessage;
        }
    }

    private void handleConnectionSelected() {
        this.storeConnectionSettings();
        this.clearMessages();
        int sel = fConnectionCombo.getSelectionIndex();
        if (sel == -1) {
            fSelectedConnection = null;
        } else {
            fSelectedConnection = fComboIndexToRemoteConnectionMap.get(sel);
        }
        this.loadConnectionSettings();
    }

    // Save settings to internal map for currently selected connection
    private void storeConnectionSettings() {
        if (fSelectedConnection == null) {
            return;
        }
        if (fUseDefaultGitLocationCheckbox.getSelection()) {
            fConnectionNameToGitPathMap.put(fSelectedConnection, null);
        } else {
            // TODO: Verify that path is valid.
            fConnectionNameToGitPathMap.put(fSelectedConnection, fGitLocationText.getText());
        }
    }

    // Load settings to UI elements for currently selected connection
    private void loadConnectionSettings() {
        if (fSelectedConnection == null) {
            return;
        }

        String gitBinary = null;
        // Try retrieving settings from map
        if (fConnectionNameToGitPathMap.containsKey(fSelectedConnection)) {
            gitBinary = fConnectionNameToGitPathMap.get(fSelectedConnection);

            // If not there, load from preference store
        } else {
            IScopeContext context = InstanceScope.INSTANCE;
            Preferences prefSyncNode = context.getNode(instanceScopeSyncNode);
            if (prefSyncNode == null) {
                Activator.log(Messages.SyncGitPreferencePage_18);
            } else {
                try {
                    // Avoid creating node if it doesn't exist
                    if (prefSyncNode.nodeExists(GIT_LOCATION_NODE_NAME)) {
                        Preferences prefGitNode = prefSyncNode.node(GIT_LOCATION_NODE_NAME);
                        gitBinary = prefGitNode.get(fSelectedConnection.getName(), null);
                    }
                } catch (BackingStoreException e) {
                    Activator.log(Messages.SyncGitPreferencePage_19, e);
                }
            }
        }

        // Set UI elements
        if (gitBinary == null) {
            fUseDefaultGitLocationCheckbox.setSelection(true);
            this.handleCheckDefaultGitLocation();
            fGitLocationText.setText(""); //$NON-NLS-1$
        } else {
            fUseDefaultGitLocationCheckbox.setSelection(false);
            this.handleCheckDefaultGitLocation();
            fGitLocationText.setText(gitBinary);
        }
    }

    private void clearMessages() {
        pathWarningMessage = null;
        pathErrorMessage = null;
        gitWarningMessage = null;
        gitErrorMessage = null;
    }

    private void handleCheckDefaultGitLocation() {
        if (fUseDefaultGitLocationCheckbox.getSelection()) {
            fGitLocationText.setEnabled(false);
            fBrowseButton.setEnabled(false);
        } else {
            fGitLocationText.setEnabled(true);
            fBrowseButton.setEnabled(true);
        }
    }

    private void populateConnectionCombo(final Combo connectionCombo) {
        connectionCombo.removeAll();

        // TODO: Handle case where service provider is not found.
        IRemoteServices rs = this.getRemoteServicesProvider();
        List<IRemoteConnection> connections = rs.getConnectionManager().getConnections();

        fComboIndexToRemoteConnectionMap.clear();
        for (int i = 0; i < connections.size(); i++) {
            connectionCombo.add(connections.get(i).getName(), i);
            fComboIndexToRemoteConnectionMap.put(i, connections.get(i));
        }

        if (connections.size() > 0) {
            connectionCombo.select(0);
        }
    }

    // Return the remote service to use or null if it cannot be found.
    // Currently, we simply always return the JSch provider, which *should* always be available.
    IRemoteServices remoteServicesProvider = null;

    private IRemoteServices getRemoteServicesProvider() {
        if (remoteServicesProvider == null) {
            remoteServicesProvider = RemoteServices.getRemoteServices("org.eclipse.remote.JSch"); //$NON-NLS-1$
        }
        return remoteServicesProvider;
    }

    private IRemoteUIConnectionManager getUIConnectionManager() {
        IRemoteServices rs = this.getRemoteServicesProvider();
        IRemoteUIConnectionManager connectionManager = RemoteUIServices.getRemoteUIServices(rs)
                .getUIConnectionManager();
        return connectionManager;
    }

    // Basic path checks that do not require running remote commands
    private boolean validatePath() {
        pathErrorMessage = null;
        pathWarningMessage = null;
        if (fUseDefaultGitLocationCheckbox.getSelection()) {
            return true;
        }
        IPath gitPath = new Path(fGitLocationText.getText());
        if (gitPath.isEmpty()) {
            pathErrorMessage = Messages.SyncGitPreferencePage_4;
            return false;
        }
        if (!gitPath.isAbsolute()) {
            pathErrorMessage = Messages.SyncGitPreferencePage_5;
            return false;
        }
        return true;
    }

    // Deeper path validation that uses Git on the remote machine
    private boolean validateGit() {
        gitErrorMessage = null;
        gitWarningMessage = null;
        if (!this.validatePath()) {
            return false;
        }

        // If use default is selected, assume the user wants to retrieve and validate the system default Git.
        if (fUseDefaultGitLocationCheckbox.getSelection()) {
            gitErrorMessage = this.setSystemDefaultGit();
            if (gitErrorMessage != null) {
                return false;
            }
        }
        // Otherwise, assume the user wants to validate whatever is in the textbox.
        IPath gitPath;
        gitPath = new Path(fGitLocationText.getText());
        List<String> args = Arrays.asList(gitPath.toString(), "--version"); //$NON-NLS-1$
        String errorMessage = null;
        CommandResults cr = null;
        try {
            cr = this.runRemoteCommand(args, Messages.SyncGitPreferencePage_6);
        } catch (RemoteExecutionException e) {
            errorMessage = this.buildErrorMessage(null, Messages.SyncGitPreferencePage_7, e);
        }

        if (errorMessage != null) {
            MessageDialog.openError(null, Messages.SyncGitPreferencePage_8, errorMessage);
            gitErrorMessage = Messages.SyncGitPreferencePage_7;
            return false;
        }
        if (cr.getExitCode() == 126) {
            gitErrorMessage = Messages.SyncGitPreferencePage_9;
            return false;
        }
        if (cr.getExitCode() == 127) {
            gitErrorMessage = Messages.SyncGitPreferencePage_10;
            return false;
        }

        int version = parseGitVersionAsInt(cr.getStdout());
        String versionString = parseGitVersionAsString(cr.getStdout());
        if (cr.getExitCode() != 0 || version == 0) {
            gitWarningMessage = Messages.SyncGitPreferencePage_11;
            return true;
        }

        if (version < 10700) {
            gitWarningMessage = Messages.SyncGitPreferencePage_12 + versionString
                    + Messages.SyncGitPreferencePage_13;
        }

        // Prefer false positives to false negatives. Return true by default.
        return true;
    }

    // Retrieve system default Git and place in textbox
    // Return error message
    private String setSystemDefaultGit() {
        List<String> args = Arrays.asList("which", "git"); //$NON-NLS-1$ //$NON-NLS-2$
        String errorMessage = null;
        CommandResults cr = null;
        try {
            cr = this.runRemoteCommand(args, Messages.SyncGitPreferencePage_16);
            errorMessage = this.buildErrorMessage(cr, Messages.SyncGitPreferencePage_17, null);
        } catch (RemoteExecutionException e) {
            errorMessage = this.buildErrorMessage(null, Messages.SyncGitPreferencePage_17, e);
        }

        if (errorMessage == null) {
            fGitLocationText.setText(cr.getStdout().trim());
        } else {
            fGitLocationText.setText(""); //$NON-NLS-1$
        }
        return errorMessage;
    }

    // Decide if page is valid and alter UI elements accordingly.
    private void validatePage() {
        boolean isValid = (pathErrorMessage == null);
        if (isValid) {
            fConnectionCombo.setEnabled(true);
        } else {
            fConnectionCombo.setEnabled(false);
        }
    }

    // Wrapper for running commands - wraps exceptions and invoking of command runner inside container run command.
    private CommandResults remoteCommandResults;

    private CommandResults runRemoteCommand(final List<String> command, final String commandDesc)
            throws RemoteExecutionException {
        try {
            new ProgressMonitorDialog(composite.getShell()).run(true, true, new IRunnableWithProgress() {
                @Override
                public void run(IProgressMonitor monitor) throws InvocationTargetException {
                    RecursiveSubMonitor progress = RecursiveSubMonitor.convert(monitor, 100);
                    progress.subTask(commandDesc);
                    try {
                        remoteCommandResults = CommandRunner.executeRemoteCommand(fSelectedConnection, command,
                                null, progress.newChild(100));
                    } catch (RemoteSyncException e) {
                        throw new InvocationTargetException(e);
                    } catch (IOException e) {
                        throw new InvocationTargetException(e);
                    } catch (InterruptedException e) {
                        throw new InvocationTargetException(e);
                    } catch (RemoteConnectionException e) {
                        throw new InvocationTargetException(e);
                    } finally {
                        monitor.done();
                    }
                }
            });
        } catch (InvocationTargetException e) {
            throw new RemoteExecutionException(e.getCause());
        } catch (InterruptedException e) {
            throw new RemoteExecutionException(e);
        }
        return remoteCommandResults;
    }

    // Builds error message for command.
    // Either the command result or the exception should be null, but not both.
    // baseMessage cannot be null.
    // Returns error message or null if no error occurred (can only occur if cr is not null).
    private String buildErrorMessage(CommandResults cr, String baseMessage, RemoteExecutionException e) {
        // Command successful
        if (cr != null && cr.getExitCode() == 0) {
            return null;
        }

        // Command runs but unsuccessfully
        if (cr != null) {
            return baseMessage + ": " + cr.getStderr(); //$NON-NLS-1$
        }

        // Command did not run - exception thrown
        String errorMessage = baseMessage;
        if (e.getMessage() != null) {
            errorMessage += ": " + e.getMessage(); //$NON-NLS-1$
        }

        return errorMessage;
    }

    private void saveAllConnectionSettings() {
        if (fConnectionNameToGitPathMap.size() == 0) {
            return;
        }

        IScopeContext context = InstanceScope.INSTANCE;
        Preferences prefSyncNode = context.getNode(instanceScopeSyncNode);
        if (prefSyncNode == null) {
            Activator.log(Messages.SyncGitPreferencePage_20);
            return;
        }

        // Avoid creating node if not needed. Connections set to default (null) are not stored.
        Preferences prefGitNode = null;
        try {
            if (prefSyncNode.nodeExists(GIT_LOCATION_NODE_NAME)) {
                prefGitNode = prefSyncNode.node(GIT_LOCATION_NODE_NAME);
            }
        } catch (BackingStoreException e) {
            Activator.log(Messages.SyncGitPreferencePage_21, e);
        }

        for (Map.Entry<IRemoteConnection, String> entry : fConnectionNameToGitPathMap.entrySet()) {
            // Avoid creating node if not necessary. Connections set to default (null) are not stored.
            if (prefGitNode == null) {
                if (entry.getValue() == null) {
                    continue;
                } else {
                    prefGitNode = prefSyncNode.node(GIT_LOCATION_NODE_NAME);
                }
            }

            if (entry.getValue() == null) {
                prefGitNode.remove(entry.getKey().getName());
            } else {
                prefGitNode.put(entry.getKey().getName(), entry.getValue());
            }
        }

        try {
            prefSyncNode.flush();
        } catch (BackingStoreException e) {
            Activator.log(e);
        }
    }

    /**
     * Parse raw output of "git --version" and return an integer representation of the version, suitable for comparisons.
     * 
     * @param versionCommandOutput
     * @return version integer or 0 on failure to parse
     */
    public static int parseGitVersionAsInt(String versionCommandOutput) {
        Matcher m = Pattern.compile("git version ([0-9]+)\\.([0-9]+)\\.([0-9]+).*") //$NON-NLS-1$
                .matcher(versionCommandOutput.trim());
        if (m.matches()) {
            return Integer.parseInt(m.group(1)) * 10000 + Integer.parseInt(m.group(2)) * 100
                    + Integer.parseInt(m.group(3));
        } else {
            return 0;
        }
    }

    /**
     * Parse raw output of "git --version" and return the version string, suitable for displaying to users.
     * 
     * @param versionCommandOutput
     * @return version string or null on failure to parse
     */
    public static String parseGitVersionAsString(String versionCommandOutput) {
        Matcher m = Pattern.compile("git version ([0-9]+\\.[0-9]+\\.[0-9]+).*") //$NON-NLS-1$
                .matcher(versionCommandOutput.trim());
        if (m.matches()) {
            return m.group(1);
        } else {
            return null;
        }
    }
}