Merge "Revert "Revert "Creates a simple deploy dialog using appcfg.""" into idea133
diff --git a/google-cloud-tools.iml b/google-cloud-tools.iml
index e07f852..9126f4b 100644
--- a/google-cloud-tools.iml
+++ b/google-cloud-tools.iml
@@ -53,6 +53,7 @@
         <SOURCES />
       </library>
     </orderEntry>
+    <orderEntry type="module" module-name="google-login" />
   </component>
 </module>
 
diff --git a/login/src/com/google/gct/login/OAuthScopeRegistry.java b/login/src/com/google/gct/login/OAuthScopeRegistry.java
index ca2fe2e..47b9686 100644
--- a/login/src/com/google/gct/login/OAuthScopeRegistry.java
+++ b/login/src/com/google/gct/login/OAuthScopeRegistry.java
@@ -32,6 +32,7 @@
   static {
     SortedSet<String> scopes = new TreeSet<String>();
     scopes.add("https://www.googleapis.com/auth/userinfo#email");
+    scopes.add("https://www.googleapis.com/auth/appengine.admin");
     sScopes = Collections.unmodifiableSortedSet(scopes);
   }
 
diff --git a/src/META-INF/plugin.xml b/src/META-INF/plugin.xml
index c34f079..c6bc850 100644
--- a/src/META-INF/plugin.xml
+++ b/src/META-INF/plugin.xml
@@ -18,6 +18,9 @@
       <implementation-class>com.google.gct.idea.appengine.synchronization.SampleSyncRegistration</implementation-class>
     </component>
     -->
+    <component>
+      <implementation-class>com.google.gct.idea.appengine.initialization.CloudPluginRegistration</implementation-class>
+    </component>
   </application-components>
 
   <project-components>
@@ -44,6 +47,9 @@
     <runConfigurationProducer implementation="com.google.gct.idea.appengine.run.AppEngineRunConfigurationProducer"/>
     -->
 
+    <!-- Dom for the App Engine config file -->
+    <dom.fileDescription implementation="com.google.gct.idea.appengine.dom.AppEngineWebFileDescription"/>
+
     <implicitUsageProvider implementation="com.google.gct.idea.appengine.validation.EndpointImplicitUsageProvider"/>
 
     <localInspection language="JAVA" shortName="ApiName" bundle="messages.EndpointBundle"  hasStaticDescription="true"
