Mage.Stats WS module for getting server stats in json format

This commit is contained in:
magenoxx 2014-08-27 03:38:43 +04:00
parent 78c0d76088
commit 71614becc2
43 changed files with 1587 additions and 0 deletions

View file

@ -0,0 +1,47 @@
package com.xmage.ws.aspect;
import com.xmage.ws.json.ResponseBuilder;
import com.xmage.ws.model.DomainErrors;
import com.xmage.ws.resource.ErrorResource;
import com.xmage.ws.resource.Resource;
import com.xmage.ws.util.IPHolderUtil;
import net.minidev.json.JSONObject;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.ws.rs.core.Response;
/**
* Base aspect for getting request metadata
*
* @author noxx
*/
@Aspect
public class RequestAspect {
private static final Logger logger = LoggerFactory.getLogger(RequestAspect.class);
@Around("execution(* *(..)) && within(com.xmage.ws.rest.services.*)")
public Object advice(ProceedingJoinPoint pjp) throws Throwable {
try {
String ip = IPHolderUtil.getRememberedIP();
String userAgent = IPHolderUtil.getRememberedUserAgent();
logger.info("ip: " + ip + ", user-agent: " + userAgent);
return pjp.proceed();
} catch (Exception e) {
logger.error("Error: ", e);
}
Resource resource = new ErrorResource(DomainErrors.Errors.STATUS_SERVER_ERROR, "server_error");
JSONObject serverError = ResponseBuilder.build(resource);
return Response.status(200).entity(serverError.toJSONString()).build();
}
}

View file

@ -0,0 +1,46 @@
package com.xmage.ws.filter;
import com.xmage.ws.util.IPHolderUtil;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* Filter gets ip address and user agent and stores it using {@link com.xmage.ws.util.IPHolderUtil}
*
* @author noxx
*/
public class IPFilter implements Filter {
private FilterConfig config;
public IPFilter() {}
public void init(FilterConfig filterConfig) throws ServletException {
this.config = filterConfig;
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String ip = request.getRemoteAddr();
IPHolderUtil.rememberIP(ip);
if (request instanceof HttpServletRequest) {
HttpServletRequest req = (HttpServletRequest) request;
String uaString = req.getHeader("User-Agent");
IPHolderUtil.rememberUserAgent(uaString);
}
chain.doFilter(request, response);
}// doFilter
public void destroy() {
/*
* called before the Filter instance is removed from service by the web
* container
*/
}
}

View file

@ -0,0 +1,15 @@
package com.xmage.ws.json;
import com.xmage.core.entity.model.EntityModel;
import com.xmage.ws.resource.Resource;
import net.minidev.json.JSONObject;
/**
* Converts {@link com.xmage.core.entity.model.EntityModel} to json.
*
* @author noxx
*/
public interface JSONBuilder<R extends EntityModel> {
JSONObject buildFrom(Resource<R> resource);
}

View file

@ -0,0 +1,36 @@
package com.xmage.ws.json;
import com.xmage.ws.model.DomainErrors;
import com.xmage.ws.resource.Resource;
import net.minidev.json.JSONObject;
public class ResponseBuilder {
public static JSONObject build(int code) {
JSONObject response = new JSONObject();
response.put("code", code);
return response;
}
public static JSONObject build(int code, String name, JSONObject jsonObject) {
JSONObject response = new JSONObject();
response.put("code", code);
response.put(name, jsonObject);
return response;
}
public static JSONObject build(Resource resource) {
if (resource.getError() != DomainErrors.Errors.STATUS_OK.getCode()) {
JSONObject response = ResponseBuilder.build(resource.getError());
response.put("message", resource.getErrorMessage());
return response;
} else {
JSONObject json = resource.getJSONBody();
return ResponseBuilder.build(DomainErrors.Errors.STATUS_OK.getCode(), resource.getName(), json);
}
}
}

View file

