com.google.cloud.tools.intellij.debugger.ui.CloudAttachDialog.java Source code

Java tutorial

Introduction

Here is the source code for com.google.cloud.tools.intellij.debugger.ui.CloudAttachDialog.java

Source

/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.cloud.tools.intellij.debugger.ui;

import com.google.cloud.tools.intellij.debugger.CloudDebugProcessState;
import com.google.cloud.tools.intellij.debugger.ProjectRepositoryState;
import com.google.cloud.tools.intellij.debugger.ProjectRepositoryValidator;
import com.google.cloud.tools.intellij.debugger.SyncResult;
import com.google.cloud.tools.intellij.login.Services;
import com.google.cloud.tools.intellij.resources.ProjectSelector;
import com.google.cloud.tools.intellij.stats.UsageTrackerProvider;
import com.google.cloud.tools.intellij.util.GctBundle;
import com.google.cloud.tools.intellij.util.GctTracking;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;

import com.intellij.dvcs.DvcsUtil;
import com.intellij.openapi.application.AccessToken;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.ValidationInfo;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.changes.ChangeListManager;
import com.intellij.ui.JBColor;
import com.intellij.ui.components.JBCheckBox;
import com.intellij.ui.components.JBLabel;

import git4idea.actions.BasicAction;
import git4idea.branch.GitBrancher;
import git4idea.commands.GitCommand;
import git4idea.commands.GitHandlerUtil;
import git4idea.commands.GitLineHandler;
import git4idea.i18n.GitBundle;
import git4idea.repo.GitRepository;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.awt.Dimension;
import java.awt.Font;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.text.DateFormat;
import java.util.Collections;
import java.util.Date;

import javax.swing.BorderFactory;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.border.Border;

/**
 * CloudAttachDialog shows a dialog allowing the user to select a target (module & version) and
 * debug.
 */
public class CloudAttachDialog extends DialogWrapper {

    private static final Logger LOG = Logger.getInstance(CloudAttachDialog.class);

    private final Project project;
    private final ProjectDebuggeeBinding wireup;
    private ProjectRepositoryValidator projectRepositoryValidator;
    private JComboBox targetSelector; // Module & version combo box
    private ProjectSelector elysiumProjectSelector; // Project combo box
    private JBLabel infoMessage;
    private String originalBranchName;
    private JPanel panel;
    private CloudDebugProcessState processResultState;
    private GitRepository sourceRepository;
    private String stashMessage = null;
    private SyncResult syncResult;
    private JBCheckBox syncStashCheckbox;
    private JBLabel warningHeader;
    private JBLabel warningMessage;

