From 187e8f70ac48a868455651d5c1d845279a3030d0 Mon Sep 17 00:00:00 2001 From: Jerjou Cheng Date: Mon, 10 Oct 2016 21:04:08 -0700 Subject: [PATCH] Add GAE standard + firebase tictactoe sample --- appengine/firebase-tictactoe/README.md | 54 ++++ appengine/firebase-tictactoe/pom.xml | 120 +++++++++ .../appengine/firetactoe/DeleteServlet.java | 42 ++++ .../example/appengine/firetactoe/Game.java | 231 ++++++++++++++++++ .../appengine/firetactoe/MoveServlet.java | 47 ++++ .../appengine/firetactoe/OfyHelper.java | 37 +++ .../appengine/firetactoe/OpenedServlet.java | 47 ++++ .../firetactoe/TicTacToeServlet.java | 101 ++++++++ .../src/main/webapp/WEB-INF/appengine-web.xml | 33 +++ .../main/webapp/WEB-INF/logging.properties | 27 ++ .../src/main/webapp/WEB-INF/view/index.jsp | 58 +++++ .../src/main/webapp/WEB-INF/web.xml | 68 ++++++ .../main/webapp/resources/credentials.json | 1 + .../webapp/resources/firebase_config.html | 3 + .../src/main/webapp/static/main.css | 82 +++++++ .../src/main/webapp/static/main.js | 174 +++++++++++++ 16 files changed, 1125 insertions(+) create mode 100644 appengine/firebase-tictactoe/README.md create mode 100644 appengine/firebase-tictactoe/pom.xml create mode 100644 appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/DeleteServlet.java create mode 100644 appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/Game.java create mode 100644 appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/MoveServlet.java create mode 100644 appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/OfyHelper.java create mode 100644 appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/OpenedServlet.java create mode 100644 appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/TicTacToeServlet.java create mode 100644 appengine/firebase-tictactoe/src/main/webapp/WEB-INF/appengine-web.xml create mode 100644 appengine/firebase-tictactoe/src/main/webapp/WEB-INF/logging.properties create mode 100644 appengine/firebase-tictactoe/src/main/webapp/WEB-INF/view/index.jsp create mode 100644 appengine/firebase-tictactoe/src/main/webapp/WEB-INF/web.xml create mode 120000 appengine/firebase-tictactoe/src/main/webapp/resources/credentials.json create mode 100644 appengine/firebase-tictactoe/src/main/webapp/resources/firebase_config.html create mode 100644 appengine/firebase-tictactoe/src/main/webapp/static/main.css create mode 100644 appengine/firebase-tictactoe/src/main/webapp/static/main.js diff --git a/appengine/firebase-tictactoe/README.md b/appengine/firebase-tictactoe/README.md new file mode 100644 index 00000000000..ebd08118ea7 --- /dev/null +++ b/appengine/firebase-tictactoe/README.md @@ -0,0 +1,54 @@ +# Tic Tac Toe on Google App Engine Standard using Firebase + +This directory contains a project that implements a realtime two-player game of +Tic Tac Toe on [Google App Engine] Standard, using the [Firebase] database +for realtime notifications when the board changes. + +[Firebase]: https://firebase.google.com +[Google App Engine]: https://cloud.google.com/appengine + +## Prerequisites + +* Install [Apache Maven][maven] +* Create a project in the [Firebase Console][fb-console] +* Install the [Google Cloud SDK][sdk] +* Download [service account credentials][creds] and move it to + `src/main/webapp/resources/credentials.json`. +* In the [Overview section][fb-overview] of the Firebase console, click 'Add + Firebase to your web app' and replace the contents of the file + `src/main/webapp/resources/firebase_config.html` with that code snippet. + +[fb-console]: https://console.firebase.google.com +[sdk]: https://cloud.google.com/sdk +[creds]: https://console.firebase.google.com/iam-admin/serviceaccounts/project?project=_&consoleReturnUrl=https:%2F%2Fconsole.firebase.google.com%2Fproject%2F_%2Fsettings%2Fgeneral%2F +[fb-overview]: https://console.firebase.google.com/project/_/overview + + +## Run the sample + +* To run the app locally: + + ```sh + $ mvn appengine:run + ``` + +## Troubleshooting + +* If you see the error `Google Cloud SDK path was not provided ...`: + * Make sure you've installed the [Google cloud SDK][sdk] + * You may have installed it in a non-standard path. In that case, set the + environment variable `GOOGLE_CLOUD_SDK_HOME` to point to where you + installed the SDK: + + ```sh + export GOOGLE_CLOUD_SDK_HOME=/path/to/google-cloud-sdk + ``` + +## Contributing changes + +See [CONTRIBUTING.md](../../CONTRIBUTING.md). + +## Licensing + +See [LICENSE](../../LICENSE). + diff --git a/appengine/firebase-tictactoe/pom.xml b/appengine/firebase-tictactoe/pom.xml new file mode 100644 index 00000000000..8dcaaeeb7ed --- /dev/null +++ b/appengine/firebase-tictactoe/pom.xml @@ -0,0 +1,120 @@ + + + 4.0.0 + war + 1.0-SNAPSHOT + com.example.appengine + appengine-firebase + + com.google.cloud + doc-samples + 1.0.0 + ../.. + + + 5.1.13 + + + + 3.3.9 + + + + + com.google.appengine + appengine-api-1.0-sdk + ${appengine.sdk.version} + + + javax.servlet + servlet-api + 2.5 + jar + provided + + + org.json + json + 20160810 + + + com.googlecode.objectify + objectify + ${objectify.version} + + + + + junit + junit + 4.12 + test + + + org.mockito + mockito-all + 1.10.19 + test + + + com.google.appengine + appengine-testing + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-api-stubs + ${appengine.sdk.version} + test + + + com.google.appengine + appengine-tools-sdk + ${appengine.sdk.version} + test + + + com.google.truth + truth + 0.30 + test + + + com.google.firebase + firebase-server-sdk + 3.0.1 + + + com.google.api-client + google-api-client-appengine + 1.22.0 + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + + com.google.cloud.tools + appengine-maven-plugin + 1.0.0 + + + + diff --git a/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/DeleteServlet.java b/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/DeleteServlet.java new file mode 100644 index 00000000000..2fcf13a5677 --- /dev/null +++ b/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/DeleteServlet.java @@ -0,0 +1,42 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * 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.example.appengine.firetactoe; + +import com.google.appengine.api.users.UserService; +import com.google.appengine.api.users.UserServiceFactory; +import com.googlecode.objectify.Objectify; +import com.googlecode.objectify.ObjectifyService; + +import java.io.IOException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class DeleteServlet extends HttpServlet { + @Override + public void doPost(HttpServletRequest req, HttpServletResponse resp) + throws IOException { + String gameId = req.getParameter("gameKey"); + Objectify ofy = ObjectifyService.ofy(); + Game game = ofy.load().type(Game.class).id(gameId).safe(); + + UserService userService = UserServiceFactory.getUserService(); + String currentUserId = userService.getCurrentUser().getUserId(); + + game.deleteChannel(currentUserId); + } +} diff --git a/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/Game.java b/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/Game.java new file mode 100644 index 00000000000..1ee48e5628c --- /dev/null +++ b/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/Game.java @@ -0,0 +1,231 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * 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.example.appengine.firetactoe; + +import com.google.common.io.CharStreams; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.database.DatabaseReference; +import com.google.firebase.database.FirebaseDatabase; +import com.googlecode.objectify.annotation.Entity; +import com.googlecode.objectify.annotation.Id; + +import org.json.JSONObject; + +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.lang.RuntimeException; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Pattern; + +@Entity +public class Game { + static final Pattern[] XWins = + {Pattern.compile("XXX......"), Pattern.compile("...XXX..."), Pattern.compile("......XXX"), + Pattern.compile("X..X..X.."), Pattern.compile(".X..X..X."), + Pattern.compile("..X..X..X"), Pattern.compile("X...X...X"), + Pattern.compile("..X.X.X..")}; + static final Pattern[] OWins = + {Pattern.compile("OOO......"), Pattern.compile("...OOO..."), Pattern.compile("......OOO"), + Pattern.compile("O..O..O.."), Pattern.compile(".O..O..O."), + Pattern.compile("..O..O..O"), Pattern.compile("O...O...O"), + Pattern.compile("..O.O.O..")}; + + @Id + public String id; + public String userX; + public String userO; + public String board; + public Boolean moveX; + public String winner; + public String winningBoard; + + private static final String SERVICE_ACCOUNT_PATH = "resources/credentials.json"; + private static final String FIREBASE_SNIPPET_PATH = "resources/firebase_config.html"; + private static String sFirebaseDbUrl; + protected static String sFirebaseSnippet; + private static final Logger LOGGER = Logger.getLogger(Game.class.getName()); + + static { + try { + sFirebaseSnippet = CharStreams.toString( + new InputStreamReader(new FileInputStream(FIREBASE_SNIPPET_PATH))); + sFirebaseDbUrl = parseFirebaseUrl(sFirebaseSnippet); + + FirebaseOptions options = new FirebaseOptions.Builder() + .setServiceAccount(new FileInputStream(SERVICE_ACCOUNT_PATH)) + .setDatabaseUrl(sFirebaseDbUrl) + .build(); + FirebaseApp.initializeApp(options); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Error reading credentials", e); + throw new RuntimeException(e); + } + } + + private static String parseFirebaseUrl(String firebaseSnippet) { + int idx = firebaseSnippet.indexOf("databaseURL"); + idx = firebaseSnippet.indexOf(':', idx); + int openQuote = firebaseSnippet.indexOf('"', idx); + int closeQuote = firebaseSnippet.indexOf('"', openQuote + 1); + return firebaseSnippet.substring(openQuote + 1, closeQuote); + } + + Game() { + } + + Game(String userX, String userO, String board, boolean moveX) { + this.id = UUID.randomUUID().toString(); + this.userX = userX; + this.userO = userO; + this.board = board; + this.moveX = moveX; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getUserX() { + return userX; + } + + public String getUserO() { + return userO; + } + + public void setUserO(String userO) { + this.userO = userO; + } + + public String getBoard() { + return board; + } + + public void setBoard(String board) { + this.board = board; + } + + public boolean getMoveX() { + return moveX; + } + + public void setMoveX(boolean moveX) { + this.moveX = moveX; + } + + public String getMessageString() { + Map state = new HashMap(); + state.put("userX", userX); + if (userO == null) { + state.put("userO", ""); + } else { + state.put("userO", userO); + } + state.put("board", board); + state.put("moveX", moveX.toString()); + state.put("winner", winner); + if (winner != null && winner != "") { + state.put("winningBoard", winningBoard); + } + JSONObject message = new JSONObject(state); + return message.toString(); + } + + //[START send_updates] + public String getChannelKey(String user) { + return user + id; + } + + public void deleteChannel(String user) { + if (user != null) { + String channelKey = getChannelKey(user); + final FirebaseDatabase database = FirebaseDatabase.getInstance(); + DatabaseReference ref = database.getReference("channels"); + ref.child(channelKey).setValue(null); + } + } + + private void sendUpdateToUser(String user) { + if (user != null) { + String channelKey = getChannelKey(user); + final FirebaseDatabase database = FirebaseDatabase.getInstance(); + DatabaseReference ref = database.getReference("channels"); + ref.child(channelKey).setValue(this); + } + } + + public void sendUpdateToClients() { + sendUpdateToUser(userX); + sendUpdateToUser(userO); + } + //[END send_updates] + + public void checkWin() { + final Pattern[] wins; + if (moveX) { + wins = XWins; + } else { + wins = OWins; + } + + for (Pattern winPattern : wins) { + if (winPattern.matcher(board).matches()) { + if (moveX) { + winner = userX; + } else { + winner = userO; + } + winningBoard = winPattern.toString(); + } + } + } + + //[START make_move] + public boolean makeMove(int position, String user) { + String currentMovePlayer; + char value; + if (getMoveX()) { + value = 'X'; + currentMovePlayer = getUserX(); + } else { + value = 'O'; + currentMovePlayer = getUserO(); + } + + if (currentMovePlayer.equals(user)) { + char[] boardBytes = getBoard().toCharArray(); + boardBytes[position] = value; + setBoard(new String(boardBytes)); + checkWin(); + setMoveX(!getMoveX()); + sendUpdateToClients(); + return true; + } + + return false; + } + //[END make_move] +} diff --git a/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/MoveServlet.java b/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/MoveServlet.java new file mode 100644 index 00000000000..c2cfa17d77e --- /dev/null +++ b/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/MoveServlet.java @@ -0,0 +1,47 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * 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.example.appengine.firetactoe; + +import com.google.appengine.api.users.UserService; +import com.google.appengine.api.users.UserServiceFactory; +import com.googlecode.objectify.Objectify; +import com.googlecode.objectify.ObjectifyService; + +import java.io.IOException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class MoveServlet extends HttpServlet { + @Override + public void doPost(HttpServletRequest req, HttpServletResponse resp) + throws IOException { + String gameId = req.getParameter("gameKey"); + Objectify ofy = ObjectifyService.ofy(); + Game game = ofy.load().type(Game.class).id(gameId).safe(); + + UserService userService = UserServiceFactory.getUserService(); + String currentUserId = userService.getCurrentUser().getUserId(); + + int cell = new Integer(req.getParameter("cell")); + if (!game.makeMove(cell, currentUserId)) { + resp.setStatus(HttpServletResponse.SC_FORBIDDEN); + } else { + ofy.save().entity(game).now(); + } + } +} diff --git a/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/OfyHelper.java b/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/OfyHelper.java new file mode 100644 index 00000000000..35958c4f029 --- /dev/null +++ b/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/OfyHelper.java @@ -0,0 +1,37 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * 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.example.appengine.firetactoe; + +import com.googlecode.objectify.ObjectifyService; + +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; + +/** + * OfyHelper, a ServletContextListener, is setup in web.xml to run before a JSP is run. This is + * required to let JSP's access Ofy. + **/ +public class OfyHelper implements ServletContextListener { + public void contextInitialized(ServletContextEvent event) { + // This will be invoked as part of a warmup request, or the first user request if no warmup + // request. + ObjectifyService.register(Game.class); + } + + public void contextDestroyed(ServletContextEvent event) { + // App Engine does not currently invoke this method. + } +} diff --git a/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/OpenedServlet.java b/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/OpenedServlet.java new file mode 100644 index 00000000000..99965bfb1c3 --- /dev/null +++ b/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/OpenedServlet.java @@ -0,0 +1,47 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * 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.example.appengine.firetactoe; + +import com.googlecode.objectify.NotFoundException; +import com.googlecode.objectify.Objectify; +import com.googlecode.objectify.ObjectifyService; + +import java.io.IOException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class OpenedServlet extends HttpServlet { + @Override + public void doPost(HttpServletRequest req, HttpServletResponse resp) + throws IOException { + String gameId = req.getParameter("gameKey"); + Objectify ofy = ObjectifyService.ofy(); + try { + Game game = ofy.load().type(Game.class).id(gameId).safe(); + if (gameId != null && req.getUserPrincipal() != null) { + game.sendUpdateToClients(); + resp.setContentType("text/plain"); + resp.getWriter().println("ok"); + } else { + resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + } catch (NotFoundException e) { + resp.setStatus(HttpServletResponse.SC_NOT_FOUND); + } + } +} diff --git a/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/TicTacToeServlet.java b/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/TicTacToeServlet.java new file mode 100644 index 00000000000..3ccd2c83624 --- /dev/null +++ b/appengine/firebase-tictactoe/src/main/java/com/example/appengine/firetactoe/TicTacToeServlet.java @@ -0,0 +1,101 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * 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.example.appengine.firetactoe; + +import com.google.appengine.api.users.UserService; +import com.google.appengine.api.users.UserServiceFactory; +import com.google.firebase.auth.FirebaseAuth; +import com.googlecode.objectify.Objectify; +import com.googlecode.objectify.ObjectifyService; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@SuppressWarnings("serial") +public class TicTacToeServlet extends HttpServlet { + + private String getGameUriWithGameParam(HttpServletRequest req, String gameKey) + throws IOException { + try { + String query = ""; + if (gameKey != null) { + query = "gameKey=" + gameKey; + } + URI thisUri = new URI(req.getRequestURL().toString()); + URI uriWithOptionalGameParam = new URI( + thisUri.getScheme(), thisUri.getUserInfo(), thisUri.getHost(), + thisUri.getPort(), thisUri.getPath(), query, ""); + return uriWithOptionalGameParam.toString(); + } catch (URISyntaxException e) { + throw new IOException(e.getMessage(), e); + } + + } + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + final UserService userService = UserServiceFactory.getUserService(); + String gameKey = req.getParameter("gameKey"); + if (userService.getCurrentUser() == null) { + resp.getWriter().println("

