diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/Plugin.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/Plugin.java index 2998ff19a5d01088b06eae754935d3b9ae543147..ae86d980b2a59fb3d8f5d3a46f73fdcb434057c3 100644 --- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/Plugin.java +++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/Plugin.java @@ -27,7 +27,7 @@ import org.duniter.elasticsearch.rest.RestModule; import org.duniter.elasticsearch.security.SecurityModule; import org.duniter.elasticsearch.service.ServiceModule; import org.duniter.elasticsearch.threadpool.ThreadPool; -import org.duniter.elasticsearch.websocket.WebsocketModule; +import org.duniter.elasticsearch.websocket.WebSocketModule; import org.elasticsearch.common.component.LifecycleComponent; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.inject.Module; @@ -66,7 +66,7 @@ public class Plugin extends org.elasticsearch.plugins.Plugin { } modules.add(new SecurityModule()); - modules.add(new WebsocketModule()); + modules.add(new WebSocketModule()); modules.add(new RestModule()); modules.add(new ServiceModule()); diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/PluginSettings.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/PluginSettings.java index 70e2f12bdb6402986ac52a9c60a81721801ee414..fe53f1c7ca20474ac70c0b5c9ecb02a67797af9e 100644 --- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/PluginSettings.java +++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/PluginSettings.java @@ -281,10 +281,14 @@ public class PluginSettings extends AbstractLifecycleComponent<PluginSettings> { return settings.getAsInt("duniter.ws.port", 9200); } + public boolean getWebSocketEnable() { + return settings.getAsBoolean("duniter.ws.enable", Boolean.TRUE); + } + /* protected methods */ protected void initI18n() throws IOException { - if (I18n.getDefaultLocale() != null) return; // already init + //if (I18n.getDefaultLocale() != null) return; // already init // --------------------------------------------------------------------// // init i18n diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/exception/DocumentNotFoundException.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/exception/DocumentNotFoundException.java new file mode 100644 index 0000000000000000000000000000000000000000..338f7012b9aa3f42bfbfa7524ece3785d9801cab --- /dev/null +++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/exception/DocumentNotFoundException.java @@ -0,0 +1,47 @@ +package org.duniter.elasticsearch.exception; + +/* + * #%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.elasticsearch.rest.RestStatus; + +/** + * Created by blavenie on 01/03/16. + */ +public class DocumentNotFoundException extends DuniterElasticsearchException { + public DocumentNotFoundException(Throwable cause) { + super(cause); + } + + public DocumentNotFoundException(String msg, Object... args) { + super(msg, args); + } + + public DocumentNotFoundException(String msg, Throwable cause, Object... args) { + super(msg, args, cause); + } + + @Override + public RestStatus status() { + return RestStatus.BAD_REQUEST; + } +} 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 f9486338ed64386fd830374ce604f373495af823..f00a564be16bb2032c1eb2d4a33f25d65faf1c6a 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 @@ -29,6 +29,7 @@ import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableSet; import com.google.gson.JsonSyntaxException; +import org.apache.commons.collections4.MapUtils; import org.duniter.core.beans.Bean; import org.duniter.core.client.model.elasticsearch.Record; import org.duniter.core.exception.TechnicalException; @@ -147,7 +148,7 @@ public abstract class AbstractService implements Bean { .execute().actionGet(); return response.getId(); } - protected void checkIssuerAndUpdateDocumentFromJson(String index, String type, String json, String id) { + protected void checkIssuerAndUpdateDocumentFromJson(String index, String type, String id, String json) { JsonNode actualObj = readAndVerifyIssuerSignature(json); String issuer = getIssuer(actualObj); @@ -159,6 +160,10 @@ public abstract class AbstractService implements Bean { logger.debug(String.format("Updating %s [%s] from issuer [%s]", type, id, issuer.substring(0, 8))); } + updateDocumentFromJson(index, type, id, json); + } + + protected void updateDocumentFromJson(String index, String type, String id, String json) { // Execute indexBlocksFromNode client.prepareUpdate(index, type, id) .setDoc(json) @@ -283,6 +288,20 @@ public abstract class AbstractService implements Bean { } } + /** + * Retrieve a field from a document id + * @param docId + * @return + */ + protected Object getFieldById(String index, String type, String docId, String fieldName) { + + Map<String, Object> result = getFieldsById(index, type, docId, fieldName); + if (MapUtils.isEmpty(result)) { + return null; + } + return result.get(fieldName); + } + 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/service/ServiceLocator.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/ServiceLocator.java index 98d46240d912a81aee7fcd89f905d6cf9d45032f..23609da14728cbbdb23236312bbf14b34f5c0bb2 100644 --- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/ServiceLocator.java +++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/ServiceLocator.java @@ -54,7 +54,7 @@ import java.io.IOException; public class ServiceLocator extends org.duniter.core.client.service.ServiceLocator { - private static final ESLogger logger = ESLoggerFactory.getLogger(ServiceLocator.class.getName()); + private static final ESLogger logger = ESLoggerFactory.getLogger("duniter.service"); private static BeanFactory beanFactory = null; diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/websocket/WebsocketModule.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/websocket/WebSocketModule.java similarity index 81% rename from duniter4j-es-core/src/main/java/org/duniter/elasticsearch/websocket/WebsocketModule.java rename to duniter4j-es-core/src/main/java/org/duniter/elasticsearch/websocket/WebSocketModule.java index e3472356762aa94f1385fd79c357c252116addb2..717933a1ee9b1c47e5a7ec06b075a61e5a0b0c1f 100644 --- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/websocket/WebsocketModule.java +++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/websocket/WebSocketModule.java @@ -38,16 +38,13 @@ package org.duniter.elasticsearch.websocket; limitations under the License. */ +import org.duniter.elasticsearch.websocket.changes.WebSocketChangeEndPoint; import org.elasticsearch.common.inject.AbstractModule; -import org.elasticsearch.common.logging.ESLogger; -import org.elasticsearch.common.logging.Loggers; -public class WebsocketModule extends AbstractModule { - private final ESLogger log = Loggers.getLogger(WebsocketModule.class); - +public class WebSocketModule extends AbstractModule { @Override protected void configure() { - log.debug("Binding websocket Module"); - bind(WebsocketServer.class).asEagerSingleton(); + bind(WebSocketServer.class).asEagerSingleton(); + bind(WebSocketChangeEndPoint.Init.class).asEagerSingleton(); } } diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/websocket/WebsocketServer.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/websocket/WebSocketServer.java similarity index 65% rename from duniter4j-es-core/src/main/java/org/duniter/elasticsearch/websocket/WebsocketServer.java rename to duniter4j-es-core/src/main/java/org/duniter/elasticsearch/websocket/WebSocketServer.java index f3a44ae8c07495699c3fe34ac374b307e3cf4bfb..598fc81f9dc16819a0493b2e19043969ce79c334 100644 --- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/websocket/WebsocketServer.java +++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/websocket/WebSocketServer.java @@ -38,7 +38,10 @@ package org.duniter.elasticsearch.websocket; limitations under the License. */ +import org.duniter.core.exception.TechnicalException; import org.duniter.elasticsearch.PluginSettings; +import org.duniter.elasticsearch.threadpool.ThreadPool; +import org.duniter.elasticsearch.websocket.changes.WebSocketChangeEndPoint; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.logging.ESLogger; import org.elasticsearch.common.logging.Loggers; @@ -47,20 +50,45 @@ import org.glassfish.tyrus.server.Server; import javax.websocket.DeploymentException; import java.security.AccessController; import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.List; -public class WebsocketServer { +public class WebSocketServer { - private final ESLogger log = Loggers.getLogger(WebsocketServer.class); + private final ESLogger log = Loggers.getLogger("duniter.ws"); + private List<Class<?>> endPoints = new ArrayList<>(); @Inject - public WebsocketServer(final PluginSettings pluginSettings) { - final String host = pluginSettings.getWebSocketHost(); - final int port = pluginSettings.getWebSocketPort(); + public WebSocketServer(final PluginSettings pluginSettings, ThreadPool threadPool) { + // If WS enable + if (pluginSettings.getWebSocketEnable()) { + // When node started + threadPool.scheduleOnStarted(() -> { + // start WS server + startServer(pluginSettings.getWebSocketHost(), + pluginSettings.getWebSocketPort(), + getEndPoints()); + }); + } + } + + + public void addEndPoint(Class<?> endPoint) { + endPoints.add(endPoint); + } + + /* -- private medthod -- */ + + private Class[] getEndPoints() { + return endPoints.toArray(new Class<?>[endPoints.size()]); + } + + private void startServer(String host, int port, Class<?>[] endPoints) { - final Server server = new Server(host, port, "/ws", null, WebsocketChangeEndPoint.class) ; + final Server server = new Server(host, port, "/ws", null, endPoints) ; try { - log.info("Starting websocket server"); + log.info("Starting Websocket server..."); AccessController.doPrivileged(new PrivilegedAction() { @Override public Object run() { @@ -70,16 +98,16 @@ public class WebsocketServer { // This is a workaround for that Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); server.start(); + log.info("Websocket server started"); return null; } catch (DeploymentException e) { throw new RuntimeException("Failed to start server", e); } } }); - log.info("Websocket server started"); } catch (Exception e) { - log.error("Failed to start Websocket server",e); - throw new RuntimeException(e); + log.error("Failed to start Websocket server", e); + throw new TechnicalException(e); } } diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/websocket/WebsocketChangeEndPoint.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/websocket/changes/WebSocketChangeEndPoint.java similarity index 79% rename from duniter4j-es-core/src/main/java/org/duniter/elasticsearch/websocket/WebsocketChangeEndPoint.java rename to duniter4j-es-core/src/main/java/org/duniter/elasticsearch/websocket/changes/WebSocketChangeEndPoint.java index 23e983af34e1c6a1d2e1088d11a2c4bf1fc9bdb7..b99e117e054800fb6cbfda1910f1ec34fb56e981 100644 --- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/websocket/WebsocketChangeEndPoint.java +++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/websocket/changes/WebSocketChangeEndPoint.java @@ -1,4 +1,4 @@ -package org.duniter.elasticsearch.websocket; +package org.duniter.elasticsearch.websocket.changes; /* * #%L @@ -40,6 +40,8 @@ package org.duniter.elasticsearch.websocket; import org.duniter.elasticsearch.service.changes.ChangeListener; import org.duniter.elasticsearch.service.changes.ChangeService; +import org.duniter.elasticsearch.websocket.WebSocketServer; +import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.logging.ESLogger; import org.elasticsearch.common.logging.Loggers; @@ -47,14 +49,22 @@ import javax.websocket.*; import javax.websocket.server.ServerEndpoint; @ServerEndpoint(value = "/_changes") -public class WebsocketChangeEndPoint implements ChangeListener{ +public class WebSocketChangeEndPoint implements ChangeListener{ - private final ESLogger log = Loggers.getLogger(WebsocketChangeEndPoint.class); + public static class Init { + + @Inject + public Init(WebSocketServer webSocketServer) { + webSocketServer.addEndPoint(WebSocketChangeEndPoint.class); + } + } + + private final ESLogger log = Loggers.getLogger("duniter.ws.changes"); private Session session; @OnOpen public void onOpen(Session session) { - log.info("Connected ... " + session.getId()); + log.debug("Connected ... " + session.getId()); this.session = session; ChangeService.registerListener(this); } @@ -71,12 +81,12 @@ public class WebsocketChangeEndPoint implements ChangeListener{ @OnMessage public void onMessage(String message) { - log.info("Received message: "+message); + log.debug("Received message: "+message); } @OnClose public void onClose(CloseReason reason) { - log.info("Closing websocket: "+reason); + log.debug("Closing websocket: "+reason); ChangeService.unregisterListener(this); this.session = null; } 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 030c42e37f70bbf5514b86f58782103a6ae4b7c3..e148805a53f11ad599cb38a4a153396744f0fb4f 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 @@ -49,7 +49,7 @@ public class PluginInit extends AbstractLifecycleComponent<PluginInit> { private final static ESLogger logger = Loggers.getLogger("gchange"); @Inject - public PluginInit(Client client, Settings settings, PluginSettings pluginSettings, ThreadPool threadPool, final Injector injector) { + public PluginInit(Settings settings, PluginSettings pluginSettings, ThreadPool threadPool, final Injector injector) { super(settings); this.pluginSettings = pluginSettings; this.threadPool = threadPool; 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 0ff97ce969d666e7ac3756df7549f4c959c7e930..95b97aafa02a919fb2fa6b23b83717b70cc5b7bc 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 @@ -25,35 +25,28 @@ package org.duniter.elasticsearch.gchange.service; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.base.Joiner; -import com.google.gson.JsonSyntaxException; -import org.duniter.core.client.model.elasticsearch.Record; +import org.apache.commons.collections4.MapUtils; import org.duniter.core.client.model.elasticsearch.RecordComment; import org.duniter.core.client.service.bma.WotRemoteService; 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.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.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder; import org.elasticsearch.action.index.IndexRequestBuilder; import org.elasticsearch.action.index.IndexResponse; -import org.elasticsearch.action.search.SearchPhaseExecutionException; -import org.elasticsearch.action.search.SearchRequestBuilder; -import org.elasticsearch.action.search.SearchResponse; -import org.elasticsearch.action.search.SearchType; import org.elasticsearch.client.Client; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; 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.IOException; @@ -73,15 +66,18 @@ public class MarketService extends AbstractService { private WotRemoteService wotRemoteService; private UserEventService userEventService; + private UserService userService; @Inject public MarketService(Client client, PluginSettings settings, CryptoService cryptoService, WotRemoteService wotRemoteService, + UserService userService, UserEventService userEventService) { super("gchange." + INDEX, client, settings, cryptoService); this.wotRemoteService = wotRemoteService; this.userEventService = userEventService; + this.userService = userService; } /** @@ -170,38 +166,33 @@ public class MarketService extends AbstractService { } public String indexCommentFromJson(String json) { - JsonNode actualObj = readAndVerifyIssuerSignature(json); - String issuer = getMandatoryField(actualObj, RecordComment.PROPERTY_ISSUER).asText(); + JsonNode commentObj = readAndVerifyIssuerSignature(json); + String issuer = getMandatoryField(commentObj, RecordComment.PROPERTY_ISSUER).asText(); if (logger.isDebugEnabled()) { logger.debug(String.format("Indexing a %s from issuer [%s]", RECORD_COMMENT_TYPE, issuer.substring(0, 8))); } String commentId = indexDocumentFromJson(INDEX, RECORD_COMMENT_TYPE, json); - // Notification - { - // Notify issuer of record (is not same as comment writer) - String recordId = getMandatoryField(actualObj, RecordComment.PROPERTY_RECORD).asText(); - Map<String, Object> recordFields = getRecordFieldsById(recordId, MarketRecord.PROPERTY_TITLE, MarketRecord.PROPERTY_ISSUER); - String recordIssuer = recordFields.get(MarketRecord.PROPERTY_ISSUER).toString(); - 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("duniter.market.event.newComment"), - issuer, recordTitle - ) - ); - } - } + // Notify record issuer + notifyRecordIssuerForComment(commentObj, true); return commentId; } public void updateCommentFromJson(String json, String id) { - checkIssuerAndUpdateDocumentFromJson(INDEX, RECORD_COMMENT_TYPE, json, id); + JsonNode commentObj = readAndVerifyIssuerSignature(json); + + if (logger.isDebugEnabled()) { + String issuer = getMandatoryField(commentObj, RecordComment.PROPERTY_ISSUER).asText(); + logger.debug(String.format("Indexing a %s from issuer [%s]", RECORD_COMMENT_TYPE, issuer.substring(0, 8))); + } + + updateDocumentFromJson(INDEX, RECORD_COMMENT_TYPE, id, json); + + // Notify record issuer + notifyRecordIssuerForComment(commentObj, false); + } public MarketService fillRecordCategories() { @@ -423,16 +414,34 @@ public class MarketService extends AbstractService { } } - /** - * Retrieve record field's values - * @param recordId - * @param fieldNames - * @return - */ - protected Map<String, Object> getRecordFieldsById(String recordId, String... fieldNames) { + // Notification + private void notifyRecordIssuerForComment(JsonNode actualObj, boolean isNewComment) { + String issuer = getMandatoryField(actualObj, RecordComment.PROPERTY_ISSUER).asText(); - return getFieldsById(INDEX, RECORD_TYPE, recordId, fieldNames); + // Notify issuer of record (is not same as comment writer) + String recordId = getMandatoryField(actualObj, RecordComment.PROPERTY_RECORD).asText(); + Map<String, Object> recordFields = getFieldsById(INDEX, RECORD_TYPE, recordId, + MarketRecord.PROPERTY_TITLE, MarketRecord.PROPERTY_ISSUER); + if (MapUtils.isEmpty(recordFields)) { // record not found + throw new DocumentNotFoundException(I18n.t("duniter.market.error.comment.recordNotFound", recordId)); + } + String recordIssuer = recordFields.get(MarketRecord.PROPERTY_ISSUER).toString(); + + // Get user title + String issuerTitle = userService.getProfileTitle(issuer); + + 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"), + issuerTitle != null ? issuerTitle : issuer.substring(0, 8), + recordTitle + ) + ); + } } - } 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 3542cd47c0b9b2dc7f7357098e6359ed7ae3f122..7505b8f40ee0390b4c7f40702af740ce133305d2 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,12 +25,20 @@ 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.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder; import org.elasticsearch.action.index.IndexRequestBuilder; import org.elasticsearch.action.index.IndexResponse; @@ -38,8 +46,10 @@ 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. @@ -54,15 +64,18 @@ public class RegistryService extends AbstractService { private final Gson gson; private BlockchainRemoteService blockchainRemoteService; + private UserEventService userEventService; @Inject public RegistryService(Client client, PluginSettings settings, CryptoService cryptoService, - BlockchainRemoteService blockchainRemoteService) { + BlockchainRemoteService blockchainRemoteService, + UserEventService userEventService) { super("gchange." + INDEX, client, settings, cryptoService); this.gson = GsonUtils.newBuilder().create(); this.blockchainRemoteService = blockchainRemoteService; + this.userEventService = userEventService; } /** @@ -136,6 +149,7 @@ public class RegistryService extends AbstractService { public String indexCommentFromJson(String json) { return checkIssuerAndIndexDocumentFromJson(INDEX, RECORD_COMMENT_TYPE, json); + } public void updateCommentFromJson(String json, String id) { diff --git a/duniter4j-es-user/pom.xml b/duniter4j-es-user/pom.xml index d4f1f5651b7f30c470046df1cf94dcf91e589ec3..12a9e711bce4a8550a9a833cbd81a32a93801da1 100644 --- a/duniter4j-es-user/pom.xml +++ b/duniter4j-es-user/pom.xml @@ -90,7 +90,7 @@ <configuration> <attach>true</attach> <appendAssemblyId>false</appendAssemblyId> - <finalName>${bundlePrefix}</finalName> + <finalName>${project.artifactId}-${project.version}</finalName> <descriptors> <descriptor> ${basedir}/src/main/assembly/plugin.xml diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/Plugin.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/Plugin.java index 0736535c3b7bd4d2e31d1e30e39e6d5604c841b8..66e193964f8cbe75b5cd6890aa78b35a3db4e395 100644 --- a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/Plugin.java +++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/Plugin.java @@ -25,6 +25,7 @@ package org.duniter.elasticsearch.user; import com.google.common.collect.Lists; import org.duniter.elasticsearch.user.rest.RestModule; import org.duniter.elasticsearch.user.service.ServiceModule; +import org.duniter.elasticsearch.user.websocket.WebSocketModule; import org.elasticsearch.common.component.LifecycleComponent; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.inject.Module; @@ -64,6 +65,8 @@ public class Plugin extends org.elasticsearch.plugins.Plugin { modules.add(new RestModule()); modules.add(new ServiceModule()); + modules.add(new WebSocketModule()); + return modules; } 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 c0e597f17c485745f559be039da07857a4daf3cb..aeb251d9bf5927d7eb7266e3614ccea0f0530190 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 @@ -31,6 +31,8 @@ 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.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.common.component.AbstractLifecycleComponent; import org.elasticsearch.common.inject.Inject; @@ -110,9 +112,6 @@ public class PluginInit extends AbstractLifecycleComponent<org.duniter.elasticse injector.getInstance(UserService.class) .deleteIndex() .createIndexIfNotExists(); - injector.getInstance(UserEventService.class) - .deleteIndex() - .createIndexIfNotExists(); if (logger.isInfoEnabled()) { @@ -126,7 +125,6 @@ public class PluginInit extends AbstractLifecycleComponent<org.duniter.elasticse injector.getInstance(HistoryService.class).createIndexIfNotExists(); injector.getInstance(UserService.class).createIndexIfNotExists(); injector.getInstance(MessageService.class).createIndexIfNotExists(); - injector.getInstance(UserEventService.class).createIndexIfNotExists(); if (logger.isInfoEnabled()) { logger.info("Checking Duniter indices... [OK]"); 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 9977f5a3b10d562522b9cd1b35780c614943479f..cad585fd3ae3cabba668a2881d8d96758d42849b 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 @@ -25,12 +25,14 @@ package org.duniter.elasticsearch.user.service; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; +import org.duniter.core.client.model.elasticsearch.UserProfile; import org.duniter.core.exception.TechnicalException; import org.duniter.core.service.CryptoService; 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; @@ -91,6 +93,7 @@ public class UserService extends AbstractService { createIndexRequestBuilder.setSettings(indexSettings); createIndexRequestBuilder.addMapping(PROFILE_TYPE, createProfileType()); createIndexRequestBuilder.addMapping(SETTINGS_TYPE, createSettingsType()); + createIndexRequestBuilder.addMapping(UserEventService.EVENT_TYPE, UserEventService.createEventType()); createIndexRequestBuilder.execute().actionGet(); return this; @@ -194,6 +197,12 @@ public class UserService extends AbstractService { } + public String getProfileTitle(String issuer) { + + Object title = getFieldById(INDEX, PROFILE_TYPE, issuer, UserProfile.PROPERTY_TITLE); + if (title == null) return null; + return title.toString(); + } /* -- Internal methods -- */ 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 index f54954a64385e4dcc1247e43ffe89d0d96d84eb5..e58ccbd2231dfebcd48c36777508aa03719e713d 100644 --- 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 @@ -84,6 +84,10 @@ public class UserEvent { return time; } + public UserEventLink getLink() { + return link; + } + public enum EventType { INFO, WARN, 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 index 7d34b28bb45e7e0d8ca85a632a746fdf00eece72..9238e4a83ca0ea2fbc3d1b090ba2c5a7dd1e4fb8 100644 --- 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 @@ -2,5 +2,6 @@ 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/UserEventService.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/event/UserEventService.java index e3cfda044265163c853c26f6271e2ca7130913c8..6d412001e432585d27bb8b6642936efae1e98066 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/event/UserEventService.java @@ -57,7 +57,7 @@ import java.util.Map; /** * Created by Benoit on 30/03/2015. */ -public class UserEventService extends AbstractService implements ChangeListener { +public class UserEventService extends AbstractService { public static final String INDEX = "user"; public static final String EVENT_TYPE = "event"; @@ -89,7 +89,6 @@ public class UserEventService extends AbstractService implements ChangeListener if (!this.mailEnable && logger.isTraceEnabled()) { logger.trace("Mail disable"); } - //ChangeService.registerListener(this); } /** @@ -128,87 +127,16 @@ public class UserEventService extends AbstractService implements ChangeListener }, TimeValue.timeValueMillis(100)); } - @Override - public void onChanges(String json) { - // TODO get doc issuer - /* String issuer = nodePubkey; - - ChangeEvent event = ChangeUtils.fromJson(objectMapper, json); - - // Skip event itself (avoid recursive call) - if (event.getIndex().equals(INDEX) && event.getType().equals(EVENT_TYPE)) { - return; - } - - if (event.getOperation() == ChangeEvent.Operation.CREATE) { - notifyNewDocument(event.getIndex(), event.getType(), event.getId(), issuer); - }*/ - - } - - @Override - public String getId() { - return "UserEventService"; - } - - /** - * Delete blockchain index, and all data - */ - public UserEventService deleteIndex() { - deleteIndexIfExists(INDEX); - return this; - } - - public boolean existsIndex() { - return super.existsIndex(INDEX); - } - - /** - * Create index need for blockchain registry, if need - */ - public UserEventService createIndexIfNotExists() { - try { - if (!existsIndex(INDEX)) { - createIndex(); - } - } - catch(JsonProcessingException e) { - throw new TechnicalException(String.format("Error while creating index [%s]", INDEX)); - } - - return this; - } - - /** - * Create index need for category registry - * @throws JsonProcessingException - */ - public UserEventService createIndex() throws JsonProcessingException { - logger.info(String.format("Creating index [%s/%s]", INDEX, EVENT_TYPE)); - - CreateIndexRequestBuilder createIndexRequestBuilder = client.admin().indices().prepareCreate(INDEX); - Settings indexSettings = Settings.settingsBuilder() - .put("number_of_shards", 2) - .put("number_of_replicas", 1) - //.put("analyzer", createDefaultAnalyzer()) - .build(); - createIndexRequestBuilder.setSettings(indexSettings); - createIndexRequestBuilder.addMapping(EVENT_TYPE, createEventType()); - createIndexRequestBuilder.execute().actionGet(); - - return this; - } - public String indexEvent(String recipient, Locale locale, UserEvent event) { // Generate json String eventJson; if (StringUtils.isNotBlank(nodePubkey)) { - eventJson = toJson(nodePubkey, recipient, locale, event, null); + eventJson = UserEventUtils.toJson(nodePubkey, recipient, locale, event, null); String signature = cryptoService.sign(eventJson, nodeKeyPair.getSecKey()); - eventJson = toJson(nodePubkey, recipient, locale, event, signature); + eventJson = UserEventUtils.toJson(nodePubkey, recipient, locale, event, signature); } else { // Node has not keyring : TODO no issuer ? - eventJson = toJson(recipient, recipient, locale, event, null); + eventJson = UserEventUtils.toJson(recipient, recipient, locale, event, null); } if (logger.isDebugEnabled()) { @@ -246,7 +174,7 @@ public class UserEventService extends AbstractService implements ChangeListener /* -- Internal methods -- */ - public XContentBuilder createEventType() { + public static XContentBuilder createEventType() { try { XContentBuilder mapping = XContentFactory.jsonBuilder().startObject().startObject(EVENT_TYPE) .startObject("properties") @@ -350,29 +278,7 @@ public class UserEventService extends AbstractService implements ChangeListener } } - private 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()); - } - if (StringUtils.isNotBlank(signature)) { - eventObject.field("signature", signature); - } - eventObject.endObject(); - return eventObject.string(); - } - catch(IOException e) { - throw new TechnicalException(e); - } - } private KeyPair getNodeKeyPairOrNull(PluginSettings pluginSettings) { @@ -402,6 +308,7 @@ public class UserEventService extends AbstractService implements ChangeListener indexEvent(recipient, locale, event); // Send email to user + // TODO : group email by day ? if (StringUtils.isNotBlank(email)) { String subjectPrefix = pluginSettings.getMailSubjectPrefix(); sendEmail(email, @@ -410,7 +317,9 @@ public class UserEventService extends AbstractService implements ChangeListener } for (UserEventListener listener: LISTENERS.values()) { - listener.onEvent(event); + if (recipient.equals(listener.getPubkey())) { + listener.onEvent(event); + } } } } 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 new file mode 100644 index 0000000000000000000000000000000000000000..20f2ed4d8c0e7b4ae1c929209848e3efd841a6d3 --- /dev/null +++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/event/UserEventUtils.java @@ -0,0 +1,84 @@ +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/WebSocketModule.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/websocket/WebSocketModule.java new file mode 100644 index 0000000000000000000000000000000000000000..3aaf2d8ed86ccae793a1f76a785cea8d0c7500eb --- /dev/null +++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/websocket/WebSocketModule.java @@ -0,0 +1,37 @@ +package org.duniter.elasticsearch.user.websocket; + +/* + * #%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.elasticsearch.common.inject.AbstractModule; +import org.elasticsearch.common.inject.Module; + +public class WebSocketModule extends AbstractModule implements Module { + + @Override protected void configure() { + bind(WebsocketUserEventEndPoint.Init.class).asEagerSingleton(); + } + + + /* protected methods */ + +} \ No newline at end of file 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 7591f42997c4c387f07585d68784edb9348a145f..2c01b7ef93c35b42183a5a799df1a32353ee15b9 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 @@ -38,21 +38,49 @@ package org.duniter.elasticsearch.user.websocket; limitations under the License. */ -//@ServerEndpoint(value = "/event/user/{pubkey}") -public class WebsocketUserEventEndPoint /*implements UserEventListener*/ { +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.websocket.WebSocketServer; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.logging.ESLogger; +import org.elasticsearch.common.logging.Loggers; +import org.nuiton.i18n.I18n; + +import javax.websocket.*; +import javax.websocket.server.ServerEndpoint; +import java.io.IOException; +import java.util.Locale; +import java.util.regex.Pattern; + +@ServerEndpoint(value = "/event/user/{pubkey}/{locale}") +public class WebsocketUserEventEndPoint implements UserEventListener { + + public static class Init { + + @Inject + public Init(WebSocketServer webSocketServer) { + webSocketServer.addEndPoint(WebsocketUserEventEndPoint.class); + } + } - /* private static final String PATH_PARAM_PUBKEY = "pubkey"; + private static final String PATH_PARAM_PUBKEY = "pubkey"; + private static final String PATH_PARAM_LOCALE = "locale"; private final static Pattern PUBKEY_PATTERN = Pattern.compile(Constants.Regex.PUBKEY); private final ESLogger log = Loggers.getLogger("duniter.ws.user.event"); private Session session; private String pubkey; - + private Locale locale; @OnOpen public void onOpen(Session session) { this.session = session; this.pubkey = session.getPathParameters() != null ? session.getPathParameters().get(PATH_PARAM_PUBKEY) : null; + this.locale = new Locale(session.getPathParameters() != null ? session.getPathParameters().get(PATH_PARAM_LOCALE) : "fr"); if (StringUtils.isBlank(pubkey) || !PUBKEY_PATTERN.matcher(pubkey).matches()) { try { @@ -63,13 +91,13 @@ public class WebsocketUserEventEndPoint /*implements UserEventListener*/ { return; } - log.info("User [%s] connecting with id [%s]", session.getId()); + log.debug(I18n.t("duniter4j.ws.user.open", pubkey, session.getId(), locale.toString())); UserEventService.registerListener(this); } @Override public void onEvent(UserEvent event) { - session.getAsyncRemote().sendText("{\"type\":\""+event.getType().name()+"\",\"message\":\"" + event.getMessage() + "\"}"); + session.getAsyncRemote().sendText(UserEventUtils.toJson(locale, event)); } @Override @@ -77,14 +105,19 @@ public class WebsocketUserEventEndPoint /*implements UserEventListener*/ { return session == null ? null : session.getId(); } + @Override + public String getPubkey() { + return pubkey; + } + @OnMessage public void onMessage(String message) { - log.info("Received message: "+message); + log.debug("Received message: "+message); } @OnClose public void onClose(CloseReason reason) { - log.info("Closing websocket: "+reason); + log.debug("Closing websocket: "+reason); UserEventService.unregisterListener(this); this.session = null; } @@ -93,6 +126,5 @@ public class WebsocketUserEventEndPoint /*implements UserEventListener*/ { public void onError(Throwable t) { log.error("Error on websocket "+(session == null ? null : session.getId()), t); } -*/ } diff --git a/duniter4j-es-user/src/main/resources/i18n/duniter4j-es-user_en_GB.properties b/duniter4j-es-user/src/main/resources/i18n/duniter4j-es-user_en_GB.properties index 7c2d2a95ba12d23aa03f5f289a2672561c9ee450..d884f6badf7a9c692748e6d5d805ef10e31d99b9 100644 --- a/duniter4j-es-user/src/main/resources/i18n/duniter4j-es-user_en_GB.properties +++ b/duniter4j-es-user/src/main/resources/i18n/duniter4j-es-user_en_GB.properties @@ -3,3 +3,4 @@ duniter4j.event.NODE_STARTED=Node started on cluster Duniter4j ES [%s] duniter4j.event.subject.ERROR=[%s] Error message duniter4j.event.subject.INFO=[%s] Information message duniter4j.event.subject.WARN=[%s] Warning message +duniter4j.ws.user.open=User [%s] connecting with id [%s] with locale [%s] diff --git a/duniter4j-es-user/src/main/resources/i18n/duniter4j-es-user_fr_FR.properties b/duniter4j-es-user/src/main/resources/i18n/duniter4j-es-user_fr_FR.properties index 3ae4dd7f9b020c5836702fd8a2e4d7c0f0196572..e179adfd2247854f9bdda7bd936d2e3281d3218e 100644 --- a/duniter4j-es-user/src/main/resources/i18n/duniter4j-es-user_fr_FR.properties +++ b/duniter4j-es-user/src/main/resources/i18n/duniter4j-es-user_fr_FR.properties @@ -3,3 +3,4 @@ duniter4j.event.NODE_STARTED=Noeud démarré sur le cluster Duniter4j ES [%s] duniter4j.event.subject.ERROR=%s Message d'erreur duniter4j.event.subject.INFO=%s Message d'information duniter4j.event.subject.WARN=%s Message d'avertissement +duniter4j.ws.user.open=Utilisateur [%s] connecté id\=[%s] sur la locale [%s]