freemarkerdor/src/main/java/cz/xelfi/quoridor/freemarkerdor/UI.java
author Jaroslav Tulach <jaroslav.tulach@apidesign.org>
Tue, 14 Sep 2010 08:56:13 +0200
changeset 264 d60370059c3c
parent 234 0a71b6bd786f
child 271 aa1c63b58149
permissions -rw-r--r--
Changing headers to GPLv3
     1 /*
     2  * Quoridor server and related libraries
     3  * Copyright (C) 2009-2010 Jaroslav Tulach <jaroslav.tulach@apidesign.org>
     4  *
     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.
     8  *
     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.
    13  *
    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/.
    17  */
    18 package cz.xelfi.quoridor.freemarkerdor;
    19 
    20 import com.sun.jersey.api.client.Client;
    21 import com.sun.jersey.api.client.UniformInterfaceException;
    22 import com.sun.jersey.api.client.WebResource;
    23 import com.sun.jersey.api.container.httpserver.HttpServerFactory;
    24 import com.sun.jersey.api.core.PackagesResourceConfig;
    25 import com.sun.jersey.api.core.ResourceConfig;
    26 import com.sun.jersey.api.view.Viewable;
    27 import com.sun.net.httpserver.HttpServer;
    28 import cz.xelfi.quoridor.Board;
    29 import cz.xelfi.quoridor.IllegalPositionException;
    30 import java.io.IOException;
    31 import java.io.InputStream;
    32 import java.io.StringWriter;
    33 import java.net.URI;
    34 import java.text.MessageFormat;
    35 import java.util.ArrayList;
    36 import java.util.Arrays;
    37 import java.util.Date;
    38 import java.util.HashMap;
    39 import java.util.List;
    40 import java.util.Locale;
    41 import java.util.Map;
    42 import java.util.MissingResourceException;
    43 import java.util.Properties;
    44 import java.util.ResourceBundle;
    45 import java.util.concurrent.Callable;
    46 import javax.ws.rs.DefaultValue;
    47 import javax.ws.rs.FormParam;
    48 import javax.ws.rs.GET;
    49 import javax.ws.rs.POST;
    50 import javax.ws.rs.Path;
    51 import javax.ws.rs.PathParam;
    52 import javax.ws.rs.Produces;
    53 import javax.ws.rs.QueryParam;
    54 import javax.ws.rs.core.CacheControl;
    55 import javax.ws.rs.core.Context;
    56 import javax.ws.rs.core.Cookie;
    57 import javax.ws.rs.core.HttpHeaders;
    58 import javax.ws.rs.core.MediaType;
    59 import javax.ws.rs.core.NewCookie;
    60 import javax.ws.rs.core.Response;
    61 import javax.ws.rs.core.Response.ResponseBuilder;
    62 import org.openide.util.Exceptions;
    63 import org.w3c.dom.Document;
    64 
    65 /**
    66  *
    67  * @author Jaroslav Tulach <jtulach@netbeans.org>
    68  */
    69 @Path("/")
    70 public final class UI {
    71     private static final String version;
    72     static {
    73         Properties p = new Properties();
    74         try {
    75             InputStream is = FreemarkerProcessor.class.getResourceAsStream("/META-INF/maven/cz.xelfi.quoridor/freemarkerdor/pom.properties"); // NOI18N
    76             if (is != null) {
    77                 p.load(is);
    78             }
    79         } catch (IOException ex) {
    80             ex.printStackTrace();
    81         }
    82         version = p.getProperty("version", "unknown"); // NOI18N
    83     }
    84     private static WebResource base;
    85     private static WebResource stat;
    86     private static WebResource web;
    87     private static Requests requests;
    88 
    89     @Context
    90     private HttpHeaders headers;
    91     private UserInfo user;
    92     private String uuid;
    93 
    94     public UI() {
    95     }
    96 
    97     private String login() {
    98         Cookie cookie = headers.getCookies().get("login");
    99         if (cookie != null) {
   100             return cookie.getValue();
   101         }
   102         return null;
   103     }
   104 
   105     private Viewable checkLogin() {
   106         String id = login();
   107         if (id != null) {
   108             UserInfo us;
   109             try {
   110                 us = base.path("users").queryParam("loginID", id).
   111                     accept(MediaType.TEXT_XML).get(UserInfo.class);
   112             } catch (Exception ex) {
   113                 ex.printStackTrace();
   114                 us = null;
   115             }
   116             if (us != null && us.getId().length() > 0) {
   117                 user = us;
   118                 uuid = id;
   119                 return null;
   120             }
   121         }
   122         return viewable("login.fmt", null);
   123     }
   124 
   125     @POST
   126     @Path("login")
   127     @Produces(MediaType.TEXT_HTML)
   128     public Response login(
   129         @FormParam("name") String name, @FormParam("password") String password
   130     ) throws Exception {
   131         uuid = base.path("login").queryParam("name", name).queryParam("password", password).
   132             accept(MediaType.TEXT_PLAIN).put(String.class);
   133         if (uuid != null) {
   134             user = new UserInfo(name);
   135             NewCookie nc = new NewCookie("login", uuid, null, null, null, 3600 * 24 * 7, false);
   136             return Response.ok().cookie(nc).entity(viewable("login.fmt", null)).build();
   137         } else {
   138             Viewable v = viewable("login.fmt", null, "message", "Invalid name or password: " + name);
   139             return Response.status(1).entity(v).build();
   140         }
   141     }
   142 
   143     @GET
   144     @Produces(MediaType.TEXT_HTML)
   145     public Response welcome(@QueryParam("maxItems") @DefaultValue("10") int maxItems) {
   146         Viewable v = checkLogin();
   147         ResponseBuilder resp = Response.ok();
   148         if (v == null) {
   149             v = welcomeImpl("maxItems", maxItems);
   150         }
   151         CacheControl cc = new CacheControl();
   152         cc.setNoCache(true);
   153         resp.cacheControl(cc);
   154         return resp.entity(v).build();
   155     }
   156 
   157     @GET
   158     @Path("games/{id}.png")
   159     @Produces("image/png")
   160     public Response getBoardImage(
   161         @PathParam("id") String id,
   162         @QueryParam("fieldSize") @DefaultValue("50") int fieldSize,
   163         @QueryParam("move") @DefaultValue("-1") int move
   164     ) throws IllegalPositionException {
   165         WebResource wr = base.path("games").path(id);
   166         if (move != -1) {
   167             wr = wr.queryParam("move", "" + move);
   168         }
   169         String txt = wr.accept(MediaType.TEXT_PLAIN).get(String.class);
   170         Board b = Board.valueOf(txt);
   171 //        Board b = new Board(txt);
   172         ResponseBuilder resp = Response.ok();
   173         CacheControl cc = new CacheControl();
   174         cc.setNoCache(true);
   175         resp.cacheControl(cc);
   176         return resp.entity(BoardImage.draw(b, fieldSize)).build();
   177     }
   178 
   179 
   180     private Response board(String id) {
   181         return board(id, "", null);
   182     }
   183     @GET
   184     @Path("games/{id}/")
   185     @Produces(MediaType.TEXT_HTML)
   186     public Response board(
   187         @PathParam("id") String id,
   188         @QueryParam("format") @DefaultValue("") String format,
   189         @QueryParam("move") @DefaultValue("-1") String move
   190     ) {
   191         return board(id, null, format, move);
   192     }
   193     private Response board(@PathParam("id") String id, String msg, String format, String m) {
   194         Viewable v = checkLogin();
   195         if (v != null) {
   196             return Response.ok().entity(v).build();
   197         }
   198         int move;
   199         try {
   200             move = Integer.parseInt(m);
   201         } catch (NumberFormatException ex) {
   202             move = -1;
   203         }
   204         WebResource url = base.path("games").queryParam("loginID", uuid).path(id);
   205         if (move >= 0) {
   206             url = url.queryParam("move", "" + move);
   207         }
   208         ResponseBuilder resp = Response.ok();
   209         CacheControl cc = new CacheControl();
   210         cc.setNoCache(true);
   211         resp.cacheControl(cc);
   212         Cookie cFormat = headers.getCookies().get("format");
   213         if (format.length() == 0) {
   214             if (cFormat != null) {
   215                 format = cFormat.getValue();
   216             } else {
   217                 if (isMobile(headers)) {
   218                     format = "small";
   219                 }
   220             }
   221         } else {
   222             if (cFormat == null || !format.equals(cFormat.getValue())) {
   223                 resp.cookie(new NewCookie("format", format));
   224             }
   225         }
   226 
   227         Document doc = url.accept(MediaType.TEXT_XML).get(Document.class);
   228         Board b;
   229         String t = doc.getElementsByTagName("board").item(0).getTextContent();
   230         try {
   231             b = Board.valueOf(doc.getElementsByTagName("board").item(0).getTextContent());
   232         } catch (Exception ex) {
   233 //            b = new Board(t);
   234 //        } catch (IllegalStateException ex) {
   235             Exceptions.printStackTrace(ex);
   236             b = Board.empty();
   237         }
   238         String bCode = null;
   239         try{
   240             bCode = stat.path("openings").path(b.getCode()+".check").queryParam("loginID", user.getId()).accept(MediaType.TEXT_PLAIN).get(String.class);
   241         }catch(Exception e){
   242             bCode = null;
   243         }
   244         if(bCode == null || "".equals(bCode))
   245             v = viewable("game.fmt", doc, "message", msg, "format", format, "board", b,"textPicture", boardToPicture(b));
   246         else
   247             v = viewable("game.fmt", doc, "message", msg, "format", format, "board", b,"textPicture", boardToPicture(b),"bCode", bCode);
   248         return resp.entity(v).build();
   249     }
   250 
   251     private static String boardToPicture(Board b) {
   252         StringWriter w = new StringWriter();
   253         try {
   254             b.write(w);
   255         } catch (IOException ex) {
   256             return ex.toString();
   257         }
   258         return w.toString();
   259     }
   260 
   261     @GET
   262     @Path("games/{id}/move")
   263     @Produces(MediaType.TEXT_HTML)
   264     public Response move(
   265         @PathParam("id") String id,
   266         @QueryParam("type") String type,
   267         @QueryParam("direction") String direction,
   268         @QueryParam("direction-next") @DefaultValue("") String directionNext,
   269         @QueryParam("column") @DefaultValue("") String column,
   270         @QueryParam("row") @DefaultValue("") String row
   271     ) {
   272         Viewable v = checkLogin();
   273         if (v != null) {
   274             return Response.ok().entity(v).build();
   275         }
   276         WebResource wr = base.path("games").path(id).
   277             queryParam("loginID", uuid).
   278             queryParam("player", user.getId());
   279         try {
   280             if (type.equals("resign")) {
   281                 wr.queryParam("move", "RESIGN").put();
   282                 return board(id);
   283             }
   284             if (type.equals("fence")) {
   285                 wr.queryParam("move", direction.charAt(0) + column + row).put();
   286                 return board(id);
   287             }
   288             if (type.equals("move")) {
   289                 wr.queryParam("move", direction + directionNext).put();
   290                 return board(id);
   291             }
   292         } catch (UniformInterfaceException ex) {
   293             return board(id, "WRONG_MOVE", "-1");
   294         }
   295         return board(id);
   296     }
   297 
   298     @GET
   299     @Path("games/{id}/comment")
   300     @Produces(MediaType.TEXT_HTML)
   301     public Response comment(
   302         @PathParam("id") String id,
   303         @QueryParam("comment") String comment
   304     ) {
   305         Viewable v = checkLogin();
   306         if (v != null) {
   307             return Response.ok().entity(v).build();
   308         }
   309         WebResource wr = base.path("games").path(id).
   310             queryParam("loginID", uuid).
   311             queryParam("player", user.getId()).
   312             queryParam("comment", comment);
   313         wr.put();
   314         
   315         return board(id);
   316     }
   317 
   318     @GET
   319     @Path("games/create")
   320     @Produces(MediaType.TEXT_HTML)
   321     public Response create(
   322         @QueryParam("white") String white,
   323         @QueryParam("black") String black
   324     ) {
   325         Viewable v = checkLogin();
   326         if (v != null) {
   327             return Response.status(Response.Status.FORBIDDEN).entity(v).build();
   328         }
   329 
   330         if (user.getId().equals(white) || user.getId().equals(black)) {
   331             Object obj =
   332                 base.path("games").
   333                 queryParam("loginID", uuid).
   334                 queryParam("white", white).
   335                 queryParam("black", black).accept(MediaType.TEXT_XML).post(Document.class);
   336             return Response.ok(welcomeImpl()).build();
   337         } else {
   338             return Response.status(Response.Status.NOT_FOUND).
   339                 entity(welcomeImpl("message", "You (" + user.getId() + ") must be white or black!")).build();
   340         }
   341     }
   342 
   343     private Viewable welcomeImpl(Object... args) {
   344         final Document got = base.path("games").queryParam("loginID", uuid).accept(MediaType.TEXT_XML).get(Document.class);
   345         return viewable("index.fmt", got, args);
   346     }
   347 
   348     @Path("requests")
   349     public Requests getRequests() {
   350         if (requests == null) {
   351             requests = new Requests(web.path("requests"));
   352         }
   353         return requests;
   354     }
   355 
   356 
   357     @GET
   358     @Path("options")
   359     public Response changeOptions(
   360         @QueryParam("email") String email,
   361         @QueryParam("language") String language,
   362         @QueryParam("verified") String verified
   363     ) throws IOException {
   364         Viewable v = checkLogin();
   365         if (v != null) {
   366             return Response.status(Response.Status.FORBIDDEN).entity(v).build();
   367         }
   368 
   369         if (email != null) {
   370             if (getRequests().isVerified(verified)) {
   371                 UserInfo ui = base.path("users/" + user.getId()).
   372                     queryParam("loginID", uuid).
   373                     queryParam("name", "email").
   374                     queryParam("value", email).accept(MediaType.TEXT_XML).post(UserInfo.class);
   375             } else {
   376                 WebResource request;
   377                 request = web.path("options").queryParam("name", "email").queryParam("email", email);
   378                 URI callback = getRequests().register(login(), request);
   379 
   380                 ResourceBundle rb = bundle(null);
   381                 String subject = rb.getString("MSG_ChangeEmailSubject");
   382                 String text = MessageFormat.format(rb.getString("MSG_ChangeEmailText"), user.getId(), callback);
   383                 EmailService.getDefault().sendEmail(email, subject, text);
   384                 return Response.ok(viewable("email.fmt", null)).build();
   385 
   386             }
   387         }
   388 
   389         if (language != null) {
   390             UserInfo ui = base.path("users/" + user.getId()).
   391                 queryParam("loginID", uuid).
   392                 queryParam("name", "language").
   393                 queryParam("value", language).
   394                 accept(MediaType.TEXT_XML).post(UserInfo.class);
   395         }
   396 
   397         return welcome(10);
   398     }
   399 
   400     @GET
   401     @Path("elo")
   402     @Produces(MediaType.TEXT_HTML)
   403     public Response getEloList(
   404             @QueryParam("historyId") @DefaultValue("0") Integer historyId){
   405         Viewable v = checkLogin();
   406         if (v != null) {
   407             return Response.status(Response.Status.FORBIDDEN).entity(v).build();
   408         }
   409         final Document got = stat.path("elo").path("list").path(historyId.toString()).accept(MediaType.TEXT_XML).get(Document.class);
   410         return Response.ok(viewable("elo.fmt", got, "historyId", historyId)).build();
   411     }
   412     
   413     @GET
   414     @Path("openings")
   415     @Produces(MediaType.TEXT_HTML)
   416     public Response getOpeningRoot(){
   417         return getOpeningNode("ROOT");
   418     }
   419 
   420     @GET
   421     @Path("openings/{code}")
   422     @Produces(MediaType.TEXT_HTML)
   423     public Response getOpeningNode(@PathParam("code") String code){
   424         Viewable v = checkLogin();
   425         if (v != null) {
   426             return Response.status(Response.Status.FORBIDDEN).entity(v).build();
   427         }
   428         final Document got = stat.path("openings").path(code).queryParam("loginID", user.getId()).accept(MediaType.TEXT_XML).get(Document.class);
   429         Board b;
   430         try {
   431             b = Board.valueOf(got.getElementsByTagName("nodeCode").item(0).getTextContent());
   432         } catch (Exception ex) {
   433             Exceptions.printStackTrace(ex);
   434             b = Board.empty();
   435         }
   436         return Response.ok(viewable("openings.fmt", got, "whitefences",b.getPlayers().get(0).getFences(),"blackfences",b.getPlayers().get(1).getFences())).build();
   437     }
   438 
   439     @GET
   440     @Path("openings/{code}/{status}")
   441     @Produces(MediaType.TEXT_HTML)
   442     public Response getOpeningNodeGames(@PathParam("code") String code, @PathParam("status") String status){
   443         Viewable v = checkLogin();
   444         if (v != null) {
   445             return Response.status(Response.Status.FORBIDDEN).entity(v).build();
   446         }
   447         final Document got = stat.path("openings").path(code).path(status).queryParam("loginID", user.getId()).accept(MediaType.TEXT_XML).get(Document.class);
   448         return Response.ok(viewable("opening_games.fmt", got,"code",code,"color",status)).build();
   449     }
   450 
   451     @GET
   452     @Path("openings/{code}.png")
   453     @Produces("image/png")
   454     public Response getOpeningBoardImage(
   455         @PathParam("code") String code,
   456         @QueryParam("fieldSize") @DefaultValue("40") int fieldSize
   457     ) throws IllegalPositionException {
   458         Board b = Board.valueOf(code);
   459         ResponseBuilder resp = Response.ok();
   460         CacheControl cc = new CacheControl();
   461         cc.setNoCache(true);
   462         resp.cacheControl(cc);
   463         return resp.entity(BoardImage.draw(b, fieldSize)).build();
   464     }
   465 
   466     //
   467     // start the server
   468     //
   469 
   470     public static void main(String[] params) throws Exception {
   471         List<String> args = new ArrayList<String>(Arrays.asList(params));
   472 
   473         String publicURL = null;
   474         if (args.size() >= 2 && args.get(0).equals("--url")) {
   475             publicURL = args.get(1);
   476             args.remove(0);
   477             args.remove(0);
   478         }
   479 
   480         int port = 9333;
   481         if (args.size() > 1) {
   482             port = Integer.parseInt(args.get(0));
   483         }
   484         String remoteAPI = args.size() >= 2 ? args.get(1) : null;
   485         String remoteStatistics = args.size() >= 3 ? args.get(2) : null;
   486 
   487         Locale.setDefault(Locale.ROOT);
   488 
   489         Callable<Void> r = startServers(port, remoteAPI, remoteStatistics, publicURL);
   490 
   491         if (args.size() < 3 || !args.get(args.size() - 1).equals("--kill")) {
   492             System.out.println("Hit enter to stop it...");
   493             System.in.read();
   494         } else {
   495             synchronized (UI.class) {
   496                 UI.class.wait();
   497             }
   498         }
   499         r.call();
   500         System.exit(0);
   501     }
   502 
   503     static Callable<Void> startServers(int port, String remoteAPI, String remoteStatistics, String publicURL) throws Exception {
   504         Client client = new Client();
   505         Client client1 = new Client();
   506 
   507         final HttpServer apiServer;
   508         if (remoteAPI == null) {
   509             throw new IllegalArgumentException("Provide URL to API server"); // NOI18N
   510         } else {
   511             base = client.resource(new URI(remoteAPI));
   512             apiServer = null;
   513         }
   514 
   515         if (remoteStatistics != null) {
   516             stat = client1.resource(new URI(remoteStatistics));
   517         } else {
   518             stat = client1.resource(new URI("http://localhost:9444"));
   519         }
   520 
   521         ResourceConfig rc = new PackagesResourceConfig(
   522             "cz.xelfi.quoridor.freemarkerdor"
   523         );
   524 
   525         final String baseUri = "http://localhost:" + port + "/";
   526         if (publicURL == null) {
   527             publicURL = baseUri;
   528         }
   529         final HttpServer server = HttpServerFactory.create(baseUri, rc);
   530         Client c3 = new Client();
   531         web = c3.resource(publicURL);
   532         server.start();
   533         System.out.println("Quoridor started at port " + port);
   534 
   535         return new Callable<Void>() {
   536             public Void call() throws Exception {
   537                 if (apiServer != null) {
   538                     apiServer.stop(0);
   539                 }
   540                 server.stop(0);
   541                 return null;
   542             }
   543         };
   544     }
   545 
   546     private ResourceBundle bundle(Locale[] locale) {
   547         ResourceBundle rb = null;
   548         String lng = user == null ? null : user.getProperty("language"); // NOI18N
   549         if (lng != null) {
   550             try {
   551                 Locale l = new Locale(lng);
   552                 rb = ResourceBundle.getBundle("cz.xelfi.quoridor.freemarkerdor.UI.Bundle", l);
   553                 if (locale != null) {
   554                     locale[0] = l;
   555                 }
   556             } catch (MissingResourceException e) {
   557                 // OK
   558             }
   559         }
   560         if (rb == null) {
   561             for (Locale l : headers.getAcceptableLanguages()) {
   562                 try {
   563                     rb = ResourceBundle.getBundle("cz.xelfi.quoridor.freemarkerdor.UI.Bundle", l);
   564                     if (locale != null) {
   565                         locale[0] = l;
   566                     }
   567                     break;
   568                 } catch (MissingResourceException e) {
   569                     // OK
   570                 }
   571             }
   572         }
   573         if (rb == null) {
   574             rb = ResourceBundle.getBundle("cz.xelfi.quoridor.freemarkerdor.UI.Bundle", Locale.ENGLISH);
   575             if (locale != null) {
   576                 locale[0] = Locale.ENGLISH;
   577             }
   578         }
   579         return rb;
   580     }
   581 
   582     private Viewable viewable(String page, Document doc, Object... more) {
   583         Locale[] locale = new Locale[1];
   584         ResourceBundle rb = bundle(locale);
   585 
   586         Map<String,Object> map = new HashMap<String,Object>();
   587         class ConvertToDate extends HashMap<Object,Object> {
   588             @Override
   589             public Object get(Object o) {
   590                 long time = Long.parseLong(o.toString());
   591                 return new Date(time);
   592             }
   593         }
   594 
   595         map.put("locale", locale[0].toString());
   596         map.put("doc", doc);
   597         if (user != null) {
   598             map.put("user", user.getId());
   599             map.put("email", user.getProperty("email"));
   600         }
   601         map.put("bundle", rb);
   602         map.put("toDate", new ConvertToDate());
   603         map.put("now", System.currentTimeMillis());
   604         map.put("version", version);
   605         for (int i = 0; i < more.length; i += 2) {
   606             map.put((String)more[i],more[i + 1]);
   607         }
   608         return new Viewable(page, map);
   609     }
   610 
   611 
   612     private static boolean isMobile(HttpHeaders headers) {
   613         final String[] keywords = {
   614             "Profile/MIDP",
   615         };
   616         List<String> agent = headers.getRequestHeader(HttpHeaders.USER_AGENT);
   617         if (agent != null) {
   618             for (String a : agent) {
   619                 for (String k : keywords) {
   620                     if (a.contains(k)) {
   621                         return true;
   622                     }
   623                 }
   624             }
   625         }
   626         return false;
   627     }
   628 
   629 }