Please sign in.

"); + return; + } + + // 1. Create or fetch a Game object from the datastore + Objectify ofy = ObjectifyService.ofy(); + Game game = null; + String userId = userService.getCurrentUser().getUserId(); + if (gameKey != null) { + game = ofy.load().type(Game.class).id(gameKey).safe(); + if (game.getUserO() == null && !userId.equals(game.getUserX())) { + game.setUserO(userId); + } + ofy.save().entity(game).now(); + } else { + game = new Game(userId, null, " ", true); + ofy.save().entity(game).now(); + gameKey = game.getId(); + } + + // 2. Create this Game in the firebase db + game.sendUpdateToClients(); + + // 3. Inject a secure token into the client, so it can get game updates + + // The 'Game' object exposes a method which creates a unique string based on the game's key + // and the user's id. + String channelId = game.getChannelKey(userId); + String token = FirebaseAuth.getInstance().createCustomToken(channelId); + req.setAttribute("token", token); + + // 4. More general template values + req.setAttribute("game_key", gameKey); + req.setAttribute("me", userId); + req.setAttribute("channel_id", channelId); + req.setAttribute("initial_message", game.getMessageString()); + req.setAttribute("game_link", getGameUriWithGameParam(req, gameKey)); + req.setAttribute("firebase_snippet", game.sFirebaseSnippet); + getServletContext().getRequestDispatcher("/WEB-INF/view/index.jsp").forward(req, resp); + } +} diff --git a/appengine/firebase-tictactoe/src/main/webapp/WEB-INF/appengine-web.xml b/appengine/firebase-tictactoe/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..ce85bef336d --- /dev/null +++ b/appengine/firebase-tictactoe/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,33 @@ + + + + true + + + + + + + + + + + + + + + diff --git a/appengine/firebase-tictactoe/src/main/webapp/WEB-INF/logging.properties b/appengine/firebase-tictactoe/src/main/webapp/WEB-INF/logging.properties new file mode 100644 index 00000000000..f36a2ec76e0 --- /dev/null +++ b/appengine/firebase-tictactoe/src/main/webapp/WEB-INF/logging.properties @@ -0,0 +1,27 @@ +# +# Copyright 2016 Google Inc. All Rights Reserved. +# +# 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. +# +# A default java.util.logging configuration. +# (All App Engine logging is through java.util.logging by default). +# +# To use this configuration, copy it into your application's WEB-INF +# folder and add the following to your appengine-web.xml: +# +# +# +# +# +# Set the default logging level for all loggers to WARNING +.level=WARNING diff --git a/appengine/firebase-tictactoe/src/main/webapp/WEB-INF/view/index.jsp b/appengine/firebase-tictactoe/src/main/webapp/WEB-INF/view/index.jsp new file mode 100644 index 00000000000..db068acf249 --- /dev/null +++ b/appengine/firebase-tictactoe/src/main/webapp/WEB-INF/view/index.jsp @@ -0,0 +1,58 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%-- + Copyright 2016 Google Inc. All Rights Reserved. + + 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. +--%> + + + + <%= request.getAttribute("firebase_snippet") %> + + + + + + +
+

