diff --git a/Mage.Stats/pom.xml b/Mage.Stats/pom.xml
new file mode 100644
index 00000000000..844fb543e2f
--- /dev/null
+++ b/Mage.Stats/pom.xml
@@ -0,0 +1,180 @@
+
+
+ 4.0.0
+
+
+ org.mage
+ mage-root
+ 1.3.0
+
+
+ org.mage
+ mage-stats
+ war
+ XMage Stats Web Service
+
+
+
+ JBoss repository
+ https://repository.jboss.org/nexus/content/groups/public-jboss/
+
+
+
+
+
+
+ org.mage
+ mage-common
+ ${project.version}
+
+
+
+ org.mage
+ mage-server
+ ${project.version}
+
+
+
+ junit
+ junit
+ 4.10
+ test
+
+
+
+ net.minidev
+ json-smart
+ 1.1.1
+
+
+
+ org.aspectj
+ aspectjrt
+ 1.6.11
+
+
+
+ net.sf.opencsv
+ opencsv
+ 2.3
+
+
+
+
+ org.jboss.resteasy
+ resteasy-jaxrs
+ 2.3.5.Final
+ provided
+
+
+
+ net.minidev
+ json-smart
+ 1.1.1
+
+
+
+ org.aspectj
+ aspectjrt
+ 1.6.11
+
+
+
+ ch.qos.logback
+ logback-classic
+ 1.0.10
+
+
+
+ org.apache.sling
+ org.apache.sling.commons.json
+ 2.0.6
+
+
+
+ org.apache.commons
+ commons-io
+ 1.3.2
+
+
+
+ org.jboss.resteasy
+ resteasy-multipart-provider
+ 2.2.0.GA
+
+
+
+ commons-io
+ commons-io
+ 2.0.1
+
+
+
+ commons-httpclient
+ commons-httpclient
+ 3.1
+
+
+
+ javax.servlet
+ servlet-api
+ 2.4
+
+
+
+ joda-time
+ joda-time
+ 2.2
+
+
+
+
+
+ mage-stats-ws
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 2.0.2
+
+ 1.7
+ 1.7
+
+
+
+ org.apache.maven.plugins
+ maven-war-plugin
+
+ src\main\webapp\WEB-INF\web.xml
+ .
+
+
+
+ org.codehaus.mojo
+ aspectj-maven-plugin
+ 1.4
+
+
+
+ src/main/java
+
+
+ 1.6
+
+
+
+
+ compile
+
+
+
+
+
+
+
+
+ UTF-8
+
+
diff --git a/Mage.Stats/src/main/java/com/xmage/core/builders/Builder.java b/Mage.Stats/src/main/java/com/xmage/core/builders/Builder.java
new file mode 100644
index 00000000000..52cf3daa5fe
--- /dev/null
+++ b/Mage.Stats/src/main/java/com/xmage/core/builders/Builder.java
@@ -0,0 +1,17 @@
+package com.xmage.core.builders;
+
+public abstract class Builder {
+
+ protected E entity;
+
+ protected Builder() {
+ //entity = E.class.newInstance();
+ }
+
+ abstract protected void validate();
+
+ public final E build() {
+ validate();
+ return entity;
+ }
+}
diff --git a/Mage.Stats/src/main/java/com/xmage/core/constants/Constants.java b/Mage.Stats/src/main/java/com/xmage/core/constants/Constants.java
new file mode 100644
index 00000000000..bd9ee16b412
--- /dev/null
+++ b/Mage.Stats/src/main/java/com/xmage/core/constants/Constants.java
@@ -0,0 +1,6 @@
+package com.xmage.core.constants;
+
+public class Constants {
+
+
+}
diff --git a/Mage.Stats/src/main/java/com/xmage/core/decorators/Decorator.java b/Mage.Stats/src/main/java/com/xmage/core/decorators/Decorator.java
new file mode 100644
index 00000000000..5d9c03e9616
--- /dev/null
+++ b/Mage.Stats/src/main/java/com/xmage/core/decorators/Decorator.java
@@ -0,0 +1,7 @@
+package com.xmage.core.decorators;
+
+/**
+ * @author noxx
+ */
+public interface Decorator {
+}
diff --git a/Mage.Stats/src/main/java/com/xmage/core/entity/model/EntityModel.java b/Mage.Stats/src/main/java/com/xmage/core/entity/model/EntityModel.java
new file mode 100644
index 00000000000..7725f01e690
--- /dev/null
+++ b/Mage.Stats/src/main/java/com/xmage/core/entity/model/EntityModel.java
@@ -0,0 +1,11 @@
+package com.xmage.core.entity.model;
+
+
+/**
+ * Marker interface for entity models.
+ *
+ * @author noxx
+ */
+public interface EntityModel {
+
+}
diff --git a/Mage.Stats/src/main/java/com/xmage/core/entity/model/ServerStats.java b/Mage.Stats/src/main/java/com/xmage/core/entity/model/ServerStats.java
new file mode 100644
index 00000000000..9cd5af8c152
--- /dev/null
+++ b/Mage.Stats/src/main/java/com/xmage/core/entity/model/ServerStats.java
@@ -0,0 +1,77 @@
+package com.xmage.core.entity.model;
+
+
+/**
+ * Class representing XMage server stats.
+ *
+ * @author noxx
+ */
+public class ServerStats implements EntityModel {
+
+ private int numberOfGamesPlayed;
+
+ private int numberOfUniquePlayers;
+
+ private String top3Players;
+
+ private int numberOfPlayersPlayedOnce;
+
+ public int getNumberOfGamesPlayed() {
+ return numberOfGamesPlayed;
+ }
+
+ public void setNumberOfGamesPlayed(int numberOfGamesPlayed) {
+ this.numberOfGamesPlayed = numberOfGamesPlayed;
+ }
+
+ public int getNumberOfUniquePlayers() {
+ return numberOfUniquePlayers;
+ }
+
+ public void setNumberOfUniquePlayers(int numberOfUniquePlayers) {
+ this.numberOfUniquePlayers = numberOfUniquePlayers;
+ }
+
+ public int getNumberOfPlayersPlayedOnce() {
+ return numberOfPlayersPlayedOnce;
+ }
+
+ public void setNumberOfPlayersPlayedOnce(int numberOfPlayersPlayedOnce) {
+ this.numberOfPlayersPlayedOnce = numberOfPlayersPlayedOnce;
+ }
+
+ public String getTop3Players() {
+ return top3Players;
+ }
+
+ public void setTop3Players(String top3Players) {
+ this.top3Players = top3Players;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ ServerStats stats = (ServerStats) o;
+
+ if (numberOfGamesPlayed != stats.numberOfGamesPlayed) return false;
+ if (numberOfUniquePlayers != stats.numberOfUniquePlayers) return false;
+ if (numberOfPlayersPlayedOnce != stats.numberOfPlayersPlayedOnce) return false;
+ if (top3Players != null ? !top3Players.equals(stats.top3Players) : stats.top3Players != null) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = numberOfGamesPlayed;
+ result = 31 * result + numberOfUniquePlayers;
+ result = 31 * result + numberOfPlayersPlayedOnce;
+ result = 31 * result + (top3Players != null ? top3Players.hashCode() : 0);
+
+ return result;
+ }
+
+
+}
diff --git a/Mage.Stats/src/main/java/com/xmage/core/entity/repositories/XMageStatsRepository.java b/Mage.Stats/src/main/java/com/xmage/core/entity/repositories/XMageStatsRepository.java
new file mode 100644
index 00000000000..4a5cc4310a7
--- /dev/null
+++ b/Mage.Stats/src/main/java/com/xmage/core/entity/repositories/XMageStatsRepository.java
@@ -0,0 +1,16 @@
+package com.xmage.core.entity.repositories;
+
+import com.xmage.core.entity.model.ServerStats;
+
+/**
+ * Repository interface for XMage server stats.
+ *
+ * Responsible for fetching stats information.
+ *
+ * @author noxx
+ */
+public interface XMageStatsRepository {
+
+ ServerStats getServerStats();
+
+}
diff --git a/Mage.Stats/src/main/java/com/xmage/core/entity/repositories/impl/XMageStatsRepositoryImpl.java b/Mage.Stats/src/main/java/com/xmage/core/entity/repositories/impl/XMageStatsRepositoryImpl.java
new file mode 100644
index 00000000000..3c11da317e4
--- /dev/null
+++ b/Mage.Stats/src/main/java/com/xmage/core/entity/repositories/impl/XMageStatsRepositoryImpl.java
@@ -0,0 +1,122 @@
+package com.xmage.core.entity.repositories.impl;
+
+import com.xmage.core.entity.model.ServerStats;
+import com.xmage.core.entity.repositories.XMageStatsRepository;
+import mage.db.EntityManager;
+import mage.db.model.Log;
+import mage.server.services.LogKeys;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.*;
+
+/**
+ * Implementation for {@link com.xmage.core.entity.repositories.XMageStatsRepository}
+ *
+ * @author noxx
+ */
+public class XMageStatsRepositoryImpl implements XMageStatsRepository {
+
+ private static final Logger logger = LoggerFactory.getLogger(XMageStatsRepositoryImpl.class);
+
+ @Override
+ public ServerStats getServerStats() {
+ ServerStats serverStats = new ServerStats();
+
+ List logs = EntityManager.instance.getAllLogs();
+ logger.info("logs found count: " + logs.size());
+
+ int numberOfGamesPlayed = 0;
+ Set playerNames = new HashSet();
+
+ // Get nicknames and games started count
+ Map nicknames = new HashMap();
+ for (Log log : logs) {
+ if (log.getKey().equals(LogKeys.KEY_GAME_STARTED)) {
+ if (log.getArguments() != null) {
+ int index = 0;
+ for (String argument : log.getArguments()) {
+ if (index > 0) {
+ inc(nicknames, argument);
+ }
+ index++;
+ }
+ }
+ numberOfGamesPlayed++;
+ }
+ }
+
+ // Sort games
+ Collection values = nicknames.values();
+ List games = new ArrayList();
+ games.addAll(values);
+ Collections.sort(games, new Comparator() {
+ @Override
+ public int compare(Integer i1, Integer i2) {
+ return i2.compareTo(i1);
+ }
+ });
+
+ // Top-3
+ List numbersToFind = new ArrayList();
+ for (Integer numberOfGames : games) {
+ numbersToFind.add(numberOfGames);
+ if (numbersToFind.size() == 3) {
+ break;
+ }
+ }
+
+ Map players = new LinkedHashMap();
+ for (Integer number : numbersToFind) {
+ for (Map.Entry entry : nicknames.entrySet()) {
+ if (entry.getValue().equals(number)) {
+ players.put(entry.getValue(), entry.getKey());
+ break;
+ }
+ }
+ if (players.size() == 3) {
+ break;
+ }
+ }
+
+ // Build top-3 string
+ StringBuilder top3 = new StringBuilder();
+ for (Map.Entry entry : players.entrySet()) {
+ top3.append("[").append(entry.getValue()).append(":").append(entry.getKey()).append("]");
+ }
+
+ // Played only once
+ Integer oneGamePlayers = 0;
+ for (Integer numberOfGames : games) {
+ if (numberOfGames == 1) {
+ oneGamePlayers++;
+ }
+ }
+
+ serverStats.setNumberOfGamesPlayed(numberOfGamesPlayed);
+ serverStats.setNumberOfUniquePlayers(nicknames.size());
+ serverStats.setTop3Players(top3.toString());
+ serverStats.setNumberOfPlayersPlayedOnce(oneGamePlayers);
+
+ return serverStats;
+ }
+
+ private static void inc(Map map, String player) {
+ if (map.containsKey(player)) {
+ Integer count = map.get(player);
+ count++;
+ map.put(player, count);
+ } else {
+ map.put(player, 1);
+ }
+ }
+
+ private static boolean check(List numbers, Integer value) {
+ for (Integer number : numbers) {
+ if (number.equals(value)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/Mage.Stats/src/main/java/com/xmage/core/exceptions/XMageStatsNotFoundException.java b/Mage.Stats/src/main/java/com/xmage/core/exceptions/XMageStatsNotFoundException.java
new file mode 100644
index 00000000000..0050af7214d
--- /dev/null
+++ b/Mage.Stats/src/main/java/com/xmage/core/exceptions/XMageStatsNotFoundException.java
@@ -0,0 +1,8 @@
+package com.xmage.core.exceptions;
+
+/**
+ *
+ * @author noxx
+ */
+public class XMageStatsNotFoundException extends Exception {
+}
diff --git a/Mage.Stats/src/main/java/com/xmage/ws/aspect/RequestAspect.java b/Mage.Stats/src/main/java/com/xmage/ws/aspect/RequestAspect.java
new file mode 100644
index 00000000000..5d6ffc977b2
--- /dev/null
+++ b/Mage.Stats/src/main/java/com/xmage/ws/aspect/RequestAspect.java
@@ -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();
+ }
+
+
+}
\ No newline at end of file
diff --git a/Mage.Stats/src/main/java/com/xmage/ws/filter/IPFilter.java b/Mage.Stats/src/main/java/com/xmage/ws/filter/IPFilter.java
new file mode 100644
index 00000000000..7fd7ba6abc3
--- /dev/null
+++ b/Mage.Stats/src/main/java/com/xmage/ws/filter/IPFilter.java
@@ -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
+ */
+ }
+
+}
diff --git a/Mage.Stats/src/main/java/com/xmage/ws/json/JSONBuilder.java b/Mage.Stats/src/main/java/com/xmage/ws/json/JSONBuilder.java
new file mode 100644
index 00000000000..1aee3735df2
--- /dev/null
+++ b/Mage.Stats/src/main/java/com/xmage/ws/json/JSONBuilder.java
@@ -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 {
+
+ JSONObject buildFrom(Resource resource);
+}
diff --git a/Mage.Stats/src/main/java/com/xmage/ws/json/ResponseBuilder.java b/Mage.Stats/src/main/java/com/xmage/ws/json/ResponseBuilder.java
new file mode 100644
index 00000000000..c1c58e4e302
--- /dev/null
+++ b/Mage.Stats/src/main/java/com/xmage/ws/json/ResponseBuilder.java
@@ -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);
+ }
+
+ }
+
+}
diff --git a/Mage.Stats/src/main/java/com/xmage/ws/json/XMageStatsJSONBuilder.java b/Mage.Stats/src/main/java/com/xmage/ws/json/XMageStatsJSONBuilder.java
new file mode 100644
index 00000000000..df7072e065a
--- /dev/null
+++ b/Mage.Stats/src/main/java/com/xmage/ws/json/XMageStatsJSONBuilder.java
@@ -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 {
+
+ 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 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;
+ }
+
+}
diff --git a/Mage.Stats/src/main/java/com/xmage/ws/model/DomainErrors.java b/Mage.Stats/src/main/java/com/xmage/ws/model/DomainErrors.java
new file mode 100644
index 00000000000..c60e869389d
--- /dev/null
+++ b/Mage.Stats/src/main/java/com/xmage/ws/model/DomainErrors.java
@@ -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;
+ }
+ }
+
+}
diff --git a/Mage.Stats/src/main/java/com/xmage/ws/model/SimpleResponse.java b/Mage.Stats/src/main/java/com/xmage/ws/model/SimpleResponse.java
new file mode 100644
index 00000000000..b859b043f8a
--- /dev/null
+++ b/Mage.Stats/src/main/java/com/xmage/ws/model/SimpleResponse.java
@@ -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;
+ }
+}
diff --git a/Mage.Stats/src/main/java/com/xmage/ws/representer/Representer.java b/Mage.Stats/src/main/java/com/xmage/ws/representer/Representer.java
new file mode 100644
index 00000000000..d5a471d3af8
--- /dev/null
+++ b/Mage.Stats/src/main/java/com/xmage/ws/representer/Representer.java
@@ -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 {
+
+ JSONObject toJSON(Resource resource);
+}
diff --git a/Mage.Stats/src/main/java/com/xmage/ws/representer/SimpleResponseRepresenter.java b/Mage.Stats/src/main/java/com/xmage/ws/representer/SimpleResponseRepresenter.java
new file mode 100644
index 00000000000..1d4117eaf7c
--- /dev/null
+++ b/Mage.Stats/src/main/java/com/xmage/ws/representer/SimpleResponseRepresenter.java
@@ -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 {
+
+ public JSONObject toJSON(Resource resource) {
+ SimpleResponse response = resource.getDefault();
+
+ JSONObject json = new JSONObject();
+ json.put("code", response.getCode());
+ json.put("message", response.getMessage());
+
+ return json;
+ }
+
+}
diff --git a/Mage.Stats/src/main/java/com/xmage/ws/representer/XMageStatsRepresenter.java b/Mage.Stats/src/main/java/com/xmage/ws/representer/XMageStatsRepresenter.java
new file mode 100644
index 00000000000..a54b3743474
--- /dev/null
+++ b/Mage.Stats/src/main/java/com/xmage/ws/representer/XMageStatsRepresenter.java
@@ -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 {
+
+ public XMageStatsRepresenter() {
+ }
+
+ public JSONObject toJSON(Resource resource) {
+ return XMageStatsJSONBuilder.getInstance().buildFrom(resource);
+ }
+}
diff --git a/Mage.Stats/src/main/java/com/xmage/ws/resource/DefaultResource.java b/Mage.Stats/src/main/java/com/xmage/ws/resource/DefaultResource.java
new file mode 100644
index 00000000000..7ad62cc24ae
--- /dev/null
+++ b/Mage.Stats/src/main/java/com/xmage/ws/resource/DefaultResource.java
@@ -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 implements Resource {
+
+ protected DomainErrors.Errors error = DomainErrors.Errors.STATUS_OK;
+
+ protected R defaultResource;
+
+ protected Representer representer;
+
+ protected java.util.List decorators = new ArrayList();
+
+ protected int version;
+
+ protected DefaultResource(Representer representer) {
+ this.representer = representer;
+ }
+
+ @Override
+ public int getError() {
+ return error.getCode();
+ }
+
+ @Override
+ public R getDefault() {
+ return defaultResource;
+ }
+
+ @Override
+ public java.util.List 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;
+ }
+}
diff --git a/Mage.Stats/src/main/java/com/xmage/ws/resource/ErrorResource.java b/Mage.Stats/src/main/java/com/xmage/ws/resource/ErrorResource.java
new file mode 100644
index 00000000000..d1cc7ddb425
--- /dev/null
+++ b/Mage.Stats/src/main/java/com/xmage/ws/resource/ErrorResource.java
@@ -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;
+ }
+}
diff --git a/Mage.Stats/src/main/java/com/xmage/ws/resource/Resource.java b/Mage.Stats/src/main/java/com/xmage/ws/resource/Resource.java
new file mode 100644
index 00000000000..5a51732ea5a
--- /dev/null
+++ b/Mage.Stats/src/main/java/com/xmage/ws/resource/Resource.java
@@ -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 {
+
+ int getError();
+
+ String getErrorMessage();
+
+ String getName();
+
+ JSONObject getJSONBody();
+
+ R getDefault();
+
+ java.util.List getDecorators();
+
+ void addDecorator(Decorator decorator);
+}
diff --git a/Mage.Stats/src/main/java/com/xmage/ws/resource/XMageStatsResource.java b/Mage.Stats/src/main/java/com/xmage/ws/resource/XMageStatsResource.java
new file mode 100644
index 00000000000..9af0b8d8acb
--- /dev/null
+++ b/Mage.Stats/src/main/java/com/xmage/ws/resource/XMageStatsResource.java
@@ -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 {
+
+ private static final Logger logger = LoggerFactory.getLogger(XMageStatsResource.class);
+
+ private static XMageStatsRepository xmageStatsRepository = new XMageStatsRepositoryImpl();
+
+ private static final Representer 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";
+ }
+
+
+}
diff --git a/Mage.Stats/src/main/java/com/xmage/ws/resource/impl/SimpleResource.java b/Mage.Stats/src/main/java/com/xmage/ws/resource/impl/SimpleResource.java
new file mode 100644
index 00000000000..d72dd96edb5
--- /dev/null
+++ b/Mage.Stats/src/main/java/com/xmage/ws/resource/impl/SimpleResource.java
@@ -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 {
+
+ public SimpleResource() {
+ super(new SimpleResponseRepresenter());
+ }
+
+ @Override
+ public String getName() {
+ return "simple";
+ }
+}
diff --git a/Mage.Stats/src/main/java/com/xmage/ws/rest/XMageStatsAPIApplication.java b/Mage.Stats/src/main/java/com/xmage/ws/rest/XMageStatsAPIApplication.java
new file mode 100644
index 00000000000..074a979e391
--- /dev/null
+++ b/Mage.Stats/src/main/java/com/xmage/ws/rest/XMageStatsAPIApplication.java
@@ -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 {
+}
\ No newline at end of file
diff --git a/Mage.Stats/src/main/java/com/xmage/ws/rest/services/XMageStatsService.java b/Mage.Stats/src/main/java/com/xmage/ws/rest/services/XMageStatsService.java
new file mode 100644
index 00000000000..7124993d3c6
--- /dev/null
+++ b/Mage.Stats/src/main/java/com/xmage/ws/rest/services/XMageStatsService.java
@@ -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);
+ }
+
+
+}
diff --git a/Mage.Stats/src/main/java/com/xmage/ws/rest/services/base/AbstractService.java b/Mage.Stats/src/main/java/com/xmage/ws/rest/services/base/AbstractService.java
new file mode 100644
index 00000000000..1ffc7c80d67
--- /dev/null
+++ b/Mage.Stats/src/main/java/com/xmage/ws/rest/services/base/AbstractService.java
@@ -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;
+ }
+}
diff --git a/Mage.Stats/src/main/java/com/xmage/ws/util/IPHolderUtil.java b/Mage.Stats/src/main/java/com/xmage/ws/util/IPHolderUtil.java
new file mode 100644
index 00000000000..634f0084e12
--- /dev/null
+++ b/Mage.Stats/src/main/java/com/xmage/ws/util/IPHolderUtil.java
@@ -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 ipThreadLocal = new ThreadLocal();
+ private static final ThreadLocal userAgentThreadLocal = new ThreadLocal();
+
+ 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();
+ }
+}
+
diff --git a/Mage.Stats/src/main/java/com/xmage/ws/util/json/JSONOperationErrorException.java b/Mage.Stats/src/main/java/com/xmage/ws/util/json/JSONOperationErrorException.java
new file mode 100644
index 00000000000..b40927abb8a
--- /dev/null
+++ b/Mage.Stats/src/main/java/com/xmage/ws/util/json/JSONOperationErrorException.java
@@ -0,0 +1,12 @@
+package com.xmage.ws.util.json;
+
+/**
+ *
+ * @author noxx
+ */
+public class JSONOperationErrorException extends RuntimeException {
+
+ public JSONOperationErrorException(String message) {
+ super(message);
+ }
+}
diff --git a/Mage.Stats/src/main/java/com/xmage/ws/util/json/JSONParser.java b/Mage.Stats/src/main/java/com/xmage/ws/util/json/JSONParser.java
new file mode 100644
index 00000000000..62e000d45c5
--- /dev/null
+++ b/Mage.Stats/src/main/java/com/xmage/ws/util/json/JSONParser.java
@@ -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 extendedIndexes = new HashMap() {{
+ 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 cache = new HashMap();
+
+ 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;
+ }
+}
diff --git a/Mage.Stats/src/main/java/com/xmage/ws/util/json/JSONValidationException.java b/Mage.Stats/src/main/java/com/xmage/ws/util/json/JSONValidationException.java
new file mode 100644
index 00000000000..a7271a49a48
--- /dev/null
+++ b/Mage.Stats/src/main/java/com/xmage/ws/util/json/JSONValidationException.java
@@ -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);
+ }
+}
diff --git a/Mage.Stats/src/main/resources/META-INF/c3p0.properties b/Mage.Stats/src/main/resources/META-INF/c3p0.properties
new file mode 100644
index 00000000000..b8792e248ab
--- /dev/null
+++ b/Mage.Stats/src/main/resources/META-INF/c3p0.properties
@@ -0,0 +1,4 @@
+###c3p0
+с3p0.testConnectionOnCheckout=true
+с3p0.acquireRetryDelay=1000
+с3p0.acquireRetryAttempts=1
\ No newline at end of file
diff --git a/Mage.Stats/src/main/resources/logback.xml b/Mage.Stats/src/main/resources/logback.xml
new file mode 100644
index 00000000000..79104d854a7
--- /dev/null
+++ b/Mage.Stats/src/main/resources/logback.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+ server.log
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Mage.Stats/src/main/resources/messages.properties b/Mage.Stats/src/main/resources/messages.properties
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/Mage.Stats/src/main/resources/messages_en.properties b/Mage.Stats/src/main/resources/messages_en.properties
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/Mage.Stats/src/main/resources/messages_ru.properties b/Mage.Stats/src/main/resources/messages_ru.properties
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/Mage.Stats/src/main/resources/xmage.properties b/Mage.Stats/src/main/resources/xmage.properties
new file mode 100644
index 00000000000..68e61418989
--- /dev/null
+++ b/Mage.Stats/src/main/resources/xmage.properties
@@ -0,0 +1,2 @@
+db.log.url=jdbc:h2:file:../Mage.Server/db/mage.h2;AUTO_SERVER=TRUE
+db.feedback.url=jdbc:h2:file:../Mage.Server/db/feedback.h2;AUTO_SERVER=TRUE
\ No newline at end of file
diff --git a/Mage.Stats/src/main/webapp/WEB-INF/web.xml b/Mage.Stats/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..14005be18fe
--- /dev/null
+++ b/Mage.Stats/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,19 @@
+
+
+
+
+ XMage Stats Restful Web Application
+
+
+ IPFilter
+ com.xmage.ws.filter.IPFilter
+
+
+
+ IPFilter
+ /*
+
+
+
diff --git a/Mage.Stats/src/main/webapp/index.jsp b/Mage.Stats/src/main/webapp/index.jsp
new file mode 100644
index 00000000000..c38169bb958
--- /dev/null
+++ b/Mage.Stats/src/main/webapp/index.jsp
@@ -0,0 +1,5 @@
+
+
+Hello World!
+
+
diff --git a/Mage.Stats/src/test/java/com/anygo/ws/json/TestJSONParser.java b/Mage.Stats/src/test/java/com/anygo/ws/json/TestJSONParser.java
new file mode 100644
index 00000000000..8960155f3b5
--- /dev/null
+++ b/Mage.Stats/src/test/java/com/anygo/ws/json/TestJSONParser.java
@@ -0,0 +1,145 @@
+package com.anygo.ws.json;
+
+import com.xmage.ws.util.json.JSONParser;
+import com.xmage.ws.util.json.JSONValidationException;
+import junit.framework.Assert;
+import net.minidev.json.JSONArray;
+import net.minidev.json.JSONObject;
+import org.junit.Test;
+
+/**
+ *
+ * @author noxx
+ */
+public class TestJSONParser {
+
+ @Test
+ public void testParse() throws Exception {
+ JSONParser parser = new JSONParser();
+ parser.parseJSON("{}");
+ parser.parseJSON("{\"test\" : 1}");
+ parser.parseJSON("{\"test\" : \"test\"}");
+ parser.parseJSON("{\"list\" : [\"1\", \"2\", \"3\"]}");
+ parser.parseJSON("{test:test}");
+
+ testError(parser, "{");
+ testError(parser, "}");
+ testError(parser, "{{}");
+ testError(parser, "{\"test\" : [}}");
+ }
+
+ @Test
+ public void testQueryForInt() throws Exception {
+ JSONParser parser = new JSONParser();
+ parser.parseJSON("{\"test\" : 1}");
+ Assert.assertEquals(1, parser.getInt("test"));
+
+ parser = new JSONParser();
+ parser.parseJSON("{test : { internal : {level : 2}}}");
+ Assert.assertEquals(2, parser.getInt("test.internal.level"));
+ Assert.assertFalse("No cache should have been used", parser.isHitCache());
+
+ Assert.assertEquals(2, parser.getInt("test.internal.level"));
+ Assert.assertTrue("Cache should have been used this time!", parser.isHitCache());
+ }
+
+ @Test
+ public void testQueryForJSONArray() throws Exception {
+ JSONParser parser = new JSONParser();
+ parser.parseJSON("{\"test\" : [\"1\", \"2\", \"3\"]}");
+ Assert.assertTrue(parser.getJSONArray("test") instanceof JSONArray);
+ Assert.assertEquals("1", parser.getJSONArray("test").get(0));
+
+ parser = new JSONParser();
+ parser.parseJSON("{\"test\" : [1,2,3]}");
+ Assert.assertTrue(parser.getJSONArray("test") instanceof JSONArray);
+ Assert.assertFalse(parser.isHitCache());
+ Assert.assertEquals(2, parser.getJSONArray("test").get(1));
+ Assert.assertTrue(parser.isHitCache());
+
+ Assert.assertTrue(parser.getJSONArray("test") instanceof JSONArray);
+ Assert.assertEquals(2, parser.getJSONArray("test").get(1));
+ Assert.assertTrue(parser.isHitCache());
+
+ parser = new JSONParser();
+ parser.parseJSON("{\"test\" : [{second_level: \"3\"}, {\"third_level\" : 2}]}");
+ Assert.assertTrue(parser.getJSONArray("test") instanceof JSONArray);
+ Assert.assertTrue(parser.getJSONArray("test").get(0) instanceof JSONObject);
+ Assert.assertEquals(2, parser.getInt("test[1].third_level"));
+ Assert.assertEquals("3", parser.getString("test[0].second_level"));
+
+ parser = new JSONParser();
+ parser.parseJSON("{\"test\" : [{1:1},{1:1},{1:1},{1:1},{1:1},{1:1},{1:1},{1:1},{1:1},{2:3},{4:5}]}");
+ Assert.assertTrue(parser.getJSONArray("test") instanceof JSONArray);
+ Assert.assertEquals(5, parser.getInt("test[10].4"));
+ }
+
+ @Test
+ //TODO: implement
+ public void testErrors() throws Exception {
+ //JSONParser parser = new JSONParser();
+ //parser.parseJSON("{test : { internal : {level : \"2\"}}}");
+ //parser.getInt("test.internal.level");
+ }
+
+ @Test
+ public void testExtendedCache() throws Exception {
+ JSONParser parser = new JSONParser();
+ parser.parseJSON("{test : { internal : {level : 2}}}");
+ Assert.assertEquals(2, parser.getInt("test.internal.level"));
+ Assert.assertFalse("No cache should have been used", parser.isHitCache());
+
+ Assert.assertTrue(parser.getJSON("test") instanceof JSONObject);
+ Assert.assertFalse("No cache should have been used", parser.isHitCache());
+ Assert.assertTrue(parser.getJSON("test.internal") instanceof JSONObject);
+ Assert.assertFalse("No cache should have been used", parser.isHitCache());
+
+ parser = new JSONParser();
+ parser.parseJSON("{test : { internal : {level : 2}}}");
+ parser.setCachePolicy(JSONParser.CachePolicy.CACHE_ALL_LEVELS);
+ Assert.assertEquals(2, parser.getInt("test.internal.level"));
+ Assert.assertFalse("No cache should have been used", parser.isHitCache());
+
+ Assert.assertTrue(parser.getJSON("test") instanceof JSONObject);
+ Assert.assertTrue("Cache should have been used this time!", parser.isHitCache());
+ Assert.assertTrue(parser.getJSON("test.internal") instanceof JSONObject);
+ Assert.assertTrue("Cache should have been used this time!", parser.isHitCache());
+ }
+
+ @Test
+ public void testExtendedIndexes() throws Exception {
+ JSONParser parser = new JSONParser();
+ parser.parseJSON("{\"test\" : [1,2,3,4,5]}");
+ Assert.assertEquals(1, parser.getInt("test[].$first"));
+ Assert.assertEquals(2, parser.getInt("test[].$second"));
+ Assert.assertEquals(3, parser.getInt("test[].$third"));
+ Assert.assertEquals(4, parser.getInt("test[].$fourth"));
+ Assert.assertEquals(5, parser.getInt("test[].$fifth"));
+
+ parser = new JSONParser();
+ parser.parseJSON("{\"test\" : [{1:1},{2:2},{3:3},{4:4},{5:5}]}");
+ Assert.assertEquals(1, parser.getInt("test[].$first.1"));
+ Assert.assertEquals(2, parser.getInt("test[].$second.2"));
+ Assert.assertEquals(3, parser.getInt("test[].$third.3"));
+ Assert.assertEquals(4, parser.getInt("test[].$fourth.4"));
+ Assert.assertEquals(5, parser.getInt("test[].$fifth.5"));
+
+ parser = new JSONParser();
+ parser.parseJSON("{\"contacts\": {\"phones\": [\n" +
+ " {\"phone\": \"100000\"},\n" +
+ " {\"phone\": \"+7 999 1234567\"}\n" +
+ " ]}}");
+
+ Assert.assertEquals("100000", parser.getString("contacts.phones[].$first.phone"));
+
+ }
+
+ private void testError(JSONParser parser, String jsonToTest) throws Exception {
+ try {
+ parser.parseJSON(jsonToTest);
+ Assert.assertTrue("Should have thrown an exception", false);
+ } catch (JSONValidationException j) {
+ // ok
+ }
+ }
+}
diff --git a/Mage.Stats/src/test/java/com/anygo/ws/rest/XMageStatsServiceTest.java b/Mage.Stats/src/test/java/com/anygo/ws/rest/XMageStatsServiceTest.java
new file mode 100644
index 00000000000..2db09546391
--- /dev/null
+++ b/Mage.Stats/src/test/java/com/anygo/ws/rest/XMageStatsServiceTest.java
@@ -0,0 +1,33 @@
+package com.anygo.ws.rest;
+
+import com.xmage.ws.model.DomainErrors;
+import com.xmage.ws.rest.services.XMageStatsService;
+import com.xmage.ws.util.json.JSONParser;
+import junit.framework.Assert;
+import org.junit.Test;
+
+import javax.ws.rs.core.Response;
+
+/**
+ * Testings XMage stats service without need to deploy.
+ *
+ * @author noxx
+ */
+public class XMageStatsServiceTest {
+
+ @Test
+ public void testAddNewAndGet() throws Exception {
+
+ XMageStatsService xMageStatsService = new XMageStatsService();
+
+ Response response = xMageStatsService.getAllStats();
+
+ JSONParser parser = new JSONParser();
+ parser.parseJSON((String) response.getEntity());
+
+ Assert.assertEquals(DomainErrors.Errors.STATUS_OK.getCode(), parser.getInt("code"));
+ System.out.println("response = " + response.getEntity().toString());
+ }
+
+
+}
diff --git a/Mage.Stats/src/test/java/com/anygo/ws/util/FileUtil.java b/Mage.Stats/src/test/java/com/anygo/ws/util/FileUtil.java
new file mode 100644
index 00000000000..166dcfb1101
--- /dev/null
+++ b/Mage.Stats/src/test/java/com/anygo/ws/util/FileUtil.java
@@ -0,0 +1,32 @@
+package com.anygo.ws.util;
+
+import java.io.*;
+
+/**
+ *
+ * @author noxx
+ */
+public class FileUtil {
+
+ private FileUtil() {}
+
+ public static String readFile(String file) throws IOException {
+ InputStream in = FileUtil.class.getResourceAsStream(file);
+ if (in == null) {
+ throw new FileNotFoundException("Couldn't find file " + file);
+ }
+ Reader fr = new InputStreamReader(in, "utf-8");
+
+ BufferedReader reader = new BufferedReader(fr);
+ String line;
+ StringBuilder stringBuilder = new StringBuilder();
+ String ls = System.getProperty("line.separator");
+
+ while ((line = reader.readLine()) != null) {
+ stringBuilder.append(line);
+ stringBuilder.append(ls);
+ }
+
+ return stringBuilder.toString();
+ }
+}
diff --git a/pom.xml b/pom.xml
index 6de9ebbb523..e836ffe54a9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -53,6 +53,7 @@
Mage.Server.Console
Mage.Tests
Mage.Updater
+ Mage.Stats