Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.vaadin.collaborationengine.TopicConnection Maven / Gradle / Ivy
/*
* Copyright 2020-2022 Vaadin Ltd.
*
* This program is available under Commercial Vaadin Runtime License 1.0
* (CVRLv1).
*
* For the full License, see http://vaadin.com/license/cvrl-1
*/
package com.vaadin.collaborationengine;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.vaadin.collaborationengine.Topic.ChangeDetails;
import com.vaadin.collaborationengine.Topic.ChangeResult;
import com.vaadin.flow.function.SerializableFunction;
import com.vaadin.flow.shared.Registration;
/**
* API for sending and subscribing to updates between clients collaborating on
* the same collaboration topic.
*
* @author Vaadin Ltd
* @since 1.0
*/
public class TopicConnection {
class CollaborationMapImplementation implements CollaborationMap {
private final String name;
private CollaborationMapImplementation(String name) {
this.name = name;
}
@Override
public Registration subscribe(MapSubscriber subscriber) {
ensureActiveConnection();
Objects.requireNonNull(subscriber, "Subscriber cannot be null");
synchronized (topic) {
Consumer mapChangeNotifier = mapChange -> {
MapChangeEvent event = new MapChangeEvent(this, mapChange);
actionDispatcher.dispatchAction(
() -> subscriber.onMapChange(event));
};
topic.getMapData(name).forEach(mapChangeNotifier);
Registration registration = subscribeToMap(name,
mapChangeNotifier);
addRegistration(registration);
return registration;
}
}
@Override
public CompletableFuture replace(String key,
Object expectedValue, Object newValue) {
ensureActiveConnection();
Objects.requireNonNull(key, MessageUtil.Required.KEY);
ObjectNode change = JsonUtil.createReplaceChange(name, key,
expectedValue, newValue);
UUID id = UUID.randomUUID();
return dispatchChangeWithBooleanResult(id, key, false, change);
}
@Override
public CompletableFuture put(String key, Object value,
EntryScope scope) {
ensureActiveConnection();
Objects.requireNonNull(key, MessageUtil.Required.KEY);
boolean connectionScope = scope == EntryScope.CONNECTION;
ObjectNode change = JsonUtil.createPutChange(name, key, null, value,
connectionScope ? topic.getCurrentNodeId() : null);
UUID id = UUID.randomUUID();
return dispatchChangeWithVoidResult(id, key, connectionScope,
change);
}
private CompletableFuture dispatchChangeWithVoidResult(UUID id,
String key, boolean connectionScope, ObjectNode change) {
CompletableFuture contextFuture = actionDispatcher
.createCompletableFuture();
topic.setChangeResultTracker(id, result -> {
if (connectionScope && result == ChangeResult.ACCEPTED) {
connectionScopedMapKeys
.computeIfAbsent(name, k -> new HashMap<>())
.put(key, id);
if (!cleanupPending) {
cleanupScopedData();
}
}
actionDispatcher
.dispatchAction(() -> contextFuture.complete(null));
});
actionDispatcher
.dispatchAction(() -> distributor.accept(id, change));
return contextFuture;
}
private CompletableFuture dispatchChangeWithBooleanResult(
UUID id, String key, boolean connectionScope,
ObjectNode change) {
CompletableFuture contextFuture = actionDispatcher
.createCompletableFuture();
topic.setChangeResultTracker(id, result -> {
if (connectionScope && result == ChangeResult.ACCEPTED) {
connectionScopedMapKeys
.computeIfAbsent(name, k -> new HashMap<>())
.put(key, id);
if (!cleanupPending) {
cleanupScopedData();
}
}
actionDispatcher.dispatchAction(() -> contextFuture
.complete(result != ChangeResult.REJECTED));
});
actionDispatcher
.dispatchAction(() -> distributor.accept(id, change));
return contextFuture;
}
@Override
public Stream getKeys() {
ensureActiveConnection();
synchronized (topic) {
List snapshot = topic.getMapData(name)
.map(MapChange::getKey).collect(Collectors.toList());
return snapshot.stream();
}
}
@Override
public T get(String key, Class type) {
return JsonUtil.toInstance(get(key), type);
}
@Override
public T get(String key, TypeReference type) {
return JsonUtil.toInstance(get(key), type);
}
private JsonNode get(String key) {
ensureActiveConnection();
Objects.requireNonNull(key, MessageUtil.Required.KEY);
synchronized (topic) {
return topic.getMapValue(name, key);
}
}
@Override
public TopicConnection getConnection() {
return TopicConnection.this;
}
@Override
public Optional getExpirationTimeout() {
Duration expirationTimeout = topic.mapExpirationTimeouts.get(name);
return Optional.ofNullable(expirationTimeout);
}
@Override
public void setExpirationTimeout(Duration expirationTimeout) {
ensureActiveConnection();
ObjectNode change = JsonUtil.createMapTimeoutChange(name,
expirationTimeout);
UUID id = UUID.randomUUID();
actionDispatcher
.dispatchAction(() -> distributor.accept(id, change));
}
private Registration subscribeToMap(String mapName,
Consumer mapChangeNotifier) {
subscribersPerMap.computeIfAbsent(mapName, key -> new ArrayList<>())
.add(mapChangeNotifier);
return () -> unsubscribeFromMap(mapName, mapChangeNotifier);
}
private void unsubscribeFromMap(String mapName,
Consumer mapChangeNotifier) {
List> notifiers = subscribersPerMap
.get(mapName);
if (notifiers == null) {
return;
}
notifiers.remove(mapChangeNotifier);
if (notifiers.isEmpty()) {
subscribersPerMap.remove(mapName);
}
}
}
class CollaborationListImplementation implements CollaborationList {
private final String name;
private CollaborationListImplementation(String name) {
this.name = name;
}
@Override
public Registration subscribe(ListSubscriber subscriber) {
ensureActiveConnection();
Objects.requireNonNull(subscriber, "Subscriber cannot be null");
synchronized (topic) {
Consumer changeNotifier = listChange -> {
ListChangeEvent event = new ListChangeEvent(this,
listChange);
actionDispatcher.dispatchAction(
() -> subscriber.onListChange(event));
};
topic.getListChanges(name).forEach(changeNotifier);
Registration registration = subscribeToList(name,
changeNotifier);
addRegistration(registration);
return registration;
}
}
@Override
public List getItems(Class type) {
return getItems(JsonUtil.fromJsonConverter(type));
}
@Override
public List getItems(TypeReference type) {
return getItems(JsonUtil.fromJsonConverter(type));
}
private List getItems(Function converter) {
ensureActiveConnection();
synchronized (topic) {
return topic.getListItems(name).map(item -> item.value)
.map(converter).collect(Collectors.toList());
}
}
@Override
public T getItem(ListKey key, Class type) {
return getItem(key, JsonUtil.fromJsonConverter(type));
}
@Override
public T getItem(ListKey key, TypeReference type) {
return getItem(key, JsonUtil.fromJsonConverter(type));
}
private T getItem(ListKey key, Function converter) {
ensureActiveConnection();
Objects.requireNonNull(key);
synchronized (topic) {
return converter.apply(topic.getListValue(name, key.getKey()));
}
}
@Override
public Stream getKeys() {
ensureActiveConnection();
synchronized (topic) {
return topic.getListItems(name)
.map(item -> new ListKey(item.id))
.collect(Collectors.toList()).stream();
}
}
@Override
public ListOperationResult apply(ListOperation operation) {
ensureActiveConnection();
Objects.requireNonNull(operation, "Operation cannot be null");
UUID scopeOwnerId = null;
if (operation.getScope() != null) {
if (operation.getScope() == EntryScope.CONNECTION) {
scopeOwnerId = topic.getCurrentNodeId();
} else {
scopeOwnerId = JsonUtil.TOPIC_SCOPE_ID;
}
}
ListKey referenceKey = operation.getReferenceKey();
ListKey valueKey = operation.getChangeKey();
ObjectNode change = JsonUtil.createListChange(operation.getType(),
name,
valueKey != null ? valueKey.getKey().toString() : null,
referenceKey != null ? referenceKey.getKey().toString()
: null,
operation.getValue(), scopeOwnerId,
operation.getConditions(), operation.getValueConditions(),
operation.getEmpty());
UUID id = UUID.randomUUID();
return new ListOperationResult<>(new ListKey(id),
dispatchChangeWithBooleanResult(id,
valueKey != null ? valueKey.getKey() : id,
operation.getScope() == EntryScope.CONNECTION,
change));
}
private CompletableFuture dispatchChangeWithBooleanResult(
UUID id, UUID key, boolean connectionScope, ObjectNode change) {
CompletableFuture contextFuture = actionDispatcher
.createCompletableFuture();
topic.setChangeResultTracker(id, result -> {
if (connectionScope && result == ChangeResult.ACCEPTED) {
connectionScopedListItems
.computeIfAbsent(name, k -> new HashMap<>())
.put(key, id);
if (!cleanupPending) {
cleanupScopedData();
}
}
actionDispatcher.dispatchAction(() -> contextFuture
.complete(result != ChangeResult.REJECTED));
});
actionDispatcher
.dispatchAction(() -> distributor.accept(id, change));
return contextFuture;
}
@Override
public TopicConnection getConnection() {
return TopicConnection.this;
}
@Override
public Optional getExpirationTimeout() {
Duration expirationTimeout = topic.listExpirationTimeouts.get(name);
return Optional.ofNullable(expirationTimeout);
}
@Override
public void setExpirationTimeout(Duration expirationTimeout) {
ensureActiveConnection();
ObjectNode change = JsonUtil.createListTimeoutChange(name,
expirationTimeout);
UUID id = UUID.randomUUID();
actionDispatcher
.dispatchAction(() -> distributor.accept(id, change));
}
private Registration subscribeToList(String listName,
Consumer changeNotifier) {
subscribersPerList
.computeIfAbsent(listName, key -> new ArrayList<>())
.add(changeNotifier);
return () -> unsubscribeFromList(listName, changeNotifier);
}
private void unsubscribeFromList(String listName,
Consumer changeNotifier) {
List> notifiers = subscribersPerList
.get(listName);
if (notifiers == null) {
return;
}
notifiers.remove(changeNotifier);
if (notifiers.isEmpty()) {
subscribersPerList.remove(listName);
}
}
}
private final Topic topic;
private final UserInfo localUser;
private final List deactivateRegistrations = new ArrayList<>();
private final Consumer topicActivationHandler;
private final Map>> subscribersPerMap = new HashMap<>();
private final Map>> subscribersPerList = new HashMap<>();
private final Map> connectionScopedMapKeys = new HashMap<>();
private final Map> connectionScopedListItems = new HashMap<>();
private volatile boolean cleanupPending;
private final BiConsumer distributor;
private final SerializableFunction connectionActivationCallback;
private Registration closeRegistration;
private ActionDispatcher actionDispatcher;
/**
* Whether activation has happened, which isn't necessarily the same as
* being active because of the asynchronous step before activation is
* actually applied.
*/
private boolean activated;
TopicConnection(CollaborationEngine ce, ConnectionContext context,
Topic topic, BiConsumer distributor,
UserInfo localUser, Consumer topicActivationHandler,
SerializableFunction connectionActivationCallback) {
this.topic = topic;
this.distributor = distributor;
this.localUser = localUser;
this.topicActivationHandler = topicActivationHandler;
this.connectionActivationCallback = connectionActivationCallback;
this.closeRegistration = context.init(this::acceptActionDispatcher,
ce.getExecutorService());
}
private void handleChange(UUID id, ChangeDetails change) {
try {
if (change instanceof MapChange) {
handleMapChange(id, (MapChange) change);
} else if (change instanceof ListChange) {
handleListChange(id, (ListChange) change);
} else {
throw new UnsupportedOperationException(
"Type '" + change.getClass().getName()
+ "' is not a supported change type");
}
} catch (RuntimeException e) {
deactivateAndClose();
throw e;
}
}
private void handleMapChange(UUID id, MapChange mapChange) {
String mapName = mapChange.getMapName();
String key = mapChange.getKey();
Map keys = connectionScopedMapKeys.get(mapName);
if (keys != null) {
if (keys.containsKey(key)
&& mapChange.getType() == MapChangeType.REPLACE) {
keys.put(key, mapChange.getRevisionId());
}
// If there is a connection scoped entry for the same key with a
// different id, cleanup the existing entry
if (!Objects.equals(id, keys.get(key))) {
UUID uuid = keys.get(key);
if (!Objects.equals(mapChange.getExpectedId(), uuid)) {
keys.remove(key);
}
}
}
if (mapChange.hasChanges()) {
EventUtil.fireEvents(subscribersPerMap.get(mapName),
notifier -> notifier.accept(mapChange), false);
}
}
private void handleListChange(UUID id, ListChange listChange) {
String listName = listChange.getListName();
UUID key = listChange.getKey();
Map keys = connectionScopedListItems.get(listName);
if (keys != null) {
// If there is a connection scoped entry for the same key with a
// different id, cleanup the existing entry
if (!Objects.equals(id, keys.get(key))) {
UUID uuid = keys.get(key);
if (!Objects.equals(listChange.getExpectedId(), uuid)) {
keys.remove(key);
}
}
}
EventUtil.fireEvents(subscribersPerList.get(listChange.getListName()),
notifier -> notifier.accept(listChange), false);
}
Topic getTopic() {
return topic;
}
/**
* Gets the user who is related to this topic connection.
*
* @return the related user, not {@code null}
*
* @since 1.0
*/
public UserInfo getUserInfo() {
return localUser;
}
private boolean isActive() {
return this.actionDispatcher != null;
}
private void addRegistration(Registration registration) {
if (registration != null) {
deactivateRegistrations.add(registration);
}
}
/**
* Gets a collaboration map that can be used to track multiple values in a
* single topic.
*
* @param name
* the name of the map
* @return the collaboration map, not null
*
* @since 1.0
*/
public CollaborationMap getNamedMap(String name) {
ensureActiveConnection();
return new CollaborationMapImplementation(name);
}
/**
* Gets a collaboration list that can be used to track a list of items in a
* single topic.
*
* @param name
* the name of the list
* @return the collaboration list, not null
*/
public CollaborationList getNamedList(String name) {
ensureActiveConnection();
return new CollaborationListImplementation(name);
}
CompletableFuture deactivateAndClose() {
final CompletableFuture result;
try {
deactivate();
} finally {
result = closeWithoutDeactivating();
}
return result;
}
private void deactivate() {
try {
cleanupScopedData();
EventUtil.fireEvents(deactivateRegistrations, Registration::remove,
false);
deactivateRegistrations.clear();
} catch (RuntimeException e) {
if (actionDispatcher != null) {
this.topicActivationHandler.accept(false);
this.actionDispatcher = null;
}
closeWithoutDeactivating();
throw e;
}
}
private CompletableFuture closeWithoutDeactivating() {
if (closeRegistration != null) {
try {
closeRegistration.remove();
if (closeRegistration instanceof AsyncRegistration) {
return ((AsyncRegistration) closeRegistration).getFuture();
}
} finally {
closeRegistration = null;
}
}
return CompletableFuture.completedFuture(null);
}
private void cleanupScopedData() {
synchronized (topic) {
connectionScopedMapKeys.forEach(
(mapName, mapKeys) -> mapKeys.forEach((key, id) -> {
ObjectNode change = JsonUtil.createPutChange(mapName,
key, null, null, null);
change.put(JsonUtil.CHANGE_EXPECTED_ID, id.toString());
distributor.accept(UUID.randomUUID(), change);
}));
connectionScopedMapKeys.clear();
connectionScopedListItems.forEach(
(listName, listItems) -> listItems.forEach((key, id) -> {
ObjectNode change = JsonUtil.createListChange(
ListOperation.OperationType.SET, listName,
key.toString(), null, null, null,
Collections.emptyMap(), Collections.emptyMap(),
null);
change.put(JsonUtil.CHANGE_EXPECTED_ID, id.toString());
distributor.accept(UUID.randomUUID(), change);
}));
connectionScopedListItems.clear();
cleanupPending = false;
}
}
private void ensureActiveConnection() {
if (!isActive()) {
throw new IllegalStateException("Cannot perform this "
+ "operation on a connection that is inactive or about to become inactive.");
}
}
private void acceptActionDispatcher(ActionDispatcher actionDispatcher) {
if (actionDispatcher != null) {
if (activated) {
throw new IllegalStateException(
"The topic connection is already active.");
}
activated = true;
if (this.actionDispatcher != null) {
/*
* Activation while already active based on non-null dispatcher
* means that deactivation has been triggered but its dispatch
* has not yet run. The dispatch will be ignored based on the
* flag which means that there's also no need to activate things
* again.
*/
return;
}
actionDispatcher.dispatchAction(() -> {
if (!activated) {
/*
* Activation canceled while waiting for dispatch.
*/
return;
}
if (this.actionDispatcher != null) {
throw new IllegalStateException(
"Activation dispatch is run out-of-order.");
}
this.actionDispatcher = actionDispatcher;
cleanupPending = true;
topicActivationHandler.accept(true);
Registration changeRegistration = subscribeToChange();
Registration callbackRegistration = connectionActivationCallback
.apply(this);
addRegistration(callbackRegistration);
addRegistration(() -> {
synchronized (topic) {
changeRegistration.remove();
}
});
distributor.accept(UUID.randomUUID(),
JsonUtil.createNodeActivate(topic.getCurrentNodeId()));
});
} else {
if (!activated) {
throw new IllegalStateException(
"The topic connection is already inactive.");
}
activated = false;
if (this.actionDispatcher == null) {
/*
* Deactivation while already inactive based on null dispatcher
* means that activation has been triggered but its dispatch has
* not yet run. The dispatch will be ignored based on the flag
* which means that there's also no need to deactivate things
* again.
*/
return;
}
this.actionDispatcher.dispatchAction(() -> {
if (activated) {
/*
* Deactivation canceled while waiting for dispatch.
*/
return;
}
if (this.actionDispatcher == null) {
throw new IllegalStateException(
"Deactivation dispatch is run out-of-order.");
}
try {
distributor.accept(UUID.randomUUID(), JsonUtil
.createNodeDeactivate(topic.getCurrentNodeId()));
this.actionDispatcher = null;
this.deactivate();
} finally {
topicActivationHandler.accept(false);
}
});
}
}
private Registration subscribeToChange() {
synchronized (topic) {
return topic.subscribeToChange((id, change) -> {
// Dispatch only if we're still active
if (actionDispatcher != null) {
actionDispatcher
.dispatchAction(() -> handleChange(id, change));
}
});
}
}
}