Firebase-enabled Tic Tac Toe

+
+ Waiting for another player to join.
+ Send them this link to play:
+ +
+
Your move! Click a square to place your piece.
+
Waiting for other player to move...
+
You won this game!
+
You lost this game.
+
+
+
+
+
+
+
+
+
+
+
+
+ Quick link to this game: <%= request.getAttribute("game_link") %> +
+
+ + diff --git a/appengine/firebase-tictactoe/src/main/webapp/WEB-INF/web.xml b/appengine/firebase-tictactoe/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..eac360d52ce --- /dev/null +++ b/appengine/firebase-tictactoe/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,68 @@ + + + + + index + + + TicTacToeServlet + com.example.appengine.firetactoe.TicTacToeServlet + + + TicTacToeServlet + /index + + + OpenedServlet + com.example.appengine.firetactoe.OpenedServlet + + + OpenedServlet + /opened + + + MoveServlet + com.example.appengine.firetactoe.MoveServlet + + + MoveServlet + /move + + + DeleteServlet + com.example.appengine.firetactoe.DeleteServlet + + + DeleteServlet + /delete + + + + ObjectifyFilter + com.googlecode.objectify.ObjectifyFilter + + + ObjectifyFilter + /* + + + com.example.appengine.firetactoe.OfyHelper + + diff --git a/appengine/firebase-tictactoe/src/main/webapp/resources/credentials.json b/appengine/firebase-tictactoe/src/main/webapp/resources/credentials.json new file mode 120000 index 00000000000..c13905c54dc --- /dev/null +++ b/appengine/firebase-tictactoe/src/main/webapp/resources/credentials.json @@ -0,0 +1 @@ +/usr/local/google/home/jerjou/opensrc/channels-api-deprecation-1e958c196e99.json \ No newline at end of file diff --git a/appengine/firebase-tictactoe/src/main/webapp/resources/firebase_config.html b/appengine/firebase-tictactoe/src/main/webapp/resources/firebase_config.html new file mode 100644 index 00000000000..25898c985af --- /dev/null +++ b/appengine/firebase-tictactoe/src/main/webapp/resources/firebase_config.html @@ -0,0 +1,3 @@ +REPLACE ME WITH YOUR FIREBASE WEBAPP CODE SNIPPET: + +https://console.firebase.google.com/project/_/overview diff --git a/appengine/firebase-tictactoe/src/main/webapp/static/main.css b/appengine/firebase-tictactoe/src/main/webapp/static/main.css new file mode 100644 index 00000000000..f314eab5b37 --- /dev/null +++ b/appengine/firebase-tictactoe/src/main/webapp/static/main.css @@ -0,0 +1,82 @@ +body { + font-family: 'Helvetica'; +} + +#board { + width:152px; + height: 152px; + margin: 20px auto; +} + +#display-area { + text-align: center; +} + +#other-player, #your-move, #their-move, #you-won, #you-lost { + display: none; +} + +#display-area.waiting #other-player { + display: block; +} + +#display-area.waiting #board, #display-area.waiting #this-game { + display: none; +} +#display-area.won #you-won { + display: block; +} +#display-area.lost #you-lost { + display: block; +} +#display-area.your-move #your-move { + display: block; +} +#display-area.their-move #their-move { + display: block; +} + + +#this-game { + font-size: 9pt; +} + +div.cell { + float: left; + width: 50px; + height: 50px; + border: none; + margin: 0px; + padding: 0px; + box-sizing: border-box; + + line-height: 50px; + font-family: "Helvetica"; + font-size: 16pt; + text-align: center; +} + +.your-move div.cell:hover { + background: lightgrey; +} + +.your-move div.cell:empty:hover { + background: lightblue; + cursor: pointer; +} + +div.l { + border-right: 1pt solid black; +} + +div.r { + border-left: 1pt solid black; +} + +div.t { + border-bottom: 1pt solid black; +} + +div.b { + border-top: 1pt solid black; +} diff --git a/appengine/firebase-tictactoe/src/main/webapp/static/main.js b/appengine/firebase-tictactoe/src/main/webapp/static/main.js new file mode 100644 index 00000000000..804b74f86be --- /dev/null +++ b/appengine/firebase-tictactoe/src/main/webapp/static/main.js @@ -0,0 +1,174 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * 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. + */ + +'use strict'; + +/** + * @fileoverview Tic-Tac-Toe, using the Firebase API + */ + +/** + * @param gameKey - a unique key for this game. + * @param me - my user id. + * @param token - secure token passed from the server + * @param channelId - id of the 'channel' we'll be listening to + */ +function initGame(gameKey, me, token, channelId, initialMessage) { + var state = { + gameKey: gameKey, + me: me + }; + + // This is our Firebase realtime DB path that we'll listen to for updates + // We'll initialize this later in openChannel() + var channel = null; + + /** + * Updates the displayed game board. + */ + function updateGame(newState) { + $.extend(state, newState); + + $('.cell').each(function(i) { + var square = $(this); + var value = state.board[i]; + square.html(' ' === value ? '' : value); + + if (state.winner && state.winningBoard) { + if (state.winningBoard[i] === value) { + if (state.winner === state.me) { + square.css('background', 'green'); + } else { + square.css('background', 'red'); + } + } else { + square.css('background', ''); + } + } + }); + + var displayArea = $('#display-area'); + + if (!state.userO) { + displayArea[0].className = 'waiting'; + } else if (state.winner === state.me) { + displayArea[0].className = 'won'; + } else if (state.winner) { + displayArea[0].className = 'lost'; + } else if (isMyMove()) { + displayArea[0].className = 'your-move'; + } else { + displayArea[0].className = 'their-move'; + } + } + + function isMyMove() { + return !state.winner && (state.moveX === (state.userX === state.me)); + } + + function myPiece() { + return state.userX === state.me ? 'X' : 'O'; + } + + /** + * Send the user's latest move back to the server + */ + function moveInSquare(e) { + var id = $(e.currentTarget).index(); + if (isMyMove() && state.board[id] === ' ') { + $.post('/move', {cell: id}); + } + } + + /** + * This method lets the server know that the user has opened the channel + * After this method is called, the server may begin to send updates + */ + function onOpened() { + $.post('/opened'); + } + + /** + * This deletes the data associated with the Firebase path + * it is critical that this data be deleted since it costs money + */ + function deleteChannel() { + $.post('/delete'); + } + + /** + * This method is called every time an event is fired from Firebase + * it updates the entire game state and checks for a winner + * if a player has won the game, this function calls the server to delete + * the data stored in Firebase + */ + function onMessage(newState) { + updateGame(newState); + + // now check to see if there is a winner + if (channel && state.winner && state.winningBoard) { + channel.off(); //stop listening on this path + deleteChannel(); //delete the data we wrote + } + } + + /** + * This function opens a realtime communication channel with Firebase + * It logs in securely using the client token passed from the server + * then it sets up a listener on the proper database path (also passed by server) + * finally, it calls onOpened() to let the server know it is ready to receive messages + */ + function openChannel() { + // sign into Firebase with the token passed from the server + firebase.auth().signInWithCustomToken(token).catch(function(error) { + console.log('Login Failed!', error.code); + console.log('Error message: ', error.message); + }); + + // setup a database reference at path /channels/channelId + channel = firebase.database().ref('channels/' + channelId); + // add a listener to the path that fires any time the value of the data changes + channel.on('value', function(data) { + onMessage(data.val()); + }); + onOpened(); + // let the server know that the channel is open + } + + /** + * This function opens a communication channel with the server + * then it adds listeners to all the squares on the board + * next it pulls down the initial game state from template values + * finally it updates the game state with those values by calling onMessage() + */ + function initialize() { + // Always include the gamekey in our requests + $.ajaxPrefilter(function(opts) { + if (opts.url.indexOf('?') > 0) + opts.url += '&gameKey=' + state.gameKey; + else + opts.url += '?gameKey=' + state.gameKey; + }); + + $('#board').on('click', '.cell', moveInSquare); + + openChannel(); + + onMessage(initialMessage); + } + + setTimeout(initialize, 100); +}