2 * Quoridor server and related libraries
3 * Copyright (C) 2009-2010 Jaroslav Tulach <jaroslav.tulach@apidesign.org>
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. Look for COPYING file in the top folder.
16 * If not, see http://www.gnu.org/licenses/.
19 package cz.xelfi.quoridor.webidor.resources;
21 import com.sun.jersey.api.json.JSONWithPadding;
22 import cz.xelfi.quoridor.IllegalPositionException;
23 import cz.xelfi.quoridor.webidor.*;
24 import cz.xelfi.quoridor.Move;
25 import java.io.BufferedReader;
26 import java.io.BufferedWriter;
28 import java.io.FileInputStream;
29 import java.io.FileOutputStream;
30 import java.io.IOException;
31 import java.io.InputStream;
32 import java.io.InputStreamReader;
33 import java.io.OutputStreamWriter;
34 import java.io.PrintWriter;
35 import java.io.Writer;
36 import java.util.ArrayList;
37 import java.util.Collections;
38 import java.util.Date;
39 import java.util.List;
40 import java.util.logging.Level;
41 import java.util.logging.Logger;
42 import java.util.regex.Matcher;
43 import java.util.regex.Pattern;
44 import javax.ws.rs.DefaultValue;
45 import javax.ws.rs.GET;
46 import javax.ws.rs.POST;
47 import javax.ws.rs.PUT;
48 import javax.ws.rs.Path;
49 import javax.ws.rs.PathParam;
50 import javax.ws.rs.Produces;
51 import javax.ws.rs.QueryParam;
52 import javax.ws.rs.WebApplicationException;
53 import javax.ws.rs.core.GenericEntity;
54 import javax.ws.rs.core.MediaType;
55 import javax.ws.rs.core.Response.Status;
59 * @author Jaroslav Tulach <jtulach@netbeans.org>
61 public final class Games {
62 private final Quoridor quoridor;
63 private final List<Game> games = new ArrayList<Game>();
64 private final File dir;
65 private static final Logger LOG = Logger.getLogger(Games.class.getName());
67 public Games(File dir, Quoridor quoridor) {
69 this.quoridor = quoridor;
70 File[] arr = dir.listFiles();
76 } catch (IOException ex) {
77 LOG.log(Level.WARNING, "Wrong game in " + f, ex);
84 @Produces({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
85 public GameId createGame(
86 @QueryParam("loginID") String id,
87 @QueryParam("white") String white,
88 @QueryParam("black") String black
89 ) throws IOException {
90 String logUser = quoridor.isLoggedIn(id);
91 if (logUser == null) {
92 throw new WebApplicationException(Status.UNAUTHORIZED);
95 throw new WebApplicationException(Status.PRECONDITION_FAILED);
98 throw new WebApplicationException(Status.PRECONDITION_FAILED);
100 if (!logUser.equals(white) && !logUser.equals(black)) {
101 throw new WebApplicationException(Status.PRECONDITION_FAILED);
104 Game g = new Game(white, black);
112 @Produces(MediaType.TEXT_PLAIN)
113 public String getBoard(
114 @PathParam("id") String id,
115 @QueryParam("move") @DefaultValue("-1") int move
117 Game g = findGame(id, move);
119 throw new IllegalArgumentException("Unknown game " + id);
121 return g.getBoard().toString();
124 public Game getBoardInfo(
125 @QueryParam("loginID") @DefaultValue("") String loginId,
126 @PathParam("id") String id,
127 @QueryParam("move") @DefaultValue("-1") int move
128 ) throws IOException {
129 Game g = findGame(id, move);
130 if (canSee(g.getId(), loginId)) {
133 throw new WebApplicationException(Status.UNAUTHORIZED);
138 @Produces({ "application/x-javascript", MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
139 public JSONWithPadding getBoardInfo(
140 @QueryParam("callback") String callback,
141 @QueryParam("loginID") @DefaultValue("") String loginId,
142 @PathParam("id") String id,
143 @QueryParam("move") @DefaultValue("-1") int move
144 ) throws IOException {
145 return new JSONWithPadding(getBoardInfo(loginId, id, move), callback);
149 private boolean canSee(GameId id, String loginId) throws IOException {
150 if (!id.isFinished()) {
153 String logUser = quoridor.isLoggedIn(loginId);
154 if (logUser == null) {
157 if (logUser.equals(id.getWhite())) {
160 if (logUser.equals(id.getBlack())) {
163 User info = quoridor.getUsers().getUserInfo(loginId, logUser);
164 if (info != null && info.hasPermission("games")) {
172 @Produces({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
173 public GameId applyMove(
174 @QueryParam("loginID") String loginId,
175 @PathParam("id") String id,
176 @QueryParam("player") String player,
177 @QueryParam("move") String move,
178 @QueryParam("comment") String comment
179 ) throws IllegalPositionException, IOException {
180 String logUser = quoridor.isLoggedIn(loginId);
181 if (logUser == null) {
182 throw new WebApplicationException(Status.UNAUTHORIZED);
184 if (!logUser.equals(player)) {
185 User info = quoridor.getUsers().getUserInfo(loginId, logUser);
186 if (info == null || !info.hasPermission("resign")) {
187 throw new WebApplicationException(Status.UNAUTHORIZED);
190 if (comment == null && move == null) {
191 throw new WebApplicationException(Status.BAD_REQUEST);
194 Game g = findGame(id);
196 throw new IllegalArgumentException("Unknown game " + id);
199 Move m = Move.valueOf(move);
200 g.apply(player, m, new Date());
202 if (comment != null) {
203 g.comment(player, comment, new Date());
207 } catch (IOException ex) {
208 LOG.log(Level.WARNING, "Cannot store game " + id, ex);
214 @Produces({ "application/x-javascript", MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
215 public JSONWithPadding listGames(
216 @QueryParam("callback") String callback,
217 @DefaultValue("") @QueryParam("loginID") String loginId,
218 @DefaultValue("") @QueryParam("status") String status
219 ) throws IOException {
220 List<GameId> arr = new ArrayList<GameId>(games.size());
221 for (Game g : games) {
222 if (!canSee(g.getId(), loginId)) {
225 if (status.length() == 0 || g.getId().getStatus().toString().equals(status)) {
229 Collections.sort(arr, GameId.NEWEST_FIRST);
230 return new JSONWithPadding(new GenericEntity<List<GameId>>(arr) {}, callback);
233 public List<Game> getGames() {
237 private Game findGame(String id) {
238 for (Game g : games) {
239 if (g.getId().getId().equals(id)) {
245 private Game findGame(String id, int move) {
246 Game g = findGame(id);
248 throw new IllegalArgumentException("Unknown game " + id);
251 return move == -1 ? g : g.snapshot(move);
252 } catch (IllegalPositionException ex) {
253 Logger.getLogger(Games.class.getName()).log(Level.SEVERE, null, ex);
258 private static final Pattern saidWho = Pattern.compile("# *([^ ]*) *@(.*):$");
260 private Game readGame(File f) throws IOException {
261 InputStream is = new FileInputStream(f);
262 BufferedReader r = new BufferedReader(new InputStreamReader(is, "UTF-8"));
269 String line = r.readLine();
274 if (line.length() == 0) {
277 if (line.startsWith("# white: ")) {
278 white = line.substring(9);
281 if (line.startsWith("# black: ")) {
282 black = line.substring(9);
285 if (line.startsWith("#")) {
286 Matcher m =saidWho.matcher(line);
295 if (line.startsWith("# ")) {
296 line = line.substring(2);
298 line = line.substring(1);
303 d = new Date(Date.parse(when));
305 } catch (IllegalArgumentException ex) {
306 LOG.warning("Unparseable date " + when + " in " + f);
308 g.comment(who, line, d);
313 if (white == null || black == null) {
314 throw new IOException("Missing white and black identification in " + f);
317 GameId id = new GameId(f.getName(), white, black, new Date(f.lastModified()), new Date(f.lastModified()), GameStatus.whiteMove, 0, false);
320 int hash = line.indexOf('#');
322 line = line.substring(0, hash);
324 if (line.equals("finish")) {
327 String[] moves = line.split(" ");
328 if (moves.length == 0) {
331 if (moves.length > 2) {
332 throw new IOException("Too much moves on line: " + line);
335 if (!"...".equals(moves[0])) {
336 g.apply(white, Move.valueOf(moves[0]), null);
338 if (moves.length == 2) {
339 g.apply(black, Move.valueOf(moves[1]), null);
341 } catch (IllegalPositionException ex) {
342 throw new IOException("Wrong move: " + ex.getMessage());
346 throw new IOException("No moves in " + f);
351 private void storeGame(Game g) throws IOException {
353 File f = new File(dir, g.getId().getId());
357 final void storeGame(Game g, File f) throws IOException {
358 FileOutputStream os = new FileOutputStream(f);
359 Writer w = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));
360 PrintWriter pw = new PrintWriter(w);
361 pw.println("# white: " + g.getId().getWhite());
362 pw.println("# black: " + g.getId().getBlack());
363 pw.println("# status: " + g.getId().getStatus());
365 boolean separate = true;
366 for (CommentedMove m : g.getMoves()) {
367 if (!separate && cnt % 2 == 1) {
371 pw.print(m.getMove().toString());
372 List<Note> notes = m.getComments();
376 for (Note n : notes) {
378 pw.print(n.getWho());
380 pw.print(n.getWhen());
382 for (String l : n.getComment().split("\n")) {