diff --git a/src/com/google/gct/idea/appengine/deploy/AppEngineUpdateAction.java b/src/com/google/gct/idea/appengine/deploy/AppEngineUpdateAction.java
new file mode 100644
index 0000000..b50389d
--- /dev/null
+++ b/src/com/google/gct/idea/appengine/deploy/AppEngineUpdateAction.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2014 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.appengine.deploy;
+
+import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.LangDataKeys;
+import com.intellij.openapi.module.Module;
+
+/**
+ * Handles the menu action to deploy to AppEngine.
+ */
+public class AppEngineUpdateAction extends AnAction {
+  @Override
+  public void actionPerformed(AnActionEvent e) {
+    final Module selectedModule = LangDataKeys.MODULE.getData(e.getDataContext());
+
+    AppEngineUpdateDialog.show(e.getProject(), selectedModule);
+  }
+}
diff --git a/src/com/google/gct/idea/appengine/deploy/AppEngineUpdateDialog.form b/src/com/google/gct/idea/appengine/deploy/AppEngineUpdateDialog.form
new file mode 100644
index 0000000..249d6a6
--- /dev/null
+++ b/src/com/google/gct/idea/appengine/deploy/AppEngineUpdateDialog.form
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="com.google.gct.idea.appengine.deploy.AppEngineUpdateDialog">
+  <grid id="27dc6" binding="myPanel" layout-manager="GridLayoutManager" row-count="4" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="2" vgap="2">
+    <margin top="0" left="0" bottom="0" right="0"/>
+    <constraints>
+      <xy x="20" y="20" width="306" height="113"/>
+    </constraints>
+    <properties>
+      <preferredSize width="275" height="135"/>
+    </properties>
+    <border type="none"/>
+    <children>
+      <component id="c6667" class="com.intellij.ui.components.JBLabel">
+        <constraints>
+          <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="4" fill="0" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties>
+          <text value="Module:"/>
+        </properties>
+      </component>
+      <vspacer id="8a93e">
+        <constraints>
+          <grid row="3" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
+        </constraints>
+      </vspacer>
+      <component id="9f4da" class="com.intellij.openapi.roots.ui.configuration.ModulesCombobox" binding="myModuleComboBox">
+        <constraints>
+          <grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="2" anchor="8" fill="1" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties/>
+      </component>
+      <component id="b23a6" class="com.intellij.ui.components.JBLabel">
+        <constraints>
+          <grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="4" fill="0" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties>
+          <text value="Project ID:"/>
+        </properties>
+      </component>
+      <component id="c6f41" class="javax.swing.JTextField" binding="myProjectId">
+        <constraints>
+          <grid row="1" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
+            <preferred-size width="150" height="-1"/>
+          </grid>
+        </constraints>
+        <properties>
+          <editable value="true"/>
+          <enabled value="true"/>
+          <horizontalAlignment value="2"/>
+          <text value=""/>
+        </properties>
+      </component>
+      <component id="1011a" class="com.intellij.ui.components.JBLabel">
+        <constraints>
+          <grid row="2" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="4" fill="0" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties>
+          <text value="Version:"/>
+        </properties>
+      </component>
+      <component id="331e4" class="javax.swing.JTextField" binding="myVersion">
+        <constraints>
+          <grid row="2" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
+            <preferred-size width="150" height="-1"/>
+          </grid>
+        </constraints>
+        <properties>
+          <editable value="true"/>
+          <enabled value="true"/>
+          <horizontalAlignment value="2"/>
+        </properties>
+      </component>
+    </children>
+  </grid>
+</form>
diff --git a/src/com/google/gct/idea/appengine/deploy/AppEngineUpdateDialog.java b/src/com/google/gct/idea/appengine/deploy/AppEngineUpdateDialog.java
new file mode 100644
index 0000000..bd8c2f1
--- /dev/null
+++ b/src/com/google/gct/idea/appengine/deploy/AppEngineUpdateDialog.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2014 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.appengine.deploy;
+
+import com.google.common.base.Strings;
+import com.google.gct.idea.appengine.dom.AppEngineWebApp;
+import com.google.gct.idea.appengine.dom.AppEngineWebFileDescription;
+import com.google.gct.idea.appengine.gradle.facet.AppEngineConfigurationProperties;
+import com.google.gct.idea.appengine.gradle.facet.AppEngineGradleFacet;
+import com.google.gct.login.GoogleLogin;
+import com.google.gct.login.IGoogleLoginCompletedCallback;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.module.ModuleManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.ui.configuration.ModulesCombobox;
+import com.intellij.openapi.ui.DialogWrapper;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.ui.ValidationInfo;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.packaging.artifacts.Artifact;
+import com.intellij.packaging.artifacts.ArtifactManager;
+import com.intellij.packaging.elements.PackagingElementResolvingContext;
+import com.intellij.packaging.impl.artifacts.ArtifactUtil;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.ui.SortedComboBoxModel;
+import com.intellij.util.xml.DomElement;
+import com.intellij.util.xml.DomFileElement;
+import com.intellij.util.xml.DomManager;
+import com.intellij.xml.util.XmlStringUtil;
+import org.eclipse.jgit.util.StringUtils;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * AppEngineUpdateDialog shows a dialog allowing the user to select a module and deploy.
+ */
+public class AppEngineUpdateDialog extends DialogWrapper {
+  private static final Logger LOG = Logger.getInstance(AppEngineUpdateDialog.class);
+
+  private ModulesCombobox myModuleComboBox;
+  private JTextField myProjectId;
+  private JTextField myVersion;
+  private JPanel myPanel;
+  private List<Module> myDeployableModules;
+  private Project myProject;
+  private Module myInitiallySelectedModule;
+
+  private AppEngineUpdateDialog(Project project, List<Module> deployableModules, Module selectedModule) {
+    super(project, true);
+    myDeployableModules = deployableModules;
+    myProject = project;
+    myInitiallySelectedModule = selectedModule;
+
+    init();
+    initValidation();
+    setTitle("Deploy to App Engine");
+    setOKButtonText("Deploy");
+
+    Window myWindow = getWindow();
+    if (myWindow != null) {
+      myWindow.setPreferredSize(new Dimension(285, 135));
+    }
+  }
+
+  /**
+   * Shows a dialog to deploy a module to AppEngine.  Will force a login if required
+   * If either the login fails or there are no valid modules to upload, it will return  after
+   * displaying an error.
+   *
+   * @param project The project whose modules will be uploaded.
+   * @param selectedModule The module selected by default in the deploy dialog.  Can be null.  If null or not a valid app engine module,
+   *                       no module will be selected by default.
+   */
+  static void show(final Project project, Module selectedModule) {
+
+    final java.util.List<Module> modules = new ArrayList<Module>();
+
+    // Filter the module list by whether we can actually deploy them to appengine.
+    for (Module module : ModuleManager.getInstance(project).getModules()) {
+      AppEngineGradleFacet facet = AppEngineGradleFacet.getAppEngineFacetByModule(module);
+      if (facet != null) {
+        modules.add(module);
+      }
+    }
+
+    // Tell the user what he has to do if he has none.
+    if (modules.size() == 0) {
+      //there are no modules to upload -- or we hit a bug due to gradle sync.
+      //TODO do we need to use the mainwindow as owner?
+      Messages.showErrorDialog(
+        XmlStringUtil.wrapInHtml(
+          "This project does not contain any App Engine modules. To add an App Engine module for your project, <br> open “File > New Module…” menu and choose one of App Engine modules.")
+        , "Error");
+      return;
+    }
+
+    if (selectedModule != null && !modules.contains(selectedModule)) {
+      selectedModule = null;
+    }
+
+    if (selectedModule == null && modules.size() == 1) {
+      selectedModule = modules.get(0);
+    }
+
+    // To invoke later, we need a final local.
+    final Module passedSelectedModule = selectedModule;
+
+    // Login on demand and queue up the dialog to show after a successful login.
+    //if login fails, it already shows an error.
+    if (!GoogleLogin.getInstance().isLoggedIn()) {
+      // log in on demand...
+      GoogleLogin.getInstance().logIn(null, new IGoogleLoginCompletedCallback() {
+        @Override
+        public void onLoginCompleted() {
+          if (GoogleLogin.getInstance().isLoggedIn()) {
+            EventQueue.invokeLater(new Runnable() {
+              @Override
+              public void run() {
+                // Success!, lets run the deploy now.
+                AppEngineUpdateDialog dialog = new AppEngineUpdateDialog(project, modules, passedSelectedModule);
+                dialog.show();
+              }
+            });
+          }
+        }
+      });
+    }
+    else {
+      AppEngineUpdateDialog dialog = new AppEngineUpdateDialog(project, modules, passedSelectedModule);
+      dialog.show();
+    }
+  }
+
+  @Nullable
+  @Override
+  protected JComponent createCenterPanel() {
+    @SuppressWarnings("unchecked")
+    final SortedComboBoxModel<Module> model = (SortedComboBoxModel<Module>)myModuleComboBox.getModel();
+    model.clear();
+    model.addAll(myDeployableModules);
+
+    if (myInitiallySelectedModule != null) {
+      // Auto select if there is only one item
+      model.setSelectedItem(myInitiallySelectedModule);
+      populateFields();
+    }
+
+    myModuleComboBox.addActionListener(new ActionListener() {
+      @Override
+      public void actionPerformed(ActionEvent e) {
+        populateFields();
+      }
+    });
+    return myPanel;
+  }
+
+  private void populateFields() {
+    myProjectId.setText("");
+    myVersion.setText("");
+
+    Module appEngineModule = myModuleComboBox.getSelectedModule();
+    if (appEngineModule != null) {
+      AppEngineGradleFacet facet = AppEngineGradleFacet.getAppEngineFacetByModule(appEngineModule);
+      if (facet == null) {
+        Messages.showErrorDialog(this.getPeer().getOwner(), "Could not acquire App Engine module information.", "Deploy");
+        return;
+      }
+
+      final AppEngineWebApp appEngineWebApp = facet.getAppEngineWebXml();
+      if (appEngineWebApp == null) {
+        Messages.showErrorDialog(this.getPeer().getOwner(), "Could not locate or parse the appengine-web.xml fle.", "Deploy");
+        return;
+      }
+
+      myProjectId.setText(appEngineWebApp.getApplication().getRawText());
+      myVersion.setText(appEngineWebApp.getVersion().getRawText());
+    }
+  }
+
+  @Override
+  protected void doOKAction() {
+    if (getOKAction().isEnabled()) {
+      GoogleLogin login = GoogleLogin.getInstance();
+      Module selectedModule = myModuleComboBox.getSelectedModule();
+      String sdk = "";
+      String war = "";
+      AppEngineGradleFacet facet = AppEngineGradleFacet.getAppEngineFacetByModule(selectedModule);
+      if (facet != null) {
+        AppEngineConfigurationProperties model = facet.getConfiguration().getState();
+        sdk = model.APPENGINE_SDKROOT;
+        war = model.WAR_DIR;
+      }
+
+      String client_secret = login.fetchOAuth2ClientSecret();
+      String client_id = login.fetchOAuth2ClientId();
+      String refresh_token = login.fetchOAuth2RefreshToken();
+
+      if (StringUtils.isEmptyOrNull(client_secret) ||
+          StringUtils.isEmptyOrNull(client_id) ||
+          StringUtils.isEmptyOrNull(refresh_token)) {
+        // The login is somehow invalid, bail -- this shouldn't happen.
+        LOG.error("StartUploading while logged in, but it doesn't have full credentials.");
+        Messages.showErrorDialog(this.getPeer().getOwner(), "Login credentials are not valid.", "Login");
+        return;
+      }
+
+      // These should not fail as they are a part of the dialog validation.
+      if (Strings.isNullOrEmpty(sdk) ||
+          Strings.isNullOrEmpty(war) ||
+          Strings.isNullOrEmpty(myProjectId.getText()) ||
+          selectedModule == null) {
+        Messages.showErrorDialog(this.getPeer().getOwner(), "Could not deploy due to missing information (sdk/war/projectid).", "Deploy");
+        LOG.error("StartUploading was called with bad module/sdk/war");
+        return;
+      }
+
+      close(OK_EXIT_CODE);  // We close before kicking off the update so it doesn't interfere with the output window coming to focus.
+
+      // Kick off the upload.  detailed status will be shown in an output window.
+      new AppEngineUpdater(myProject, selectedModule, sdk, war, myProjectId.getText(), myVersion.getText(),
+                           client_secret, client_id, refresh_token).startUploading();
+    }
+  }
+
+  @Override
+  protected ValidationInfo doValidate() {
+    // These should not normally occur..
+    if (!GoogleLogin.getInstance().isLoggedIn()) {
+      return new ValidationInfo("You must be logged in to perform this action.");
+    }
+
+    Module module = myModuleComboBox.getSelectedModule();
+    if (module == null) {
+      return new ValidationInfo("Select a module");
+    }
+
+    AppEngineGradleFacet facet = AppEngineGradleFacet.getAppEngineFacetByModule(module);
+    if (facet == null) {
+      return new ValidationInfo("Could not find App Engine gradle configuration on Module");
+    }
+
+    // We'll let AppCfg error if the project is wrong.  The user can see this in the console window.
+    // Note that version can be blank to indicate current version.
+    if (Strings.isNullOrEmpty(myProjectId.getText())) {
+      return new ValidationInfo("Please enter a Project ID.");
+    }
+
+    return null;
+  }
+
+}
diff --git a/src/com/google/gct/idea/appengine/deploy/AppEngineUpdater.java b/src/com/google/gct/idea/appengine/deploy/AppEngineUpdater.java
new file mode 100644
index 0000000..aa48dd4
--- /dev/null
+++ b/src/com/google/gct/idea/appengine/deploy/AppEngineUpdater.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2014 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.appengine.deploy;
+
+import com.android.tools.idea.gradle.invoker.GradleInvoker;
+import com.google.common.base.Strings;
+import com.google.gct.idea.appengine.sdk.AppEngineSdk;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.ExecutionManager;
+import com.intellij.execution.Executor;
+import com.intellij.execution.configurations.CommandLineBuilder;
+import com.intellij.execution.configurations.GeneralCommandLine;
+import com.intellij.execution.configurations.JavaParameters;
+import com.intellij.execution.configurations.ParametersList;
+import com.intellij.execution.executors.DefaultRunExecutor;
+import com.intellij.execution.filters.TextConsoleBuilderFactory;
+import com.intellij.execution.process.*;
+import com.intellij.execution.ui.ConsoleView;
+import com.intellij.execution.ui.RunContentDescriptor;
+import com.intellij.execution.ui.RunnerLayoutUi;
+import com.intellij.execution.ui.actions.CloseAction;
+import com.intellij.openapi.actionSystem.ActionManager;
+import com.intellij.openapi.actionSystem.ActionPlaces;
+import com.intellij.openapi.actionSystem.DefaultActionGroup;
+import com.intellij.openapi.actionSystem.IdeActions;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.progress.ProgressIndicator;
+import com.intellij.openapi.progress.ProgressManager;
+import com.intellij.openapi.progress.Task;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.util.Key;
+import com.intellij.openapi.util.KeyValue;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.util.net.HttpConfigurable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.awt.*;
+import java.util.List;
+
+/**
+ * Compiles and deploys a module to AppEngine using AppCfg.
+ *
+ * @author benwu
+ */
+class AppEngineUpdater {
+  private static final Logger LOG = Logger.getInstance("#com.google.gct.idea.appengine.deploy.AppEngineUpdater");
+  private final Project myProject;
+  private final Module myModule;
+  private final String myExplodedWarPath;
+  private final String mySdkPath;
+  private final String myClientSecret;
+  private final String myClientId;
+  private final String myRefreshToken;
+  private final String myVersion;
+  private final String myAppEngineProject;
+
+  AppEngineUpdater(Project project,
+                   Module module,
+                   String sdkPath,
+                   String explodedWarPath,
+                   String appEngineProject,
+                   String version,
+                   String clientSecret,
+                   String clientId,
+                   String refreshToken) {
+    myProject = project;
+    myModule = module;
+    mySdkPath = sdkPath;
+    myExplodedWarPath = explodedWarPath;
+    myClientSecret = clientSecret;
+    myClientId = clientId;
+    myRefreshToken = refreshToken;
+    myVersion = version;
+    myAppEngineProject = appEngineProject;
+  }
+
+  /**
+   * Starts the compile and upload async process.
+   */
+  void startUploading() {
+    FileDocumentManager.getInstance().saveAllDocuments();
+    ProgressManager.getInstance().run(new Task.Backgroundable(myModule.getProject(), "Deploying application", true, null) {
+      @Override
+      public void run(@NotNull ProgressIndicator indicator) {
+        compileAndUpload();
+      }
+    });
+  }
+
+  private void compileAndUpload() {
+    final Runnable startUploading = new Runnable() {
+      @Override
+      public void run() {
+        ApplicationManager.getApplication().invokeLater(new Runnable() {
+          @Override
+          public void run() {
+            startUploadingProcess();
+          }
+        });
+      }
+    };
+
+    GradleInvoker.getInstance(myProject).compileJava(new Module[]{myModule});
+    startUploading.run();
+  }
+
+  private void startUploadingProcess() {
+    final Process process;
+    final GeneralCommandLine commandLine;
+
+    try {
+      JavaParameters parameters = new JavaParameters();
+      parameters.configureByModule(myModule, JavaParameters.JDK_ONLY);
+      parameters.setMainClass("com.google.appengine.tools.admin.AppCfg");
+      AppEngineSdk mySdk = new AppEngineSdk(mySdkPath);
+      parameters.getClassPath().add(mySdk.getToolsApiJarFile().getAbsolutePath());
+
+      final List<KeyValue<String, String>> list = HttpConfigurable.getJvmPropertiesList(false, null);
+      if (!list.isEmpty()) {
+        final ParametersList parametersList = parameters.getVMParametersList();
+        for (KeyValue<String, String> value : list) {
+          parametersList.defineProperty(value.getKey(), value.getValue());
+        }
+      }
+
+      final ParametersList programParameters = parameters.getProgramParametersList();
+      programParameters.add("--application=" + myAppEngineProject);
+      if (!Strings.isNullOrEmpty(myVersion)) {
+        programParameters.add("--version=" + myVersion);
+      }
+      programParameters.add("--oauth2");
+      programParameters.add("--oauth2_client_secret=" + myClientSecret);
+      programParameters.add("--oauth2_client_id=" + myClientId);
+      programParameters.add("--oauth2_refresh_token=" + myRefreshToken);
+      programParameters.add("update");
+      programParameters.add(FileUtil.toSystemDependentName(myExplodedWarPath));
+
+      commandLine = CommandLineBuilder.createFromJavaParameters(parameters);
+
+      process = commandLine.createProcess();
+    }
+    catch (ExecutionException e) {
+      final String message = e.getMessage();
+      LOG.error("Cannot start uploading: " + message);
+
+      if (!EventQueue.isDispatchThread()) {
+        EventQueue.invokeLater(new Runnable() {
+          @Override
+          public void run() {
+            Messages.showErrorDialog("Cannot start uploading: " + message, "Error");
+          }
+        });
+      }
+      else {
+        Messages.showErrorDialog("Cannot start uploading: " + message, "Error");
+      }
+
+      return;
+    }
+
+    final ProcessHandler processHandler = new FilteredOSProcessHandler(process, commandLine.getCommandLineString(),
+                                                                       new String[]{myRefreshToken, myClientSecret, myClientId});
+    final Executor executor = DefaultRunExecutor.getRunExecutorInstance();
+    final ConsoleView console = TextConsoleBuilderFactory.getInstance().createBuilder(myModule.getProject()).getConsole();
+    final RunnerLayoutUi ui = RunnerLayoutUi.Factory.getInstance(myModule.getProject())
+      .create("Deploy", "Deploy to AppEngine", "Deploy Application", myModule.getProject());
+    final DefaultActionGroup group = new DefaultActionGroup();
+    ui.getOptions().setLeftToolbar(group, ActionPlaces.UNKNOWN);
+    ui.addContent(ui.createContent("upload", console.getComponent(), "Deploy Application", null, console.getPreferredFocusableComponent()));
+
+    console.attachToProcess(processHandler);
+    final RunContentDescriptor contentDescriptor =
+      new RunContentDescriptor(console, processHandler, ui.getComponent(), "Deploy to AppEngine");
+    group.add(ActionManager.getInstance().getAction(IdeActions.ACTION_STOP_PROGRAM));
+    group.add(new CloseAction(executor, contentDescriptor, myModule.getProject()));
+
+    ExecutionManager.getInstance(myModule.getProject()).getContentManager().showRunContent(executor, contentDescriptor);
+    processHandler.startNotify();
+  }
+
+  private class FilteredOSProcessHandler extends OSProcessHandler {
+    String[] tokensToFilter;
+
+    FilteredOSProcessHandler(@NotNull final Process process, @Nullable final String commandLine, String[] filteredTokens) {
+      super(process, commandLine);
+      tokensToFilter = filteredTokens;
+    }
+
+    @Override
+    public void notifyTextAvailable(final String text, final Key outputType) {
+      String newText = text;
+      if (tokensToFilter != null) {
+        for (String token : tokensToFilter) {
+          newText = newText.replace(token, "*****");
+        }
+      }
+      super.notifyTextAvailable(newText, outputType);
+    }
+  }
+}
diff --git a/src/com/google/gct/idea/appengine/dom/AppEngineWebApp.java b/src/com/google/gct/idea/appengine/dom/AppEngineWebApp.java
new file mode 100644
index 0000000..ce3ba7d
--- /dev/null
+++ b/src/com/google/gct/idea/appengine/dom/AppEngineWebApp.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2014 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.appengine.dom;
+
+import com.intellij.util.xml.DomElement;
+import com.intellij.util.xml.GenericDomValue;
+
+/**
+ * This is the Dom for the App Engine config file.
+ */
+public interface AppEngineWebApp extends DomElement {
+  GenericDomValue<String> getApplication();
+  GenericDomValue<String> getVersion();
+}
diff --git a/src/com/google/gct/idea/appengine/dom/AppEngineWebFileDescription.java b/src/com/google/gct/idea/appengine/dom/AppEngineWebFileDescription.java
new file mode 100644
index 0000000..9efe86f
--- /dev/null
+++ b/src/com/google/gct/idea/appengine/dom/AppEngineWebFileDescription.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2014 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.appengine.dom;
+
+import com.intellij.openapi.module.Module;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.util.xml.DomFileDescription;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * This is the file description for the App Engine xml config file
+ */
+public class AppEngineWebFileDescription extends DomFileDescription<AppEngineWebApp> {
+  @NonNls public static final String APP_ENGINE_WEB_XML_NAME = "appengine-web.xml";
+
+  public AppEngineWebFileDescription() {
+    super(AppEngineWebApp.class, "appengine-web-app");
+  }
+
+  @Override
+  public boolean isMyFile(@NotNull XmlFile file, @Nullable Module module) {
+    return file.getName().equals(APP_ENGINE_WEB_XML_NAME);
+  }
+}
diff --git a/src/com/google/gct/idea/appengine/gradle/facet/AppEngineGradleFacet.java b/src/com/google/gct/idea/appengine/gradle/facet/AppEngineGradleFacet.java
index 48df14f..198f472 100644
--- a/src/com/google/gct/idea/appengine/gradle/facet/AppEngineGradleFacet.java
+++ b/src/com/google/gct/idea/appengine/gradle/facet/AppEngineGradleFacet.java
@@ -15,6 +15,8 @@
  */
 package com.google.gct.idea.appengine.gradle.facet;
 
