diff --git a/appengine/channel/pom.xml b/appengine/channel/pom.xml new file mode 100644 index 00000000000..f1d809c13b4 --- /dev/null +++ b/appengine/channel/pom.xml @@ -0,0 +1,111 @@ + + + 4.0.0 + war + 1.0-SNAPSHOT + com.example.appengine + appengine-channel + + com.google.cloud + doc-samples + 1.0.0 + ../.. + + + 5.1.5 + + + + 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 + 20160212 + + + com.googlecode.objectify + objectify + ${objectify.version} + + + + + junit + junit + 4.10 + 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.28 + test + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + + + com.google.appengine + appengine-maven-plugin + ${appengine.sdk.version} + + + + diff --git a/appengine/channel/src/main/java/com/example/appengine/channel/Game.java b/appengine/channel/src/main/java/com/example/appengine/channel/Game.java new file mode 100644 index 00000000000..c35ac6a2429 --- /dev/null +++ b/appengine/channel/src/main/java/com/example/appengine/channel/Game.java @@ -0,0 +1,178 @@ +/* + * 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.channel; + +import com.google.appengine.api.channel.ChannelMessage; +import com.google.appengine.api.channel.ChannelService; +import com.google.appengine.api.channel.ChannelServiceFactory; +import com.googlecode.objectify.annotation.Entity; +import com.googlecode.objectify.annotation.Id; +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +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; + private String userX; + private String userO; + private String board; + private Boolean moveX; + private String winner; + private String winningBoard; + + 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(); + } + + public String getChannelKey(String user) { + return user + id; + } + + private void sendUpdateToUser(String user) { + if (user != null) { + ChannelService channelService = ChannelServiceFactory.getChannelService(); + String channelKey = getChannelKey(user); + channelService.sendMessage(new ChannelMessage(channelKey, getMessageString())); + } + } + + public void sendUpdateToClients() { + sendUpdateToUser(userX); + sendUpdateToUser(userO); + } + + 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(); + } + } + } + + 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; + } +} diff --git a/appengine/channel/src/main/java/com/example/appengine/channel/GetTokenServlet.java b/appengine/channel/src/main/java/com/example/appengine/channel/GetTokenServlet.java new file mode 100644 index 00000000000..bc564b10b1f --- /dev/null +++ b/appengine/channel/src/main/java/com/example/appengine/channel/GetTokenServlet.java @@ -0,0 +1,50 @@ +/* + * 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.channel; + +import com.google.appengine.api.channel.ChannelService; +import com.google.appengine.api.channel.ChannelServiceFactory; +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 GetTokenServlet extends HttpServlet { + @Override + public void doPost(HttpServletRequest req, HttpServletResponse resp) + throws IOException { + UserService userService = UserServiceFactory.getUserService(); + String gameId = req.getParameter("gamekey"); + Objectify ofy = ObjectifyService.ofy(); + Game game = ofy.load().type(Game.class).id(gameId).safe(); + + String currentUserId = userService.getCurrentUser().getUserId(); + if (currentUserId.equals(game.getUserX()) || currentUserId.equals(game.getUserO())) { + String channelKey = game.getChannelKey(currentUserId); + ChannelService channelService = ChannelServiceFactory.getChannelService(); + resp.setContentType("text/plain"); + resp.getWriter().println(channelService.createChannel(channelKey)); + return; + } + resp.setStatus(HttpServletResponse.SC_FORBIDDEN); + } +} diff --git a/appengine/channel/src/main/java/com/example/appengine/channel/MoveServlet.java b/appengine/channel/src/main/java/com/example/appengine/channel/MoveServlet.java new file mode 100644 index 00000000000..160113842c7 --- /dev/null +++ b/appengine/channel/src/main/java/com/example/appengine/channel/MoveServlet.java @@ -0,0 +1,46 @@ +/* + * 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.channel; + +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 { + UserService userService = UserServiceFactory.getUserService(); + String gameId = req.getParameter("gamekey"); + int piece = new Integer(req.getParameter("i")); + Objectify ofy = ObjectifyService.ofy(); + Game game = ofy.load().type(Game.class).id(gameId).safe(); + + String currentUserId = userService.getCurrentUser().getUserId(); + if (!game.makeMove(piece, currentUserId)) { + resp.setStatus(HttpServletResponse.SC_FORBIDDEN); + } else { + ofy.save().entity(game).now(); + } + } +} diff --git a/appengine/channel/src/main/java/com/example/appengine/channel/OfyHelper.java b/appengine/channel/src/main/java/com/example/appengine/channel/OfyHelper.java new file mode 100644 index 00000000000..57993fe967f --- /dev/null +++ b/appengine/channel/src/main/java/com/example/appengine/channel/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.channel; + +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/channel/src/main/java/com/example/appengine/channel/OpenedServlet.java b/appengine/channel/src/main/java/com/example/appengine/channel/OpenedServlet.java new file mode 100644 index 00000000000..63106f4761c --- /dev/null +++ b/appengine/channel/src/main/java/com/example/appengine/channel/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.channel; + +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/channel/src/main/java/com/example/appengine/channel/TicTacToeServlet.java b/appengine/channel/src/main/java/com/example/appengine/channel/TicTacToeServlet.java new file mode 100644 index 00000000000..201d71c9910 --- /dev/null +++ b/appengine/channel/src/main/java/com/example/appengine/channel/TicTacToeServlet.java @@ -0,0 +1,97 @@ +/* + * 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.channel; + +import com.google.appengine.api.channel.ChannelService; +import com.google.appengine.api.channel.ChannelServiceFactory; +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 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 = ""; + } else { + query = "g=" + 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(); + final URI uriWithOptionalGameParam; + String gameKey = req.getParameter("g"); + if (userService.getCurrentUser() == null) { + resp.getWriter().println("

Please sign in.

"); + + return; + } + + 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(); + } + + ChannelService channelService = ChannelServiceFactory.getChannelService(); + String token = channelService.createChannel(game.getChannelKey(userId)); + + ofy.save().entity(game).now(); + + req.setAttribute("game_key", gameKey); + req.setAttribute("me", userId); + req.setAttribute("token", token); + req.setAttribute("initial_message", game.getMessageString()); + req.setAttribute("game_link", getGameUriWithGameParam(req, gameKey)); + getServletContext().getRequestDispatcher("/WEB-INF/view/index.jsp").forward(req, resp); + } +} diff --git a/appengine/channel/src/main/webapp/WEB-INF/appengine-web.xml b/appengine/channel/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..e4b0ccdddd7 --- /dev/null +++ b/appengine/channel/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,27 @@ + + + + osv-unikernel + 1 + true + + + + + + + diff --git a/appengine/channel/src/main/webapp/WEB-INF/logging.properties b/appengine/channel/src/main/webapp/WEB-INF/logging.properties new file mode 100644 index 00000000000..f36a2ec76e0 --- /dev/null +++ b/appengine/channel/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/channel/src/main/webapp/WEB-INF/view/index.jsp b/appengine/channel/src/main/webapp/WEB-INF/view/index.jsp new file mode 100644 index 00000000000..78e6ee6aa28 --- /dev/null +++ b/appengine/channel/src/main/webapp/WEB-INF/view/index.jsp @@ -0,0 +1,270 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%-- + Copyright 2015 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. +--%> + + + + + + + + +
+

Channel-based Tic Tac Toe

+ + + +
+ You won this game! +
+
+ You lost this game. +
+
+
+
+
+
+
+
+
+
+
+
+
+ Quick link to this game: <%= request.getAttribute("game_link") %> +
+
+ + diff --git a/appengine/channel/src/main/webapp/WEB-INF/web.xml b/appengine/channel/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..4e24e305c23 --- /dev/null +++ b/appengine/channel/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,57 @@ + + + + + TicTacToeServlet + com.example.appengine.channel.TicTacToeServlet + + + TicTacToeServlet + / + + + OpenedServlet + com.example.appengine.channel.OpenedServlet + + + OpenedServlet + /opened + + + MoveServlet + com.example.appengine.channel.MoveServlet + + + MoveServlet + /move + + + + ObjectifyFilter + com.googlecode.objectify.ObjectifyFilter + + + ObjectifyFilter + /* + + + com.example.appengine.channel.OfyHelper + + diff --git a/pom.xml b/pom.xml index cb181a14a9e..04cfd040bb7 100644 --- a/pom.xml +++ b/pom.xml @@ -45,6 +45,7 @@ appengine/analytics appengine/appidentity + appengine/channel appengine/datastore appengine/guestbook-objectify appengine/helloworld