freemarkerdor/src/main/java/cz/xelfi/quoridor/freemarkerdor/UI.java
author Jaroslav Tulach <jtulach@netbeans.org>
Thu, 25 Nov 2010 23:08:20 +0100
changeset 278 3a472605338f
parent 271 aa1c63b58149
permissions -rw-r--r--
Initial support log via OpenID. Now I need to generate a login cookie and we'll be done
     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 java.util.logging.Level;
    47 import java.util.logging.Logger;
    48 import javax.ws.rs.DefaultValue;
    49 import javax.ws.rs.FormParam;
    50 import javax.ws.rs.GET;
    51 import javax.ws.rs.POST;
    52 import javax.ws.rs.Path;
    53 import javax.ws.rs.PathParam;
    54 import javax.ws.rs.Produces;
    55 import javax.ws.rs.QueryParam;
    56 import javax.ws.rs.WebApplicationException;
    57 import javax.ws.rs.core.CacheControl;
    58 import javax.ws.rs.core.Context;
    59 import javax.ws.rs.core.Cookie;
    60 import javax.ws.rs.core.HttpHeaders;
    61 import javax.ws.rs.core.MediaType;
    62 import javax.ws.rs.core.NewCookie;
    63 import javax.ws.rs.core.Response;
    64 import javax.ws.rs.core.Response.ResponseBuilder;
    65 import org.openid4java.consumer.ConsumerManager;
    66 import org.openid4java.consumer.VerificationResult;
    67 import org.openid4java.discovery.DiscoveryInformation;
    68 import org.openid4java.message.AuthRequest;
    69 import org.openid4java.message.MessageExtension;
    70 import org.openid4java.message.Parameter;
    71 import org.openid4java.message.ParameterList;
    72 import org.openid4java.message.ax.AxMessage;
    73 import org.openid4java.message.ax.FetchRequest;
    74 import org.openid4java.message.ax.FetchResponse;
    75 import org.openide.util.Exceptions;
    76 import org.w3c.dom.Document;
    77 
    78 /**
    79  *
    80  * @author Jaroslav Tulach <jtulach@netbeans.org>
    81  */
    82 @Path("/")
    83 public final class UI {
    84     private static final String version;
    85     static {
    86         Properties p = new Properties();
    87         try {
    88             InputStream is = FreemarkerProcessor.class.getResourceAsStream("/META-INF/maven/cz.xelfi.quoridor/freemarkerdor/pom.properties"); // NOI18N
    89             if (is != null) {
    90                 p.load(is);
    91             }
    92         } catch (IOException ex) {
    93             ex.printStackTrace();
    94         }
    95         version = p.getProperty("version", "unknown"); // NOI18N
    96     }
    97     private static final Logger LOG = Logger.getLogger(UI.class.getName());
    98     private static WebResource base;
    99     private static WebResource stat;
   100     private static WebResource web;
   101     private static Requests requests;
   102     private static ConsumerManager manager;
   103     private static Map<String,DiscoveryInformation> ids = new HashMap<String, DiscoveryInformation>();
   104 
   105     @Context
   106     private HttpHeaders headers;
   107     private UserInfo user;
   108     private String uuid;
   109 
   110     public UI() {
   111     }
   112 
   113     private String login() {
   114         Cookie cookie = headers.getCookies().get("login");
   115         if (cookie != null) {
   116             return cookie.getValue();
   117         }
   118         return null;
   119     }
   120 
   121     private Viewable checkLogin() {
   122         String id = login();
   123         if (id != null) {
   124             UserInfo us;
   125             try {
   126                 us = base.path("users").queryParam("loginID", id).
   127                     accept(MediaType.TEXT_XML).get(UserInfo.class);
   128             } catch (Exception ex) {
   129                 ex.printStackTrace();
   130                 us = null;
   131             }
   132             if (us != null && us.getId().length() > 0) {
   133                 user = us;
   134                 uuid = id;
   135                 return null;
   136             }
   137         }
   138         return viewable("login.fmt", null);
   139     }
   140 
   141     @GET
   142     @Path("openid")
   143     @Produces(MediaType.TEXT_HTML)
   144     public Viewable openidResponse(
   145         @QueryParam("openid.assoc_handle") String handle,
   146         @QueryParam("openid.claimed_id") String claimedID,
   147         @QueryParam("openid.identity") String identity,
   148         @QueryParam("openid.mode") String mode /*id_res */,
   149         @QueryParam("openid.ns") String ns, /* http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0 */
   150         @QueryParam("openid.op_endpoint") String endpoint,
   151         @QueryParam("openid.response_nonce") String nonce,
   152         @QueryParam("openid.return_to") String returnTo,
   153         @QueryParam("openid.sig") String sig,
   154         @QueryParam("openid.signed") String signed
   155     ) throws Exception {
   156         if (signed == null) {
   157             return viewable("openid.fmt", null);
   158         }
   159         
   160         if (manager == null) {
   161             manager = new ConsumerManager(); 
   162         }
   163         ParameterList pl = new ParameterList();
   164         pl.set(new Parameter("openid.assoc_handle", handle));
   165         pl.set(new Parameter("openid.claimed_id", claimedID));
   166         pl.set(new Parameter("openid.identity", identity));
   167         pl.set(new Parameter("openid.mode", mode));
   168         pl.set(new Parameter("openid.ns", ns));
   169         pl.set(new Parameter("openid.op_endpoint", endpoint));
   170         pl.set(new Parameter("openid.response_nonce", nonce));
   171         pl.set(new Parameter("openid.return_to", returnTo));
   172         pl.set(new Parameter("openid.sig", sig));
   173         pl.set(new Parameter("openid.signed", signed));
   174         
   175         DiscoveryInformation info = ids.get(claimedID);
   176         VerificationResult res = manager.verify(returnTo, pl, info);
   177         String userId = res != null && res.getVerifiedId() != null ? 
   178             res.getVerifiedId().getIdentifier() : null;
   179         
   180         if (res.getAuthResponse().hasExtension(AxMessage.OPENID_NS_AX)) {
   181             MessageExtension ext = res.getAuthResponse().getExtension(AxMessage.OPENID_NS_AX);
   182 
   183             if (ext instanceof FetchResponse) {
   184                 FetchResponse fetchResp = (FetchResponse) ext;
   185 
   186                 String firstName = fetchResp.getAttributeValue("FirstName");
   187                 String lastName = fetchResp.getAttributeValue("LastName");
   188                 String email = fetchResp.getAttributeValue("Email");
   189                 
   190                 LOG.log(Level.INFO, "First name: {0}", firstName);
   191                 LOG.log(Level.INFO, "Last name: {0}", lastName);
   192                 LOG.log(Level.INFO, "Email: {0}", email);
   193                 
   194                 
   195             } 
   196         }        
   197         
   198         return viewable("openid.fmt", null, "id", userId);
   199     }
   200     @POST
   201     @Path("openid")
   202     @Produces(MediaType.TEXT_HTML)
   203     public Viewable openid(@FormParam("openid_identifier") String openid) throws Exception {
   204         if (manager == null) {
   205             manager = new ConsumerManager(); 
   206         }
   207         List l = manager.discover(openid);
   208         DiscoveryInformation info = manager.associate(l);
   209         AuthRequest auth = manager.authenticate(info, web.path("openid").getURI().toString());
   210         FetchRequest fetch = FetchRequest.createFetchRequest();
   211         fetch.addAttribute("FirstName", "http://schema.openid.net/namePerson/first", true);
   212         fetch.addAttribute("LastName", "http://schema.openid.net/namePerson/last", true);
   213         fetch.addAttribute("Email", "http://schema.openid.net/contact/email", true);
   214         auth.addExtension(fetch);        
   215         ids.put(info.getClaimedIdentifier().toString(), info);
   216         
   217         return viewable("openid.fmt", null, "url", auth.getDestinationUrl(true));
   218     }
   219     
   220     @POST
   221     @Path("login")
   222     @Produces(MediaType.TEXT_HTML)
   223     public Response login(
   224         @FormParam("name") String name, @FormParam("password") String password
   225     ) throws Exception {
   226         uuid = base.path("login").queryParam("name", name).queryParam("password", password).
   227             accept(MediaType.TEXT_PLAIN).put(String.class);
   228         if (uuid != null) {
   229             user = new UserInfo(name);
   230             NewCookie nc = new NewCookie("login", uuid, null, null, null, 3600 * 24 * 7, false);
   231             return Response.ok().cookie(nc).entity(viewable("login.fmt", null)).build();
   232         } else {
   233             Viewable v = viewable("login.fmt", null, "message", "Invalid name or password: " + name);
   234             return Response.status(1).entity(v).build();
   235         }
   236     }
   237 
   238     @GET
   239     @Produces(MediaType.TEXT_HTML)
   240     public Response welcome(@QueryParam("maxItems") @DefaultValue("10") int maxItems) {
   241         Viewable v = checkLogin();
   242         ResponseBuilder resp = Response.ok();
   243         if (v == null) {
   244             v = welcomeImpl("maxItems", maxItems);
   245         }
   246         CacheControl cc = new CacheControl();
   247         cc.setNoCache(true);
   248         resp.cacheControl(cc);
   249         return resp.entity(v).build();
   250     }
   251 
   252     @GET
   253     @Path("games/{id}.png")
   254     @Produces("image/png")
   255     public Response getBoardImage(
   256         @PathParam("id") String id,
   257         @QueryParam("fieldSize") @DefaultValue("50") int fieldSize,
   258         @QueryParam("move") @DefaultValue("-1") int move
   259     ) throws IllegalPositionException {
   260         WebResource wr = base.path("games").path(id);
   261         if (move != -1) {
   262             wr = wr.queryParam("move", "" + move);
   263         }
   264         String txt = wr.accept(MediaType.TEXT_PLAIN).get(String.class);
   265         Board b = Board.valueOf(txt);
   266 //        Board b = new Board(txt);
   267         ResponseBuilder resp = Response.ok();
   268         CacheControl cc = new CacheControl();
   269         cc.setNoCache(true);
   270         resp.cacheControl(cc);
   271         return resp.entity(BoardImage.draw(b, fieldSize)).build();
   272     }
   273 
   274 
   275     private Response board(String id) {
   276         return board(id, "", null);
   277     }
   278     @GET
   279     @Path("games/{id}/")
   280     @Produces(MediaType.TEXT_HTML)
   281     public Response board(
   282         @PathParam("id") String id,
   283         @QueryParam("format") @DefaultValue("") String format,
   284         @QueryParam("move") @DefaultValue("-1") String move
   285     ) {
   286         return board(id, null, format, move);
   287     }
   288     private Response board(@PathParam("id") String id, String msg, String format, String m) {
   289         Viewable v = checkLogin();
   290         if (v != null) {
   291             return Response.ok().entity(v).build();
   292         }
   293         int move;
   294         try {
   295             move = Integer.parseInt(m);
   296         } catch (NumberFormatException ex) {
   297             move = -1;
   298         }
   299         WebResource url = base.path("games").queryParam("loginID", uuid).path(id);
   300         if (move >= 0) {
   301             url = url.queryParam("move", "" + move);
   302         }
   303         ResponseBuilder resp = Response.ok();
   304         CacheControl cc = new CacheControl();
   305         cc.setNoCache(true);
   306         resp.cacheControl(cc);
   307         Cookie cFormat = headers.getCookies().get("format");
   308         if (format.length() == 0) {
   309             if (cFormat != null) {
   310                 format = cFormat.getValue();
   311             } else {
   312                 if (isMobile(headers)) {
   313                     format = "small";
   314                 }
   315             }
   316         } else {
   317             if (cFormat == null || !format.equals(cFormat.getValue())) {
   318                 resp.cookie(new NewCookie("format", format));
   319             }
   320         }
   321 
   322         Document doc = url.accept(MediaType.TEXT_XML).get(Document.class);
   323         Board b;
   324         String t = doc.getElementsByTagName("board").item(0).getTextContent();
   325         try {
   326             b = Board.valueOf(doc.getElementsByTagName("board").item(0).getTextContent());
   327         } catch (Exception ex) {
   328 //            b = new Board(t);
   329 //        } catch (IllegalStateException ex) {
   330             Exceptions.printStackTrace(ex);
   331             b = Board.empty();
   332         }
   333         String bCode = null;
   334         try{
   335             bCode = stat.path("openings").path(b.getCode()+".check").queryParam("loginID", user.getId()).accept(MediaType.TEXT_PLAIN).get(String.class);
   336         }catch(Exception e){
   337             bCode = null;
   338         }
   339         if(bCode == null || "".equals(bCode))
   340             v = viewable("game.fmt", doc, "message", msg, "format", format, "board", b,"textPicture", boardToPicture(b));
   341         else
   342             v = viewable("game.fmt", doc, "message", msg, "format", format, "board", b,"textPicture", boardToPicture(b),"bCode", bCode);
   343         return resp.entity(v).build();
   344     }
   345 
   346     private static String boardToPicture(Board b) {
   347         StringWriter w = new StringWriter();
   348         try {
   349             b.write(w);
   350         } catch (IOException ex) {
   351             return ex.toString();
   352         }
   353         return w.toString();
   354     }
   355 
   356     @GET
   357     @Path("games/{id}/move")
   358     @Produces(MediaType.TEXT_HTML)
   359     public Response move(
   360         @PathParam("id") String id,
   361         @QueryParam("type") String type,
   362         @QueryParam("direction") String direction,
   363         @QueryParam("direction-next") @DefaultValue("") String directionNext,
   364         @QueryParam("column") @DefaultValue("") String column,
   365         @QueryParam("row") @DefaultValue("") String row
   366     ) {
   367         Viewable v = checkLogin();
   368         if (v != null) {
   369             return Response.ok().entity(v).build();
   370         }
   371         WebResource wr = base.path("games").path(id).
   372             queryParam("loginID", uuid).
   373             queryParam("player", user.getId());
   374         try {
   375             if (type.equals("resign")) {
   376                 wr.queryParam("move", "RESIGN").put();
   377                 return board(id);
   378             }
   379             if (type.equals("fence")) {
   380                 wr.queryParam("move", direction.charAt(0) + column + row).put();
   381                 return board(id);
   382             }
   383             if (type.equals("move")) {
   384                 wr.queryParam("move", direction + directionNext).put();
   385                 return board(id);
   386             }
   387         } catch (UniformInterfaceException ex) {
   388             return board(id, "WRONG_MOVE", "-1");
   389         }
   390         return board(id);
   391     }
   392 
   393     @GET
   394     @Path("games/{id}/comment")
   395     @Produces(MediaType.TEXT_HTML)
   396     public Response comment(
   397         @PathParam("id") String id,
   398         @QueryParam("comment") String comment
   399     ) {
   400         Viewable v = checkLogin();
   401         if (v != null) {
   402             return Response.ok().entity(v).build();
   403         }
   404         WebResource wr = base.path("games").path(id).
   405             queryParam("loginID", uuid).
   406             queryParam("player", user.getId()).
   407             queryParam("comment", comment);
   408         wr.put();
   409         
   410         return board(id);
   411     }
   412 
   413     @GET
   414     @Path("games/create")
   415     @Produces(MediaType.TEXT_HTML)
   416     public Response create(
   417         @QueryParam("white") String white,
   418         @QueryParam("black") String black
   419     ) {
   420         Viewable v = checkLogin();
   421         if (v != null) {
   422             return Response.status(Response.Status.FORBIDDEN).entity(v).build();
   423         }
   424 
   425         if (user.getId().equals(white) || user.getId().equals(black)) {
   426             Object obj =
   427                 base.path("games").
   428                 queryParam("loginID", uuid).
   429                 queryParam("white", white).
   430                 queryParam("black", black).accept(MediaType.TEXT_XML).post(Document.class);
   431             return Response.ok(welcomeImpl()).build();
   432         } else {
   433             return Response.status(Response.Status.NOT_FOUND).
   434                 entity(welcomeImpl("message", "You (" + user.getId() + ") must be white or black!")).build();
   435         }
   436     }
   437 
   438     private Viewable welcomeImpl(Object... args) {
   439         final Document got = base.path("games").queryParam("loginID", uuid).accept(MediaType.TEXT_XML).get(Document.class);
   440         return viewable("index.fmt", got, args);
   441     }
   442 
   443     @Path("requests")
   444     public Requests getRequests() {
   445         if (requests == null) {
   446             requests = new Requests(web.path("requests"));
   447         }
   448         return requests;
   449     }
   450 
   451 
   452     @GET
   453     @Path("options")
   454     public Response changeOptions(
   455         @QueryParam("email") String email,
   456         @QueryParam("language") String language,
   457         @QueryParam("verified") String verified
   458     ) throws IOException {
   459         Viewable v = checkLogin();
   460         if (v != null) {
   461             return Response.status(Response.Status.FORBIDDEN).entity(v).build();
   462         }
   463 
   464         if (email != null) {
   465             if (getRequests().isVerified(verified)) {
   466                 UserInfo ui = base.path("users/" + user.getId()).
   467                     queryParam("loginID", uuid).
   468                     queryParam("name", "email").
   469                     queryParam("value", email).accept(MediaType.TEXT_XML).post(UserInfo.class);
   470             } else {
   471                 WebResource request;
   472                 request = web.path("options").queryParam("name", "email").queryParam("email", email);
   473                 URI callback = getRequests().register(login(), request);
   474 
   475                 ResourceBundle rb = bundle(null);
   476                 String subject = rb.getString("MSG_ChangeEmailSubject");
   477                 String text = MessageFormat.format(rb.getString("MSG_ChangeEmailText"), user.getId(), callback);
   478                 EmailService.getDefault().sendEmail(email, subject, text);
   479                 return Response.ok(viewable("email.fmt", null)).build();
   480 
   481             }
   482         }
   483 
   484         if (language != null) {
   485             UserInfo ui = base.path("users/" + user.getId()).
   486                 queryParam("loginID", uuid).
   487                 queryParam("name", "language").
   488                 queryParam("value", language).
   489                 accept(MediaType.TEXT_XML).post(UserInfo.class);
   490         }
   491 
   492         return welcome(10);
   493     }
   494 
   495     @GET
   496     @Path("elo")
   497     @Produces(MediaType.TEXT_HTML)
   498     public Response getEloList(
   499             @QueryParam("historyId") @DefaultValue("0") Integer historyId){
   500         Viewable v = checkLogin();
   501         if (v != null) {
   502             return Response.status(Response.Status.FORBIDDEN).entity(v).build();
   503         }
   504         final Document got = stat.path("elo").path("list").path(historyId.toString()).accept(MediaType.TEXT_XML).get(Document.class);
   505         return Response.ok(viewable("elo.fmt", got, "historyId", historyId)).build();
   506     }
   507     
   508     @GET
   509     @Path("openings")
   510     @Produces(MediaType.TEXT_HTML)
   511     public Response getOpeningRoot(){
   512         return getOpeningNode("ROOT");
   513     }
   514 
   515     @GET
   516     @Path("openings/{code}")
   517     @Produces(MediaType.TEXT_HTML)
   518     public Response getOpeningNode(@PathParam("code") String code){
   519         Viewable v = checkLogin();
   520         if (v != null) {
   521             return Response.status(Response.Status.FORBIDDEN).entity(v).build();
   522         }
   523         final Document got = stat.path("openings").path(code).queryParam("loginID", user.getId()).accept(MediaType.TEXT_XML).get(Document.class);
   524         Board b;
   525         try {
   526             b = Board.valueOf(got.getElementsByTagName("nodeCode").item(0).getTextContent());
   527         } catch (Exception ex) {
   528             Exceptions.printStackTrace(ex);
   529             b = Board.empty();
   530         }
   531         return Response.ok(viewable("openings.fmt", got, "whitefences",b.getPlayers().get(0).getFences(),"blackfences",b.getPlayers().get(1).getFences())).build();
   532     }
   533 
   534     @GET
   535     @Path("openings/{code}/{status}")
   536     @Produces(MediaType.TEXT_HTML)
   537     public Response getOpeningNodeGames(@PathParam("code") String code, @PathParam("status") String status){
   538         Viewable v = checkLogin();
   539         if (v != null) {
   540             return Response.status(Response.Status.FORBIDDEN).entity(v).build();
   541         }
   542         final Document got = stat.path("openings").path(code).path(status).queryParam("loginID", user.getId()).accept(MediaType.TEXT_XML).get(Document.class);
   543         return Response.ok(viewable("opening_games.fmt", got,"code",code,"color",status)).build();
   544     }
   545 
   546     @GET
   547     @Path("openings/{code}.png")
   548     @Produces("image/png")
   549     public Response getOpeningBoardImage(
   550         @PathParam("code") String code,
   551         @QueryParam("fieldSize") @DefaultValue("40") int fieldSize
   552     ) throws IllegalPositionException {
   553         Board b = Board.valueOf(code);
   554         ResponseBuilder resp = Response.ok();
   555         CacheControl cc = new CacheControl();
   556         cc.setNoCache(true);
   557         resp.cacheControl(cc);
   558         return resp.entity(BoardImage.draw(b, fieldSize)).build();
   559     }
   560 
   561     //
   562     // start the server
   563     //
   564 
   565     public static void main(String[] params) throws Exception {
   566         List<String> args = new ArrayList<String>(Arrays.asList(params));
   567 
   568         String publicURL = null;
   569         if (args.size() >= 2 && args.get(0).equals("--url")) {
   570             publicURL = args.get(1);
   571             args.remove(0);
   572             args.remove(0);
   573         }
   574 
   575         int port = 9333;
   576         if (args.size() > 1) {
   577             port = Integer.parseInt(args.get(0));
   578         }
   579         String remoteAPI = args.size() >= 2 ? args.get(1) : null;
   580         String remoteStatistics = args.size() >= 3 ? args.get(2) : null;
   581 
   582         Locale.setDefault(Locale.ROOT);
   583 
   584         Callable<Void> r = startServers(port, remoteAPI, remoteStatistics, publicURL);
   585 
   586         if (args.size() < 3 || !args.get(args.size() - 1).equals("--kill")) {
   587             System.out.println("Hit enter to stop it...");
   588             System.in.read();
   589         } else {
   590             synchronized (UI.class) {
   591                 UI.class.wait();
   592             }
   593         }
   594         r.call();
   595         System.exit(0);
   596     }
   597 
   598     static Callable<Void> startServers(int port, String remoteAPI, String remoteStatistics, String publicURL) throws Exception {
   599         Client client = new Client();
   600         Client client1 = new Client();
   601 
   602         final HttpServer apiServer;
   603         if (remoteAPI == null) {
   604             throw new IllegalArgumentException("Provide URL to API server"); // NOI18N
   605         } else {
   606             base = client.resource(new URI(remoteAPI));
   607             apiServer = null;
   608         }
   609 
   610         if (remoteStatistics != null) {
   611             stat = client1.resource(new URI(remoteStatistics));
   612         } else {
   613             stat = client1.resource(new URI("http://localhost:9444"));
   614         }
   615 
   616         ResourceConfig rc = new PackagesResourceConfig(
   617             "cz.xelfi.quoridor.freemarkerdor"
   618         );
   619 
   620         final String baseUri = "http://localhost:" + port + "/";
   621         if (publicURL == null) {
   622             publicURL = baseUri;
   623         }
   624         final HttpServer server = HttpServerFactory.create(baseUri, rc);
   625         Client c3 = new Client();
   626         web = c3.resource(publicURL);
   627         server.start();
   628         System.out.println("Quoridor started at port " + port);
   629 
   630         return new Callable<Void>() {
   631             public Void call() throws Exception {
   632                 if (apiServer != null) {
   633                     apiServer.stop(0);
   634                 }
   635                 server.stop(0);
   636                 return null;
   637             }
   638         };
   639     }
   640 
   641     private ResourceBundle bundle(Locale[] locale) {
   642         ResourceBundle rb = null;
   643         String lng = user == null ? null : user.getProperty("language"); // NOI18N
   644         if (lng != null) {
   645             try {
   646                 Locale l = new Locale(lng);
   647                 rb = ResourceBundle.getBundle("cz.xelfi.quoridor.freemarkerdor.UI.Bundle", l);
   648                 if (locale != null) {
   649                     locale[0] = l;
   650                 }
   651             } catch (MissingResourceException e) {
   652                 // OK
   653             }
   654         }
   655         if (rb == null) {
   656             try {
   657                 for (Locale l : headers.getAcceptableLanguages()) {
   658                     try {
   659                         rb = ResourceBundle.getBundle("cz.xelfi.quoridor.freemarkerdor.UI.Bundle", l);
   660                         if (locale != null) {
   661                             locale[0] = l;
   662                         }
   663                         break;
   664                     } catch (MissingResourceException e) {
   665                         // OK
   666                     }
   667                 }
   668             } catch (WebApplicationException ex) {
   669                 // OK, can't parse header
   670             }
   671         }
   672         if (rb == null) {
   673             rb = ResourceBundle.getBundle("cz.xelfi.quoridor.freemarkerdor.UI.Bundle", Locale.ENGLISH);
   674             if (locale != null) {
   675                 locale[0] = Locale.ENGLISH;
   676             }
   677         }
   678         return rb;
   679     }
   680 
   681     private Viewable viewable(String page, Document doc, Object... more) {
   682         Locale[] locale = new Locale[1];
   683         ResourceBundle rb = bundle(locale);
   684 
   685         Map<String,Object> map = new HashMap<String,Object>();
   686         class ConvertToDate extends HashMap<Object,Object> {
   687             @Override
   688             public Object get(Object o) {
   689                 long time = Long.parseLong(o.toString());
   690                 return new Date(time);
   691             }
   692         }
   693 
   694         map.put("locale", locale[0].toString());
   695         map.put("doc", doc);
   696         if (user != null) {
   697             map.put("user", user.getId());
   698             map.put("email", user.getProperty("email"));
   699         }
   700         map.put("bundle", rb);
   701         map.put("toDate", new ConvertToDate());
   702         map.put("now", System.currentTimeMillis());
   703         map.put("version", version);
   704         for (int i = 0; i < more.length; i += 2) {
   705             map.put((String)more[i],more[i + 1]);
   706         }
   707         return new Viewable(page, map);
   708     }
   709 
   710 
   711     private static boolean isMobile(HttpHeaders headers) {
   712         final String[] keywords = {
   713             "Profile/MIDP",
   714         };
   715         List<String> agent = headers.getRequestHeader(HttpHeaders.USER_AGENT);
   716         if (agent != null) {
   717             for (String a : agent) {
   718                 for (String k : keywords) {
   719                     if (a.contains(k)) {
   720                         return true;
   721                     }
   722                 }
   723             }
   724         }
   725         return false;
   726     }
   727 
   728 }