+import com.google.common.base.Strings;
+import com.google.gct.idea.appengine.dom.AppEngineWebApp;
 import com.intellij.facet.Facet;
 import com.intellij.facet.FacetManager;
 import com.intellij.facet.FacetType;
@@ -22,10 +24,18 @@
 import com.intellij.facet.FacetTypeRegistry;
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.module.Module;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.util.xml.DomManager;
 import org.jetbrains.annotations.NonNls;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
+import java.io.File;
+
 /**
  * App Engine Gradle facet for App Engine Modules with a Gradle build file
  */
@@ -50,6 +60,30 @@
     super(facetType, module, name, configuration, null);
   }
 
+  /**
+   * Returns an object holding information from the appengine-web.xml file.
+   */
+  public AppEngineWebApp getAppEngineWebXml() {
+    AppEngineConfigurationProperties model = getConfiguration().getState();
+    if (model == null || Strings.isNullOrEmpty(model.WEB_APP_DIR)) {
+      return null;
+    }
+
+    String path = model.WEB_APP_DIR + "/WEB-INF/appengine-web.xml";
+    VirtualFile appEngineFile = LocalFileSystem.getInstance().findFileByPath(path.replace(File.separatorChar, '/'));
+    if (appEngineFile == null) {
+      return null;
+    }
+
+    PsiFile psiFile = PsiManager.getInstance(getModule().getProject()).findFile(appEngineFile);
+    if (psiFile == null || !(psiFile instanceof XmlFile)) {
+      return null;
+    }
+
+    final DomManager domManager = DomManager.getDomManager(getModule().getProject());
+    return domManager.getFileElement((XmlFile)psiFile, AppEngineWebApp.class).getRootElement();
+  }
+
   public static FacetType<AppEngineGradleFacet, AppEngineGradleFacetConfiguration> getFacetType() {
     return FacetTypeRegistry.getInstance().findFacetType(ID);
   }