    /**
     * Initializes the cloud debugger dialog.
     */
    public CloudAttachDialog(@NotNull Project project, @VisibleForTesting ProjectDebuggeeBinding wireup) {
        super(project, true);

        this.project = project;
        init();
        initValidation();
        setTitle(GctBundle.getString("clouddebug.attachtitle"));
        setOKButtonText(GctBundle.getString("clouddebug.attach"));
        infoMessage.setVisible(true);
        syncStashCheckbox.setVisible(false);
        syncStashCheckbox.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent event) {
                if (syncStashCheckbox.isVisible()) {
                    warningHeader.setVisible(!syncStashCheckbox.isSelected());
                    warningMessage.setVisible(!syncStashCheckbox.isSelected());
                    // Show force attach text if the user chooses not to sync/stash
                    setOkText(!syncStashCheckbox.isSelected());
                }
            }
        });

        warningHeader.setVisible(false);
        warningHeader.setFont(
                new Font(warningHeader.getFont().getName(), Font.BOLD, warningHeader.getFont().getSize() - 1));
        warningHeader.setForeground(JBColor.RED);
        warningMessage.setVisible(false);
        warningMessage.setFont(
                new Font(warningMessage.getFont().getName(), Font.PLAIN, warningHeader.getFont().getSize() - 1));
        warningMessage.setText(GctBundle.getString("clouddebug.sourcedoesnotmatch"));

        infoMessage.setFont(
                new Font(warningMessage.getFont().getName(), Font.PLAIN, warningHeader.getFont().getSize() - 1));
        Border paddingBorder = BorderFactory.createEmptyBorder(2, 0, 2, 0);
        infoMessage.setBorder(paddingBorder);

        Window window = getWindow();
        if (window != null) {
            window.setPreferredSize(new Dimension(355, 175));
        }
        BasicAction.saveAll();

        this.wireup = wireup == null
                ? new ProjectDebuggeeBinding(elysiumProjectSelector, targetSelector, getOKAction())
                : wireup;
        targetSelector.setEnabled(false);
        targetSelector.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent event) {
                if (targetSelector.isEnabled()) {
                    buildResult();
                    checkSyncStashState();
                } else {
                    warningHeader.setVisible(false);
                    warningMessage.setVisible(false);
                }
            }
        });

        setOKActionEnabled(isContinued() || doValidate() == null);
    }

    @Nullable
    @Override
    protected JComponent createCenterPanel() {
        return panel;
    }

    @Override
    protected void doOKAction() {
        if (getOKAction().isEnabled()) {
            UsageTrackerProvider.getInstance().trackEvent(GctTracking.CLOUD_DEBUGGER_START_SESSION).ping();
            // TODO : add source context tracking info
            if (syncStashCheckbox.isSelected()) {
                syncOrStash();
            } else {
                buildResult();
                close(OK_EXIT_CODE); // We close before kicking off the update so it doesn't interfere with
                // the output window coming to focus.
            }
        }
    }

    @Override
    protected ValidationInfo doValidate() {
        // These should not normally occur.
        if (!Services.getLoginService().isLoggedIn()) {
            return new ValidationInfo(GctBundle.getString("clouddebug.nologin"));
        }

        if (Strings.isNullOrEmpty(elysiumProjectSelector.getText())) {
            return new ValidationInfo(GctBundle.getString("clouddebug.noprojectid"), elysiumProjectSelector);
        }

        // validation should run only after the query for debug targets has results
        // assumption: either an ErrorHolder or one or more DebugTargets are added to the selector when
        //             the result is available
        if (!targetSelector.isEnabled()) {
            if (targetSelector.getItemCount() > 0) {
                if (targetSelector.getSelectedItem() instanceof ErrorHolder) {
                    return new ValidationInfo(((ErrorHolder) targetSelector.getSelectedItem()).getErrorMessage(),
                            elysiumProjectSelector);
                } else {
                    return new ValidationInfo(GctBundle.getString("clouddebug.nomodulesfound"),
                            elysiumProjectSelector);
                }
            } else if (wireup.isCdbQueried()) {
                // We went to CDB and detected no debuggees.
                return new ValidationInfo(GctBundle.getString("clouddebug.debug.targets.accessdenied"),
                        elysiumProjectSelector);
            }
        }

        // validation should run only after the query for debug targets has results
        // assumption: either an ErrorHolder or one or more DebugTargets are added to the selector when
        //             the result is available
        if (targetSelector.getSelectedItem() == null && targetSelector.getItemCount() > 0) {
            return new ValidationInfo(GctBundle.getString("clouddebug.nomodulesfound"), targetSelector);
        }

        return null;
    }

    @Nullable
    public CloudDebugProcessState getResultState() {
        return processResultState;
    }

    public void setInputState(@Nullable CloudDebugProcessState inputState) {
        wireup.setInputState(inputState);
    }

    @VisibleForTesting
    ProjectSelector getElysiumProjectSelector() {
        return elysiumProjectSelector;
    }

    @VisibleForTesting
    JComboBox getTargetSelector() {
        return targetSelector;
    }

    @VisibleForTesting
    JLabel getWarningHeader() {
        return warningHeader;
    }

    @VisibleForTesting
    JLabel getWarningMessage() {
        return warningMessage;
    }

    @VisibleForTesting
    JBCheckBox getSyncStashCheckbox() {
        return syncStashCheckbox;
    }

    @VisibleForTesting
    void setProjectRepositoryValidator(ProjectRepositoryValidator projectRepositoryValidator) {
        this.projectRepositoryValidator = projectRepositoryValidator;
    }

    private void buildResult() {
        processResultState = wireup.buildResult(project);
        ProjectRepositoryState repositoryState = ProjectRepositoryState.fromProcessState(processResultState);
        repositoryState.setStashMessage(stashMessage);
        repositoryState.setSourceRepository(sourceRepository);
        repositoryState.setOriginalBranchName(originalBranchName);
    }

    /**
     * Checks whether a stash or sync is needed based on the chosen target and local state.
     */
    private void checkSyncStashState() {
        if (processResultState == null) {
            LOG.error("unexpected result state during a check sync stash state");
            return;
        }

        syncResult = projectRepositoryValidator == null
                ? new ProjectRepositoryValidator(processResultState).checkSyncStashState()
                : projectRepositoryValidator.checkSyncStashState();

        // reset state
        syncStashCheckbox.setVisible(false);
        syncStashCheckbox.setSelected(false);
        warningHeader.setVisible(false);
        warningMessage.setVisible(false);
        checkBackgroundSessions();

        if (syncResult.needsStash() && syncResult.needsSync()) {
            setOkText(false);
            syncStashCheckbox.setVisible(true);
            assert syncResult.getTargetSyncSha() != null;
            syncStashCheckbox.setText(GctBundle.getString("clouddebug.stash.local.changes.and.sync",
                    syncResult.getTargetSyncSha().substring(0, 7)));
            syncStashCheckbox.setSelected(true);
        } else if (syncResult.needsStash()) {
            setOkText(false);
            syncStashCheckbox.setVisible(true);
            syncStashCheckbox.setText(GctBundle.getString("clouddebug.stashbuttontext"));
            syncStashCheckbox.setSelected(true);
        } else if (syncResult.needsSync() && syncResult.getTargetSyncSha() == null) {
            setOkText(true);
            warningHeader.setVisible(true);
            warningMessage.setVisible(true);
            warningMessage.setText(GctBundle.getString("clouddebug.no.matching.sha"));
        } else if (syncResult.needsSync()) {
            setOkText(false);
            syncStashCheckbox.setVisible(true);
            assert syncResult.getTargetSyncSha() != null;
            syncStashCheckbox.setText("Sync to " + syncResult.getTargetSyncSha().substring(0, 7));
            syncStashCheckbox.setSelected(true);
        } else if (!syncResult.hasRemoteRepository()) {
            setOkText(true);
            warningHeader.setVisible(true);
            warningMessage.setVisible(true);
            if (syncResult.getRepositoryType() != null) {
                warningMessage.setText(GctBundle.getString("clouddebug.repositories.are.not.supported",
                        syncResult.getRepositoryType()));
            } else {
                warningMessage.setText(GctBundle.getString("clouddebug.no.remote.repository"));
            }
        } else {
            setOkText(false);
        }
    }

    private void setOkText(boolean showForcedWording) {
        if (showForcedWording) {
            setOKButtonText(
                    isContinued() && targetMatchesCurrentState() ? GctBundle.getString("clouddebug.continueanyway")
                            : GctBundle.getString("clouddebug.attach.anyway"));
        } else {
            setOKButtonText(
                    isContinued() && targetMatchesCurrentState() ? GctBundle.getString("clouddebug.continuesession")
                            : GctBundle.getString("clouddebug.attach"));
        }
    }

    private void checkBackgroundSessions() {
        boolean hasUnselectedBackgroundSessions = isContinued() && !targetMatchesCurrentState();
        if (hasUnselectedBackgroundSessions) {
            warningHeader.setVisible(true);
            warningMessage.setVisible(true);
            warningMessage.setText(GctBundle.getString("clouddebug.terminate.background"));
        }
    }

    private boolean isContinued() {
        CloudDebugProcessState state = wireup.getInputState();
        return state != null && state.getCurrentServerBreakpointList().size() > 0;
    }

    private boolean targetMatchesCurrentState() {
        CloudDebugProcessState state = wireup.getInputState();

        return state != null && targetSelector != null && targetSelector.getSelectedItem() != null && StringUtil
                .equals(state.getDebuggeeId(), ((DebugTarget) targetSelector.getSelectedItem()).getId());
    }

    private void refreshAndClose() {
        buildResult();
        close(OK_EXIT_CODE);
    }

    private boolean stash() {
        if (!syncResult.hasLocalRepository()) {
            LOG.error("unexpected null local repro in call to stash");
            return false;
        }

        final ChangeListManager changeListManager = ChangeListManager.getInstance(project);
        if (changeListManager.isFreezedWithNotification("Can not stash changes now")) {
            return false;
        }

        final GitLineHandler handler = new GitLineHandler(project, sourceRepository.getRoot(), GitCommand.STASH);
        handler.addParameters("save");
        handler.addParameters("--keep-index");
        String date = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(new Date());
        stashMessage = "Cloud Debugger saved changes from branch " + originalBranchName + " at " + date;
        handler.addParameters(stashMessage);
        AccessToken token = DvcsUtil.workingTreeChangeStarted(project);
        try {
            GitHandlerUtil.doSynchronously(handler, GitBundle.getString("stashing.title"),
                    handler.printableCommandLine());
        } finally {
            DvcsUtil.workingTreeChangeFinished(project, token);
        }
        return true;
    }

    /**
     * Performs the actual sync/stash needed before attaching.
     */
    private void syncOrStash() {
        // When the user edits a document in intelliJ, there are spurious updates to the timestamp of
        // the document for an unspecified amount of time (even though there are no real edits).
        // So, we save-all right before we stash to (help) ensure we don't get a conflict dialog.
        // The conflict dialog happens when the timestamps of the document and file are mismatched.
        // So when we do the git operations, we want the document and file timestamps to match exactly.
        BasicAction.saveAll();

        sourceRepository = syncResult.getLocalRepository();

        if (syncResult.needsStash() || syncResult.needsSync()) {
            if (sourceRepository.getCurrentBranch() != null) {
                originalBranchName = sourceRepository.getCurrentBranch().getName();
            } else {
                originalBranchName = sourceRepository.getCurrentRevision();
            }
        }

        if (syncResult.needsStash() && !stash()) {
            return;
        }

        if (!Strings.isNullOrEmpty(syncResult.getTargetSyncSha())) {
            //try to check out that revision.
            final GitBrancher brancher = ServiceManager.getService(project, GitBrancher.class);
            if (sourceRepository == null) {
                LOG.error("unexpected null source repo with a target SHA.");
                return;
            }
            assert syncResult.getTargetSyncSha() != null;
            brancher.checkout(syncResult.getTargetSyncSha(), false, Collections.singletonList(sourceRepository),
                    new Runnable() {
                        @Override
                        public void run() {
                            refreshAndClose();
                        }
                    });
        } else {
            refreshAndClose();
        }
    }
}