- Prerequisites
- How to enable collaborative editing in client side
- How to enable collaborative editing in Java
- How to perform Scaling in Collaborative Editing.
Contact Support
Collaborative Editing Java
15 Feb 202522 minutes to read
Allows multiple users to work on the same document simultaneously. This can be done in real-time, so that collaborators can see the changes as they are made. Collaborative editing can be a great way to improve efficiency, as it allows team members to work together on a document without having to wait for others to finish their changes.
Prerequisites
The following are needed to enable collaborative editing in Document Editor.
SockJS
Redis
How to enable collaborative editing in client side
Step 1: Enable collaborative editing in Document Editor
To enable collaborative editing, inject CollaborativeEditingHandler
and set the property enableCollaborativeEditing
to true in the Document Editor, like in the code snippet below.
// Inject collaborative editing module.
ej.documenteditor.DocumentEditor.Inject(ej.documenteditor.CollaborativeEditingHandler);
ej.documenteditor.DocumentEditorContainer.Inject(ej.documenteditor.Toolbar);
let container = new ej.documenteditor.DocumentEditorContainer({ enableToolbar: true, height: '590px',});
container.appendTo('#container');
// Enable collaborative editing in Document Editor.
container.documentEditor.enableCollaborativeEditing = true;
Step 2: Configure SockJS to send and receive changes
To broadcast the changes made and receive changes from remote users, configure SockJS like below.
var stompClient = null;
//Initialize SockJS
function onCreated(data) {
var socket = new SockJS('/ws');
stompClient = Stomp.over(socket);
stompClient.connect({}, onConnected, function (error) {
console.error('Error during WebSocket connection', error);
});
}
// Subscribe to the topic.
function onConnected() {
if (stompClient.connected) {
stompClient.subscribe('/topic/public/' + documentName, onMessageReceived);
joinGroup(documentName);
}
}
//Receive the remote action and apply to currenty document.
function onMessageReceived(data) {
var content = JSON.parse(data.body);
if (content.payload.operations != null) {
container.documentEditor.collaborativeEditingHandlerModule.applyRemoteAction("action", content.payload);
}
}
Step 3: Subscribe to specific topic while opening the document
When opening a document, we need to generate a unique ID for each document. These unique IDs are then used to create rooms using SockJS, which facilitates sending and receiving data from the server.
function openDocument(responseText, roomName) {
let data = JSON.parse(responseText);
//Update the room and version information to collaborative editing handler.
connections.updateRoomInfo(roomName, data.version, serviceUrl + 'api/collaborativeediting/');
//open the document
container.documentEditor.open(data.sfdt);
setTimeout(function () {
// connect to server using ScketJS
connectToRoom({ action: 'connect', roomName: roomName, currentUser: container.currentUser });
});
}
//Send the user information to the other users that I have joined.
function connectToRoom(documentName) {
var userInfo = {
currentUser: username,
clientVersion: 0,
roomName: documentName,
connectionId: "",
};
// Send the joinGroup message to the server
stompClient.send("/app/join/" + documentName, {}, JSON.stringify(userInfo));
}
Step 4: Broadcast current editing changes to remote users
Changes made on the client-side need to be sent to the server-side to broadcast them to other connected users. To send the changes made to the server, use the method shown below from the document editor using the contentChange
event.
container.contentChange = function (args) {
if (connections) {
connections.sendActionToServer(args.operations);
}
}
How to enable collaborative editing in Java
Step 1: Configure SockJS hub to create room for collaborative editing session.
To manage groups for each document, create a folder named “Hub” and add a file named DocumentEditorHub.java
inside it. Add the following code to the file to manage SockJS groups using room names.
Join the group by using unique id of the document by using joinGroup
method.
@MessageMapping("/join/{documentName}")
public void joinGroup(ActionInfo info, SimpMessageHeaderAccessor headerAccessor,
@DestinationVariable String documentName) throws JsonProcessingException {
// To get the connection Id
String connectionId = headerAccessor.getSessionId();
info.setConnectionId(connectionId);
String docName = info.getRoomName();
HashMap<String, Object> additionalHeaders = new HashMap<>();
additionalHeaders.put("action", "connectionId");
MessageHeaders headers = new MessageHeaders(additionalHeaders);
// send the connection Id to the client
broadcastToRoom(docName, info, headers);
…………
…………
…………
}
public static void broadcastToRoom(String roomName, Object payload, MessageHeaders headers) {
messagingTemplate.convertAndSend("/topic/public/" + roomName, MessageBuilder.createMessage(payload, headers));
}
Step 2: Handle user disconnection using SockJS.
@EventListener
public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) throws Exception {
String sessionId = event.getSessionId();
try (Jedis jedis = RedisSubscriber.getJedis()) {
// to get the user details of the provided sessionId
String docName = jedis.hget("documentMap", sessionId);
// Publish a message indicating the user's departure from the group
jedis.publish(docName, "LEAVE|" + sessionId);
} catch (JedisConnectionException e) {
System.out.println(e);
}
}
Step 3: Configure Redis cache connection string in application level.
Configure the Redis that stores temporary data for the collaborative editing session. Provide the Redis connection string in application.properties
file.
//Redis configuration
spring.datasource.redishost= "<Redis host string>"
spring.datasource.redisport= "<Redis port>"
spring.datasource.redispassword= "<Redis password>"
spring.datasource.redisssl = <boolean>
Step 4: Configure Web API actions for collaborative editing.
Import File
- When opening a document, create a Redis cache to store temporary data for the collaborative editing session.
- If the Redis cache already exists, retrieve the records from the Redis cache and apply them to the WordProcessorHelper instance using the
updateActions
method before converting it to the SFDT format.
public String importFile(@RequestBody FilesPathInfo file) throws Exception {
try {
ClassLoader classLoader = getClass().getClassLoader();
// Get source document from database/file system/blob storage
WordProcessorHelper document = getDocumentFromBucketS3(file.getFileName(), datasourceAccessKey,
datasourceSecretKey, datasourceBucketName);
documentName=file.getFileName();
// Get the list of pending operations for the document
List<ActionInfo> actions = getPendingOperations(file.getFileName(), 0, -1);
if (actions != null && actions.size() > 0) {
// If there are any pending actions, update the document with these actions
document.updateActions(actions);
}
// Serialize the updated document to SFDT format
String json = WordProcessorHelper.serialize(document);
// Return the serialized content as a JSON string
return json;
} catch (Exception e) {
e.printStackTrace();
return "{\"sections\":[{\"blocks\":[{\"inlines\":[{\"text\":" + e.getMessage() + "}]}]}]}";
}
}
Update editing records to Redis
- Each edit operation made by the user is sent to the server and is pushed to the Redis. Each operation receives a version number after being inserted into the Redis.
- After inserting the records to the server, the position of the current editing operation must be transformed against any previous editing operations not yet synced with the client using the TransformOperation method.
- After performing the transformation, the current operation is broadcast to all connected users within the group.
public ActionInfo updateAction(@RequestBody ActionInfo param) throws Exception {
String roomName = param.getRoomName();
ActionInfo transformedAction = addOperationsToCache(param);
HashMap<String, Object> action = new HashMap<>();
action.put("action", "updateAction");
DocumentEditorHub.publishToRedis(roomName, transformedAction);
DocumentEditorHub.broadcastToRoom(roomName, transformedAction, new MessageHeaders(action));
return transformedAction;
}
private ActionInfo addOperationsToCache(ActionInfo action) throws Exception {
int clientVersion = action.getVersion();
…………
…………
…………
// Define the keys for Redis operations based on the action's room name
String[] keys = { roomName + CollaborativeEditingHelper.versionInfoSuffix, roomName,
roomName + CollaborativeEditingHelper.revisionInfoSuffix,
roomName + CollaborativeEditingHelper.actionsToRemoveSuffix };
// Prepare values for the Redis script
String[] values = { serializedAction, String.valueOf(clientVersion),
String.valueOf(CollaborativeEditingHelper.saveThreshold) };
…………
…………
…………
// Return the updated action
return action;
}
Add Web API to get previous operation as a backup to get lost operations
On the client side, messages send from server using SockJS may be received in a different order, or some operations may be missed due to network issues. In these cases, we need a backup method to retrieve missing records from the Redis.
Using the following method, we can retrieve all operations after the last successful client-synced version and return all missing operations to the requesting client.
@PostMapping("/api/collaborativeediting/GetActionsFromServer")
public String getActionsFromServer(@RequestBody ActionInfo param) throws ClassNotFoundException {
try (Jedis jedis = RedisSubscriber.getJedis()) {
// Initialize necessary variables from the parameters and helper class
int saveThreshold = CollaborativeEditingHelper.saveThreshold;
String roomName = param.getRoomName();
int lastSyncedVersion = param.getVersion();
int clientVersion = param.getVersion();
// Fetch actions that are effective and pending based on the last synced version
List<ActionInfo> actions = GetEffectivePendingVersion(roomName, lastSyncedVersion, jedis);
List<ActionInfo> currentAction = new ArrayList<>();
for (ActionInfo action : actions) {
// Increment the version for each action sequentially
action.setVersion(++clientVersion);
// Filter actions to only include those that are newer than the client's last
// known version
if (action.getVersion() > lastSyncedVersion) {
// Transform actions that have not been transformed yet
if (!action.isTransformed()) {
CollaborativeEditingHandler.transformOperation(action, new ArrayList<>(actions));
}
currentAction.add(action);
}
}
// Serialize the filtered and transformed actions to JSON and return
return gson.toJson(currentAction);
} catch (Exception ex) {
ex.printStackTrace();
// In case of an exception, return an empty JSON object
return "{}";
}
}
How to perform Scaling in Collaborative Editing.
Role of Scaling in Collaborative editing
As the number of users increases, collaborative application face challenges in maintaining responsiveness and performance. This is where scaling becomes crucial. Scaling refers to the ability of an application to handle growing demands by effectively distributing the workload across multiple resources.
During scaling the users may connected to different servers, so collaborative editing application introduces a specific challenge like, updating the edit operations to all the users connected in different serves. To overcome this issue you need to use Redis Cache pub/sub
for message relay(syncing the editing operations to the users connected to different server instance)
Use of Redis Pub/Sub in scaling environment
Redis offers Pub/Sub functionality. The publish/subscribe (pub/sub) pattern provides asynchronous communication among multiple AWS services without creating interdependency. When a user edits a document, the application can publish the changes to a Redis channel. Clients (in different server instances) subscribed to that channel receive real-time updates, reflecting the changes in their document views.
Steps to configure Redis in Collaborative Editing Application
Refer to the below steps to know about the Redis pub/sub implementation to sync the messages.
Step 1: Configure Redis in application level to establish the connection.
//Redis configuration
spring.datasource.redishost= "<Redis host link>"
spring.datasource.redisport= "<Redis port number>"
Step 2: Publish each editing operation to a Redis channel
Publish each editing operation to Redis channel with the room name. This will send notifications to all the users(in different servers) subscribed to that specific channel. Refer to the publishToRedis() method in DocumentEditorHub.Java for details.
try (Jedis jedis = RedisSubscriber.jedisPool.getResource()) {
jedis.publish("collaborativeedtiting", new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(payload));
break;
} catch (JedisConnectionException e) {
}
Step 3: Subscribe to the specific channel using the Redis cache ‘Subscribe’
Redis cache will be initialized and subscribe to the specific channel using the Redis cache ‘Subscribe’ option. This ensures that users in any server will get notified when an editing operation is published to the Redis cache using the onMessage() API. Refer to the code snippet in RedisSubscriber.Java for details.
@PostConstruct
public void subscribeToInstanceChannel() {
//Subscriber to `collaborativeediting`
String channel = "collaborativeedtiting";
new Thread(() -> {
JedisPoolConfig poolConfig = new JedisPoolConfig();
jedisPool = new JedisPool(poolConfig, REDIS_HOST, REDIS_PORT);
try (Jedis jedis = jedisPool.getResource()) {
jedis.subscribe(new JedisPubSub() {
@Override
public void onMessage(String channel, String message) {
-------------
------
// Message will be broadcasted to all the users connected to that room using sockjs
DocumentEditorHub.broadcastToRoom(action.getRoomName(), action, updateActionheaders);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
@Override
public void onSubscribe(String channel, int subscribedChannels) {
System.out.println("Subscribed to channel: " + channel);
}
}, channel);
} catch (JedisConnectionException e) {
// Handle the connection exception
System.out.println("Connection failed. Retrying ...");
}
}).start();
}
Full version of the code discussed about can be found in below GitHub location.
GitHub Example: Collaborative editing examples