From 6e4a6a88144179665a9cc257d8d3cda9171568dd Mon Sep 17 00:00:00 2001
From: blavenie <benoit.lavenier@e-is.pro>
Date: Wed, 7 Dec 2016 08:47:42 +0100
Subject: [PATCH] - ES: Move some POJO to es-user - ES: simplify UserEvent
 management

---
 .../client/model/elasticsearch/Event.java     |  78 -------
 .../client/model/elasticsearch/Record.java    |  10 +
 .../model/elasticsearch/UserProfile.java      |  38 +++
 .../core/client/service/HttpServiceImpl.java  |   3 -
 .../rest/security/RestSecurityAuthAction.java |   5 +-
 .../service/AbstractService.java              |  63 ++++-
 .../elasticsearch/threadpool/ThreadPool.java  |  10 +
 .../elasticsearch/gchange/PluginInit.java     |   4 -
 .../gchange/service/MarketService.java        |  21 +-
 .../gchange/service/RegistryService.java      |  11 +-
 .../elasticsearch/user/PluginInit.java        |   8 +-
 .../elasticsearch/user/model/UserEvent.java   | 217 ++++++++++++++++++
 .../event => model}/UserEventCodes.java       |   2 +-
 .../event => model}/UserEventLink.java        |  33 ++-
 .../elasticsearch/user/model/UserProfile.java |  74 ++++++
 .../user/service/ServiceModule.java           |   1 -
 .../service/{event => }/UserEventService.java | 146 +++++++-----
 .../user/service/UserService.java             |   1 -
 .../user/service/event/UserEvent.java         |  96 --------
 .../user/service/event/UserEventListener.java |   7 -
 .../user/service/event/UserEventUtils.java    |  84 -------
 .../websocket/WebsocketUserEventEndPoint.java |  10 +-
 22 files changed, 538 insertions(+), 384 deletions(-)
 delete mode 100644 duniter4j-core-client/src/main/java/org/duniter/core/client/model/elasticsearch/Event.java
 create mode 100644 duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserEvent.java
 rename duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/{service/event => model}/UserEventCodes.java (94%)
 rename duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/{service/event => model}/UserEventLink.java (65%)
 create mode 100644 duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserProfile.java
 rename duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/{event => }/UserEventService.java (68%)
 delete mode 100644 duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/event/UserEvent.java
 delete mode 100644 duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/event/UserEventListener.java
 delete mode 100644 duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/event/UserEventUtils.java

diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/elasticsearch/Event.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/elasticsearch/Event.java
deleted file mode 100644
index ffb926ef..00000000
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/elasticsearch/Event.java
+++ /dev/null
@@ -1,78 +0,0 @@
-package org.duniter.core.client.model.elasticsearch;
-
-/*
- * #%L
- * Duniter4j :: Core Client API
- * %%
- * Copyright (C) 2014 - 2016 EIS
- * %%
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the 
- * License, or (at your option) any later version.
- * 
- * This program 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 General Public License for more details.
- * 
- * You should have received a copy of the GNU General Public 
- * License along with this program.  If not, see
- * <http://www.gnu.org/licenses/gpl-3.0.html>.
- * #L%
- */
-
-import org.nuiton.i18n.I18n;
-
-import java.util.Locale;
-
-/**
- * Created by blavenie on 29/11/16.
- */
-public class Event extends Record {
-
-    public static final String PROPERTY_TYPE="type";
-    public static final String PROPERTY_CODE="code";
-    public static final String PROPERTY_MESSAGE="message";
-    public static final String PROPERTY_PARAMS="params";
-
-    private String type;
-
-    private String code;
-
-    private String message;
-
-    private String[] params;
-
-    public String getType() {
-        return type;
-    }
-
-    public void setType(String type) {
-        this.type = type;
-    }
-
-    public String getCode() {
-        return code;
-    }
-
-    public void setCode(String code) {
-        this.code = code;
-    }
-
-    public String getMessage() {
-        return message;
-    }
-
-    public void setMessage(String message) {
-        this.message = message;
-    }
-
-    public String[] getParams() {
-        return params;
-    }
-
-    public void setParams(String[] params) {
-        this.params = params;
-    }
-}
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/elasticsearch/Record.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/elasticsearch/Record.java
index ad56aeb6..67d8cf86 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/elasticsearch/Record.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/elasticsearch/Record.java
@@ -37,6 +37,16 @@ public class Record {
     private String signature;
     private Integer time;
 
+    public Record() {
+    }
+
+    public Record(Record another) {
+        this.issuer = another.getIssuer();
+        this.hash = another.getHash();
+        this.signature = another.getSignature();
+        this.time = another.getTime();
+    }
+
     public String getIssuer() {
         return issuer;
     }
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/elasticsearch/UserProfile.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/elasticsearch/UserProfile.java
index e5fd532c..e9efcdc4 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/elasticsearch/UserProfile.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/elasticsearch/UserProfile.java
@@ -30,5 +30,43 @@ public class UserProfile extends Record {
     public static final String PROPERTY_TITLE = "title";
     public static final String PROPERTY_DESCRIPTION="description";
     public static final String PROPERTY_CITY="city";
+    public static final String PROPERTY_EMAIL="email";
+    public static final String PROPERTY_LOCALE="locale";
 
+    private String title;
+    private String description;
+    private String email;
+    private String locale;
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    public String getEmail() {
+        return email;
+    }
+
+    public void setEmail(String email) {
+        this.email = email;
+    }
+
+    public String getLocale() {
+        return locale;
+    }
+
+    public void setLocale(String locale) {
+        this.locale = locale;
+    }
 }
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/HttpServiceImpl.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/HttpServiceImpl.java
index dd92996d..07480993 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/HttpServiceImpl.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/HttpServiceImpl.java
@@ -25,8 +25,6 @@ package org.duniter.core.client.service;
 import com.google.common.base.Joiner;
 import com.google.gson.Gson;
 import org.apache.http.client.utils.URIBuilder;
-import org.apache.http.conn.HttpHostConnectException;
-import org.apache.http.entity.mime.content.InputStreamBody;
 import org.duniter.core.beans.InitializingBean;
 import org.duniter.core.client.config.Configuration;
 import org.duniter.core.client.model.bma.Error;
@@ -52,7 +50,6 @@ import java.io.*;
 import java.net.ConnectException;
 import java.net.SocketTimeoutException;
 import java.net.URI;
-import java.net.URISyntaxException;
 import java.nio.charset.StandardCharsets;
 
 /**
diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/rest/security/RestSecurityAuthAction.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/rest/security/RestSecurityAuthAction.java
index b62408e8..300e5d92 100644
--- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/rest/security/RestSecurityAuthAction.java
+++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/rest/security/RestSecurityAuthAction.java
@@ -22,6 +22,7 @@ package org.duniter.elasticsearch.rest.security;
  * #L%
  */
 
+import com.google.gson.Gson;
 import org.duniter.core.client.model.bma.gson.GsonUtils;
 import org.duniter.core.client.service.ServiceLocator;
 import org.duniter.core.util.StringUtils;
@@ -44,6 +45,7 @@ public class RestSecurityAuthAction extends BaseRestHandler {
 
     private ChallengeMessageStore challengeMessageStore;
     private SecurityTokenStore securityTokenStore;
+    private Gson gson;
 
     @Inject
     public RestSecurityAuthAction(Settings settings, RestController controller, Client client,
@@ -53,6 +55,7 @@ public class RestSecurityAuthAction extends BaseRestHandler {
         super(settings, controller, client);
         this.challengeMessageStore = challengeMessageStore;
         this.securityTokenStore = securityTokenStore;
+        this.gson = GsonUtils.newBuilder().create();
         controller.registerHandler(POST, "/auth", this);
         securityController.allow(POST, "/auth");
     }
@@ -60,7 +63,7 @@ public class RestSecurityAuthAction extends BaseRestHandler {
     @Override
     protected void handleRequest(final RestRequest request, RestChannel restChannel, Client client) throws Exception {
 
-        AuthData authData = GsonUtils.newBuilder().create().fromJson(request.content().toUtf8(), AuthData.class);
+        AuthData authData = gson.fromJson(request.content().toUtf8(), AuthData.class);
 
         // TODO Authorization: Basic   instead ?
 
diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/AbstractService.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/AbstractService.java
index f00a564b..905328c9 100644
--- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/AbstractService.java
+++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/AbstractService.java
@@ -23,6 +23,7 @@ package org.duniter.elasticsearch.service;
  */
 
 
+import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.base.Joiner;
@@ -34,6 +35,7 @@ import org.duniter.core.beans.Bean;
 import org.duniter.core.client.model.elasticsearch.Record;
 import org.duniter.core.exception.TechnicalException;
 import org.duniter.core.service.CryptoService;
+import org.duniter.core.util.CollectionUtils;
 import org.duniter.core.util.StringUtils;
 import org.duniter.elasticsearch.PluginSettings;
 import org.duniter.elasticsearch.exception.AccessDeniedException;
@@ -60,6 +62,7 @@ import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.common.xcontent.XContentFactory;
 import org.elasticsearch.index.query.QueryBuilders;
 import org.elasticsearch.search.SearchHit;
+import org.elasticsearch.search.SearchHitField;
 import org.nuiton.i18n.I18n;
 
 import java.io.*;
@@ -104,6 +107,7 @@ public abstract class AbstractService implements Bean {
         this.pluginSettings = pluginSettings;
         this.cryptoService = cryptoService;
         this.objectMapper = new ObjectMapper();
+        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
         this.retryCount = pluginSettings.getNodeRetryCount();
         this.retryWaitDuration = pluginSettings.getNodeRetryWaitDuration();
     }
@@ -263,23 +267,15 @@ public abstract class AbstractService implements Bean {
             // Read query result
             SearchHit[] searchHits = response.getHits().getHits();
             for (SearchHit searchHit : searchHits) {
-                if (searchHit.source() != null) {
-                    JsonNode source = objectMapper.readTree(searchHit.source());
-                    for(String fieldName: fieldNames) {
-                        result.put(fieldName, getMandatoryField(source, fieldName));
-                    }
-
-                }
-                else {
-                    for(String fieldName: fieldNames) {
-                        result.put(fieldName, searchHit.getFields().get(fieldName).getValue());
-                    }
+                Map<String, SearchHitField> hitFields = searchHit.getFields();
+                for(String fieldName: hitFields.keySet()) {
+                    result.put(fieldName, hitFields.get(fieldName).getValue());
                 }
                 break;
             }
             return result;
         }
-        catch(SearchPhaseExecutionException | JsonSyntaxException | IOException e) {
+        catch(SearchPhaseExecutionException | JsonSyntaxException e) {
             // Failed or no item on index
             throw new TechnicalException(String.format("[%s/%s] Unable to retrieve fields [%s] for document [%s]",
                     index, type,
@@ -302,6 +298,49 @@ public abstract class AbstractService implements Bean {
         return result.get(fieldName);
     }
 
+    /**
+     * Retrieve a document by id
+     * @param docId
+     * @return
+     */
+    public <T extends Object> T getSourceById(String index, String type, String docId, Class<T> classOfT, String... fieldNames) {
+
+        // Prepare request
+        SearchRequestBuilder searchRequest = client
+                .prepareSearch(index)
+                .setTypes(type)
+                .setSearchType(SearchType.DFS_QUERY_THEN_FETCH);
+
+        searchRequest.setQuery(QueryBuilders.matchQuery("_id", docId));
+        if (CollectionUtils.isNotEmpty(fieldNames)) {
+            searchRequest.setFetchSource(fieldNames, null);
+        }
+        else {
+            searchRequest.setFetchSource(true); // full source
+        }
+
+        // Execute query
+        try {
+            SearchResponse response = searchRequest.execute().actionGet();
+
+            // Read query result
+            SearchHit[] searchHits = response.getHits().getHits();
+            for (SearchHit searchHit : searchHits) {
+                if (searchHit.source() != null) {
+                    return objectMapper.readValue(searchHit.source(), classOfT);
+                }
+                break;
+            }
+            return null;
+        }
+        catch(SearchPhaseExecutionException | JsonSyntaxException | IOException e) {
+            // Failed to get source
+            throw new TechnicalException(String.format("[%s/%s] Error while getting [%s]",
+                    index, type,
+                    docId), e);
+        }
+    }
+
     protected void bulkFromClasspathFile(String classpathFile, String indexName, String indexType) {
         bulkFromClasspathFile(classpathFile, indexName, indexType, null);
     }
diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/threadpool/ThreadPool.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/threadpool/ThreadPool.java
index 85944904..0be7bc3a 100644
--- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/threadpool/ThreadPool.java
+++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/threadpool/ThreadPool.java
@@ -126,6 +126,16 @@ public class ThreadPool extends AbstractLifecycleComponent<ThreadPool> {
         });
     }
 
+    /**
+     * Schedules an rest that runs on the scheduler thread, when possible (0 delay).
+     *
+     * @param command the rest to take
+     * @return a ScheduledFuture who's get will return when the task is complete and throw an exception if it is canceled
+     */
+    public ScheduledFuture<?> schedule(Runnable command) {
+        return scheduler.schedule(new LoggingRunnable(command), 0, TimeUnit.MILLISECONDS);
+    }
+
     /**
      * Schedules an rest that runs on the scheduler thread, after a delay.
      *
diff --git a/duniter4j-es-gchange/src/main/java/org/duniter/elasticsearch/gchange/PluginInit.java b/duniter4j-es-gchange/src/main/java/org/duniter/elasticsearch/gchange/PluginInit.java
index e148805a..51b73d86 100644
--- a/duniter4j-es-gchange/src/main/java/org/duniter/elasticsearch/gchange/PluginInit.java
+++ b/duniter4j-es-gchange/src/main/java/org/duniter/elasticsearch/gchange/PluginInit.java
@@ -26,10 +26,6 @@ import org.duniter.elasticsearch.gchange.service.MarketService;
 import org.duniter.elasticsearch.gchange.service.RegistryService;
 import org.duniter.elasticsearch.gchange.service.SynchroService;
 import org.duniter.elasticsearch.threadpool.ThreadPool;
-import org.duniter.elasticsearch.user.service.event.UserEvent;
-import org.duniter.elasticsearch.user.service.event.UserEventCodes;
-import org.duniter.elasticsearch.user.service.event.UserEventService;
-import org.elasticsearch.client.Client;
 import org.elasticsearch.cluster.health.ClusterHealthStatus;
 import org.elasticsearch.common.component.AbstractLifecycleComponent;
 import org.elasticsearch.common.inject.Inject;
diff --git a/duniter4j-es-gchange/src/main/java/org/duniter/elasticsearch/gchange/service/MarketService.java b/duniter4j-es-gchange/src/main/java/org/duniter/elasticsearch/gchange/service/MarketService.java
index 95b97aaf..1236558c 100644
--- a/duniter4j-es-gchange/src/main/java/org/duniter/elasticsearch/gchange/service/MarketService.java
+++ b/duniter4j-es-gchange/src/main/java/org/duniter/elasticsearch/gchange/service/MarketService.java
@@ -35,10 +35,9 @@ import org.duniter.elasticsearch.gchange.PluginSettings;
 import org.duniter.elasticsearch.gchange.model.MarketRecord;
 import org.duniter.elasticsearch.gchange.model.event.GchangeEventCodes;
 import org.duniter.elasticsearch.service.AbstractService;
+import org.duniter.elasticsearch.user.model.UserEvent;
 import org.duniter.elasticsearch.user.service.UserService;
-import org.duniter.elasticsearch.user.service.event.UserEvent;
-import org.duniter.elasticsearch.user.service.event.UserEventLink;
-import org.duniter.elasticsearch.user.service.event.UserEventService;
+import org.duniter.elasticsearch.user.service.UserEventService;
 import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder;
 import org.elasticsearch.action.index.IndexRequestBuilder;
 import org.elasticsearch.action.index.IndexResponse;
@@ -432,15 +431,15 @@ public class MarketService extends AbstractService {
 
         String recordTitle = recordFields.get(MarketRecord.PROPERTY_TITLE).toString();
         if (!issuer.equals(recordIssuer)) {
-            userEventService.notifyUser(recordIssuer,
-                    new UserEvent(UserEvent.EventType.INFO,
-                            GchangeEventCodes.NEW_COMMENT.name(),
-                            new UserEventLink(INDEX, RECORD_TYPE, recordId),
-                            I18n.n(isNewComment ? "duniter.market.event.newComment": "duniter.market.event.updateComment"),
+            userEventService.notifyUser(
+                    UserEvent.newBuilder(UserEvent.EventType.INFO, GchangeEventCodes.NEW_COMMENT.name())
+                    .setMessage(
+                            isNewComment ? I18n.n("duniter.market.event.newComment") : I18n.n("duniter.market.event.updateComment"),
                             issuerTitle != null ? issuerTitle : issuer.substring(0, 8),
-                            recordTitle
-                    )
-            );
+                            recordTitle)
+                    .setRecipient(recordIssuer)
+                    .setLink(INDEX, RECORD_TYPE, recordId)
+                    .build());
         }
     }
 
diff --git a/duniter4j-es-gchange/src/main/java/org/duniter/elasticsearch/gchange/service/RegistryService.java b/duniter4j-es-gchange/src/main/java/org/duniter/elasticsearch/gchange/service/RegistryService.java
index 7505b8f4..8792b19d 100644
--- a/duniter4j-es-gchange/src/main/java/org/duniter/elasticsearch/gchange/service/RegistryService.java
+++ b/duniter4j-es-gchange/src/main/java/org/duniter/elasticsearch/gchange/service/RegistryService.java
@@ -25,20 +25,13 @@ package org.duniter.elasticsearch.gchange.service;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.google.gson.Gson;
-import org.apache.commons.collections4.MapUtils;
 import org.duniter.core.client.model.bma.gson.GsonUtils;
-import org.duniter.core.client.model.elasticsearch.RecordComment;
 import org.duniter.core.client.service.bma.BlockchainRemoteService;
 import org.duniter.core.exception.TechnicalException;
 import org.duniter.core.service.CryptoService;
-import org.duniter.elasticsearch.exception.DocumentNotFoundException;
 import org.duniter.elasticsearch.gchange.PluginSettings;
-import org.duniter.elasticsearch.gchange.model.MarketRecord;
-import org.duniter.elasticsearch.gchange.model.event.GchangeEventCodes;
 import org.duniter.elasticsearch.service.AbstractService;
-import org.duniter.elasticsearch.user.service.event.UserEvent;
-import org.duniter.elasticsearch.user.service.event.UserEventLink;
-import org.duniter.elasticsearch.user.service.event.UserEventService;
+import org.duniter.elasticsearch.user.service.UserEventService;
 import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder;
 import org.elasticsearch.action.index.IndexRequestBuilder;
 import org.elasticsearch.action.index.IndexResponse;
@@ -46,10 +39,8 @@ import org.elasticsearch.client.Client;
 import org.elasticsearch.common.inject.Inject;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.common.xcontent.XContentFactory;
-import org.nuiton.i18n.I18n;
 
 import java.io.IOException;
-import java.util.Map;
 
 /**
  * Created by Benoit on 30/03/2015.
diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/PluginInit.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/PluginInit.java
index aeb251d9..1b9f5303 100644
--- a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/PluginInit.java
+++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/PluginInit.java
@@ -24,15 +24,13 @@ package org.duniter.elasticsearch.user;
 
 import org.duniter.elasticsearch.PluginSettings;
 import org.duniter.elasticsearch.threadpool.ThreadPool;
+import org.duniter.elasticsearch.user.model.UserEvent;
 import org.duniter.elasticsearch.user.service.HistoryService;
 import org.duniter.elasticsearch.user.service.MessageService;
 import org.duniter.elasticsearch.user.service.SynchroService;
 import org.duniter.elasticsearch.user.service.UserService;
-import org.duniter.elasticsearch.user.service.event.UserEvent;
-import org.duniter.elasticsearch.user.service.event.UserEventCodes;
-import org.duniter.elasticsearch.user.service.event.UserEventService;
-import org.duniter.elasticsearch.user.websocket.WebsocketUserEventEndPoint;
-import org.duniter.elasticsearch.websocket.WebSocketServer;
+import org.duniter.elasticsearch.user.model.UserEventCodes;
+import org.duniter.elasticsearch.user.service.UserEventService;
 import org.elasticsearch.cluster.health.ClusterHealthStatus;
 import org.elasticsearch.common.component.AbstractLifecycleComponent;
 import org.elasticsearch.common.inject.Inject;
diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserEvent.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserEvent.java
new file mode 100644
index 00000000..5e8e4921
--- /dev/null
+++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserEvent.java
@@ -0,0 +1,217 @@
+package org.duniter.elasticsearch.user.model;
+
+/*
+ * #%L
+ * Duniter4j :: Core Client API
+ * %%
+ * Copyright (C) 2014 - 2016 EIS
+ * %%
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the 
+ * License, or (at your option) any later version.
+ * 
+ * This program 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public 
+ * License along with this program.  If not, see
+ * <http://www.gnu.org/licenses/gpl-3.0.html>.
+ * #L%
+ */
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.duniter.core.client.model.elasticsearch.Record;
+import org.duniter.core.exception.TechnicalException;
+import org.nuiton.i18n.I18n;
+
+import java.util.Locale;
+
+/**
+ * Created by blavenie on 29/11/16.
+ */
+public class UserEvent extends Record {
+
+    public enum EventType {
+        INFO,
+        WARN,
+        ERROR
+    }
+
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    public static Builder newBuilder(UserEvent.EventType type, String code) {
+        return new Builder(type, code, null, null);
+    }
+
+    public static Builder newBuilder(UserEvent.EventType type, String code, String message, String... params) {
+        return new Builder(type, code, message, params);
+    }
+
+    public static final String PROPERTY_TYPE="type";
+    public static final String PROPERTY_CODE="code";
+    public static final String PROPERTY_MESSAGE="message";
+    public static final String PROPERTY_PARAMS="params";
+    public static final String PROPERTY_LINK="link";
+    public static final String PROPERTY_RECIPIENT="recipient";
+
+
+    private EventType type;
+
+    private String recipient;
+
+    private String code;
+
+    private String message;
+
+    private String[] params;
+
+    private UserEventLink link;
+
+    public UserEvent() {
+        super();
+    }
+
+    public UserEvent(EventType type, String code, String message, String... params) {
+        super();
+        this.type = type;
+        this.code = code;
+        this.message = message;
+        this.params = params;
+        setTime(getDefaultTime());
+    }
+
+    public UserEvent(UserEvent another) {
+        super(another);
+        this.type = another.getType();
+        this.code = another.getCode();
+        this.params = another.getParams();
+        this.link = (another.getLink() != null) ? new UserEventLink(another.getLink()) : null;
+        this.message = another.getMessage();
+        this.recipient = another.getRecipient();
+    }
+
+    public EventType getType() {
+        return type;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    public String[] getParams() {
+        return params;
+    }
+
+    public UserEventLink getLink() {
+        return link;
+    }
+
+    public String getLocalizedMessage(Locale locale) {
+        return I18n.l(locale, this.message, this.params);
+    }
+
+    public void setType(EventType type) {
+        this.type = type;
+    }
+
+    public void setCode(String code) {
+        this.code = code;
+    }
+
+    public void setMessage(String message) {
+        this.message = message;
+    }
+
+    public void setParams(String[] params) {
+        this.params = params;
+    }
+
+    public void setLink(UserEventLink link) {
+        this.link = link;
+    }
+
+    public String getRecipient() {
+        return recipient;
+    }
+
+    public void setRecipient(String recipient) {
+        this.recipient = recipient;
+    }
+
+    @JsonIgnore
+    public String toJson() {
+        try {
+            ObjectMapper mapper = new ObjectMapper();
+            return mapper.writeValueAsString(this);
+        } catch(Exception e) {
+            throw new TechnicalException(e);
+        }
+    }
+
+    @JsonIgnore
+    public String toJson(Locale locale) {
+        UserEvent copy = new UserEvent(this);
+        copy.setMessage(getLocalizedMessage(locale));
+        return copy.toJson();
+    }
+
+    public static class Builder {
+
+        private UserEvent result;
+
+        private Builder() {
+            result = new UserEvent();
+        }
+
+        public Builder(UserEvent.EventType type, String code, String message, String... params) {
+            result = new UserEvent(type, code, message, params);
+        }
+
+        public Builder setMessage(String message, String... params) {
+            result.setMessage(message);
+            result.setParams(params);
+            return this;
+        }
+
+        public Builder setRecipient(String recipient) {
+            result.setRecipient(recipient);
+            return this;
+        }
+
+        public Builder setIssuer(String issuer) {
+            result.setIssuer(issuer);
+            return this;
+        }
+
+        public Builder setLink(String index, String type, String id) {
+            result.setLink(new UserEventLink(index, type, id));
+            return this;
+        }
+
+        public Builder setLink(String index, String type, String id, String anchor) {
+            result.setLink(new UserEventLink(index, type, id, anchor));
+            return this;
+        }
+
+        public UserEvent build() {
+            if (result.getTime() == null) {
+                result.setTime(getDefaultTime());
+            }
+            return new UserEvent(result);
+        }
+    }
+
+    private static int getDefaultTime() {
+        return Math.round(1f * System.currentTimeMillis() / 1000);
+    }
+}
diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/event/UserEventCodes.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserEventCodes.java
similarity index 94%
rename from duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/event/UserEventCodes.java
rename to duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserEventCodes.java
index 6c334ef9..39e4d4e0 100644
--- a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/event/UserEventCodes.java
+++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserEventCodes.java
@@ -1,4 +1,4 @@
-package org.duniter.elasticsearch.user.service.event;
+package org.duniter.elasticsearch.user.model;
 
 /*
  * #%L
diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/event/UserEventLink.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserEventLink.java
similarity index 65%
rename from duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/event/UserEventLink.java
rename to duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserEventLink.java
index 23e1c87b..fb03cd71 100644
--- a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/event/UserEventLink.java
+++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserEventLink.java
@@ -1,4 +1,4 @@
-package org.duniter.elasticsearch.user.service.event;
+package org.duniter.elasticsearch.user.model;
 
 /*
  * #%L
@@ -22,25 +22,38 @@ package org.duniter.elasticsearch.user.service.event;
  * #L%
  */
 
-import org.nuiton.i18n.I18n;
-
-import java.util.Locale;
-
 /**
  * Created by blavenie on 29/11/16.
  */
 public class UserEventLink {
 
-    private final String index;
+    private String index;
+
+    private String type;
 
-    private final String type;
+    private String id;
 
-    private final String id;
+    private String anchor;
+
+    public UserEventLink() {
+    }
 
     public UserEventLink(String index, String type, String id) {
+        this(index, type, id, null);
+    }
+
+    public UserEventLink(String index, String type, String id, String anchor) {
         this.index = index;
         this.type = type;
         this.id = id;
+        this.anchor = anchor;
+    }
+
+    public UserEventLink(UserEventLink another) {
+        this.index = another.getIndex();
+        this.type = another.getType();
+        this.id = another.getId();
+        this.anchor = another.getAnchor();
     }
 
     public String getIndex() {
@@ -54,4 +67,8 @@ public class UserEventLink {
     public String getId() {
         return id;
     }
+
+    public String getAnchor() {
+        return anchor;
+    }
 }
diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserProfile.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserProfile.java
new file mode 100644
index 00000000..54608f00
--- /dev/null
+++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserProfile.java
@@ -0,0 +1,74 @@
+package org.duniter.elasticsearch.user.model;
+
+/*
+ * #%L
+ * Duniter4j :: Core Client API
+ * %%
+ * Copyright (C) 2014 - 2016 EIS
+ * %%
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the 
+ * License, or (at your option) any later version.
+ * 
+ * This program 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public 
+ * License along with this program.  If not, see
+ * <http://www.gnu.org/licenses/gpl-3.0.html>.
+ * #L%
+ */
+
+import org.duniter.core.client.model.elasticsearch.Record;
+
+/**
+ * Created by blavenie on 01/03/16.
+ */
+public class UserProfile extends Record {
+
+    public static final String PROPERTY_TITLE = "title";
+    public static final String PROPERTY_DESCRIPTION="description";
+    public static final String PROPERTY_CITY="city";
+    public static final String PROPERTY_EMAIL="email";
+    public static final String PROPERTY_LOCALE="locale";
+
+    private String title;
+    private String description;
+    private String email;
+    private String locale;
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    public String getEmail() {
+        return email;
+    }
+
+    public void setEmail(String email) {
+        this.email = email;
+    }
+
+    public String getLocale() {
+        return locale;
+    }
+
+    public void setLocale(String locale) {
+        this.locale = locale;
+    }
+}
diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/ServiceModule.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/ServiceModule.java
index a24ab24b..30e1b815 100644
--- a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/ServiceModule.java
+++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/ServiceModule.java
@@ -22,7 +22,6 @@ package org.duniter.elasticsearch.user.service;
  * #L%
  */
 
-import org.duniter.elasticsearch.user.service.event.UserEventService;
 import org.elasticsearch.common.inject.AbstractModule;
 import org.elasticsearch.common.inject.Module;
 
diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/event/UserEventService.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/UserEventService.java
similarity index 68%
rename from duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/event/UserEventService.java
rename to duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/UserEventService.java
index 6d412001..1f002846 100644
--- a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/event/UserEventService.java
+++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/UserEventService.java
@@ -1,4 +1,4 @@
-package org.duniter.elasticsearch.user.service.event;
+package org.duniter.elasticsearch.user.service;
 
 /*
  * #%L
@@ -25,25 +25,21 @@ package org.duniter.elasticsearch.user.service.event;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.base.Preconditions;
 import org.duniter.core.exception.TechnicalException;
 import org.duniter.core.service.CryptoService;
 import org.duniter.core.service.MailService;
-import org.duniter.core.util.CollectionUtils;
 import org.duniter.core.util.StringUtils;
 import org.duniter.core.util.crypto.CryptoUtils;
 import org.duniter.core.util.crypto.KeyPair;
 import org.duniter.elasticsearch.PluginSettings;
 import org.duniter.elasticsearch.service.AbstractService;
-import org.duniter.elasticsearch.service.changes.ChangeEvent;
-import org.duniter.elasticsearch.service.changes.ChangeListener;
-import org.duniter.elasticsearch.service.changes.ChangeService;
-import org.duniter.elasticsearch.service.changes.ChangeUtils;
 import org.duniter.elasticsearch.threadpool.ThreadPool;
-import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder;
+import org.duniter.elasticsearch.user.model.UserEvent;
+import org.duniter.elasticsearch.user.model.UserProfile;
 import org.elasticsearch.action.index.IndexResponse;
 import org.elasticsearch.client.Client;
 import org.elasticsearch.common.inject.Inject;
-import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.unit.TimeValue;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.common.xcontent.XContentFactory;
@@ -59,16 +55,26 @@ import java.util.Map;
  */
 public class UserEventService extends AbstractService {
 
+    public interface UserEventListener {
+        String getId();
+        String getPubkey();
+        void onEvent(UserEvent event);
+    }
+
     public static final String INDEX = "user";
     public static final String EVENT_TYPE = "event";
     private static final Map<String, UserEventListener> LISTENERS = new HashMap<>();
 
     public static void registerListener(UserEventListener listener) {
-        LISTENERS.put(listener.getId(), listener);
+        synchronized (LISTENERS) {
+            LISTENERS.put(listener.getId(), listener);
+        }
     }
 
-    public static void unregisterListener(UserEventListener listener) {
-        LISTENERS.remove(listener.getId());
+    public static synchronized void unregisterListener(UserEventListener listener) {
+        synchronized (LISTENERS) {
+            LISTENERS.remove(listener.getId());
+        }
     }
 
     private final MailService mailService;
@@ -78,7 +84,8 @@ public class UserEventService extends AbstractService {
     public final boolean mailEnable;
 
     @Inject
-    public UserEventService(Client client, PluginSettings settings, CryptoService cryptoService, MailService mailService,
+    public UserEventService(Client client, PluginSettings settings, CryptoService cryptoService,
+                            MailService mailService,
                             ThreadPool threadPool) {
         super("duniter.event." + INDEX, client, settings, cryptoService);
         this.mailService = mailService;
@@ -95,20 +102,28 @@ public class UserEventService extends AbstractService {
      * Notify cluster admin
      */
     public void notifyAdmin(UserEvent event) {
-        Locale locale = I18n.getDefaultLocale(); // TODO get locale from admin
 
-        // Add new event to index
+        UserProfile adminProfile;
         if (StringUtils.isNotBlank(nodePubkey)) {
-            indexEvent(nodePubkey, locale, event);
+            adminProfile = getUserProfile(nodePubkey, UserProfile.PROPERTY_EMAIL, UserProfile.PROPERTY_LOCALE);
+        }
+        else {
+            adminProfile = new UserProfile();
         }
 
-        // Retrieve admin email
-        String adminEmail = pluginSettings.getMailAdmin();
-        if (StringUtils.isBlank(adminEmail) && StringUtils.isNotBlank(nodePubkey)) {
-            adminEmail = getEmailByPk(nodePubkey);
+        // Add new event to index
+        Locale locale = StringUtils.isNotBlank(adminProfile.getLocale()) ?
+                new Locale(adminProfile.getLocale()) :
+                I18n.getDefaultLocale();
+        if (StringUtils.isNotBlank(nodePubkey)) {
+            event.setRecipient(nodePubkey);
+            indexEvent(locale, event);
         }
 
         // Send email to admin
+        String adminEmail = StringUtils.isNotBlank(adminProfile.getEmail()) ?
+                adminProfile.getEmail() :
+                pluginSettings.getMailAdmin();
         if (StringUtils.isNotBlank(adminEmail)) {
             String subjectPrefix = pluginSettings.getMailSubjectPrefix();
             sendEmail(adminEmail,
@@ -120,31 +135,43 @@ public class UserEventService extends AbstractService {
     /**
      * Notify a user
      */
-    public void notifyUser(String recipient, UserEvent event) {
+    public void notifyUser(UserEvent event) {
         // Notify user
         threadPool.schedule(() -> {
-            doNotifyUser(recipient, event);
+            doNotifyUser(event);
         }, TimeValue.timeValueMillis(100));
     }
 
-    public String indexEvent(String recipient, Locale locale, UserEvent event) {
+    public String indexEvent(Locale locale, UserEvent event) {
+        Preconditions.checkNotNull(event.getRecipient());
+        Preconditions.checkNotNull(event.getType());
+        Preconditions.checkNotNull(event.getCode());
+
         // Generate json
         String eventJson;
         if (StringUtils.isNotBlank(nodePubkey)) {
-            eventJson = UserEventUtils.toJson(nodePubkey, recipient, locale, event, null);
-            String signature = cryptoService.sign(eventJson, nodeKeyPair.getSecKey());
-            eventJson = UserEventUtils.toJson(nodePubkey, recipient, locale, event, signature);
+            UserEvent signedEvent = new UserEvent(event);
+            signedEvent.setMessage(event.getLocalizedMessage(locale));
+            // set issuer, hash, signature
+            signedEvent.setIssuer(nodePubkey);
+            String hash = cryptoService.hash(toJson(signedEvent));
+            signedEvent.setHash(hash);
+            String signature = cryptoService.sign(toJson(signedEvent), nodeKeyPair.getSecKey());
+            signedEvent.setSignature(signature);
+            eventJson = toJson(signedEvent);
         } else {
-            // Node has not keyring : TODO no issuer ?
-            eventJson = UserEventUtils.toJson(recipient, recipient, locale, event, null);
+            // Node has not keyring: do NOT sign it
+            // TODO : autogen a key pair ?
+            eventJson = event.toJson(locale);
         }
 
         if (logger.isDebugEnabled()) {
-            logger.debug(String.format("Indexing a event to recipient [%s]", recipient.substring(0, 8)));
+            logger.debug(String.format("Indexing a event to recipient [%s]", event.getRecipient().substring(0, 8)));
         }
 
         // do indexation
         return indexEvent(eventJson, false /*checkSignature*/);
+
     }
 
     public String indexEvent(String eventJson) {
@@ -155,7 +182,7 @@ public class UserEventService extends AbstractService {
 
         if (checkSignature) {
             JsonNode jsonNode = readAndVerifyIssuerSignature(eventJson);
-            String recipient = jsonNode.get(org.duniter.core.client.model.elasticsearch.Event.PROPERTY_ISSUER).asText();
+            String recipient = getMandatoryField(jsonNode, UserEvent.PROPERTY_ISSUER).asText();
             if (logger.isDebugEnabled()) {
                 logger.debug(String.format("Indexing a event to recipient [%s]", recipient.substring(0, 8)));
             }
@@ -248,15 +275,6 @@ public class UserEventService extends AbstractService {
         }
     }
 
-    private String getEmailByPk(String issuerPk) {
-        // TODO get it from user profile ?
-        return pluginSettings.getMailAdmin();
-    }
-
-    private String getEmailSubject(Locale locale, UserEvent event) {
-
-        return  I18n.l(locale, "duniter4j.event.subject."+event.getType().name());
-    }
 
     /**
      * Send email
@@ -278,8 +296,6 @@ public class UserEventService extends AbstractService {
         }
     }
 
-
-
     private KeyPair getNodeKeyPairOrNull(PluginSettings pluginSettings) {
 
         if (StringUtils.isNotBlank(pluginSettings.getKeyringSalt()) &&
@@ -299,27 +315,45 @@ public class UserEventService extends AbstractService {
     /**
      * Notify a user
      */
-    private void doNotifyUser(String recipient, UserEvent event) {
+    private void doNotifyUser(final UserEvent event) {
+        Preconditions.checkNotNull(event.getRecipient());
 
-        String email = getEmailByPk(recipient);
-        Locale locale = I18n.getDefaultLocale(); // TODO get locale
+        // Get user profile locale
+        UserProfile userProfile = getUserProfile(event.getRecipient(),
+                UserProfile.PROPERTY_EMAIL, UserProfile.PROPERTY_TITLE, UserProfile.PROPERTY_LOCALE);
 
-        // Add new event to index
-        indexEvent(recipient, locale, event);
+        Locale locale = userProfile.getLocale() != null ? new Locale(userProfile.getLocale()) : null;
 
-        // Send email to user
-        // TODO : group email by day ?
-        if (StringUtils.isNotBlank(email)) {
-            String subjectPrefix = pluginSettings.getMailSubjectPrefix();
-            sendEmail(email,
-                    I18n.l(locale, "duniter4j.event.subject."+event.getType().name(), subjectPrefix),
-                    event.getLocalizedMessage(locale));
-        }
+        // Add new event to index
+        indexEvent(locale, event);
 
-        for (UserEventListener listener: LISTENERS.values()) {
-            if (recipient.equals(listener.getPubkey())) {
-                listener.onEvent(event);
+        // Notify listeners
+        threadPool.schedule(() -> {
+            synchronized (LISTENERS) {
+                for (UserEventListener listener : LISTENERS.values()) {
+                    if (event.getRecipient().equals(listener.getPubkey())) {
+                        listener.onEvent(event);
+                    }
+                }
             }
+        });
+    }
+
+    private UserProfile getUserProfile(String pubkey, String... fieldnames) {
+        UserProfile result = getSourceById(UserService.INDEX, UserService.PROFILE_TYPE, pubkey, UserProfile.class, fieldnames);
+        if (result == null) result = new UserProfile();
+        return result;
+    }
+
+    private UserProfile getUserProfileOrNull(String pubkey, String... fieldnames) {
+        return getSourceById(UserService.INDEX, UserService.PROFILE_TYPE, pubkey, UserProfile.class, fieldnames);
+    }
+
+    private String toJson(UserEvent userEvent) {
+        try {
+            return objectMapper.writeValueAsString(userEvent);
+        } catch(JsonProcessingException e) {
+            throw new TechnicalException("Unable to serialize UserEvent object", e);
         }
     }
 }
diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/UserService.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/UserService.java
index cad585fd..0cfbf566 100644
--- a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/UserService.java
+++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/UserService.java
@@ -32,7 +32,6 @@ import org.duniter.core.service.MailService;
 import org.duniter.elasticsearch.PluginSettings;
 import org.duniter.elasticsearch.exception.AccessDeniedException;
 import org.duniter.elasticsearch.service.AbstractService;
-import org.duniter.elasticsearch.user.service.event.UserEventService;
 import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder;
 import org.elasticsearch.action.index.IndexResponse;
 import org.elasticsearch.action.update.UpdateResponse;
diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/event/UserEvent.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/event/UserEvent.java
deleted file mode 100644
index e58ccbd2..00000000
--- a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/event/UserEvent.java
+++ /dev/null
@@ -1,96 +0,0 @@
-package org.duniter.elasticsearch.user.service.event;
-
-/*
- * #%L
- * Duniter4j :: ElasticSearch Plugin
- * %%
- * Copyright (C) 2014 - 2016 EIS
- * %%
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the 
- * License, or (at your option) any later version.
- * 
- * This program 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 General Public License for more details.
- * 
- * You should have received a copy of the GNU General Public 
- * License along with this program.  If not, see
- * <http://www.gnu.org/licenses/gpl-3.0.html>.
- * #L%
- */
-
-import org.nuiton.i18n.I18n;
-
-import java.util.Locale;
-
-/**
- * Created by blavenie on 29/11/16.
- */
-public class UserEvent {
-
-    private final EventType type;
-
-    private final String code;
-
-    private final long time;
-
-    private final String message;
-
-    private final String[] params;
-
-    private final UserEventLink link;
-
-    public UserEvent(EventType type, String code) {
-        this(type, code, null, "duniter.event." + code, null);
-    }
-
-    public UserEvent(EventType type, String code, String message, String... params) {
-        this(type, code, null, message, params);
-    }
-
-    public UserEvent(EventType type, String code, UserEventLink link, String message, String... params) {
-        this.type = type;
-        this.code = code;
-        this.params = params;
-        this.link = link;
-        this.message = message;
-        this.time = Math.round(1d * System.currentTimeMillis() / 1000);
-    }
-
-    public EventType getType() {
-        return type;
-    }
-
-    public String getCode() {
-        return code;
-    }
-
-    public String getMessage() {
-        return message;
-    }
-
-    public String getLocalizedMessage(Locale locale) {
-        return I18n.l(locale, message, params);
-    }
-
-    public String[] getParams() {
-        return params;
-    }
-
-    public long getTime() {
-        return time;
-    }
-
-    public UserEventLink getLink() {
-        return link;
-    }
-
-    public enum EventType {
-        INFO,
-        WARN,
-        ERROR
-    }
-}
diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/event/UserEventListener.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/event/UserEventListener.java
deleted file mode 100644
index 9238e4a8..00000000
--- a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/event/UserEventListener.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package org.duniter.elasticsearch.user.service.event;
-
-public interface UserEventListener {
-    String getId();
-    String getPubkey();
-    void onEvent(UserEvent event);
-}
\ No newline at end of file
diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/event/UserEventUtils.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/event/UserEventUtils.java
deleted file mode 100644
index 20f2ed4d..00000000
--- a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/event/UserEventUtils.java
+++ /dev/null
@@ -1,84 +0,0 @@
-package org.duniter.elasticsearch.user.service.event;
-
-import org.duniter.core.exception.TechnicalException;
-import org.duniter.core.util.CollectionUtils;
-import org.duniter.core.util.StringUtils;
-import org.elasticsearch.common.xcontent.XContentBuilder;
-import org.elasticsearch.common.xcontent.XContentFactory;
-
-import java.io.IOException;
-import java.util.Locale;
-
-/**
- * Created by blavenie on 02/12/16.
- */
-public abstract class UserEventUtils {
-
-    public static String toJson(String issuer, String recipient, Locale locale, UserEvent event, String signature) {
-        try {
-            XContentBuilder eventObject = XContentFactory.jsonBuilder().startObject()
-                    .field("type", event.getType().name())
-                    .field("issuer", issuer) // TODO isuer = node pubkey
-                    .field("recipient", recipient)
-                    .field("time", event.getTime())
-                    .field("code", event.getCode())
-                    .field("message", event.getLocalizedMessage(locale));
-            if (CollectionUtils.isNotEmpty(event.getParams())) {
-                eventObject.array("params", event.getParams());
-            }
-
-            // Link
-            UserEventLink link = event.getLink();
-            if (link != null) {
-                eventObject.startObject("link")
-                        .field("index", link.getIndex())
-                        .field("type", link.getType());
-                if (StringUtils.isNotBlank(link.getId())) {
-                    eventObject.field("id", link.getId());
-                }
-                eventObject.endObject();
-            }
-
-            if (StringUtils.isNotBlank(signature)) {
-                eventObject.field("signature", signature);
-            }
-            eventObject.endObject();
-            return eventObject.string();
-        }
-        catch(IOException e) {
-            throw new TechnicalException(e);
-        }
-
-    }
-
-    public static String toJson(Locale locale, UserEvent event) {
-        try {
-            XContentBuilder eventObject = XContentFactory.jsonBuilder().startObject()
-                    .field("type", event.getType().name())
-                    .field("time", event.getTime())
-                    .field("code", event.getCode())
-                    .field("message", event.getLocalizedMessage(locale));
-            if (CollectionUtils.isNotEmpty(event.getParams())) {
-                eventObject.array("params", event.getParams());
-            }
-
-            // Link
-            UserEventLink link = event.getLink();
-            if (link != null) {
-                eventObject.startObject("link")
-                        .field("index", link.getIndex())
-                        .field("type", link.getType());
-                if (StringUtils.isNotBlank(link.getId())) {
-                    eventObject.field("id", link.getId());
-                }
-                eventObject.endObject();
-            }
-            eventObject.endObject();
-            return eventObject.string();
-        }
-        catch(IOException e) {
-            throw new TechnicalException(e);
-        }
-
-    }
-}
diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/websocket/WebsocketUserEventEndPoint.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/websocket/WebsocketUserEventEndPoint.java
index 2c01b7ef..5037ac3e 100644
--- a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/websocket/WebsocketUserEventEndPoint.java
+++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/websocket/WebsocketUserEventEndPoint.java
@@ -40,10 +40,8 @@ package org.duniter.elasticsearch.user.websocket;
 
 import org.duniter.core.client.model.bma.Constants;
 import org.duniter.core.util.StringUtils;
-import org.duniter.elasticsearch.user.service.event.UserEvent;
-import org.duniter.elasticsearch.user.service.event.UserEventListener;
-import org.duniter.elasticsearch.user.service.event.UserEventService;
-import org.duniter.elasticsearch.user.service.event.UserEventUtils;
+import org.duniter.elasticsearch.user.model.UserEvent;
+import org.duniter.elasticsearch.user.service.UserEventService;
 import org.duniter.elasticsearch.websocket.WebSocketServer;
 import org.elasticsearch.common.inject.Inject;
 import org.elasticsearch.common.logging.ESLogger;
@@ -57,7 +55,7 @@ import java.util.Locale;
 import java.util.regex.Pattern;
 
 @ServerEndpoint(value = "/event/user/{pubkey}/{locale}")
-public class WebsocketUserEventEndPoint implements UserEventListener {
+public class WebsocketUserEventEndPoint implements UserEventService.UserEventListener {
 
     public static class Init {
 
@@ -97,7 +95,7 @@ public class WebsocketUserEventEndPoint implements UserEventListener {
 
     @Override
     public void onEvent(UserEvent event) {
-        session.getAsyncRemote().sendText(UserEventUtils.toJson(locale, event));
+        session.getAsyncRemote().sendText(event.toJson(locale));
     }
 
     @Override
-- 
GitLab