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