/** * Copyright 2010 The University of Nottingham * * This file is part of lobbyservice. * * lobbyservice is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * lobbyservice is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with lobbyservice. If not, see <http://www.gnu.org/licenses/>. * */ package uk.ac.horizon.ug.lobby.browser; import com.google.appengine.api.datastore.Key; import com.google.appengine.api.datastore.KeyFactory; import com.google.appengine.api.users.User; import com.google.appengine.api.users.UserService; import com.google.appengine.api.users.UserServiceFactory; import java.io.BufferedReader; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.Writer; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.TreeSet; import java.util.logging.Level; import java.util.logging.Logger; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.EntityTransaction; import javax.persistence.Query; import javax.servlet.http.*; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.json.JSONWriter; import uk.ac.horizon.ug.lobby.ConfigurationUtils; import uk.ac.horizon.ug.lobby.Constants; import uk.ac.horizon.ug.lobby.HttpUtils; import uk.ac.horizon.ug.lobby.RequestException; import uk.ac.horizon.ug.lobby.browser.JoinUtils.JoinAuthInfo; import uk.ac.horizon.ug.lobby.model.Account; import uk.ac.horizon.ug.lobby.model.EMF; import uk.ac.horizon.ug.lobby.model.GameClientTemplate; import uk.ac.horizon.ug.lobby.model.GameIndex; import uk.ac.horizon.ug.lobby.model.GameInstance; import uk.ac.horizon.ug.lobby.model.GameInstanceFactory; import uk.ac.horizon.ug.lobby.model.GameInstanceFactoryLocationType; import uk.ac.horizon.ug.lobby.model.GameInstanceFactoryStatus; import uk.ac.horizon.ug.lobby.model.GameInstanceFactoryType; import uk.ac.horizon.ug.lobby.model.GameInstanceNominalStatus; import uk.ac.horizon.ug.lobby.model.GameInstanceSlot; import uk.ac.horizon.ug.lobby.model.GameInstanceSlotStatus; import uk.ac.horizon.ug.lobby.model.GameInstanceStatus; import uk.ac.horizon.ug.lobby.model.GameServer; import uk.ac.horizon.ug.lobby.model.GameTemplate; import uk.ac.horizon.ug.lobby.model.GameTemplateVisibility; import uk.ac.horizon.ug.lobby.model.ServerConfiguration; import uk.ac.horizon.ug.lobby.protocol.ClientRequestScope; import uk.ac.horizon.ug.lobby.protocol.ClientRequirement; import uk.ac.horizon.ug.lobby.protocol.GameJoinResponseStatus; import uk.ac.horizon.ug.lobby.protocol.GameQuery; import uk.ac.horizon.ug.lobby.protocol.GameTemplateInfo; import uk.ac.horizon.ug.lobby.protocol.JSONUtils; import uk.ac.horizon.ug.lobby.protocol.LocationConstraint; import uk.ac.horizon.ug.lobby.protocol.TimeConstraint; import uk.ac.horizon.ug.lobby.server.CronExpressionException; import uk.ac.horizon.ug.lobby.server.FactoryUtils; import uk.ac.horizon.ug.lobby.user.UserGameTemplateServlet; import uk.me.jstott.jcoord.LatLng; /** * Get Game (templates) info, for public browsing * * @author cmg * */ @SuppressWarnings("serial") public class QueryGameTemplateServlet extends HttpServlet implements Constants { private static final double ONE_MILLION = 1000000; private static final double ONE_THOUSAND = 1000; static Logger logger = Logger.getLogger(QueryGameTemplateServlet.class.getName()); private GameTemplate getGameTemplate(HttpServletRequest req) throws RequestException { String id = HttpUtils.getIdFromPath(req); EntityManager em = EMF.get().createEntityManager(); try { Key key = GameTemplate.idToKey(id); GameTemplate gt = em.find(GameTemplate.class, key); if (gt==null) throw new RequestException(HttpServletResponse.SC_NOT_FOUND, "GameTemplate "+id+" not found"); return gt; } finally { em.close(); } } @Override public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { // parse request String line = null; String auth = null; GameQuery gq = null; GameTemplate gt = null; try { BufferedReader br = req.getReader(); line = br.readLine(); JSONObject json = new JSONObject(line); gq = JSONUtils.parseGameQuery(json); // second line is digital signature (if given) auth = br.readLine(); logger.info("GameQuery "+gq); gt = getGameTemplate(req); } catch (RequestException e) { resp.sendError(e.getErrorCode(), e.getMessage()); return; } catch (JSONException e) { logger.warning(e.toString()); resp.sendError(HttpServletResponse.SC_BAD_REQUEST, e.toString()); return; } try { JoinUtils.JoinAuthInfo jai = JoinUtils.authenticateOptional(gq.getClientId(), gq.getDeviceId(), req.getRequestURI(), line, auth); GameIndex gindex = handleGameQuery(gq, gt, jai); // response JSONUtils.sendGameIndex(resp, gindex); } catch (RequestException e) { resp.sendError(e.getErrorCode(), e.getMessage()); return; } catch (JSONException e) { logger.warning(e.toString()); resp.sendError(HttpServletResponse.SC_BAD_REQUEST, e.toString()); return; } } public GameIndex testHandleGameQuery(GameQuery gq, GameTemplate gt, JoinAuthInfo jai) throws RequestException, JSONException { return handleGameQuery(gq, gt, jai); } public static final int DEFAULT_MAX_RESULTS = 30; static GameIndex handleGameQuery(GameQuery gq, GameTemplate gt, JoinAuthInfo jai) throws RequestException, JSONException { // get some of the general server info ServerConfiguration sc = ConfigurationUtils.getServerConfiguration(); GameIndex servergindex = sc.getGameIndex(); GameIndex gindex = new GameIndex(); gindex.setDocs(servergindex.getDocs()); gindex.setGenerator(servergindex.getGenerator()); gindex.setLastBuildDate(System.currentTimeMillis()); gindex.setTtlMinutes(servergindex.getTtlMinutes()); gindex.setVersion(servergindex.getVersion()); // template-specific - describe template in top-level index gindex.setTitle(gt.getTitle()); gindex.setDescription(gt.getDescription()); gindex.setLanguage(gt.getLanguage()); gindex.setImageUrl(gt.getImageUrl()); gindex.setLink(gt.getLink()); // for now actual instances but not factories will count against max int maxResults = DEFAULT_MAX_RESULTS; if (gq.getMaxResults()!=null) maxResults = gq.getMaxResults(); if (maxResults<=0) return gindex; // matching combinations List<GameTemplateInfo> gtis = new LinkedList<GameTemplateInfo>(); gindex.setItems(gtis); // Check GameClientTemplate for clientType, clientTitle, locationSpecific and version List<GameClientTemplate> posgcts = getGameClientTemplates(gq, gt.getId()); logger.info("Game "+gt.getId()+" has "+posgcts.size()+" possible clients"); // post-filter - location boolean locationSpecific = false; boolean locationIndependent = false; List<GameClientTemplate> gcts = new LinkedList<GameClientTemplate>(); List<GameClientTemplate> noloc_gcts = new LinkedList<GameClientTemplate>(); for (GameClientTemplate gct : posgcts) { if (gct.isLocationSpecific()) locationSpecific = true; else { locationIndependent = true; noloc_gcts.add(gct); } // add gcts (if location ok) gcts.add(gct); } //============================================================================ // GameSlots? if (jai.gc!=null) { EntityManager em = EMF.get().createEntityManager(); try { Query q = null; if (jai.account==null) { // TODO slot status? q = em.createQuery("SELECT x FROM "+GameInstanceSlot.class.getSimpleName()+" x WHERE x."+ GAME_CLIENT_KEY+" = :"+GAME_CLIENT_KEY+" AND x."+GAME_TEMPLATE_ID+" = :"+GAME_TEMPLATE_ID+ " AND x."+STATUS+" IN ( '"+GameInstanceSlotStatus.ACTIVE+"', '"+GameInstanceSlotStatus.ALLOCATED+"')"); q.setParameter(GAME_CLIENT_KEY, jai.gc.getKey()); } else { q = em.createQuery("SELECT x FROM "+GameInstanceSlot.class.getSimpleName()+" x WHERE x."+ ACCOUNT_KEY+" = :"+ACCOUNT_KEY+" AND x."+GAME_TEMPLATE_ID+" = :"+GAME_TEMPLATE_ID+ " AND x."+STATUS+" IN ( '"+GameInstanceSlotStatus.ACTIVE+"', '"+GameInstanceSlotStatus.ALLOCATED+"')"); q.setParameter(ACCOUNT_KEY, jai.account.getKey()); } q.setParameter(GAME_TEMPLATE_ID, gt.getId()); List<GameInstanceSlot> posgiss = (List<GameInstanceSlot>)q.getResultList(); for (GameInstanceSlot posgis: posgiss) { if (posgis.getGameInstanceKey()==null) { logger.warning("GameInstanceSlot without gameInstanceKey: "+posgis); continue; } GameInstance gi = em.find(GameInstance.class, posgis.getGameInstanceKey()); if (gi==null) { logger.warning("Could not find GameInstance for slot: "+posgis); continue; } boolean include = false; // include? switch (gi.getNominalStatus()) { case CANCELLED: case ENDED: // no break; case PLANNED: case TEMPORARILY_UNAVAILABLE: case AVAILABLE: if (checkGameInstanceSlotTime(gi, gq.getTimeConstraint())) include = true; break; } boolean locationOk = true; if (locationSpecific && gq.getLocationConstraint()!=null) { LocationConstraint lc = gq.getLocationConstraint(); locationOk = checkLocationConstraint(lc, gi.getLatitudeE6(), gi.getLongitudeE6(), gi.getRadiusMetres()); } if (include) { GameTemplateInfo gti = new GameTemplateInfo(); gti.setGameInstance(gi); gti.setGameTemplate(gt); // GameClientTemplates?? gti.setJoinUrl(QueryGameTemplateServlet.makeJoinUrl(sc, gi)); gti.setGameSlotId(posgis.getKey().getName()); if (posgis.getGameClientKey()!=null) { // client id is key name gti.setClientId(posgis.getGameClientKey().getName()); } else logger.warning("GameInstanceSlot does not identify client: "+posgis); // TODO clients as already identified? if (locationOk) gti.setGameClientTemplates(gcts); else gti.setGameClientTemplates(noloc_gcts); gtis.add(gti); } } } finally { em.close(); } } // =========================================================================== if (gcts.size()==0) { logger.info("Game "+gt.getId()+" does not support any client(s) specified"); // no matching client - so can't play // TODO more detail? alternative? gtis.add(getGameIndexMessage("Client not supported", "This game does not support you client", null)); return gindex; } logger.info("Found "+gcts.size()+" possible client templates, of which "+noloc_gcts.size()+" location-independent"); // ensure GameInstanceFactorys get a chance to create relevant GameInstances... EntityManager em = EMF.get().createEntityManager(); try { // Check GameInstance for startTime, endTime, location (if location-specific) and nominalStatus (not CANCELLED) Map<String,Object> qps = new HashMap<String,Object>(); StringBuilder qb = new StringBuilder(); //qps = new HashMap<String,Object>(); //qb = new StringBuilder(); qb.append("SELECT x FROM GameInstance x WHERE x."+GAME_TEMPLATE_ID+" = :"+GAME_TEMPLATE_ID+" AND x."+NOMINAL_STATUS+" IN ( 'PLANNED', 'POSSIBLE', 'AVAILABLE', 'TEMPORARILY_UNAVAILABLE' ) AND x."+VISIBILITY+" = '"+GameTemplateVisibility.PUBLIC.toString()+"'"); qps.put(GAME_TEMPLATE_ID, gt.getId()); //qps.put(GameInstanceNominalStatus.CANCELLED.toString(), GameInstanceNominalStatus.CANCELLED); //qps.put(GameInstanceNominalStatus.ENDED.toString(), GameInstanceNominalStatus.ENDED); if (gq.getIncludeFullGames()==null || !gq.getIncludeFullGames()) { qb.append(" AND x."+FULL+" = FALSE"); } // query constraints // NB GAE only allows range query on one variable - we'll use startTime for now if (gq.getTimeConstraint()!=null) { TimeConstraint tc = gq.getTimeConstraint(); boolean usedEndTime = false, usedStartTime = false; if (tc.isLimitEndTime() && tc.getMaxTime()!=null) { // end time is limited and we have max time, so check game end time against max qps.put(MAX_TIME, tc.getMaxTime()); qb.append(" AND x."+END_TIME+" <= :"+MAX_TIME); usedEndTime = true; } else if (tc.getMaxTime()!=null) { // end time is not limited, but we still have max time, so check start against max qps.put(MAX_TIME, tc.getMaxTime()); qb.append(" AND x."+START_TIME+" <= :"+MAX_TIME); usedStartTime = true; } if (!usedEndTime && !tc.isIncludeStarted() && tc.getMinTime()!=null) { // only consider games starting after min... qps.put(MIN_TIME, tc.getMinTime()); qb.append(" AND x."+START_TIME+" >= :"+MIN_TIME); usedStartTime = true; } else if (!usedStartTime && tc.isIncludeStarted() && tc.getMinTime()!=null) { // no limit on early start, but we must get some playing time in after our earliest start... if (tc.getMinDurationMs()!=null) qps.put(MIN_TIME, tc.getMinTime()+tc.getMinDurationMs()); else qps.put(MIN_TIME, tc.getMinTime()); qb.append(" AND x."+END_TIME+" >= :"+MIN_TIME); usedEndTime = true; } if (usedEndTime) qb.append(" ORDER BY x."+END_TIME+" ASC"); else qb.append(" ORDER BY x."+START_TIME+" ASC"); } else qb.append(" ORDER BY x."+START_TIME+" ASC"); logger.info("Query: "+qb.toString()); Query q = em.createQuery(qb.toString()); for (String qp : qps.keySet()) { q.setParameter(qp, qps.get(qp)); } List<GameInstance> posgis = (List<GameInstance>)q.getResultList(); logger.info("Found "+posgis.size()+" possible GameInstances on initial query"); for (GameInstance gi : posgis) { if (!checkGameInstanceTime(gi, gq.getTimeConstraint())) continue; // location boolean locationOk = true; if (locationSpecific && gq.getLocationConstraint()!=null) { LocationConstraint lc = gq.getLocationConstraint(); locationOk = checkLocationConstraint(lc, gi.getLatitudeE6(), gi.getLongitudeE6(), gi.getRadiusMetres()); } if (locationOk || locationIndependent) { // already done? boolean done = false; for (GameTemplateInfo gti2 : gtis) if (gti2.getGameInstance().getKey().equals(gi.getKey())) { done = true; break; } if (!done) { // useful GameTemplateInfo gti = new GameTemplateInfo(); gti.setGameTemplate(gt); gti.setGameInstance(gi); gti.setJoinUrl(makeJoinUrl(sc, gi)); if (locationOk) gti.setGameClientTemplates(gcts); else gti.setGameClientTemplates(noloc_gcts); gtis.add(gti); if (gtis.size()>=maxResults) // no more instances... break; } } } } finally { em.close(); } em = EMF.get().createEntityManager(); try { //================================================================================== // now check GameInstanceTemplates... // Check GameInstance for startTime, endTime, location (if location-specific) and nominalStatus (not CANCELLED) HashMap<String,Object> qps = new HashMap<String,Object>(); StringBuilder qb = new StringBuilder(); //qps = new HashMap<String,Object>(); //qb = new StringBuilder(); qb.append("SELECT x FROM GameInstanceFactory x WHERE x."+GAME_TEMPLATE_ID+" = :"+GAME_TEMPLATE_ID+" AND x."+STATUS+" = '"+GameInstanceFactoryStatus.ACTIVE.toString()+"' AND x."+VISIBILITY+" = '"+GameTemplateVisibility.PUBLIC.toString()+"'"); qps.put(GAME_TEMPLATE_ID, gt.getId()); // query constraints // NB GAE only allows range query on one variable - we'll use startTime for now if (gq.getTimeConstraint()!=null) { TimeConstraint tc = gq.getTimeConstraint(); boolean usedMaxTime = false, usedMinTime = false; if (tc.getMinTime()!=null) { qps.put(MIN_TIME, tc.getMinTime()); qb.append(" AND x."+MAX_TIME+" >= :"+MIN_TIME); usedMaxTime = true; } else if (tc.getMaxTime()!=null) { qps.put(MAX_TIME, tc.getMaxTime()); qb.append(" AND x."+MIN_TIME+" <= :"+MAX_TIME); usedMinTime = true; } if (usedMaxTime) qb.append(" ORDER BY x."+MAX_TIME+" ASC"); else qb.append(" ORDER BY x."+MIN_TIME+" ASC"); } else qb.append(" ORDER BY x."+MIN_TIME+" ASC"); logger.info("Query: "+qb.toString()); //Query q Query q = em.createQuery(qb.toString()); for (String qp : qps.keySet()) { q.setParameter(qp, qps.get(qp)); } List<GameInstanceFactory> posgifs = (List<GameInstanceFactory>)q.getResultList(); logger.info("Found "+posgifs.size()+" possible GameInstanceFactories on initial query"); // matching combinations //done: List<GameTemplateInfo> gtis = new LinkedList<GameTemplateInfo>(); for (GameInstanceFactory gif : posgifs) { // location boolean locationOk = true; // only 'SPECIFIED_LOCATION' limits at this stage if (locationSpecific && gq.getLocationConstraint()!=null && gif.getLocationType()==GameInstanceFactoryLocationType.SPECIFIED_LOCATION) { LocationConstraint lc = gq.getLocationConstraint(); locationOk = checkLocationConstraint(lc, gif.getLatitudeE6(), gif.getLongitudeE6(), gif.getRadiusMetres()); } /* // PLAYER_LOCATION - not yet supported else if (gif.getLocationType()==GameInstanceFactoryLocationType.PLAYER_LOCATION) { if (gq.getLatitudeE6()==null || gq.getLongitudeE6()==null) { logger.warning("GameInstanceFactory "+gif.getTitle()+" is PLAYER_LOCATION - player location not provided in query"); // can't use even if we have a locationIndependent client! continue; } } */ if (!locationOk && !locationIndependent) continue; // next gif // TODO PLAYER_LOCATION might limit success due to maxNumInstancesConcurrent! // calculate next game time from CRON pattern - check there is one and it is in range! if (gif.getStartTimeOptionsJson()==null) { logger.warning("GameTemplateFactory has no startTimeOptionsJson: "+gif); continue; } TimeConstraint tc = gq.getTimeConstraint(); // earliest possible start time that might be relevant... long minTime = System.currentTimeMillis(); if (gif.getServerCreateTimeOffsetMs()<0) minTime = minTime - gif.getServerCreateTimeOffsetMs(); // will make it bigger! if (tc!=null && tc.getMinTime()!=null && tc.getMinTime()>minTime) minTime = tc.getMinTime(); // Can't factory start retrospectively //if (tc!=null && tc.isIncludeStarted()) // TODO long-running / variable length //minTime = minTime-gif.getDurationMs(); if (gif.getMinTime()>minTime) minTime = gif.getMinTime(); long maxTime = Long.MAX_VALUE; if (tc!=null && tc.getMaxTime()!=null) maxTime = tc.getMaxTime(); if (gif.getMaxTime()<maxTime) maxTime = gif.getMaxTime(); long firstStartTime = 0; try { TreeSet values[] = FactoryUtils.parseTimeOptionsJson(gif.getStartTimeOptionsJson()); // find fist CRON firing no earlier than that firstStartTime = FactoryUtils.getNextCronTime(gif.getStartTimeCron(), values, minTime, maxTime); } catch (CronExpressionException cee) { logger.warning("GameTemplateFactory error in startTimeCron: "+cee+" for "+gif); continue; } if (firstStartTime==0) { logger.warning("GameTemplateFactory has no startTime in range "+minTime+"-"+maxTime+": "+gif); continue; } if (firstStartTime<minTime) { logger.warning("FactoryUtils.getNextCronTime returned past startTime "+firstStartTime+" vs "+minTime); continue; } // GameInstance code... if (tc!=null && tc.getMinDurationMs()!=null && tc.getMinDurationMs() > gif.getDurationMs()) { logger.info("GameInstanceFactory "+gif.getTitle()+" too short: "+gif.getDurationMs()+" vs "+tc.getMinDurationMs()); continue; // not long enough } if (tc!=null && tc.getMaxDurationMs()!=null && tc.getMaxDurationMs() < gif.getDurationMs()) { logger.info("GameInstanceFactory "+gif.getTitle()+" too long: "+gif.getDurationMs()+" vs "+tc.getMaxDurationMs()); continue; // too long } // GameInstanceFactory in useful itself GameTemplateInfo gti = new GameTemplateInfo(); gti.setGameTemplate(gt); gti.setGameInstanceFactory(gif); // now in startTimeOptionsJson... // try { // gti.setGameTimeOptions(FactoryUtils.getGameTimeOptions(gif.getStartTimeCron(), firstStartTime)); // } catch (CronExpressionException e) { // logger.warning("Generating GameTimeOptions: "+e); // } gti.setFirstStartTime(firstStartTime); if (gif.getType()==GameInstanceFactoryType.ON_DEMAND) { // link to GIF newInstance request handler gti.setNewInstanceUrl(makeNewInstanceUrl(sc, gif)); } else { // note re-query, not join! gti.setQueryUrl(GetGameIndexServlet.makeQueryUrl(sc, gt)); } if (locationOk) gti.setGameClientTemplates(gcts); else gti.setGameClientTemplates(noloc_gcts); gtis.add(gti); } if (gtis.size()==0) { // TODO more detail, suggestions. gtis.add(getGameIndexMessage("No games found", "There were no games that matched your query", null)); } return gindex; } finally { em.close(); } } private static boolean checkGameInstanceTime(GameInstance gi, TimeConstraint tc) { // recheck times (for simplicity) if (tc!=null) { // start time... if (!tc.isIncludeStarted() && tc.getMinTime()!=null) { if (tc.getMinTime()>gi.getStartTime()) { logger.info("GameInstance "+gi.getTitle()+" starts too early ("+gi.getStartTime()+" vs "+tc.getMinTime()+")"); return false; // starts too early } } // our earliest possible start time... long ourStart = gi.getStartTime(); if (tc.getMinTime()!=null && tc.getMinTime()>ourStart) ourStart = tc.getMinTime(); // our latest possible end time... long ourEnd = gi.getEndTime(); // end time... if (tc.isLimitEndTime() && tc.getMaxTime()!=null) { if (gi.getEndTime()>tc.getMaxTime()) { logger.info("GameInstance "+gi.getTitle()+" end too late ("+gi.getEndTime()+" vs "+tc.getMaxTime()+")"); return false; // ends too late } } // duration long duration = ourEnd - ourStart; if (duration <= 0) { logger.info("GameInstance "+gi.getTitle()+" would end before it starts for us ("+gi.getStartTime()+"-"+gi.getEndTime()+", min="+tc.getMinTime()+")"); return false; // no time left } if (tc.getMinDurationMs()!=null && tc.getMinDurationMs() > duration) { logger.info("GameInstance "+gi.getTitle()+" too short: "+duration+" vs "+tc.getMinDurationMs()); return false; // not long enough } if (tc.getMaxDurationMs()!=null && tc.getMaxDurationMs() < duration) { logger.info("GameInstance "+gi.getTitle()+" too long: "+duration+" vs "+tc.getMaxDurationMs()); return false; // too long } } return true; } /** more lax since we are already playing */ private static boolean checkGameInstanceSlotTime(GameInstance gi, TimeConstraint tc) { // recheck times (for simplicity) if (tc!=null) { // any overlap will do if (tc.getMinTime()!=null) { if (gi.getEndTime()<tc.getMinTime()) return false; } if (tc.getMaxTime()!=null) { if (gi.getStartTime()>tc.getMaxTime()) return false; } // start time... // ok- we'll take account of includeStarted as well if (!tc.isIncludeStarted() && tc.getMinTime()!=null) { if (tc.getMinTime()>gi.getStartTime()) { logger.info("GameInstance "+gi.getTitle()+" starts too early ("+gi.getStartTime()+" vs "+tc.getMinTime()+")"); return false; // starts too early } } } return true; } /** * @param lc * @param latitudeE6 * @param longitudeE6 * @param radiusMetres * @return */ private static boolean checkLocationConstraint(LocationConstraint lc, int latitudeE6, int longitudeE6, double radiusMetres) throws RequestException { if (lc.getType()!=null) { switch(lc.getType()) { case CIRCLE: { if (lc.getLatitudeE6()==null || lc.getLongitudeE6()==null) throw new RequestException(HttpServletResponse.SC_BAD_REQUEST,"locationContraint/CIRCLE requires latitudeE6 and longitudeE6"); if (radiusMetres==0) { logger.info("GameInstance/Factory unlimited range - ok"); return true; // unlimited range } LatLng l1 = new LatLng(lc.getLatitudeE6()/ONE_MILLION, lc.getLongitudeE6()/ONE_MILLION); LatLng l2 = new LatLng(latitudeE6/ONE_MILLION, longitudeE6/ONE_MILLION); // km to m double distanceMetres = l1.distance(l2)*ONE_THOUSAND; // if the game centre is in my range or i am in the game's range... // (i.e. if i could travel to the game, or i am already somewhere in range) // (just allowing any overlap is probably too optimistic, e.g. the // nominal radius might include sea in order to be inclusive; requiring // containment probably too restrictive, esp. for big playing areas which // are likely to imply you only need to be in part of it) double queryRadius = (lc.getRadiusMetres()!=null) ? lc.getRadiusMetres() : 0; if (distanceMetres <= radiusMetres || distanceMetres <= queryRadius) { logger.info("GameInstance/Factory in range ("+distanceMetres+" vs "+radiusMetres+" and "+lc.getRadiusMetres()+")"); return true; } else { logger.info("GameInstance/Factory out of range ("+distanceMetres+" vs "+radiusMetres+" and "+lc.getRadiusMetres()+")"); } // failed return false; } } } return true; } private static List<GameClientTemplate> getGameClientTemplates(GameQuery gq, String gameTemplateId) { return getGameClientTemplates(gq.getClientTitle(), gq.getCharacteristicsJson(), gameTemplateId); } static List<GameClientTemplate> getGameClientTemplates(String clientTitle, String characteristicsJson, String gameTemplateId) { EntityManager em = EMF.get().createEntityManager(); try { // Check GameClientTemplate for clientType, clientTitle, locationSpecific and version Map<String,Object> qps = new HashMap<String,Object>(); StringBuilder qb = new StringBuilder(); qb.append("SELECT x FROM GameClientTemplate x WHERE x."+GAME_TEMPLATE_ID+" = :"+GAME_TEMPLATE_ID); qps.put(GAME_TEMPLATE_ID, gameTemplateId); if (clientTitle!=null) { qb.append(" AND x."+CLIENT_TITLE+" = :"+CLIENT_TITLE); qps.put(CLIENT_TITLE, clientTitle); } // post-check minor version & update version Query q = em.createQuery(qb.toString()); for (String qp : qps.keySet()) { q.setParameter(qp, qps.get(qp)); } List<GameClientTemplate> posgcts = (List<GameClientTemplate>)q.getResultList(); // post-filter List<GameClientTemplate> gcts = new LinkedList<GameClientTemplate>(); try { JSONObject characteristics = characteristicsJson!=null ? new JSONObject(characteristicsJson) : new JSONObject(); for (GameClientTemplate gct : posgcts) { try { List<ClientRequirement> crs = gct.getRequirementsJson()==null ? new LinkedList<ClientRequirement>() : JSONUtils.parseClientRequirements(new JSONArray(gct.getRequirementsJson())); if (JoinUtils.satisfiesClientRequirements(characteristics, crs)==JoinUtils.SatisfiesClientRequirements.NO) continue; } catch (Exception e) { logger.log(Level.WARNING,"Error checking client requirements", e); continue; } // add gcts (if location ok) gcts.add(gct); } } catch (Exception e) { logger.log(Level.WARNING,"Error checking client requirements", e); } return gcts; } finally { em.close(); } } /** make a 'GameTemplateInfo' just to convey a problem or alternative, not a game */ private static GameTemplateInfo getGameIndexMessage(String message, String detail, String imageUrl) { GameTemplate gt = new GameTemplate(); gt.setTitle(message); gt.setDescription(detail); if (imageUrl!=null) gt.setImageUrl(imageUrl); GameTemplateInfo gti = new GameTemplateInfo(); gti.setGameTemplate(gt); return gti; } private static final String JOIN_PATH = "browser/JoinGameInstance/"; static String makeJoinUrl(ServerConfiguration sc, GameInstance gi) { StringBuilder sb = new StringBuilder(); if (sc.getBaseUrl()==null) { logger.warning("Server BaseURL not configured"); sb.append(GetGameIndexServlet.DEFAULT_BASE_URL); } else { sb.append(sc.getBaseUrl()); if (!sc.getBaseUrl().endsWith("/")) sb.append("/"); } sb.append(JOIN_PATH); sb.append(KeyFactory.keyToString(gi.getKey())); return sb.toString(); } private static final String NEW_INSTANCE_PATH = "browser/NewGameInstance/"; private static String makeNewInstanceUrl(ServerConfiguration sc, GameInstanceFactory gi) { StringBuilder sb = new StringBuilder(); if (sc.getBaseUrl()==null) { logger.warning("Server BaseURL not configured"); sb.append(GetGameIndexServlet.DEFAULT_BASE_URL); } else { sb.append(sc.getBaseUrl()); if (!sc.getBaseUrl().endsWith("/")) sb.append("/"); } sb.append(NEW_INSTANCE_PATH); sb.append(KeyFactory.keyToString(gi.getKey())); return sb.toString(); } }