diff --git a/src/com/google/gct/idea/appengine/initialization/CloudPluginRegistration.java b/src/com/google/gct/idea/appengine/initialization/CloudPluginRegistration.java
new file mode 100644
index 0000000..1f103af
--- /dev/null
+++ b/src/com/google/gct/idea/appengine/initialization/CloudPluginRegistration.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2014 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.appengine.initialization;
+
+import com.google.gct.idea.appengine.deploy.AppEngineUpdateAction;
+import com.intellij.openapi.actionSystem.ActionManager;
+import com.intellij.openapi.actionSystem.Anchor;
+import com.intellij.openapi.actionSystem.Constraints;
+import com.intellij.openapi.actionSystem.DefaultActionGroup;
+import com.intellij.openapi.components.ApplicationComponent;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Initializes the menus for deploy.
+ */
+public class CloudPluginRegistration implements ApplicationComponent {
+
+  // We are reusing the flag for login.
+  private final static String SHOW_DEPLOY = "show.google.login.button";
+
+  public CloudPluginRegistration() {
+  }
+
+  @Override
+  public void initComponent() {
+    if (Boolean.getBoolean(SHOW_DEPLOY)) {
+      ActionManager am = ActionManager.getInstance();
+
+      AppEngineUpdateAction action = new AppEngineUpdateAction();
+      action.getTemplatePresentation().setText("Deploy Module to App Engine...");
+
+      am.registerAction("GoogleCloudTools.AppEngineUpdate", action);
+      DefaultActionGroup buildMenu = (DefaultActionGroup)am.getAction("BuildMenu");
+
+      DefaultActionGroup appEngineUpdateGroup = new DefaultActionGroup();
+      appEngineUpdateGroup.addSeparator();
+      appEngineUpdateGroup.add(action);
+      buildMenu.add(appEngineUpdateGroup, new Constraints(Anchor.AFTER, "Compile"));
+    }
+  }
+
+  @Override
+  public void disposeComponent() {
+  }
+
+  @NotNull
+  @Override
+  public String getComponentName() {
+    return "CloudPluginRegistration";
+  }
+}