Merge "Remove the "install client libraries" action." into idea133
diff --git a/google-cloud-tools.iml b/google-cloud-tools.iml
index 2613b4a..6d1ab7c 100644
--- a/google-cloud-tools.iml
+++ b/google-cloud-tools.iml
@@ -26,7 +26,6 @@
<orderEntry type="module" module-name="gradle" />
<orderEntry type="module" module-name="jetgroovy" />
<orderEntry type="library" scope="TEST" name="mockito" level="project" />
- <orderEntry type="module" module-name="login" />
<orderEntry type="module-library">
<library>
<CLASSES>
@@ -54,6 +53,8 @@
<SOURCES />
</library>
</orderEntry>
+ <orderEntry type="module" module-name="google-login" />
+ <orderEntry type="module" module-name="bootstrap" />
</component>
</module>
diff --git a/lib-working/google-gct-login-context-pg.jar b/lib-working/google-gct-login-context-pg.jar
deleted file mode 100644
index a0e8f00..0000000
--- a/lib-working/google-gct-login-context-pg.jar
+++ /dev/null
Binary files differ
diff --git a/lib-working/javax.servlet-api-3.1.0.jar b/lib-working/javax.servlet-api-3.1.0.jar
deleted file mode 100644
index 6b14c3d..0000000
--- a/lib-working/javax.servlet-api-3.1.0.jar
+++ /dev/null
Binary files differ
diff --git a/login/login.iml b/login/google-login.iml
similarity index 61%
rename from login/login.iml
rename to login/google-login.iml
index 897f97a..da52fcf 100644
--- a/login/login.iml
+++ b/login/google-login.iml
@@ -5,15 +5,19 @@
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/testSrc" isTestSource="true" />
+ <sourceFolder url="file://$MODULE_DIR$/resources" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module" module-name="openapi" />
+ <orderEntry type="module" module-name="platform-impl" />
<orderEntry type="library" name="Guava" level="project" />
- <orderEntry type="module-library" scope="TEST">
+ <orderEntry type="library" name="google-api-java-client" level="project" />
+ <orderEntry type="library" name="jcip" level="project" />
+ <orderEntry type="module-library">
<library>
<CLASSES>
- <root url="jar://$APPLICATION_HOME_DIR$/lib/junit.jar!/" />
+ <root url="jar://$MODULE_DIR$/lib/google-api-services-oauth2-v2-rev70-1.18.0-rc.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
@@ -22,7 +26,7 @@
<orderEntry type="module-library">
<library>
<CLASSES>
- <root url="jar://$APPLICATION_HOME_DIR$/lib/jcip-annotations.jar!/" />
+ <root url="jar://$MODULE_DIR$/lib/google-gct-login-context-pg.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
@@ -31,19 +35,27 @@
<orderEntry type="module-library">
<library>
<CLASSES>
- <root url="jar://$MODULE_DIR$/../lib-working/google.login.jar!/" />
+ <root url="jar://$MODULE_DIR$/lib/google-http-client-jackson-1.18.0-rc.jar!/" />
+ </CLASSES>
+ <JAVADOC />
+ <SOURCES />
+ </library>
+ </orderEntry>
+ <orderEntry type="module-library">
+ <library>
+ <CLASSES>
+ <root url="jar://$MODULE_DIR$/lib/google.gdt.eclipse.login.common.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES>
- <root url="jar://$MODULE_DIR$/../lib-working/google.login.jar!/" />
+ <root url="jar://$MODULE_DIR$/lib/google.gdt.eclipse.login.common.jar!/" />
</SOURCES>
</library>
</orderEntry>
- <orderEntry type="library" name="google-api-java-client" level="project" />
<orderEntry type="module-library">
<library>
<CLASSES>
- <root url="jar://$MODULE_DIR$/../lib-working/google-http-client-jackson-1.18.0-rc.jar!/" />
+ <root url="jar://$MODULE_DIR$/lib/jackson-core-asl-1.9.11.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
@@ -52,40 +64,13 @@
<orderEntry type="module-library">
<library>
<CLASSES>
- <root url="jar://$MODULE_DIR$/../lib-working/google-api-services-oauth2-v2-rev70-1.18.0-rc.jar!/" />
+ <root url="jar://$MODULE_DIR$/lib/javax.servlet-api-3.0.1.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
- <orderEntry type="module-library">
- <library>
- <CLASSES>
- <root url="jar://$MODULE_DIR$/../lib-working/jackson-core-asl-1.9.11.jar!/" />
- </CLASSES>
- <JAVADOC />
- <SOURCES />
- </library>
- </orderEntry>
- <orderEntry type="module-library">
- <library>
- <CLASSES>
- <root url="jar://$MODULE_DIR$/../lib-working/javax.servlet-api-3.1.0.jar!/" />
- </CLASSES>
- <JAVADOC />
- <SOURCES />
- </library>
- </orderEntry>
- <orderEntry type="module" module-name="platform-impl" />
- <orderEntry type="module-library">
- <library>
- <CLASSES>
- <root url="jar://$MODULE_DIR$/../lib-working/google-gct-login-context-pg.jar!/" />
- </CLASSES>
- <JAVADOC />
- <SOURCES />
- </library>
- </orderEntry>
+ <orderEntry type="library" scope="TEST" name="JUnit3" level="project" />
</component>
</module>
diff --git a/lib-working/google-api-services-oauth2-v2-rev70-1.18.0-rc.jar b/login/lib/google-api-services-oauth2-v2-rev70-1.18.0-rc.jar
similarity index 100%
rename from lib-working/google-api-services-oauth2-v2-rev70-1.18.0-rc.jar
rename to login/lib/google-api-services-oauth2-v2-rev70-1.18.0-rc.jar
Binary files differ
diff --git a/login/lib/google-gct-login-context-pg.jar b/login/lib/google-gct-login-context-pg.jar
new file mode 100644
index 0000000..0a7b382
--- /dev/null
+++ b/login/lib/google-gct-login-context-pg.jar
Binary files differ
diff --git a/lib-working/google-http-client-jackson-1.18.0-rc.jar b/login/lib/google-http-client-jackson-1.18.0-rc.jar
similarity index 100%
rename from lib-working/google-http-client-jackson-1.18.0-rc.jar
rename to login/lib/google-http-client-jackson-1.18.0-rc.jar
Binary files differ
diff --git a/lib-working/google.login.jar b/login/lib/google.gdt.eclipse.login.common.jar
similarity index 100%
rename from lib-working/google.login.jar
rename to login/lib/google.gdt.eclipse.login.common.jar
Binary files differ
diff --git a/lib-working/jackson-core-asl-1.9.11.jar b/login/lib/jackson-core-asl-1.9.11.jar
similarity index 100%
rename from lib-working/jackson-core-asl-1.9.11.jar
rename to login/lib/jackson-core-asl-1.9.11.jar
Binary files differ
diff --git a/login/lib/javax.servlet-api-3.0.1.jar b/login/lib/javax.servlet-api-3.0.1.jar
new file mode 100644
index 0000000..4e2edcc
--- /dev/null
+++ b/login/lib/javax.servlet-api-3.0.1.jar
Binary files differ
diff --git a/login/resources/icons/google.png b/login/resources/icons/google.png
new file mode 100644
index 0000000..3f7dfe4
--- /dev/null
+++ b/login/resources/icons/google.png
Binary files differ
diff --git a/login/resources/icons/[email protected] b/login/resources/icons/[email protected]
new file mode 100644
index 0000000..ad846a4
--- /dev/null
+++ b/login/resources/icons/[email protected]
Binary files differ
diff --git a/login/resources/icons/googleFavicon.png b/login/resources/icons/googleFavicon.png
new file mode 100644
index 0000000..e9c3aba
--- /dev/null
+++ b/login/resources/icons/googleFavicon.png
Binary files differ
diff --git a/login/resources/icons/[email protected] b/login/resources/icons/[email protected]
new file mode 100644
index 0000000..59cdbf9
--- /dev/null
+++ b/login/resources/icons/[email protected]
Binary files differ
diff --git a/resources/icons/loginAvatar.png b/login/resources/icons/loginAvatar.png
similarity index 100%
rename from resources/icons/loginAvatar.png
rename to login/resources/icons/loginAvatar.png
Binary files differ
diff --git a/resources/icons/[email protected] b/login/resources/icons/[email protected]
similarity index 100%
rename from resources/icons/[email protected]
rename to login/resources/icons/[email protected]
Binary files differ
diff --git a/login/src/META-INF/plugin.xml b/login/src/META-INF/plugin.xml
new file mode 100644
index 0000000..1bd7803
--- /dev/null
+++ b/login/src/META-INF/plugin.xml
@@ -0,0 +1,48 @@
+<!--
+ ~ 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.
+ -->
+<idea-plugin version="2">
+ <id>com.google.gct.login</id>
+ <name>Google Login</name>
+ <version>1.0</version>
+ <vendor>Google</vendor>
+
+ <description>Provides Google authentication support.</description>
+
+ <!-- please see http://confluence.jetbrains.net/display/IDEADEV/Build+Number+Ranges for description -->
+ <idea-version since-build="107.105"/>
+
+ <application-components>
+ </application-components>
+
+ <project-components>
+ </project-components>
+
+ <actions>
+ <action id="GoogleLogin.LoginService"
+ class="com.google.gct.login.ui.GoogleLoginAction"
+ text="Google Login">
+ <add-to-group group-id="MainToolBar" anchor="first" />
+ </action>
+ </actions>
+
+ <extensions defaultExtensionNs="com.intellij">
+ </extensions>
+
+ <extensionPoints>
+ <extensionPoint name="googleLoginListener" interface="com.google.gct.login.GoogleLoginListener"/>
+ </extensionPoints>
+
+</idea-plugin>
\ No newline at end of file
diff --git a/login/src/com/google/gct/login/CancellableServerReceiver.java b/login/src/com/google/gct/login/CancellableServerReceiver.java
new file mode 100644
index 0000000..dfe1cd8
--- /dev/null
+++ b/login/src/com/google/gct/login/CancellableServerReceiver.java
@@ -0,0 +1,250 @@
+/*
+ * 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.login;
+
+import com.google.api.client.extensions.java6.auth.oauth2.VerificationCodeReceiver;
+import com.google.api.client.repackaged.com.google.common.base.Throwables;
+import com.google.api.client.repackaged.org.mortbay.jetty.Connector;
+import com.google.api.client.repackaged.org.mortbay.jetty.Request;
+import com.google.api.client.repackaged.org.mortbay.jetty.Server;
+import com.google.api.client.repackaged.org.mortbay.jetty.handler.AbstractHandler;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.Socket;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * A cancellable Server Receiver.
+ */
+class CancellableServerReceiver implements VerificationCodeReceiver {
+ private static final String CALLBACK_PATH = "/Callback";
+
+ /** Server or {@code null} before {@link #getRedirectUri()}. */
+ private Server server;
+
+ /** Verification code or {@code null} for none. */
+ String code;
+
+ /** Error code or {@code null} for none. */
+ String error;
+
+ /** Lock on the code and error. */
+ final Lock lock = new ReentrantLock();
+
+ /** Condition for receiving an authorization response. */
+ final Condition gotAuthorizationResponse = lock.newCondition();
+
+ /** Port to use or {@code -1} to select an unused port in {@link #getRedirectUri()}. */
+ private int port;
+
+ /** Host name to use. */
+ private final String host;
+
+ /**
+ * Constructor that starts the server on {@code "localhost"} selects an unused port.
+ *
+ * <p>
+ * Use {@link Builder} if you need to specify any of the optional parameters.
+ * </p>
+ */
+ public CancellableServerReceiver() {
+ this("localhost", -1);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param host Host name to use
+ * @param port Port to use or {@code -1} to select an unused port
+ */
+ CancellableServerReceiver(String host, int port) {
+ this.host = host;
+ this.port = port;
+ }
+
+ @Override
+ public String getRedirectUri() throws IOException {
+ if (port == -1) {
+ port = getUnusedPort();
+ }
+ server = new Server(port);
+ for (Connector c : server.getConnectors()) {
+ c.setHost(host);
+ }
+ server.addHandler(new CallbackHandler());
+ try {
+ server.start();
+ } catch (Exception e) {
+ Throwables.propagateIfPossible(e);
+ throw new IOException(e);
+ }
+ return "http://" + host + ":" + port + CALLBACK_PATH;
+ }
+
+ @Override
+ public String waitForCode() throws IOException {
+ lock.lock();
+ try {
+ while (code == null && error == null) {
+ gotAuthorizationResponse.awaitUninterruptibly();
+ }
+ if (error != null) {
+ throw new IOException("User authorization failed (" + error + ")");
+ }
+ return code;
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ @Override
+ public void stop() throws IOException {
+ if (server != null) {
+ try {
+ server.stop();
+ } catch (Exception e) {
+ Throwables.propagateIfPossible(e);
+ throw new IOException(e);
+ }
+ lock.lock();
+ try {
+ error = "Request cancelled.";
+ code = null;
+ gotAuthorizationResponse.signal();
+ }
+ finally {
+ lock.unlock();
+ }
+ server = null;
+ }
+ }
+
+ /** Returns the host name to use. */
+ public String getHost() {
+ return host;
+ }
+
+ /**
+ * Returns the port to use or {@code -1} to select an unused port in {@link #getRedirectUri()}.
+ */
+ public int getPort() {
+ return port;
+ }
+
+ private static int getUnusedPort() throws IOException {
+ Socket s = new Socket();
+ s.bind(null);
+ try {
+ return s.getLocalPort();
+ } finally {
+ s.close();
+ }
+ }
+
+ /**
+ * Builder.
+ *
+ * <p>
+ * Implementation is not thread-safe.
+ * </p>
+ */
+ public static final class Builder {
+
+ /** Host name to use. */
+ private String host = "localhost";
+
+ /** Port to use or {@code -1} to select an unused port. */
+ private int port = -1;
+
+ /** Builds the {@link LocalServerReceiver}. */
+ public CancellableServerReceiver build() {
+ return new CancellableServerReceiver(host, port);
+ }
+
+ /** Returns the host name to use. */
+ public String getHost() {
+ return host;
+ }
+
+ /** Sets the host name to use. */
+ public Builder setHost(String host) {
+ this.host = host;
+ return this;
+ }
+
+ /** Returns the port to use or {@code -1} to select an unused port. */
+ public int getPort() {
+ return port;
+ }
+
+ /** Sets the port to use or {@code -1} to select an unused port. */
+ public Builder setPort(int port) {
+ this.port = port;
+ return this;
+ }
+ }
+
+ /**
+ * Jetty handler that takes the verifier token passed over from the OAuth provider and stashes it
+ * where {@link #waitForCode} will find it.
+ */
+ class CallbackHandler extends AbstractHandler {
+
+ @Override
+ public void handle(String target, HttpServletRequest request, HttpServletResponse response, int dispatch) throws IOException {
+ if (!CALLBACK_PATH.equals(target)) {
+ return;
+ }
+ writeLandingHtml(response);
+ response.flushBuffer();
+ ((Request)request).setHandled(true);
+ lock.lock();
+ try {
+ error = request.getParameter("error");
+ code = request.getParameter("code");
+ gotAuthorizationResponse.signal();
+ }
+ finally {
+ lock.unlock();
+ }
+ }
+
+ private void writeLandingHtml(HttpServletResponse response) throws IOException {
+ response.setStatus(HttpServletResponse.SC_OK);
+ response.setContentType("text/html");
+
+ PrintWriter doc = response.getWriter();
+ doc.println("<html>");
+ doc.println("<head><title>OAuth 2.0 Authentication Token Received</title></head>");
+ doc.println("<body>");
+ doc.println("Received verification code. Closing...");
+ doc.println("<script type='text/javascript'>");
+ // We open "" in the same window to trigger JS ownership of it, which lets
+ // us then close it via JS, at least in Chrome.
+ doc.println("window.setTimeout(function() {");
+ doc.println(" window.open('https://developers.google.com/', '_self', '');}, 100);");
+ doc.println("</script>");
+ doc.println("</body>");
+ doc.println("</HTML>");
+ doc.flush();
+ }
+ }
+}
diff --git a/login/src/com/google/gct/login/CredentialedUser.java b/login/src/com/google/gct/login/CredentialedUser.java
index 957f03b..8a42a6f 100644
--- a/login/src/com/google/gct/login/CredentialedUser.java
+++ b/login/src/com/google/gct/login/CredentialedUser.java
@@ -44,7 +44,7 @@
googleLoginState = null;
}
- public CredentialedUser(GoogleLoginState state, final IGoogleLoginUpdateUser updateUserCallback) {
+ public CredentialedUser(GoogleLoginState state, final IGoogleLoginCompletedCallback updateUserCallback) {
this.email = state.getEmail();
googleLoginState = state;
credential = googleLoginState.makeCredential();
@@ -95,7 +95,7 @@
this.isActive = isActive;
}
- private void initializeUserInfo(Userinfoplus userInfo, final IGoogleLoginUpdateUser updateUserCallback) {
+ private void initializeUserInfo(Userinfoplus userInfo, final IGoogleLoginCompletedCallback updateUserCallback) {
if(userInfo == null) {
name = null;
image = null;
@@ -105,7 +105,7 @@
@Override
public void setProperty(Image newImage) {
image = newImage;
- updateUserCallback.updateUser();
+ updateUserCallback.onLoginCompleted();
}
};
GoogleLoginUtils.getUserPicture(userInfo, pictureCallback);
diff --git a/login/src/com/google/gct/login/CredentialedUserRoster.java b/login/src/com/google/gct/login/CredentialedUserRoster.java
index d383ca2..7d50c70 100644
--- a/login/src/com/google/gct/login/CredentialedUserRoster.java
+++ b/login/src/com/google/gct/login/CredentialedUserRoster.java
@@ -15,10 +15,12 @@
*/
package com.google.gct.login;
+import com.google.common.collect.Lists;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
-import java.util.HashMap;
+import java.util.Collection;
+import java.util.LinkedHashMap;
import java.util.Map;
/**
@@ -27,16 +29,21 @@
* {@link CredentialedUser} objects.
*/
public class CredentialedUserRoster {
- private final Map<String, CredentialedUser> allUsers = new HashMap<String, CredentialedUser>();
+ private final LinkedHashMap<String, CredentialedUser> allUsers = new LinkedHashMap<String, CredentialedUser>();
private CredentialedUser activeUser;
+ private Collection<GoogleLoginListener> listeners;
+
+ public CredentialedUserRoster() {
+ listeners = Lists.newLinkedList();
+ }
/**
* Returns a copy of the map of the current logged in users.
* @return Copy of current logged in users.
*/
- public Map<String, CredentialedUser> getAllUsers() {
+ public LinkedHashMap<String, CredentialedUser> getAllUsers() {
synchronized (this) {
- Map<String, CredentialedUser> clone = new HashMap<String, CredentialedUser>();
+ LinkedHashMap<String, CredentialedUser> clone = new LinkedHashMap<String, CredentialedUser>();
clone.putAll(allUsers);
return clone;
}
@@ -82,6 +89,7 @@
activeUser = allUsers.get(userEmail);
activeUser.setActive(true);
GoogleLoginPrefs.saveActiveUser(userEmail);
+ notifyLoginStatusChange();
}
}
@@ -94,6 +102,7 @@
activeUser.setActive(false);
activeUser = null;
GoogleLoginPrefs.removeActiveUser();
+ notifyLoginStatusChange();
}
}
}
@@ -151,8 +160,29 @@
}
allUsers.remove(userEmail);
+ notifyLoginStatusChange();
return true;
}
}
+ /**
+ * Register a specified {@link GoogleLoginListener} to be notified of changes to the
+ * logged-in state.
+ *
+ * @param listener the specified {@code GoogleLoginListener}
+ */
+ void addLoginListener(GoogleLoginListener listener) {
+ synchronized(listeners) {
+ listeners.add(listener);
+ }
+ }
+
+ private void notifyLoginStatusChange() {
+ synchronized(listeners) {
+ for (GoogleLoginListener listener : listeners) {
+ listener.statusChanged();
+ }
+ }
+ }
+
}
diff --git a/login/src/com/google/gct/login/GoogleLogin.java b/login/src/com/google/gct/login/GoogleLogin.java
index 549a5e9..50d6c67 100644
--- a/login/src/com/google/gct/login/GoogleLogin.java
+++ b/login/src/com/google/gct/login/GoogleLogin.java
@@ -30,21 +30,28 @@
import com.google.gdt.eclipse.login.common.OAuthData;
import com.google.gdt.eclipse.login.common.OAuthDataStore;
import com.google.gdt.eclipse.login.common.UiFacade;
-
import com.google.gdt.eclipse.login.common.VerificationCodeHolder;
import com.intellij.ide.BrowserUtil;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.extensions.Extensions;
+import com.intellij.openapi.progress.ProgressIndicator;
+import com.intellij.openapi.progress.Task;
+import com.intellij.openapi.progress.util.ProgressIndicatorBase;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.util.IconLoader;
+import com.intellij.openapi.wm.ex.ProgressIndicatorEx;
import net.jcip.annotations.Immutable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
+import javax.swing.Icon;
import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
import java.util.SortedSet;
@@ -71,6 +78,7 @@
this.uiFacade = new AndroidUiFacade();
this.users = new CredentialedUserRoster();
this.dataStore = new AndroidPreferencesOAuthDataStore();
+ addLoginListenersFromExtensionPoints();
}
/**
@@ -90,7 +98,7 @@
* @throws InvalidThreadTypeException
*/
public static void promptToLogIn() throws InvalidThreadTypeException {
- promptToLogIn(null);
+ promptToLogIn(null, null);
}
/**
@@ -98,12 +106,15 @@
* if there is current no active user. Does nothing if there is an active
* user. This function must be called from the event dispatch thread (EDT).
* @param message If not null, this message would be the title of the dialog.
+ * @param callback if not null, then this callback is called when the login
+ * either succeeds or fails.
* @throws InvalidThreadTypeException
*/
- public static void promptToLogIn(final String message) throws InvalidThreadTypeException {
+ public static void promptToLogIn(final String message, @Nullable final IGoogleLoginCompletedCallback callback)
+ throws InvalidThreadTypeException {
if (!instance.isLoggedIn()) {
if(ApplicationManager.getApplication().isDispatchThread()) {
- getInstance().logIn(message);
+ getInstance().logIn(message, callback);
} else {
throw new InvalidThreadTypeException("promptToLogin");
}
@@ -275,8 +286,7 @@
* See {@link #logIn(String)}.
*/
public void logIn() {
- users.removeActiveUser();
- logIn(null);
+ logIn(null, null);
}
/**
@@ -291,28 +301,67 @@
* as accessing Google API services. It should say something like
* "Importing a project from Google Project Hosting requires signing
* in."
+ * @param callback if not null, then this callback is called when the login
+ * either succeeds or fails.
*/
- public void logIn(final String message) {
+ public void logIn(final String message, @Nullable final IGoogleLoginCompletedCallback callback) {
+ users.removeActiveUser();
+ uiFacade.notifyStatusIndicator();
+
final GoogleLoginState state = createGoogleLoginState();
- ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
+ new Task.Modal(null, "Please sign in via the opened browser...", true) {
+ private boolean loggedIn = false;
+
@Override
- public void run() {
- boolean loggedIn = state.logInWithLocalServer(message);
+ public void run(@NotNull ProgressIndicator indicator) {
+ indicator.setIndeterminate(true);
+ if (!(indicator instanceof ProgressIndicatorEx)) {
+ return;
+ }
+ ((ProgressIndicatorEx)indicator).addStateDelegate(new ProgressIndicatorBase() {
+ @Override
+ public void cancel() {
+ assert uiFacade != null;
+ uiFacade.stop();
+ super.cancel();
+ }
+ });
+
+ loggedIn = state.logInWithLocalServer(message);
+ }
+
+ @Override
+ public void onCancel() {
+ notifyOnComplete();
+ }
+
+ @Override
+ public void onSuccess() {
+ notifyOnComplete();
+ }
+
+ private void notifyOnComplete() {
// TODO: add user preference to chose to use pop-up copy and paste dialog
-
if(loggedIn) {
- IGoogleLoginUpdateUser callback = new IGoogleLoginUpdateUser() {
+ IGoogleLoginCompletedCallback localCallback = new IGoogleLoginCompletedCallback() {
+
@Override
- public void updateUser() {
+ public void onLoginCompleted() {
uiFacade.notifyStatusIndicator();
+ if(callback != null) {
+ callback.onLoginCompleted();
+ }
}
};
- users.addUser(new CredentialedUser(state, callback));
+ users.addUser(new CredentialedUser(state, localCallback));
+ }
+ else if (callback != null) {
+ callback.onLoginCompleted();
}
}
- });
+ }.queue();
}
/**
@@ -392,7 +441,7 @@
* Returns a copy of the map of the current logged in users.
* @return Copy of current logged in users.
*/
- public Map<String, CredentialedUser> getAllUsers() {
+ public LinkedHashMap<String, CredentialedUser> getAllUsers() {
return users.getAllUsers();
}
@@ -419,12 +468,11 @@
/**
* Gets all the implementations of {@link GoogleLoginListener} and registers them to
* <code>state</code>.
- * @param state the {@link GoogleLoginState} for which we want to register listeners to.
*/
- private static void addLoginListenersFromExtensionPoints(GoogleLoginState state) {
+ private void addLoginListenersFromExtensionPoints() {
GoogleLoginListener[] loginListeners = Extensions.getExtensions(GoogleLoginListener.EP_NAME);
for(GoogleLoginListener listener : loginListeners) {
- state.addLoginListener(listener);
+ users.addLoginListener(listener);
}
}
@@ -441,8 +489,6 @@
new AndroidPreferencesOAuthDataStore(),
uiFacade,
new AndroidLoggerFacade());
-
- addLoginListenersFromExtensionPoints(state);
return state;
}
@@ -504,6 +550,8 @@
*/
private class AndroidUiFacade implements UiFacade {
private GoogleLoginActionButton myButton;
+ private final static String GOOGLE_IMG = "/icons/[email protected]";
+ private volatile CancellableServerReceiver receiver = null;
@Override
public String obtainVerificationCodeFromUserInteraction(String title, GoogleAuthorizationCodeRequestUrl authCodeRequestUrl) {
@@ -516,9 +564,21 @@
return Strings.emptyToNull(dialog.getVerificationCode());
}
+ public void stop() {
+ CancellableServerReceiver localreceiver = receiver;
+ if (localreceiver != null) {
+ try {
+ localreceiver.stop();
+ }
+ catch(IOException e) {
+ logErrorAndDisplayDialog("Google Login", e);
+ }
+ }
+ }
+
@Override
public VerificationCodeHolder obtainVerificationCodeFromExternalUserInteraction(String title) {
- VerificationCodeReceiver receiver = new LocalServerReceiver();
+ receiver = new CancellableServerReceiver();
String redirectUrl;
try {
redirectUrl = receiver.getRedirectUri();
@@ -543,6 +603,9 @@
logErrorAndDisplayDialog(title == null? "Google Login" : title, e);
return null;
}
+ finally {
+ receiver = null;
+ }
return new VerificationCodeHolder(verificationCode, redirectUrl);
}
@@ -554,7 +617,16 @@
@Override
public boolean askYesOrNo(String title, String message) {
- return (Messages.showYesNoDialog(message, title, Messages.getQuestionIcon()) == Messages.YES);
+ String updatedMessage = message;
+ if(message.equals("Are you sure you want to sign out?")) {
+ CredentialedUser activeUser = getActiveUser();
+ String name = activeUser.getName().isEmpty() ? "" : activeUser.getName() + " ";
+ updatedMessage = "Are you sure you want to sign out " + name
+ + "(" + activeUser.getEmail() + ")?";
+ }
+
+ Icon icon = IconLoader.getIcon(GOOGLE_IMG);
+ return (Messages.showYesNoDialog(updatedMessage, title, icon) == Messages.YES);
}
@Override
@@ -600,7 +672,8 @@
}
public void initializeUsers() {
- SortedSet<String> allUsers = GoogleLoginPrefs.getStoredUsers();
+ String activeUserString = GoogleLoginPrefs.getActiveUser();
+ List<String> allUsers = GoogleLoginPrefs.getStoredUsers();
for(String aUser : allUsers) {
// Add a new user, so that loadOAuth called from the GoogleLoginState constructor
// will be able to create a customized key to get that user's OAuth data
@@ -609,9 +682,9 @@
// CredentialedUser's credentials will be updated from the persistent storage in GoogleLoginState constructor
GoogleLoginState delegate = createGoogleLoginState();
- IGoogleLoginUpdateUser callback = new IGoogleLoginUpdateUser() {
+ IGoogleLoginCompletedCallback callback = new IGoogleLoginCompletedCallback() {
@Override
- public void updateUser() {
+ public void onLoginCompleted() {
uiFacade.notifyStatusIndicator();
}
};
@@ -619,7 +692,6 @@
users.addUser(new CredentialedUser(delegate, callback));
}
- String activeUserString = GoogleLoginPrefs.getActiveUser();
if(activeUserString == null) {
users.removeActiveUser();
} else {
diff --git a/login/src/com/google/gct/login/GoogleLoginListener.java b/login/src/com/google/gct/login/GoogleLoginListener.java
index 11c4f4c..560f5f0 100644
--- a/login/src/com/google/gct/login/GoogleLoginListener.java
+++ b/login/src/com/google/gct/login/GoogleLoginListener.java
@@ -15,13 +15,17 @@
*/
package com.google.gct.login;
-import com.google.gdt.eclipse.login.common.LoginListener;
import com.intellij.openapi.extensions.ExtensionPointName;
/**
* Listener for changes in the login status.
*/
-public interface GoogleLoginListener extends LoginListener {
+public interface GoogleLoginListener {
public static ExtensionPointName<GoogleLoginListener> EP_NAME =
- new ExtensionPointName<GoogleLoginListener>("com.google.gct.googleLoginListener");
+ new ExtensionPointName<GoogleLoginListener>("com.google.gct.login.googleLoginListener");
+
+ /**
+ * Called when the login or active status of the user changes.
+ */
+ void statusChanged();
}
diff --git a/login/src/com/google/gct/login/GoogleLoginPrefs.java b/login/src/com/google/gct/login/GoogleLoginPrefs.java
index 8795238..1ad2b9f 100644
--- a/login/src/com/google/gct/login/GoogleLoginPrefs.java
+++ b/login/src/com/google/gct/login/GoogleLoginPrefs.java
@@ -23,6 +23,8 @@
import com.intellij.openapi.diagnostic.Logger;
import org.jetbrains.annotations.NotNull;
+import java.util.ArrayList;
+import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.prefs.BackingStoreException;
@@ -42,9 +44,6 @@
private static String preferencesPath = PREFERENCES_PATH;
private static final String OAUTH_DATA_EMAIL_KEY = "credentials_email";
- private static final String OAUTH_DATA_ACCESS_TOKEN_KEY = "credentials_access_token";
- private static final String OAUTH_DATA_ACCESS_TOKEN_EXPIRY_TIME_KEY =
- "credentials_access_token_expiry_time";
private static final String OAUTH_DATA_REFRESH_TOKEN_KEY = "credentials_refresh_token";
private static final String ICON_ONLY_KEY = "icon_only";
private static final String LOGOUT_ON_EXIT_KEY = "logout_on_exit";
@@ -61,11 +60,7 @@
public static void saveOAuthData(OAuthData credentials) {
Preferences prefs = getPrefs();
String userEmail = credentials.getStoredEmail();
- prefs.put(getCustomUserKey(OAUTH_DATA_ACCESS_TOKEN_KEY, userEmail), credentials.getAccessToken());
prefs.put(getCustomUserKey(OAUTH_DATA_REFRESH_TOKEN_KEY, userEmail), credentials.getRefreshToken());
- prefs.put(
- getCustomUserKey(OAUTH_DATA_ACCESS_TOKEN_EXPIRY_TIME_KEY, userEmail),
- Long.toString(credentials.getAccessTokenExpiryTime()));
// we save the scopes so that if the user updates the plugin and the
// scopes change, we can force the plugin to log out.
@@ -89,7 +84,6 @@
public static OAuthData loadOAuthData() {
Preferences prefs = getPrefs();
- String accessToken = prefs.get(getCustomUserKey(OAUTH_DATA_ACCESS_TOKEN_KEY), null);
String refreshToken = prefs.get(getCustomUserKey(OAUTH_DATA_REFRESH_TOKEN_KEY), null);
String storedEmail = prefs.get(getCustomUserKey(OAUTH_DATA_EMAIL_KEY), null);
String storedScopesString = prefs.get(getCustomUserKey(OAUTH_SCOPES_KEY), "");
@@ -99,13 +93,7 @@
for (String scope : storedScopesString.split(DELIMITER)) {
storedScopes.add(scope);
}
- long accessTokenExpiryTime = 0;
- String accessTokenExpiryTimeString = prefs.get(getCustomUserKey(OAUTH_DATA_ACCESS_TOKEN_EXPIRY_TIME_KEY), null);
- if (accessTokenExpiryTimeString != null) {
- accessTokenExpiryTime = Long.parseLong(accessTokenExpiryTimeString);
- }
- return new OAuthData(
- accessToken, refreshToken, storedEmail, storedScopes, accessTokenExpiryTime);
+ return new OAuthData(null, refreshToken, storedEmail, storedScopes, 0);
}
/**
@@ -118,11 +106,9 @@
}
Preferences prefs = getPrefs();
- prefs.remove(getCustomUserKey(OAUTH_DATA_ACCESS_TOKEN_KEY));
prefs.remove(getCustomUserKey(OAUTH_DATA_REFRESH_TOKEN_KEY));
prefs.remove(getCustomUserKey(OAUTH_DATA_EMAIL_KEY));
prefs.remove(getCustomUserKey(OAUTH_SCOPES_KEY));
- prefs.remove(getCustomUserKey(OAUTH_DATA_ACCESS_TOKEN_EXPIRY_TIME_KEY));
removeUser(prefs, activeUser.getEmail());
flushPrefs(prefs);
}
@@ -163,10 +149,10 @@
* @return the stored list of users.
*/
@NotNull
- public static SortedSet<String> getStoredUsers() {
+ public static List<String> getStoredUsers() {
Preferences prefs = getPrefs();
String allUsersString = prefs.get(USERS, "");
- SortedSet<String> allUsers = new TreeSet<String>();
+ List<String> allUsers = new ArrayList<String>();
if(allUsersString.isEmpty()) {
return allUsers;
}
@@ -252,7 +238,7 @@
return;
}
- SortedSet<String> allUsers = new TreeSet<String>();
+ List<String> allUsers = new ArrayList<String>();
Splitter splitter = Splitter.on(DELIMITER).omitEmptyStrings();
for (String scope : splitter.split(allUsersString)) {
allUsers.add(scope);
@@ -269,7 +255,7 @@
private static void removeUser(Preferences prefs, String user) {;
String allUsersString = prefs.get(USERS, "");
- SortedSet<String> allUsers = new TreeSet<String>();
+ List<String> allUsers = new ArrayList<String>();
for (String scope : allUsersString.split(DELIMITER)) {
allUsers.add(scope);
}
diff --git a/login/src/com/google/gct/login/GoogleLoginUtils.java b/login/src/com/google/gct/login/GoogleLoginUtils.java
index 5204ef2..a4a8fce 100644
--- a/login/src/com/google/gct/login/GoogleLoginUtils.java
+++ b/login/src/com/google/gct/login/GoogleLoginUtils.java
@@ -69,6 +69,7 @@
@Override
public void run() {
Image image = Toolkit.getDefaultToolkit().getImage(newUrl);
+ Toolkit.getDefaultToolkit().prepareImage(image, -1, -1, null);
pictureCallback.setProperty(image);
}
});
diff --git a/login/src/com/google/gct/login/IGoogleLoginUpdateUser.java b/login/src/com/google/gct/login/IGoogleLoginCompletedCallback.java
similarity index 77%
rename from login/src/com/google/gct/login/IGoogleLoginUpdateUser.java
rename to login/src/com/google/gct/login/IGoogleLoginCompletedCallback.java
index 771081b..5661916 100644
--- a/login/src/com/google/gct/login/IGoogleLoginUpdateUser.java
+++ b/login/src/com/google/gct/login/IGoogleLoginCompletedCallback.java
@@ -15,6 +15,12 @@
*/
package com.google.gct.login;
-public interface IGoogleLoginUpdateUser {
- public void updateUser();
+/**
+ * Callback for when a login is completed.
+ */
+public interface IGoogleLoginCompletedCallback {
+ /**
+ * Called when log in is complete.
+ */
+ public void onLoginCompleted();
}
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/login/src/com/google/gct/login/ui/GoogleLoginActionButton.java b/login/src/com/google/gct/login/ui/GoogleLoginActionButton.java
index 6782f01..21b0628 100644
--- a/login/src/com/google/gct/login/ui/GoogleLoginActionButton.java
+++ b/login/src/com/google/gct/login/ui/GoogleLoginActionButton.java
@@ -31,14 +31,24 @@
/**
* The Google Login button that appears on the main toolbar.
*/
-public class GoogleLoginActionButton extends ActionButton {
+public final class GoogleLoginActionButton extends ActionButton {
private Icon defaultIcon;
private final static String SIGN_IN_MESSAGE = "Sign in to Google...";
private final static String DEFAULT_AVATAR = "/icons/loginAvatar.png";
+ private final static String SHOW_LOGIN_BUTTON_PROPERTY = "show.google.login.button";
+
public GoogleLoginActionButton(AnAction action, Presentation presentation, String place, @NotNull Dimension minimumSize) {
super(action, presentation, place, minimumSize);
+ // The Google login toolbar item is hidden by default temporarily.
+ // To view the login button, add "-Dshow.google.login.button=true" to the JVM options
+ boolean showLoginButton = Boolean.getBoolean(SHOW_LOGIN_BUTTON_PROPERTY);
+ if(!showLoginButton) {
+ setVisible(false);
+ return;
+ }
+
GoogleLogin.getInstance().setLoginMenuItemContribution(this);
defaultIcon = IconLoader.getIcon(DEFAULT_AVATAR);
updateUi();
diff --git a/login/src/com/google/gct/login/ui/GoogleLoginUsersPanel.java b/login/src/com/google/gct/login/ui/GoogleLoginUsersPanel.java
index da72f86..a98ce85 100644
--- a/login/src/com/google/gct/login/ui/GoogleLoginUsersPanel.java
+++ b/login/src/com/google/gct/login/ui/GoogleLoginUsersPanel.java
@@ -17,6 +17,7 @@
import com.google.gct.login.CredentialedUser;
import com.google.gct.login.GoogleLogin;
+import com.intellij.ide.BrowserUtil;
import com.intellij.ui.components.JBList;
import com.intellij.ui.components.JBScrollPane;
@@ -32,39 +33,143 @@
import javax.swing.event.ListSelectionListener;
import java.awt.BorderLayout;
+import java.awt.Cursor;
+import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
-import java.util.Map;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseMotionListener;
+import java.util.LinkedHashMap;
/**
* The Google Login Panel that displays the currently logged in users and buttons to
* add a new user and sign out a logged in user.
*/
public class GoogleLoginUsersPanel extends JPanel implements ListSelectionListener {
+ private static final String PLAY_CONSOLE_URL = "https://play.google.com/apps/publish/#ProfilePlace";
+ private static final String CLOUD_CONSOLE_URL = "https://console.developers.google.com/accountsettings";
+ private final static String LEARN_MORE_URL = "https://developers.google.com/cloud/devtools/android_studio_templates/";
private JBList list;
private DefaultListModel listModel;
-
private static final int MAX_VISIBLE_ROW_COUNT = 3;
private static final String addAccountString = "Add Account";
+ private static final String signInString = "Sign In";
private static final String signOutString = "Sign Out";
private JButton signOutButton;
private JButton addAccountButton;
+ private boolean valueChanged = false;
+ private boolean ignoreSelection = false;
public GoogleLoginUsersPanel() {
super(new BorderLayout());
int indexToSelect = initializeUsers();
+ final UsersListCellRenderer usersListCellRenderer = new UsersListCellRenderer();
//Create the list that displays the users and put it in a scroll pane.
- list = new JBList(listModel);
+ list = new JBList(listModel) {
+ @Override
+ public Dimension getPreferredScrollableViewportSize() {
+ int numUsers = listModel.size();
+ Dimension superPreferredSize = super.getPreferredScrollableViewportSize();
+ if(numUsers <= 1) {
+ return superPreferredSize;
+ }
+
+ if(GoogleLogin.getInstance().getActiveUser() == null){
+ return superPreferredSize;
+ } else if(!isActiveUserInVisibleArea()) {
+ return superPreferredSize;
+ } else {
+ // if there is an active user in the visible area
+ int usersToShow = numUsers > MAX_VISIBLE_ROW_COUNT ? MAX_VISIBLE_ROW_COUNT : numUsers;
+ int scrollHeight = ((usersToShow - 1) * usersListCellRenderer.getMainPanelHeight())
+ + usersListCellRenderer.getActivePanelHeight();
+ return new Dimension((int)superPreferredSize.getWidth(), scrollHeight);
+ }
+ }
+ };
+
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
list.setSelectedIndex(indexToSelect);
list.addListSelectionListener(this);
list.setVisibleRowCount(getVisibleRowCount());
- list.setCellRenderer(new UsersListCellRenderer());
+ list.setCellRenderer(usersListCellRenderer);
JBScrollPane listScrollPane = new JBScrollPane(list);
- addAccountButton = new JButton(addAccountString);
+ list.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent mouseEvent) {
+ list.updateUI();
+
+ if(listModel.getSize() == 1 && (listModel.get(0) instanceof NoUsersListItem)) {
+ // When there are no users available
+ if(usersListCellRenderer.inLearnMoreUrl(mouseEvent.getPoint())){
+ BrowserUtil.browse(LEARN_MORE_URL);
+ }
+ } else {
+ // When users are available
+ if(!valueChanged) {
+ // Clicking on an already active user
+ int index = list.locationToIndex(mouseEvent.getPoint());
+ if (index >= 0) {
+ boolean inPlayUrl = usersListCellRenderer.inPlayConsoleUrl(mouseEvent.getPoint(), index);
+ if(inPlayUrl){
+ BrowserUtil.browse(PLAY_CONSOLE_URL);
+ } else {
+ boolean inCloudUrl = usersListCellRenderer.inCloudConsoleUrl(mouseEvent.getPoint(), index);
+ if(inCloudUrl) {
+ BrowserUtil.browse(CLOUD_CONSOLE_URL);
+ }
+ }
+ }
+ }
+ }
+ valueChanged = false;
+ }
+ });
+
+ list.addMouseMotionListener(new MouseMotionListener() {
+ @Override
+ public void mouseMoved(MouseEvent mouseEvent) {
+ // Determine if the user under the cursor is an active user, a non-active user or a non-user
+ int index = list.locationToIndex(mouseEvent.getPoint());
+ if (index >= 0) {
+ // If current object is the non-user list item, use default cursor
+ Object currentObject = listModel.get(index);
+ if(currentObject instanceof NoUsersListItem) {
+ if(usersListCellRenderer.inLearnMoreUrl(mouseEvent.getPoint())) {
+ list.setCursor(new Cursor(Cursor.HAND_CURSOR));
+ } else {
+ list.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
+ }
+ return;
+ }
+
+ if (((UsersListItem)currentObject).isActiveUser()) {
+ // Active user
+ boolean inPlayUrl = usersListCellRenderer.inPlayConsoleUrl(mouseEvent.getPoint(), index);
+ boolean inCloudUrl = usersListCellRenderer.inCloudConsoleUrl(mouseEvent.getPoint(), index);
+ if (inPlayUrl || inCloudUrl) {
+ list.setCursor(new Cursor(Cursor.HAND_CURSOR));
+ } else {
+ list.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
+ }
+ } else {
+ // For non-active user
+ list.setCursor(new Cursor(Cursor.HAND_CURSOR));
+ }
+ }
+ }
+
+ @Override
+ public void mouseDragged(MouseEvent e) {
+ }
+ });
+
+ boolean noUsersAvailable = (listModel.getSize() == 1) && (listModel.get(0) instanceof NoUsersListItem);
+ addAccountButton = new JButton(noUsersAvailable ? signInString : addAccountString);
AddAccountListener addAccountListener = new AddAccountListener();
addAccountButton.addActionListener(addAccountListener);
addAccountButton.setHorizontalAlignment(SwingConstants.LEFT);
@@ -75,7 +180,13 @@
if(list.isSelectionEmpty()) {
signOutButton.setEnabled(false);
} else {
- signOutButton.setEnabled(true);
+ // If list contains the NoUsersListItem place holder
+ // sign out button should be hidden
+ if(noUsersAvailable) {
+ signOutButton.setVisible(false);
+ } else {
+ signOutButton.setEnabled(true);
+ }
}
//Create a panel to hold the buttons
@@ -96,18 +207,7 @@
class SignOutListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
- //This method can be called only if there's a valid selection
- int index = list.getSelectedIndex();
-
- boolean signedOut = GoogleLogin.getInstance().logOut();
- if(signedOut) {
- // remove logged out user
- listModel.remove(index);
- if (listModel.getSize() == 0) {
- signOutButton.setEnabled(false);
-
- }
- }
+ GoogleLogin.getInstance().logOut();
}
}
@@ -119,20 +219,18 @@
public void actionPerformed(ActionEvent e) {
GoogleLogin.getInstance().logIn();
}
-
- protected boolean alreadyInList(String name) {
- return listModel.contains(name);
- }
}
//This method is required by ListSelectionListener.
@Override
public void valueChanged(ListSelectionEvent e) {
+ if(ignoreSelection) {
+ return;
+ }
+ valueChanged = true;
if (e.getValueIsAdjusting() == false) {
-
if (list.getSelectedIndex() == -1) {
signOutButton.setEnabled(false);
-
} else {
signOutButton.setEnabled(true);
@@ -141,6 +239,19 @@
if(!selectedUser.isActiveUser()) {
GoogleLogin.getInstance().setActiveUser(selectedUser.getUserEmail());
}
+
+ // Change order of elements in the list so that the
+ // active user becomes the first element in the list
+ ignoreSelection = true;
+ try {
+ listModel.remove(list.getSelectedIndex());
+ listModel.add(0, selectedUser);
+
+ // Re-select the active user
+ list.setSelectedIndex(0);
+ } finally {
+ ignoreSelection = false;
+ }
}
}
}
@@ -150,7 +261,7 @@
}
private int initializeUsers() {
- Map<String, CredentialedUser> allUsers = GoogleLogin.getInstance().getAllUsers();
+ LinkedHashMap<String, CredentialedUser> allUsers = GoogleLogin.getInstance().getAllUsers();
listModel = new DefaultListModel();
int activeUserIndex = allUsers.size();
@@ -161,6 +272,17 @@
}
}
+ if(listModel.getSize() == 0) {
+ // Add no user panel
+ listModel.addElement(NoUsersListItem.INSTANCE);
+ } else if ((activeUserIndex != 0) && (activeUserIndex < listModel.getSize())) {
+ // Change order of elements in the list so that the
+ // active user becomes the first element in the list
+ UsersListItem activeUser = (UsersListItem)listModel.remove(activeUserIndex);
+ listModel.add(0, activeUser);
+ activeUserIndex = 0;
+ }
+
return activeUserIndex;
}
@@ -178,4 +300,16 @@
return size;
}
}
+
+ private boolean isActiveUserInVisibleArea() {
+ int max = listModel.getSize() < MAX_VISIBLE_ROW_COUNT ?
+ listModel.getSize() : MAX_VISIBLE_ROW_COUNT;
+
+ for(int i = 0; i < max; i++){
+ if(((UsersListItem)listModel.get(i)).isActiveUser()) {
+ return true;
+ }
+ }
+ return false;
+ }
}
diff --git a/login/src/com/google/gct/login/IGoogleLoginUpdateUser.java b/login/src/com/google/gct/login/ui/NoUsersListItem.java
similarity index 68%
copy from login/src/com/google/gct/login/IGoogleLoginUpdateUser.java
copy to login/src/com/google/gct/login/ui/NoUsersListItem.java
index 771081b..26b1427 100644
--- a/login/src/com/google/gct/login/IGoogleLoginUpdateUser.java
+++ b/login/src/com/google/gct/login/ui/NoUsersListItem.java
@@ -13,8 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.google.gct.login;
+package com.google.gct.login.ui;
-public interface IGoogleLoginUpdateUser {
- public void updateUser();
+/**
+ * A place holder for when no user exist. This allows us to create
+ * a customized panel when no users exist.
+ */
+public class NoUsersListItem {
+ public static NoUsersListItem INSTANCE = new NoUsersListItem();
+
+ private NoUsersListItem() {
+ }
}
diff --git a/login/src/com/google/gct/login/ui/UsersListCellRenderer.java b/login/src/com/google/gct/login/ui/UsersListCellRenderer.java
index d897609..e6fee90 100644
--- a/login/src/com/google/gct/login/ui/UsersListCellRenderer.java
+++ b/login/src/com/google/gct/login/ui/UsersListCellRenderer.java
@@ -16,10 +16,12 @@
package com.google.gct.login.ui;
import com.intellij.ui.JBColor;
-import com.intellij.ui.components.JBCheckBox;
import com.intellij.util.ui.UIUtil;
+import javax.swing.BorderFactory;
+import javax.swing.Box;
import javax.swing.BoxLayout;
+import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
@@ -29,95 +31,284 @@
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
+import java.awt.FlowLayout;
import java.awt.Font;
-import java.awt.Graphics;
+import java.awt.FontMetrics;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Image;
+import java.awt.Point;
+import java.awt.Toolkit;
+import java.net.URL;
+
/**
* A custom cell render for {@link GoogleLoginUsersPanel#list} that manages
* how each user item in the Google Login panel would be displayed.
*/
public class UsersListCellRenderer extends JComponent implements ListCellRenderer {
- private final Color ACTIVE_COLOR = JBColor.LIGHT_GRAY;
- private final Font PLAIN_NAME_FONT;
- private final Font PLAIN_EMAIL_FONT;
+ private final static String CLOUD_LABEL_TEXT = "Open Google Developers Console";
+ private final static String PLAY_LABEL_TEXT = "Open Play Developer Console";
+ private final static String DEFAULT_AVATAR = "/icons/[email protected]";
+ private final static String GOOGLE_IMG = "/icons/google.png";
+ private final static String SIGN_IN_TEXT = "<HTML> Sign in with your Google account to start <br> adding "
+ + "Cloud functionality to your <br> Android applications from Android Studio. </HTML>";
+ private final static String LEARN_MORE_TEXT = "Learn more";
+ private final Color LEARN_MORE_COLOR;
+ private final Color SIGN_IN_COLOR;
+ private final Color ACTIVE_COLOR;
+ private final Color INACTIVE_COLOR;
+ private final int PLAIN_USER_IMAGE_WIDTH = 48;
+ private final int PLAIN_USER_IMAGE_HEIGHT = 48;
+ private final int ACTIVE_USER_IMAGE_WIDTH = 96;
+ private final int ACTIVE_USER_IMAGE_HEIGHT = 96;
+ private final int GOOGLE_IMAGE_WIDTH = 96;
+ private final int GOOGLE_IMAGE_HEIGHT = 35;
+ private final int GOOGLE_IMAGE_NORTH = 18;
+ private final int GOOGLE_IMAGE_WEST = 18;
+ private final int WELCOME_LABEL_NORTH = 15;
+ private final int WELCOME_LABEL_SOUTH = 25;
+ private final int WELCOME_LABEL_EAST = 38;
+ private final int USER_LABEL_VERTICAL_STRUT = 3;
+ private final int HGAP = 10;
+ private final int VGAP = 10;
+ private final int GENERAL_FONT_HEIGHT;
+ private final Font NAME_FONT;
+ private final Font GENERAL_FONT;
private final Dimension MAIN_PANEL_DIMENSION;
+ private final Dimension ACTIVE_MAIN_PANEL_DIMENSION;
+ private final Dimension CLOUD_LABEL_DIMENSION;
+ private final Dimension PLAY_LABEL_DIMENSION;
+ private final Dimension LEARN_MORE_LABEL_DIMENSION;
public UsersListCellRenderer() {
- PLAIN_NAME_FONT = new Font("Helvetica", Font.BOLD, 13);
- PLAIN_EMAIL_FONT = new Font("Helvetica", Font.PLAIN, 13);;
+ NAME_FONT = new Font("Helvetica", Font.BOLD, 13);
+ GENERAL_FONT = new Font("Helvetica", Font.PLAIN, 13);
MAIN_PANEL_DIMENSION = new Dimension(250, 68);
+ ACTIVE_MAIN_PANEL_DIMENSION = new Dimension(250, 116);
+ SIGN_IN_COLOR = new Color(666666);
+ LEARN_MORE_COLOR = new Color(666);
+
+ ACTIVE_COLOR = new Color(0xffffff);
+ INACTIVE_COLOR = new Color(0xf5f5f5);
+
+ FontMetrics fontMetrics = getFontMetrics(GENERAL_FONT);
+ GENERAL_FONT_HEIGHT = fontMetrics.getHeight();
+ CLOUD_LABEL_DIMENSION = new Dimension(fontMetrics.stringWidth(CLOUD_LABEL_TEXT), GENERAL_FONT_HEIGHT);
+ PLAY_LABEL_DIMENSION = new Dimension(fontMetrics.stringWidth(PLAY_LABEL_TEXT), GENERAL_FONT_HEIGHT);
+ LEARN_MORE_LABEL_DIMENSION = new Dimension(fontMetrics.stringWidth(LEARN_MORE_TEXT), GENERAL_FONT_HEIGHT);
}
@Override
public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
+ if(value instanceof NoUsersListItem) {
+ return createNoUserDisplay();
+ }
+
if(!(value instanceof UsersListItem)) {
return null;
}
+ UsersListItem usersListItem = (UsersListItem)value;
- JPanel mainPanel = new JPanel();
- setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
- mainPanel.setPreferredSize(MAIN_PANEL_DIMENSION);
+ boolean calcIsSelected;
+ if (list.getSelectedIndex() == index) {
+ calcIsSelected = true;
+ } else {
+ calcIsSelected = false;
+ }
+
+ JPanel mainPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, HGAP, VGAP));
+ mainPanel.setMinimumSize(calcIsSelected ? ACTIVE_MAIN_PANEL_DIMENSION : MAIN_PANEL_DIMENSION);
mainPanel.setAlignmentX(LEFT_ALIGNMENT);
- // TODO: make mainPanel components left-justified
// Update colors
- final Color bg = isSelected ? ACTIVE_COLOR : UIUtil.getListBackground();
- final Color fg = isSelected ? UIUtil.getListSelectionForeground() : UIUtil.getListForeground();
+ final Color bg = calcIsSelected ? ACTIVE_COLOR : INACTIVE_COLOR;
+ final Color fg = calcIsSelected ? UIUtil.getListSelectionForeground() : UIUtil.getListForeground();
mainPanel.setBackground(bg);
mainPanel.setForeground(fg);
+ // TODO: add step to cache scaled image
+ Image image = usersListItem.getUserPicture();
+ if(image == null){
+ // use default image
+ URL url = UsersListCellRenderer.class.getResource(DEFAULT_AVATAR);
+ image = Toolkit.getDefaultToolkit().getImage(url);
+ }
- Image imageIcon = ((UsersListItem)value).getUserPicture();
- mainPanel.add(new ImagePanel(imageIcon));
- mainPanel.add(createTextDisplay(isSelected, (UsersListItem)value));
+ int imageWidth = calcIsSelected ? ACTIVE_USER_IMAGE_WIDTH : PLAIN_USER_IMAGE_WIDTH;
+ int imageHeight = calcIsSelected ? ACTIVE_USER_IMAGE_HEIGHT : PLAIN_USER_IMAGE_HEIGHT;
+ Image scaledImage = image.getScaledInstance(imageWidth, imageHeight, java.awt.Image.SCALE_SMOOTH);
- // TODO: add Separator to bottom of panel
+ JComponent textPanel;
+ if (calcIsSelected) {
+ textPanel = createActiveTextDisplay(usersListItem);
+ } else {
+ textPanel = createTextDisplay(calcIsSelected, usersListItem);
+ }
+
+ mainPanel.add(new JLabel(new ImageIcon(scaledImage)));
+ mainPanel.add(textPanel);
+ mainPanel.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, UIUtil.getBorderColor()));
return mainPanel;
}
- protected JComponent createTextDisplay(boolean isSelected, UsersListItem usersListItem) {
- final JPanel panel = new JPanel();
- panel.setLayout(new GridLayout(2,1));
+ public boolean inPlayConsoleUrl(Point point, int activeIndex) {
+ // 2 is for the number of labels before this one
+ double playYStart = VGAP + ACTIVE_USER_IMAGE_HEIGHT - PLAY_LABEL_DIMENSION.getHeight()
+ - CLOUD_LABEL_DIMENSION.getHeight() - 2 + (MAIN_PANEL_DIMENSION.getHeight() * activeIndex)
+ + USER_LABEL_VERTICAL_STRUT;
+ double playYEnd = playYStart + PLAY_LABEL_DIMENSION.getHeight();
+ double playXStart = ACTIVE_USER_IMAGE_WIDTH + HGAP + VGAP;
+ double playXEnd = playXStart + PLAY_LABEL_DIMENSION.getWidth();
- final Color bg = isSelected ? ACTIVE_COLOR : UIUtil.getListBackground();
+ if((point.getX() > playXStart) && (point.getX() < playXEnd)
+ && (point.getY() > playYStart) && (point.getY() < playYEnd)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ public boolean inCloudConsoleUrl(Point point, int activeIndex) {
+ // 3 is for the number of labels before this one
+ double playYStart = VGAP + ACTIVE_USER_IMAGE_HEIGHT - CLOUD_LABEL_DIMENSION.getHeight()
+ - 3 + (MAIN_PANEL_DIMENSION.getHeight() * activeIndex) + (USER_LABEL_VERTICAL_STRUT * 2);
+ double playYEnd = playYStart + CLOUD_LABEL_DIMENSION.getHeight();
+ double playXStart = ACTIVE_USER_IMAGE_WIDTH + HGAP + VGAP;
+ double playXEnd = playXStart + CLOUD_LABEL_DIMENSION.getWidth();
+
+ if((point.getX() > playXStart) && (point.getX() < playXEnd)
+ && (point.getY() > playYStart) && (point.getY() < playYEnd)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ public boolean inLearnMoreUrl(Point point) {
+ // 3 is for the number of labels and row of texts
+ double urlYStart = GOOGLE_IMAGE_NORTH + GOOGLE_IMAGE_HEIGHT + WELCOME_LABEL_NORTH
+ + (GENERAL_FONT_HEIGHT * 3) + 3;
+ double urlYEnd = urlYStart + LEARN_MORE_LABEL_DIMENSION.getHeight();
+ double urlXStart = GOOGLE_IMAGE_WEST;
+ double urlXEnd = urlXStart + LEARN_MORE_LABEL_DIMENSION.getWidth();
+
+ if((point.getX() > urlXStart) && (point.getX() < urlXEnd)
+ && (point.getY() > urlYStart) && (point.getY() < urlYEnd)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ public int getMainPanelHeight() {
+ return (int)MAIN_PANEL_DIMENSION.getHeight();
+ }
+
+ public int getActivePanelHeight() {
+ return (int)ACTIVE_MAIN_PANEL_DIMENSION.getHeight();
+ }
+
+ private JComponent createTextDisplay(boolean isSelected, UsersListItem usersListItem) {
+ final JPanel panel = new JPanel();
+ panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
+
+ final Color bg = isSelected ? ACTIVE_COLOR : INACTIVE_COLOR;
final Color fg = isSelected ? UIUtil.getListSelectionForeground() : UIUtil.getListForeground();
panel.setBackground(bg);
panel.setForeground(fg);
JLabel nameLabel = new JLabel( usersListItem.getUserName());
- nameLabel.setFont(PLAIN_NAME_FONT);
+ nameLabel.setFont(NAME_FONT);
+ nameLabel.setForeground(JBColor.BLACK);
panel.add(nameLabel);
+ panel.add(Box.createVerticalStrut(USER_LABEL_VERTICAL_STRUT));
JLabel emailLabel = new JLabel(usersListItem.getUserEmail());
- emailLabel.setFont(PLAIN_EMAIL_FONT);
+ emailLabel.setFont(GENERAL_FONT);
panel.add(emailLabel);
return panel;
}
- private class ImagePanel extends JPanel {
- private Image img;
- private final int IMAGE_STARTING_POINT_X = 10;
- private final int IMAGE_STARTING_POINT_Y = 10;
- private final Dimension PANEL_DIMENSION = new Dimension(68, 68);
- private final Dimension PLAIN_IMAGE_SIZE = new Dimension(48, 48);
+ private JComponent createActiveTextDisplay(UsersListItem usersListItem) {
+ JPanel mainPanel = new JPanel();
+ mainPanel.setLayout(new GridBagLayout());
- public ImagePanel(Image image) {
- img = image;
- setPreferredSize(PANEL_DIMENSION);
- setMinimumSize(PANEL_DIMENSION);
- setMaximumSize(PANEL_DIMENSION);
- setSize(PANEL_DIMENSION);
- setLayout(null);
- }
+ mainPanel.setBackground(ACTIVE_COLOR);
+ mainPanel.setForeground(UIUtil.getListSelectionForeground());
+ mainPanel.setPreferredSize(new Dimension(220, ACTIVE_USER_IMAGE_HEIGHT));
- @Override
- public void paintComponent(Graphics graphics) {
- graphics.drawImage(img, IMAGE_STARTING_POINT_X, IMAGE_STARTING_POINT_Y,
- PLAIN_IMAGE_SIZE.width, PLAIN_IMAGE_SIZE.height, null);
- }
+ JPanel bottomPanel = new JPanel();
+ bottomPanel.setLayout(new BoxLayout(bottomPanel, BoxLayout.PAGE_AXIS));
+ bottomPanel.setBackground(ACTIVE_COLOR);
+ bottomPanel.setForeground(UIUtil.getListSelectionForeground());
+ bottomPanel.setPreferredSize(new Dimension(220, (GENERAL_FONT_HEIGHT * 2) + USER_LABEL_VERTICAL_STRUT));
+
+ JLabel playLabel = new JLabel(PLAY_LABEL_TEXT);
+ playLabel.setFont(GENERAL_FONT);
+ playLabel.setForeground(JBColor.BLUE);
+ playLabel.setPreferredSize(PLAY_LABEL_DIMENSION);
+ bottomPanel.add(playLabel, BOTTOM_ALIGNMENT);
+ bottomPanel.add(Box.createVerticalStrut(USER_LABEL_VERTICAL_STRUT));
+
+ JLabel cloudLabel = new JLabel(CLOUD_LABEL_TEXT);
+ cloudLabel.setFont(GENERAL_FONT);
+ cloudLabel.setForeground(JBColor.BLUE);
+ cloudLabel.setPreferredSize(CLOUD_LABEL_DIMENSION);
+ bottomPanel.add(cloudLabel, BOTTOM_ALIGNMENT);
+
+ GridBagConstraints topConstraints = new GridBagConstraints();
+ topConstraints.gridx = 0;
+ topConstraints.gridy = 0;
+ topConstraints.anchor = GridBagConstraints.NORTHWEST;
+
+ GridBagConstraints bottomConstraints = new GridBagConstraints();
+ bottomConstraints.gridx = 0;
+ bottomConstraints.gridy = 1;
+ bottomConstraints.weightx = 1;
+ bottomConstraints.weighty = 5;
+ bottomConstraints.anchor = GridBagConstraints.SOUTHWEST;
+
+ JComponent topPanel = createTextDisplay(true, usersListItem);
+ mainPanel.add(topPanel, topConstraints);
+ mainPanel.add(bottomPanel, bottomConstraints);
+ return mainPanel;
+ }
+
+ private JPanel createNoUserDisplay() {
+ JPanel mainPanel = new JPanel();
+ BoxLayout layout = new BoxLayout(mainPanel, BoxLayout.Y_AXIS);
+ mainPanel.setLayout(layout);
+ mainPanel.setBackground(JBColor.WHITE);
+ mainPanel.setBorder(BorderFactory.createEmptyBorder(0, GOOGLE_IMAGE_WEST, 0, 0));
+
+ URL url = UsersListCellRenderer.class.getResource(GOOGLE_IMG);
+ Image image = Toolkit.getDefaultToolkit().getImage(url);
+ Image scaledImage = image.getScaledInstance(
+ GOOGLE_IMAGE_WIDTH, GOOGLE_IMAGE_HEIGHT, java.awt.Image.SCALE_SMOOTH);
+ JLabel imageLabel = new JLabel(new ImageIcon(scaledImage));
+
+ JLabel signInLabel = new JLabel(SIGN_IN_TEXT);
+ signInLabel.setFont(GENERAL_FONT);
+ signInLabel.setForeground(SIGN_IN_COLOR);
+ Dimension textSize = signInLabel.getPreferredSize();
+ signInLabel.setPreferredSize(new Dimension((int)textSize.getWidth() + WELCOME_LABEL_EAST, (int)textSize.getHeight()));
+
+ JLabel urlLabel = new JLabel(LEARN_MORE_TEXT);
+ urlLabel.setFont(GENERAL_FONT);
+ urlLabel.setForeground(LEARN_MORE_COLOR);
+ urlLabel.setPreferredSize(LEARN_MORE_LABEL_DIMENSION);
+
+ mainPanel.add(Box.createVerticalStrut(GOOGLE_IMAGE_NORTH));
+ mainPanel.add(imageLabel);
+ mainPanel.add(Box.createVerticalStrut(WELCOME_LABEL_NORTH));
+ mainPanel.add(signInLabel);
+ mainPanel.add(urlLabel);
+ mainPanel.add(Box.createVerticalStrut(WELCOME_LABEL_SOUTH));
+
+ return mainPanel;
}
}
diff --git a/resources/templates/GcmEndpoints/root/build.gradle.ftl b/resources/templates/GcmEndpoints/root/build.gradle.ftl
index cc39703..70ae1f9 100644
--- a/resources/templates/GcmEndpoints/root/build.gradle.ftl
+++ b/resources/templates/GcmEndpoints/root/build.gradle.ftl
@@ -1,6 +1,3 @@
-// Currently, the appengine gradle plugin's appengine devappserver launch doesn't interact well with Intellij/AndroidStudio's
-// Gradle integration. As a temporary solution, please launch from the command line.
-// ./gradlew modulename:appengineRun
// If you would like more information on the gradle-appengine-plugin please refer to the github page
// https://github.com/GoogleCloudPlatform/gradle-appengine-plugin
diff --git a/resources/templates/GcmEndpoints/root/src/main/MessagingEndpoint.java.ftl b/resources/templates/GcmEndpoints/root/src/main/MessagingEndpoint.java.ftl
index 05a161a..09e8cc1 100644
--- a/resources/templates/GcmEndpoints/root/src/main/MessagingEndpoint.java.ftl
+++ b/resources/templates/GcmEndpoints/root/src/main/MessagingEndpoint.java.ftl
@@ -1,3 +1,9 @@
+/*
+ For step-by-step instructions on connecting your Android application to this backend module,
+ see "App Engine Backend with Google Cloud Messaging" template documentation at
+ https://github.com/GoogleCloudPlatform/gradle-appengine-templates/tree/master/GcmEndpoints
+*/
+
package ${packageName};
import com.google.android.gcm.server.Constants;
diff --git a/resources/templates/GcmEndpoints/root/src/main/RegistrationEndpoint.java.ftl b/resources/templates/GcmEndpoints/root/src/main/RegistrationEndpoint.java.ftl
index 95fa77d..0f4d085 100644
--- a/resources/templates/GcmEndpoints/root/src/main/RegistrationEndpoint.java.ftl
+++ b/resources/templates/GcmEndpoints/root/src/main/RegistrationEndpoint.java.ftl
@@ -1,3 +1,9 @@
+/*
+ For step-by-step instructions on connecting your Android application to this backend module,
+ see "App Engine Backend with Google Cloud Messaging" template documentation at
+ https://github.com/GoogleCloudPlatform/gradle-appengine-templates/tree/master/GcmEndpoints
+*/
+
package ${packageName};
import com.google.api.server.spi.config.Api;
@@ -73,4 +79,4 @@
return ofy().load().type(RegistrationRecord.class).filter("regId", regId).first().now();
}
-}
\ No newline at end of file
+}
diff --git a/resources/templates/GcmEndpoints/root/src/webapp/index.html.ftl b/resources/templates/GcmEndpoints/root/src/webapp/index.html.ftl
index 7070937..21e9927 100644
--- a/resources/templates/GcmEndpoints/root/src/webapp/index.html.ftl
+++ b/resources/templates/GcmEndpoints/root/src/webapp/index.html.ftl
@@ -101,8 +101,13 @@
function init() {
var apiName = 'messaging'
var apiVersion = 'v1'
- // set the apiRoot to work on a deployed app and locally
- var apiRoot = '//' + window.location.host + '/_ah/api';
+ var apiRoot = 'https://' + window.location.host + '/_ah/api';
+ if (window.location.hostname == 'localhost'
+ || window.location.hostname == '127.0.0.1'
+ || ((window.location.port != "") && (window.location.port > 1023))) {
+ // We're probably running against the DevAppServer
+ apiRoot = 'http://' + window.location.host + '/_ah/api';
+ }
var callback = function() {
enableClick();
}
diff --git a/resources/templates/GcmEndpoints/template.xml b/resources/templates/GcmEndpoints/template.xml
index d853856..deeab15 100644
--- a/resources/templates/GcmEndpoints/template.xml
+++ b/resources/templates/GcmEndpoints/template.xml
@@ -27,7 +27,7 @@
<parameter id="endpointOwnerDomain" name="Endpoint Owner Domain" type="string" constraints="package" default="mycompany.com"/>
<parameter id="endpointPackagePath" name="Endpoint Package Path" type="string" constraints="package" default="myapp"/>
<parameter id="apiKey" name="Gcm Api Key" type="string" default="REPLACE WITH YOUR API KEY"/>
- <parameter id="appEngineVersion" name="AppEngine Version" type="string" default="1.9.6" />
+ <parameter id="appEngineVersion" name="AppEngine Version" type="string" default="1.9.8" />
<parameter id="docUrl" name="Doc URL" type="string" visibility="false"
default="https://github.com/GoogleCloudPlatform/gradle-appengine-templates/tree/master/GcmEndpoints"/>
<parameter id="sideIconPath" name="Side icon path" type="string" visibility="false"
diff --git a/resources/templates/HelloEndpoints/root/build.gradle.ftl b/resources/templates/HelloEndpoints/root/build.gradle.ftl
index cfa6468..f77265d 100644
--- a/resources/templates/HelloEndpoints/root/build.gradle.ftl
+++ b/resources/templates/HelloEndpoints/root/build.gradle.ftl
@@ -1,6 +1,3 @@
-// Currently, the appengine gradle plugin's appengine devappserver launch doesn't interact well with Intellij/AndroidStudio's
-// Gradle integration. As a temporary solution, please launch from the command line.
-// ./gradlew modulename:appengineRun
// If you would like more information on the gradle-appengine-plugin please refer to the github page
// https://github.com/GoogleCloudPlatform/gradle-appengine-plugin
diff --git a/resources/templates/HelloEndpoints/root/src/main/MyEndpoint.java.ftl b/resources/templates/HelloEndpoints/root/src/main/MyEndpoint.java.ftl
index 9a4c188..b420697 100644
--- a/resources/templates/HelloEndpoints/root/src/main/MyEndpoint.java.ftl
+++ b/resources/templates/HelloEndpoints/root/src/main/MyEndpoint.java.ftl
@@ -1,3 +1,9 @@
+/*
+ For step-by-step instructions on connecting your Android application to this backend module,
+ see "App Engine Java Endpoints Module" template documentation at
+ https://github.com/GoogleCloudPlatform/gradle-appengine-templates/tree/master/HelloEndpoints
+*/
+
package ${packageName};
import com.google.api.server.spi.config.Api;
@@ -19,4 +25,4 @@
return response;
}
-}
\ No newline at end of file
+}
diff --git a/resources/templates/HelloEndpoints/root/src/webapp/index.html.ftl b/resources/templates/HelloEndpoints/root/src/webapp/index.html.ftl
index a75f804..6116784 100644
--- a/resources/templates/HelloEndpoints/root/src/webapp/index.html.ftl
+++ b/resources/templates/HelloEndpoints/root/src/webapp/index.html.ftl
@@ -92,7 +92,13 @@
function init() {
var apiName = 'myApi';
var apiVersion = 'v1';
- var apiRoot = '//' + window.location.host + '/_ah/api';
+ var apiRoot = 'https://' + window.location.host + '/_ah/api';
+ if (window.location.hostname == 'localhost'
+ || window.location.hostname == '127.0.0.1'
+ || ((window.location.port != "") && (window.location.port > 1023))) {
+ // We're probably running against the DevAppServer
+ apiRoot = 'http://' + window.location.host + '/_ah/api';
+ }
var callback = function() {
enableClick();
}
diff --git a/resources/templates/HelloEndpoints/template.xml b/resources/templates/HelloEndpoints/template.xml
index 78ed749..e0b0ab1 100644
--- a/resources/templates/HelloEndpoints/template.xml
+++ b/resources/templates/HelloEndpoints/template.xml
@@ -26,7 +26,7 @@
<parameter id="appId" name="Application Id" type="string" constraints="nonempty" default="myApplicationId"/>
<parameter id="endpointOwnerDomain" name="Endpoint Owner Domain" type="string" constraints="package" default="mycompany.com"/>
<parameter id="endpointPackagePath" name="Endpoint Package Path" type="string" constraints="package" default="myapp"/>
- <parameter id="appEngineVersion" name="AppEngine Version" type="string" default="1.9.6" />
+ <parameter id="appEngineVersion" name="AppEngine Version" type="string" default="1.9.8" />
<parameter id="docUrl" name="Doc URL" type="string" visibility="false"
default="https://github.com/GoogleCloudPlatform/gradle-appengine-templates/tree/master/HelloEndpoints"/>
diff --git a/resources/templates/HelloWorld/root/build.gradle.ftl b/resources/templates/HelloWorld/root/build.gradle.ftl
index 8cb5faa..6d73c1b 100644
--- a/resources/templates/HelloWorld/root/build.gradle.ftl
+++ b/resources/templates/HelloWorld/root/build.gradle.ftl
@@ -1,6 +1,3 @@
-// Currently, the appengine gradle plugin's appengine devappserver launch doesn't interact well with Intellij/AndroidStudio's
-// Gradle integration. As a temporary solution, please launch from the command line.
-// ./gradlew modulename:appengineRun
// If you would like more information on the gradle-appengine-plugin please refer to the github page
// https://github.com/GoogleCloudPlatform/gradle-appengine-plugin
diff --git a/resources/templates/HelloWorld/root/src/main/MyServlet.java.ftl b/resources/templates/HelloWorld/root/src/main/MyServlet.java.ftl
index bf30f05..9f89f42 100644
--- a/resources/templates/HelloWorld/root/src/main/MyServlet.java.ftl
+++ b/resources/templates/HelloWorld/root/src/main/MyServlet.java.ftl
@@ -1,3 +1,9 @@
+/*
+ For step-by-step instructions on connecting your Android application to this backend module,
+ see "App Engine Java Servlet Module" template documentation at
+ https://github.com/GoogleCloudPlatform/gradle-appengine-templates/tree/master/HelloWorld
+*/
+
package ${packageName};
import java.io.IOException;
@@ -21,4 +27,4 @@
}
resp.getWriter().println("Hello " + name);
}
-}
\ No newline at end of file
+}
diff --git a/resources/templates/HelloWorld/template.xml b/resources/templates/HelloWorld/template.xml
index 8be72c9..6cada3e 100644
--- a/resources/templates/HelloWorld/template.xml
+++ b/resources/templates/HelloWorld/template.xml
@@ -24,7 +24,7 @@
<parameter id="packageName" name="Package name" type="string" constraints="package|nonempty" default="com.mycompany.myapp"/>
<parameter id="appId" name="Application Id" type="string" constraints="nonempty" default="myApplicationId"/>
- <parameter id="appEngineVersion" name="AppEngine Version" type="string" default="1.9.6" />
+ <parameter id="appEngineVersion" name="AppEngine Version" type="string" default="1.9.8" />
<parameter id="docUrl" name="Doc URL" type="string" visibility="false"
default="https://github.com/GoogleCloudPlatform/gradle-appengine-templates/tree/master/HelloWorld"/>
<parameter id="sideIconPath" name="Side icon path" type="string" visibility="false"
diff --git a/src/META-INF/plugin.xml b/src/META-INF/plugin.xml
index c428928..ecba450 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"
@@ -117,23 +123,6 @@
<reference id="GoogleCloudTools.GenerateEndpoint"/>
<add-to-group group-id="ToolsMenu" anchor="last"/>
</group>
-
- <!-- Google Login -->
- <!--
- <action id="GoogleLogin.UserProfile"
- class="com.google.gct.login.ui.GoogleLoginAction"
- text="Google Login">
- <add-to-group group-id="MainToolBar" anchor="first" />
- </action>
- -->
-
</actions>
- <!-- Extension point for Google Login Listener -->
- <!--
- <extensionPoints>
- <extensionPoint name="googleLoginListener" interface="com.google.gct.login.GoogleLoginListener"/>
- </extensionPoints>
- -->
-
</idea-plugin>
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/login/src/com/google/gct/login/IGoogleLoginUpdateUser.java b/src/com/google/gct/idea/appengine/dom/AppEngineWebApp.java
similarity index 64%
copy from login/src/com/google/gct/login/IGoogleLoginUpdateUser.java
copy to src/com/google/gct/idea/appengine/dom/AppEngineWebApp.java
index 771081b..ce3ba7d 100644
--- a/login/src/com/google/gct/login/IGoogleLoginUpdateUser.java
+++ b/src/com/google/gct/idea/appengine/dom/AppEngineWebApp.java
@@ -13,8 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.google.gct.login;
+package com.google.gct.idea.appengine.dom;
-public interface IGoogleLoginUpdateUser {
- public void updateUser();
+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/action/GenerateEndpointAction.java b/src/com/google/gct/idea/appengine/gradle/action/GenerateEndpointAction.java
index f442f43..7e236b2 100644
--- a/src/com/google/gct/idea/appengine/gradle/action/GenerateEndpointAction.java
+++ b/src/com/google/gct/idea/appengine/gradle/action/GenerateEndpointAction.java
@@ -83,6 +83,7 @@
try {
if (!AppEngineUtils.isAppEngineModule(project, module)) {
Messages.showErrorDialog(project, "Endpoints can only be generated for App Engine projects.", ERROR_MESSAGE_TITLE);
+ return;
}
}
catch (FileNotFoundException error) {
diff --git a/src/com/google/gct/idea/appengine/gradle/facet/AppEngineConfigurationProperties.java b/src/com/google/gct/idea/appengine/gradle/facet/AppEngineConfigurationProperties.java
new file mode 100644
index 0000000..9baee10
--- /dev/null
+++ b/src/com/google/gct/idea/appengine/gradle/facet/AppEngineConfigurationProperties.java
@@ -0,0 +1,36 @@
+/*
+ * 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.gradle.facet;
+
+import com.intellij.util.xmlb.annotations.AbstractCollection;
+import org.jetbrains.android.util.AndroidCommonUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Holds configuration property values for the AppEngine facet.
+ */
+public class AppEngineConfigurationProperties {
+ public String HTTP_ADDRESS;
+ public Integer HTTP_PORT;
+ @AbstractCollection(surroundWithTag = false, elementTag = "jvmflags", elementValueAttribute = "")
+ public List<String> JVM_FLAGS = new ArrayList<String>();
+ public String WAR_DIR;
+ public String WEB_APP_DIR;
+ public String APPENGINE_SDKROOT;
+}
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 08ef0cd..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,7 +15,8 @@
*/
package com.google.gct.idea.appengine.gradle.facet;
-import com.google.gct.idea.appengine.gradle.project.IdeaAppEngineProject;
+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;
@@ -23,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
*/
@@ -38,8 +47,6 @@
public static final FacetTypeId<AppEngineGradleFacet> TYPE_ID = new FacetTypeId<AppEngineGradleFacet>(ID);
- private IdeaAppEngineProject myIdeaAppEngineProject;
-
@Nullable
public static AppEngineGradleFacet getInstance(@NotNull Module module) {
return FacetManager.getInstance(module).getFacetByType(TYPE_ID);
@@ -53,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);
}
@@ -62,13 +93,4 @@
if (module == null) return null;
return FacetManager.getInstance(module).getFacetByType(TYPE_ID);
}
-
- public IdeaAppEngineProject getIdeaAppEngineProject() {
- return myIdeaAppEngineProject;
- }
-
- public void setIdeaAppEngineProject(IdeaAppEngineProject ideaAppEngineProject) {
- myIdeaAppEngineProject = ideaAppEngineProject;
- }
-
}
diff --git a/src/com/google/gct/idea/appengine/gradle/facet/AppEngineGradleFacetConfiguration.java b/src/com/google/gct/idea/appengine/gradle/facet/AppEngineGradleFacetConfiguration.java
index c5466e5..817393d 100644
--- a/src/com/google/gct/idea/appengine/gradle/facet/AppEngineGradleFacetConfiguration.java
+++ b/src/com/google/gct/idea/appengine/gradle/facet/AppEngineGradleFacetConfiguration.java
@@ -19,16 +19,19 @@
import com.intellij.facet.ui.FacetEditorContext;
import com.intellij.facet.ui.FacetEditorTab;
import com.intellij.facet.ui.FacetValidatorsManager;
+import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.util.InvalidDataException;
import com.intellij.openapi.util.WriteExternalException;
-import com.intellij.util.xmlb.XmlSerializer;
import org.jdom.Element;
+import org.jetbrains.annotations.Nullable;
/**
- * Currently an empty configuration for App Engine Gradle configurations
+ * A configuration for App Engine Gradle Facets that is populated during gradle project import
*/
-public class AppEngineGradleFacetConfiguration implements FacetConfiguration {
+public class AppEngineGradleFacetConfiguration implements FacetConfiguration, PersistentStateComponent<AppEngineConfigurationProperties> {
+ AppEngineConfigurationProperties myProperties = new AppEngineConfigurationProperties();
+
@Override
public FacetEditorTab[] createEditorTabs(FacetEditorContext editorContext, FacetValidatorsManager validatorsManager) {
return new FacetEditorTab[] {
@@ -38,12 +41,22 @@
@Override
public void readExternal(Element element) throws InvalidDataException {
- XmlSerializer.deserializeInto(this, element);
+ //Deprecated abstract method, using persistent state component now
}
@Override
public void writeExternal(Element element) throws WriteExternalException {
- XmlSerializer.serializeInto(this, element);
+ //Deprecated abstract method, using persistent state component now
}
+ @Nullable
+ @Override
+ public AppEngineConfigurationProperties getState() {
+ return myProperties;
+ }
+
+ @Override
+ public void loadState(AppEngineConfigurationProperties state) {
+ myProperties = state;
+ }
}
diff --git a/src/com/google/gct/idea/appengine/gradle/project/IdeaAppEngineProject.java b/src/com/google/gct/idea/appengine/gradle/project/IdeaAppEngineProject.java
index 97a0270..b54e7e7 100644
--- a/src/com/google/gct/idea/appengine/gradle/project/IdeaAppEngineProject.java
+++ b/src/com/google/gct/idea/appengine/gradle/project/IdeaAppEngineProject.java
@@ -24,7 +24,7 @@
import java.io.File;
/**
- * Project wrapper for App Engine Gradle Projects
+ * Transient project wrapper for App Engine Gradle Projects during gradle imports
*/
public class IdeaAppEngineProject {
@NotNull private final String myModuleName;
diff --git a/src/com/google/gct/idea/appengine/gradle/service/AppEngineGradleProjectDataService.java b/src/com/google/gct/idea/appengine/gradle/service/AppEngineGradleProjectDataService.java
index 36b25de..94e2ce9 100644
--- a/src/com/google/gct/idea/appengine/gradle/service/AppEngineGradleProjectDataService.java
+++ b/src/com/google/gct/idea/appengine/gradle/service/AppEngineGradleProjectDataService.java
@@ -99,7 +99,18 @@
model.commit();
}
}
- facet.setIdeaAppEngineProject(ideaAppEngineProject);
+
+ //deserialize state from ideaAppEngineProject into facet config.
+ if (facet != null) {
+ facet.getConfiguration().getState().APPENGINE_SDKROOT = ideaAppEngineProject.getDelegate().getAppEngineSdkRoot();
+ facet.getConfiguration().getState().HTTP_ADDRESS = ideaAppEngineProject.getDelegate().getHttpAddress();
+ facet.getConfiguration().getState().HTTP_PORT = ideaAppEngineProject.getDelegate().getHttpPort();
+ for (String flag : ideaAppEngineProject.getDelegate().getJvmFlags()) {
+ facet.getConfiguration().getState().JVM_FLAGS.add(flag);
+ }
+ facet.getConfiguration().getState().WAR_DIR = ideaAppEngineProject.getDelegate().getWarDir().getAbsolutePath();
+ facet.getConfiguration().getState().WEB_APP_DIR = ideaAppEngineProject.getDelegate().getWebAppDir().getAbsolutePath();
+ }
}
@NotNull
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";
+ }
+}
diff --git a/src/com/google/gct/idea/appengine/run/AppEngineRunConfiguration.java b/src/com/google/gct/idea/appengine/run/AppEngineRunConfiguration.java
index 763eb1c..62ecf53 100644
--- a/src/com/google/gct/idea/appengine/run/AppEngineRunConfiguration.java
+++ b/src/com/google/gct/idea/appengine/run/AppEngineRunConfiguration.java
@@ -15,6 +15,8 @@
*/
package com.google.gct.idea.appengine.run;
+import com.google.gct.idea.appengine.gradle.facet.AppEngineConfigurationProperties;
+import com.google.gct.idea.appengine.gradle.facet.AppEngineGradleFacet;
import com.google.gct.idea.appengine.sdk.AppEngineSdk;
import com.intellij.execution.ExecutionException;
@@ -59,11 +61,13 @@
private String myServerAddress = "";
private String mySdkPath = "";
private String myServerPort = "";
+ private Boolean mySyncWithGradle = false;
private String myVmArgs = "";
private String myWarPath = "";
private static final String KEY_SERVER_ADDRESS = "serverAddress";
private static final String KEY_SERVER_PORT = "serverPort";
+ private static final String KEY_SYNC = "sync";
private static final String KEY_SDK_PATH = "sdkPath";
private static final String KEY_VM_ARGS = "vmArgs";
private static final String KEY_WAR_PATH = "warPath";
@@ -100,6 +104,14 @@
this.myServerPort = serverPort;
}
+ public Boolean getSyncWithGradle() {
+ return mySyncWithGradle;
+ }
+
+ public void setSyncWithGradle(Boolean syncWithGradle) {
+ mySyncWithGradle = syncWithGradle;
+ }
+
public String getVmArgs() {
return myVmArgs;
}
@@ -108,6 +120,7 @@
this.myVmArgs = vmArgs;
}
+
public AppEngineRunConfiguration(String name, Project project, ConfigurationFactory factory) {
super(name, new JavaRunConfigurationModule(project, false), factory);
}
@@ -120,7 +133,7 @@
ArrayList<Module> res = new ArrayList<Module>();
for (Module module : modules) {
Facet[] facetList = FacetManager.getInstance(module).getAllFacets();
- for(Facet f : facetList) {
+ for (Facet f : facetList) {
if (f.getTypeId() == AppEngineGradleFacet.TYPE_ID) {
res.add(module);
break;
@@ -149,6 +162,23 @@
return state;
}
+ // Syncs a run configuration with information from build.gradle via the App Engine Gradle facet
+ protected void syncWithBuildFileViaFacet() {
+ Module module = getConfigurationModule().getModule();
+ if (module != null) {
+ AppEngineGradleFacet facet = AppEngineGradleFacet.getInstance(module);
+ if (facet != null) {
+ AppEngineConfigurationProperties model = facet.getConfiguration().getState();
+ if (model != null) {
+ myServerPort = model.HTTP_PORT.toString();
+ myServerAddress = model.HTTP_ADDRESS;
+ mySdkPath = model.APPENGINE_SDKROOT;
+ myWarPath = model.WAR_DIR;
+ }
+ }
+ }
+ }
+
@Override
public final void checkConfiguration() throws RuntimeConfigurationException {
JavaRunConfigurationModule configurationModule = getConfigurationModule();
@@ -158,11 +188,21 @@
if (module == null) {
return;
}
- /* this will be useful in future when we enable the appengine gradle facet
AppEngineGradleFacet facet = AppEngineGradleFacet.getAppEngineFacetByModule(module);
if (facet == null) {
- throw new RuntimeConfigurationError("No App-Engine Facet");
- }*/
+ throw new RuntimeConfigurationError(
+ "App Engine Gradle configuration not detected on module, maybe you need to Sync Project with Gradle");
+ }
+
+ if (mySyncWithGradle &&
+ (facet.getConfiguration().getState() == null || StringUtil.isEmpty(facet.getConfiguration().getState().WEB_APP_DIR))) {
+ throw new RuntimeConfigurationError(
+ "App Engine Gradle configuration does not appear to be loaded, please Sync Project with Gradle files before running");
+ }
+
+ if (mySyncWithGradle) {
+ syncWithBuildFileViaFacet();
+ }
if (mySdkPath == null || mySdkPath.trim().isEmpty() || !new AppEngineSdk(mySdkPath).canRunDevAppServer()) {
throw new RuntimeConfigurationError("Invalid App-Engine SDK Path");
@@ -255,6 +295,7 @@
myServerPort = StringUtil.notNullize(JDOMExternalizer.readString(element, KEY_SERVER_PORT));
myVmArgs = StringUtil.notNullize(JDOMExternalizer.readString(element, KEY_VM_ARGS));
myWarPath = StringUtil.notNullize(JDOMExternalizer.readString(element, KEY_WAR_PATH));
+ mySyncWithGradle = JDOMExternalizer.readBoolean(element, KEY_SYNC);
}
@Override
@@ -266,6 +307,7 @@
JDOMExternalizer.write(element, KEY_SERVER_PORT, myServerPort);
JDOMExternalizer.write(element, KEY_VM_ARGS, myVmArgs);
JDOMExternalizer.write(element, KEY_WAR_PATH, myWarPath);
+ JDOMExternalizer.write(element, KEY_SYNC, mySyncWithGradle);
PathMacroManager.getInstance(getProject()).collapsePathsRecursively(element);
}
}
diff --git a/src/com/google/gct/idea/appengine/run/AppEngineRunConfigurationSettingsEditor.form b/src/com/google/gct/idea/appengine/run/AppEngineRunConfigurationSettingsEditor.form
index aef286c..02fe9d7 100644
--- a/src/com/google/gct/idea/appengine/run/AppEngineRunConfigurationSettingsEditor.form
+++ b/src/com/google/gct/idea/appengine/run/AppEngineRunConfigurationSettingsEditor.form
@@ -1,16 +1,16 @@
<?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.run.AppEngineRunConfigurationSettingsEditor">
- <grid id="27dc6" binding="mainPanel" layout-manager="GridLayoutManager" row-count="6" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
+ <grid id="27dc6" binding="mainPanel" layout-manager="GridLayoutManager" row-count="8" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
- <xy x="17" y="25" width="500" height="169"/>
+ <xy x="17" y="25" width="500" height="285"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="78f2" class="javax.swing.JLabel">
<constraints>
- <grid row="2" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ <grid row="3" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="War Path"/>
@@ -18,7 +18,7 @@
</component>
<component id="80c76" class="javax.swing.JLabel">
<constraints>
- <grid row="5" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ <grid row="6" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Server Port"/>
@@ -26,7 +26,7 @@
</component>
<component id="84c27" class="javax.swing.JTextField" binding="myServerPortField">
<constraints>
- <grid row="5" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
+ <grid row="6" 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>
@@ -36,29 +36,15 @@
</component>
<component id="9770e" class="javax.swing.JLabel">
<constraints>
- <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ <grid row="2" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="App Engine SDK"/>
</properties>
</component>
- <component id="2305e" class="javax.swing.JLabel">
- <constraints>
- <grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
- </constraints>
- <properties>
- <text value="Module"/>
- </properties>
- </component>
- <component id="f369c" class="javax.swing.JComboBox" binding="myModuleComboBox">
- <constraints>
- <grid row="1" 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="4f254" class="javax.swing.JLabel">
<constraints>
- <grid row="3" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ <grid row="4" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="VM Args"/>
@@ -66,7 +52,7 @@
</component>
<component id="2344d" class="javax.swing.JTextField" binding="myVmArgsField">
<constraints>
- <grid row="3" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
+ <grid row="4" 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>
@@ -74,19 +60,19 @@
</component>
<component id="786dc" class="com.intellij.openapi.ui.TextFieldWithBrowseButton" binding="myAppEngineSdkField">
<constraints>
- <grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
+ <grid row="2" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
</component>
<component id="b37fb" class="com.intellij.openapi.ui.TextFieldWithBrowseButton" binding="myWarPathField">
<constraints>
- <grid row="2" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
+ <grid row="3" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
</component>
<component id="fa285" class="javax.swing.JTextField" binding="myServerAddressField">
<constraints>
- <grid row="4" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
+ <grid row="5" 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>
@@ -96,12 +82,39 @@
</component>
<component id="d19b0" class="javax.swing.JLabel">
<constraints>
- <grid row="4" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ <grid row="5" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Server Address"/>
</properties>
</component>
+ <component id="2305e" class="javax.swing.JLabel">
+ <constraints>
+ <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties>
+ <text value="Module"/>
+ </properties>
+ </component>
+ <vspacer id="5f8a6">
+ <constraints>
+ <grid row="7" column="1" 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="f369c" class="javax.swing.JComboBox" binding="myModuleComboBox">
+ <constraints>
+ <grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties/>
+ </component>
+ <component id="3e43f" class="javax.swing.JCheckBox" binding="mySynchronizeWithBuildGradleCheckBox" default-binding="true">
+ <constraints>
+ <grid row="1" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties>
+ <text value="Synchronize with build.gradle configuration"/>
+ </properties>
+ </component>
</children>
</grid>
</form>
diff --git a/src/com/google/gct/idea/appengine/run/AppEngineRunConfigurationSettingsEditor.java b/src/com/google/gct/idea/appengine/run/AppEngineRunConfigurationSettingsEditor.java
index b77872b..160c604 100644
--- a/src/com/google/gct/idea/appengine/run/AppEngineRunConfigurationSettingsEditor.java
+++ b/src/com/google/gct/idea/appengine/run/AppEngineRunConfigurationSettingsEditor.java
@@ -15,7 +15,7 @@
*/
package com.google.gct.idea.appengine.run;
-import com.google.appengine.gradle.model.AppEngineModel;
+import com.google.gct.idea.appengine.gradle.facet.AppEngineConfigurationProperties;
import com.google.gct.idea.appengine.gradle.facet.AppEngineGradleFacet;
import com.intellij.execution.ui.ConfigurationModuleSelector;
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory;
@@ -25,11 +25,13 @@
import com.intellij.openapi.ui.TextFieldWithBrowseButton;
import org.jetbrains.annotations.NotNull;
-import javax.swing.JButton;
+import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JTextField;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
/** GUI for configuring App Engine Run Configurations */
public class AppEngineRunConfigurationSettingsEditor extends SettingsEditor<AppEngineRunConfiguration> {
@@ -40,28 +42,57 @@
private TextFieldWithBrowseButton myWarPathField;
private TextFieldWithBrowseButton myAppEngineSdkField;
private JTextField myServerAddressField;
+ private JCheckBox mySynchronizeWithBuildGradleCheckBox;
private final Project myProject;
private final ConfigurationModuleSelector moduleSelector;
public AppEngineRunConfigurationSettingsEditor(Project project) {
- this.myProject = project;
+ myProject = project;
moduleSelector = new ConfigurationModuleSelector(project, myModuleComboBox);
+ myModuleComboBox.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ updateSync();
+ }
+ });
+ mySynchronizeWithBuildGradleCheckBox.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ updateSync();
+ }
+ });
myAppEngineSdkField.addBrowseFolderListener("Select App Engine Sdk Root", null, null,
FileChooserDescriptorFactory.createSingleFolderDescriptor());
myWarPathField.addBrowseFolderListener("Select Exploded War Root", null, myProject,
FileChooserDescriptorFactory.createSingleFolderDescriptor());
}
- // TODO: unused currently, but useful once gradle facet for App Engine is completely set up.
+ protected void updateSync() {
+ boolean isSync = mySynchronizeWithBuildGradleCheckBox.isSelected();
+ if (isSync) {
+ syncWithBuildFile();
+ }
+ myWarPathField.setEditable(!isSync);
+ myServerAddressField.setEditable(!isSync);
+ myServerPortField.setEditable(!isSync);
+ myAppEngineSdkField.setEditable(!isSync);
+ }
+
+ // Syncs a run configuration with information from build.gradle via the App Engine Gradle facet
+ // This is also a near-duplicate of the sync in AppEngineRunConfiguration, but this is required
+ // here to update the UI correctly when "sync" is checked and turned on, if we didn't reflect the
+ // configuration in the UI, we wouldn't need this.
protected void syncWithBuildFile() {
+
AppEngineGradleFacet facet = AppEngineGradleFacet.getInstance(moduleSelector.getModule());
- if(facet != null) {
- // proof of concept of usefulness of Gradle model
- AppEngineModel model = facet.getIdeaAppEngineProject().getDelegate();
- myServerPortField.setText(model.getHttpPort().toString());
- myServerAddressField.setText(model.getHttpAddress());
- myAppEngineSdkField.setText(model.getAppEngineSdkRoot());
- myWarPathField.setText(model.getWarDir().getAbsolutePath());
+ if (facet != null) {
+ AppEngineConfigurationProperties model = facet.getConfiguration().getState();
+ if (model != null) {
+ myServerPortField.setText(model.HTTP_PORT.toString());
+ myServerAddressField.setText(model.HTTP_ADDRESS);
+ myAppEngineSdkField.setText(model.APPENGINE_SDKROOT);
+ myWarPathField.setText(model.WAR_DIR);
+ }
}
}
@@ -77,6 +108,8 @@
myServerAddressField.setText(configuration.getServerAddress());
myVmArgsField.setText(configuration.getVmArgs());
moduleSelector.reset(configuration);
+ mySynchronizeWithBuildGradleCheckBox.setSelected(configuration.getSyncWithGradle());
+ updateSync();
}
@Override
@@ -87,6 +120,7 @@
configuration.setServerPort(myServerPortField.getText());
configuration.setVmArgs(myVmArgsField.getText());
configuration.setWarPath(myWarPathField.getText());
+ configuration.setSyncWithGradle(mySynchronizeWithBuildGradleCheckBox.isSelected());
}
@NotNull
diff --git a/src/com/google/gct/idea/appengine/wizard/NewAppEngineModuleAction.java b/src/com/google/gct/idea/appengine/wizard/NewAppEngineModuleAction.java
index 0d2874d..d93e6ea 100644
--- a/src/com/google/gct/idea/appengine/wizard/NewAppEngineModuleAction.java
+++ b/src/com/google/gct/idea/appengine/wizard/NewAppEngineModuleAction.java
@@ -116,7 +116,6 @@
}
ApplicationManager.getApplication().runWriteAction(new Runnable() {
-
@Override
public void run() {
List<File> allFilesToOpen = new ArrayList<File>();
@@ -131,23 +130,15 @@
TemplateUtils.openEditors(project, allFilesToOpen, true);
GradleProjectImporter projectImporter = GradleProjectImporter.getInstance();
- projectImporter.requestProjectSync(project, new GradleSyncListener() {
- @Override
- public void syncStarted(@NotNull Project project) {
- }
+ projectImporter.requestProjectSync(project, new GradleSyncListener.Adapter() {
@Override
- public void syncEnded(@NotNull final Project project) {
+ public void syncSucceeded(@NotNull final Project project) {
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
Module module = ModuleManager.getInstance(project).findModuleByName(moduleName);
-
- Parameter appEngineVersionParam = template.getMetadata().getParameter("appEngineVersion");
- String appEngineVersion = (appEngineVersionParam == null) ? "unknown" : appEngineVersionParam.initial;
-
- createRunConfiguration(project, module, moduleRoot, appEngineVersion);
- addAppEngineGradleFacet();
+ createRunConfiguration(project, module);
}
});
}
@@ -194,7 +185,7 @@
String backendModulePath = (String) replacementMap.get(TemplateMetadata.ATTR_PROJECT_OUT);
replacementMap.put(ATTR_SERVER_MODULE_PATH, FileUtil.getRelativePath(targetFolder.getPath(), backendModulePath, '/'));
replacementMap.put(TemplateMetadata.ATTR_PROJECT_OUT, targetFolder.getPath());
- List<SourceProvider> sourceProviders = IdeaSourceProvider.getSourceProvidersForFile(facet, targetFolder, facet.getMainSourceSet());
+ List<SourceProvider> sourceProviders = IdeaSourceProvider.getSourceProvidersForFile(facet, targetFolder, facet.getMainSourceProvider());
SourceProvider sourceProvider = sourceProviders.get(0);
File manifestDirectory = NewTemplateObjectWizard.findManifestDirectory(sourceProvider);
replacementMap.put(TemplateMetadata.ATTR_MANIFEST_DIR, manifestDirectory.getPath());
@@ -217,7 +208,7 @@
return null;
}
- private static void createRunConfiguration(Project project, Module module, File moduleRoot, String appEngineVersion) {
+ private static void createRunConfiguration(Project project, Module module) {
// Create a run configuration for this module
final RunManagerEx runManager = RunManagerEx.getInstanceEx(project);
final RunnerAndConfigurationSettings settings = runManager.
@@ -225,30 +216,9 @@
settings.setSingleton(true);
final AppEngineRunConfiguration configuration = (AppEngineRunConfiguration)settings.getConfiguration();
configuration.setModule(module);
- configuration.setWarPath(new File(moduleRoot, "build/exploded-app").getAbsolutePath());
- String gradleHomePath = GradleSettings.getInstance(project).getServiceDirectoryPath();
- if (StringUtil.isEmpty(gradleHomePath)) {
- gradleHomePath = new File(System.getProperty("user.home"), ".gradle").getAbsolutePath();
- }
- // This is a little strange because the sdk is "downloaded", but in our templates that's where the sdk is
- // TODO, perhaps extract this from the build.gradle
- // TODO, add support for the appengine environment/system properties (probably in the runconfig not here)
- configuration.setSdkPath(new File(gradleHomePath, "/appengine-sdk/appengine-java-sdk-" + appEngineVersion).getAbsolutePath());
- configuration.setServerPort("8080");
+ // pull configuration out of gradle
+ configuration.setSyncWithGradle(true);
runManager.addConfiguration(settings, false);
}
-
- private static void addAppEngineGradleFacet() {
- // Module does not have AppEngine-Gradle facet. Create one and add it.
- // Commented out for now, ENABLE when AppEngine Gradle facet is ready.
- // FacetManager facetManager = FacetManager.getInstance(module);
- // ModifiableFacetModel model = facetManager.createModifiableModel();
- //try {
- // Facet facet = facetManager.createFacet(AppEngineGradleFacet.getFacetType(), AppEngineGradleFacet.NAME, null);
- // model.addFacet(facet);
- //} finally {
- // model.commit();
- //}
- }
}
diff --git a/src/com/google/gct/idea/settings/ExportSettings.java b/src/com/google/gct/idea/settings/ExportSettings.java
new file mode 100644
index 0000000..6228f25
--- /dev/null
+++ b/src/com/google/gct/idea/settings/ExportSettings.java
@@ -0,0 +1,115 @@
+/*
+ * 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.settings;
+
+import com.intellij.ide.IdeBundle;
+import com.intellij.ide.plugins.IdeaPluginDescriptor;
+import com.intellij.ide.plugins.PluginManager;
+import com.intellij.ide.plugins.PluginManagerCore;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.application.PathManager;
+import com.intellij.openapi.components.ExportableApplicationComponent;
+import com.intellij.openapi.components.ExportableComponent;
+import com.intellij.openapi.components.ServiceBean;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.util.containers.ContainerUtil;
+import com.intellij.util.io.ZipUtil;
+
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.*;
+import java.util.jar.JarOutputStream;
+
+/**
+ * Provides functionality to export the IDEA settings into a specified jar
+ */
+public class ExportSettings {
+ // TODO: use ImportSettings.SETTINGS_JAR_MARKER after merge
+ public static final String SETTINGS_JAR_MARKER = "IntelliJ IDEA Global Settings";
+
+ public static void doExport(String path) {
+ final Set<ExportableComponent> exportableComponents =
+ new HashSet<ExportableComponent>(Arrays.asList(ApplicationManager.getApplication().getComponents(ExportableApplicationComponent.class)));
+ exportableComponents.addAll(ServiceBean.loadServicesFromBeans(ExportableComponent.EXTENSION_POINT, ExportableComponent.class));
+
+ if (exportableComponents.isEmpty()) {
+ return;
+ }
+
+ Set<File> exportFiles = new HashSet<File>();
+ for (final ExportableComponent markedComponent : exportableComponents) {
+ ContainerUtil.addAll(exportFiles, markedComponent.getExportFiles());
+ }
+
+ ApplicationManager.getApplication().saveSettings();
+
+ final File saveFile = new File(path);
+ try {
+ if (saveFile.exists()) {
+ final int ret = Messages
+ .showOkCancelDialog(IdeBundle.message("prompt.overwrite.settings.file", FileUtil.toSystemDependentName(saveFile.getPath())),
+ IdeBundle.message("title.file.already.exists"), Messages.getWarningIcon());
+ if (ret != Messages.OK) return;
+ }
+
+ final JarOutputStream output = new JarOutputStream(new FileOutputStream(saveFile));
+ try {
+ final File configPath = new File(PathManager.getConfigPath());
+ final HashSet<String> writtenItemRelativePaths = new HashSet<String>();
+ for (File file : exportFiles) {
+ final String rPath = FileUtil.getRelativePath(configPath, file);
+ assert rPath != null;
+ final String relativePath = FileUtil.toSystemIndependentName(rPath);
+ if (file.exists()) {
+ ZipUtil.addFileOrDirRecursively(output, saveFile, file, relativePath, null, writtenItemRelativePaths);
+ }
+ }
+
+ exportInstalledPlugins(saveFile, output, writtenItemRelativePaths);
+
+ final File magicFile = new File(FileUtil.getTempDirectory(), SETTINGS_JAR_MARKER);
+ FileUtil.createIfDoesntExist(magicFile);
+ magicFile.deleteOnExit();
+ ZipUtil.addFileToZip(output, magicFile, SETTINGS_JAR_MARKER, writtenItemRelativePaths, null);
+ }
+ finally {
+ output.close();
+ }
+ }
+ catch (IOException e1) {
+ Messages.showErrorDialog(IdeBundle.message("error.writing.settings", e1.toString()),IdeBundle.message("title.error.writing.file"));
+ }
+
+ }
+
+ private static void exportInstalledPlugins(File saveFile, JarOutputStream output, HashSet<String> writtenItemRelativePaths) throws IOException {
+ final List<String> oldPlugins = new ArrayList<String>();
+ for (IdeaPluginDescriptor descriptor : PluginManagerCore.getPlugins()) {
+ if (!descriptor.isBundled() && descriptor.isEnabled()) {
+ oldPlugins.add(descriptor.getPluginId().getIdString());
+ }
+ }
+ if (!oldPlugins.isEmpty()) {
+ final File tempFile = File.createTempFile("installed", "plugins");
+ tempFile.deleteOnExit();
+ PluginManagerCore.savePluginsList(oldPlugins, false, tempFile);
+ ZipUtil.addDirToZipRecursively(output, saveFile, tempFile, "/" + PluginManager.INSTALLED_TXT, null, writtenItemRelativePaths);
+ }
+ }
+}
diff --git a/src/com/google/gct/idea/settings/ImportSettings.java b/src/com/google/gct/idea/settings/ImportSettings.java
new file mode 100644
index 0000000..a509fc6
--- /dev/null
+++ b/src/com/google/gct/idea/settings/ImportSettings.java
@@ -0,0 +1,137 @@
+package com.google.gct.idea.settings;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.intellij.ide.IdeBundle;
+import com.intellij.ide.actions.ImportSettingsFilenameFilter;
+import com.intellij.ide.plugins.PluginManager;
+import com.intellij.ide.startup.StartupActionScriptManager;
+import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.application.ApplicationNamesInfo;
+import com.intellij.openapi.application.PathManager;
+import com.intellij.openapi.application.ex.ApplicationEx;
+import com.intellij.openapi.components.ExportableApplicationComponent;
+import com.intellij.openapi.components.ExportableComponent;
+import com.intellij.openapi.components.ServiceBean;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.updateSettings.impl.UpdateSettings;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.util.io.ZipUtil;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.*;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipException;
+import java.util.zip.ZipFile;
+
+/**
+ * Provides functionality to update IDEA setting from a jar containing new settings
+ */
+public class ImportSettings {
+ private static final String DIALOG_TITLE = "Setting Synchronization";
+ public static final String SETTINGS_JAR_MARKER = "IntelliJ IDEA Global Settings";
+
+ /**
+ * Parse and update the IDEA settings in the jar at <code>path</code>.
+ * Note: This function might require a restart of the application.
+ * @param path The location of the jar with the new IDEA settings.
+ */
+ public static void doImport(String path) {
+ final File saveFile = new File(path);
+ try {
+ if (!saveFile.exists()) {
+ Messages.showErrorDialog(IdeBundle.message("error.cannot.find.file", presentableFileName(saveFile)), DIALOG_TITLE);
+ return;
+ }
+
+ // What is this file used for?
+ final ZipEntry magicEntry = new ZipFile(saveFile).getEntry(SETTINGS_JAR_MARKER);
+ if (magicEntry == null) {
+ Messages.showErrorDialog("The file " + presentableFileName(saveFile) + " contains no settings to import",
+ DIALOG_TITLE);
+ return;
+ }
+
+ final ArrayList<ExportableComponent> registeredComponents = new ArrayList<ExportableComponent>(
+ Arrays.asList(ApplicationManager.getApplication().getComponents(ExportableApplicationComponent.class)));
+ registeredComponents.addAll(ServiceBean.loadServicesFromBeans(ExportableComponent.EXTENSION_POINT, ExportableComponent.class));
+
+ List<ExportableComponent> storedComponents = getComponentsStored(saveFile, registeredComponents);
+
+ Set<String> relativeNamesToExtract = new HashSet<String>();
+ for (final ExportableComponent aComponent : storedComponents) {
+ final File[] exportFiles = aComponent.getExportFiles();
+ for (File exportFile : exportFiles) {
+ final File configPath = new File(PathManager.getConfigPath());
+ final String rPath = FileUtil.getRelativePath(configPath, exportFile);
+ assert rPath != null;
+ final String relativePath = FileUtil.toSystemIndependentName(rPath);
+ relativeNamesToExtract.add(relativePath);
+ }
+ }
+
+ relativeNamesToExtract.add(PluginManager.INSTALLED_TXT);
+
+ final File tempFile = new File(PathManager.getPluginTempPath() + "/" + saveFile.getName());
+ FileUtil.copy(saveFile, tempFile);
+ File outDir = new File(PathManager.getConfigPath());
+ final ImportSettingsFilenameFilter filenameFilter = new ImportSettingsFilenameFilter(relativeNamesToExtract);
+ StartupActionScriptManager.ActionCommand unzip = new StartupActionScriptManager.UnzipCommand(tempFile, outDir, filenameFilter);
+ StartupActionScriptManager.addActionCommand(unzip);
+
+ // remove temp file
+ StartupActionScriptManager.ActionCommand deleteTemp = new StartupActionScriptManager.DeleteCommand(tempFile);
+ StartupActionScriptManager.addActionCommand(deleteTemp);
+
+ UpdateSettings.getInstance().forceCheckForUpdateAfterRestart();
+
+ String key = ApplicationManager.getApplication().isRestartCapable()
+ ? "message.settings.imported.successfully.restart"
+ : "message.settings.imported.successfully";
+ final int ret = Messages.showOkCancelDialog(IdeBundle.message(key,
+ ApplicationNamesInfo.getInstance().getProductName(),
+ ApplicationNamesInfo.getInstance().getFullProductName()),
+ IdeBundle.message("title.restart.needed"), Messages.getQuestionIcon());
+ if (ret == Messages.OK) {
+ ((ApplicationEx)ApplicationManager.getApplication()).restart(true);
+ }
+ }
+ catch (ZipException e1) {
+ Messages.showErrorDialog(
+ "Error reading file " + presentableFileName(saveFile) + ".\\nThere was " + e1.getMessage(),
+ DIALOG_TITLE);
+ }
+ catch (IOException e1) {
+ Messages.showErrorDialog(IdeBundle.message("error.reading.settings.file.2", presentableFileName(saveFile), e1.getMessage()),
+ DIALOG_TITLE);
+ }
+ }
+
+ private static String presentableFileName(final File file) {
+ return "'" + FileUtil.toSystemDependentName(file.getPath()) + "'";
+ }
+
+ private static List<ExportableComponent> getComponentsStored(File zipFile,
+ ArrayList<ExportableComponent> registeredComponents)
+ throws IOException {
+ final File configPath = new File(PathManager.getConfigPath());
+
+ final ArrayList<ExportableComponent> components = new ArrayList<ExportableComponent>();
+ for (ExportableComponent component : registeredComponents) {
+ final File[] exportFiles = component.getExportFiles();
+ for (File exportFile : exportFiles) {
+ final String rPath = FileUtil.getRelativePath(configPath, exportFile);
+ assert rPath != null;
+ String relativePath = FileUtil.toSystemIndependentName(rPath);
+ if (exportFile.isDirectory()) relativePath += "/";
+ if (ZipUtil.isZipContainsEntry(zipFile, relativePath)) {
+ components.add(component);
+ break;
+ }
+ }
+ }
+ return components;
+ }
+}