com.google.gct.idea.debugger.ui.CloudAttachDialog.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gct.idea.debugger.ui.CloudAttachDialog.java

Source

/*
 * Copyright (C) 2015 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.gct.idea.debugger.ui;

import com.google.api.services.debugger.Debugger;
import com.google.api.services.debugger.model.Debuggee;
import com.google.api.services.debugger.model.DebuggeeLabelsEntry;
import com.google.api.services.debugger.model.ListDebuggeesResponse;
import com.google.common.base.Strings;
import com.google.gct.idea.debugger.CloudDebugProcessState;
import com.google.gct.idea.debugger.CloudDebuggerClient;
import com.google.gct.idea.debugger.ProjectRepositoryState;
import com.google.gct.idea.debugger.ProjectRepositoryValidator;
import com.google.gct.idea.elysium.ProjectSelector;
import com.google.gct.idea.util.GctBundle;
import com.google.gct.login.CredentialedUser;
import com.google.gct.login.GoogleLogin;
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.vcs.changes.ChangeListManager;
import com.intellij.ui.DocumentAdapter;
import com.intellij.ui.JBColor;
import com.intellij.ui.components.JBCheckBox;
import com.intellij.ui.components.JBLabel;
import com.intellij.util.containers.HashMap;

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 javax.swing.*;
import javax.swing.border.Border;
import javax.swing.event.DocumentEvent;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.text.DateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.Map;

/**
 * CloudAttachDialog shows a dialog allowing the user to select a module and debug.
 */
public class CloudAttachDialog extends DialogWrapper {
    private static final Logger LOG = Logger.getInstance(CloudAttachDialog.class);

    private final Project myProject;
    private final ProjectDebuggeeBinding myWireup;
    private JComboBox myDebuggeeTarget;
    private ProjectSelector myElysiumProjectId;
    private JBLabel myInfoPanel;
    private String myOriginalBranchName;
    private JPanel myPanel;
    private CloudDebugProcessState myProcessResultState;
    private GitRepository mySourceRepository;
    private String myStashMessage = null;
    private ProjectRepositoryValidator.SyncResult mySyncResult;
    private JBCheckBox mySyncStashCheckbox;
    private JBLabel myWarningLabel;
    private JBLabel myWarningLabel2;