@ -0,0 +1,44 @@
package com.xmage.ws.json;
import com.xmage.core.entity.model.ServerStats;
import com.xmage.ws.resource.Resource;
import net.minidev.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.SimpleDateFormat;
/**
* Converts {@link com.xmage.core.entity.model.ServerStats} resource to json.
*
* @author noxx
*/
public class XMageStatsJSONBuilder implements JSONBuilder<ServerStats> {
private static final Logger logger = LoggerFactory.getLogger(XMageStatsJSONBuilder.class);
private static final SimpleDateFormat sdf = new SimpleDateFormat("dd.MM.yyyy");
static class StaticHolder {
static XMageStatsJSONBuilder instance = new XMageStatsJSONBuilder();
}
public static XMageStatsJSONBuilder getInstance() {
return StaticHolder.instance;
}
public JSONObject buildFrom(Resource<ServerStats> resource) {
ServerStats serverStats = resource.getDefault();
JSONObject statsJson = new JSONObject();
statsJson.put("numberOfGamesPlayed", serverStats.getNumberOfGamesPlayed());
statsJson.put("numberOfUniquePlayers", serverStats.getNumberOfUniquePlayers());
statsJson.put("numberOfPlayersPlayedOnlyOnce", serverStats.getNumberOfPlayersPlayedOnce());
statsJson.put("top3Players", serverStats.getTop3Players());
return statsJson;
}
}

View file

