From 70b2f578fec73edddca78286f849c694af03b189 Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Tue, 30 Sep 2025 12:41:09 -0600 Subject: [PATCH 01/20] build(deps): use ditto 5.0.0-preview.3 in kmp --- kotlin-multiplatform/composeApp/build.gradle.kts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/kotlin-multiplatform/composeApp/build.gradle.kts b/kotlin-multiplatform/composeApp/build.gradle.kts index 7d2c6ad8a..1698953c4 100644 --- a/kotlin-multiplatform/composeApp/build.gradle.kts +++ b/kotlin-multiplatform/composeApp/build.gradle.kts @@ -45,7 +45,7 @@ kotlin { implementation(libs.koin.android) } commonMain.dependencies { - implementation("com.ditto:ditto-kotlin:5.0.0-preview.1") + implementation("com.ditto:ditto-kotlin:5.0.0-preview.3") implementation(compose.runtime) implementation(compose.foundation) @@ -83,26 +83,26 @@ kotlin { implementation(libs.kotlinx.coroutines.swing) // This will include binaries for all the supported platforms and architectures - implementation("com.ditto:ditto-binaries:5.0.0-preview.1") + implementation("com.ditto:ditto-binaries:5.0.0-preview.3") // To reduce your module artifact's size, consider including just the necessary platforms and architectures /* // macOS Apple Silicon - implementation("com.ditto:ditto-binaries:5.0.0-preview.1") { + implementation("com.ditto:ditto-binaries:5.0.0-preview.3") { capabilities { requireCapability("com.ditto:ditto-binaries-macos-arm64") } } // Windows x86_64 - implementation("com.ditto:ditto-binaries:5.0.0-preview.1") { + implementation("com.ditto:ditto-binaries:5.0.0-preview.3") { capabilities { requireCapability("com.ditto:ditto-binaries-windows-x64") } } // Linux x86_64 - implementation("com.ditto:ditto-binaries:5.0.0-preview.1") { + implementation("com.ditto:ditto-binaries:5.0.0-preview.3") { capabilities { requireCapability("com.ditto:ditto-binaries-linux-x64") } From 80b2251d9a3c5ffa32aafe8aabb5d3e6c8a6fb34 Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Thu, 2 Oct 2025 09:19:37 -0600 Subject: [PATCH 02/20] apply patch --- java-spring/java-spring-maven/.gitignore | 4 + java-spring/java-spring-maven/mvnw | 295 ++++++++++++++++++ java-spring/java-spring-maven/mvnw.cmd | 189 +++++++++++ java-spring/java-spring-maven/pom.xml | 125 ++++++++ .../quickstart/service/DittoService.java | 40 ++- .../quickstart/service/DittoTaskService.java | 4 +- 6 files changed, 640 insertions(+), 17 deletions(-) create mode 100644 java-spring/java-spring-maven/.gitignore create mode 100755 java-spring/java-spring-maven/mvnw create mode 100644 java-spring/java-spring-maven/mvnw.cmd create mode 100644 java-spring/java-spring-maven/pom.xml diff --git a/java-spring/java-spring-maven/.gitignore b/java-spring/java-spring-maven/.gitignore new file mode 100644 index 000000000..40d8b2587 --- /dev/null +++ b/java-spring/java-spring-maven/.gitignore @@ -0,0 +1,4 @@ +.kotlin/ +bin/ +build/ +target/ diff --git a/java-spring/java-spring-maven/mvnw b/java-spring/java-spring-maven/mvnw new file mode 100755 index 000000000..bd8896bf2 --- /dev/null +++ b/java-spring/java-spring-maven/mvnw @@ -0,0 +1,295 @@ +#!/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 +# +# http://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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + 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" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/java-spring/java-spring-maven/mvnw.cmd b/java-spring/java-spring-maven/mvnw.cmd new file mode 100644 index 000000000..92450f932 --- /dev/null +++ b/java-spring/java-spring-maven/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@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 http://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 Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/java-spring/java-spring-maven/pom.xml b/java-spring/java-spring-maven/pom.xml new file mode 100644 index 000000000..a8ed7ec43 --- /dev/null +++ b/java-spring/java-spring-maven/pom.xml @@ -0,0 +1,125 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.4.10 + + + com.example + demo + 0.0.1-SNAPSHOT + demo + Demo project for Spring Boot + + + + + + + + + + + + + + + 17 + + + + + com.ditto + ditto-java + 5.0.0-preview.3 + + + + + com.ditto + ditto-binaries + 5.0.0-preview.3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-webflux + + + + com.h2database + h2 + runtime + + + org.springframework.boot + spring-boot-starter-test + test + + + io.projectreactor + reactor-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/java-spring/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java b/java-spring/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java index 97c67c022..5f0b20478 100644 --- a/java-spring/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java +++ b/java-spring/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java @@ -1,7 +1,7 @@ package com.ditto.example.spring.quickstart.service; import com.ditto.example.spring.quickstart.configuration.DittoConfigurationKeys; -import com.ditto.example.spring.quickstart.configuration.DittoSecretsConfiguration; +//import com.ditto.example.spring.quickstart.configuration.DittoSecretsConfiguration; import com.ditto.java.*; import com.ditto.java.serialization.DittoCborSerializable; import jakarta.annotation.Nonnull; @@ -43,29 +43,39 @@ public class DittoService implements DisposableBean { dittoDir.mkdirs(); /* - * Setup Ditto Identity + * Setup Ditto Config * https://docs.ditto.live/sdk/latest/install-guides/java#integrating-and-initializing */ - DittoIdentity identity = new DittoIdentity.OnlinePlayground( - DittoSecretsConfiguration.DITTO_APP_ID, - DittoSecretsConfiguration.DITTO_PLAYGROUND_TOKEN, - // This is required to be set to false to use the correct URLs - false, - DittoSecretsConfiguration.DITTO_AUTH_URL - ); - DittoConfig dittoConfig = new DittoConfig.Builder(dittoDir) - .identity(identity) +// DittoConfig dittoConfig = new DittoConfig.Builder(DittoSecretsConfiguration.DITTO_APP_ID) +// .serverConnect(DittoSecretsConfiguration.DITTO_AUTH_URL) +// .build(); + + DittoConfig dittoConfig = new DittoConfig.Builder("755e5ea1-25f2-42a8-af99-692c53ce7c34") + .serverConnect("https://755e5ea1-25f2-42a8-af99-692c53ce7c34.cloud-stg.ditto.live") .build(); - this.ditto = new Ditto(dittoConfig); + this.ditto = DittoFactory.create(dittoConfig); + + this.ditto.getAuth().setExpirationHandler((expiringDitto, _timeUntilExpiration) -> + expiringDitto.getAuth() +// .login( +// DittoSecretsConfiguration.DITTO_PLAYGROUND_TOKEN, +// DittoAuthenticationProvider.development() +// ).thenRun(() -> { }) + .login( + "a9bf9cf2-171a-44a3-bfd7-12a8d550d632", + DittoAuthenticationProvider.development() + ).thenRun(() -> { }) + ); this.ditto.setDeviceName("Spring Java"); this.ditto.updateTransportConfig(transportConfig -> { transportConfig.connect(connect -> { // Set the Ditto Websocket URL - connect.websocketUrls().add(DittoSecretsConfiguration.DITTO_WEBSOCKET_URL); +// connect.websocketUrls().add(DittoSecretsConfiguration.DITTO_WEBSOCKET_URL); + connect.websocketUrls().add("wss://755e5ea1-25f2-42a8-af99-692c53ce7c34.cloud-stg.ditto.live"); }); logger.info("Transport config: {}", transportConfig); @@ -109,7 +119,7 @@ private DittoAsyncCancellable observePeersPresence() { for (DittoPeer peer : graph.getRemotePeers()) { logger.info("Peer: {}", peer.getDeviceName()); for (DittoConnection connection : peer.getConnections()) { - logger.info("\t- {} {} {}", connection.getId(), connection.getConnectionType(), connection.getApproximateDistanceInMeters()); + logger.info("\t- {} {}", connection.getId(), connection.getConnectionType()); } } }); @@ -174,7 +184,7 @@ private void setSyncStateIntoDittoStore(boolean newState) throws DittoError { try { future.toCompletableFuture().join().close(); - } catch (IOException e) { + } catch (DittoError e) { throw new RuntimeException(e); } } diff --git a/java-spring/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java b/java-spring/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java index 687a6382b..90289e482 100644 --- a/java-spring/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java +++ b/java-spring/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java @@ -96,7 +96,7 @@ public void updateTask(@Nonnull String taskId, @Nonnull String newTitle) { @Nonnull public Flux> observeAll() { - final String selectQuery = "SELECT * FROM %s WHERE NOT deleted ORDER BY title ASC".formatted(TASKS_COLLECTION_NAME); + final String selectQuery = "SELECT * FROM %s WHERE NOT deleted".formatted(TASKS_COLLECTION_NAME); return Flux.create(emitter -> { Ditto ditto = dittoService.getDitto(); @@ -115,7 +115,7 @@ public Flux> observeAll() { } try { observer.close(); - } catch (IOException e) { + } catch (DittoError e) { throw new RuntimeException(e); } }); From b93cbf22d9a2b87875c307a467c909c7f2aa6d32 Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Thu, 2 Oct 2025 09:28:34 -0600 Subject: [PATCH 03/20] build(deps): upgrade to ditto 5.0.0-preview.3 in java-spring --- java-spring/build.gradle.kts | 12 ++++++------ kotlin-multiplatform/README.md | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/java-spring/build.gradle.kts b/java-spring/build.gradle.kts index d952a7e0e..4b81afcc5 100644 --- a/java-spring/build.gradle.kts +++ b/java-spring/build.gradle.kts @@ -31,29 +31,29 @@ spotbugs { dependencies { // ditto-java artifact includes the Java API for Ditto - implementation("com.ditto:ditto-java:5.0.0-preview.1") + implementation("com.ditto:ditto-java:5.0.0-preview.3") // This will include binaries for all the supported platforms and architectures - implementation("com.ditto:ditto-binaries:5.0.0-preview.1") + implementation("com.ditto:ditto-binaries:5.0.0-preview.3") // To reduce your module artifact's size, consider including just the necessary platforms and architectures /* // macOS Apple Silicon - implementation("com.ditto:ditto-binaries:5.0.0-preview.1") { + implementation("com.ditto:ditto-binaries:5.0.0-preview.3") { capabilities { requireCapability("com.ditto:ditto-binaries-macos-arm64") } } // Windows x86_64 - implementation("com.ditto:ditto-binaries:5.0.0-preview.1") { + implementation("com.ditto:ditto-binaries:5.0.0-preview.3") { capabilities { requireCapability("com.ditto:ditto-binaries-windows-x64") } } // Linux x86_64 - implementation("com.ditto:ditto-binaries:5.0.0-preview.1") { + implementation("com.ditto:ditto-binaries:5.0.0-preview.3") { capabilities { requireCapability("com.ditto:ditto-binaries-linux-x64") } @@ -68,7 +68,7 @@ dependencies { runtimeOnly("com.h2database:h2") testImplementation("org.springframework.boot:spring-boot-starter-test") testRuntimeOnly("org.junit.platform:junit-platform-launcher") - + // Selenium WebDriver for visual browser testing testImplementation("org.seleniumhq.selenium:selenium-java:4.11.0") testImplementation("io.github.bonigarcia:webdrivermanager:5.9.2") diff --git a/kotlin-multiplatform/README.md b/kotlin-multiplatform/README.md index 8bf4a5fac..dac8044c7 100644 --- a/kotlin-multiplatform/README.md +++ b/kotlin-multiplatform/README.md @@ -21,4 +21,4 @@ For more information, see - [Kotlin Multiplatform Install Guide](https://docs.di ## Additional Resources - [Kotlin Multiplatform Roadmap and Support Policy](https://docs.ditto.live/sdk/latest/install-guides/kotlin/multiplatform-roadmap) -- [API Reference](https://software.ditto.live/java/ditto-java/5.0.0-preview.1/api-reference/) +- [API Reference](https://software.ditto.live/java/ditto-java/5.0.0-preview.3/api-reference/) From 9043df51a7e7d871ff940ebf5de20377a9dc9b9f Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Thu, 2 Oct 2025 09:30:37 -0600 Subject: [PATCH 04/20] refactor: update java apis to use as*() deserialization methods --- .../spring/quickstart/service/DittoService.java | 4 +--- .../spring/quickstart/service/DittoTaskService.java | 10 +++++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/java-spring/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java b/java-spring/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java index 5f0b20478..3d3eb45db 100644 --- a/java-spring/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java +++ b/java-spring/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java @@ -1,7 +1,6 @@ package com.ditto.example.spring.quickstart.service; import com.ditto.example.spring.quickstart.configuration.DittoConfigurationKeys; -//import com.ditto.example.spring.quickstart.configuration.DittoSecretsConfiguration; import com.ditto.java.*; import com.ditto.java.serialization.DittoCborSerializable; import jakarta.annotation.Nonnull; @@ -15,7 +14,6 @@ import reactor.core.publisher.Sinks; import java.io.File; -import java.io.IOException; import java.util.List; import java.util.concurrent.CompletionStage; @@ -154,7 +152,7 @@ private DittoStoreObserver setupAndObserveSyncState() { if (!items.isEmpty()) { newSyncState = items.get(0).getValue() .get(DITTO_SYNC_STATE_ID) - .getBoolean(); + .asBoolean(); } if (newSyncState) { diff --git a/java-spring/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java b/java-spring/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java index 90289e482..c2e61f1e8 100644 --- a/java-spring/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java +++ b/java-spring/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java @@ -52,7 +52,7 @@ public void toggleTaskDone(@Nonnull String taskId) { .build() ).toCompletableFuture().join(); - boolean isDone = tasks.getItems().get(0).getValue().get("done").getBoolean(); + boolean isDone = tasks.getItems().get(0).getValue().get("done").asBoolean(); dittoService.getDitto().getStore().execute( "UPDATE %s SET done = :done WHERE _id = :taskId".formatted(TASKS_COLLECTION_NAME), @@ -128,10 +128,10 @@ public Flux> observeAll() { private Task itemToTask(@Nonnull DittoQueryResultItem item) { DittoCborSerializable.Dictionary value = item.getValue(); return new Task( - value.get("_id").getString(), - value.get("title").getString(), - value.get("done").getBoolean(), - value.get("deleted").getBoolean() + value.get("_id").asString(), + value.get("title").asString(), + value.get("done").asBoolean(), + value.get("deleted").asBoolean() ); } } From 72feebd8b89bce45e1debee6f4ecadb9be035b14 Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Thu, 2 Oct 2025 09:39:14 -0600 Subject: [PATCH 05/20] refactor: use DittoSecretsConfiguration --- .../quickstart/service/DittoService.java | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/java-spring/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java b/java-spring/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java index 3d3eb45db..97ffa691a 100644 --- a/java-spring/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java +++ b/java-spring/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java @@ -1,6 +1,7 @@ package com.ditto.example.spring.quickstart.service; import com.ditto.example.spring.quickstart.configuration.DittoConfigurationKeys; +import com.ditto.example.spring.quickstart.configuration.DittoSecretsConfiguration; import com.ditto.java.*; import com.ditto.java.serialization.DittoCborSerializable; import jakarta.annotation.Nonnull; @@ -45,24 +46,16 @@ public class DittoService implements DisposableBean { * https://docs.ditto.live/sdk/latest/install-guides/java#integrating-and-initializing */ -// DittoConfig dittoConfig = new DittoConfig.Builder(DittoSecretsConfiguration.DITTO_APP_ID) -// .serverConnect(DittoSecretsConfiguration.DITTO_AUTH_URL) -// .build(); - - DittoConfig dittoConfig = new DittoConfig.Builder("755e5ea1-25f2-42a8-af99-692c53ce7c34") - .serverConnect("https://755e5ea1-25f2-42a8-af99-692c53ce7c34.cloud-stg.ditto.live") + DittoConfig dittoConfig = new DittoConfig.Builder(DittoSecretsConfiguration.DITTO_APP_ID) + .serverConnect(DittoSecretsConfiguration.DITTO_AUTH_URL) .build(); this.ditto = DittoFactory.create(dittoConfig); this.ditto.getAuth().setExpirationHandler((expiringDitto, _timeUntilExpiration) -> expiringDitto.getAuth() -// .login( -// DittoSecretsConfiguration.DITTO_PLAYGROUND_TOKEN, -// DittoAuthenticationProvider.development() -// ).thenRun(() -> { }) .login( - "a9bf9cf2-171a-44a3-bfd7-12a8d550d632", + DittoSecretsConfiguration.DITTO_PLAYGROUND_TOKEN, DittoAuthenticationProvider.development() ).thenRun(() -> { }) ); @@ -72,8 +65,7 @@ public class DittoService implements DisposableBean { this.ditto.updateTransportConfig(transportConfig -> { transportConfig.connect(connect -> { // Set the Ditto Websocket URL -// connect.websocketUrls().add(DittoSecretsConfiguration.DITTO_WEBSOCKET_URL); - connect.websocketUrls().add("wss://755e5ea1-25f2-42a8-af99-692c53ce7c34.cloud-stg.ditto.live"); + connect.websocketUrls().add(DittoSecretsConfiguration.DITTO_WEBSOCKET_URL); }); logger.info("Transport config: {}", transportConfig); From 56895f54eba0d6a6c1c409038dde6ec18ac3d455 Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Thu, 2 Oct 2025 10:23:17 -0600 Subject: [PATCH 06/20] refactor: update APIs for v5 preview 3 --- .../quickstart/ditto/DittoManager.android.kt | 12 +-- .../ditto/quickstart/data/DittoCredentials.kt | 6 ++ .../com/ditto/quickstart/di/DittoModule.kt | 11 ++- .../ditto/quickstart/ditto/DittoManager.kt | 81 ++++++++++--------- .../quickstart/ditto/DittoManager.desktop.kt | 9 ++- .../quickstart/ditto/DittoManager.ios.kt | 11 +-- 6 files changed, 77 insertions(+), 53 deletions(-) create mode 100644 kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/data/DittoCredentials.kt diff --git a/kotlin-multiplatform/composeApp/src/androidMain/kotlin/com/ditto/quickstart/ditto/DittoManager.android.kt b/kotlin-multiplatform/composeApp/src/androidMain/kotlin/com/ditto/quickstart/ditto/DittoManager.android.kt index 8c3067448..a02da2bfc 100644 --- a/kotlin-multiplatform/composeApp/src/androidMain/kotlin/com/ditto/quickstart/ditto/DittoManager.android.kt +++ b/kotlin-multiplatform/composeApp/src/androidMain/kotlin/com/ditto/quickstart/ditto/DittoManager.android.kt @@ -1,11 +1,13 @@ package com.ditto.quickstart.ditto +import com.ditto.kotlin.Ditto import com.ditto.kotlin.DittoConfig -import com.ditto.kotlin.DittoIdentity +import com.ditto.kotlin.DittoFactory import com.ditto.quickstart.App -actual fun createDittoConfig(identity: DittoIdentity): DittoConfig = - DittoConfig( - identity = identity, - context = App.instance +actual fun createDitto(config: DittoConfig): Ditto = + DittoFactory.create( + context = App.instance, + config = config, ) + diff --git a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/data/DittoCredentials.kt b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/data/DittoCredentials.kt new file mode 100644 index 000000000..32398477b --- /dev/null +++ b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/data/DittoCredentials.kt @@ -0,0 +1,6 @@ +package com.ditto.quickstart.data + +data class DittoCredentials( + val appId: String, + val appToken: String +) diff --git a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/di/DittoModule.kt b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/di/DittoModule.kt index c6769f61a..2b82553b0 100644 --- a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/di/DittoModule.kt +++ b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/di/DittoModule.kt @@ -1,10 +1,19 @@ package com.ditto.quickstart.di +import com.ditto.quickstart.data.DittoCredentials import com.ditto.quickstart.ditto.DittoManager import org.koin.dsl.module fun dittoModule() = module { single { - DittoManager() + DittoCredentials( + appId = "da244a14-bb3d-435d-92b0-b4f667a1b004", + appToken = "161848a6-8a68-48d7-8b44-ecdf46648ca6" + ) + } + single { + DittoManager( + credentials = get(), + ) } } diff --git a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ditto/DittoManager.kt b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ditto/DittoManager.kt index 2a2ca322a..c34f85524 100644 --- a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ditto/DittoManager.kt +++ b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ditto/DittoManager.kt @@ -2,8 +2,8 @@ package com.ditto.quickstart.ditto import com.ditto.example.kotlin.quickstart.configuration.DittoSecretsConfiguration import com.ditto.kotlin.Ditto +import com.ditto.kotlin.DittoAuthenticationProvider import com.ditto.kotlin.DittoConfig -import com.ditto.kotlin.DittoIdentity import com.ditto.kotlin.DittoLog import com.ditto.kotlin.DittoLogLevel import com.ditto.kotlin.DittoLogger @@ -11,6 +11,7 @@ import com.ditto.kotlin.DittoQueryResult import com.ditto.kotlin.DittoSyncSubscription import com.ditto.kotlin.error.DittoError import com.ditto.kotlin.serialization.DittoCborSerializable +import com.ditto.quickstart.data.DittoCredentials import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO @@ -28,9 +29,11 @@ private const val TAG = "DittoManager" * Keeping this class inaccessible from the ViewModel, will prevent the abuse of ditto APIs like: * "ditto.sync", "ditto.transport", "ditto.store", etc * - * Because ditto also have a "database" component, it is fine to expose this class to a Repository. + * Because ditto also has a "database" component, it is fine to expose this class to a Repository. */ -class DittoManager { +class DittoManager( + val credentials: DittoCredentials +) { private val scope = CoroutineScope(SupervisorJob()) private var createJob: Job? = null private var closeJob: Job? = null @@ -42,26 +45,28 @@ class DittoManager { // SDKS-1294: Don't create Ditto in a scope using Dispatchers.IO createJob = scope.launch(Dispatchers.Default) { ditto = try { - val identity = DittoIdentity.OnlinePlayground( - appId = DittoSecretsConfiguration.DITTO_APP_ID, - token = DittoSecretsConfiguration.DITTO_PLAYGROUND_TOKEN, - // Cloud sync is intentionally disabled to avoid authentication issues in test environments. - // When enabled, Ditto Cloud Sync requires additional auth setup that causes certificate - // validation failures in BrowserStack. Disabling ensures sync occurs via WebSocket URLs only. - enableDittoCloudSync = false, - customAuthUrl = DittoSecretsConfiguration.DITTO_AUTH_URL, + val config = DittoConfig( + databaseId = credentials.appId, + connect = DittoConfig.Connect.Server( + url = "https://${credentials.appId}.cloud.ditto.live", + ), ) - val config = createDittoConfig(identity = identity) - DittoLogger.minimumLogLevel = DittoLogLevel.Verbose - Ditto(config = config).apply { - updateTransportConfig { config -> - config.connect.websocketUrls.add(DittoSecretsConfiguration.DITTO_WEBSOCKET_URL) + createDitto( + config = config + ).apply { + auth?.setExpirationHandler { ditto, secondsRemaining -> + // Authenticate when a token is expiring + val clientInfo = ditto.auth?.login( + token = credentials.appToken, + provider = DittoAuthenticationProvider.development(), + ) } } } catch (e: Throwable) { DittoLog.e(TAG, "Failed to create Ditto instance: $e") + e.printStackTrace() null } } @@ -74,6 +79,25 @@ class DittoManager { return ditto } + fun destroyDitto() { + closeJob = scope.launch(Dispatchers.IO) { + getDitto()?.stopSync() + getDitto()?.close() + ditto = null + } + } + + suspend fun startSync() { + val ditto = getDitto() ?: return + ditto.startSync() + } + + suspend fun stopSync() { + getDitto()?.stopSync() + } + + suspend fun isSyncing() = getDitto()?.isSyncActive == true + suspend fun executeDql( query: String, parameters: DittoCborSerializable.Dictionary = DittoCborSerializable.Dictionary() @@ -97,30 +121,11 @@ class DittoManager { suspend fun registerObserver( query: String, arguments: DittoCborSerializable.Dictionary? = null - ): Flow = requireNotNull(getDitto()).store.registerObserver( + ): Flow = requireNotNull(getDitto()).store.observe( query = query, arguments = arguments ) - suspend fun startSync() { - val ditto = getDitto() ?: return - ditto.startSync() - } - - suspend fun stopSync() { - getDitto()?.stopSync() - } - - suspend fun isSyncing() = getDitto()?.isSyncActive == true - - fun destroyDitto() { - closeJob = scope.launch(Dispatchers.IO) { - getDitto()?.stopSync() - getDitto()?.close() - ditto = null - } - } - private suspend fun waitForWorkInProgress() { createJob?.join() closeJob?.join() @@ -131,6 +136,4 @@ class DittoManager { * Defines how to create a Ditto Config in Multiplatform, and on each platform pass the required dependencies - for * example, on Android we require Context. */ -internal expect fun createDittoConfig( - identity: DittoIdentity, -): DittoConfig +internal expect fun createDitto(config: DittoConfig): Ditto diff --git a/kotlin-multiplatform/composeApp/src/desktopMain/kotlin/com/ditto/quickstart/ditto/DittoManager.desktop.kt b/kotlin-multiplatform/composeApp/src/desktopMain/kotlin/com/ditto/quickstart/ditto/DittoManager.desktop.kt index a5ecb377c..c39001f70 100644 --- a/kotlin-multiplatform/composeApp/src/desktopMain/kotlin/com/ditto/quickstart/ditto/DittoManager.desktop.kt +++ b/kotlin-multiplatform/composeApp/src/desktopMain/kotlin/com/ditto/quickstart/ditto/DittoManager.desktop.kt @@ -1,7 +1,10 @@ package com.ditto.quickstart.ditto +import com.ditto.kotlin.Ditto import com.ditto.kotlin.DittoConfig -import com.ditto.kotlin.DittoIdentity +import com.ditto.kotlin.DittoFactory -actual fun createDittoConfig(identity: DittoIdentity): DittoConfig = - DittoConfig(identity = identity) +actual fun createDitto(config: DittoConfig): Ditto = + DittoFactory.create( + config = config + ) diff --git a/kotlin-multiplatform/composeApp/src/iosMain/kotlin/com/ditto/quickstart/ditto/DittoManager.ios.kt b/kotlin-multiplatform/composeApp/src/iosMain/kotlin/com/ditto/quickstart/ditto/DittoManager.ios.kt index 34825a13c..c39001f70 100644 --- a/kotlin-multiplatform/composeApp/src/iosMain/kotlin/com/ditto/quickstart/ditto/DittoManager.ios.kt +++ b/kotlin-multiplatform/composeApp/src/iosMain/kotlin/com/ditto/quickstart/ditto/DittoManager.ios.kt @@ -1,9 +1,10 @@ package com.ditto.quickstart.ditto +import com.ditto.kotlin.Ditto import com.ditto.kotlin.DittoConfig -import com.ditto.kotlin.DittoIdentity - -actual fun createDittoConfig(identity: DittoIdentity): DittoConfig { - return DittoConfig(identity = identity) -} +import com.ditto.kotlin.DittoFactory +actual fun createDitto(config: DittoConfig): Ditto = + DittoFactory.create( + config = config + ) From e6bc58fe48f4ab87e7e9186e7ac3fae124dedd94 Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Mon, 6 Oct 2025 14:44:12 -0600 Subject: [PATCH 07/20] fix: add SUBSCRIPTION_QUERY_SELECT_TASKS with no ORDER BY --- .../com/ditto/quickstart/ditto/DittoManager.android.kt | 1 - .../quickstart/data/repository/DittoTaskRepository.kt | 8 ++++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/kotlin-multiplatform/composeApp/src/androidMain/kotlin/com/ditto/quickstart/ditto/DittoManager.android.kt b/kotlin-multiplatform/composeApp/src/androidMain/kotlin/com/ditto/quickstart/ditto/DittoManager.android.kt index a02da2bfc..2a30cd607 100644 --- a/kotlin-multiplatform/composeApp/src/androidMain/kotlin/com/ditto/quickstart/ditto/DittoManager.android.kt +++ b/kotlin-multiplatform/composeApp/src/androidMain/kotlin/com/ditto/quickstart/ditto/DittoManager.android.kt @@ -10,4 +10,3 @@ actual fun createDitto(config: DittoConfig): Ditto = context = App.instance, config = config, ) - diff --git a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/data/repository/DittoTaskRepository.kt b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/data/repository/DittoTaskRepository.kt index 29f5bb1cc..464fce7d1 100644 --- a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/data/repository/DittoTaskRepository.kt +++ b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/data/repository/DittoTaskRepository.kt @@ -24,12 +24,16 @@ import com.ditto.quickstart.data.dto.UpdateTaskDoneDto import com.ditto.quickstart.data.dto.UpdateTaskTitleDto import com.ditto.quickstart.ditto.DittoManager +private const val SUBSCRIPTION_QUERY_SELECT_TASKS = """ +SELECT * FROM tasks WHERE NOT deleted +""" + private const val QUERY_SELECT_TASKS = """ SELECT * FROM tasks WHERE NOT deleted ORDER BY title ASC """ private const val QUERY_SELECT_TASK = """ -SELECT * FROM tasks WHERE deleted = false AND _id = :taskId LIMIT 1 +SELECT * FROM tasks WHERE NOT deleted AND _id = :taskId LIMIT 1 """ private const val QUERY_INSERT_TASK = """ @@ -129,7 +133,7 @@ class DittoTaskRepository( ) private suspend fun registerSubscription() { - syncSubscription = dittoManager.registerSubscription(QUERY_SELECT_TASKS) + syncSubscription = dittoManager.registerSubscription(SUBSCRIPTION_QUERY_SELECT_TASKS) } private suspend fun registerObserver() { From 250c45f7de97dcea9ea5ff8f177084b5d9ce3059 Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Fri, 10 Oct 2025 10:42:08 -0600 Subject: [PATCH 08/20] refactor: explicitly enable P2P transports --- .../spring/quickstart/service/DittoService.java | 10 +++++++--- .../kotlin/com/ditto/quickstart/ditto/DittoManager.kt | 9 ++++++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/java-spring/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java b/java-spring/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java index 97ffa691a..f5ca7d5c4 100644 --- a/java-spring/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java +++ b/java-spring/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java @@ -62,13 +62,17 @@ public class DittoService implements DisposableBean { this.ditto.setDeviceName("Spring Java"); - this.ditto.updateTransportConfig(transportConfig -> { - transportConfig.connect(connect -> { + this.ditto.updateTransportConfig(config -> { + config.connect(connect -> { // Set the Ditto Websocket URL connect.websocketUrls().add(DittoSecretsConfiguration.DITTO_WEBSOCKET_URL); }); + config.peerToPeer(p2p -> { + p2p.bluetoothLe().isEnabled(true); + p2p.lan().isEnabled(true); + }); - logger.info("Transport config: {}", transportConfig); + logger.info("Transport config: {}", config); }); presenceObserver = observePeersPresence(); diff --git a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ditto/DittoManager.kt b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ditto/DittoManager.kt index c34f85524..9db3c4ff9 100644 --- a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ditto/DittoManager.kt +++ b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ditto/DittoManager.kt @@ -45,6 +45,8 @@ class DittoManager( // SDKS-1294: Don't create Ditto in a scope using Dispatchers.IO createJob = scope.launch(Dispatchers.Default) { ditto = try { + DittoLogger.minimumLogLevel = DittoLogLevel.Info + val config = DittoConfig( databaseId = credentials.appId, connect = DittoConfig.Connect.Server( @@ -52,7 +54,6 @@ class DittoManager( ), ) - DittoLogger.minimumLogLevel = DittoLogLevel.Verbose createDitto( config = config ).apply { @@ -63,6 +64,12 @@ class DittoManager( provider = DittoAuthenticationProvider.development(), ) } + }.apply { + updateTransportConfig { config -> + config.peerToPeer.lan.enabled = true + config.peerToPeer.bluetoothLe.enabled = true + config.peerToPeer.wifiAware.enabled = true + } } } catch (e: Throwable) { DittoLog.e(TAG, "Failed to create Ditto instance: $e") From 5d906967285f187a577ebb212b3a954734560e49 Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Fri, 10 Oct 2025 10:43:15 -0600 Subject: [PATCH 09/20] refactor: vertically align Add Task screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit resolves issue on iOS where a new task couldn’t be added because the button is hidden behind the keyboard --- .../kotlin/com/ditto/quickstart/ui/TaskAddEditScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ui/TaskAddEditScreen.kt b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ui/TaskAddEditScreen.kt index 589c66333..0ffaf2aa2 100644 --- a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ui/TaskAddEditScreen.kt +++ b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ui/TaskAddEditScreen.kt @@ -47,7 +47,7 @@ fun TaskAddEditScreen( Column( modifier = modifier.padding(horizontal = 16.dp), - verticalArrangement = Arrangement.Center + verticalArrangement = Arrangement.Top, ) { Text( text = title, From 2ca3be26017c6cb04f02f96d010833695309f90d Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Tue, 14 Oct 2025 13:56:05 -0600 Subject: [PATCH 10/20] refactor: move java-spring repo configuration to settings.gradle.kts --- .../kotlin/quickstart-conventions.gradle.kts | 4 --- java-spring/settings.gradle.kts | 29 +++++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/java-spring/buildSrc/src/main/kotlin/quickstart-conventions.gradle.kts b/java-spring/buildSrc/src/main/kotlin/quickstart-conventions.gradle.kts index 035293fdd..6a8f8bc98 100644 --- a/java-spring/buildSrc/src/main/kotlin/quickstart-conventions.gradle.kts +++ b/java-spring/buildSrc/src/main/kotlin/quickstart-conventions.gradle.kts @@ -15,10 +15,6 @@ java { } } -repositories { - mavenCentral() -} - tasks.withType { useJUnitPlatform() } diff --git a/java-spring/settings.gradle.kts b/java-spring/settings.gradle.kts index 5c6516ee1..81c039648 100644 --- a/java-spring/settings.gradle.kts +++ b/java-spring/settings.gradle.kts @@ -1,5 +1,34 @@ +pluginManagement { + repositories { + google { + @Suppress("UnstableApiUsage") + mavenContent { + includeGroupAndSubgroups("androidx") + includeGroupAndSubgroups("com.android") + includeGroupAndSubgroups("com.google") + } + } + mavenCentral() + gradlePluginPortal() + } +} + plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0" } rootProject.name = "spring-quickstart-java" + +dependencyResolutionManagement { + @Suppress("UnstableApiUsage") + repositories { + google { + mavenContent { + includeGroupAndSubgroups("androidx") + includeGroupAndSubgroups("com.android") + includeGroupAndSubgroups("com.google") + } + } + mavenCentral() + } +} From bd018f1ff4feef7961316afbfe6ea82d89a6809c Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Tue, 14 Oct 2025 17:55:10 -0600 Subject: [PATCH 11/20] refactor: rename java-spring -> java-server --- .github/CODEOWNERS | 2 +- ...{java-spring-ci.yml => java-server-ci.yml} | 154 +++++++----------- README.md | 2 +- {java-spring => java-server}/.gitignore | 0 {java-spring => java-server}/README.md | 0 {java-spring => java-server}/build.gradle.kts | 0 .../buildSrc/build.gradle.kts | 0 .../kotlin/quickstart-conventions.gradle.kts | 0 .../config/checkstyle/checkstyle.xml | 0 .../config/pmd/pmd.xml | 0 .../gradle.properties | 0 .../gradle/wrapper/gradle-wrapper.jar | Bin .../gradle/wrapper/gradle-wrapper.properties | 0 {java-spring => java-server}/gradlew | 0 {java-spring => java-server}/gradlew.bat | 0 .../java-spring-maven/.gitignore | 0 .../java-spring-maven/mvnw | 0 .../java-spring-maven/mvnw.cmd | 0 .../java-spring-maven/pom.xml | 0 .../settings.gradle.kts | 2 +- .../quickstart/QuickstartApplication.java | 0 .../configuration/DittoConfigurationKeys.java | 0 .../controller/DittoConfigRestController.java | 0 .../controller/TaskContentController.java | 0 .../controller/TaskRestController.java | 0 .../quickstart/service/DittoService.java | 0 .../quickstart/service/DittoTaskService.java | 0 .../spring/quickstart/service/Task.java | 0 .../src/main/resources/application.properties | 0 .../templates/fragments/editForm.html | 0 .../templates/fragments/taskList.html | 0 .../src/main/resources/templates/index.html | 0 .../TaskVisibilityIntegrationTest.java | 0 .../resources/application-test.properties | 0 scripts/bump-versions.js | 4 +- 35 files changed, 63 insertions(+), 101 deletions(-) rename .github/workflows/{java-spring-ci.yml => java-server-ci.yml} (64%) rename {java-spring => java-server}/.gitignore (100%) rename {java-spring => java-server}/README.md (100%) rename {java-spring => java-server}/build.gradle.kts (100%) rename {java-spring => java-server}/buildSrc/build.gradle.kts (100%) rename {java-spring => java-server}/buildSrc/src/main/kotlin/quickstart-conventions.gradle.kts (100%) rename {java-spring => java-server}/config/checkstyle/checkstyle.xml (100%) rename {java-spring => java-server}/config/pmd/pmd.xml (100%) rename {java-spring => java-server}/gradle.properties (100%) rename {java-spring => java-server}/gradle/wrapper/gradle-wrapper.jar (100%) rename {java-spring => java-server}/gradle/wrapper/gradle-wrapper.properties (100%) rename {java-spring => java-server}/gradlew (100%) rename {java-spring => java-server}/gradlew.bat (100%) rename {java-spring => java-server}/java-spring-maven/.gitignore (100%) rename {java-spring => java-server}/java-spring-maven/mvnw (100%) rename {java-spring => java-server}/java-spring-maven/mvnw.cmd (100%) rename {java-spring => java-server}/java-spring-maven/pom.xml (100%) rename {java-spring => java-server}/settings.gradle.kts (94%) rename {java-spring => java-server}/src/main/java/com/ditto/example/spring/quickstart/QuickstartApplication.java (100%) rename {java-spring => java-server}/src/main/java/com/ditto/example/spring/quickstart/configuration/DittoConfigurationKeys.java (100%) rename {java-spring => java-server}/src/main/java/com/ditto/example/spring/quickstart/controller/DittoConfigRestController.java (100%) rename {java-spring => java-server}/src/main/java/com/ditto/example/spring/quickstart/controller/TaskContentController.java (100%) rename {java-spring => java-server}/src/main/java/com/ditto/example/spring/quickstart/controller/TaskRestController.java (100%) rename {java-spring => java-server}/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java (100%) rename {java-spring => java-server}/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java (100%) rename {java-spring => java-server}/src/main/java/com/ditto/example/spring/quickstart/service/Task.java (100%) rename {java-spring => java-server}/src/main/resources/application.properties (100%) rename {java-spring => java-server}/src/main/resources/templates/fragments/editForm.html (100%) rename {java-spring => java-server}/src/main/resources/templates/fragments/taskList.html (100%) rename {java-spring => java-server}/src/main/resources/templates/index.html (100%) rename {java-spring => java-server}/src/test/java/com/ditto/example/spring/quickstart/TaskVisibilityIntegrationTest.java (100%) rename {java-spring => java-server}/src/test/resources/application-test.properties (100%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4113c7759..baf6c89d2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -15,7 +15,7 @@ /dotnet-tui/ @busec0 @kristopherjohnson /dotnet-winforms/ @busec0 @kristopherjohnson /flutter_app/ @pvditto @teodorciuraru -/java-spring/ @phatblat @busec0 +/java-server/ @phatblat @busec0 /javascript-tui/ @konstantinbe @pvditto @teodorciuraru /javascript-web/ @konstantinbe @pvditto @teodorciuraru /kotlin-multiplatform/ @busec0 @phatblat diff --git a/.github/workflows/java-spring-ci.yml b/.github/workflows/java-server-ci.yml similarity index 64% rename from .github/workflows/java-spring-ci.yml rename to .github/workflows/java-server-ci.yml index 418d77b11..2b38e24f2 100644 --- a/.github/workflows/java-spring-ci.yml +++ b/.github/workflows/java-server-ci.yml @@ -1,12 +1,21 @@ # -# .github/workflows/java-spring-ci.yml -# Workflow for building and testing java-spring with BrowserStack integration +# .github/workflows/java-server-ci.yml +# Workflow for building and testing java-server with BrowserStack integration # --- -name: Java Spring CI +name: Java Server CI on: + push: + branches: [main] + paths: + - "java-server/**" + - ".github/workflows/java-server-ci.yml" pull_request: + branches: [main] + paths: + - "java-server/**" + - ".github/workflows/java-server-ci.yml" workflow_dispatch: concurrency: @@ -39,7 +48,7 @@ jobs: echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> .env - name: Run linter (PMD + SpotBugs) - working-directory: java-spring + working-directory: java-server run: ./gradlew pmdMain pmdTest spotbugsMain build: @@ -68,20 +77,20 @@ jobs: echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> .env - name: Build Spring Boot JAR - working-directory: java-spring + working-directory: java-server env: DITTO_APP_ID: ${{ secrets.DITTO_APP_ID }} DITTO_PLAYGROUND_TOKEN: ${{ secrets.DITTO_PLAYGROUND_TOKEN }} DITTO_AUTH_URL: ${{ secrets.DITTO_AUTH_URL }} DITTO_WEBSOCKET_URL: ${{ secrets.DITTO_WEBSOCKET_URL }} DITTO_ENABLE_CLOUD_SYNC: true - run: ./gradlew bootJar -x test + run: ./gradlew bootJar - name: Upload JAR artifacts uses: actions/upload-artifact@v4 with: - name: java-spring-jar-${{ github.run_number }} - path: java-spring/build/libs/*.jar + name: java-server-jar-${{ github.run_number }} + path: java-server/build/libs/*.jar retention-days: 1 browserstack-test: @@ -89,18 +98,24 @@ jobs: runs-on: macos-latest needs: [build] if: github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' - timeout-minutes: 150 - outputs: - build_id: ${{ steps.test.outputs.build_id }} + timeout-minutes: 45 steps: - uses: actions/checkout@v4 + - name: Seed test task to Ditto Cloud + id: seed_task + uses: ./.github/actions/seed-ditto-document + with: + ditto-api-key: ${{ secrets.DITTO_API_KEY }} + ditto-api-url: ${{ secrets.DITTO_API_URL }} + app-name: "java-server" + - name: Download JAR artifacts uses: actions/download-artifact@v4 with: - name: java-spring-jar-${{ github.run_number }} - path: java-spring/build/libs/ + name: java-server-jar-${{ github.run_number }} + path: java-server/build/libs/ - name: Setup Java uses: actions/setup-java@v4 @@ -152,23 +167,19 @@ jobs: [ -f browserstack-local.log ] && cat browserstack-local.log exit 1 - - name: Get BrowserStack build info - id: build-info - uses: ./.github/actions/generate-browserstack-names - - name: Create BrowserStack config - working-directory: java-spring + working-directory: java-server run: | - # Load platforms from centralized config and convert to YAML format - PLATFORMS=$(yq eval '.["java-spring"].platforms[] | " - os: \(.os)\n osVersion: \"\(.osVersion)\"\n browserName: \(.browserName)\n browserVersion: \(.browserVersion)"' ../.github/browserstack-devices.yml) - cat > browserstack.yml << EOF userName: ${{ secrets.BROWSERSTACK_USERNAME }} accessKey: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - projectName: ${{ steps.build-info.outputs.project-name }} - buildName: ${{ steps.build-info.outputs.build-name }} + projectName: Ditto Java Server Tasks + buildName: Java Server Selenium Tests #${{ github.run_number }} platforms: - $PLATFORMS + - os: Windows + osVersion: 11 + browserName: Chrome + browserVersion: latest browserstackLocal: true debug: true video: true @@ -176,7 +187,7 @@ jobs: EOF - name: Start Spring Boot app in background - working-directory: java-spring + working-directory: java-server env: DITTO_APP_ID: ${{ secrets.DITTO_APP_ID }} DITTO_PLAYGROUND_TOKEN: ${{ secrets.DITTO_PLAYGROUND_TOKEN }} @@ -199,79 +210,30 @@ jobs: sleep 2 done - - name: Seed and execute Selenium tests on BrowserStack cloud browsers - id: test - uses: nick-fields/retry@v3 + - name: Execute Selenium tests on BrowserStack cloud browsers + working-directory: java-server env: BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} BROWSERSTACK_LOCAL: true - with: - max_attempts: 5 - timeout_minutes: 20 - retry_wait_seconds: 900 - command: | - # Seed test task to Ditto Cloud - echo "Seeding test task to Ditto Cloud..." - TIMESTAMP=$(date +%s) - INVERTED_TIMESTAMP=$((9999999999 - TIMESTAMP)) - SEED_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ - -H 'Content-type: application/json' \ - -H "Authorization: Bearer ${{ secrets.DITTO_API_KEY }}" \ - -d "{ - \"statement\": \"INSERT INTO tasks DOCUMENTS (:newTask) ON ID CONFLICT DO UPDATE\", - \"args\": { - \"newTask\": { - \"_id\": \"${INVERTED_TIMESTAMP}_java-spring_ci_test_${{ github.run_id }}_${{ github.run_number }}\", - \"title\": \"${INVERTED_TIMESTAMP}_java-spring_ci_test_${{ github.run_id }}_${{ github.run_number }}\", - \"done\": false, - \"deleted\": false - } - } - }" \ - "https://${{ secrets.DITTO_API_URL }}/api/v4/store/execute") - - HTTP_CODE=$(echo "$SEED_RESPONSE" | tail -n1) - BODY=$(echo "$SEED_RESPONSE" | sed '$d') - - if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 201 ]; then - TASK_TITLE="${INVERTED_TIMESTAMP}_java-spring_ci_test_${{ github.run_id }}_${{ github.run_number }}" - echo "Seeded task: $TASK_TITLE" - else - echo "Error: Failed to seed task. HTTP Status: $HTTP_CODE" - echo "Response: $BODY" - exit 1 - fi - - # Export as environment variable (for System.getenv()) - export DITTO_CLOUD_TASK_TITLE="$TASK_TITLE" - - # Run BrowserStack Selenium tests - cd java-spring - ./gradlew test --tests "*TaskVisibilityIntegrationTest.shouldPassWithExistingTask" \ - -DBROWSERSTACK_USERNAME="${{ secrets.BROWSERSTACK_USERNAME }}" \ - -DBROWSERSTACK_ACCESS_KEY="${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - -DBROWSERSTACK_LOCAL=true \ - -DDITTO_CLOUD_TASK_TITLE="$TASK_TITLE" \ - --info - - # Query BrowserStack API to get the build ID - echo "Fetching build ID from BrowserStack..." - BUILDS_RESPONSE=$(curl -s -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - "https://api.browserstack.com/automate/builds.json?limit=1") - - BUILD_ID=$(echo "$BUILDS_RESPONSE" | yq eval '.[0].hashed_id' -) - - if [ "$BUILD_ID" != "null" ] && [ -n "$BUILD_ID" ]; then - echo "build_id=$BUILD_ID" >> $GITHUB_OUTPUT - echo "✅ Build ID: $BUILD_ID" - else - echo "⚠️ Could not retrieve build ID from BrowserStack API" - fi + TEST_TASK_TITLE: ${{ steps.seed_task.outputs.document-title }} + GITHUB_TEST_DOC_ID: ${{ steps.seed_task.outputs.document-title }} + run: | + TITLE="${{ steps.seed_task.outputs.document-title }}" + + # Run only the BrowserStack test method, not all test methods + ./gradlew test --tests "*TaskVisibilityIntegrationTest.shouldPassWithExistingTask" \ + -DBROWSERSTACK_USERNAME="${{ secrets.BROWSERSTACK_USERNAME }}" \ + -DBROWSERSTACK_ACCESS_KEY="${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ + -DBROWSERSTACK_BUILD_NAME="Java Spring Selenium Tests #${{ github.run_number }}" \ + -DBROWSERSTACK_LOCAL=true \ + -DTEST_TASK_TITLE="$TITLE" \ + -DGITHUB_TEST_DOC_ID="$TITLE" \ + --info - name: Stop Spring Boot app if: always() - working-directory: java-spring + working-directory: java-server run: | if [ -f app.pid ]; then kill $(cat app.pid) || true @@ -287,7 +249,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: browserstack-test-reports-${{ github.run_number }} - path: java-spring/build/reports/tests/ + path: java-server/build/reports/tests/ retention-days: 1 - name: Upload app logs @@ -296,14 +258,14 @@ jobs: with: name: spring-boot-logs-${{ github.run_number }} path: | - java-spring/app.log + java-server/app.log browserstack-local.log retention-days: 1 summary: name: CI Report runs-on: ubuntu-latest - needs: [lint, build, browserstack-test] + needs: [browserstack-test] if: always() steps: @@ -330,10 +292,10 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY # BrowserStack link - if [[ "${{ needs.browserstack-test.result }}" != "skipped" ]] && [ -n "${{ needs.browserstack-test.outputs.build_id }}" ]; then + if [[ "${{ needs.browserstack-test.result }}" != "skipped" ]]; then echo "### BrowserStack Session" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "🔗 [View Test Results](https://automate.browserstack.com/dashboard/v2/builds/${{ needs.browserstack-test.outputs.build_id }}/)" >> $GITHUB_STEP_SUMMARY + echo "🔗 [View Test Results](https://automate.browserstack.com/builds?project=Ditto+Java+Spring+Tasks&build=Java+Spring+Selenium+Tests+%23${{ github.run_number }})" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Tested Browser:**" >> $GITHUB_STEP_SUMMARY echo "- Chrome Latest (Windows 11)" >> $GITHUB_STEP_SUMMARY diff --git a/README.md b/README.md index b54c7fbaa..5572d5ea5 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ build and run them. - [Android Kotlin](android-kotlin/README.md) - [Android Java](android-java/README.md) - [Android C++](android-cpp/README.md) -- [Java Server](java-spring/README.md) +- [Java Server](java-server/README.md) - [C++ TUI](cpp-tui/README.md) - [C# .NET MAUI](dotnet-maui/README.md) - [C# .NET TUI](dotnet-tui/README.md) diff --git a/java-spring/.gitignore b/java-server/.gitignore similarity index 100% rename from java-spring/.gitignore rename to java-server/.gitignore diff --git a/java-spring/README.md b/java-server/README.md similarity index 100% rename from java-spring/README.md rename to java-server/README.md diff --git a/java-spring/build.gradle.kts b/java-server/build.gradle.kts similarity index 100% rename from java-spring/build.gradle.kts rename to java-server/build.gradle.kts diff --git a/java-spring/buildSrc/build.gradle.kts b/java-server/buildSrc/build.gradle.kts similarity index 100% rename from java-spring/buildSrc/build.gradle.kts rename to java-server/buildSrc/build.gradle.kts diff --git a/java-spring/buildSrc/src/main/kotlin/quickstart-conventions.gradle.kts b/java-server/buildSrc/src/main/kotlin/quickstart-conventions.gradle.kts similarity index 100% rename from java-spring/buildSrc/src/main/kotlin/quickstart-conventions.gradle.kts rename to java-server/buildSrc/src/main/kotlin/quickstart-conventions.gradle.kts diff --git a/java-spring/config/checkstyle/checkstyle.xml b/java-server/config/checkstyle/checkstyle.xml similarity index 100% rename from java-spring/config/checkstyle/checkstyle.xml rename to java-server/config/checkstyle/checkstyle.xml diff --git a/java-spring/config/pmd/pmd.xml b/java-server/config/pmd/pmd.xml similarity index 100% rename from java-spring/config/pmd/pmd.xml rename to java-server/config/pmd/pmd.xml diff --git a/java-spring/gradle.properties b/java-server/gradle.properties similarity index 100% rename from java-spring/gradle.properties rename to java-server/gradle.properties diff --git a/java-spring/gradle/wrapper/gradle-wrapper.jar b/java-server/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from java-spring/gradle/wrapper/gradle-wrapper.jar rename to java-server/gradle/wrapper/gradle-wrapper.jar diff --git a/java-spring/gradle/wrapper/gradle-wrapper.properties b/java-server/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from java-spring/gradle/wrapper/gradle-wrapper.properties rename to java-server/gradle/wrapper/gradle-wrapper.properties diff --git a/java-spring/gradlew b/java-server/gradlew similarity index 100% rename from java-spring/gradlew rename to java-server/gradlew diff --git a/java-spring/gradlew.bat b/java-server/gradlew.bat similarity index 100% rename from java-spring/gradlew.bat rename to java-server/gradlew.bat diff --git a/java-spring/java-spring-maven/.gitignore b/java-server/java-spring-maven/.gitignore similarity index 100% rename from java-spring/java-spring-maven/.gitignore rename to java-server/java-spring-maven/.gitignore diff --git a/java-spring/java-spring-maven/mvnw b/java-server/java-spring-maven/mvnw similarity index 100% rename from java-spring/java-spring-maven/mvnw rename to java-server/java-spring-maven/mvnw diff --git a/java-spring/java-spring-maven/mvnw.cmd b/java-server/java-spring-maven/mvnw.cmd similarity index 100% rename from java-spring/java-spring-maven/mvnw.cmd rename to java-server/java-spring-maven/mvnw.cmd diff --git a/java-spring/java-spring-maven/pom.xml b/java-server/java-spring-maven/pom.xml similarity index 100% rename from java-spring/java-spring-maven/pom.xml rename to java-server/java-spring-maven/pom.xml diff --git a/java-spring/settings.gradle.kts b/java-server/settings.gradle.kts similarity index 94% rename from java-spring/settings.gradle.kts rename to java-server/settings.gradle.kts index 81c039648..46a5b6491 100644 --- a/java-spring/settings.gradle.kts +++ b/java-server/settings.gradle.kts @@ -17,7 +17,7 @@ plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0" } -rootProject.name = "spring-quickstart-java" +rootProject.name = "quickstart-java" dependencyResolutionManagement { @Suppress("UnstableApiUsage") diff --git a/java-spring/src/main/java/com/ditto/example/spring/quickstart/QuickstartApplication.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/QuickstartApplication.java similarity index 100% rename from java-spring/src/main/java/com/ditto/example/spring/quickstart/QuickstartApplication.java rename to java-server/src/main/java/com/ditto/example/spring/quickstart/QuickstartApplication.java diff --git a/java-spring/src/main/java/com/ditto/example/spring/quickstart/configuration/DittoConfigurationKeys.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/configuration/DittoConfigurationKeys.java similarity index 100% rename from java-spring/src/main/java/com/ditto/example/spring/quickstart/configuration/DittoConfigurationKeys.java rename to java-server/src/main/java/com/ditto/example/spring/quickstart/configuration/DittoConfigurationKeys.java diff --git a/java-spring/src/main/java/com/ditto/example/spring/quickstart/controller/DittoConfigRestController.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/controller/DittoConfigRestController.java similarity index 100% rename from java-spring/src/main/java/com/ditto/example/spring/quickstart/controller/DittoConfigRestController.java rename to java-server/src/main/java/com/ditto/example/spring/quickstart/controller/DittoConfigRestController.java diff --git a/java-spring/src/main/java/com/ditto/example/spring/quickstart/controller/TaskContentController.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/controller/TaskContentController.java similarity index 100% rename from java-spring/src/main/java/com/ditto/example/spring/quickstart/controller/TaskContentController.java rename to java-server/src/main/java/com/ditto/example/spring/quickstart/controller/TaskContentController.java diff --git a/java-spring/src/main/java/com/ditto/example/spring/quickstart/controller/TaskRestController.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/controller/TaskRestController.java similarity index 100% rename from java-spring/src/main/java/com/ditto/example/spring/quickstart/controller/TaskRestController.java rename to java-server/src/main/java/com/ditto/example/spring/quickstart/controller/TaskRestController.java diff --git a/java-spring/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java similarity index 100% rename from java-spring/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java rename to java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java diff --git a/java-spring/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java similarity index 100% rename from java-spring/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java rename to java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java diff --git a/java-spring/src/main/java/com/ditto/example/spring/quickstart/service/Task.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/Task.java similarity index 100% rename from java-spring/src/main/java/com/ditto/example/spring/quickstart/service/Task.java rename to java-server/src/main/java/com/ditto/example/spring/quickstart/service/Task.java diff --git a/java-spring/src/main/resources/application.properties b/java-server/src/main/resources/application.properties similarity index 100% rename from java-spring/src/main/resources/application.properties rename to java-server/src/main/resources/application.properties diff --git a/java-spring/src/main/resources/templates/fragments/editForm.html b/java-server/src/main/resources/templates/fragments/editForm.html similarity index 100% rename from java-spring/src/main/resources/templates/fragments/editForm.html rename to java-server/src/main/resources/templates/fragments/editForm.html diff --git a/java-spring/src/main/resources/templates/fragments/taskList.html b/java-server/src/main/resources/templates/fragments/taskList.html similarity index 100% rename from java-spring/src/main/resources/templates/fragments/taskList.html rename to java-server/src/main/resources/templates/fragments/taskList.html diff --git a/java-spring/src/main/resources/templates/index.html b/java-server/src/main/resources/templates/index.html similarity index 100% rename from java-spring/src/main/resources/templates/index.html rename to java-server/src/main/resources/templates/index.html diff --git a/java-spring/src/test/java/com/ditto/example/spring/quickstart/TaskVisibilityIntegrationTest.java b/java-server/src/test/java/com/ditto/example/spring/quickstart/TaskVisibilityIntegrationTest.java similarity index 100% rename from java-spring/src/test/java/com/ditto/example/spring/quickstart/TaskVisibilityIntegrationTest.java rename to java-server/src/test/java/com/ditto/example/spring/quickstart/TaskVisibilityIntegrationTest.java diff --git a/java-spring/src/test/resources/application-test.properties b/java-server/src/test/resources/application-test.properties similarity index 100% rename from java-spring/src/test/resources/application-test.properties rename to java-server/src/test/resources/application-test.properties diff --git a/scripts/bump-versions.js b/scripts/bump-versions.js index 5aad30fb3..a34ace14f 100755 --- a/scripts/bump-versions.js +++ b/scripts/bump-versions.js @@ -245,7 +245,7 @@ const APP_CONFIGS = { reason: "Requires Ditto v5+, not on synchronized releases", }, - "java-spring": { + "java-server": { skip: true, reason: "Requires Ditto v5+, not on synchronized releases", }, @@ -263,7 +263,7 @@ ${colors.bright}Options:${colors.reset} -h, --help Show help ${colors.bright}Notes:${colors.reset} -- Excludes: kotlin-multiplatform, java-spring (not on synchronized releases) +- Excludes: kotlin-multiplatform, java-server (not on synchronized releases) - Updates lockfiles automatically for each app - Stops on first error `); From 3c8eb6e8bbba3cd35c70c6e8e74cb3ce3ac87ffa Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Fri, 17 Oct 2025 16:48:59 -0600 Subject: [PATCH 12/20] refactor: use DittoSecretsConfiguration to configure Ditto --- .../com/ditto/quickstart/di/DittoModule.kt | 9 +++------ .../ditto/quickstart/ditto/DittoManager.kt | 20 +++++++++++-------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/di/DittoModule.kt b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/di/DittoModule.kt index 2b82553b0..42f92ed2a 100644 --- a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/di/DittoModule.kt +++ b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/di/DittoModule.kt @@ -1,19 +1,16 @@ package com.ditto.quickstart.di -import com.ditto.quickstart.data.DittoCredentials +import com.ditto.example.kotlin.quickstart.configuration.DittoSecretsConfiguration import com.ditto.quickstart.ditto.DittoManager import org.koin.dsl.module fun dittoModule() = module { single { - DittoCredentials( - appId = "da244a14-bb3d-435d-92b0-b4f667a1b004", - appToken = "161848a6-8a68-48d7-8b44-ecdf46648ca6" - ) + DittoSecretsConfiguration } single { DittoManager( - credentials = get(), + secrets = get(), ) } } diff --git a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ditto/DittoManager.kt b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ditto/DittoManager.kt index 9db3c4ff9..9877a5d13 100644 --- a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ditto/DittoManager.kt +++ b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ditto/DittoManager.kt @@ -11,7 +11,6 @@ import com.ditto.kotlin.DittoQueryResult import com.ditto.kotlin.DittoSyncSubscription import com.ditto.kotlin.error.DittoError import com.ditto.kotlin.serialization.DittoCborSerializable -import com.ditto.quickstart.data.DittoCredentials import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO @@ -31,10 +30,15 @@ private const val TAG = "DittoManager" * * Because ditto also has a "database" component, it is fine to expose this class to a Repository. */ -class DittoManager( - val credentials: DittoCredentials -) { - private val scope = CoroutineScope(SupervisorJob()) +class DittoManager { + val secrets: DittoSecretsConfiguration + + constructor(secrets: DittoSecretsConfiguration) { + this.secrets = secrets + this.scope = CoroutineScope(SupervisorJob()) + } + + private val scope: CoroutineScope private var createJob: Job? = null private var closeJob: Job? = null private var ditto: Ditto? = null @@ -48,9 +52,9 @@ class DittoManager( DittoLogger.minimumLogLevel = DittoLogLevel.Info val config = DittoConfig( - databaseId = credentials.appId, + databaseId = secrets.DITTO_APP_ID, connect = DittoConfig.Connect.Server( - url = "https://${credentials.appId}.cloud.ditto.live", + url = secrets.DITTO_AUTH_URL, ), ) @@ -60,7 +64,7 @@ class DittoManager( auth?.setExpirationHandler { ditto, secondsRemaining -> // Authenticate when a token is expiring val clientInfo = ditto.auth?.login( - token = credentials.appToken, + token = secrets.DITTO_PLAYGROUND_TOKEN, provider = DittoAuthenticationProvider.development(), ) } From aa40f3f5e661d131f03a791a6ac0fbfede0e8285 Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Thu, 30 Oct 2025 09:32:44 -0600 Subject: [PATCH 13/20] fic(ci): set DITTO_CLOUD_TASK_TITLE var --- .github/workflows/java-server-ci.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/java-server-ci.yml b/.github/workflows/java-server-ci.yml index 2b38e24f2..fd4654fee 100644 --- a/.github/workflows/java-server-ci.yml +++ b/.github/workflows/java-server-ci.yml @@ -216,19 +216,15 @@ jobs: BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} BROWSERSTACK_LOCAL: true - TEST_TASK_TITLE: ${{ steps.seed_task.outputs.document-title }} - GITHUB_TEST_DOC_ID: ${{ steps.seed_task.outputs.document-title }} + TASK_TITLE: "${{ steps.seed_task.outputs.document-title }}" run: | - TITLE="${{ steps.seed_task.outputs.document-title }}" - # Run only the BrowserStack test method, not all test methods ./gradlew test --tests "*TaskVisibilityIntegrationTest.shouldPassWithExistingTask" \ -DBROWSERSTACK_USERNAME="${{ secrets.BROWSERSTACK_USERNAME }}" \ -DBROWSERSTACK_ACCESS_KEY="${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ -DBROWSERSTACK_BUILD_NAME="Java Spring Selenium Tests #${{ github.run_number }}" \ -DBROWSERSTACK_LOCAL=true \ - -DTEST_TASK_TITLE="$TITLE" \ - -DGITHUB_TEST_DOC_ID="$TITLE" \ + -DDITTO_CLOUD_TASK_TITLE="$TASK_TITLE" \ --info - name: Stop Spring Boot app From 27b6b1146071967aeacfe2e6032287ff7b8e0aeb Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Thu, 30 Oct 2025 11:57:30 -0600 Subject: [PATCH 14/20] refactor(ci): move vars to env --- .github/workflows/java-server-ci.yml | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/java-server-ci.yml b/.github/workflows/java-server-ci.yml index fd4654fee..26f64ba42 100644 --- a/.github/workflows/java-server-ci.yml +++ b/.github/workflows/java-server-ci.yml @@ -213,18 +213,15 @@ jobs: - name: Execute Selenium tests on BrowserStack cloud browsers working-directory: java-server env: - BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + BROWSERSTACK_BUILD_NAME: "Java Spring Selenium Tests #${{ github.run_number }}" + BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} BROWSERSTACK_LOCAL: true - TASK_TITLE: "${{ steps.seed_task.outputs.document-title }}" + DITTO_CLOUD_TASK_TITLE: "${{ steps.seed_task.outputs.document-title }}" run: | # Run only the BrowserStack test method, not all test methods - ./gradlew test --tests "*TaskVisibilityIntegrationTest.shouldPassWithExistingTask" \ - -DBROWSERSTACK_USERNAME="${{ secrets.BROWSERSTACK_USERNAME }}" \ - -DBROWSERSTACK_ACCESS_KEY="${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - -DBROWSERSTACK_BUILD_NAME="Java Spring Selenium Tests #${{ github.run_number }}" \ - -DBROWSERSTACK_LOCAL=true \ - -DDITTO_CLOUD_TASK_TITLE="$TASK_TITLE" \ + ./gradlew test \ + --tests "*TaskVisibilityIntegrationTest.shouldPassWithExistingTask" \ --info - name: Stop Spring Boot app From 9210e4205dca3e3fad8fdc542a83459298f2740f Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Wed, 5 Nov 2025 08:53:01 -0700 Subject: [PATCH 15/20] ci: update Browserstack workflow for java server --- .github/workflows/java-server-ci.yml | 123 +++++++++++++++++++-------- 1 file changed, 87 insertions(+), 36 deletions(-) diff --git a/.github/workflows/java-server-ci.yml b/.github/workflows/java-server-ci.yml index 26f64ba42..d620a04ee 100644 --- a/.github/workflows/java-server-ci.yml +++ b/.github/workflows/java-server-ci.yml @@ -6,16 +6,7 @@ name: Java Server CI on: - push: - branches: [main] - paths: - - "java-server/**" - - ".github/workflows/java-server-ci.yml" pull_request: - branches: [main] - paths: - - "java-server/**" - - ".github/workflows/java-server-ci.yml" workflow_dispatch: concurrency: @@ -84,7 +75,7 @@ jobs: DITTO_AUTH_URL: ${{ secrets.DITTO_AUTH_URL }} DITTO_WEBSOCKET_URL: ${{ secrets.DITTO_WEBSOCKET_URL }} DITTO_ENABLE_CLOUD_SYNC: true - run: ./gradlew bootJar + run: ./gradlew bootJar -x test - name: Upload JAR artifacts uses: actions/upload-artifact@v4 @@ -98,19 +89,13 @@ jobs: runs-on: macos-latest needs: [build] if: github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' - timeout-minutes: 45 + timeout-minutes: 150 + outputs: + build_id: ${{ steps.test.outputs.build_id }} steps: - uses: actions/checkout@v4 - - name: Seed test task to Ditto Cloud - id: seed_task - uses: ./.github/actions/seed-ditto-document - with: - ditto-api-key: ${{ secrets.DITTO_API_KEY }} - ditto-api-url: ${{ secrets.DITTO_API_URL }} - app-name: "java-server" - - name: Download JAR artifacts uses: actions/download-artifact@v4 with: @@ -167,19 +152,27 @@ jobs: [ -f browserstack-local.log ] && cat browserstack-local.log exit 1 + - name: Get BrowserStack build info + id: build-info + uses: ./.github/actions/generate-browserstack-names + with: + platform-suffix: " (Java)" + title-max-length: "90" + commit-max-length: "130" + - name: Create BrowserStack config working-directory: java-server run: | + # Load platforms from centralized config and convert to YAML format + PLATFORMS=$(yq eval '.["java-spring"].platforms[] | " - os: \(.os)\n osVersion: \"\(.osVersion)\"\n browserName: \(.browserName)\n browserVersion: \(.browserVersion)"' ../.github/browserstack-devices.yml) + cat > browserstack.yml << EOF userName: ${{ secrets.BROWSERSTACK_USERNAME }} accessKey: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - projectName: Ditto Java Server Tasks - buildName: Java Server Selenium Tests #${{ github.run_number }} + projectName: ${{ steps.build-info.outputs.project-name }} + buildName: ${{ steps.build-info.outputs.build-name }} platforms: - - os: Windows - osVersion: 11 - browserName: Chrome - browserVersion: latest + $PLATFORMS browserstackLocal: true debug: true video: true @@ -210,19 +203,77 @@ jobs: sleep 2 done - - name: Execute Selenium tests on BrowserStack cloud browsers - working-directory: java-server + - name: Seed and execute Selenium tests on BrowserStack cloud browsers + # working-directory: java-server + id: test + uses: nick-fields/retry@v3 env: BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} BROWSERSTACK_BUILD_NAME: "Java Spring Selenium Tests #${{ github.run_number }}" BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} BROWSERSTACK_LOCAL: true - DITTO_CLOUD_TASK_TITLE: "${{ steps.seed_task.outputs.document-title }}" - run: | - # Run only the BrowserStack test method, not all test methods - ./gradlew test \ - --tests "*TaskVisibilityIntegrationTest.shouldPassWithExistingTask" \ - --info + with: + max_attempts: 5 + timeout_minutes: 20 + retry_wait_seconds: 900 + command: | + # Seed test task to Ditto Cloud + echo "Seeding test task to Ditto Cloud..." + TIMESTAMP=$(date +%s) + INVERTED_TIMESTAMP=$((9999999999 - TIMESTAMP)) + SEED_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ + -H 'Content-type: application/json' \ + -H "Authorization: Bearer ${{ secrets.DITTO_API_KEY }}" \ + -d "{ + \"statement\": \"INSERT INTO tasks DOCUMENTS (:newTask) ON ID CONFLICT DO UPDATE\", + \"args\": { + \"newTask\": { + \"_id\": \"${INVERTED_TIMESTAMP}_java-spring_ci_test_${{ github.run_id }}_${{ github.run_number }}\", + \"title\": \"${INVERTED_TIMESTAMP}_java-spring_ci_test_${{ github.run_id }}_${{ github.run_number }}\", + \"done\": false, + \"deleted\": false + } + } + }" \ + "https://${{ secrets.DITTO_API_URL }}/api/v4/store/execute") + + HTTP_CODE=$(echo "$SEED_RESPONSE" | tail -n1) + BODY=$(echo "$SEED_RESPONSE" | sed '$d') + + if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 201 ]; then + TASK_TITLE="${INVERTED_TIMESTAMP}_java-spring_ci_test_${{ github.run_id }}_${{ github.run_number }}" + echo "Seeded task: $TASK_TITLE" + else + echo "Error: Failed to seed task. HTTP Status: $HTTP_CODE" + echo "Response: $BODY" + exit 1 + fi + + # Export as environment variable (for System.getenv()) + export DITTO_CLOUD_TASK_TITLE="$TASK_TITLE" + + # Run BrowserStack Selenium tests + cd java-spring + ./gradlew test --tests "*TaskVisibilityIntegrationTest.shouldPassWithExistingTask" \ + -DBROWSERSTACK_USERNAME="${{ secrets.BROWSERSTACK_USERNAME }}" \ + -DBROWSERSTACK_ACCESS_KEY="${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ + -DBROWSERSTACK_LOCAL=true \ + -DDITTO_CLOUD_TASK_TITLE="$TASK_TITLE" \ + --info + + # Query BrowserStack API to get the build ID + echo "Fetching build ID from BrowserStack..." + BUILDS_RESPONSE=$(curl -s -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ + "https://api.browserstack.com/automate/builds.json?limit=1") + + BUILD_ID=$(echo "$BUILDS_RESPONSE" | yq eval '.[0].hashed_id' -) + + if [ "$BUILD_ID" != "null" ] && [ -n "$BUILD_ID" ]; then + echo "build_id=$BUILD_ID" >> $GITHUB_OUTPUT + echo "✅ Build ID: $BUILD_ID" + else + echo "⚠️ Could not retrieve build ID from BrowserStack API" + fi - name: Stop Spring Boot app if: always() @@ -258,7 +309,7 @@ jobs: summary: name: CI Report runs-on: ubuntu-latest - needs: [browserstack-test] + needs: [lint, build, browserstack-test] if: always() steps: @@ -285,10 +336,10 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY # BrowserStack link - if [[ "${{ needs.browserstack-test.result }}" != "skipped" ]]; then + if [[ "${{ needs.browserstack-test.result }}" != "skipped" ]] && [ -n "${{ needs.browserstack-test.outputs.build_id }}" ]; then echo "### BrowserStack Session" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "🔗 [View Test Results](https://automate.browserstack.com/builds?project=Ditto+Java+Spring+Tasks&build=Java+Spring+Selenium+Tests+%23${{ github.run_number }})" >> $GITHUB_STEP_SUMMARY + echo "🔗 [View Test Results](https://automate.browserstack.com/dashboard/v2/builds/${{ needs.browserstack-test.outputs.build_id }}/)" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Tested Browser:**" >> $GITHUB_STEP_SUMMARY echo "- Chrome Latest (Windows 11)" >> $GITHUB_STEP_SUMMARY From ff97b9d4d7e7b0cd6ecf702b3b7a617967afed70 Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Thu, 6 Nov 2025 05:18:17 -0700 Subject: [PATCH 16/20] fix: restore ordering on display query --- .../spring/quickstart/service/DittoTaskService.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java index c2e61f1e8..2ce001a85 100644 --- a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java +++ b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java @@ -96,15 +96,15 @@ public void updateTask(@Nonnull String taskId, @Nonnull String newTitle) { @Nonnull public Flux> observeAll() { - final String selectQuery = "SELECT * FROM %s WHERE NOT deleted".formatted(TASKS_COLLECTION_NAME); + final String subscriptionQuery = "SELECT * FROM %s WHERE NOT deleted".formatted(TASKS_COLLECTION_NAME); + final String displayQuery = subscriptionQuery + " ORDER BY title ASC"; return Flux.create(emitter -> { Ditto ditto = dittoService.getDitto(); try { - DittoSyncSubscription subscription = ditto.getSync().registerSubscription(selectQuery); - DittoStoreObserver observer = ditto.getStore().registerObserver(selectQuery, results -> { - emitter.next(results.getItems().stream().map(this::itemToTask).toList()); - }); + DittoSyncSubscription subscription = ditto.getSync().registerSubscription(subscriptionQuery); + DittoStoreObserver observer = ditto.getStore().registerObserver(displayQuery, results -> + emitter.next(results.getItems().stream().map(this::itemToTask).toList())); emitter.onDispose(() -> { // TODO: Can't just catch, this potentially leaks the `observer` resource. From 988ae804c4cb0f3b1ba70472846cf231e5f1e5f2 Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Thu, 6 Nov 2025 05:43:43 -0700 Subject: [PATCH 17/20] refactor: init scope statically --- .../com/ditto/quickstart/ditto/DittoManager.kt | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ditto/DittoManager.kt b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ditto/DittoManager.kt index 9877a5d13..1272da773 100644 --- a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ditto/DittoManager.kt +++ b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ditto/DittoManager.kt @@ -30,15 +30,10 @@ private const val TAG = "DittoManager" * * Because ditto also has a "database" component, it is fine to expose this class to a Repository. */ -class DittoManager { - val secrets: DittoSecretsConfiguration - - constructor(secrets: DittoSecretsConfiguration) { - this.secrets = secrets - this.scope = CoroutineScope(SupervisorJob()) - } - - private val scope: CoroutineScope +class DittoManager( + val secrets: DittoSecretsConfiguration, +) { + private val scope: CoroutineScope = CoroutineScope(SupervisorJob()) private var createJob: Job? = null private var closeJob: Job? = null private var ditto: Ditto? = null From 9e5f0b9445684497b00dc7056f4f95f313e70f73 Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Thu, 6 Nov 2025 05:45:02 -0700 Subject: [PATCH 18/20] refactor: log auth response --- .../commonMain/kotlin/com/ditto/quickstart/ditto/DittoManager.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ditto/DittoManager.kt b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ditto/DittoManager.kt index 1272da773..9e2fe50d5 100644 --- a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ditto/DittoManager.kt +++ b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ditto/DittoManager.kt @@ -62,6 +62,7 @@ class DittoManager( token = secrets.DITTO_PLAYGROUND_TOKEN, provider = DittoAuthenticationProvider.development(), ) + DittoLog.d(TAG, "Auth response: $clientInfo") } }.apply { updateTransportConfig { config -> From 04dc73cd848b634d7b65f29fa711560a36c7ca34 Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Thu, 6 Nov 2025 05:45:14 -0700 Subject: [PATCH 19/20] style: remove redundant apply --- .../kotlin/com/ditto/quickstart/ditto/DittoManager.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ditto/DittoManager.kt b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ditto/DittoManager.kt index 9e2fe50d5..49e137614 100644 --- a/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ditto/DittoManager.kt +++ b/kotlin-multiplatform/composeApp/src/commonMain/kotlin/com/ditto/quickstart/ditto/DittoManager.kt @@ -56,7 +56,7 @@ class DittoManager( createDitto( config = config ).apply { - auth?.setExpirationHandler { ditto, secondsRemaining -> + auth?.setExpirationHandler { ditto, _ -> // Authenticate when a token is expiring val clientInfo = ditto.auth?.login( token = secrets.DITTO_PLAYGROUND_TOKEN, @@ -64,7 +64,6 @@ class DittoManager( ) DittoLog.d(TAG, "Auth response: $clientInfo") } - }.apply { updateTransportConfig { config -> config.peerToPeer.lan.enabled = true config.peerToPeer.bluetoothLe.enabled = true From 1c5c997c9c1056101a0a47a161e54278d80d6bd3 Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Thu, 6 Nov 2025 06:32:09 -0700 Subject: [PATCH 20/20] chore: add russhwolf to codeowners --- .github/CODEOWNERS | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index baf6c89d2..00d05a84d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,21 +8,21 @@ # SDK-specific owners /android-cpp/ @kristopherjohnson @teodorciuraru -/android-java/ @phatblat @busec0 -/android-kotlin/ @phatblat @busec0 +/android-java/ @russhwolf @phatblat @busec0 +/android-kotlin/ @russhwolf @phatblat @busec0 /cpp-tui/ @kristopherjohnson @teodorciuraru /dotnet-maui/ @busec0 @kristopherjohnson /dotnet-tui/ @busec0 @kristopherjohnson /dotnet-winforms/ @busec0 @kristopherjohnson /flutter_app/ @pvditto @teodorciuraru -/java-server/ @phatblat @busec0 +/java-server/ @russhwolf @phatblat @busec0 /javascript-tui/ @konstantinbe @pvditto @teodorciuraru /javascript-web/ @konstantinbe @pvditto @teodorciuraru -/kotlin-multiplatform/ @busec0 @phatblat +/kotlin-multiplatform/ @russhwolf @phatblat @busec0 /react-native/ @teodorciuraru @kristopherjohnson /react-native-expo/ @teodorciuraru @kristopherjohnson /rust-tui/ @kristopherjohnson -/swift/ @phatblat @konstantinbe @busec0 +/swift/ @konstantinbe @busec0 # TODO: Enable once we have 2 reviewers for Edge Server # /edge-server/ @baxterjo