    public CloudAttachDialog(@NotNull Project project) {
        super(project, true);

        myProject = project;
        init();
        initValidation();
        setTitle(GctBundle.getString("clouddebug.attachtitle"));
        setOKButtonText(GctBundle.getString("clouddebug.attach"));
        mySyncStashCheckbox.setVisible(false);
        mySyncStashCheckbox.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (mySyncStashCheckbox.isVisible()) {
                    myWarningLabel.setVisible(!mySyncStashCheckbox.isSelected());
                    myWarningLabel2.setVisible(!mySyncStashCheckbox.isSelected());
                    myInfoPanel.setVisible(mySyncStashCheckbox.isSelected());
                    if (mySyncStashCheckbox.isSelected()) {
                        setOKButtonText(getIsContinued() ? GctBundle.getString("clouddebug.continuesession")
                                : GctBundle.getString("clouddebug.attach"));
                    } else {
                        setOKButtonText(getIsContinued() ? GctBundle.getString("clouddebug.continueanyway")
                                : GctBundle.getString("clouddebug.attach.anyway"));
                    }
                }
            }
        });

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

        myInfoPanel.setFont(
                new Font(myWarningLabel2.getFont().getName(), Font.PLAIN, myWarningLabel.getFont().getSize() - 1));
        Border paddingBorder = BorderFactory.createEmptyBorder(2, 0, 2, 0);
        myInfoPanel.setBorder(paddingBorder);

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

        myWireup = new ProjectDebuggeeBinding(myElysiumProjectId, myDebuggeeTarget);
        myDebuggeeTarget.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                buildResult();
                checkSyncStashState();
            }
        });
    }

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

    @Override
    protected void doOKAction() {
        if (getOKAction().isEnabled()) {
            if (mySyncStashCheckbox.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 (!GoogleLogin.getInstance().isLoggedIn()) {
            return new ValidationInfo(GctBundle.getString("clouddebug.nologin"));
        }

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

        if (myDebuggeeTarget.getSelectedItem() == null) {
            return new ValidationInfo(GctBundle.getString("clouddebug.nomodule"));
        }

        return null;
    }

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

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

    private void buildResult() {
        myProcessResultState = myWireup.buildResult(myProject);
        ProjectRepositoryState repositoryState = ProjectRepositoryState.fromProcessState(myProcessResultState);
        repositoryState.setStashMessage(myStashMessage);
        repositoryState.setSourceRepository(mySourceRepository);
        repositoryState.setOriginalBranchName(myOriginalBranchName);
    }

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

        if (mySyncResult.needsStash() && mySyncResult.needsSync()) {
            setOKButtonText(getIsContinued() ? "Continue Session" : GctBundle.getString("clouddebug.attach"));
            mySyncStashCheckbox.setVisible(true);
            assert mySyncResult.getTargetSyncSHA() != null;
            mySyncStashCheckbox
                    .setText("Stash local changes and Sync to " + mySyncResult.getTargetSyncSHA().substring(0, 7));
            mySyncStashCheckbox.setSelected(true);
            myWarningLabel.setVisible(false);
            myWarningLabel2.setVisible(false);
            myInfoPanel.setVisible(true);
        } else if (mySyncResult.needsStash()) {
            setOKButtonText(getIsContinued() ? "Continue Session" : GctBundle.getString("clouddebug.attach"));
            mySyncStashCheckbox.setVisible(true);
            mySyncStashCheckbox.setText(GctBundle.getString("clouddebug.stashbuttontext"));
            mySyncStashCheckbox.setSelected(true);
            myWarningLabel.setVisible(false);
            myWarningLabel2.setVisible(false);
            myInfoPanel.setVisible(true);
        } else if (mySyncResult.needsSync()) {
            setOKButtonText(getIsContinued() ? "Continue Session" : GctBundle.getString("clouddebug.attach"));
            mySyncStashCheckbox.setVisible(true);
            assert mySyncResult.getTargetSyncSHA() != null;
            mySyncStashCheckbox.setText("Sync to " + mySyncResult.getTargetSyncSHA().substring(0, 7));
            mySyncStashCheckbox.setSelected(true);
            myWarningLabel.setVisible(false);
            myWarningLabel2.setVisible(false);
            myInfoPanel.setVisible(true);
        } else if (!mySyncResult.isDeterminable()) {
            setOKButtonText(getIsContinued() ? "Continue Anyway" : GctBundle.getString("clouddebug.attach.anyway"));
            myWarningLabel.setVisible(true);
            myWarningLabel2.setVisible(true);
            myInfoPanel.setVisible(true);
            myWarningLabel2.setText("Could not verify that current source matches module.");
        } else {
            setOKButtonText(getIsContinued() ? "Continue Session" : GctBundle.getString("clouddebug.attach"));
            mySyncStashCheckbox.setVisible(false);
            myWarningLabel.setVisible(false);
            myWarningLabel2.setVisible(false);
            myInfoPanel.setVisible(true);
        }
    }

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

    private void populateFields() {
        myElysiumProjectId.setText("");
    }

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

    private boolean stash() {
        if (mySyncResult.getTargetRepository() == null) {
            LOG.error("unexpected null local repro in call to stash");
            return false;
        }

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

        final GitLineHandler handler = new GitLineHandler(myProject, mySourceRepository.getRoot(),
                GitCommand.STASH);
        handler.addParameters("save");
        handler.addParameters("--keep-index");
        String date = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(new Date());
        myStashMessage = "Cloud Debugger saved changes from branch " + myOriginalBranchName + " at " + date;
        handler.addParameters(myStashMessage);
        AccessToken token = DvcsUtil.workingTreeChangeStarted(myProject);
        try {
            GitHandlerUtil.doSynchronously(handler, GitBundle.getString("stashing.title"),
                    handler.printableCommandLine());
        } finally {
            DvcsUtil.workingTreeChangeFinished(myProject, 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();

        mySourceRepository = mySyncResult.getTargetRepository();

        if (mySyncResult.needsStash() || mySyncResult.needsSync()) {
            if (mySourceRepository.getCurrentBranch() != null) {
                myOriginalBranchName = mySourceRepository.getCurrentBranch().getName();
            } else {
                myOriginalBranchName = mySourceRepository.getCurrentRevision();
            }
        }

        if (mySyncResult.needsStash()) {
            if (!stash()) {
                return;
            }
        }

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

    /**
     * This binding between the project and debuggee is refactored out to make it reusable in the future.
     */
    private static class ProjectDebuggeeBinding {
        private static final Logger LOG = Logger.getInstance(ProjectDebuggeeBinding.class);
        private final JComboBox myDebugeeTarget;
        private final ProjectSelector myElysiumProjectId;
        private Debugger myCloudDebuggerClient = null;
        private CredentialedUser myCredentialedUser = null;
        private CloudDebugProcessState myInputState;

        public ProjectDebuggeeBinding(@NotNull ProjectSelector elysiumProjectId, @NotNull JComboBox debugeeTarget) {
            myElysiumProjectId = elysiumProjectId;
            myDebugeeTarget = debugeeTarget;

            myElysiumProjectId.getDocument().addDocumentListener(new DocumentAdapter() {
                @Override
                protected void textChanged(DocumentEvent e) {
                    refreshDebugTargetList();
                }
            });

            myElysiumProjectId.addModelListener(new TreeModelListener() {
                @Override
                public void treeNodesChanged(TreeModelEvent e) {
                }

                @Override
                public void treeNodesInserted(TreeModelEvent e) {
                }

                @Override
                public void treeNodesRemoved(TreeModelEvent e) {
                }

                @Override
                public void treeStructureChanged(TreeModelEvent e) {
                    refreshDebugTargetList();
                }
            });
        }

        @NotNull
        public CloudDebugProcessState buildResult(Project project) {
            Long number = myElysiumProjectId.getProjectNumber();
            String projectNumberString = number != null ? number.toString() : null;
            ProjectDebuggeeBinding.DebugTarget selectedItem = (ProjectDebuggeeBinding.DebugTarget) myDebugeeTarget
                    .getSelectedItem();
            String savedDebuggeeId = selectedItem != null ? selectedItem.getId() : null;
            String savedProjectDescription = myElysiumProjectId.getText();

            return new CloudDebugProcessState(myCredentialedUser != null ? myCredentialedUser.getEmail() : null,
                    savedDebuggeeId, savedProjectDescription, projectNumberString, project);
        }

        @Nullable
        public Debugger getCloudDebuggerClient() {
            CredentialedUser credentialedUser = myElysiumProjectId.getSelectedUser();
            if (myCredentialedUser == credentialedUser) {
                return myCloudDebuggerClient;
            }

            myCredentialedUser = credentialedUser;
            myCloudDebuggerClient = myCredentialedUser != null
                    ? CloudDebuggerClient.getCloudDebuggerClient(myCredentialedUser.getEmail())
                    : null;

            return myCloudDebuggerClient;
        }

        @Nullable
        public CloudDebugProcessState getInputState() {
            return myInputState;
        }

        public void setInputState(@Nullable CloudDebugProcessState inputState) {
            myInputState = inputState;
            if (myInputState != null) {
                myElysiumProjectId.setText(myInputState.getProjectName());
            }
        }

        /**
         * Refreshes the list of attachable debug targets based on the project selection.
         */
        @SuppressWarnings("unchecked")
        private void refreshDebugTargetList() {
            try {
                myDebugeeTarget.removeAllItems();
                DebugTarget targetSelection = null;
                if (myElysiumProjectId.getProjectNumber() != null && getCloudDebuggerClient() != null) {

                    ListDebuggeesResponse debuggees = getCloudDebuggerClient().debuggees().list()
                            .setProject(myElysiumProjectId.getProjectNumber().toString()).execute();

                    if (debuggees != null && debuggees.getDebuggees() != null) {
                        Map<String, DebugTarget> perModuleCache = new HashMap<String, DebugTarget>();

                        for (Debuggee debuggee : debuggees.getDebuggees()) {
                            DebugTarget item = new DebugTarget(debuggee, myElysiumProjectId.getText());
                            if (!Strings.isNullOrEmpty(item.getModule())
                                    && !Strings.isNullOrEmpty(item.getVersion())) {
                                //If we already have an existing item for that module+version, compare the minor
                                // versions and only use the latest minor version.
                                String key = String.format("%s:%s", item.getModule(), item.getVersion());
                                DebugTarget existing = perModuleCache.get(key);
                                if (existing != null && existing.getMinorVersion() > item.getMinorVersion()) {
                                    continue;
                                }
                                if (existing != null) {
                                    myDebugeeTarget.removeItem(existing);
                                }
                                perModuleCache.put(key, item);
                            }
                            if (myInputState != null && !Strings.isNullOrEmpty(myInputState.getDebuggeeId())) {
                                assert myInputState.getDebuggeeId() != null;
                                if (myInputState.getDebuggeeId().equals(item.getId())) {
                                    targetSelection = item;
                                }
                            }
                            myDebugeeTarget.addItem(item);
                        }
                    }
                    if (targetSelection != null) {
                        myDebugeeTarget.setSelectedItem(targetSelection);
                    }
                }
            } catch (IOException ex) {
                LOG.error("Error listing debuggees from Cloud Debugger API", ex);
            }
        }

        public static class DebugTarget {
            private static final String MODULE = "module";

            private final Debuggee myDebuggee;
            private String myDescription;
            private long myMinorVersion = 0;
            private String myModule;
            private String myVersion;

            public DebugTarget(@NotNull Debuggee debuggee, @NotNull String projectName) {
                myDebuggee = debuggee;
                if (myDebuggee.getLabels() != null) {
                    myDescription = "";
                    myModule = "";
                    myVersion = "";
                    String minorVersion = "";

                    //Get the module name, major version and minor version strings.
                    for (DebuggeeLabelsEntry entry : myDebuggee.getLabels()) {
                        if (entry.getKey().equalsIgnoreCase(MODULE)) {
                            myModule = entry.getValue();
                        } else if (entry.getKey().equalsIgnoreCase("minorversion")) {
                            minorVersion = entry.getValue();
                        } else if (entry.getKey().equalsIgnoreCase("version")) {
                            myVersion = entry.getValue();
                        } else {
                            //This is fallback logic where we dump the labels verbatim if they
                            //change from underneath us.
                            myDescription += String.format("%s:%s", entry.getKey(), entry.getValue());
                        }
                    }

                    //Build a description from the strings.
                    if (!Strings.isNullOrEmpty(myModule)) {
                        myDescription = GctBundle.getString("cloud.debug.version.with.module.format", myModule,
                                myVersion);
                    } else if (!Strings.isNullOrEmpty(myVersion)) {
                        myDescription = GctBundle.getString("cloud.debug.versionformat", myVersion);
                    }

                    //Record the minor version.  We only show the latest minor version.
                    try {
                        if (!Strings.isNullOrEmpty(minorVersion)) {
                            myMinorVersion = Long.parseLong(minorVersion);
                        }
                    } catch (NumberFormatException ex) {
                        LOG.warn("unable to parse minor version: " + minorVersion);
                    }
                }

                //Finally if nothing worked (maybe labels aren't enabled?), we fall
                //back to the old logic of using description with the project name stripped out.
                if (Strings.isNullOrEmpty(myDescription)) {
                    myDescription = myDebuggee.getDescription();
                    if (myDescription != null && !Strings.isNullOrEmpty(projectName)
                            && myDescription.startsWith(projectName + "-")) {
                        myDescription = myDescription.substring(projectName.length() + 1);
                    }
                }
            }

            public String getId() {
                return myDebuggee.getId();
            }

            @Override
            public String toString() {
                return myDescription;
            }

            public long getMinorVersion() {
                return myMinorVersion;
            }

            public String getModule() {
                return myModule;
            }

            public String getVersion() {
                return myVersion;
            }
        }
    }

}