@ -0,0 +1,41 @@
package com.xmage.ws.model;
/**
* Domain status error codes.
*
* @author noxx
*/
public class DomainErrors {
public enum Errors {
STATUS_OK(100, "OK"),
STATUS_SERVER_ERROR(101, "Server Internal Error"),
STATUS_AUTH_FAILED(102, "Auth failed"),
STATUS_ACCESS_DENIED(108, "Access denied"),
STATUS_NOT_ENOUGH_PARAMETERS(301, "Not enough parameters"),
STATUS_WRONG_PARAM_FORMAT(302, "Wrong param format"),
STATUS_NOT_IMPLEMENTED(800, "Not implemented"),
STATUS_NOT_FOUND(1000, "Resource Not Found");
private int code;
private String message;
Errors(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
public void setCustomMessage(String message) {
this.message = message;
}
}
}

View file

@ -0,0 +1,40 @@
package com.xmage.ws.model;
/**
* Some services may return simple response that is not related to domain or contain minor information.
* Example: return OK or FALSE only for checking server state.
*
* @author noxx
*/
public class SimpleResponse {
private int code;
private String message;
public SimpleResponse(int code, String message) {
this.code = code;
this.message = message;
}
public SimpleResponse(DomainErrors.Errors error) {
this(error.getCode(), error.getMessage());
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}

View file

@ -0,0 +1,14 @@
package com.xmage.ws.representer;
import com.xmage.ws.resource.Resource;
import net.minidev.json.JSONObject;
/**
* Now we have only JSON based representation.
*
* @author noxx
*/
public interface Representer<R> {
JSONObject toJSON(Resource<R> resource);
}

View file

@ -0,0 +1,24 @@
package com.xmage.ws.representer;
import com.xmage.ws.model.SimpleResponse;
import com.xmage.ws.resource.Resource;
import net.minidev.json.JSONObject;
/**
* This is useful when we have {@link SimpleResponse}
*
* @author noxx
*/
public class SimpleResponseRepresenter implements Representer<SimpleResponse> {
public JSONObject toJSON(Resource<SimpleResponse> resource) {
SimpleResponse response = resource.getDefault();
JSONObject json = new JSONObject();
json.put("code", response.getCode());
json.put("message", response.getMessage());
return json;
}
}

View file

@ -0,0 +1,20 @@
package com.xmage.ws.representer;
import com.xmage.core.entity.model.ServerStats;
import com.xmage.ws.json.XMageStatsJSONBuilder;
import com.xmage.ws.resource.Resource;
import net.minidev.json.JSONObject;
/**
*
* @author noxx
*/
public class XMageStatsRepresenter implements Representer<ServerStats> {
public XMageStatsRepresenter() {
}
public JSONObject toJSON(Resource<ServerStats> resource) {
return XMageStatsJSONBuilder.getInstance().buildFrom(resource);
}
}

View file

@ -0,0 +1,65 @@
package com.xmage.ws.resource;
import com.xmage.core.decorators.Decorator;
import com.xmage.ws.model.DomainErrors;
import com.xmage.ws.representer.Representer;
import net.minidev.json.JSONObject;
import java.util.ArrayList;
/**
*
* @author noxx
*/
public abstract class DefaultResource<R> implements Resource<R> {
protected DomainErrors.Errors error = DomainErrors.Errors.STATUS_OK;
protected R defaultResource;
protected Representer<R> representer;
protected java.util.List<Decorator> decorators = new ArrayList<Decorator>();
protected int version;
protected DefaultResource(Representer<R> representer) {
this.representer = representer;
}
@Override
public int getError() {
return error.getCode();
}
@Override
public R getDefault() {
return defaultResource;
}
@Override
public java.util.List<Decorator> getDecorators() {
return decorators;
}
@Override
public void addDecorator(Decorator decorator) {
if (decorator != null) {
this.decorators.add(decorator);
}
}
@Override
public JSONObject getJSONBody() {
return representer.toJSON(this);
}
@Override
public String getErrorMessage() {
return error.getMessage();
}
public int getVersion() {
return version;
}
}

View file

@ -0,0 +1,23 @@
package com.xmage.ws.resource;
import com.xmage.ws.model.DomainErrors;
/**
*
* @author noxx
*/
public class ErrorResource extends DefaultResource {
private String name;
public ErrorResource(DomainErrors.Errors error, String name) {
super(null);
this.name = name;
this.error = error;
}
@Override
public String getName() {
return this.name;
}
}

View file

@ -0,0 +1,25 @@
package com.xmage.ws.resource;
import com.xmage.core.decorators.Decorator;
import net.minidev.json.JSONObject;
/**
*
* @author noxx
*/
public interface Resource<R> {
int getError();
String getErrorMessage();
String getName();
JSONObject getJSONBody();
R getDefault();
java.util.List<Decorator> getDecorators();
void addDecorator(Decorator decorator);
}

View file

@ -0,0 +1,51 @@
package com.xmage.ws.resource;
import com.xmage.core.entity.model.ServerStats;
import com.xmage.core.entity.repositories.XMageStatsRepository;
import com.xmage.core.entity.repositories.impl.XMageStatsRepositoryImpl;
import com.xmage.ws.model.DomainErrors;
import com.xmage.ws.representer.XMageStatsRepresenter;
import com.xmage.ws.representer.Representer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class XMageStatsResource extends DefaultResource<ServerStats> {
private static final Logger logger = LoggerFactory.getLogger(XMageStatsResource.class);
private static XMageStatsRepository xmageStatsRepository = new XMageStatsRepositoryImpl();
private static final Representer<ServerStats> defaultRepresenter = new XMageStatsRepresenter();
public XMageStatsResource() {
super(defaultRepresenter);
}
public XMageStatsResource(ServerStats event) {
super(defaultRepresenter);
defaultResource = event;
}
public Resource getAll() {
try {
ServerStats serverStats = xmageStatsRepository.getServerStats();
if (serverStats != null) {
defaultResource = serverStats;
} else {
error = DomainErrors.Errors.STATUS_NOT_FOUND;
}
} catch (Exception e) {
logger.error("Getting server stats error:", e);
error = DomainErrors.Errors.STATUS_SERVER_ERROR;
}
return this;
}
@Override
public String getName() {
return "serverStats";
}
}

View file

@ -0,0 +1,17 @@
package com.xmage.ws.resource.impl;
import com.xmage.ws.model.SimpleResponse;
import com.xmage.ws.representer.SimpleResponseRepresenter;
import com.xmage.ws.resource.DefaultResource;
public class SimpleResource extends DefaultResource<SimpleResponse> {
public SimpleResource() {
super(new SimpleResponseRepresenter());
}
@Override
public String getName() {
return "simple";
}
}

View file

@ -0,0 +1,8 @@
package com.xmage.ws.rest;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
@ApplicationPath("/api")
public class XMageStatsAPIApplication extends Application {
}

View file

@ -0,0 +1,34 @@
package com.xmage.ws.rest.services;
import com.xmage.ws.resource.XMageStatsResource;
import com.xmage.ws.resource.Resource;
import com.xmage.ws.rest.services.base.AbstractService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;
/**
*
* @author noxx
*/
@Path("/xmage/stats")
@Produces("application/json;charset=utf-8")
public class XMageStatsService extends AbstractService {
static final Logger logger = LoggerFactory.getLogger(XMageStatsService.class);
@GET
@Path("/getAll")
public Response getAllStats() {
logger.trace("getAllStats");
Resource resource = new XMageStatsResource().getAll();
return responseWithError(resource);
}
}

View file

@ -0,0 +1,73 @@
package com.xmage.ws.rest.services.base;
import com.xmage.ws.json.ResponseBuilder;
import com.xmage.ws.model.DomainErrors;
import com.xmage.ws.resource.Resource;
import net.minidev.json.JSONObject;
import org.apache.sling.commons.json.JSONException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.ws.rs.core.Response;
/**
* General approach for ws requests/responses.
*
* Consists of building response object, verifying response, prettifying, execution time calculating.
*
* @author noxx
*/
public abstract class AbstractService {
private static final Logger logger = LoggerFactory.getLogger(AbstractService.class);
/**
* Create {@link Response} from {@link com.xmage.ws.resource.Resource}
*
* @param resource Resource to build response based on
* @return
*/
public final Response responseWithError(Resource resource) {
long t1 = System.currentTimeMillis();
JSONObject response = buildResponse(resource);
response = verifyResponse(response);
String json = prettifyResponse(response);
Response responseObject = Response.status(200).entity(json).build();
long t2 = System.currentTimeMillis();
logger.info("responseWithError time: " + (t2 - t1) + "ms");
return responseObject;
}
private JSONObject buildResponse(Resource resource) {
JSONObject response = null;
try {
response = ResponseBuilder.build(resource);
} catch (Exception e) {
logger.error("responseWithError: ", e);
}
return response;
}
private String prettifyResponse(JSONObject response) {
String json = response.toJSONString();
try {
json = new org.apache.sling.commons.json.JSONObject(json).toString(1);
} catch (JSONException jse) {
jse.printStackTrace();
}
return json;
}
private JSONObject verifyResponse(JSONObject response) {
if (response == null) {
logger.error("Something bad happened on response creation");
response = ResponseBuilder.build(DomainErrors.Errors.STATUS_SERVER_ERROR.getCode());
}
return response;
}
}

View file

@ -0,0 +1,32 @@
package com.xmage.ws.util;
/**
* Stores ip addresses to allow access from.
* Stores user-agents to allow access for.
*
* @author noxx
*/
public class IPHolderUtil {
private static final ThreadLocal<String> ipThreadLocal = new ThreadLocal<String>();
private static final ThreadLocal<String> userAgentThreadLocal = new ThreadLocal<String>();
private IPHolderUtil() {}
public static void rememberIP(String ip) {
ipThreadLocal.set(ip);
}
public static String getRememberedIP() {
return ipThreadLocal.get();
}
public static void rememberUserAgent(String userAgent) {
userAgentThreadLocal.set(userAgent);
}
public static String getRememberedUserAgent() {
return userAgentThreadLocal.get();
}
}

View file

@ -0,0 +1,12 @@
package com.xmage.ws.util.json;
/**
*
* @author noxx
*/
public class JSONOperationErrorException extends RuntimeException {
public JSONOperationErrorException(String message) {
super(message);
}
}

View file

@ -0,0 +1,198 @@
package com.xmage.ws.util.json;
import net.minidev.json.JSONArray;
import net.minidev.json.JSONObject;
import net.minidev.json.JSONValue;
import java.util.HashMap;
import java.util.Map;
/**
* Enhances working with json.
*
* @author noxx
*/
public class JSONParser {
public enum CachePolicy {
CACHE_ONE_LEVEL_ONLY,
CACHE_ALL_LEVELS
}
private static final Map<String, Integer> extendedIndexes = new HashMap<String, Integer>() {{
put("$first", 0);
put("$second", 1);
put("$third", 2);
put("$fourth", 3);
put("$fifth", 4);
}};
private String json;
private JSONObject root;
private boolean hitCache;
private CachePolicy cachePolicy = CachePolicy.CACHE_ONE_LEVEL_ONLY;
private Map<String, Object> cache = new HashMap<String, Object>();
public void parseJSON(String jsonString) throws JSONValidationException {
parseJSON(jsonString, true);
}
public void parseJSON(String jsonString, boolean validate) throws JSONValidationException {
this.json = jsonString;
prepare();
if (validate) {
validate();
}
}
public Object get(String path) {
return getObject(path);
}
public int getInt(String path) {
return (Integer)getObject(path);
}
public int getIntSafe(String path) {
if (getObject(path) == null) {
return 0;
}
return (Integer)getObject(path);
}
public String getString(String path) {
return (String)getObject(path);
}
public JSONObject getJSON(String path) {
return (JSONObject)getObject(path);
}
private Object getObject(String path) {
this.hitCache = false;
if (cache.containsKey(path)) {
this.hitCache = true;
return cache.get(path);
}
String[] params = path.split("\\.");
JSONObject json = this.root;
JSONArray jsonArray = null;
String currentPath = "";
for (int i = 0; i < params.length - 1; i++) {
String param = params[i];
if (cachePolicy.equals(CachePolicy.CACHE_ALL_LEVELS)) {
if (!currentPath.isEmpty()) {
currentPath += ".";
}
currentPath += param;
}
if (param.startsWith("$")) {
if (jsonArray == null) {
throw new JSONOperationErrorException("Not illegal syntax at this place: " + param);
}
int index = getIndex(param);
json = (JSONObject) jsonArray.get(index);
jsonArray = null;
} else if (param.contains("[")) {
int find = param.indexOf("[");
String newParam = param.substring(0, find);
String s = param.substring(find+1, param.indexOf("]"));
if (s.isEmpty()) {
jsonArray = (JSONArray) json.get(newParam);
json = null;
} else {
int index = Integer.parseInt(s);
json = (JSONObject)((JSONArray) json.get(newParam)).get(index);
jsonArray = null;
}
} else {
Object obj = json.get(param);
if (obj instanceof JSONObject) {
json = (JSONObject) obj;
jsonArray = null;
} else if (obj instanceof JSONArray) {
jsonArray = (JSONArray) obj;
json = null;
} else if (obj == null) {
throw new IllegalStateException("json object is null");
} else {
throw new IllegalStateException("json object ('"+param+"') has wrong type: " + obj.getClass());
}
}
if (cachePolicy.equals(CachePolicy.CACHE_ALL_LEVELS)) {
saveToCache(currentPath, json);
}
}
String name = params[params.length - 1];
Object value;
if (name.startsWith("$")) {
if (jsonArray == null) {
throw new JSONOperationErrorException("Not illegal syntax at this place: " + name);
}
int index = getIndex(name);
value = jsonArray.get(index);
} else {
value = json.get(name);
}
saveToCache(path, value);
return value;
}
private int getIndex(String extendedIndex) {
if (extendedIndexes.containsKey(extendedIndex)) {
return extendedIndexes.get(extendedIndex);
} else {
throw new JSONOperationErrorException("Can't parse extended index: " + extendedIndex);
}
}
private void saveToCache(String path, Object value) {
cache.put(path, value);
}
public JSONArray getJSONArray(String path) {
return (JSONArray)getObject(path);
}
private void prepare() {
reset();
if (this.json != null) {
this.json = this.json.trim();
}
}
private void validate() throws JSONValidationException {
if (this.json == null) {
throw new JSONValidationException("JSON is null");
}
try {
this.root = (JSONObject) JSONValue.parse(this.json);
if (this.root == null) {
throw new JSONValidationException("Root json is null");
}
} catch (Exception e) {
throw new JSONValidationException("JSON is not valid", e);
}
}
public void reset() {
this.hitCache = false;
this.cachePolicy = CachePolicy.CACHE_ONE_LEVEL_ONLY;
this.cache.clear();
}
public boolean isHitCache() {
return hitCache;
}
public void setCachePolicy(CachePolicy cachePolicy) {
this.cachePolicy = cachePolicy;
}
}

View file

@ -0,0 +1,16 @@
package com.xmage.ws.util.json;
/**
*
* @author noxx
*/
public class JSONValidationException extends Exception {
public JSONValidationException(String message) {
super(message);
}
public JSONValidationException(String message, Exception e) {
super(message, e);
}
}