Reimplement network handler

This commit is contained in:
ChronosX88 2023-11-15 00:15:18 +03:00
parent 1eb00914a8
commit 87da965616
21 changed files with 237 additions and 105 deletions

View File

@ -19,7 +19,6 @@ dependencies {
implementation 'net.sourceforge.streamsupport:android-retrofuture:1.7.0' implementation 'net.sourceforge.streamsupport:android-retrofuture:1.7.0'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.3' implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.3'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.15.3' implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.15.3'
implementation 'javax.cache:cache-api:1.1.1'
implementation 'com.github.ben-manes.caffeine:jcache:3.1.5' implementation 'com.github.ben-manes.caffeine:jcache:3.1.5'
compileOnly 'org.projectlombok:lombok:1.18.30' compileOnly 'org.projectlombok:lombok:1.18.30'
annotationProcessor 'org.projectlombok:lombok:1.18.30' annotationProcessor 'org.projectlombok:lombok:1.18.30'

View File

@ -1,12 +1,9 @@
package io.github.chronosx88.JGUN; package io.github.chronosx88.JGUN;
import javax.cache.Cache; import com.github.benmanes.caffeine.cache.Cache;
import javax.cache.CacheManager; import com.github.benmanes.caffeine.cache.Caffeine;
import javax.cache.Caching;
import javax.cache.configuration.MutableConfiguration; import java.util.Objects;
import javax.cache.expiry.CreatedExpiryPolicy;
import javax.cache.expiry.Duration;
import javax.cache.spi.CachingProvider;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -14,11 +11,9 @@ public class Dup {
private final Cache<String, Long> cache; private final Cache<String, Long> cache;
public Dup(long age) { public Dup(long age) {
CachingProvider cachingProvider = Caching.getCachingProvider(); this.cache = Caffeine.newBuilder()
CacheManager cacheManager = cachingProvider.getCacheManager(); .expireAfterWrite(age, TimeUnit.SECONDS)
MutableConfiguration<String, Long> config = new MutableConfiguration<>(); .build();
config.setExpiryPolicyFactory(CreatedExpiryPolicy.factoryOf(new Duration(TimeUnit.SECONDS, age)));
this.cache = cacheManager.createCache("dup", config);
} }
private void track(String id) { private void track(String id) {
@ -26,7 +21,11 @@ public class Dup {
} }
public boolean isDuplicated(String id) { public boolean isDuplicated(String id) {
if(cache.containsKey(id)) { Long timestamp = null;
try {
timestamp = cache.getIfPresent(id);
} catch (NullPointerException ignored) {}
if(Objects.nonNull(timestamp)) {
return true; return true;
} else { } else {
track(id); track(id);

View File

@ -1,5 +1,6 @@
package io.github.chronosx88.JGUN; package io.github.chronosx88.JGUN;
import io.github.chronosx88.JGUN.models.MemoryGraph;
import io.github.chronosx88.JGUN.nodes.GunClient; import io.github.chronosx88.JGUN.nodes.GunClient;
import io.github.chronosx88.JGUN.storage.Storage; import io.github.chronosx88.JGUN.storage.Storage;
@ -10,10 +11,10 @@ import java.util.concurrent.ConcurrentHashMap;
public class Gun { public class Gun {
private GunClient gunClient; private GunClient gunClient;
private final Map<String, NodeChangeListener> changeListeners = new ConcurrentHashMap<>(); private final Storage storage;
private final Map<String, NodeChangeListener.Map> mapChangeListeners = new ConcurrentHashMap<>();
public Gun(InetAddress address, int port, Storage storage) { public Gun(InetAddress address, int port, Storage storage) {
this.storage = storage;
try { try {
this.gunClient = new GunClient(address, port, storage); this.gunClient = new GunClient(address, port, storage);
this.gunClient.connectBlocking(); this.gunClient.connectBlocking();
@ -29,10 +30,18 @@ public class Gun {
} }
protected void addChangeListener(String nodeID, NodeChangeListener listener) { protected void addChangeListener(String nodeID, NodeChangeListener listener) {
changeListeners.put(nodeID, listener); storage.addChangeListener(nodeID, listener);
} }
protected void addMapChangeListener(String nodeID, NodeChangeListener.Map listener) { protected void addMapChangeListener(String nodeID, NodeChangeListener.Map listener) {
mapChangeListeners.put(nodeID, listener); storage.addMapChangeListener(nodeID, listener);
}
protected void sendPutRequest(MemoryGraph data) {
// TODO
}
protected void sendGetRequest(String key, String field) {
// TODO
} }
} }

View File

@ -1,79 +1,146 @@
package io.github.chronosx88.JGUN; package io.github.chronosx88.JGUN;
import io.github.chronosx88.JGUN.futures.BaseCompletableFuture; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.chronosx88.JGUN.futures.FutureGet;
import io.github.chronosx88.JGUN.futures.FuturePut;
import io.github.chronosx88.JGUN.futures.GetResult;
import io.github.chronosx88.JGUN.futures.Result;
import io.github.chronosx88.JGUN.models.BaseMessage; import io.github.chronosx88.JGUN.models.BaseMessage;
import io.github.chronosx88.JGUN.models.MemoryGraph; import io.github.chronosx88.JGUN.models.MemoryGraph;
import io.github.chronosx88.JGUN.models.Node;
import io.github.chronosx88.JGUN.models.NodeMetadata;
import io.github.chronosx88.JGUN.models.acks.BaseAck; import io.github.chronosx88.JGUN.models.acks.BaseAck;
import io.github.chronosx88.JGUN.models.acks.GetAck; import io.github.chronosx88.JGUN.models.acks.GetAck;
import io.github.chronosx88.JGUN.models.acks.PutAck;
import io.github.chronosx88.JGUN.models.requests.GetRequest; import io.github.chronosx88.JGUN.models.requests.GetRequest;
import io.github.chronosx88.JGUN.models.requests.PutRequest; import io.github.chronosx88.JGUN.models.requests.PutRequest;
import io.github.chronosx88.JGUN.nodes.Peer; import io.github.chronosx88.JGUN.nodes.Peer;
import io.github.chronosx88.JGUN.storage.Storage; import io.github.chronosx88.JGUN.storage.Storage;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
public class NetworkHandler { public class NetworkHandler {
private final Map<String, BaseCompletableFuture<?>> pendingFutures = new ConcurrentHashMap<>(); private final Map<String, FutureGet> pendingGetRequests = new ConcurrentHashMap<>();
private final Map<String, FuturePut> pendingPutRequests = new ConcurrentHashMap<>();
private final Peer peer; private final Peer peer;
private final Storage graphStorage; private final Storage storage;
private final Dup dup; private final Dup dup;
private final Executor executorService = Executors.newCachedThreadPool(); private final Executor executorService = Executors.newCachedThreadPool();
private final ObjectMapper objectMapper = new ObjectMapper();
public NetworkHandler(Storage graphStorage, Peer peer, Dup dup) { public NetworkHandler(Storage storage, Peer peer, Dup dup) {
this.graphStorage = graphStorage; this.storage = storage;
this.peer = peer; this.peer = peer;
this.dup = dup; this.dup = dup;
} }
public void addPendingFuture(BaseCompletableFuture<?> future) { public void addPendingGetRequest(FutureGet future) {
pendingFutures.put(future.getFutureID(), future); this.pendingGetRequests.put(future.getFutureID(), future);
} }
public void handleIncomingMessage(BaseMessage message) { public void addPendingPutRequest(FuturePut future) {
if (message instanceof GetRequest) { this.pendingPutRequests.put(future.getFutureID(), future);
handleGet((GetRequest) message);
} else if (message instanceof PutRequest) {
handlePut((PutRequest) message);
} else if (message instanceof BaseAck) {
handleAck((BaseAck) message);
} }
peer.emit(message.toString());
public void handleIncomingMessage(String message) {
BaseMessage parsedMessage;
try {
parsedMessage = objectMapper.readValue(message, BaseMessage.class);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
if (dup.isDuplicated(parsedMessage.getId())) {
// TODO log
return;
}
final BaseMessage msg = parsedMessage;
executorService.execute(() -> {
BaseMessage response = null;
if (msg instanceof GetRequest) {
response = handleGet((GetRequest) msg);
} else if (msg instanceof PutRequest) {
response = handlePut((PutRequest) msg);
} else if (msg instanceof BaseAck) {
response = handleAck((BaseAck) msg);
}
if (Objects.nonNull(response)) {
String respString;
try {
respString = objectMapper.writeValueAsString(response);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
peer.emit(respString);
}
});
peer.emit(message);
} }
private GetAck handleGet(GetRequest request) { private GetAck handleGet(GetRequest request) {
// TODO Node node = storage.getNode(request.getParams().getNodeID());
throw new UnsupportedOperationException("TODO"); if (Objects.isNull(node)) return null;
String fieldName = request.getParams().getField();
if (Objects.nonNull(fieldName)) {
Object fieldValue = node.getValues().get(fieldName);
if (Objects.nonNull(fieldValue)) {
node = Node.builder()
.values(Map.of(fieldName, fieldValue))
.metadata(NodeMetadata.builder()
.nodeID(node.getMetadata().getNodeID())
.states(Map.of(fieldName, node.getMetadata().getStates().get(fieldName)))
.build())
.build();
}
}
return GetAck.builder()
.id(Dup.random())
.replyTo(request.getId())
.data(MemoryGraph.builder()
.nodes(Map.of(node.getMetadata().getNodeID(), node))
.build())
.ok(true)
.build();
} }
private PutAck handlePut(PutRequest request) { private BaseAck handlePut(PutRequest request) {
// TODO storage.mergeUpdate(request.getGraph());
throw new UnsupportedOperationException("TODO"); return BaseAck.builder()
.id(Dup.random())
.replyTo(request.getId())
.ok(true)
.build();
} }
private void handleAck(BaseAck ack) { private BaseAck handleAck(BaseAck ack) {
if (ack instanceof GetAck) { if (ack instanceof GetAck) {
// TODO FutureGet future = pendingGetRequests.get(ack.getReplyTo());
} else if (ack instanceof PutAck) { if (Objects.nonNull(future)) {
// TODO GetAck getAck = (GetAck) ack;
future.complete(GetResult.builder()
.ok(getAck.isOk())
.data(getAck.getData())
.build());
} }
return handlePut(PutRequest
throw new UnsupportedOperationException("TODO"); .builder()
.graph(((GetAck) ack).getData())
.build());
} else {
FuturePut future = pendingPutRequests.get(ack.getReplyTo());
if (Objects.nonNull(future)) {
future.complete(Result.builder()
.ok(ack.isOk())
.build());
}
System.out.println("Got ack! { #: '" + ack.getId() + "', @: '" + ack.getReplyTo() + "' }");
return null;
} }
public void sendPutRequest(String messageID, MemoryGraph data) {
executorService.execute(() -> {
// TODO
});
}
public void sendGetRequest(String messageID, String key, String field) {
executorService.execute(() -> {
// TODO
});
} }
} }

View File

@ -2,6 +2,7 @@ package io.github.chronosx88.JGUN;
import io.github.chronosx88.JGUN.futures.FutureGet; import io.github.chronosx88.JGUN.futures.FutureGet;
import io.github.chronosx88.JGUN.futures.FuturePut; import io.github.chronosx88.JGUN.futures.FuturePut;
import io.github.chronosx88.JGUN.nodes.GunClient;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
@ -9,10 +10,10 @@ import java.util.HashMap;
public class PathReference { public class PathReference {
private final ArrayList<String> path = new ArrayList<>(); private final ArrayList<String> path = new ArrayList<>();
private Gun database; private Gun gun;
public PathReference(Gun db) { public PathReference(Gun gun) {
this.database = db; this.gun = gun;
} }
public PathReference get(String key) { public PathReference get(String key) {
@ -31,10 +32,10 @@ public class PathReference {
} }
public void on(NodeChangeListener changeListener) { public void on(NodeChangeListener changeListener) {
database.addChangeListener(String.join("/", path), changeListener); gun.addChangeListener(String.join("/", path), changeListener);
} }
public void map(NodeChangeListener.Map forEachListener) { public void map(NodeChangeListener.Map forEachListener) {
database.addMapChangeListener(String.join("/", path), forEachListener); gun.addMapChangeListener(String.join("/", path), forEachListener);
} }
} }

View File

@ -2,7 +2,7 @@ package io.github.chronosx88.JGUN.futures;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java9.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import lombok.Getter; import lombok.Getter;

View File

@ -1,8 +1,6 @@
package io.github.chronosx88.JGUN.futures; package io.github.chronosx88.JGUN.futures;
import io.github.chronosx88.JGUN.models.MemoryGraph; public class FutureGet extends BaseCompletableFuture<GetResult> {
public class FutureGet extends BaseCompletableFuture<MemoryGraph> {
public FutureGet(String id) { public FutureGet(String id) {
super(id); super(id);
} }

View File

@ -3,7 +3,7 @@ package io.github.chronosx88.JGUN.futures;
/** /**
* Return success of PUT operation * Return success of PUT operation
*/ */
public class FuturePut extends BaseCompletableFuture<Boolean> { public class FuturePut extends BaseCompletableFuture<Result> {
public FuturePut(String id) { public FuturePut(String id) {
super(id); super(id);
} }

View File

@ -0,0 +1,11 @@
package io.github.chronosx88.JGUN.futures;
import io.github.chronosx88.JGUN.models.MemoryGraph;
import lombok.Getter;
import lombok.experimental.SuperBuilder;
@Getter
@SuperBuilder
public class GetResult extends Result {
private final MemoryGraph data;
}

View File

@ -0,0 +1,12 @@
package io.github.chronosx88.JGUN.futures;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.experimental.SuperBuilder;
@Getter
@SuperBuilder
public class Result {
private boolean ok;
}

View File

@ -1,9 +1,23 @@
package io.github.chronosx88.JGUN.models; package io.github.chronosx88.JGUN.models;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import io.github.chronosx88.JGUN.models.acks.GetAck;
import io.github.chronosx88.JGUN.models.requests.GetRequest;
import io.github.chronosx88.JGUN.models.requests.PutRequest;
import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.experimental.SuperBuilder;
@Data @Data
@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
@JsonSubTypes({
@JsonSubTypes.Type(GetRequest.class),
@JsonSubTypes.Type(PutRequest.class),
@JsonSubTypes.Type(GetAck.class)
})
@SuperBuilder
public abstract class BaseMessage { public abstract class BaseMessage {
@JsonProperty("#") @JsonProperty("#")
private String id; private String id;

View File

@ -11,8 +11,11 @@ import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
@Data @Data
@Builder
@Jacksonized
public class MemoryGraph { public class MemoryGraph {
@JsonIgnore @JsonIgnore
@Builder.Default
public final Map<String, Node> nodes = new LinkedHashMap<>(); public final Map<String, Node> nodes = new LinkedHashMap<>();
@JsonAnyGetter @JsonAnyGetter

View File

@ -5,12 +5,15 @@ import io.github.chronosx88.JGUN.models.BaseMessage;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized; import lombok.extern.jackson.Jacksonized;
@Data @Data
@SuperBuilder
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public abstract class BaseAck extends BaseMessage { @Jacksonized
public class BaseAck extends BaseMessage {
@JsonProperty("@") @JsonProperty("@")
private String replyTo; private String replyTo;
private String ok; private boolean ok;
} }

View File

@ -5,11 +5,12 @@ import io.github.chronosx88.JGUN.models.MemoryGraph;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized; import lombok.extern.jackson.Jacksonized;
@Data @Data
@SuperBuilder
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Builder
@Jacksonized @Jacksonized
public class GetAck extends BaseAck { public class GetAck extends BaseAck {
@JsonProperty("put") @JsonProperty("put")

View File

@ -1,12 +0,0 @@
package io.github.chronosx88.JGUN.models.acks;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.extern.jackson.Jacksonized;
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@Jacksonized
public class PutAck extends BaseAck {}

View File

@ -2,11 +2,15 @@ package io.github.chronosx88.JGUN.models.requests;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import io.github.chronosx88.JGUN.models.BaseMessage; import io.github.chronosx88.JGUN.models.BaseMessage;
import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.extern.jackson.Jacksonized;
@Data @Data
@Builder
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Jacksonized
public class GetRequest extends BaseMessage { public class GetRequest extends BaseMessage {
@JsonProperty("get") @JsonProperty("get")
private GetRequestParams params; private GetRequestParams params;

View File

@ -1,7 +1,13 @@
package io.github.chronosx88.JGUN.models.requests; package io.github.chronosx88.JGUN.models.requests;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.Data;
import lombok.extern.jackson.Jacksonized;
@Data
@Builder
@Jacksonized
public class GetRequestParams { public class GetRequestParams {
@JsonProperty("#") @JsonProperty("#")
private String nodeID; private String nodeID;

View File

@ -2,6 +2,7 @@ package io.github.chronosx88.JGUN.models.requests;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import io.github.chronosx88.JGUN.models.BaseMessage; import io.github.chronosx88.JGUN.models.BaseMessage;
import io.github.chronosx88.JGUN.models.MemoryGraph;
import io.github.chronosx88.JGUN.models.Node; import io.github.chronosx88.JGUN.models.Node;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
@ -14,5 +15,5 @@ import lombok.extern.jackson.Jacksonized;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class PutRequest extends BaseMessage { public class PutRequest extends BaseMessage {
@JsonProperty("put") @JsonProperty("put")
private Node[] params; private MemoryGraph graph;
} }

View File

@ -26,7 +26,7 @@ public class GunClient extends WebSocketClient implements Peer {
@Override @Override
public void onMessage(String message) { public void onMessage(String message) {
// TODO handler.handleIncomingMessage(message);
} }
@Override @Override

View File

@ -45,7 +45,7 @@ public class MemoryStorage extends Storage {
} }
@Override @Override
void updateNode(Node node) { protected void updateNode(Node node) {
Node currentNode = nodes.get(node.getMetadata().getNodeID()); Node currentNode = nodes.get(node.getMetadata().getNodeID());
currentNode.values.putAll(node.values); currentNode.values.putAll(node.values);
currentNode.getMetadata().getStates().putAll(node.getMetadata().getStates()); currentNode.getMetadata().getStates().putAll(node.getMetadata().getStates());
@ -72,7 +72,7 @@ public class MemoryStorage extends Storage {
} }
@Override @Override
void putDeferredNode(DeferredNode node) { protected void putDeferredNode(DeferredNode node) {
deferredNodes.put(node.getMetadata().getNodeID(), node); deferredNodes.put(node.getMetadata().getNodeID(), node);
} }
} }

View File

@ -7,36 +7,34 @@ import io.github.chronosx88.JGUN.models.MemoryGraph;
import io.github.chronosx88.JGUN.models.Node; import io.github.chronosx88.JGUN.models.Node;
import io.github.chronosx88.JGUN.models.NodeMetadata; import io.github.chronosx88.JGUN.models.NodeMetadata;
import java.util.Collection; import java.util.*;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
public abstract class Storage { public abstract class Storage {
abstract Node getNode(String id); public abstract Node getNode(String id);
abstract void updateNode(Node node); protected abstract void updateNode(Node node);
abstract void addNode(String id, Node node); public abstract void addNode(String id, Node node);
abstract boolean hasNode(String id); public abstract boolean hasNode(String id);
abstract Set<Map.Entry<String, Node>> entries(); public abstract Set<Map.Entry<String, Node>> entries();
abstract Collection<Node> nodes(); public abstract Collection<Node> nodes();
abstract boolean isEmpty(); public abstract boolean isEmpty();
abstract void putDeferredNode(DeferredNode node); protected abstract void putDeferredNode(DeferredNode node);
private final Map<String, List<NodeChangeListener>> changeListeners = new HashMap<>();
private final Map<String, List<NodeChangeListener.Map>> mapChangeListeners = new HashMap<>();
/** /**
* Merge graph update (usually received from the network) * Merge graph update (usually received from the network)
* *
* @param update Graph update * @param update Graph update
* @param changeListeners User callbacks which fired when Node has changed (.on() API)
* @param mapChangeListeners User callbacks which fired when Node has changed (.map() API)
*/ */
public void mergeUpdate(MemoryGraph update, Map<String, NodeChangeListener> changeListeners, Map<String, NodeChangeListener.Map> mapChangeListeners) { public void mergeUpdate(MemoryGraph update) {
long machine = System.currentTimeMillis(); long machine = System.currentTimeMillis();
MemoryGraph diff = new MemoryGraph(); MemoryGraph diff = new MemoryGraph();
for (Map.Entry<String, Node> entry : update.getNodes().entrySet()) { for (Map.Entry<String, Node> entry : update.getNodes().entrySet()) {
@ -56,11 +54,11 @@ public abstract class Storage {
} }
if (changeListeners.containsKey(diffEntry.getKey())) { if (changeListeners.containsKey(diffEntry.getKey())) {
changeListeners.get(diffEntry.getKey()).onChange(diffEntry.getValue()); changeListeners.get(diffEntry.getKey()).forEach((e) -> e.onChange(diffEntry.getValue()));
} }
if (mapChangeListeners.containsKey(diffEntry.getKey())) { if (mapChangeListeners.containsKey(diffEntry.getKey())) {
for (Map.Entry<String, Object> nodeEntry : changedNode.getValues().entrySet()) { for (Map.Entry<String, Object> nodeEntry : changedNode.getValues().entrySet()) {
mapChangeListeners.get(nodeEntry.getKey()).onChange(nodeEntry.getKey(), nodeEntry.getValue()); mapChangeListeners.get(nodeEntry.getKey()).forEach((e) -> e.onChange(nodeEntry.getKey(), nodeEntry.getValue()));
} }
} }
} }
@ -113,4 +111,22 @@ public abstract class Storage {
return changedNode; return changedNode;
} }
public void addChangeListener(String nodeID, NodeChangeListener listener) {
this.changeListeners.putIfAbsent(nodeID, new ArrayList<>());
this.changeListeners.get(nodeID).add(listener);
}
public void clearChangeListeners(String nodeID) {
this.changeListeners.remove(nodeID);
}
public void addMapChangeListener(String nodeID, NodeChangeListener.Map listener) {
this.mapChangeListeners.putIfAbsent(nodeID, new ArrayList<>());
this.mapChangeListeners.get(nodeID).add(listener);
}
public void clearMapChangeListeners(String nodeID) {
this.mapChangeListeners.remove(nodeID);
}
} }