diff --git a/Server side with distributed cache/Java/Using Redis/.gitignore b/Server side with distributed cache/Java/Using Redis/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/Server side with distributed cache/Java/Using Redis/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/Server side with distributed cache/Java/Using Redis/ReadMe.md b/Server side with distributed cache/Java/Using Redis/ReadMe.md new file mode 100644 index 0000000..1e06dec --- /dev/null +++ b/Server side with distributed cache/Java/Using Redis/ReadMe.md @@ -0,0 +1,38 @@ +# How to host the DocumentEditor service. + +Opening and saving documents using Redis. + +## Maven Project + +Please find the list of commands used for running and deploying the spring boot application in Azure. + +Clean the package using + +> mvn clean package + +Run the application locally using + +> mvn spring-boot:run + +Build the package using + +> mvn package + +Above package generation command creates the "tomcat-0.0.1-SNAPSHOT.war" in the below location. + +> target\tomcat-0.0.1-SNAPSHOT.war + +Please create a Azure app service with Java & Tomcat. + +After creating the app service + +>Development Tools -> Advanced Tools -> Go -> Debug console -> CMD + +Once the file manager is opened please navigate to + +>site -> wwwroot -> webapps + +Upload the generated war file "tomcat-0.0.1-SNAPSHOT.war" the application will be hosted under + +>{site-name}/tomcat-0.0.1-SNAPSHOT + diff --git a/Server side with distributed cache/Java/Using Redis/mvnw b/Server side with distributed cache/Java/Using Redis/mvnw new file mode 100644 index 0000000..a16b543 --- /dev/null +++ b/Server side with distributed cache/Java/Using Redis/mvnw @@ -0,0 +1,310 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/Server side with distributed cache/Java/Using Redis/mvnw.cmd b/Server side with distributed cache/Java/Using Redis/mvnw.cmd new file mode 100644 index 0000000..c8d4337 --- /dev/null +++ b/Server side with distributed cache/Java/Using Redis/mvnw.cmd @@ -0,0 +1,182 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/Server side with distributed cache/Java/Using Redis/pom.xml b/Server side with distributed cache/Java/Using Redis/pom.xml new file mode 100644 index 0000000..6b42da2 --- /dev/null +++ b/Server side with distributed cache/Java/Using Redis/pom.xml @@ -0,0 +1,202 @@ + + + 4.0.0 + com.syncfusion + tomcat + 0.0.1-SNAPSHOT + war + tomcat + Demo project for Spring Boot + + + Syncfusion-Java + Syncfusion Java repo + https://jars.syncfusion.com/repository/maven-public/ + + + + org.springframework.boot + spring-boot-starter-parent + 2.3.4.RELEASE + + + + + UTF-8 + UTF-8 + 1.8 + com.syncfusion.tomcat.TomcatApplication + + + + javax.servlet + javax.servlet-api + 4.0.1 + provided + + + com.syncfusion + syncfusion-javahelper + 26.2.7 + + + com.syncfusion + syncfusion-docio + 26.2.7 + + + com.syncfusion + syncfusion-ej2-spellchecker + 26.2.7 + + + org.luaj + luaj-jse + 3.0.1 + + + + + software.amazon.awssdk + s3 + 2.24.9 + + + com.syncfusion + syncfusion-ej2-wordprocessor + 26.2.7 + + + redis.clients + jedis + 3.7.0 + + + org.springframework + spring-messaging + + + org.springframework + spring-websocket + + + org.springframework.boot + spring-boot-starter-web + + + com.google.code.gson + gson + + + org.springframework.boot + spring-boot-starter-tomcat + provided + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-data-redis + + + redis.clients + jedis + 3.3.0 + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-dependency-plugin + 3.1.2 + + 22.0.2 + 22.0.2 + + + + + copy-dependencies + compile + + copy-dependencies + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/lib + + + + + + + + + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + + org.apache.maven.plugins + + + maven-dependency-plugin + + + [2.8,) + + + + copy-dependencies + + + + + + + + + + + + + + + + + diff --git a/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/BackgroundService.java b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/BackgroundService.java new file mode 100644 index 0000000..2e2e8f5 --- /dev/null +++ b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/BackgroundService.java @@ -0,0 +1,132 @@ +package com.syncfusion.tomcat; + +import java.io.ByteArrayOutputStream; +import java.io.Console; +import java.io.FileOutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Semaphore; + +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import com.syncfusion.docio.WordDocument; +import com.syncfusion.ej2.wordprocessor.ActionInfo; +import com.syncfusion.ej2.wordprocessor.CollaborativeEditingHandler; +import com.syncfusion.ej2.wordprocessor.WordProcessorHelper; +import com.syncfusion.tomcat.controller.RedisSubscriber; + +import ch.qos.logback.classic.Logger; +import redis.clients.jedis.Jedis; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.regions.Region; + +@Service +public class BackgroundService { + + private static final Logger logger = (Logger) LoggerFactory.getLogger(BackgroundService.class); + private final List itemsToProcess = new ArrayList<>(); + private final Semaphore semaphore = new Semaphore(1); + + @Value("${spring.datasource.accesskey}") + private String datasourceAccessKey; + @Value("${spring.datasource.secretkey}") + private String datasourceSecretKey; + @Value("${spring.datasource.bucketname}") + private String datasourceBucketName; + @Value("${spring.datasource.regionname}") + private String datasourceRegionName; + + @Scheduled(fixedRate = 5000) // Runs every 10 seconds + public void runBackgroundTask() { + try { + semaphore.acquire(); + synchronized (itemsToProcess) { + while (!itemsToProcess.isEmpty()) { + SaveInfo item = itemsToProcess.remove(0); + logger.info("Processing item : ",item); + // Process the item here + applyOperationsToSourceDocument(item); + clearRecordsFromRedisCache(item); + } + } + } catch (InterruptedException e) { + // Handle the exception if needed + } finally { + semaphore.release(); + } + } + + public void addItemToProcess(SaveInfo item) { + synchronized (itemsToProcess) { + itemsToProcess.add(item); + } + } + + private void applyOperationsToSourceDocument(SaveInfo workItem) { + try { + ArrayList actions = (ArrayList) workItem.getActions(); + for (ActionInfo action : actions) { + if (!action.isTransformed()) { + CollaborativeEditingHandler.transformOperation(action, actions); + } + } + ClassLoader classLoader = getClass().getClassLoader(); + String fileName = CollaborativeEditingController.documentName; + WordProcessorHelper document = CollaborativeEditingController.getDocumentFromBucketS3(fileName,datasourceAccessKey, + datasourceSecretKey, datasourceBucketName); + CollaborativeEditingHandler handler = new CollaborativeEditingHandler(document); + + if (actions != null && actions.size() > 0) { + for (ActionInfo info : actions) { + handler.updateAction(info); + } + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + WordDocument doc = WordProcessorHelper.save(WordProcessorHelper.serialize(handler.getDocument())); + doc.save(outputStream, com.syncfusion.docio.FormatType.Docx); + + byte[] data = outputStream.toByteArray(); + + AwsCredentials credentials = AwsBasicCredentials.create(datasourceAccessKey, datasourceSecretKey); + StaticCredentialsProvider credentialsProvider = StaticCredentialsProvider.create(credentials); + S3Client s3Client = S3Client.builder().region(Region.US_EAST_1).credentialsProvider(credentialsProvider) + .build(); + PutObjectRequest objectRequest = PutObjectRequest.builder().bucket(datasourceBucketName).key(fileName).build(); + s3Client.putObject(objectRequest, software.amazon.awssdk.core.sync.RequestBody.fromBytes(data)); + s3Client.close(); + + // String currentDir = System.getProperty("user.dir") + "/src/main/resources/static/files"; + // try (FileOutputStream fos = new FileOutputStream(currentDir + workItem.getRoomName())) { + // // Write the byte array to the file + // fos.write(data); + // fos.close(); + // } catch (Exception ex) { + // ex.printStackTrace(); + // } + outputStream.close(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void clearRecordsFromRedisCache(SaveInfo workItem) { + Boolean partialSave = workItem.getPartialSave(); + String roomName = workItem.getRoomName(); + try (Jedis jedis = RedisSubscriber.getJedis()) { + if (!partialSave) { + jedis.del(roomName); + jedis.del(roomName + CollaborativeEditingHelper.revisionInfoSuffix); + jedis.del(roomName + CollaborativeEditingHelper.versionInfoSuffix); + } + jedis.del(roomName + CollaborativeEditingHelper.actionsToRemoveSuffix); + } + } +} \ No newline at end of file diff --git a/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/CollaborativeEditingController.java b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/CollaborativeEditingController.java new file mode 100644 index 0000000..99671a5 --- /dev/null +++ b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/CollaborativeEditingController.java @@ -0,0 +1,323 @@ +package com.syncfusion.tomcat; + +import java.io.ByteArrayInputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.messaging.MessageHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.syncfusion.ej2.wordprocessor.ActionInfo; +import com.syncfusion.ej2.wordprocessor.CollaborativeEditingHandler; +import com.syncfusion.ej2.wordprocessor.DocumentOperation; +import com.syncfusion.ej2.wordprocessor.WordProcessorHelper; +import com.syncfusion.tomcat.controller.DocumentEditorHub; +import com.syncfusion.tomcat.controller.RedisSubscriber; +import org.springframework.core.io.Resource; + +import redis.clients.jedis.Jedis; +import redis.clients.jedis.exceptions.JedisException; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.ResponseInputStream; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.regions.Region; + +@RestController +@Component +public class CollaborativeEditingController { + + @Value("classpath:static/files/*") + private Resource[] resources; + + // Redis Configuration + @Value("${spring.datasource.redishost}") + private static String REDIS_HOST; + @Value("${spring.datasource.redisport}") + private static int REDIS_PORT; + @Value("${spring.datasource.redispassword}") + private String REDIS_PASSWORD; + @Value("${spring.datasource.redisssl}") + private boolean REDISSSL; + + @Value("${spring.datasource.accesskey}") + private String datasourceAccessKey; + @Value("${spring.datasource.secretkey}") + private String datasourceSecretKey; + @Value("${spring.datasource.bucketname}") + private String datasourceBucketName; + @Value("${spring.datasource.regionname}") + private String datasourceRegionName; + + @Autowired + private BackgroundService backgroundService; + + private final Gson gson; + protected static String documentName; + + // Dependency injection through constructor + @Autowired + public CollaborativeEditingController(Gson gson) { + this.gson = gson; + } + + @CrossOrigin(origins = "*", allowedHeaders = "*") + @PostMapping("/api/collaborativeediting/ImportFile") + public String importFile(@RequestBody FilesPathInfo file) throws Exception { + try { + ClassLoader classLoader = getClass().getClassLoader(); + WordProcessorHelper document = getDocumentFromBucketS3(file.getFileName(), datasourceAccessKey, + datasourceSecretKey, datasourceBucketName); + documentName=file.getFileName(); + // Get the list of pending operations for the document + List 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() + "}]}]}]}"; + } + } + + // Method to retrieve pending operations from a Redis list between specified + // indexes + private List getPendingOperations(String listKey, int startIndex, int endIndex) { + try (Jedis jedis = RedisSubscriber.getJedis()) { + // Initialize the list to hold ActionInfo objects + // List actionInfoList = new ArrayList<>(); + Object response = jedis.eval(CollaborativeEditingHelper.pendingOperations, 2, listKey, + listKey + CollaborativeEditingHelper.actionsToRemoveSuffix, String.valueOf(startIndex), + String.valueOf(endIndex)); + List results = (List) response; + List actions = new ArrayList<>(); + for (Object result : results) { + List resultList = (List) result; + for (Object item : resultList) { + actions.add(gson.fromJson((String) item, ActionInfo.class)); + } + } + return actions; + } catch (Exception ex) { + ex.printStackTrace(); + return null; + } + } + + @CrossOrigin(origins = "*", allowedHeaders = "*") + @PostMapping("/api/collaborativeediting/UpdateAction") + public ActionInfo updateAction(@RequestBody ActionInfo param) throws Exception { + String roomName = param.getRoomName(); + ActionInfo transformedAction = addOperationsToCache(param); + HashMap action = new HashMap<>(); + action.put("action", "updateAction"); + DocumentEditorHub.publishToRedis(roomName, transformedAction); + DocumentEditorHub.broadcastToRoom(roomName, transformedAction, new MessageHeaders(action)); + return transformedAction; + } + + @CrossOrigin(origins = "*", allowedHeaders = "*") + @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 actions = getEffectivePendingVersion(roomName, lastSyncedVersion, jedis); + List 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 "{}"; + } + } + + private List getEffectivePendingVersion(String roomName, int lastSyncedVersion, Jedis jedis) { + // Define Redis keys for accessing the room data and its revision information + String[] keys = { roomName, roomName + CollaborativeEditingHelper.revisionInfoSuffix }; + // Prepare Redis values for the script: start index and save threshold + String[] values = { Integer.toString(lastSyncedVersion), + String.valueOf(CollaborativeEditingHelper.saveThreshold) }; + Object response = jedis.eval(CollaborativeEditingHelper.effectivePendingOperations, keys.length, keys[0], + keys[1], values[0], values[1]); + // Deserialize the fetched actions from Redis and convert them into a list of + // ActionInfo objects + List results = (List) response; + List actions = new ArrayList<>(); + for (Object result : results) { + if (result instanceof String) { + actions.add(gson.fromJson((String) result, ActionInfo.class)); + } + } + return actions; + } + + private ActionInfo addOperationsToCache(ActionInfo action) throws Exception { + int clientVersion = action.getVersion(); + // Serialize the action + String serializedAction = gson.toJson(action); + String roomName = action.getRoomName(); + + // 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) }; + + try (Jedis jedis = RedisSubscriber.getJedis()) { + try { + // Execute the Lua script in Redis and store the results + Object response = jedis.eval(CollaborativeEditingHelper.insertScript, keys.length, keys[0], keys[1], + keys[2], keys[3], values[0], values[1], values[2]); + List results = (List) response; + // Parse the version number from the script results + int version = Integer.parseInt(results.get(0).toString()); + // Deserialize the list of previous operations from the script results + ArrayList previousOperations = new ArrayList(); + Object data = results.get(1); + if (data instanceof List) { + for (Object result : (List) data) { + if (result instanceof String) { + previousOperations.add(gson.fromJson((String) result, ActionInfo.class)); + } + } + } + // Increment the version for each previous operation + previousOperations.forEach(op -> op.setVersion(op.getVersion() + 1)); + // Check if there are multiple previous operations to determine if + // transformation is needed + if (previousOperations.size() > 1) { + // Set the current action to the last operation in the list + action = previousOperations.get(previousOperations.size() - 1); + for (ActionInfo op : previousOperations) { + // Transform operations that have not been transformed yet + List operation = op.getOperations(); + if (operation != null && !op.isTransformed()) { + CollaborativeEditingHandler.transformOperation(op, previousOperations); + } + } + } + // Update the action's version and mark it as transformed + action.setVersion(version); + action.setTransformed(true); + // Update the record in the cache with the new version + updateRecordToCache(version, action, jedis); + // Check if there are cleared operations to be saved + if (results.size() > 2 && results.get(2) != null) { + autoSaveChangesToSourceDocument((List) results.get(2), action); + } + + } catch (Exception e) { + e.printStackTrace(); + } + } catch (JedisException e) { + e.printStackTrace(); + } + // Return the updated action + return action; + } + + private void autoSaveChangesToSourceDocument(List clearedOperations, ActionInfo action) { + List actions = new ArrayList<>(); + for (Object operation : clearedOperations) { + ActionInfo actionInfo = gson.fromJson((String) operation, ActionInfo.class); + actions.add(actionInfo); + } + // Prepare the message for saving the cleared operations + SaveInfo message = new SaveInfo(); + message.setActions(actions); + message.setPartialSave(true); + message.setRoomName(action.getRoomName()); + backgroundService.addItemToProcess(message); + } + + private void updateRecordToCache(int version, ActionInfo action, Jedis jedis) { + // Serialize the action + String serializedAction = gson.toJson(action); + + // Prepare Redis keys and values for the script execution + String roomName = action.getRoomName(); + String revisionInfoKey = roomName + CollaborativeEditingHelper.revisionInfoSuffix; + String previousVersion = String.valueOf(version - 1); + String saveThreshold = String.valueOf(CollaborativeEditingHelper.saveThreshold); + + // Execute the Lua script with the prepared keys and values + try { + jedis.eval(CollaborativeEditingHelper.updateRecord, 2, roomName, revisionInfoKey, serializedAction, + previousVersion, saveThreshold); + } catch (Exception ex) { + ex.printStackTrace(); + // Handle the exception as needed, e.g., logging or rethrowing + } + } + protected static WordProcessorHelper getDocumentFromBucketS3(String documentId, String accessKey, String secretKey, + String bucketName) { + try { + AwsCredentials credentials = AwsBasicCredentials.create(accessKey, secretKey); + StaticCredentialsProvider credentialsProvider = StaticCredentialsProvider.create(credentials); + S3Client s3Client = S3Client.builder().region(Region.US_EAST_1).credentialsProvider(credentialsProvider) + .build(); + ResponseInputStream objectData = s3Client + .getObject(GetObjectRequest.builder().bucket(bucketName).key(documentId).build()); + // Read the object data into a byte array + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = objectData.read(buffer)) != -1) { + byteArrayOutputStream.write(buffer, 0, bytesRead); + } + s3Client.close(); + byte[] data = byteArrayOutputStream.toByteArray(); + // Create an input stream from the byte array + try (InputStream stream = new ByteArrayInputStream(data)) { + return WordProcessorHelper.load(stream, true); + } + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + +} diff --git a/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/CollaborativeEditingHelper.java b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/CollaborativeEditingHelper.java new file mode 100644 index 0000000..3623f69 --- /dev/null +++ b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/CollaborativeEditingHelper.java @@ -0,0 +1,129 @@ +package com.syncfusion.tomcat; + +public class CollaborativeEditingHelper { + + // Maximum number of operation we can queue in single revision. + // If we reach this limit, we will save the operations to source document. + public static final int saveThreshold = 150; + + // Suffix key to store revision information in redis cache. + public static final String versionInfoSuffix = "_version_info"; + + // Suffix key to store version information in redis cache. + public static final String revisionInfoSuffix = "_revision_info"; + + // Suffix key to store user information in redis cache. + public static final String userInfoSuffix = "_user_info"; + + // Suffix key to store removed actions information in redis cache. + public static final String actionsToRemoveSuffix = "_actions_to_remove"; + + // Key to store room information with connection Id in redis cache. + public static final String connectionIdRoomMappingKey = "ej_de_connection_id_room_mapping"; + + public static final String insertScript = "-- Define keys for version, list, and revision \n"+ + "local versionKey = KEYS[1] \n"+ + "local listKey = KEYS[2] \n"+ + "local revisionKey = KEYS[3] \n"+ + "local updateKey = KEYS[4] \n"+ + "-- Define arguments: item to insert, client's version, and threshold for cache \n"+ + "local item = ARGV[1] \n"+ + "local clientVersion = tonumber(ARGV[2]) \n"+ + "local threshold = tonumber(ARGV[3]) \n"+ + "-- Increment the version for each operation \n"+ + "local version = redis.call('INCR', versionKey) \n"+ + "-- Retrieve the current revision, or initialize it if it doesn't exist \n"+ + "local revision = redis.call('GET', revisionKey) \n"+ + "if not revision then \n"+ + "redis.call('SET', revisionKey, '0') \n"+ + "revision = 0 \n"+ + "else \n"+ + "revision = tonumber(revision) \n"+ + "end \n"+ + "-- Calculate the effective version by multiplying revision by threshold \n"+ + "local effectiveVersion = revision * threshold \n"+ + "-- Adjust clientVersion based on effectiveVersion \n"+ + "clientVersion = clientVersion - effectiveVersion \n"+ + "-- Add the new item to the list and get the new length \n"+ + "local length = redis.call('RPUSH', listKey, item) \n"+ + "-- Retrieve operations since the client's version \n"+ + "local previousOps = redis.call('LRANGE', listKey, clientVersion, -1) \n"+ + "-- Define a limit for cache based on threshold \n"+ + "local cacheLimit = threshold * 2; \n"+ + "local elementToRemove = nil \n"+ + "-- If the length of the list reaches the cache limit, trim the list \n"+ + "if length % cacheLimit == 0 then \n"+ + "elementToRemove = redis.call('LRANGE', listKey, 0, threshold - 1) \n"+ + "redis.call('LTRIM', listKey, threshold, -1) \n"+ + "-- Increment the revision after trimming \n"+ + "redis.call('INCR', revisionKey) \n"+ + "-- Add elements to remove to updateKey \n"+ + "for _, v in ipairs(elementToRemove) do \n"+ + "redis.call('RPUSH', updateKey, v) \n"+ + "end \n"+ + "end \n"+ + "-- Return the current version, operations since client's version, and elements removed \n"+ + "local values = {version, previousOps, elementToRemove} \n"+ + "return values \n"; + + public static final String updateRecord = "-- Define keys for list and revision \n" + + "local listKey = KEYS[1] \n" + + "local revisionKey = KEYS[2] \n"+ + "-- Define arguments: item to insert, client's version, and threshold for cache \n"+ + "local item = ARGV[1] \n"+ + "local clientVersion = ARGV[2] \n"+ + "local threshold = tonumber(ARGV[3]) \n"+ + "-- Retrieve the current revision from Redis, or initialize it if it doesn't exist \n"+ + "local revision = redis.call('GET', revisionKey) \n"+ + "if not revision then \n"+ + "revision = 0 \n"+ + "else \n"+ + "revision = tonumber(revision) \n"+ + "end \n"+ + "-- Calculate the effective version by multiplying revision by threshold \n"+ + "local effectiveVersion = revision * threshold \n"+ + "-- Adjust clientVersion based on effectiveVersion \n"+ + "clientVersion = tonumber(clientVersion) - effectiveVersion \n"+ + "-- Update the list at the position calculated by the adjusted clientVersion \n"+ + "-- This effectively 'inserts' the item into the list at the position reflecting the client's view of the list \n"+ + "redis.call('LSET', listKey, clientVersion, item) \n"; + + public static final String effectivePendingOperations = "-- Define the keys for accessing the list and revision in Redis \n"+ + "local listKey = KEYS[1] \n"+ + "local revisionKey = KEYS[2] \n"+ + "-- Convert the first argument to a number to represent the client's version \n"+ + "local clientVersion = tonumber(ARGV[1]) \n"+ + "-- Convert the second argument to a number for the threshold value \n"+ + "local threshold = tonumber(ARGV[2]) \n"+ + "-- Retrieve the current revision number from Redis \n"+ + "local revision = redis.call('GET', revisionKey) \n"+ + "if not revision then \n"+ + "revision = 0 \n"+ + "else \n"+ + "revision = tonumber(revision) \n"+ + "end \n"+ + "-- Calculate the effective version by multiplying the revision number by the threshold \n"+ + "-- This helps in determining the actual version of the document considering the revisions \n"+ + "local effectiveVersion = revision * threshold \n"+ + "-- Adjust the client's version by subtracting the effective version \n"+ + "-- This calculation aligns the client's version with the server's version, accounting for any revisions \n"+ + "clientVersion = clientVersion - effectiveVersion \n"+ + "-- Return a range of list elements starting from the adjusted client version to the end of the list \n"+ + "-- This command retrieves all operations that have occurred since the client's last known state \n"+ + "if clientVersion >= 0 then \n"+ + "return redis.call('LRANGE', listKey, clientVersion, -1) \n"+ + "else \n"+ + "return {} \n"+ + "end \n"; + + public static final String pendingOperations = "local listKey = KEYS[1] \n"+ + "local processingKey = KEYS[2] \n"+ + "local startIndex = tonumber(ARGV[1]) \n"+ + "local endIndex = tonumber(ARGV[2]) \n"+ + "-- Fetch the list of operations from the listKey \n"+ + "local listValues = redis.call('LRANGE', listKey, startIndex, endIndex) \n"+ + "-- Fetch the list of operations from the processingKey \n"+ + "local processingValues = redis.call('LRANGE', processingKey, startIndex, endIndex) \n"+ + "-- Return both lists as a combined result \n"+ + "return {processingValues, listValues} \n"; +} diff --git a/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/CustomParameter.java b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/CustomParameter.java new file mode 100644 index 0000000..7811985 --- /dev/null +++ b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/CustomParameter.java @@ -0,0 +1,22 @@ +package com.syncfusion.tomcat; + +public class CustomParameter { + public String content; + public String type; + + public String getContent() { + return content; + } + + public String getType() { + return type; + } + + public void setContent(String value) { + content = value; + } + + public void setType(String value) { + type = value; + } +} diff --git a/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/CustomRestrictParameter.java b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/CustomRestrictParameter.java new file mode 100644 index 0000000..e26517a --- /dev/null +++ b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/CustomRestrictParameter.java @@ -0,0 +1,31 @@ +package com.syncfusion.tomcat; + +public class CustomRestrictParameter { + public String passwordBase64; + public String saltBase64; + public int spinCount; + + public String getPasswordBase64() { + return passwordBase64; + } + + public String getSaltBase64() { + return saltBase64; + } + + public int getSpinCount() { + return spinCount; + } + + public void setPasswordBase64(String value) { + passwordBase64 = value; + } + + public void setSaltBase64(String value) { + saltBase64 = value; + } + + public void setSpinCount(int value) { + spinCount = value; + } +} diff --git a/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/DocumentContent.java b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/DocumentContent.java new file mode 100644 index 0000000..3e6458c --- /dev/null +++ b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/DocumentContent.java @@ -0,0 +1,49 @@ +package com.syncfusion.tomcat; + +import java.util.List; + +import com.syncfusion.ej2.wordprocessor.ActionInfo; + +public class DocumentContent { + private int version; + private String sfdt; + private List actions; + + // Default constructor + public DocumentContent() { + } + + // Parameterized constructor + + public DocumentContent(int version, String sfdt, List actions) { + this.version = version; + this.sfdt = sfdt; + this.actions = actions; + } + + // Getter and setter methods + public int getVersion() { + return version; + } + + public void setVersion(int version) { + this.version = version; + } + + public String getSfdt() { + return sfdt; + } + + public void setSfdt(String sfdt) { + this.sfdt = sfdt; + } + + public List getActions() { + return actions; + } + + public void setActions(List actions) { + this.actions = actions; + } + +} diff --git a/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/FileNameInfo.java b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/FileNameInfo.java new file mode 100644 index 0000000..0f366c3 --- /dev/null +++ b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/FileNameInfo.java @@ -0,0 +1,114 @@ +package com.syncfusion.tomcat; + +import java.util.ArrayList; +import com.syncfusion.ej2.wordprocessor.ActionInfo; +import com.syncfusion.tomcat.controller.DocumentEditorHub; + +public class FileNameInfo { + + private int fileIndex; + private String fileName; + + public FileNameInfo(int index, String fileName) { + this.setFileIndex(index); + this.setFileName(fileName); +// if (DocumentEditorHub.roomList.containsKey(fileName)) { +// ArrayList users = DocumentEditorHub.roomList.get(fileName); +// for (ActionInfo user : users) { +// activeUsers.add(constructInitials(user.getCurrentUser())); +// } +// } + } + + public String constructInitials(String authorName) { + String[] splittedName = authorName.split(" "); + StringBuilder initials = new StringBuilder(); + for (String namePart : splittedName) { + if (namePart.length() > 0 && !namePart.isEmpty()) { + initials.append(namePart.charAt(0)); + } + } + return initials.toString(); + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public int getFileIndex() { + return fileIndex; + } + + public void setFileIndex(int fileIndex) { + this.fileIndex = fileIndex; + } + + private String documentName; + private String createdOn; + private String sharedWith; + private String documentID; + private String sharedBy; + private String owner; + private ArrayList activeUsers = new ArrayList<>(); + + public String getDocumentName() { + return documentName; + } + + public void setDocumentName(String documentName) { + this.documentName = documentName; + } + + public String getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(String createdOn) { + this.createdOn = createdOn; + } + + public String getSharedWith() { + return sharedWith; + } + + public void setSharedWith(String sharedWith) { + this.sharedWith = sharedWith; + } + + public String getDocumentID() { + return documentID; + } + + public void setDocumentID(String documentID) { + this.documentID = documentID; + } + + public String getSharedBy() { + return sharedBy; + } + + public void setSharedBy(String sharedBy) { + this.sharedBy = sharedBy; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public ArrayList getActiveUsers() { + return activeUsers; + } + + public void setActiveUsers(ArrayList activeUsers) { + this.activeUsers = activeUsers; + } + +} \ No newline at end of file diff --git a/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/FilesPathInfo.java b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/FilesPathInfo.java new file mode 100644 index 0000000..0001e5d --- /dev/null +++ b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/FilesPathInfo.java @@ -0,0 +1,22 @@ +package com.syncfusion.tomcat; + +public class FilesPathInfo { + private String fileName; + private String roomName; + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public String getRoomName() { + return roomName; + } + + public void setRoomName(String roomName) { + this.roomName = roomName; + } +} diff --git a/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/SaveInfo.java b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/SaveInfo.java new file mode 100644 index 0000000..a501780 --- /dev/null +++ b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/SaveInfo.java @@ -0,0 +1,45 @@ +package com.syncfusion.tomcat; + +import com.syncfusion.ej2.wordprocessor.ActionInfo; +import java.util.List; + +public class SaveInfo { + private String roomName; + private List actions; + private String userId; + private int version; + private boolean partialSave; + + public void setVersion(int version) { + this.version = version; + } + + public void setActions(List clearedOperations) { + this.actions = clearedOperations; + } + + public void setPartialSave(boolean partialSave) { + this.partialSave = partialSave; + } + + public void setUserID(String userID) { + this.userId = userID; + } + + public void setRoomName(String roomName2) { + this.roomName = roomName2; + } + + public List getActions() { + return actions; + } + + public String getRoomName() { + return roomName; + } + + public Boolean getPartialSave() { + return partialSave; + } + +} diff --git a/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/TomcatApplication.java b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/TomcatApplication.java new file mode 100644 index 0000000..3d7f2bf --- /dev/null +++ b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/TomcatApplication.java @@ -0,0 +1,196 @@ +package com.syncfusion.tomcat; + +import java.util.ArrayList; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; +import org.springframework.core.io.Resource; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import com.syncfusion.ej2.wordprocessor.WordProcessorHelper; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.syncfusion.ej2.wordprocessor.FormatType; + +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Response; +import software.amazon.awssdk.services.s3.model.S3Object; + +@SpringBootApplication(exclude = { SecurityAutoConfiguration.class }) +@RestController +@EnableAsync +@EnableScheduling +public class TomcatApplication extends SpringBootServletInitializer { + @Value("${spring.datasource.accesskey}") + private String datasourceAccessKey; + @Value("${spring.datasource.secretkey}") + private String datasourceSecretKey; + @Value("${spring.datasource.bucketname}") + private String datasourceBucketName; + @Value("${spring.datasource.regionname}") + private String datasourceRegionName; + + @Value("classpath:static/files/*") + private Resource[] resources; + + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + return application.sources(TomcatApplication.class); + } + + public static void main(String[] args) { + SpringApplication.run(TomcatApplication.class, args); + } + + @CrossOrigin + @RequestMapping(value = "/") + public String hello() { + return "Hello From Syncfusion Document Editor Java Service"; + } + + @CrossOrigin + @RequestMapping(value = "/test") + public String test() { + System.out.println("==== in test ===="); + return "{\"sections\":[{\"blocks\":[{\"inlines\":[{\"texdocNamet\":\"Hello World\"}]}]}]}"; + } + + @CrossOrigin + @RequestMapping(value = "/api/wordeditor/Import") + public String uploadFile(@RequestParam("files") MultipartFile file) throws Exception { + try { + return WordProcessorHelper.load(file.getInputStream(), FormatType.Docx); + } catch (Exception e) { + e.printStackTrace(); + return "{\"sections\":[{\"blocks\":[{\"inlines\":[{\"text\":" + e.getMessage() + "}]}]}]}"; + } + } + + @CrossOrigin + @RequestMapping(value = "/api/wordeditor/RestrictEditing") + public String[] restrictEditing(@RequestBody CustomRestrictParameter param) throws Exception { + if (param.passwordBase64 == "" && param.passwordBase64 == null) + return null; + return WordProcessorHelper.computeHash(param.passwordBase64, param.saltBase64, param.spinCount); + } + + @CrossOrigin + @RequestMapping(value = "/api/wordeditor/SystemClipboard") + public String systemClipboard(@RequestBody CustomParameter param) { + if (param.content != null && param.content != "") { + try { + return WordProcessorHelper.loadString(param.content, GetFormatType(param.type.toLowerCase())); + } catch (Exception e) { + return ""; + } + } + return ""; + } + + static FormatType GetFormatType(String format) { + switch (format) { + case ".dotx": + case ".docx": + case ".docm": + case ".dotm": + return FormatType.Docx; + case ".dot": + case ".doc": + return FormatType.Doc; + case ".rtf": + return FormatType.Rtf; + case ".txt": + return FormatType.Txt; + case ".xml": + return FormatType.WordML; + case ".html": + return FormatType.Html; + default: + return FormatType.Docx; + } + } + + + @CrossOrigin(origins = "*", allowedHeaders = "*") + @GetMapping("/api/wordeditor/GetDataSource") + public String GetDataSource() throws Exception { + ArrayList files = GetFilesInfo(); + ArrayList dataSource = new ArrayList(); + for (int i = 0; i < files.size(); i++) { + dataSource.add(new FileNameInfo(i + 1, files.get(i).getFileName())); + } + GsonBuilder gsonBu = new GsonBuilder(); + Gson gson = gsonBu.disableHtmlEscaping().create(); + return gson.toJson(dataSource); + } + + @CrossOrigin(origins = "*", allowedHeaders = "*") + @GetMapping("/api/wordeditor/GetDataSourceS3") + public String GetDataSourceS3() throws Exception { + int i=0; + ArrayList dataSource = new ArrayList(); + String bucketName = datasourceBucketName; + + // Create an S3 client + S3Client s3Client = S3Client.builder() + .region(Region.US_EAST_1) // Change to your bucket's region + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create(datasourceAccessKey,datasourceSecretKey) + )) + .build(); + ListObjectsV2Request listObjectsReq = ListObjectsV2Request.builder() + .bucket(bucketName) + .maxKeys(10) // Optional: limit the number of results + .build(); + ListObjectsV2Response listObjectsRes; + do { + listObjectsRes = s3Client.listObjectsV2(listObjectsReq); + + for (S3Object s3Object : listObjectsRes.contents()) { + System.out.println(" - " + s3Object.key() + " (size: " + s3Object.size() + " bytes)"); + dataSource.add(new FileNameInfo(i + 1, s3Object.key())); + } + + // Set continuation token for pagination + listObjectsReq = listObjectsReq.toBuilder() + .continuationToken(listObjectsRes.nextContinuationToken()) + .build(); + + } while (listObjectsRes.isTruncated()); + GsonBuilder gsonBu = new GsonBuilder(); + Gson gson = gsonBu.disableHtmlEscaping().create(); + return gson.toJson(dataSource); + } + + private ArrayList GetFilesInfo() throws Exception { + + ArrayList filesInfo = new ArrayList(); + try { + for (int i = 0; i < resources.length; i++) { + FilesPathInfo path = new FilesPathInfo(); + path.setFileName(resources[i].getFilename()); + filesInfo.add(path); + } + } catch (Exception e) { + throw new Exception("error", e); + } + return filesInfo; + } +} diff --git a/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/config/WebSocketConfig.java b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/config/WebSocketConfig.java new file mode 100644 index 0000000..73c2cdf --- /dev/null +++ b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/config/WebSocketConfig.java @@ -0,0 +1,22 @@ +package com.syncfusion.tomcat.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.*; + +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/ws").setAllowedOrigins("*").withSockJS(); + } + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.setApplicationDestinationPrefixes("/app"); + registry.enableSimpleBroker("/topic"); + } + +} \ No newline at end of file diff --git a/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/controller/DocumentEditorHub.java b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/controller/DocumentEditorHub.java new file mode 100644 index 0000000..ce1535d --- /dev/null +++ b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/controller/DocumentEditorHub.java @@ -0,0 +1,202 @@ +package com.syncfusion.tomcat.controller; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.event.EventListener; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.simp.SimpMessageHeaderAccessor; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.stereotype.Controller; +import org.springframework.web.socket.messaging.SessionDisconnectEvent; + +import com.syncfusion.tomcat.CollaborativeEditingHelper; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.JedisPubSub; +import redis.clients.jedis.exceptions.JedisConnectionException; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.syncfusion.ej2.wordprocessor.ActionInfo; + +@Controller +public class DocumentEditorHub { + + // Redis Configuration + @Value("${spring.datasource.redishost}") + private String REDIS_HOST; + @Value("${spring.datasource.redisport}") + private int REDIS_PORT; + @Value("${spring.datasource.redispassword") + private String REDIS_PASSWORD; + + public static SimpMessagingTemplate messagingTemplate; + private static final int MAX_RETRIES = 5; + private static final long RETRY_INTERVAL_MS = 1000; + static ObjectMapper mapper = new ObjectMapper(); + + @Autowired + public DocumentEditorHub(SimpMessagingTemplate messagingTemplate) { + DocumentEditorHub.messagingTemplate = messagingTemplate; + } + + @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 additionalHeaders = new HashMap<>(); + additionalHeaders.put("action", "connectionId"); + MessageHeaders headers = new MessageHeaders(additionalHeaders); + // send the connection Id to the client + broadcastToRoom(docName, info, headers); + JedisPoolConfig poolConfig = new JedisPoolConfig(); + poolConfig.setMaxTotal(50); + JedisPool jedisPool = new JedisPool(poolConfig, REDIS_HOST, REDIS_PORT); + try (Jedis jedis = RedisSubscriber.getJedis()) { + // to maintain the session id with its corresponding ActionInfo details. + jedis.hset("documentMap", connectionId, documentName); + // add the user details to the Redis cache + String openedDocName = docName + CollaborativeEditingHelper.userInfoSuffix; + jedis.rpush(openedDocName, mapper.writeValueAsString(info)); + // Subscribe to the room, so that all users can get the JOIN/LEAVE notification + joinLeaveUsersubscribe(openedDocName); + // publish the user list to the redis + jedis.publish(openedDocName, "JOIN|" + connectionId); + + } catch (JedisConnectionException e) { + System.out.println(e); + } + } + + @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); + } + } + + private void joinLeaveUsersubscribe(String openedDocName) { + new Thread(() -> { + try (Jedis jedis = RedisSubscriber.getJedis()) { + jedis.subscribe(new JedisPubSub() { + @Override + public void onMessage(String channel, String message) { + String[] parts = message.split("\\|"); + if (parts.length == 2) { + String eventType = parts[0]; + String sessionId = parts[1]; + notifyUsers(channel, eventType, sessionId); + } + } + }, openedDocName); + } catch (JedisConnectionException e) { + System.out.println(e); + } + }).start(); + } + + public void notifyUsers(String docName, String eventType, String sessionId) { + try (Jedis jedis = RedisSubscriber.getJedis()) { + if ("JOIN".equals(eventType)) { + HashMap addUser = new HashMap<>(); + addUser.put("action", "addUser"); + MessageHeaders addUserheaders = new MessageHeaders(addUser); + // get the list of users from Redis + String type = jedis.type(docName); + List userJsonStrings = jedis.lrange(docName, 0, -1); + System.out.println("userJsonStrings to join" + userJsonStrings); + ArrayList actionsList = new ArrayList<>(); + ObjectMapper mapper = new ObjectMapper(); + for (String userJson : userJsonStrings) { + try { + ActionInfo actionInfo = mapper.readValue(userJson, ActionInfo.class); + actionsList.add(actionInfo); + } catch (Exception e) { + System.err.println("Error parsing user information JSON: " + e.getMessage()); + } + } + // Broadcast the user list to all the users connected in that room + broadcastToRoom(docName, actionsList, addUserheaders); + } else if ("LEAVE".equals(eventType)) { + // get the user list from the redis + List userJsonStrings = jedis.lrange(docName, 0, -1); + System.out.println("userJsonStrings to leave" + userJsonStrings); + if (!userJsonStrings.isEmpty()) { + ObjectMapper mapper = new ObjectMapper(); + for (String userJson : userJsonStrings) { + ActionInfo action = null; + try { + action = mapper.readValue(userJson, ActionInfo.class); + } catch (JsonMappingException e) { + e.printStackTrace(); + } catch (JsonProcessingException e) { + e.printStackTrace(); + } + if (action.getConnectionId().equals(sessionId)) { + // Remove the user from the user list + jedis.srem(docName, userJson); + HashMap removeUser = new HashMap<>(); + removeUser.put("action", "removeUser"); + MessageHeaders removeUserheaders = new MessageHeaders(removeUser); + // Broadcast the removal notification to all users in the document + broadcastToRoom(docName, action, removeUserheaders); + // Remove the session ID from the session-document mapping + jedis.hdel("documentMap", sessionId); + break; + } + } + } else { + System.out.println("No users found in the document."); + } + if (userJsonStrings.isEmpty()) { + jedis.del(docName); + } + } + + } catch (JedisConnectionException e) { + System.out.println(e); + } + + } + + public static void broadcastToRoom(String roomName, Object payload, MessageHeaders headers) { + messagingTemplate.convertAndSend("/topic/public/" + roomName, MessageBuilder.createMessage(payload, headers)); + } + + public static void publishToRedis(String roomName, Object payload) throws JsonProcessingException { + int retries = 0; + while (retries < MAX_RETRIES) { + try (Jedis jedis = RedisSubscriber.getJedis()) { + + jedis.publish("collaborativedtiting", mapper.writeValueAsString(payload)); + System.out.println("Message published to Redis" + mapper.writeValueAsString(payload)); + break; + } catch (JedisConnectionException e) { + retries++; + System.out.println("Connection failed. Retrying in " + RETRY_INTERVAL_MS + " milliseconds..."); + try { + Thread.sleep(RETRY_INTERVAL_MS); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } +} \ No newline at end of file diff --git a/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/controller/RedisSubscriber.java b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/controller/RedisSubscriber.java new file mode 100644 index 0000000..2020744 --- /dev/null +++ b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/controller/RedisSubscriber.java @@ -0,0 +1,93 @@ +package com.syncfusion.tomcat.controller; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + +import javax.annotation.PostConstruct; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.messaging.MessageHeaders; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.syncfusion.ej2.wordprocessor.ActionInfo; + +import redis.clients.jedis.HostAndPort; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.JedisPubSub; +import redis.clients.jedis.exceptions.JedisConnectionException; + +@Component +public class RedisSubscriber { + public static JedisPool jedisPool; + + + // Redis Configuration + @Value("${spring.datasource.redishost}") + private String REDIS_HOST; + @Value("${spring.datasource.redisport}") + private int REDIS_PORT; + @Value("${spring.datasource.redispassword}") + private String REDIS_PASSWORD; + @Value("${spring.datasource.redisssl}") + private boolean REDISSSL; + + @Autowired + @PostConstruct + @Bean + public void subscribeToInstanceChannel() { + String channel = "collaborativedtiting"; + new Thread(() -> { + JedisPoolConfig poolConfig = new JedisPoolConfig(); + poolConfig.setMaxTotal(100); + poolConfig.setMaxIdle(20); + poolConfig.setMinIdle(10); + poolConfig.setTestWhileIdle(true); + jedisPool = new JedisPool(poolConfig, REDIS_HOST, REDIS_PORT,50000,REDIS_PASSWORD,REDISSSL); + try (Jedis jedis = RedisSubscriber.getJedis()) { + jedis.subscribe(new JedisPubSub() { + @Override + public void onMessage(String channel, String message) { + System.out.println("Received message from channel " + channel + ": " + message); + ObjectMapper objectMapper = new ObjectMapper(); + try { + ActionInfo action = objectMapper.readValue(message, ActionInfo.class); + HashMap updateAction = new HashMap<>(); + updateAction.put("action", "updateAction"); + MessageHeaders updateActionheaders = new MessageHeaders(updateAction); + 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 ....."); + e.printStackTrace(); + } + finally { + if (jedisPool != null) { + jedisPool.close(); // Ensure pool is closed when done + } + } + }).start(); + } + + public static Jedis getJedis() { + // TODO Auto-generated method stub + return jedisPool.getResource(); + } +} diff --git a/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/controller/WebSocketEventListener.java b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/controller/WebSocketEventListener.java new file mode 100644 index 0000000..3bfbd8c --- /dev/null +++ b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/controller/WebSocketEventListener.java @@ -0,0 +1,25 @@ +package com.syncfusion.tomcat.controller; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.event.EventListener; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.messaging.SessionConnectedEvent; + + +@Component +public class WebSocketEventListener { + + private static final Logger logger = LoggerFactory.getLogger(WebSocketEventListener.class); + + @EventListener + public void handleWebSocketConnectListener(SessionConnectedEvent event) { + logger.info("Received a new web socket connection"); + StompHeaderAccessor headers = StompHeaderAccessor.wrap(event.getMessage()); + String sessionId = headers.getSessionId(); + // messagingTemplate.convertAndSend("/topic/session-id", sessionId); + } + + +} diff --git a/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/others/BackgroundQueueImpl.java b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/others/BackgroundQueueImpl.java new file mode 100644 index 0000000..3bfa6d3 --- /dev/null +++ b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/others/BackgroundQueueImpl.java @@ -0,0 +1,163 @@ +//package com.syncfusion.tomcat; +// +//import java.io.ByteArrayOutputStream; +//import java.io.FileOutputStream; +//import java.util.ArrayList; +//import java.util.concurrent.BlockingQueue; +//import java.util.concurrent.CompletableFuture; +//import java.util.concurrent.ExecutorService; +//import java.util.concurrent.Executors; +//import java.util.concurrent.LinkedBlockingQueue; +// +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.context.annotation.Lazy; +//import org.springframework.scheduling.annotation.Async; +//import org.springframework.stereotype.Component; +// +//import com.syncfusion.docio.WordDocument; +//import com.syncfusion.ej2.wordprocessor.ActionInfo; +//import com.syncfusion.ej2.wordprocessor.CollaborativeEditingHandler; +//import com.syncfusion.ej2.wordprocessor.WordProcessorHelper; +//import com.syncfusion.tomcat.SaveInfo; +//import com.syncfusion.tomcat.controller.RedisSubscriber; +// +//import redis.clients.jedis.Jedis; +// +//@Component +//class BackgroundQueueImpl implements IBackgroundQueue { +// +// public final BlockingQueue queue; +// private final ExecutorService executorService; +// private static boolean isRunning = false; +//// private int capacity=1; +// +// public BackgroundQueueImpl() { +// // Initialize the queue with a specified capacity +// this.queue = new LinkedBlockingQueue<>(); +// this.executorService = Executors.newFixedThreadPool(4); +// } +// +// @Override +// public CompletableFuture queueBackgroundWorkItemAsync(SaveInfo workItem) { +// if (workItem == null) { +// throw new IllegalArgumentException("workItem cannot be null"); +// } +// +// return CompletableFuture.runAsync(() -> { +// try { +// queue.put(workItem); // Blocking operation if the queue is full +// if (!queue.isEmpty()) { +// start(); // Start processing if queue contains element +// } +// } catch (InterruptedException e) { +// Thread.currentThread().interrupt(); // Restore interrupted status +// throw new RuntimeException("Failed to enqueue work item", e); +// } +// }, executorService); +// } +// +// @Override +// public CompletableFuture dequeueAsync() { +// +// return CompletableFuture.supplyAsync(() -> { +// try { +// Thread.sleep(5000); +// return queue.take(); // Blocking operation if the queue is empty +// } catch (InterruptedException e) { +// e.printStackTrace(); +// } +// return null; +// }); +// } +// +// public void start() { +// if (isRunning) { +// throw new IllegalStateException("Service is already running"); +// } +// try { +// executorService.submit(() -> backgroundProcessing()); +// isRunning = true; +// } catch (Exception ex) { +// ex.printStackTrace(); +// } +// } +// +// @Async +// public void backgroundProcessing() { +// while (true) { +// try { +// SaveInfo workItem = dequeueAsync().get(); +// applyOperationsToSourceDocument(workItem); +// clearRecordsFromRedisCache(workItem); +// } catch (Exception ex) { +// ex.printStackTrace(); +// } +// } +// +// } +// +// private void applyOperationsToSourceDocument(SaveInfo workItem) { +// +// try { +// ArrayList actions = (ArrayList) workItem.getActions(); +// for (ActionInfo action : actions) { +// if (!action.isTransformed()) { +// CollaborativeEditingHandler.transformOperation(action, actions); +// } +// } +// +// ClassLoader classLoader = getClass().getClassLoader(); +// WordProcessorHelper document = WordProcessorHelper +// .load(classLoader.getResourceAsStream("static/files/" + workItem.getRoomName()), true); +// CollaborativeEditingHandler handler = new CollaborativeEditingHandler(document); +// +// if (actions != null && actions.size() > 0) { +// for (ActionInfo info : actions) { +// handler.updateAction(info); +// } +// ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); +// WordDocument doc = WordProcessorHelper.save(WordProcessorHelper.serialize(handler.getDocument())); +// doc.save(outputStream, com.syncfusion.docio.FormatType.Docx); +// +// byte[] data = outputStream.toByteArray(); +// +// String currentDir = System.getProperty("user.dir") + "/src/main/resources/static/files"; +// try (FileOutputStream fos = new FileOutputStream(currentDir + workItem.getRoomName())) { +// // Write the byte array to the file +// fos.write(data); +// fos.close(); +// } catch (Exception ex) { +// ex.printStackTrace(); +// } +// outputStream.close(); +// } +// } catch (Exception e) { +// // TODO Auto-generated catch block +// e.printStackTrace(); +// } +// } +// +// private void clearRecordsFromRedisCache(SaveInfo workItem) { +// Boolean partialSave = workItem.getPartialSave(); +// String roomName = workItem.getRoomName(); +// try (Jedis jedis = RedisSubscriber.getJedis()) { +// if (!partialSave) { +// jedis.del(roomName); +// jedis.del(roomName + CollaborativeEditingHelper.revisionInfoSuffix); +// jedis.del(roomName + CollaborativeEditingHelper.versionInfoSuffix); +// } +// jedis.del(roomName + CollaborativeEditingHelper.actionsToRemoveSuffix); +// } +// } +// +// public void stop() { +// if (isRunning) { +// executorService.shutdown(); +// isRunning = false; +// } +// } +// +// public void shutDown() { +// executorService.shutdown(); +// } +//} diff --git a/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/others/IBackgroundQueue.java b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/others/IBackgroundQueue.java new file mode 100644 index 0000000..50b708c --- /dev/null +++ b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/others/IBackgroundQueue.java @@ -0,0 +1,88 @@ +//package com.syncfusion.tomcat; +// +//import java.util.concurrent.CompletableFuture; +//import com.syncfusion.tomcat.SaveInfo; +// +//public interface IBackgroundQueue { +// CompletableFuture queueBackgroundWorkItemAsync(SaveInfo workItem); +// +// CompletableFuture dequeueAsync(); +//} +// +////@Component +////abstract class BackgroundQueue implements IBackgroundQueue{ +//// @Override +//// public void queueBackgroundWorkItemAsync(SaveInfo message){ +//// try { +//// BlockingQueue queue = new LinkedBlockingQueue<>(); +//// queue .put(message); // Add workItem to the queue (blocks if queue is full) +//// } catch (InterruptedException e) { +//// System.out.println(e); +//// } +//// } +////} +// +////@Component +////class BackgroundQueueImpl implements IBackgroundQueue { +//// +//// public final BlockingQueue queue; +//// private final ExecutorService executorService; +//// +//// @Autowired +//// QueuedHostedService queuedHostedService; +//// +//// private int capacity=1; +//// +//// public BackgroundQueueImpl() { +//// // Initialize the queue with a specified capacity +//// this.queue = new LinkedBlockingQueue<>(capacity); +//// this.executorService=Executors.newFixedThreadPool(4); +//// } +//// +//// @Override +//// public CompletableFuture queueBackgroundWorkItemAsync(SaveInfo workItem) { +//// if (workItem == null) { +//// throw new IllegalArgumentException("workItem cannot be null"); +//// } +//// +//// return CompletableFuture.runAsync(() -> { +//// try { +//// queue.put(workItem); // Blocking operation if the queue is full +//// if (!queue.isEmpty() ) { +//// if (queuedHostedService != null) { +//// queuedHostedService.start(); // Start processing if queue is full +//// } +//// synchronized (queue) { +//// System.out.println("Queue contents:"); +//// for (SaveInfo item : queue) { +//// System.out.println(item); +//// } +//// } +//// } +//// } catch (InterruptedException e) { +//// Thread.currentThread().interrupt(); // Restore interrupted status +//// throw new RuntimeException("Failed to enqueue work item", e); +//// } +//// },executorService); +//// } +//// +//// @Override +//// public CompletableFuture dequeueAsync() { +//// +//// return CompletableFuture.supplyAsync(() -> { +//// try { +//// Thread.sleep(5000); +//// return queue.take(); // Blocking operation if the queue is empty +//// } catch (InterruptedException e) { +//// e.printStackTrace(); +////// Thread.currentThread().interrupt(); // Restore interrupted status +////// throw new RuntimeException("Failed to dequeue work item", e); +//// } +//// return null; +//// }); +//// } +//// +//// public void shutDown() { +//// executorService.shutdown(); +//// } +////} diff --git a/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/others/IQueuedHostedService.java b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/others/IQueuedHostedService.java new file mode 100644 index 0000000..2024be3 --- /dev/null +++ b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/others/IQueuedHostedService.java @@ -0,0 +1,8 @@ +//package com.syncfusion.tomcat; +// +//public interface IQueuedHostedService { +// +// void backgroundProcessing(); +// void start(); +// +//} diff --git a/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/others/QueuedHostedService.java b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/others/QueuedHostedService.java new file mode 100644 index 0000000..badfbfe --- /dev/null +++ b/Server side with distributed cache/Java/Using Redis/src/main/java/com/syncfusion/tomcat/others/QueuedHostedService.java @@ -0,0 +1,127 @@ +//package com.syncfusion.tomcat; +// +//import java.io.ByteArrayOutputStream; +//import java.io.FileOutputStream; +//import java.util.ArrayList; +//import java.util.List; +//import java.util.concurrent.CompletableFuture; +//import java.util.concurrent.ExecutorService; +//import java.util.concurrent.Executors; +//import java.util.concurrent.atomic.AtomicBoolean; +// +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.scheduling.annotation.Async; +//import org.springframework.stereotype.Service; +// +//import com.syncfusion.docio.WordDocument; +//import com.syncfusion.ej2.wordprocessor.ActionInfo; +//import com.syncfusion.ej2.wordprocessor.CollaborativeEditingHandler; +//import com.syncfusion.ej2.wordprocessor.WordProcessorHelper; +//import com.syncfusion.tomcat.CollaborativeEditingHelper.SaveInfo; +//import com.syncfusion.tomcat.controller.RedisSubscriber; +// +//import redis.clients.jedis.Jedis; +// +//@Service +//public class QueuedHostedService implements IQueuedHostedService { +// +// +// private IBackgroundQueue queue; +// private static boolean isRunning=false; +// private final ExecutorService executorService=Executors.newSingleThreadExecutor(); +// +// @Autowired +// public QueuedHostedService(IBackgroundQueue iqueue) { +// queue=iqueue; +// +// } +// +// public void start() { +// if(isRunning) { +// throw new IllegalStateException("Service is already running"); +// } +// try { +// executorService.submit(()->backgroundProcessing()); +// isRunning=true; +// } +// catch(Exception ex) { +// ex.printStackTrace(); +// } +// } +// +// public void stop() { +// if(isRunning) { +// executorService.shutdown(); +// isRunning=false; +// } +// } +// +// @Async +// public void backgroundProcessing() { +// while(true) { +// try { +// SaveInfo workItem=queue.dequeueAsync().get(); +// applyOperationsToSourceDocument(workItem); +// clearRecordsFromRedisCache(workItem); +// } +// catch (Exception ex){ +// ex.printStackTrace(); +// } +// } +// +// } +// +// private void applyOperationsToSourceDocument(SaveInfo workItem) { +// ClassLoader classLoader = getClass().getClassLoader(); +// try { +// WordProcessorHelper document = WordProcessorHelper +// .load(classLoader.getResourceAsStream("static/files/" + workItem.getRoomName()), true); +// CollaborativeEditingHandler handler= new CollaborativeEditingHandler(document); +// +// ArrayList actions = (ArrayList) workItem.getAction(); +// if(actions != null && actions.size()>0) { +// for(ActionInfo action : actions){ +// if(!action.isTransformed()) { +// CollaborativeEditingHandler.transformOperation(action, actions); +// } +// } +// +// for(int i=0;i + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/Server side with distributed cache/Java/Using Redis/src/main/webapp/META-INF/MANIFEST.MF b/Server side with distributed cache/Java/Using Redis/src/main/webapp/META-INF/MANIFEST.MF new file mode 100644 index 0000000..254272e --- /dev/null +++ b/Server side with distributed cache/Java/Using Redis/src/main/webapp/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Class-Path: + diff --git a/Server side with distributed cache/Java/Using Redis/src/test/java/com/syncfusion/tomcat/TomcatApplicationTests.java b/Server side with distributed cache/Java/Using Redis/src/test/java/com/syncfusion/tomcat/TomcatApplicationTests.java new file mode 100644 index 0000000..19b6fac --- /dev/null +++ b/Server side with distributed cache/Java/Using Redis/src/test/java/com/syncfusion/tomcat/TomcatApplicationTests.java @@ -0,0 +1,13 @@ +package com.syncfusion.tomcat; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class TomcatApplicationTests { + + @Test + void contextLoads() { + } + +}