diff --git a/.github/scripts/build_javadocs.sh b/.github/scripts/build_javadocs.sh index 97d81e29d47..3783b69812b 100755 --- a/.github/scripts/build_javadocs.sh +++ b/.github/scripts/build_javadocs.sh @@ -207,7 +207,8 @@ java/util/concurrent/TimeoutException.java" python3 "$ROOT_DIR/scripts/check-backend-jdk-surface.py" { find "$BACKEND_STAGE" -name "*.java" | grep -v '/com/codename1/impl/' - find "$ROOT_DIR/Ports/CLDC11/src" -name "*.java" + # The class library's internal com.codename1.impl.JdkHost lives there too. + find "$ROOT_DIR/Ports/CLDC11/src" -name "*.java" | grep -v '/com/codename1/impl/' echo "$BACKEND_PROMOTED_JDK" | while IFS= read -r promoted; do echo "$ROOT_DIR/vm/JavaAPI/src/$promoted" done diff --git a/.github/workflows/ant.yml b/.github/workflows/ant.yml index 70e9cf4e3ac..8c0445710b3 100644 --- a/.github/workflows/ant.yml +++ b/.github/workflows/ant.yml @@ -22,7 +22,9 @@ jobs: build-linux-jdk8: runs-on: ubuntu-24.04 - # Includes archetype integration tests and backend packaging after the Ant build. + # Includes archetype integration tests and backend packaging after the Ant + # build, and the Swing and JavaFX compatibility modules in the reactor it + # builds and tests. timeout-minutes: 75 steps: diff --git a/.github/workflows/desktop-compat-benchmarks.yml b/.github/workflows/desktop-compat-benchmarks.yml new file mode 100644 index 00000000000..c7d00ce9704 --- /dev/null +++ b/.github/workflows/desktop-compat-benchmarks.yml @@ -0,0 +1,293 @@ +name: Desktop compatibility benchmarks + +# The two in-tree desktop samples (scripts/desktop-compat-samples) built as what +# their developer ships today -- jar, jlink runtime image, jpackage application +# image -- and as native Codename One executables, then measured side by side: +# distributable size, memory, start-up and idle CPU. +# +# This is a measurement, not a gate: a job fails when the harness breaks, never +# because a number moved. Each leg uploads benchmarks-.json and the same +# figures as a markdown table. scripts/desktop-compat-benchmarks/README.md says +# what every column means and how it was taken. +# +# linux the reference leg: the same install script, driver and input +# scripts as the container the committed numbers came from +# macos sizes, and the runtime figures run-macos-bench.sh takes (start-up +# to first window, idle memory and CPU; no scripted input) +# windows sizes, and the same reduced runtime figures through +# win-measure.ps1. NOTHING on this leg has run outside CI: nobody +# had a Windows machine when it was written. +# +# Shared runners are noisy and have few cores. Compare the columns of one run +# with each other, not one run with another. +on: + workflow_dispatch: + pull_request: + branches: [ master ] + paths: + - 'scripts/desktop-compat-benchmarks/**' + - '.github/workflows/desktop-compat-benchmarks.yml' + push: + branches: [ master ] + paths: + - 'scripts/desktop-compat-benchmarks/**' + - '.github/workflows/desktop-compat-benchmarks.yml' + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +env: + # The modules a native build of an imported desktop project resolves, plus + # everything they depend on (-am). The port module is added per leg. + BENCH_MODULES: parparvm,swing-compat,javafx-compat,fxml-compiler,codenameone-maven-plugin,cn1app-archetype + +jobs: + linux: + name: Linux (x64, Xvfb) + runs-on: ubuntu-latest + timeout-minutes: 240 + steps: + - uses: actions/checkout@v6 + - name: Set up JDK 8 + uses: actions/setup-java@v5 + with: + java-version: '8' + distribution: 'temurin' + cache: 'maven' + - run: echo "JDK_8_HOME=$JAVA_HOME" >> "$GITHUB_ENV" + # The same script the benchmark container is built from: the GTK stack, + # Xvfb and a window manager, JDK 17 and 21, zig, the OpenJFX jmods. + - name: Install the toolchain and the measuring tools + run: sudo bash scripts/desktop-compat-benchmarks/install-linux-deps.sh /opt/bench/tools + - name: Build Codename One (JDK 8) + env: + RETRY_ONLY_MATCHING: transient + RETRY_ATTEMPTS: '5' + RETRY_DELAY_SECONDS: '60' + run: | + cd maven + JAVA_HOME="$JDK_8_HOME" bash ../scripts/ci/retry.sh xvfb-run -a mvn -B \ + -pl "linux,$BENCH_MODULES" -am -Plocal-dev-javase -DskipTests \ + -Darchetype.test.skip=true -Dmaven.javadoc.skip=true install + - name: Build and measure the samples + env: + BENCH_JAVAFX_JMODS: /opt/bench/tools/javafx-jmods + CN1_CC: /opt/bench/tools/cn1-zig-cc + BENCH_XVFB: '1' + BENCH_IMAGE_NAME: GitHub-hosted ubuntu-latest runner + BENCH_COMMIT: ${{ github.sha }} + run: | + ARCH="$(dpkg --print-architecture)" + export JAVA17_HOME="/usr/lib/jvm/java-17-openjdk-$ARCH" + export BENCH_JDK21_HOME="/usr/lib/jvm/java-21-openjdk-$ARCH" + export PATH="/opt/bench/tools/zig:$PATH" + bash scripts/desktop-compat-benchmarks/bench-samples.sh \ + --work "$RUNNER_TEMP/bench" --out "$GITHUB_WORKSPACE/benchmark-results" + cat benchmark-results/benchmarks-linux.md >> "$GITHUB_STEP_SUMMARY" + - name: Upload the results + if: always() + uses: actions/upload-artifact@v7 + with: + name: benchmarks-linux + path: benchmark-results/ + if-no-files-found: warn + - name: Upload the logs and screenshots + if: always() + uses: actions/upload-artifact@v7 + with: + name: benchmarks-linux-logs + path: ${{ runner.temp }}/bench/*/logs/ + if-no-files-found: ignore + + macos: + name: macOS (arm64) + runs-on: macos-latest + timeout-minutes: 240 + steps: + - uses: actions/checkout@v6 + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + java-version: '21' + distribution: 'temurin' + - run: echo "BENCH_JDK21_HOME=$JAVA_HOME" >> "$GITHUB_ENV" + - name: Set up JDK 17 + uses: actions/setup-java@v5 + with: + java-version: '17' + distribution: 'temurin' + - run: echo "JAVA17_HOME=$JAVA_HOME" >> "$GITHUB_ENV" + # Zulu: Temurin publishes no JDK 8 for Apple silicon. + - name: Set up JDK 8 + uses: actions/setup-java@v5 + with: + java-version: '8' + distribution: 'zulu' + cache: 'maven' + - run: echo "JDK_8_HOME=$JAVA_HOME" >> "$GITHUB_ENV" + - name: Build Codename One (JDK 8) + env: + RETRY_ONLY_MATCHING: transient + RETRY_ATTEMPTS: '5' + RETRY_DELAY_SECONDS: '60' + run: | + cd maven + JAVA_HOME="$JDK_8_HOME" bash ../scripts/ci/retry.sh mvn -B \ + -pl "mac,ios,$BENCH_MODULES" -am -Plocal-dev-javase -DskipTests \ + -Darchetype.test.skip=true -Dmaven.javadoc.skip=true -Djava.awt.headless=true install + - name: OpenJFX jmods + run: | + bash scripts/desktop-compat-benchmarks/javafx-jmods.sh "$BENCH_JDK21_HOME" "$RUNNER_TEMP/javafx-jmods" + echo "BENCH_JAVAFX_JMODS=$RUNNER_TEMP/javafx-jmods" >> "$GITHUB_ENV" + - name: Build the samples (sizes) + env: + BENCH_COMMIT: ${{ github.sha }} + run: | + bash scripts/desktop-compat-benchmarks/bench-samples.sh --stage build \ + --work "$RUNNER_TEMP/bench" --out "$GITHUB_WORKSPACE/benchmark-results" + # The runner has a logged-in desktop session, which is what this needs: + # it opens each application's window in turn. + - name: Measure the samples (start-up, idle memory and CPU) + run: | + for NAME in swing-gallery javafx-gallery; do + bash scripts/desktop-compat-benchmarks/run-macos-bench.sh --work "$RUNNER_TEMP/bench" --name "$NAME" \ + || echo "::warning::run-macos-bench.sh failed for $NAME" + done + python3 scripts/desktop-compat-benchmarks/benchutil.py collect benchmark-results/benchmarks-macos.json \ + "$RUNNER_TEMP"/bench/*/result.json + python3 scripts/desktop-compat-benchmarks/report.py --out benchmark-results/benchmarks-macos.md \ + "$RUNNER_TEMP"/bench/*/result.json + cat benchmark-results/benchmarks-macos.md >> "$GITHUB_STEP_SUMMARY" + - name: Upload the results + if: always() + uses: actions/upload-artifact@v7 + with: + name: benchmarks-macos + path: benchmark-results/ + if-no-files-found: warn + - name: Upload the logs + if: always() + uses: actions/upload-artifact@v7 + with: + name: benchmarks-macos-logs + path: ${{ runner.temp }}/bench/*/logs/ + if-no-files-found: ignore + + windows: + name: Windows (x64) + runs-on: windows-latest + timeout-minutes: 240 + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v6 + # clang-cl uses the MSVC headers, CRT and linker, so the MSVC developer + # environment has to be on PATH for the cmake the builder spawns. + - name: Set up the MSVC developer environment + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: amd64 + - name: Install Ninja + run: | + python -m pip install --upgrade ninja + ninja --version + clang-cl --version + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + java-version: '21' + distribution: 'temurin' + - run: echo "BENCH_JDK21_HOME=$JAVA_HOME" >> "$GITHUB_ENV" + - name: Set up JDK 17 + uses: actions/setup-java@v5 + with: + java-version: '17' + distribution: 'temurin' + - run: echo "JAVA17_HOME=$JAVA_HOME" >> "$GITHUB_ENV" + - name: Set up JDK 8 + uses: actions/setup-java@v5 + with: + java-version: '8' + distribution: 'temurin' + cache: 'maven' + - run: echo "JDK_8_HOME=$JAVA_HOME" >> "$GITHUB_ENV" + # The scripts call python3; the runner's interpreter is "python". + - name: Provide python3 + run: | + mkdir -p "$RUNNER_TEMP/bin" + printf '#!/bin/sh\nexec python "$@"\n' > "$RUNNER_TEMP/bin/python3" + chmod +x "$RUNNER_TEMP/bin/python3" + echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH" + - name: Build Codename One (JDK 8) + env: + RETRY_ONLY_MATCHING: transient + RETRY_ATTEMPTS: '5' + RETRY_DELAY_SECONDS: '60' + run: | + cd maven + JAVA_HOME="$JDK_8_HOME" bash ../scripts/ci/retry.sh mvn -B \ + -pl "windows,$BENCH_MODULES" -am -Plocal-dev-javase -DskipTests \ + -Darchetype.test.skip=true -Dmaven.javadoc.skip=true install + - name: OpenJFX jmods + run: | + bash scripts/desktop-compat-benchmarks/javafx-jmods.sh "$BENCH_JDK21_HOME" "$RUNNER_TEMP/javafx-jmods" + echo "BENCH_JAVAFX_JMODS=$RUNNER_TEMP/javafx-jmods" >> "$GITHUB_ENV" + - name: Build the samples (sizes) + env: + BENCH_COMMIT: ${{ github.sha }} + run: | + bash scripts/desktop-compat-benchmarks/bench-samples.sh --stage build \ + --work "$RUNNER_TEMP/bench" --out "$GITHUB_WORKSPACE/benchmark-results" + - name: Measure the samples (start-up, idle memory and CPU) + shell: pwsh + run: | + $bench = Join-Path $env:RUNNER_TEMP 'bench' + foreach ($name in 'swing-gallery', 'javafx-gallery') { + $dir = Join-Path $bench $name + $parts = Join-Path $dir 'parts' + $java = Join-Path $dir 'baseline\runtime\bin\java.exe' + $baseline = Join-Path $parts 'baseline.json' + if ((Test-Path $java) -and (Test-Path $baseline)) { + $main = (Get-Content $baseline -Raw | ConvertFrom-Json).main_class + $cp = (Join-Path $dir 'baseline\app') + '\*' + & scripts/desktop-compat-benchmarks/win-measure.ps1 -Label jvm_default ` + -Out (Join-Path $parts 'run.jvm_default.json') -Exe $java -Arguments '-cp', $cp, $main + & scripts/desktop-compat-benchmarks/win-measure.ps1 -Label jvm_tuned ` + -Out (Join-Path $parts 'run.jvm_tuned.json') -Exe $java ` + -Arguments '-XX:+UseSerialGC', '-Xss256k', '-XX:TieredStopAtLevel=1', '-Xmx64m', '-cp', $cp, $main + } else { + Write-Host "::warning::no baseline runtime image for $name" + } + $marker = Join-Path $dir 'cn1\executable.txt' + if (Test-Path $marker) { + $exe = Join-Path $dir ('cn1\dist\' + (Get-Content $marker -Raw).Trim()) + & scripts/desktop-compat-benchmarks/win-measure.ps1 -Label cn1_native ` + -Out (Join-Path $parts 'run.cn1_native.json') -Exe $exe + } else { + Write-Host "::warning::no native Codename One build for $name" + } + python scripts/desktop-compat-benchmarks/benchutil.py assemble $parts (Join-Path $dir 'result.json') + } + $results = @(Get-ChildItem -Path $bench -Filter result.json -Recurse -Depth 1 | ForEach-Object { $_.FullName }) + python scripts/desktop-compat-benchmarks/benchutil.py collect benchmark-results/benchmarks-windows.json @results + python scripts/desktop-compat-benchmarks/report.py --out benchmark-results/benchmarks-windows.md @results + Get-Content benchmark-results/benchmarks-windows.md | Add-Content $env:GITHUB_STEP_SUMMARY + - name: Upload the results + if: always() + uses: actions/upload-artifact@v7 + with: + name: benchmarks-windows + path: benchmark-results/ + if-no-files-found: warn + - name: Upload the logs + if: always() + uses: actions/upload-artifact@v7 + with: + name: benchmarks-windows-logs + path: ${{ runner.temp }}/bench/*/logs/ + if-no-files-found: ignore diff --git a/.github/workflows/desktop-compat.yml b/.github/workflows/desktop-compat.yml new file mode 100644 index 00000000000..7e57f60999e --- /dev/null +++ b/.github/workflows/desktop-compat.yml @@ -0,0 +1,146 @@ +name: Desktop compatibility + +# Swing and JavaFX applications built as Codename One applications: the +# relocated toolkits (maven/swing-compat, maven/javafx-compat), the headless +# harness their tests run on (maven/compat-testing) and the build steps that +# relocate and package them (build-engine, the Maven and Gradle plugins). The +# JDK API they run on is the device's class library (vm/JavaAPI, +# Ports/CLDC11), under the JDK's own names. +# +# unit tests the runtimes against the JDK's own behaviour, +# the remapper, the importer, and the sample +# loaded remapped and painted headless +# desktop-compat-test.sh every sample under scripts/desktop-compat-samples +# imported unmodified, built by Maven and Gradle, +# checked for unrelocated toolkit references, +# staged for iOS, Android, JavaScript and the +# desktop with identical uploads from both +# tools, and built once in a project that also +# holds an Android application +# desktop-compat-realport the two galleries that script staged, run on +# the real JavaSE port under xvfb and driven +# with the display server's pointer and keys; +# fails on anything thrown and on a click, a +# drag or a key that changes no pixel +on: + pull_request: + branches: [ master ] + paths: + - 'maven/swing-compat/**' + - 'maven/javafx-compat/**' + - 'maven/fxml-compiler/**' + - 'maven/compat-testing/**' + - 'maven/build-engine/**' + - 'maven/codenameone-maven-plugin/**' + - 'maven/codenameone-gradle-plugin/**' + - 'maven/project-model/**' + - 'maven/cn1app-archetype/**' + - 'maven/pom.xml' + - 'maven/integration-tests/desktop-compat-test.sh' + - 'maven/integration-tests/desktop-compat-realport/**' + - 'maven/integration-tests/inc/**' + - 'scripts/desktop-compat-samples/**' + # The script builds one project holding both layers from this sample. + - 'scripts/android-compat-samples/gallery/**' + - '.github/workflows/desktop-compat.yml' + push: + branches: [ master ] + paths: + - 'maven/swing-compat/**' + - 'maven/javafx-compat/**' + - 'maven/fxml-compiler/**' + - 'maven/compat-testing/**' + - 'maven/build-engine/**' + - 'maven/codenameone-maven-plugin/**' + - 'maven/codenameone-gradle-plugin/**' + - 'maven/project-model/**' + - 'maven/cn1app-archetype/**' + - 'maven/pom.xml' + - 'maven/integration-tests/desktop-compat-test.sh' + - 'maven/integration-tests/desktop-compat-realport/**' + - 'maven/integration-tests/inc/**' + - 'scripts/desktop-compat-samples/**' + - 'scripts/android-compat-samples/gallery/**' + - '.github/workflows/desktop-compat.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + desktop-compat: + runs-on: ubuntu-24.04 + timeout-minutes: 60 + env: + CN1_GRADLE_REQUIRED: '1' + steps: + - uses: actions/checkout@v6 + # Installed FIRST so the JDK 8 below is the one JAVA_HOME names. + - name: Set up JDK 17 for Gradle + uses: actions/setup-java@v5 + with: + java-version: '17' + distribution: 'temurin' + - run: echo "GRADLE_JAVA_HOME=$JAVA_HOME" >> $GITHUB_ENV + - name: Set up JDK 8 + uses: actions/setup-java@v5 + with: + java-version: '8' + distribution: 'temurin' + cache: 'maven' + # xvfb for the CSS compiler, which opens an AWT frame. Nothing in the + # compatibility layers opens a window: their tests run on the headless + # implementation in maven/compat-testing. + - name: Install xvfb + run: bash scripts/ci/apt-get-install.sh xvfb + - name: Build Codename One Maven artifacts (includes archetypes and both plugins) + env: + RETRY_ONLY_MATCHING: transient + RETRY_ATTEMPTS: '5' + RETRY_DELAY_SECONDS: '60' + run: | + cd maven + bash ../scripts/ci/retry.sh xvfb-run -a mvn install -Plocal-dev-javase -DskipTests -Darchetype.test.skip=true -Dmaven.javadoc.skip=true + # A module added to the layer (an FXML compiler, say) joins this list. + - name: Unit tests + working-directory: maven + run: xvfb-run -a mvn -B -Plocal-dev-javase -pl compat-testing,swing-compat,javafx-compat,fxml-compiler,build-engine test + # The JavaFX sample has to stay what it claims to be: a JavaFX project + # that builds on its own, against OpenJFX, with nothing of the layer on + # its class path. Everything else here compiles it against the layer, + # which would also accept a sample written to the layer's shape. JDK 17 + # because OpenJFX 17 is; the Swing sample needs no such step, the JDK + # that compiles it in the unit tests is the toolkit. + - name: The JavaFX sample compiles against OpenJFX + env: + RETRY_ONLY_MATCHING: transient + RETRY_ATTEMPTS: '5' + RETRY_DELAY_SECONDS: '60' + run: JAVA_HOME="$GRADLE_JAVA_HOME" bash scripts/ci/retry.sh mvn -B -f scripts/desktop-compat-samples/javafx-gallery/pom.xml compile + - name: Refresh archetype catalog + run: | + cd maven + xvfb-run -a mvn archetype:update-local-catalog -Plocal-dev-javase + xvfb-run -a mvn archetype:crawl -Plocal-dev-javase + - name: Desktop samples, imported and built by Maven and Gradle + working-directory: maven/integration-tests + run: xvfb-run -a bash desktop-compat-test.sh + # The galleries the step above staged, on the real port, under the + # display server's own pointer and keys. The unit tests fire events at + # the node they are meant for; this is the only place a dead button or a + # key the port never delivers can fail. It runs on GRADLE_JAVA_HOME, + # the JDK 17: the port needs 11 or newer at run time. The screen is + # named because xvfb-run's default is smaller than the application. + - name: The galleries on the real JavaSE port, under real input + working-directory: maven/integration-tests + run: xvfb-run -a -s "-screen 0 1400x1000x24" bash desktop-compat-realport/run.sh + - name: Upload the test projects' logs + if: failure() + uses: actions/upload-artifact@v7 + with: + name: desktop-compat-logs + path: | + maven/integration-tests/build/desktop-compat/**/*.log + maven/integration-tests/build/desktop-compat-realport/**/realport.log + maven/integration-tests/build/desktop-compat-realport/**/frames/*.png + if-no-files-found: ignore diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index d639ad5005b..feac4ff776f 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -394,7 +394,7 @@ jobs: -Dcn1.binaries="${CN1_BINARIES}" \ -Pcompile-android \ -DbackendAnalysis \ - -pl android,ios,project-model,build-engine,codenameone-maven-plugin,codenameone-gradle-plugin,backend,backend-test,backend-parparvm,javac,android-res-compiler,android-compat -am verify + -pl android,ios,project-model,build-engine,codenameone-maven-plugin,codenameone-gradle-plugin,backend,backend-test,backend-parparvm,javac,android-res-compiler,android-compat,swing-compat,javafx-compat,fxml-compiler -am verify - name: Type-check the SQLCipher-backed database package if: ${{ matrix.java-version == 8 }} env: @@ -612,7 +612,7 @@ jobs: - name: Generate static analysis HTML summaries if: ${{ !cancelled() && matrix.java-version == 8 }} env: - QUALITY_REPORT_TARGET_DIRS: maven/core-unittests/target:maven/android/target:maven/ios/target:vm/ByteCodeTranslator/target:maven/codenameone-maven-plugin/target:maven/build-engine/target:maven/project-model/target:maven/codenameone-gradle-plugin/target:maven/build-hint-catalog/target:maven/build-hint-tools/target:maven/backend/target:maven/backend-test/target:maven/backend-parparvm/target:maven/javac/target:maven/android-res-compiler/target:maven/android-compat/target + QUALITY_REPORT_TARGET_DIRS: maven/core-unittests/target:maven/android/target:maven/ios/target:vm/ByteCodeTranslator/target:maven/codenameone-maven-plugin/target:maven/build-engine/target:maven/project-model/target:maven/codenameone-gradle-plugin/target:maven/build-hint-catalog/target:maven/build-hint-tools/target:maven/backend/target:maven/backend-test/target:maven/backend-parparvm/target:maven/javac/target:maven/android-res-compiler/target:maven/android-compat/target:maven/swing-compat/target:maven/javafx-compat/target:maven/fxml-compiler/target QUALITY_REPORT_SERVER_URL: ${{ github.server_url }} QUALITY_REPORT_REPOSITORY: ${{ github.repository }} QUALITY_REPORT_REF: ${{ github.event.pull_request.head.sha || github.sha }} @@ -634,6 +634,9 @@ jobs: "build-engine:maven/build-engine/target" \ "android-res-compiler:maven/android-res-compiler/target" \ "android-compat:maven/android-compat/target" \ + "swing-compat:maven/swing-compat/target" \ + "javafx-compat:maven/javafx-compat/target" \ + "fxml-compiler:maven/fxml-compiler/target" \ "project-model:maven/project-model/target" \ "gradle-plugin:maven/codenameone-gradle-plugin/target" \ "backend:maven/backend/target" \ @@ -764,11 +767,11 @@ jobs: - name: Generate quality report summary if: ${{ !cancelled() && matrix.java-version == 8 }} env: - QUALITY_REPORT_TARGET_DIRS: maven/core-unittests/target:maven/android/target:maven/ios/target:vm/ByteCodeTranslator/target:maven/codenameone-maven-plugin/target:maven/build-engine/target:maven/project-model/target:maven/codenameone-gradle-plugin/target:maven/build-hint-catalog/target:maven/build-hint-tools/target:maven/backend/target:maven/backend-test/target:maven/backend-parparvm/target:maven/javac/target:maven/android-res-compiler/target:maven/android-compat/target + QUALITY_REPORT_TARGET_DIRS: maven/core-unittests/target:maven/android/target:maven/ios/target:vm/ByteCodeTranslator/target:maven/codenameone-maven-plugin/target:maven/build-engine/target:maven/project-model/target:maven/codenameone-gradle-plugin/target:maven/build-hint-catalog/target:maven/build-hint-tools/target:maven/backend/target:maven/backend-test/target:maven/backend-parparvm/target:maven/javac/target:maven/android-res-compiler/target:maven/android-compat/target:maven/swing-compat/target:maven/javafx-compat/target:maven/fxml-compiler/target # Every project listed here must produce a SpotBugs report and that # report must be empty. A missing report fails the gate too, so a # SpotBugs run that silently stops running is not mistaken for clean. - QUALITY_REPORT_REQUIRED_SPOTBUGS: core-unittests:android:ios:ByteCodeTranslator:codenameone-maven-plugin:build-engine:project-model:codenameone-gradle-plugin:build-hint-catalog:build-hint-tools:backend:backend-test:javac:android-res-compiler:android-compat + QUALITY_REPORT_REQUIRED_SPOTBUGS: core-unittests:android:ios:ByteCodeTranslator:codenameone-maven-plugin:build-engine:project-model:codenameone-gradle-plugin:build-hint-catalog:build-hint-tools:backend:backend-test:javac:android-res-compiler:android-compat:swing-compat:javafx-compat:fxml-compiler # PMD and Checkstyle run on the core and on the backend runtime, and each # must produce its report: without one, the forbidden-rule gate below it # has nothing to read and passes. diff --git a/CLAUDE.md b/CLAUDE.md index fc26e59b1ab..a6834f96f6d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -351,7 +351,9 @@ recognized and never reported. Note the rule is about the *cast*: a The scope is what a translation actually sees -- `maven/core`, `maven/ios`, `vm/JavaAPI` and `maven/android-compat`, whose runtime an application with -Android sources ships relocated inside its own classes. **The Android port is not covered**, and adding it back would be a +Android sources ships relocated inside its own classes, and +`maven/swing-compat` and `maven/javafx-compat`, which are relocated into an +application with desktop sources the same way. **The Android port is not covered**, and adding it back would be a mistake: ART implements `CHECKCAST` to spec, so a `catch (Throwable)` around a `(NotificationManager) getSystemService(...)` there is live, correct code, and `Ports/Android` is never translated. That is the same reason `Ports/CLDC11` is @@ -797,13 +799,24 @@ An Android Studio module's `src/main` dropped into `common/src/main/android` (views are peers, layouts run Android's measure/layout). Apps compile against it under the real names; its own framework resources live in `src/main/framework-res`. -- **The remap step** (`build-engine/.../AndroidRemapper`, goal `remap-android`) - relocates `android/`, `androidx/` and `com/google/android/material/` to +- **The remap step** (goal `remap-compat`; `remap-android` is the same goal + under its old name, kept so existing poms need no edit) relocates `android/`, + `androidx/` and `com/google/android/material/` to `com/codename1/androidcompat/...`, copies the relocated runtime into the app's - classes, maps the JDK classes CLDC lacks (`java.io.File`, the file streams, - `BufferedReader`, `PrintWriter`, `Closeable`) to `com.codename1.androidcompat.jdk`, - and generates the `android:onClick`, fragment-factory and WebView JS-bridge - dispatchers from the compiled classes. + classes, and generates the `android:onClick`, fragment-factory and WebView + JS-bridge dispatchers from the compiled classes. The JDK API keeps its + names: the device's class library (`vm/JavaAPI`, mirrored by + `Ports/CLDC11`) carries it, and JavaSE and Android run the real JDK. Only + `Relocation.JDK_SHIMS` moves anything of `java.*` -- `java.util.regex` to + `com.codename1.util.regex`, `ProcessBuilder`/`Process` to + `com.codename1.io` -- and every layer shares that table. +- **The remap is one pass over every layer**, split in `build-engine`: + `CompatLayers` declares each layer (artifact, source prefixes, target + package), `Relocation` holds the package and JDK-shim tables, + `ClassRelocator` rewrites a class against all active layers at once, + `CompatRemapper` drives the step and calls `AndroidRemapper` for what is + Android's own. The archetype binds `remap-compat` and then + `bytecode-compliance` in `process-classes`, in that order. Traps that have already cost a fix: @@ -828,6 +841,180 @@ Traps that have already cost a fix: (`.github/workflows/android-compat.yml`); it fails if any shipped class still names `android/`. +### Desktop compatibility: Swing and JavaFX apps as Codename One apps + +A desktop project's `src/main` dropped into `common/src/main/desktop` (or +imported with `cn1:import-desktop-project`) builds unmodified, for every target: + +- **`maven/swing-compat`** is `java.awt`, `javax.swing`, `java.beans`, + `javax.imageio` and a SwingX subset (`org.jdesktop`) written over Codename + One. **It is authored already relocated**, under + `com.codename1.desktopcompat.java.awt...`, because the JDK owns `java.awt` and + no module can declare it. Applications therefore compile against the *JDK's* + Swing, and the remap points them at this one. +- **`maven/javafx-compat`** is the `javafx.*` subset. Nothing owns that name, so + it is authored under the real names like `android-compat`, applications + compile against its jar, and the remap moves it to `com/codename1/fxcompat/`. + Its internals live in `com/codename1/fxcompat/runtime/`, which the remap maps + to `com/codename1/fxcompat/rt/`. +- **There is no JDK module.** A JDK class a device lacks is added to + `vm/JavaAPI` and `Ports/CLDC11` under its real name, never shimmed in a + layer. `CompatRewrites` redirects the few *members* a device cannot have + as written -- resource and class-loader lookups, bundles and services to + `com.codename1.impl.compat.Resources`, the default uncaught-exception + handler, `String`'s regex members, `Runtime.exec`, the `Math` functions + CLDC lacks -- for the desktop layers only. +- **`maven/fxml-compiler`** turns each `.fxml` into + `com.codename1.generated.fxml.Fxml_` (in the remap step, after javac) + and each `.css` into a `.cn1css` table (from `prepare-desktop-sources`); + `FxmlDispatchGenerator` then runs in the remap, on the compiled classes, to + widen and reach the controllers' private `@FXML` members. The CSS value + classes are compiled twice: `fxcompat/runtime/css` is copied into the compiler + under `com.codename1.fxml.css`, so build and device share one parser. +- **`maven/compat-testing`** is the headless implementation and + `MainThreadRule` both layers' tests run on. + +What holds it together: + +- **Unsupported API is a build error, and that is the coverage gate.** + `bytecode-compliance` runs *after* the remap, so a member the layer lacks is + a reference to something that does not exist; `BytecodeCompliance` reports it + under the original name with the source line. What it cannot see: an + application *override* of a method the layer's type lacks passes, and is + simply never called. +- **The API shape is tested, not trusted.** `ApiShapeTest` compares every + public class of `swing-compat` with the JDK class (SwingX with the real + `swingx-all`), and `NoJdkDesktopReferenceTest` fails if a shipped class names + the real `java/awt`. In `javafx-compat`, `ApiNamingTest` requires every + public member that is not JavaFX API to be named `cn1...`/`CN1_...` and listed + in `src/test/resources/cn1-hooks.txt`. A new hook means a new line there. +- **Coordinates are logical pixels, 1/96 inch**, scaled by the display density + (Display property `desktopcompat.scale` overrides; both layers read it) and + snapped edge by edge. Never hand a Swing or JavaFX coordinate to Codename One + unscaled. +- **The application's own main class starts the desktop app**, as editable + source in `common/src/main/java`. `cn1:import-desktop-project` + (`DesktopProjectImporter`) writes it: a Swing app's extends + `DesktopLifecycle` and calls its `main` in `runMain`, a JavaFX app's extends + `FxLifecycle` and `new`s the `Application` in `createApplication` -- no + reflection, no generated class, no properties file. An entry class the main + class cannot name (not public) is reached through a `CN1DesktopLauncher` in + its own package. The remap renames every static `main(String[])` and its + call sites to `cn1DesktopMain`, the main class's call included. + `main`/`start` run on the EDT, which is the Swing EDT and the JavaFX + application thread. +- **Registries are generated over placeholders.** + `com.codename1.generated.compat.CompatRegistry` (resources, bundle classes + and service providers; `ApplicationRegistry` writes it, and the lifecycles + and the library-mode entry points call its `cn1Install()`) and the FXML + dispatch class ship as empty placeholders with the right shape and are + replaced per application; a new method on one needs the generator and the + placeholder changed together. On JavaSE and Android the ports install + `CompatResourceSupport` from `init`, so the real JDK's `URL`, + `ResourceBundle` and `ServiceLoader` read the flattened bundle and the + registry's providers. +- A desktop layer is active only when its jar is present **and** the compiled + classes name its API, so a Swing app does not ship the JavaFX runtime. + Compile- and runtime-scope dependency jars that name a layer are unpacked and relocated as + application code (`CompatLibraries`). + +- **Library mode: desktop sources inside a Codename One app.** A main class + that extends neither lifecycle -- `-Dcn1.desktop.library=true` on the + import, or a main class that starts an Android application -- leaves the + desktop classes shipping relocated as a library. Extending the lifecycle is + what installs the exit hook: there closing the last window exits, otherwise + `SwingInterop.asComponent` / `FxInterop.asComponent` wrap a component, a + parent or a scene as a `com.codename1.ui.Component`, and a `JFrame` or + `Stage` shown is a form over the host's -- closing the last one returns + there and never exits. Both `asComponent` + parameters are `Object` **on purpose**: the remap copies the relocated + runtime into `target/classes`, which shadows the jar on the next non-clean + compile, so a non-relocated public class naming a toolkit type in a + signature stops compiling against application source. `hellocodenameone` + is the library-mode project (`common/src/main/desktop`, the + `DesktopCompat*` screenshot tests); `DesktopCompatScenesTest` in + `build-engine` runs the same scenes headless from the remapped classes. + +`swing-compat`, `javafx-compat` and `fxml-compiler` are in the +SpotBugs zero-findings gate, each with its own `spotbugs-exclude.xml`. + +Traps that have already cost a fix: + +- **SpotBugs XML uses single-quoted attributes.** Grepping the report for + `type="` finds nothing and reads as zero findings. Match either quote, or + count `.txt` beside it against what + `desktop-compat-test.sh` staged (`desktop-compat.yml` does, under + `xvfb-run`). It opens a window: run it under xvfb or in a container. The + port turns Space and Enter into the same key code, and drops every + Ctrl/Alt/Meta combination before the form sees it. + ### Integration Tests Located in `maven/integration-tests/`: diff --git a/CodenameOne/src/com/codename1/annotations/LinkOnly.java b/CodenameOne/src/com/codename1/annotations/LinkOnly.java new file mode 100644 index 00000000000..50e3acdd2f2 --- /dev/null +++ b/CodenameOne/src/com/codename1/annotations/LinkOnly.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/// Marks a class of a compatibility layer that exists so that a library +/// naming it LINKS, and that does nothing useful when it is reached. +/// +/// A third-party library written against a desktop toolkit is bundled with +/// the application whole, and almost every one of them has corners no device +/// can serve: a layout manager that can save itself through +/// `java.beans.XMLEncoder`, which is reflection from end to end, or that asks +/// whether a component is one of AWT's heavyweight widgets. The library's +/// classes name those types, so the types have to exist or nothing that +/// translates the application ahead of time can build it -- although the +/// application never takes those paths. +/// +/// What the build does with the mark (`BytecodeCompliance`): a reference from +/// a bundled library is accepted, and a reference from the application's own +/// classes is reported exactly as if the class did not exist. The layer's +/// contract with a developer is therefore unchanged -- API they call is API +/// that works -- while a library gets to link. +/// +/// A marked class says in its own documentation what each member does when it +/// is reached after all; where that cannot be an honest answer it throws +/// `UnsupportedOperationException` naming the API. +/// +/// Retained in the class file and no further: the build reads it there, and +/// nothing reads it on a device. +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.TYPE) +public @interface LinkOnly { +} diff --git a/CodenameOne/src/com/codename1/impl/CoreJdkHost.java b/CodenameOne/src/com/codename1/impl/CoreJdkHost.java new file mode 100644 index 00000000000..e96d31876f4 --- /dev/null +++ b/CodenameOne/src/com/codename1/impl/CoreJdkHost.java @@ -0,0 +1,449 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl; + +import com.codename1.io.ConnectionRequest; +import com.codename1.io.FileSystemStorage; +import com.codename1.io.Log; +import com.codename1.io.NetworkManager; +import com.codename1.io.Preferences; +import com.codename1.l10n.L10NManager; +import com.codename1.security.Hash; +import com.codename1.ui.Display; + +import java.io.ByteArrayInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/// The [JdkHost] of a Codename One application: the Java class library's +/// file system, preferences, network, log and event thread, served by +/// `FileSystemStorage`, `Preferences`, `NetworkManager`, `Log` and `Display`. +/// +/// Internal. The ParparVM ports (iOS and macOS, Windows, Linux, JavaScript) +/// install it from their `init` through [CoreJdkHostInstaller]. On Java SE and +/// Android the real JDK runs and nothing installs it. `JdkHost` itself is part +/// of the class library, not of the core: the core compiles against it, and +/// the Java SE and Android ports carry a copy so this class always has its +/// superclass. +/// +/// Paths are `FileSystemStorage` paths, or plain absolute paths, which are +/// taken as `file://` paths: `java.io.File` hands a path over as the +/// application wrote it. Nothing here runs before the display is initialized +/// except what checks for it. +public class CoreJdkHost extends JdkHost { + + private static FileSystemStorage fs() { + return FileSystemStorage.getInstance(); + } + + /// `path` as a `FileSystemStorage` path: `/x` and a drive path `C:/x` + /// gain the `file://` scheme; anything with a scheme is left alone. + static String storage(String path) { + if (path.startsWith("/")) { + return "file://" + path; + } + if (path.length() >= 3 && path.charAt(1) == ':' && path.charAt(2) == '/' + && ((path.charAt(0) >= 'A' && path.charAt(0) <= 'Z') + || (path.charAt(0) >= 'a' && path.charAt(0) <= 'z'))) { + return "file:///" + path; + } + return path; + } + + @Override + public FileStat fileStat(String path) { + path = storage(path); + FileSystemStorage fs = fs(); + if (!fs.exists(path)) { + return null; + } + boolean directory = fs.isDirectory(path); + return new FileStat(directory, directory ? 0L : fs.getLength(path), fs.getLastModified(path)); + } + + @Override + public String[] fileList(String path) throws IOException { + path = storage(path); + FileSystemStorage fs = fs(); + if (!fs.exists(path) || !fs.isDirectory(path)) { + return null; + } + return fs.listFiles(path); + } + + @Override + public InputStream fileRead(String path) throws IOException { + path = storage(path); + FileSystemStorage fs = fs(); + if (!fs.exists(path) || fs.isDirectory(path)) { + throw new FileNotFoundException(path); + } + return fs.openInputStream(path); + } + + @Override + public OutputStream fileWrite(String path, boolean append) throws IOException { + path = storage(path); + FileSystemStorage fs = fs(); + if (fs.exists(path) && fs.isDirectory(path)) { + throw new FileNotFoundException(path + " (Is a directory)"); + } + if (append && fs.exists(path)) { + long length = fs.getLength(path); + if (length > Integer.MAX_VALUE) { + // The storage API takes the offset as an int. Narrowed, it + // wraps, and the write would land inside the file. + throw new IOException(path + " (Too large to append to)"); + } + return fs.openOutputStream(path, (int) length); + } + return fs.openOutputStream(path); + } + + @Override + public boolean fileMkdir(String path) { + path = storage(path); + FileSystemStorage fs = fs(); + if (fs.exists(path)) { + return false; + } + // mkdir(2) creates one level; a storage that creates the missing + // parents too would make File.mkdir answer true where the JDK does not. + String parent = parentOf(path); + if (parent != null && !(fs.exists(parent) && fs.isDirectory(parent))) { + return false; + } + fs.mkdir(path); + return fs.exists(path) && fs.isDirectory(path); + } + + @Override + public boolean fileDelete(String path) { + path = storage(path); + FileSystemStorage fs = fs(); + if (!fs.exists(path)) { + return false; + } + if (fs.isDirectory(path)) { + // The JDK deletes only an empty directory; some storages delete + // the whole tree. + String[] entries; + try { + entries = fs.listFiles(path); + } catch (IOException e) { + return false; + } + if (entries != null && entries.length > 0) { + return false; + } + } + fs.delete(path); + return !fs.exists(path); + } + + /// Renames within one directory, which is all `FileSystemStorage` can do; + /// a move to another directory answers false and the caller copies. + @Override + public boolean fileRename(String from, String to) { + from = storage(from); + to = storage(to); + FileSystemStorage fs = fs(); + int fromSlash = lastSeparator(from); + int toSlash = lastSeparator(to); + if (fromSlash < 0 || toSlash < 0 || fs.exists(to) || !fs.exists(from) + || !from.substring(0, fromSlash).equals(to.substring(0, toSlash))) { + return false; + } + fs.rename(from, to.substring(toSlash + 1)); + return fs.exists(to) && !fs.exists(from); + } + + /// The parent of the storage path `path`, or null at a root. + private static String parentOf(String path) { + String p = path; + while (p.length() > 1 && p.endsWith("/")) { + p = p.substring(0, p.length() - 1); + } + int slash = lastSeparator(p); + if (slash <= 0) { + return null; + } + String parent = p.substring(0, slash); + // "file://" itself is no directory: the parent of /x is the root. + return parent.endsWith(":/") || parent.endsWith(":") ? parent + "/" : parent; + } + + private static int lastSeparator(String path) { + int slash = path.lastIndexOf('/'); + char sep = fs().getFileSystemSeparator(); + return sep == '/' ? slash : Math.max(slash, path.lastIndexOf(sep)); + } + + @Override + public String appHome() { + return fs().getAppHomePath(); + } + + @Override + public String cachesDir() { + FileSystemStorage fs = fs(); + return fs.hasCachesDir() ? fs.getCachesDir() : null; + } + + @Override + public String[] fileRoots() { + return fs().getRoots(); + } + + @Override + public long fileTotalSpace(String path) { + String root = rootOf(storage(path)); + return root == null ? 0L : fs().getRootSizeBytes(root); + } + + @Override + public long fileUsableSpace(String path) { + String root = rootOf(storage(path)); + return root == null ? 0L : fs().getRootAvailableSpace(root); + } + + /// The longest root `path` lies under, else the first root, else null. + private static String rootOf(String path) { + String[] roots = fs().getRoots(); + if (roots == null || roots.length == 0) { + return null; + } + String best = null; + for (String r : roots) { + if (r != null && path.startsWith(r) && (best == null || r.length() > best.length())) { + best = r; + } + } + return best != null ? best : roots[0]; + } + + @Override + public boolean fileTest(String path, int what) { + if (what == FILE_HIDDEN) { + String p = storage(path); + FileSystemStorage fs = fs(); + return fs.exists(p) && fs.isHidden(p); + } + return super.fileTest(path, what); + } + + @Override + public String preference(String key) { + return Preferences.get(key, (String) null); + } + + @Override + public void setPreference(String key, String value) { + Preferences.set(key, value); + } + + @Override + public void deletePreference(String key) { + Preferences.delete(key); + } + + @Override + public InputStream openUrl(String url) throws IOException { + ConnectionRequest request = new ConnectionRequest(); + request.setUrl(url); + request.setPost(false); + // Failures reach the caller as exceptions, not the user as the + // default error dialog. + request.setFailSilently(true); + NetworkManager.getInstance().addToQueueAndWait(request); + int code = request.getResponseCode(); + if (code == 404 || code == 410) { + throw new FileNotFoundException(url); + } + if (code >= 400) { + throw new IOException("Server returned HTTP response code: " + code + " for URL: " + url); + } + byte[] data = request.getResponseData(); + if (data == null) { + throw new IOException("No response from " + url); + } + return new ByteArrayInputStream(data); + } + + /// The levels of [JdkHost] are those of `Log`. + @Override + public void log(int level, String message, Throwable thrown) { + if (message != null) { + Log.p(message, Math.max(Log.DEBUG, Math.min(Log.ERROR, level))); + } + if (thrown != null) { + Log.e(thrown); + } + } + + /// `os.name` is the platform name; anything else is the display property + /// of that name. + @Override + public String property(String key) { + if (!Display.isInitialized()) { + return null; + } + if ("os.name".equals(key)) { + return Display.getInstance().getPlatformName(); + } + return Display.getInstance().getProperty(key, null); + } + + /// Closes the application the way the platform does; the status has + /// nowhere to go. + @Override + public void exit(int status) { + if (Display.isInitialized()) { + Display.getInstance().exitApplication(); + } + } + + /// Hands what an event dispatch thread task throws to the default + /// uncaught exception handler, when one is set, instead of the + /// platform's error handling; see + /// [com.codename1.impl.compat.UncaughtExceptions]. + @Override + public void watchUncaughtExceptions() { + com.codename1.impl.compat.UncaughtExceptions.watch(); + } + + @Override + public boolean isUiThread() { + return Display.isInitialized() && Display.getInstance().isEdt(); + } + + /// On the event dispatch thread the task runs through `invokeAndBlock`, + /// so the UI keeps serving events while it blocks. + @Override + public void blockUi(Runnable task) { + if (isUiThread()) { + Display.getInstance().invokeAndBlock(task); + } else { + task.run(); + } + } + + /// Through `com.codename1.impl.compat.Resources`, which knows the flat + /// name a nested resource ships under and the index the generated + /// registry recorded. + @Override + public InputStream resource(String name) { + return com.codename1.impl.compat.Resources.open(name); + } + + /// Derived from what `L10NManager` formats, since it exposes the + /// separators only through formatting. + @Override + public String[] localeNumberSymbols() { + if (!Display.isInitialized()) { + return null; + } + L10NManager l10n = L10NManager.getInstance(); + char decimal = firstSeparator(l10n.format(1.5, 1)); + char grouping = firstSeparator(l10n.format(1234567.0, 0)); + return new String[] { + decimal == 0 ? null : String.valueOf(decimal), + grouping == 0 || grouping == decimal ? null : String.valueOf(grouping), + emptyToNull(l10n.getCurrencySymbol()) + }; + } + + /// `null` for a null or empty string. A port's `L10NManager` may answer + /// either when it knows no currency symbol. + private static String emptyToNull(String s) { + return s == null || s.length() == 0 ? null : s; + } + + /// The first character of `text` that sits between two ASCII digits, or 0. + private static char firstSeparator(String text) { + if (text == null) { + return 0; + } + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (c < '0' || c > '9') { + boolean between = i > 0 && i + 1 < text.length() + && text.charAt(i + 1) >= '0' && text.charAt(i + 1) <= '9'; + return between ? c : 0; + } + } + return 0; + } + + @Override + public void randomBytes(byte[] out) { + com.codename1.security.SecureRandom.fill(out); + } + + @Override + public Digest digest(String algorithm) { + if (!Hash.MD5.equalsIgnoreCase(algorithm) && !Hash.SHA1.equalsIgnoreCase(algorithm) + && !Hash.SHA224.equalsIgnoreCase(algorithm) && !Hash.SHA256.equalsIgnoreCase(algorithm) + && !Hash.SHA384.equalsIgnoreCase(algorithm) && !Hash.SHA512.equalsIgnoreCase(algorithm)) { + return null; + } + try { + return new HashDigest(Hash.create(algorithm)); + } catch (com.codename1.security.CryptoException e) { + // A port without this digest; the class library reports the + // algorithm as unavailable. + return null; + } + } + + /// A core `Hash` as a [JdkHost.Digest]. + private static final class HashDigest implements Digest { + private final Hash hash; + + HashDigest(Hash hash) { + this.hash = hash; + } + + @Override + public void update(byte[] data, int offset, int length) { + hash.update(data, offset, length); + } + + @Override + public byte[] digest() { + return hash.digest(); + } + + @Override + public void reset() { + hash.reset(); + } + + @Override + public int length() { + return hash.digestLength(); + } + } +} diff --git a/CodenameOne/src/com/codename1/impl/CoreJdkHostInstaller.java b/CodenameOne/src/com/codename1/impl/CoreJdkHostInstaller.java new file mode 100644 index 00000000000..46bd3068572 --- /dev/null +++ b/CodenameOne/src/com/codename1/impl/CoreJdkHostInstaller.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl; + +/// Installs [CoreJdkHost] as the class library's [JdkHost]. +/// +/// Internal. The ParparVM ports call [#install] first thing in their `init`. +/// It is a class of its own so that a port names neither `JdkHost` nor +/// `CoreJdkHost`: compiling a call to a static method of `CoreJdkHost` would +/// need its superclass on the port's class path, and the Java SE based tools +/// that load `Display` or the implementation class never load either. +public final class CoreJdkHostInstaller { + private CoreJdkHostInstaller() { + } + + /// Makes [CoreJdkHost] the host the class library uses. + public static void install() { + JdkHost.install(new CoreJdkHost()); + } +} diff --git a/CodenameOne/src/com/codename1/impl/compat/CompatRegistryBase.java b/CodenameOne/src/com/codename1/impl/compat/CompatRegistryBase.java new file mode 100644 index 00000000000..938566d533c --- /dev/null +++ b/CodenameOne/src/com/codename1/impl/compat/CompatRegistryBase.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.compat; + +import com.codename1.impl.JdkHost; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/// What the build knows about one application's classes: the superclass of +/// the registry it generates for every application built from desktop or +/// Android sources, `com.codename1.generated.compat.CompatRegistry`. +/// +/// A device creates no class from its name and reads no `META-INF/services` +/// directory, so the class library's `java.util.ResourceBundle` and +/// `java.util.ServiceLoader` ask the installed [JdkHost.Registry] instead. +/// The generated subclass records, while it installs: +/// +/// - every resource the application ships, in [Resources]'s index; +/// - every class the application may need created by name -- its resource +/// bundle classes and its service providers -- under a number its +/// [#cn1Create(int)] turns into a `new` and its [#cn1Type(int)] into a +/// class constant; +/// - the providers of every service, in the order `ServiceLoader` offers +/// them. +/// +/// It then makes itself the registry `JdkHost` serves. The generated +/// `cn1Install()` does all of that once, however often it is called. +public abstract class CompatRegistryBase implements JdkHost.Registry { + + private final Map classes = new HashMap(); + private final Map> providers = new HashMap>(); + + /// For the generated subclass. + protected CompatRegistryBase() { + } + + /// Whether a generated registry is installed already. + public static boolean cn1Installed() { + return JdkHost.getRegistry() instanceof CompatRegistryBase; + } + + /// Records that the application has the class `className`, as + /// `Class.getName()` answers it, created and typed under `id`. + public final void cn1Class(String className, int id) { + classes.put(className, Integer.valueOf(id)); + } + + /// Records that `className` provides `service`, both as + /// `Class.getName()` answers them. A provider recorded twice for one + /// service is kept once, where it was first recorded. + public final void cn1Provider(String service, String className) { + List list = providers.get(service); + if (list == null) { + list = new ArrayList(); + providers.put(service, list); + } + if (!list.contains(className)) { + list.add(className); + } + } + + /// Makes this the registry `JdkHost` serves. + public final void cn1Activate() { + JdkHost.setRegistry(this); + } + + /// A new instance of the class recorded under `id`, or null. + protected abstract Object cn1Create(int id); + + /// The class recorded under `id`, or null. + protected abstract Class cn1Type(int id); + + @Override + public final String[] providers(String service) { + List list = providers.get(service); + if (list == null) { + return null; + } + return list.toArray(new String[list.size()]); + } + + @Override + public final Object newInstance(String className) { + Integer id = classes.get(className); + return id == null ? null : cn1Create(id.intValue()); + } + + @Override + public final Class type(String className) { + Integer id = classes.get(className); + return id == null ? null : cn1Type(id.intValue()); + } +} diff --git a/CodenameOne/src/com/codename1/impl/compat/ResourceNames.java b/CodenameOne/src/com/codename1/impl/compat/ResourceNames.java new file mode 100644 index 00000000000..987f3d2785e --- /dev/null +++ b/CodenameOne/src/com/codename1/impl/compat/ResourceNames.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.compat; + +/// How a classpath resource is named once it ships: the one definition the +/// build and the runtime share. +/// +/// A desktop application keeps its resources in package directories +/// (`com/example/img/logo.png`) and asks for them by that path. A Codename +/// One application bundle is flat: every resource sits at its root. The build +/// therefore ships each nested resource under a flat name, and the runtime +/// turns the path an application asks for into that same name. Both call +/// [#flatName(String)], so they cannot disagree; the build engine reads it +/// from the core for that reason. +/// +/// #### The rule +/// +/// - A path with no directory -- `logo.png` -- keeps its name. +/// - In any other path every `_` becomes `_u` and every `/` becomes `__`: +/// `com/example/img/my_logo.png` ships as +/// `com__example__img__my_ulogo.png`. +/// +/// `_` is an escape character there, so two different nested paths never +/// share a flat name, and the original path can be read back from it. The +/// file extension is the end of the last segment and is never touched, which +/// matters because a Codename One build treats some extensions specially. A +/// nested path can still produce the name of a file that is at the root on +/// its own account; the build reports that as an error rather than letting +/// one replace the other. +/// +/// This class depends on nothing but `java.lang`, and must stay that way. +public final class ResourceNames { + + private ResourceNames() { + } + + /// The resource path, absolute and without its leading slash, that + /// `Class.getResource(name)` means for a class called `className`: a name + /// starting with `/` is absolute, any other is relative to the class's + /// package. `.` and `..` segments are resolved. Answers null for a path + /// that climbs above the root or names nothing. + /// + /// #### Parameters + /// + /// - `className`: the class's name in either form, `com.example.Main` or + /// `com/example/Main`; null for the absolute lookup a class loader does + /// + /// - `name`: the name as the application passed it + public static String resolve(String className, String name) { + if (name == null) { + return null; + } + if (name.startsWith("/")) { + return normalize(name); + } + if (className == null) { + return normalize(name); + } + String cls = className.replace('.', '/'); + int slash = cls.lastIndexOf('/'); + return normalize(slash < 0 ? name : cls.substring(0, slash + 1) + name); + } + + /// `path` with leading slashes dropped, empty and `.` segments removed + /// and `..` segments resolved; null when it climbs above the root or is + /// empty once resolved. + public static String normalize(String path) { + if (path == null) { + return null; + } + int n = path.length(); + StringBuilder out = new StringBuilder(n); + int i = 0; + while (i < n) { + int end = path.indexOf('/', i); + if (end < 0) { + end = n; + } + int len = end - i; + if (len == 0 || (len == 1 && path.charAt(i) == '.')) { + // An empty segment or the directory itself: nothing to add. + i = end + 1; + continue; + } + if (len == 2 && path.charAt(i) == '.' && path.charAt(i + 1) == '.') { + if (out.length() == 0) { + return null; + } + int cut = out.length() - 1; + while (cut >= 0 && out.charAt(cut) != '/') { + cut--; + } + out.setLength(cut < 0 ? 0 : cut); + i = end + 1; + continue; + } + if (out.length() > 0) { + out.append('/'); + } + out.append(path.substring(i, end)); + i = end + 1; + } + return out.length() == 0 ? null : out.toString(); + } + + /// The name `path` ships under at the root of the application bundle. + /// `path` is a normalized resource path ([#normalize(String)]); see the + /// class description for the rule. + public static String flatName(String path) { + if (path.indexOf('/') < 0) { + return path; + } + int n = path.length(); + StringBuilder out = new StringBuilder(n + 16); + for (int i = 0; i < n; i++) { + char c = path.charAt(i); + if (c == '/') { + out.append("__"); + } else if (c == '_') { + out.append("_u"); + } else { + out.append(c); + } + } + return out.toString(); + } + + /// The nested path a flat name was made from, the inverse of + /// [#flatName(String)]; null when `flat` is not the flat name of any + /// nested path, which includes every name of a file that was at the root + /// all along. + public static String nestedPath(String flat) { + int n = flat.length(); + StringBuilder out = new StringBuilder(n); + boolean nested = false; + for (int i = 0; i < n; i++) { + char c = flat.charAt(i); + if (c != '_') { + out.append(c); + continue; + } + if (i + 1 >= n) { + return null; + } + char next = flat.charAt(++i); + if (next == '_') { + out.append('/'); + nested = true; + } else if (next == 'u') { + out.append('_'); + } else { + return null; + } + } + return nested ? out.toString() : null; + } +} diff --git a/CodenameOne/src/com/codename1/impl/compat/Resources.java b/CodenameOne/src/com/codename1/impl/compat/Resources.java new file mode 100644 index 00000000000..977676664ce --- /dev/null +++ b/CodenameOne/src/com/codename1/impl/compat/Resources.java @@ -0,0 +1,564 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.compat; + +import com.codename1.impl.JdkHost; +import com.codename1.ui.Display; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.ResourceBundle; +import java.util.ServiceLoader; +import java.util.Set; + +/// Classpath resources for an application built from desktop or Android +/// sources: what `Class.getResource`, `Class.getResourceAsStream` and the +/// `ClassLoader` lookups do on a desktop. `com.codename1.impl.CoreJdkHost` +/// reads application resources through [#open(String)] as well, so the class +/// library's `java.util.ResourceBundle`, `java.util.ServiceLoader` and +/// `cn1res:` URLs find what the build shipped. +/// +/// Nothing calls these methods by name. The build's remap step rewrites an +/// application's calls so that they arrive here: `getClass().getResource(n)` +/// becomes `Resources.getResource(getClass(), n)`, and so on for each method +/// below that takes the original receiver as its first parameter. +/// +/// #### How a resource is found +/// +/// A device has no classpath and no directories: every resource of the +/// application sits at the root of its bundle. The build ships each nested +/// resource under the flat name [ResourceNames#flatName(String)] gives its +/// path, and records the paths in an index (the registry generated with the +/// application, `com.codename1.generated.compat.CompatRegistry`, which calls +/// [#cn1BeginIndex()] and [#cn1AddResource(String)]). A lookup resolves +/// the name the way the JDK does -- relative to the class's package unless it +/// starts with `/`, with `.` and `..` resolved -- answers null when the index +/// has no such path, and otherwise reads the flat name. Nothing is opened to +/// find out whether a nested resource exists. +/// +/// A name with no directory is not flattened, so the build's index may not +/// know it (a file from the ordinary Codename One resource directory is one +/// the desktop sources never listed). Such a name is looked for in the +/// bundle. +/// +/// Where no build step ran -- this module's own tests, a layer's tests -- no +/// index is installed, and a resource is read under the path that was asked +/// for, then under its flat name. +/// +/// #### URLs +/// +/// `getResource` answers a [URL] in the `cn1res:` scheme whose path is the +/// resource's ORIGINAL absolute path: `cn1res:/com/example/img/logo.png`. +/// `openStream()` reads it through this class, and `toString()`, +/// `toExternalForm()`, `getPath()` and `getFile()` are the same on every +/// platform and every run, so code that passes `url.toExternalForm()` to an +/// image or stylesheet loader keeps working: the loader opens the string as a +/// URL again, or hands its path to [#open(String)]. +/// +/// The device's own `java.net.URL` knows the scheme. A port that runs on a +/// real JDK, whose `URL` does not, installs a [UrlFactory] that attaches a +/// handler reading through [#openUrlPath(String)]. +/// +/// #### Bundles and services on a real JDK +/// +/// The JDK's `ResourceBundle` and `ServiceLoader` look a nested name up +/// through a class loader, which cannot see a resource the build flattened. +/// A port on a real JDK therefore installs, with +/// [#cn1SetClassLoader(ClassLoader)], a loader that falls back to this +/// class, and the build sends an application's `ResourceBundle.getBundle` +/// and `ServiceLoader.load` calls to the methods here that pass it. Where +/// none is installed -- a device, whose class library reads the generated +/// registry -- those methods are the calls they stand in for. +/// +/// #### Class names +/// +/// A relative name is resolved against `Class.getName()`. A build that +/// renames classes after the remap step (an obfuscator) changes that name, +/// so relative lookups need the application's classes kept under their +/// names; absolute names are unaffected. +public final class Resources { + + /// The paths the build shipped, or null when no build step recorded any. + private static Set index; + + private static Provider provider; + + private static ClassLoader contextLoader; + + private static UrlFactory urlFactory; + + private static ClassLoader portLoader; + + private Resources() { + } + + /// Where the bytes of a flat resource name come from. The default reads + /// the application bundle; a test installs its own with + /// [#cn1SetProvider(Provider)]. + public interface Provider { + /// The resource shipped as `flatName` (no leading slash), or null + /// when the bundle has none. + InputStream open(String flatName) throws IOException; + } + + /// Replaces the source of resource bytes; null restores the default. + public static void cn1SetProvider(Provider p) { + provider = p; + } + + /// Makes the `cn1res:` URLs [#getResource(Class, String)] answers, for a + /// port whose `java.net.URL` does not know the scheme. + public interface UrlFactory { + /// The `cn1res:` URL of `encodedPath`, an absolute path that is + /// already percent-encoded; [Resources#openUrlPath(String)] reads it. + URL create(String encodedPath) throws IOException; + } + + /// Installs the port's URL factory; null restores the default + /// `new URL("cn1res", null, path)`. + public static void cn1SetUrlFactory(UrlFactory factory) { + urlFactory = factory; + } + + /// Installs the class loader a port on a real JDK resolves flattened + /// resources through; see the class description. Null removes it. + public static void cn1SetClassLoader(ClassLoader loader) { + portLoader = loader; + } + + /// The loader [#cn1SetClassLoader(ClassLoader)] installed, or null. + public static ClassLoader cn1ClassLoader() { + return portLoader; + } + + /// Opens the resource a `cn1res:` URL names by `encodedPath`, its + /// percent-encoded path: what such a URL's stream reads. + public static InputStream openUrlPath(String encodedPath) { + String path = decode(encodedPath); + return path == null ? null : open(path); + } + + /// `ResourceBundle.getBundle(baseName)`, through the port's loader when + /// one is installed. + public static ResourceBundle getBundle(String baseName) { + return getBundle(baseName, Locale.getDefault()); + } + + /// `ResourceBundle.getBundle(baseName, locale)`, through the port's + /// loader when one is installed. + public static ResourceBundle getBundle(String baseName, Locale locale) { + ClassLoader loader = portLoader; + if (loader == null) { + return ResourceBundle.getBundle(baseName, locale); + } + return ResourceBundle.getBundle(baseName, locale, loader); + } + + /// The folder of the services files `ServiceLoader` reads on a JDK. + public static final String SERVICES = "META-INF/services/"; + + /// For a port on a real JDK, whose `ServiceLoader` reads services files + /// through the installed loader: the services file to read for `service` + /// (dotted, as `Class.getName()` answers it), listing what the generated + /// registry lists -- the application's providers, then each bundled + /// library's, and only those that ship, as on a device. Empty when the + /// registry lists none; null when no generated registry is installed, and + /// the files on the class path are all there is. + public static String cn1ServicesFile(String service) { + if (!CompatRegistryBase.cn1Installed()) { + return null; + } + String[] providers = JdkHost.getRegistry().providers(service); + StringBuilder out = new StringBuilder(); + if (providers != null) { + for (String provider : providers) { + out.append(provider).append('\n'); + } + } + return out.toString(); + } + + /// `ServiceLoader.load(service)`, through the port's loader when one is + /// installed. + public static ServiceLoader load(Class service) { + ClassLoader loader = portLoader; + if (loader == null) { + return ServiceLoader.load(service); + } + return ServiceLoader.load(service, loader); + } + + /// Starts an index: from here on a nested path exists only if + /// [#cn1AddResource(String)] named it. The generated registry calls + /// this once, then adds every path. + public static void cn1BeginIndex() { + index = new HashSet(); + } + + /// Records that the build shipped the resource at `path`, an absolute + /// path without its leading slash. Only after [#cn1BeginIndex()]: a path + /// added to no index would be one forgotten in silence. + public static void cn1AddResource(String path) { + Set current = index; + if (current == null) { + throw new IllegalStateException("cn1BeginIndex() has not been called"); + } + current.add(path); + } + + /// Every path the build recorded, in no particular order; empty when + /// no build step recorded any. A loader that has to find a resource by + /// what is IN it -- a font by the name it gives itself -- reads these. + public static String[] cn1ResourcePaths() { + Set current = index; + if (current == null) { + return new String[0]; + } + String[] out = new String[current.size()]; + int i = 0; + for (String path : current) { + out[i++] = path; + } + return out; + } + + /// Forgets the index, as if no build step had run. For tests. + public static void cn1ClearIndex() { + index = null; + } + + /// `Class.getResourceAsStream(name)` for `cls`. + public static InputStream getResourceAsStream(Class cls, String name) { + return open(ResourceNames.resolve(cls.getName(), requireName(name))); + } + + /// `Class.getResource(name)` for `cls`. + public static URL getResource(Class cls, String name) { + return url(ResourceNames.resolve(cls.getName(), requireName(name))); + } + + /// `ClassLoader.getResourceAsStream(name)`. An application has one + /// loader, so `loader` only has to be non-null. As on a desktop, a class + /// loader's names are absolute and written WITHOUT a leading slash; one + /// that has it finds nothing. + public static InputStream getResourceAsStream(ClassLoader loader, String name) { + requireLoader(loader); + return getSystemResourceAsStream(name); + } + + /// `ClassLoader.getResource(name)`; see + /// [#getResourceAsStream(ClassLoader, String)]. + public static URL getResource(ClassLoader loader, String name) { + requireLoader(loader); + return getSystemResource(name); + } + + /// `ClassLoader.getResources(name)`: the one resource of that name, or + /// none. + public static Enumeration getResources(ClassLoader loader, String name) { + requireLoader(loader); + return getSystemResources(name); + } + + /// `ClassLoader.getSystemResourceAsStream(name)`. + public static InputStream getSystemResourceAsStream(String name) { + return requireName(name).startsWith("/") ? null : open(ResourceNames.normalize(name)); + } + + /// `ClassLoader.getSystemResource(name)`. + public static URL getSystemResource(String name) { + return requireName(name).startsWith("/") ? null : url(ResourceNames.normalize(name)); + } + + /// `ClassLoader.getSystemResources(name)`. + public static Enumeration getSystemResources(String name) { + List found = new ArrayList(1); + URL url = getSystemResource(name); + if (url != null) { + found.add(url); + } + return new UrlEnumeration(found); + } + + /// `Thread.getContextClassLoader()`: the application's one class loader, + /// or whatever [#setContextClassLoader(Thread, ClassLoader)] was last + /// given. It is good for the resource lookups above and for nothing + /// else; the build rejects any other use of a class loader. + public static ClassLoader getContextClassLoader(Thread thread) { + if (thread == null) { + throw new NullPointerException(); + } + if (contextLoader != null) { + return contextLoader; + } + return portLoader != null ? portLoader : ClassLoader.getSystemClassLoader(); + } + + /// `loader.loadClass(name)`: the class of that name when the generated + /// registry knows it -- the application's resource bundle classes and + /// service providers, the classes it may need by name. Anything else + /// throws `ClassNotFoundException`, as a desktop does for a class that is + /// absent, which is how desktop code probing for an optional class reads + /// it. No class is looked up by its string name: a device loads nothing + /// at run time, and an obfuscated Android or JavaScript build has renamed + /// it, so the answer is the registry's on every port alike. + public static Class loadClass(ClassLoader loader, String name) throws ClassNotFoundException { + if (loader == null || name == null) { + throw new NullPointerException(); + } + JdkHost.Registry registry = JdkHost.getRegistry(); + Class type = registry == null ? null : registry.type(name); + if (type == null) { + throw new ClassNotFoundException(name); + } + return type; + } + + /// `Thread.setContextClassLoader(loader)`. There is one loader, so this + /// only remembers the object for [#getContextClassLoader(Thread)] to + /// hand back, for every thread alike. + public static void setContextClassLoader(Thread thread, ClassLoader loader) { + if (thread == null) { + throw new NullPointerException(); + } + contextLoader = loader; + } + + /// Whether the application ships a resource at `path`, an absolute path + /// with or without its leading slash. + public static boolean exists(String path) { + String p = ResourceNames.normalize(path); + if (p == null) { + return false; + } + if (index != null && (index.contains(p) || p.indexOf('/') >= 0)) { + return index.contains(p); + } + // Closed just below; PMD does not follow a close inside a catch. + InputStream in = read(p); //NOPMD CloseResource + if (in == null) { + return false; + } + try { + in.close(); + } catch (IOException e) { + // It was there to be opened, which is all that was asked. + } + return true; + } + + /// Opens the resource at `path`, an absolute path with or without its + /// leading slash, as the application named it on a desktop + /// (`/com/example/img/logo.png`). Answers null when there is none. This + /// is what a `cn1res:` URL reads, and what a loader that was handed such + /// a URL's path should call. + public static InputStream open(String path) { + String p = ResourceNames.normalize(path); + if (p == null) { + return null; + } + if (index != null && p.indexOf('/') >= 0 && !index.contains(p)) { + return null; + } + return read(p); + } + + private static InputStream read(String p) { + boolean nested = p.indexOf('/') >= 0; + if (index == null && nested && provider == null) { + // Nothing was flattened by a build: the resource may still be + // where the application's own build put it. + InputStream direct = Resources.class.getResourceAsStream("/" + p); + if (direct != null) { + return direct; + } + } + String flat = ResourceNames.flatName(p); + if (provider != null) { + try { + return provider.open(flat); + } catch (IOException e) { + // A resource that cannot be read is one that is not there. + return null; + } + } + if (Display.isInitialized()) { + return Display.getInstance().getResourceAsStream(Resources.class, "/" + flat); + } + return Resources.class.getResourceAsStream("/" + flat); + } + + private static URL url(String p) { + if (p == null || !exists(p)) { + return null; + } + String encoded = "/" + encode(p); + UrlFactory factory = urlFactory; + try { + if (factory != null) { + return factory.create(encoded); + } + return new URL("cn1res", null, encoded); + } catch (MalformedURLException e) { + // The device's URL knows cn1res, and a port that runs elsewhere + // installs a factory; a URL nobody can make is no resource. + return null; + } catch (IOException e) { + return null; + } + } + + /// The inverse of [#encode(String)]: UTF-8 percent escapes decoded, + /// anything else as it stands. Null for a malformed escape. + static String decode(String encoded) { + if (encoded == null || encoded.indexOf('%') < 0) { + return encoded; + } + int n = encoded.length(); + byte[] bytes = new byte[n]; + StringBuilder out = new StringBuilder(n); + int i = 0; + while (i < n) { + char c = encoded.charAt(i); + if (c != '%') { + out.append(c); + i++; + continue; + } + int count = 0; + while (i < n && encoded.charAt(i) == '%') { + if (i + 2 >= n) { + return null; + } + int high = Character.digit(encoded.charAt(i + 1), 16); + int low = Character.digit(encoded.charAt(i + 2), 16); + if (high < 0 || low < 0) { + return null; + } + bytes[count++] = (byte) ((high << 4) | low); + i += 3; + } + try { + out.append(new String(bytes, 0, count, "UTF-8")); + } catch (java.io.UnsupportedEncodingException e) { + return null; + } + } + return out.toString(); + } + + private static String requireName(String name) { + if (name == null) { + throw new NullPointerException(); + } + return name; + } + + private static void requireLoader(ClassLoader loader) { + if (loader == null) { + throw new NullPointerException(); + } + } + + private static final String HEX = "0123456789ABCDEF"; + + /// Percent-encodes what would otherwise end a URL's path or be read as + /// an escape: everything but letters, digits and `/ - . _ ~ $ + ! ' ( ) + /// , @ = & ;`, as UTF-8. + static String encode(String path) { + int n = path.length(); + StringBuilder out = null; + for (int i = 0; i < n; i++) { + char c = path.charAt(i); + boolean plain = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') + || "/-._~$+!'(),@=&;".indexOf(c) >= 0; + if (plain) { + if (out != null) { + out.append(c); + } + continue; + } + if (out == null) { + out = new StringBuilder(n + 16); + out.append(path.substring(0, i)); + } + int code = c; + if (c >= 0xd800 && c <= 0xdbff && i + 1 < n) { + char low = path.charAt(i + 1); + if (low >= 0xdc00 && low <= 0xdfff) { + code = 0x10000 + ((c - 0xd800) << 10) + (low - 0xdc00); + i++; + } + } + if (code < 0x80) { + hex(out, code); + } else if (code < 0x800) { + hex(out, 0xc0 | (code >> 6)); + hex(out, 0x80 | (code & 0x3f)); + } else if (code < 0x10000) { + hex(out, 0xe0 | (code >> 12)); + hex(out, 0x80 | ((code >> 6) & 0x3f)); + hex(out, 0x80 | (code & 0x3f)); + } else { + hex(out, 0xf0 | (code >> 18)); + hex(out, 0x80 | ((code >> 12) & 0x3f)); + hex(out, 0x80 | ((code >> 6) & 0x3f)); + hex(out, 0x80 | (code & 0x3f)); + } + } + return out == null ? path : out.toString(); + } + + private static void hex(StringBuilder out, int b) { + out.append('%').append(HEX.charAt((b >> 4) & 0xf)).append(HEX.charAt(b & 0xf)); + } + + private static final class UrlEnumeration implements Enumeration { + private final List urls; + private int next; + + UrlEnumeration(List urls) { + this.urls = urls; + } + + @Override + public boolean hasMoreElements() { + return next < urls.size(); + } + + @Override + public URL nextElement() { + if (next >= urls.size()) { + throw new java.util.NoSuchElementException(); + } + return urls.get(next++); + } + } +} diff --git a/CodenameOne/src/com/codename1/impl/compat/UncaughtExceptions.java b/CodenameOne/src/com/codename1/impl/compat/UncaughtExceptions.java new file mode 100644 index 00000000000..e8af8292405 --- /dev/null +++ b/CodenameOne/src/com/codename1/impl/compat/UncaughtExceptions.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.compat; + +import com.codename1.ui.Display; +import com.codename1.ui.events.ActionEvent; +import com.codename1.ui.events.ActionListener; + +/// Makes the default uncaught exception handler an application sets see what +/// its event dispatch thread tasks throw, as a desktop's does. +/// +/// An exception escaping a task on the Codename One EDT never reaches a +/// thread's uncaught handler: the platform's EDT error handling catches it +/// first. So the first time a handler is installed after the display is +/// up, an EDT error handler is registered that hands the throwable to the +/// default handler as it stands when the error happens, and consumes it. +/// +/// The build sends an application's `Thread.setDefaultUncaughtExceptionHandler` +/// calls to [#setDefaultUncaughtExceptionHandler], so this happens on every +/// platform, including the ones whose `Thread` is the JDK's. Only the +/// application's own call registers the forwarder: a handler a platform +/// installed for itself (Android has one that ends the process) does not. +/// The device's class library calls [#watch()] from its own `Thread`, which +/// shares the one registration. +/// +/// Internal: the build and the class library call this; an application +/// does not. +public final class UncaughtExceptions { + + private static boolean watched; + + private UncaughtExceptions() { + } + + /// `Thread.setDefaultUncaughtExceptionHandler(handler)`, and then + /// [#watch()] when `handler` is not null. + public static void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler handler) { + Thread.setDefaultUncaughtExceptionHandler(handler); + if (handler != null) { + watch(); + } + } + + /// Registers the forwarder, once, when the display is initialized. A + /// call before then does nothing, and a later one registers it. + public static void watch() { + if (watched || !Display.isInitialized()) { + return; + } + watched = true; + Display.getInstance().addEdtErrorHandler(new EdtErrorForwarder()); + } + + /// Forwards an event dispatch thread error to the default uncaught + /// exception handler, as it stands when the error happens. + private static final class EdtErrorForwarder implements ActionListener { + @Override + public void actionPerformed(ActionEvent evt) { + Thread.UncaughtExceptionHandler handler = Thread.getDefaultUncaughtExceptionHandler(); + Object source = evt.getSource(); + if (handler != null && source instanceof Throwable) { + evt.consume(); + handler.uncaughtException(Thread.currentThread(), (Throwable) source); + } + } + } +} diff --git a/CodenameOne/src/com/codename1/impl/compat/osgi/Bundle.java b/CodenameOne/src/com/codename1/impl/compat/osgi/Bundle.java new file mode 100644 index 00000000000..dab3ecaa3a2 --- /dev/null +++ b/CodenameOne/src/com/codename1/impl/compat/osgi/Bundle.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.compat.osgi; + +/// `org.osgi.framework.Bundle`, as the type [FrameworkUtil#getBundle(Class)] +/// is declared to answer. Nothing implements it: see the package description. +public interface Bundle { + + /// The bundle's execution context. + BundleContext getBundleContext(); +} diff --git a/CodenameOne/src/com/codename1/impl/compat/osgi/BundleContext.java b/CodenameOne/src/com/codename1/impl/compat/osgi/BundleContext.java new file mode 100644 index 00000000000..bf4415b23fa --- /dev/null +++ b/CodenameOne/src/com/codename1/impl/compat/osgi/BundleContext.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.compat.osgi; + +/// `org.osgi.framework.BundleContext`, with the two members a library uses +/// to fetch one service. Nothing implements it: see the package description. +public interface BundleContext { + + /// A reference to a service registered under `clazz`, or null. + ServiceReference getServiceReference(Class clazz); + + /// The service `reference` refers to, or null. + S getService(ServiceReference reference); +} diff --git a/CodenameOne/src/com/codename1/impl/compat/osgi/FrameworkUtil.java b/CodenameOne/src/com/codename1/impl/compat/osgi/FrameworkUtil.java new file mode 100644 index 00000000000..bf0be867717 --- /dev/null +++ b/CodenameOne/src/com/codename1/impl/compat/osgi/FrameworkUtil.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.compat.osgi; + +/// `org.osgi.framework.FrameworkUtil`, outside any framework: see the +/// package description. Only [#getBundle(Class)] is here. +public final class FrameworkUtil { + + private FrameworkUtil() { + } + + /// Null, for every class: no class of the application was loaded by an + /// OSGi bundle, which is how the real method says "not running in OSGi". + public static Bundle getBundle(Class classFromBundle) { + return null; + } +} diff --git a/CodenameOne/src/com/codename1/impl/compat/osgi/ServiceReference.java b/CodenameOne/src/com/codename1/impl/compat/osgi/ServiceReference.java new file mode 100644 index 00000000000..a55baaf6d82 --- /dev/null +++ b/CodenameOne/src/com/codename1/impl/compat/osgi/ServiceReference.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.compat.osgi; + +/// `org.osgi.framework.ServiceReference`, as a type only. Nothing implements +/// it: see the package description. +public interface ServiceReference { +} diff --git a/CodenameOne/src/com/codename1/impl/compat/osgi/package-info.java b/CodenameOne/src/com/codename1/impl/compat/osgi/package-info.java new file mode 100644 index 00000000000..a3cbd21afdd --- /dev/null +++ b/CodenameOne/src/com/codename1/impl/compat/osgi/package-info.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +/// The four OSGi types a library asks, to find out whether it is running in +/// an OSGi framework: [FrameworkUtil], [Bundle], [BundleContext] and +/// [ServiceReference], with only the members that question takes. +/// +/// A library that supports OSGi and the plain class path names these types +/// in the one method that probes for a framework, and its jar declares the +/// OSGi API an optional dependency. The build's remap step points +/// `org.osgi.framework` at this package, so that such a library links, and +/// [FrameworkUtil#getBundle(Class)] answers what the real one answers for a +/// class no framework loaded: null. The library then takes its class path +/// route. +/// +/// This is not an OSGi implementation and will not grow into one. No bundle, +/// context or service reference is ever created; the three interfaces exist +/// so that the probe's descriptors resolve. +package com.codename1.impl.compat.osgi; diff --git a/CodenameOne/src/com/codename1/impl/compat/package-info.java b/CodenameOne/src/com/codename1/impl/compat/package-info.java new file mode 100644 index 00000000000..1625cf6433b --- /dev/null +++ b/CodenameOne/src/com/codename1/impl/compat/package-info.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +/// What an application built from desktop or Android sources needs of the +/// core at run time: the classpath resources it ships, under the flat names a +/// device bundle gives them ([ResourceNames], [Resources]), and the +/// superclass of the registry its build generates ([CompatRegistryBase]), +/// through which the class library creates the resource bundles and service +/// providers it has. +/// +/// Internal: the build and generated code call these classes; an +/// application does not. +package com.codename1.impl.compat; diff --git a/CodenameOne/src/com/codename1/impl/process/RuntimeExec.java b/CodenameOne/src/com/codename1/impl/process/RuntimeExec.java new file mode 100644 index 00000000000..be97b4ad400 --- /dev/null +++ b/CodenameOne/src/com/codename1/impl/process/RuntimeExec.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.process; + +import com.codename1.io.Process; +import com.codename1.io.ProcessBuilder; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/// `Runtime.exec`, for desktop code relocated onto [ProcessBuilder]. +/// +/// The device runtime's `java.lang.Runtime` has no `exec`, so the build +/// rewrites `runtime.exec(...)` into a call here that takes the receiver +/// first, the way it rewrites every instance member the device lacks. Each +/// method does what the JavaSE overload of the same parameters does: a +/// command string is split at whitespace with no quoting (a shell is never +/// implied), and an environment array, when not null, replaces the inherited +/// environment entirely, and a null working directory inherits the parent's. +/// @hidden +public final class RuntimeExec { + private RuntimeExec() { + } + + /// `Runtime.exec(String)`. + public static Process exec(Runtime runtime, String command) throws IOException { + return exec(runtime, command, null); + } + + /// `Runtime.exec(String, String[])`. + public static Process exec(Runtime runtime, String command, String[] envp) throws IOException { + return exec(runtime, command, envp, null); + } + + /// `Runtime.exec(String, String[], File)`. + public static Process exec(Runtime runtime, String command, String[] envp, File dir) throws IOException { + if (command == null) { + throw new NullPointerException("command"); + } + if (command.length() == 0) { + throw new IllegalArgumentException("Empty command"); + } + return exec(runtime, split(command), envp, dir); + } + + /// `Runtime.exec(String[])`. + public static Process exec(Runtime runtime, String[] cmdarray) throws IOException { + return exec(runtime, cmdarray, null); + } + + /// `Runtime.exec(String[], String[])`. + public static Process exec(Runtime runtime, String[] cmdarray, String[] envp) throws IOException { + return exec(runtime, cmdarray, envp, null); + } + + /// `Runtime.exec(String[], String[], File)`. + public static Process exec(Runtime runtime, String[] cmdarray, String[] envp, File dir) throws IOException { + if (runtime == null) { + // What calling exec on a null Runtime throws. + throw new NullPointerException("runtime"); + } + if (cmdarray == null) { + throw new NullPointerException("cmdarray"); + } + if (cmdarray.length == 0) { + // JavaSE reads cmdarray[0] unguarded. + throw new IndexOutOfBoundsException("Empty command"); + } + ProcessBuilder builder = new ProcessBuilder(cmdarray); + if (envp != null) { + Map env = builder.environment(); + env.clear(); + for (String raw : envp) { + // As JavaSE: anything after a NUL is dropped and an entry with + // no '=' after its first character is skipped, not rejected. + int nul = raw.indexOf('\0'); + String entry = nul >= 0 ? raw.substring(0, nul) : raw; + int split = entry.indexOf('=', 1); + if (split > 0) { + env.put(entry.substring(0, split), entry.substring(split + 1)); + } + } + } + builder.directory(dir); + return builder.start(); + } + + /// The words of `command`, as `java.util.StringTokenizer` splits it with + /// its default delimiters: space, tab, newline, carriage return, form feed. + static String[] split(String command) { + List words = new ArrayList(); + int start = -1; + int n = command.length(); + for (int i = 0; i < n; i++) { + if (isDelimiter(command.charAt(i))) { + if (start >= 0) { + words.add(command.substring(start, i)); + start = -1; + } + } else if (start < 0) { + start = i; + } + } + if (start >= 0) { + words.add(command.substring(start)); + } + return words.toArray(new String[words.size()]); + } + + private static boolean isDelimiter(char c) { + return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f'; + } +} diff --git a/CodenameOne/src/com/codename1/io/Process.java b/CodenameOne/src/com/codename1/io/Process.java index 59e26c53795..02b2b889577 100644 --- a/CodenameOne/src/com/codename1/io/Process.java +++ b/CodenameOne/src/com/codename1/io/Process.java @@ -32,6 +32,16 @@ /// blocking stream I/O on the EDT. Interrupting a wait does not kill the child. /// Unlike JavaSE, this class is AutoCloseable: close forcibly terminates the direct /// child and closes its three streams. Descendants are not terminated. +/// +/// Every member this class shares with `java.lang.Process` has the same name, +/// parameters and modifiers, and no method is abstract here that is concrete +/// there, so a desktop class compiled against `java.lang.Process` -- a caller +/// or a subclass -- runs against this one once its class references are +/// renamed. The JavaSE members with no counterpart are `toHandle()`, +/// `onExit()`, `info()`, `children()`, `descendants()`, +/// `supportsNormalTermination()` and the reader and writer accessors: their +/// types (`ProcessHandle`, `CompletableFuture`, `BufferedReader`) are not part +/// of the device runtime. @com.codename1.impl.SharedWithBackend public abstract class Process implements AutoCloseable { /// The child's stdin (the parent writes to it). @@ -44,8 +54,17 @@ public abstract class Process implements AutoCloseable { public abstract int exitValue(); /// Requests termination of the direct child. On Windows this is forcible. public abstract void destroy(); - /// Forcibly terminates the direct child; completion is asynchronous. - public abstract Process destroyForcibly(); + /// Forcibly terminates the direct child; completion is asynchronous. A + /// subclass with no forcible kill inherits [#destroy()], as in JavaSE. + public Process destroyForcibly() { + destroy(); + return this; + } + /// The operating system's id for the child. A port that cannot name it + /// throws UnsupportedOperationException, which is also JavaSE's default. + public long pid() { + throw new UnsupportedOperationException(getClass().getName() + ".pid() not supported"); + } /// Whether the direct child is still running. public boolean isAlive() { try { diff --git a/CodenameOne/src/com/codename1/io/ProcessBuilder.java b/CodenameOne/src/com/codename1/io/ProcessBuilder.java index c6ae14b674d..69fac7401ec 100644 --- a/CodenameOne/src/com/codename1/io/ProcessBuilder.java +++ b/CodenameOne/src/com/codename1/io/ProcessBuilder.java @@ -24,6 +24,7 @@ import com.codename1.impl.ImplementationFactory; import com.codename1.impl.process.ProcessSupport; +import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; @@ -31,7 +32,7 @@ import java.util.Map; /// Creates desktop/backend subprocesses using JavaSE-style command lists and pipes. -/// The working directory is an OS path String, not a Codename One file URL. +/// The working directory is a `java.io.File`, as in JavaSE. /// Shell parsing is never implicit: invoke a shell explicitly if needed. /// Mobile/browser ports compile this API but start() throws /// UnsupportedOperationException. Support does not imply permission to run every @@ -39,10 +40,18 @@ /// /// File redirection and inheritIO are not provided in this initial subset. Use /// the streams and drain stdout/stderr concurrently, or merge them first. +/// +/// The members shared with `java.lang.ProcessBuilder` have its names and +/// parameter types, so desktop code compiled against the JavaSE class runs +/// against this one once its class references are renamed. JavaSE members +/// with no counterpart: `inheritIO()`, the `redirectInput`/`redirectOutput`/`redirectError` +/// family with `ProcessBuilder.Redirect` (no port can attach a child's +/// streams to anything but pipes), and `startPipeline(List)`, which needs +/// those redirects. @com.codename1.impl.SharedWithBackend public final class ProcessBuilder { private List command; - private String directory; + private File directory; private Map environment; private boolean mergeError; @@ -82,15 +91,12 @@ public ProcessBuilder command(String... command) { } return this; } - /// Null inherits the parent's working directory. Paths are native OS paths. - public ProcessBuilder directory(String directory) { - if (directory != null) { - checkString(directory); - } + /// Null inherits the parent's working directory. + public ProcessBuilder directory(File directory) { this.directory = directory; return this; } - public String directory() { + public File directory() { return directory; } /// A mutable copy of the inherited environment, initialized on first access. @@ -143,7 +149,34 @@ public Process start() throws IOException { } } } - return support.start(args, directory, env, mergeError); + String dir = null; + if (directory != null) { + dir = osPath(directory.getAbsolutePath()); + checkString(dir); + } + return support.start(args, dir, env, mergeError); + } + + /// The OS path of an absolute `File` path. On a port whose `java.io.File` + /// sits over `FileSystemStorage` that path can carry the `file:` scheme, + /// which a child process's working directory cannot. + static String osPath(String path) { + if (!path.startsWith("file:")) { + return path; + } + int i = 5; + while (i + 1 < path.length() && path.charAt(i) == '/' && path.charAt(i + 1) == '/') { + i++; + } + String p = path.substring(i); + // file:///C:/x names the drive path C:/x. + if (p.length() >= 4 && p.charAt(0) == '/' && p.charAt(2) == ':' && p.charAt(3) == '/') { + char d = p.charAt(1); + if ((d >= 'A' && d <= 'Z') || (d >= 'a' && d <= 'z')) { + return p.substring(1); + } + } + return p; } private static void checkString(String value) { if (value == null) { diff --git a/CodenameOne/src/com/codename1/ui/Image.java b/CodenameOne/src/com/codename1/ui/Image.java index e5824e00cf9..6a6f041a264 100644 --- a/CodenameOne/src/com/codename1/ui/Image.java +++ b/CodenameOne/src/com/codename1/ui/Image.java @@ -1469,6 +1469,13 @@ public Image modifyAlpha(byte alpha, int removeColor) { public Graphics getGraphics() { Graphics g = new Graphics(Display.impl.getNativeGraphics(image)); rgbCache = null; // the cache will become invalid + // So do the scaled copies. A port that cannot draw an image at a size + // draws scaled(w, h) instead, which is a copy this image keeps -- at + // the image's own size too. An image drawn on again after it had been + // shown once went on showing the copy made the first time: on the + // native Linux port a picture that was drawn in steps stayed as it was + // after the first one, until the collector happened to drop the copy. + scaleCache = null; return g; } diff --git a/CodenameOne/src/com/codename1/ui/geom/GeneralPath.java b/CodenameOne/src/com/codename1/ui/geom/GeneralPath.java index ee90cc09155..e0c43d89c6c 100644 --- a/CodenameOne/src/com/codename1/ui/geom/GeneralPath.java +++ b/CodenameOne/src/com/codename1/ui/geom/GeneralPath.java @@ -1971,8 +1971,23 @@ private static Shape intersection(Rectangle r, Shape s, GeneralPath out) { maxY = (float) (boundingRect.getY() + boundingRect.getHeight()); } if (rect2D.contains(curve.x(0.5), curve.y(0.5))) { - // It is fully inside, we can just add it to the out path - curve.addToPath(out, false); + // It is fully inside, we can just add it to the out path. + // It CONTINUES the subpath: passing join == false here + // made every curve open a subpath of its own, so a shape + // with a curved corner came back as one sliver per corner + // (the corner and the edge after it, closed across the + // shape) and its inside was no longer inside. Every port + // that narrows a shape clip with clipRect goes through + // this, which is how a gradient filled through a rounded + // rectangle was drawn as a hollow, skewed outline. The + // segment before may have ended outside and been clamped + // to the rectangle, in which case the outline first runs + // from there to where the curve really starts. + if (started && (Math.abs(prevX - origPrevX) > epsilon + || Math.abs(prevY - origPrevY) > epsilon)) { + out.lineTo((float) origPrevX, (float) origPrevY); + } + curve.addToPath(out, started); prevX = (float) curveEnd.getX(); prevY = (float) curveEnd.getY(); origPrevX = curveEnd.getX(); diff --git a/vm/JavaAPI/src/java/util/regex/MatchResult.java b/CodenameOne/src/com/codename1/util/regex/MatchResult.java similarity index 77% rename from vm/JavaAPI/src/java/util/regex/MatchResult.java rename to CodenameOne/src/com/codename1/util/regex/MatchResult.java index eb94ceec620..b2d71de20db 100644 --- a/vm/JavaAPI/src/java/util/regex/MatchResult.java +++ b/CodenameOne/src/com/codename1/util/regex/MatchResult.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, Codename One and/or its affiliates. All rights reserved. + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as @@ -20,14 +20,22 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package java.util.regex; +package com.codename1.util.regex; +/// What one match found: the same shape as `java.util.regex.MatchResult`. public interface MatchResult { - public int end(); - public int end(int group); - public String group(); - public String group(int group); - public int groupCount(); - public int start(); - public int start(int group); + + int start(); + + int start(int group); + + int end(); + + int end(int group); + + String group(); + + String group(int group); + + int groupCount(); } diff --git a/CodenameOne/src/com/codename1/util/regex/Matcher.java b/CodenameOne/src/com/codename1/util/regex/Matcher.java new file mode 100644 index 00000000000..aea7c9ff9bd --- /dev/null +++ b/CodenameOne/src/com/codename1/util/regex/Matcher.java @@ -0,0 +1,622 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.util.regex; + +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.function.Function; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/// The same shape as `java.util.regex.Matcher`, over this package's [Pattern]. +/// +/// Everything a match is read through is here -- `matches`, `lookingAt`, +/// `find`, groups by number and by name, regions, and the replacement +/// methods with the JDK's `$1` and `${name}` syntax. `hitEnd`, +/// `requireEnd` and the two bounds switches are not provided. +public final class Matcher implements MatchResult { + + private Pattern pattern; + CharSequence text; + int from; + int to; + + /// The state the nodes of a pattern read and write while matching. + boolean requireEnd; + int matchEnd; + int endAt = -1; + int subEnd; + int oldLast = -1; + int[] starts; + int[] ends; + int[] opened; + int[] loopCount; + int[] loopStart; + + private int first = -1; + private int last; + private int appendPosition; + + Matcher(Pattern pattern, CharSequence text) { + if (text == null) { + throw new NullPointerException(); + } + this.text = text; + use(pattern); + reset(); + } + + private void use(Pattern p) { + pattern = p; + starts = new int[p.groupCount + 1]; + ends = new int[p.groupCount + 1]; + opened = new int[p.groupCount + 1]; + loopCount = new int[p.loopCount]; + loopStart = new int[p.loopCount]; + } + + int[] saveGroups() { + int n = starts.length; + if (n == 1) { + return null; + } + int[] saved = new int[n * 2]; + System.arraycopy(starts, 0, saved, 0, n); + System.arraycopy(ends, 0, saved, n, n); + return saved; + } + + void restoreGroups(int[] saved) { + if (saved != null) { + int n = starts.length; + System.arraycopy(saved, 0, starts, 0, n); + System.arraycopy(saved, n, ends, 0, n); + } + } + + private void clearGroups() { + for (int g = 0; g < starts.length; g++) { + starts[g] = -1; + ends[g] = -1; + opened[g] = -1; + } + } + + public Pattern pattern() { + return pattern; + } + + public Matcher usePattern(Pattern newPattern) { + if (newPattern == null) { + throw new IllegalArgumentException("Pattern cannot be null"); + } + use(newPattern); + clearGroups(); + first = -1; + return this; + } + + public Matcher reset() { + first = -1; + last = 0; + oldLast = -1; + appendPosition = 0; + from = 0; + to = text.length(); + clearGroups(); + return this; + } + + public Matcher reset(CharSequence input) { + if (input == null) { + throw new NullPointerException(); + } + text = input; + return reset(); + } + + public Matcher region(int start, int end) { + if (start < 0 || start > text.length() || end < 0 || end > text.length() || start > end) { + throw new IndexOutOfBoundsException("start " + start + ", end " + end); + } + reset(); + from = start; + to = end; + return this; + } + + public int regionStart() { + return from; + } + + public int regionEnd() { + return to; + } + + private boolean attempt(int start, boolean whole) { + clearGroups(); + requireEnd = whole; + endAt = -1; + if (oldLast < 0) { + oldLast = start; + } + boolean matched = pattern.root.match(this, start); + if (matched) { + first = start; + last = matchEnd; + starts[0] = start; + ends[0] = matchEnd; + } else { + first = -1; + clearGroups(); + } + oldLast = last; + return matched; + } + + public boolean matches() { + return attempt(from, true); + } + + public boolean lookingAt() { + return attempt(from, false); + } + + private boolean search(int start) { + if (oldLast < 0) { + oldLast = start; + } + requireEnd = false; + endAt = -1; + for (int i = start; i <= to; i++) { + clearGroups(); + if (pattern.root.match(this, i)) { + first = i; + last = matchEnd; + starts[0] = i; + ends[0] = matchEnd; + oldLast = last; + return true; + } + } + first = -1; + clearGroups(); + oldLast = last; + return false; + } + + public boolean find() { + int next = last; + if (next == first) { + next++; + } + if (next < from) { + next = from; + } + if (next > to) { + first = -1; + clearGroups(); + return false; + } + return search(next); + } + + public boolean find(int start) { + if (start < 0 || start > text.length()) { + throw new IndexOutOfBoundsException("Illegal start index"); + } + reset(); + return search(start); + } + + /// Whether the last match operation found something. + public boolean hasMatch() { + return first >= 0; + } + + private void requireMatch() { + if (first < 0) { + throw new IllegalStateException("No match found"); + } + } + + private void requireGroup(int group) { + requireMatch(); + if (group < 0 || group > pattern.groupCount) { + throw new IndexOutOfBoundsException("No group " + group); + } + } + + private int named(String name) { + if (name == null) { + throw new NullPointerException("Null group name"); + } + Integer group = pattern.names.get(name); + if (group == null) { + throw new IllegalArgumentException("No group with name <" + name + ">"); + } + return group.intValue(); + } + + @Override + public int start() { + requireMatch(); + return first; + } + + @Override + public int start(int group) { + requireGroup(group); + return starts[group]; + } + + public int start(String name) { + requireMatch(); + return starts[named(name)]; + } + + @Override + public int end() { + requireMatch(); + return last; + } + + @Override + public int end(int group) { + requireGroup(group); + return ends[group]; + } + + public int end(String name) { + requireMatch(); + return ends[named(name)]; + } + + @Override + public String group() { + return group(0); + } + + @Override + public String group(int group) { + requireGroup(group); + if (starts[group] < 0 || ends[group] < 0) { + return null; + } + return text.subSequence(starts[group], ends[group]).toString(); + } + + public String group(String name) { + requireMatch(); + int group = named(name); + if (starts[group] < 0 || ends[group] < 0) { + return null; + } + return text.subSequence(starts[group], ends[group]).toString(); + } + + @Override + public int groupCount() { + return pattern.groupCount; + } + + /// The match as it is now, unaffected by what this matcher does next. + public MatchResult toMatchResult() { + return new Snapshot(this); + } + + private static final class Snapshot implements MatchResult { + private final int[] starts; + private final int[] ends; + private final String text; + private final boolean matched; + + Snapshot(Matcher m) { + matched = m.first >= 0; + starts = new int[m.starts.length]; + ends = new int[m.ends.length]; + System.arraycopy(m.starts, 0, starts, 0, starts.length); + System.arraycopy(m.ends, 0, ends, 0, ends.length); + text = m.text.toString(); + } + + private void check(int group) { + if (!matched) { + throw new IllegalStateException("No match found"); + } + if (group < 0 || group >= starts.length) { + throw new IndexOutOfBoundsException("No group " + group); + } + } + + @Override + public int start() { + return start(0); + } + + @Override + public int start(int group) { + check(group); + return starts[group]; + } + + @Override + public int end() { + return end(0); + } + + @Override + public int end(int group) { + check(group); + return ends[group]; + } + + @Override + public String group() { + return group(0); + } + + @Override + public String group(int group) { + check(group); + return starts[group] < 0 || ends[group] < 0 ? null : text.substring(starts[group], ends[group]); + } + + @Override + public int groupCount() { + return starts.length - 1; + } + } + + /// Each match from here on, as [#toMatchResult()] would answer it: from + /// after the last match found, or from the start of the region when + /// there is none. The matcher is not reset, so a match already consumed + /// is not answered again and a region stays in force. + /// + /// Lazy, as the JDK's is: each match is found only when the stream asks + /// for it, so `results().limit(1)` scans no further than the first. + public Stream results() { + return StreamSupport.stream(Spliterators.spliteratorUnknownSize(new Results(this), + Spliterator.ORDERED | Spliterator.NONNULL), false); + } + + /// The matches of one [#results()]. + private static final class Results implements Iterator { + private final Matcher matcher; + private MatchResult next; + private boolean done; + + Results(Matcher matcher) { + this.matcher = matcher; + } + + @Override + public boolean hasNext() { + if (next == null && !done) { + if (matcher.find()) { + next = matcher.toMatchResult(); + } else { + done = true; + } + } + return next != null; + } + + @Override + public MatchResult next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + MatchResult out = next; + next = null; + return out; + } + } + + /// A replacement in which `$` and backslash stand for themselves. + public static String quoteReplacement(String s) { + if (s.indexOf('\\') < 0 && s.indexOf('$') < 0) { + return s; + } + StringBuilder sb = new StringBuilder(s.length() + 8); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '\\' || c == '$') { + sb.append('\\'); + } + sb.append(c); + } + return sb.toString(); + } + + private void expand(String replacement, StringBuilder out) { + int cursor = 0; + int length = replacement.length(); + while (cursor < length) { + char c = replacement.charAt(cursor); + if (c == '\\') { + cursor++; + if (cursor == length) { + throw new IllegalArgumentException("character to be escaped is missing"); + } + out.append(replacement.charAt(cursor)); + cursor++; + } else if (c == '$') { + cursor++; + if (cursor == length) { + throw new IllegalArgumentException("Illegal group reference: group index is missing"); + } + char next = replacement.charAt(cursor); + int group; + if (next == '{') { + cursor++; + int start = cursor; + while (cursor < length) { + char n = replacement.charAt(cursor); + if (Pattern.asciiLetter(n) || n >= '0' && n <= '9') { + cursor++; + } else { + break; + } + } + if (cursor == start) { + throw new IllegalArgumentException("named capturing group has 0 length name"); + } + if (cursor == length || replacement.charAt(cursor) != '}') { + throw new IllegalArgumentException("named capturing group is missing trailing '}'"); + } + String name = replacement.substring(start, cursor); + char lead = name.charAt(0); + if (lead >= '0' && lead <= '9') { + throw new IllegalArgumentException("capturing group name {" + name + + "} starts with digit character"); + } + Integer found = pattern.names.get(name); + if (found == null) { + throw new IllegalArgumentException("No group with name {" + name + "}"); + } + group = found.intValue(); + cursor++; + } else { + group = next - '0'; + if (group < 0 || group > 9) { + throw new IllegalArgumentException("Illegal group reference"); + } + cursor++; + if (group > pattern.groupCount) { + throw new IndexOutOfBoundsException("No group " + group); + } + while (cursor < length) { + int digit = replacement.charAt(cursor) - '0'; + if (digit < 0 || digit > 9) { + break; + } + int wider = group * 10 + digit; + if (wider > pattern.groupCount) { + break; + } + group = wider; + cursor++; + } + } + if (starts[group] >= 0 && ends[group] >= 0) { + out.append(text.subSequence(starts[group], ends[group]).toString()); + } + } else { + out.append(c); + cursor++; + } + } + } + + public Matcher appendReplacement(StringBuilder sb, String replacement) { + requireMatch(); + StringBuilder expanded = new StringBuilder(); + expand(replacement, expanded); + sb.append(text.subSequence(appendPosition, first).toString()); + sb.append(expanded.toString()); + appendPosition = last; + return this; + } + + public Matcher appendReplacement(StringBuffer sb, String replacement) { + requireMatch(); + StringBuilder expanded = new StringBuilder(); + expand(replacement, expanded); + sb.append(text.subSequence(appendPosition, first).toString()); + sb.append(expanded.toString()); + appendPosition = last; + return this; + } + + public StringBuilder appendTail(StringBuilder sb) { + sb.append(text.subSequence(appendPosition, text.length()).toString()); + return sb; + } + + public StringBuffer appendTail(StringBuffer sb) { + sb.append(text.subSequence(appendPosition, text.length()).toString()); + return sb; + } + + public String replaceAll(String replacement) { + reset(); + if (!find()) { + return text.toString(); + } + StringBuilder sb = new StringBuilder(); + do { + appendReplacement(sb, replacement); + } while (find()); + return appendTail(sb).toString(); + } + + public String replaceAll(Function replacer) { + if (replacer == null) { + throw new NullPointerException(); + } + reset(); + if (!find()) { + return text.toString(); + } + StringBuilder sb = new StringBuilder(); + do { + appendReplacement(sb, replacer.apply(this)); + } while (find()); + return appendTail(sb).toString(); + } + + public String replaceFirst(String replacement) { + if (replacement == null) { + throw new NullPointerException("replacement"); + } + reset(); + if (!find()) { + return text.toString(); + } + StringBuilder sb = new StringBuilder(); + appendReplacement(sb, replacement); + return appendTail(sb).toString(); + } + + public String replaceFirst(Function replacer) { + if (replacer == null) { + throw new NullPointerException(); + } + reset(); + if (!find()) { + return text.toString(); + } + StringBuilder sb = new StringBuilder(); + appendReplacement(sb, replacer.apply(this)); + return appendTail(sb).toString(); + } + + @Override + public String toString() { + return "java.util.regex.Matcher[pattern=" + pattern.pattern() + " region=" + from + "," + to + + " lastmatch=" + (first >= 0 ? group() : "") + "]"; + } +} diff --git a/CodenameOne/src/com/codename1/util/regex/Pattern.java b/CodenameOne/src/com/codename1/util/regex/Pattern.java new file mode 100644 index 00000000000..c6278cb896c --- /dev/null +++ b/CodenameOne/src/com/codename1/util/regex/Pattern.java @@ -0,0 +1,1973 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.util.regex; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.function.Predicate; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/// `java.util.regex.Pattern` for the Codename One runtime: a backtracking +/// matcher written for this module, with the JDK's syntax and the JDK's +/// answers for the constructs it has. +/// +/// #### Supported +/// +/// - Literals, `.`, and every escape for a character: `\t \n \r \f \a \e`, +/// `\0nn`, `\xhh`, `\x{h...}`, backslash-u with four hex digits, `\cX`, +/// and `\Q...\E`. +/// - Character classes with ranges, negation, nested unions and `&&` +/// intersections; `\d \D \w \W \s \S \h \H \v \V`; `\R`. +/// - The POSIX classes (`\p{Alpha}`, `\p{Punct}`, ...), the `java...` +/// classes, and the Unicode properties a device can answer: `L`, `Lu`, +/// `Ll`, `N`/`Nd`, `IsAlphabetic`, `IsLetter`, `IsDigit`, `IsUppercase`, +/// `IsLowercase`, `IsWhite_Space`. +/// - Greedy, reluctant and possessive quantifiers. +/// - Capturing, non-capturing and named groups, back references by number +/// and by name, alternation, atomic groups. +/// - Lookahead and bounded lookbehind, positive and negative. +/// - `^ $ \A \z \Z \G \b \B`. +/// - `CASE_INSENSITIVE`, `MULTILINE`, `DOTALL`, `COMMENTS`, `LITERAL`, +/// `UNIX_LINES` and `UNICODE_CASE`, as arguments and inline (`(?i)`, +/// `(?i:...)`, `(?-i)`). +/// +/// #### Refused +/// +/// These throw [PatternSyntaxException] (the flags an +/// `UnsupportedOperationException`) naming the construct, when the pattern +/// is compiled. Nothing is accepted and then matched differently. +/// +/// - Unicode scripts, blocks and general categories other than the ones +/// above (`\p{IsGreek}`, `\p{InArrows}`, `\p{Sc}`, ...). +/// - `\X`, `\N{name}`, `\b{g}`. +/// - `CANON_EQ` and `UNICODE_CHARACTER_CLASS` (`(?U)`). +/// - A code point above U+FFFF inside a character class. +/// +/// #### Differences +/// +/// Text is matched one UTF-16 `char` at a time: `.` and a character class +/// take half of a surrogate pair. Which characters are letters is what +/// `isLetter` in this class answers, the scripts a device font is likely +/// to have rather than all of Unicode. A group repeated many thousand times +/// over a long text recurses once for each repetition, as the JDK's does, +/// on a stack that is smaller on a device. +public final class Pattern { + + public static final int UNIX_LINES = 0x01; + public static final int CASE_INSENSITIVE = 0x02; + public static final int COMMENTS = 0x04; + public static final int MULTILINE = 0x08; + public static final int LITERAL = 0x10; + public static final int DOTALL = 0x20; + public static final int UNICODE_CASE = 0x40; + public static final int CANON_EQ = 0x80; + public static final int UNICODE_CHARACTER_CLASS = 0x100; + + private static final int ALL_FLAGS = UNIX_LINES | CASE_INSENSITIVE | COMMENTS | MULTILINE | LITERAL + | DOTALL | UNICODE_CASE | CANON_EQ | UNICODE_CHARACTER_CLASS; + + private final String regex; + private final int flags; + final Node root; + final int groupCount; + final int loopCount; + final Map names; + + private Pattern(String regex, int flags) { + if (regex == null) { + throw new NullPointerException(); + } + // As in the JDK: a bit no flag constant names is a caller's mistake, + // not something to ignore. + if ((flags & ~ALL_FLAGS) != 0) { + throw new IllegalArgumentException("Unknown flag 0x" + Integer.toHexString(flags & ~ALL_FLAGS)); + } + if ((flags & CANON_EQ) != 0) { + throw new UnsupportedOperationException("Pattern.CANON_EQ is not supported by the Codename One runtime"); + } + if ((flags & UNICODE_CHARACTER_CLASS) != 0) { + throw new UnsupportedOperationException( + "Pattern.UNICODE_CHARACTER_CLASS is not supported by the Codename One runtime"); + } + this.regex = regex; + Parser parser = new Parser(regex, flags); + this.root = parser.compile(); + // As in the JDK, flags() reports the top-level state the parse ended + // in: (?i) outside every group counts, (?i) inside one does not. + this.flags = parser.topLevelFlags(); + this.groupCount = parser.groups; + this.loopCount = parser.loops; + this.names = parser.names; + } + + public static Pattern compile(String regex) { + return new Pattern(regex, 0); + } + + public static Pattern compile(String regex, int flags) { + return new Pattern(regex, flags); + } + + public String pattern() { + return regex; + } + + @Override + public String toString() { + return regex; + } + + public int flags() { + return flags; + } + + public Matcher matcher(CharSequence input) { + return new Matcher(this, input); + } + + public static boolean matches(String regex, CharSequence input) { + return compile(regex).matcher(input).matches(); + } + + public String[] split(CharSequence input) { + return split(input, 0); + } + + /// Splits `input` around the matches of this pattern, by the JDK's + /// rules for `limit`. + public String[] split(CharSequence input, int limit) { + int index = 0; + boolean limited = limit > 0; + List list = new ArrayList(); + Matcher m = matcher(input); + while (m.find()) { + if (!limited || list.size() < limit - 1) { + if (index == 0 && m.start() == 0 && m.end() == 0) { + // No empty leading piece for a zero-width match at the start. + continue; + } + list.add(input.subSequence(index, m.start()).toString()); + index = m.end(); + } else if (list.size() == limit - 1) { + list.add(input.subSequence(index, input.length()).toString()); + index = m.end(); + } + } + if (index == 0) { + return new String[] {input.toString()}; + } + if (!limited || list.size() < limit) { + list.add(input.subSequence(index, input.length()).toString()); + } + int size = list.size(); + if (limit == 0) { + while (size > 0 && list.get(size - 1).length() == 0) { + size--; + } + } + String[] out = new String[size]; + for (int i = 0; i < size; i++) { + out[i] = list.get(i); + } + return out; + } + + /// The pieces [#split(CharSequence)] would answer, found one at a time + /// as the stream asks for them, as the JDK's are: the search starts on + /// the first request, so a `limit` stops it and a change to a mutable + /// input before then is seen. + public Stream splitAsStream(final CharSequence input) { + return StreamSupport.stream(Spliterators.spliteratorUnknownSize(new Pieces(this, input), + Spliterator.ORDERED | Spliterator.NONNULL), false); + } + + /// The pieces of one [#splitAsStream(CharSequence)]. Empty pieces are + /// held back until a non-empty one follows them, because trailing ones + /// are dropped, as `split` drops them. + private static final class Pieces implements Iterator { + private final Pattern pattern; + private final CharSequence input; + private Matcher matcher; + private int current; + private String pending; + private int empties; + private String next; + + Pieces(Pattern pattern, CharSequence input) { + this.pattern = pattern; + this.input = input; + } + + @Override + public boolean hasNext() { + if (next == null) { + next = advance(); + } + return next != null; + } + + @Override + public String next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + String out = next; + next = null; + return out; + } + + private String advance() { + if (matcher == null) { + matcher = pattern.matcher(input); + // An empty input is one empty piece, as split answers. + if (input.length() == 0) { + empties = 1; + } + } + while (true) { + if (empties > 0) { + empties--; + return ""; + } + if (pending != null) { + String out = pending; + pending = null; + return out; + } + if (current == input.length()) { + return null; + } + while (pending == null && matcher.find()) { + String piece = input.subSequence(current, matcher.start()).toString(); + current = matcher.end(); + if (piece.length() > 0) { + pending = piece; + } else if (current > 0) { + // No empty leading piece for a zero-width match at the start. + empties++; + } + } + if (pending == null) { + String last = input.subSequence(current, input.length()).toString(); + current = input.length(); + if (last.length() == 0) { + // Only trailing empty pieces were owed: dropped. + empties = 0; + return null; + } + pending = last; + } + } + } + } + + /// A pattern that matches `s` and nothing else. + public static String quote(String s) { + int slashE = s.indexOf("\\E"); + if (slashE < 0) { + return "\\Q" + s + "\\E"; + } + StringBuilder sb = new StringBuilder(s.length() * 2); + sb.append("\\Q"); + int current = 0; + while (slashE >= 0) { + sb.append(s.substring(current, slashE)); + current = slashE + 2; + sb.append("\\E\\\\E\\Q"); + slashE = s.indexOf("\\E", current); + } + sb.append(s.substring(current)); + sb.append("\\E"); + return sb.toString(); + } + + public Predicate asPredicate() { + return new Predicate() { + @Override + public boolean test(String s) { + return matcher(s).find(); + } + }; + } + + public Predicate asMatchPredicate() { + return new Predicate() { + @Override + public boolean test(String s) { + return matcher(s).matches(); + } + }; + } + + // ------------------------------------------------------------------ + // Characters + // ------------------------------------------------------------------ + + static boolean lineTerminator(char c, boolean unix) { + if (c == '\n') { + return true; + } + return !unix && (c == '\r' || c == 0x85 || c == 0x2028 || c == 0x2029); + } + + static boolean asciiLetter(char c) { + return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z'; + } + + /// A word character for `\b` and `\B`: the ASCII `[a-zA-Z0-9_]` that + /// `\w` matches. The JDK has defined the boundary this way since 19 + /// (JDK-8264160); before, it took any Unicode letter or digit, so `\b` + /// fell inside a word `\w+` could not match. The Unicode reading comes + /// only with `UNICODE_CHARACTER_CLASS`, which this runtime refuses. + static boolean wordChar(char c) { + return c == '_' || asciiLetter(c) || c >= '0' && c <= '9'; + } + + static boolean isLetterOrDigit(char c) { + return isLetter(c) || Character.isDigit(c); + } + + /// Whether `c` is a letter: the scripts a device font is likely to + /// have rather than all of Unicode. Written out here because + /// `Character.isLetter` is not available on every device runtime. + static boolean isLetter(char c) { + if (c < 0x80) { + return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z'; + } + if (c < 0x100) { + return c == 0xaa || c == 0xb5 || c == 0xba || c >= 0xc0 && c != 0xd7 && c != 0xf7; + } + return c <= 0x24f // Latin Extended + || c >= 0x370 && c <= 0x3ff && c != 0x37e && c != 0x387 // Greek + || c >= 0x400 && c <= 0x481 || c >= 0x48a && c <= 0x52f // Cyrillic + || c >= 0x531 && c <= 0x556 || c >= 0x561 && c <= 0x587 // Armenian + || c >= 0x5d0 && c <= 0x5ea // Hebrew + || c >= 0x620 && c <= 0x64a || c >= 0x671 && c <= 0x6d3 // Arabic + || c >= 0x904 && c <= 0x939 // Devanagari + || c >= 0xe01 && c <= 0xe30 // Thai + || c >= 0x10a0 && c <= 0x10ff && c != 0x10fb // Georgian + || c >= 0x1e00 && c <= 0x1fff // Latin and Greek Extended Additional + || c >= 0x3041 && c <= 0x3096 || c >= 0x30a1 && c <= 0x30fa // Kana + || c >= 0x3400 && c <= 0x4dbf || c >= 0x4e00 && c <= 0x9fff // CJK + || c >= 0xac00 && c <= 0xd7a3; // Hangul + } + + /// Whether `a` and `b` are the same character when case is ignored: + /// for ASCII letters only, or by the device's case tables. + static boolean sameIgnoringCase(char a, char b, boolean unicode) { + if (a == b) { + return true; + } + if (a < 0x80 && b < 0x80) { + return asciiLetter(a) && (a ^ 0x20) == b; + } + if (!unicode) { + return false; + } + char ua = Character.toUpperCase(a); + char ub = Character.toUpperCase(b); + return ua == ub || Character.toLowerCase(ua) == Character.toLowerCase(ub); + } + + // ------------------------------------------------------------------ + // The compiled form: a chain of nodes, each matching at a position and + // then asking the next one to match the rest. + // ------------------------------------------------------------------ + + abstract static class Node { + Node next; + + abstract boolean match(Matcher m, int i); + } + + /// The end of the whole pattern. + static final class Accept extends Node { + @Override + boolean match(Matcher m, int i) { + if (m.requireEnd && i != m.to) { + return false; + } + m.matchEnd = i; + return true; + } + } + + /// The end of a pattern inside another: an atomic group or a lookaround. + static final class SubEnd extends Node { + @Override + boolean match(Matcher m, int i) { + if (m.endAt >= 0 && i != m.endAt) { + return false; + } + m.subEnd = i; + return true; + } + } + + static final class Pass extends Node { + @Override + boolean match(Matcher m, int i) { + return next.match(m, i); + } + } + + /// A node that takes exactly one character. + abstract static class Single extends Node { + abstract boolean is(char c); + + @Override + boolean match(Matcher m, int i) { + return i < m.to && is(m.text.charAt(i)) && next.match(m, i + 1); + } + } + + static final class Char extends Single { + final char c; + + Char(char c) { + this.c = c; + } + + @Override + boolean is(char ch) { + return ch == c; + } + } + + static final class CharIgnoringCase extends Single { + final char c; + final boolean unicode; + + CharIgnoringCase(char c, boolean unicode) { + this.c = c; + this.unicode = unicode; + } + + @Override + boolean is(char ch) { + return sameIgnoringCase(ch, c, unicode); + } + } + + static final class Any extends Single { + final boolean all; + final boolean unix; + + Any(boolean all, boolean unix) { + this.all = all; + this.unix = unix; + } + + @Override + boolean is(char c) { + return all || !lineTerminator(c, unix); + } + } + + static final class InClass extends Single { + final CharSet set; + + InClass(CharSet set) { + this.set = set; + } + + @Override + boolean is(char c) { + return set.has(c); + } + } + + /// A set of characters: ranges, the predicates a device answers, other + /// sets united with it, and one it is intersected with. + static final class CharSet { + static final int LETTER = 1; + static final int UPPER = 2; + static final int LOWER = 4; + static final int DIGIT = 8; + static final int WHITESPACE = 16; + static final int LETTER_OR_DIGIT = 32; + static final int TITLE = 64; + + private char[] ranges = new char[8]; + private int count; + private int kinds; + private final List united = new ArrayList(); + CharSet and; + boolean negate; + /// 0 for exact, 1 to ignore the case of ASCII letters, 2 of any. + final int ignoreCase; + + CharSet(int ignoreCase) { + this.ignoreCase = ignoreCase; + } + + CharSet add(int lo, int hi) { + if (count + 2 > ranges.length) { + char[] grown = new char[ranges.length * 2]; + System.arraycopy(ranges, 0, grown, 0, count); + ranges = grown; + } + ranges[count++] = (char) lo; + ranges[count++] = (char) hi; + return this; + } + + CharSet add(int c) { + return add(c, c); + } + + CharSet kind(int kind) { + kinds |= kind; + return this; + } + + CharSet unite(CharSet other) { + united.add(other); + return this; + } + + CharSet not() { + CharSet out = new CharSet(0); + out.united.add(this); + out.negate = true; + return out; + } + + boolean isEmpty() { + return count == 0 && kinds == 0 && united.isEmpty(); + } + + private boolean inRanges(char c) { + for (int k = 0; k < count; k += 2) { + if (c >= ranges[k] && c <= ranges[k + 1]) { + return true; + } + } + return false; + } + + private boolean inKinds(char c) { + if (kinds == 0) { + return false; + } + return (kinds & LETTER) != 0 && isLetter(c) + || (kinds & UPPER) != 0 && Character.isUpperCase(c) + || (kinds & LOWER) != 0 && Character.isLowerCase(c) + || (kinds & DIGIT) != 0 && Character.isDigit(c) + || (kinds & WHITESPACE) != 0 && Character.isWhitespace(c) + || (kinds & LETTER_OR_DIGIT) != 0 && isLetterOrDigit(c) + || (kinds & TITLE) != 0 && Character.isTitleCase(c); + } + + boolean has(char c) { + boolean in = inRanges(c) || inKinds(c); + if (!in && ignoreCase != 0 && count > 0) { + if (c < 0x80) { + in = asciiLetter(c) && inRanges((char) (c ^ 0x20)); + } else if (ignoreCase == 2) { + in = inRanges(Character.toUpperCase(c)) || inRanges(Character.toLowerCase(c)); + } + } + for (int k = 0; !in && k < united.size(); k++) { + in = united.get(k).has(c); + } + if (in && and != null) { + in = and.has(c); + } + return in != negate; + } + } + + /// One character, repeated. Counted in a loop rather than by recursion, + /// so `.*` over a long text costs no stack. + static final class SingleLoop extends Node { + final Single atom; + final int min; + final int max; + final int mode; + + SingleLoop(Single atom, int min, int max, int mode) { + this.atom = atom; + this.min = min; + this.max = max < 0 ? Integer.MAX_VALUE : max; + this.mode = mode; + } + + @Override + boolean match(Matcher m, int i) { + CharSequence text = m.text; + int to = m.to; + int n = 0; + int j = i; + if (mode == Parser.RELUCTANT) { + while (n < min) { + if (j >= to || !atom.is(text.charAt(j))) { + return false; + } + j++; + n++; + } + while (true) { + if (next.match(m, j)) { + return true; + } + if (n >= max || j >= to || !atom.is(text.charAt(j))) { + return false; + } + j++; + n++; + } + } + while (n < max && j < to && atom.is(text.charAt(j))) { + j++; + n++; + } + if (n < min) { + return false; + } + if (mode == Parser.POSSESSIVE) { + return next.match(m, j); + } + while (true) { + if (next.match(m, j)) { + return true; + } + if (n == min) { + return false; + } + n--; + j--; + } + } + } + + /// Anything else, repeated. The body ends in a [LoopTail] that comes + /// back here to decide between another round and the rest. + static final class Loop extends Node { + Node body; + final int id; + final int min; + final int max; + final boolean reluctant; + + Loop(int id, int min, int max, boolean reluctant) { + this.id = id; + this.min = min; + this.max = max < 0 ? Integer.MAX_VALUE : max; + this.reluctant = reluctant; + } + + @Override + boolean match(Matcher m, int i) { + int count = m.loopCount[id]; + int position = m.loopStart[id]; + m.loopCount[id] = 0; + m.loopStart[id] = -1; + boolean matched = step(m, i); + m.loopCount[id] = count; + m.loopStart[id] = position; + return matched; + } + + boolean step(Matcher m, int i) { + int count = m.loopCount[id]; + if (count < min) { + return round(m, i, count); + } + if (reluctant) { + return next.match(m, i) || count < max && round(m, i, count); + } + return count < max && round(m, i, count) || next.match(m, i); + } + + private boolean round(Matcher m, int i, int count) { + int position = m.loopStart[id]; + if (count >= min && i == position) { + // The round before this one took nothing; another would + // take nothing for ever. + return false; + } + m.loopCount[id] = count + 1; + m.loopStart[id] = i; + boolean matched = body.match(m, i); + m.loopCount[id] = count; + m.loopStart[id] = position; + return matched; + } + } + + static final class LoopTail extends Node { + final Loop loop; + + LoopTail(Loop loop) { + this.loop = loop; + } + + @Override + boolean match(Matcher m, int i) { + return loop.step(m, i); + } + } + + static final class Branch extends Node { + final Node[] alternatives; + + Branch(Node[] alternatives) { + this.alternatives = alternatives; + } + + @Override + boolean match(Matcher m, int i) { + for (Node alternative : alternatives) { + if (alternative.match(m, i)) { + return true; + } + } + return false; + } + } + + static final class GroupOpen extends Node { + final int group; + + GroupOpen(int group) { + this.group = group; + } + + @Override + boolean match(Matcher m, int i) { + int saved = m.opened[group]; + m.opened[group] = i; + if (next.match(m, i)) { + return true; + } + m.opened[group] = saved; + return false; + } + } + + static final class GroupClose extends Node { + final int group; + + GroupClose(int group) { + this.group = group; + } + + @Override + boolean match(Matcher m, int i) { + int start = m.starts[group]; + int end = m.ends[group]; + m.starts[group] = m.opened[group]; + m.ends[group] = i; + if (next.match(m, i)) { + return true; + } + m.starts[group] = start; + m.ends[group] = end; + return false; + } + } + + static final class BackReference extends Node { + final int group; + final boolean ignoreCase; + final boolean unicode; + + BackReference(int group, boolean ignoreCase, boolean unicode) { + this.group = group; + this.ignoreCase = ignoreCase; + this.unicode = unicode; + } + + @Override + boolean match(Matcher m, int i) { + if (group >= m.starts.length) { + // A reference to a group the pattern never defines. + return false; + } + int start = m.starts[group]; + int end = m.ends[group]; + if (start < 0) { + return false; + } + int length = end - start; + if (i + length > m.to) { + return false; + } + for (int k = 0; k < length; k++) { + char a = m.text.charAt(start + k); + char b = m.text.charAt(i + k); + if (a != b && !(ignoreCase && sameIgnoringCase(a, b, unicode))) { + return false; + } + } + return next.match(m, i + length); + } + } + + static final class Anchor extends Node { + static final int BEGIN = 0; + static final int LINE_START = 1; + static final int LINE_END = 2; + static final int END_OR_FINAL_TERMINATOR = 3; + static final int END = 4; + static final int LAST_MATCH = 5; + static final int WORD = 6; + static final int NOT_WORD = 7; + + final int kind; + final boolean unix; + + Anchor(int kind, boolean unix) { + this.kind = kind; + this.unix = unix; + } + + private boolean lineEnd(Matcher m, int i, boolean multiline) { + CharSequence text = m.text; + int end = m.to; + if (unix) { + if (i < end) { + if (text.charAt(i) != '\n') { + return false; + } + return multiline || i == end - 1; + } + return true; + } + if (!multiline) { + if (i < end - 2) { + return false; + } + if (i == end - 2 && (text.charAt(i) != '\r' || text.charAt(i + 1) != '\n')) { + return false; + } + } + if (i < end) { + char c = text.charAt(i); + if (c == '\n') { + // Never between the two characters of one line end. + return !(i > 0 && text.charAt(i - 1) == '\r'); + } + return c == '\r' || c == 0x85 || c == 0x2028 || c == 0x2029; + } + return true; + } + + private boolean lineStart(Matcher m, int i) { + if (i == m.to) { + // Not at the end of the input, even after a line end. + return false; + } + if (i > m.from) { + char c = m.text.charAt(i - 1); + if (!lineTerminator(c, unix)) { + return false; + } + if (c == '\r' && m.text.charAt(i) == '\n') { + return false; + } + } + return true; + } + + @Override + boolean match(Matcher m, int i) { + boolean here; + switch (kind) { + case BEGIN: + here = i == m.from; + break; + case LINE_START: + here = lineStart(m, i); + break; + case LINE_END: + here = lineEnd(m, i, true); + break; + case END_OR_FINAL_TERMINATOR: + here = lineEnd(m, i, false); + break; + case END: + here = i == m.to; + break; + case LAST_MATCH: + here = i == m.oldLast; + break; + default: + boolean left = i > m.from && wordChar(m.text.charAt(i - 1)); + boolean right = i < m.to && wordChar(m.text.charAt(i)); + here = (left != right) == (kind == WORD); + break; + } + return here && next.match(m, i); + } + } + + /// `(?>X)`: what `X` took first is kept, whatever follows. + static final class Atomic extends Node { + final Node body; + + Atomic(Node body) { + this.body = body; + } + + @Override + boolean match(Matcher m, int i) { + int[] groups = m.saveGroups(); + int endAt = m.endAt; + m.endAt = -1; + boolean matched = body.match(m, i); + m.endAt = endAt; + if (!matched) { + return false; + } + if (next.match(m, m.subEnd)) { + return true; + } + m.restoreGroups(groups); + return false; + } + } + + static final class Look extends Node { + final Node body; + final boolean behind; + final boolean negative; + final int min; + final int max; + + Look(Node body, boolean behind, boolean negative, int min, int max) { + this.body = body; + this.behind = behind; + this.negative = negative; + this.min = min; + this.max = max; + } + + @Override + boolean match(Matcher m, int i) { + int[] groups = m.saveGroups(); + int endAt = m.endAt; + boolean found = false; + if (behind) { + int lowest = Math.max(m.from, i - max); + for (int j = i - min; !found && j >= lowest; j--) { + m.endAt = i; + found = body.match(m, j); + } + } else { + m.endAt = -1; + found = body.match(m, i); + } + m.endAt = endAt; + if (found == negative) { + if (found) { + m.restoreGroups(groups); + } + return false; + } + if (negative) { + m.restoreGroups(groups); + } + if (next.match(m, i)) { + return true; + } + m.restoreGroups(groups); + return false; + } + } + + // ------------------------------------------------------------------ + // The parser + // ------------------------------------------------------------------ + + /// A piece of the chain being built: where it starts, the node whose + /// `next` is still open, and how many characters it can take. + private static final class Piece { + Node first; + Node last; + int min; + /// -1 for no upper bound. + int max; + /// Whether there is one way only for this to match at a position. + boolean deterministic = true; + + Piece(Node first, Node last, int min, int max) { + this.first = first; + this.last = last; + this.min = min; + this.max = max; + } + + static Piece of(Node node, int length) { + return new Piece(node, node, length, length); + } + + static Piece empty() { + return of(new Pass(), 0); + } + + Piece unbounded() { + max = -1; + return this; + } + + void then(Piece other) { + last.next = other.first; + last = other.last; + deterministic &= other.deterministic; + min = sum(min, other.min); + max = max < 0 || other.max < 0 ? -1 : sum(max, other.max); + } + + private static int sum(int a, int b) { + long s = (long) a + b; + return s > 1000000000L ? 1000000000 : (int) s; + } + } + + /// Pieces put one after another. + private static final class Seq { + private Piece all; + + void then(Piece next) { + if (all == null) { + all = next; + } else { + all.then(next); + } + } + + Piece piece() { + return all == null ? Piece.empty() : all; + } + } + + private static final class Parser { + static final int GREEDY = 0; + static final int RELUCTANT = 1; + static final int POSSESSIVE = 2; + + private final String p; + private int at; + private int flags; + int groups; + int loops; + final Map names = new HashMap(); + + Parser(String pattern, int flags) { + this.p = pattern; + this.flags = flags; + } + + private PatternSyntaxException error(String description) { + return new PatternSyntaxException(description, p, Math.min(at, p.length())); + } + + private PatternSyntaxException unsupported(String construct) { + return error(construct + " is not supported by the Codename One runtime"); + } + + private boolean has(int flag) { + return (flags & flag) != 0; + } + + /// The flags in force after the whole pattern, which a group's own + /// inline flags never reach because the group restores them. + int topLevelFlags() { + return flags; + } + + Node compile() { + Piece whole; + if (has(LITERAL)) { + Seq all = new Seq(); + for (at = 0; at < p.length(); at++) { + all.then(literal(p.charAt(at))); + } + whole = all.piece(); + } else { + whole = alternatives(); + if (at < p.length()) { + throw error("Unmatched closing ')'"); + } + } + whole.last.next = new Accept(); + return whole.first; + } + + private Piece literal(int c) { + if (c > 0xffff) { + // Two chars in the text, so two nodes here. + int v = c - 0x10000; + Piece pair = Piece.of(new Char((char) (0xd800 + (v >> 10))), 1); + pair.then(Piece.of(new Char((char) (0xdc00 + (v & 0x3ff))), 1)); + return pair; + } + char ch = (char) c; + if (has(CASE_INSENSITIVE)) { + boolean unicode = has(UNICODE_CASE); + boolean cased = ch < 0x80 ? asciiLetter(ch) + : unicode && (Character.toUpperCase(ch) != ch || Character.toLowerCase(ch) != ch); + if (cased) { + return Piece.of(new CharIgnoringCase(ch, unicode), 1); + } + } + return Piece.of(new Char(ch), 1); + } + + private CharSet newSet() { + return new CharSet(has(CASE_INSENSITIVE) ? has(UNICODE_CASE) ? 2 : 1 : 0); + } + + private void skipComments() { + if (!has(COMMENTS)) { + return; + } + while (at < p.length()) { + char c = p.charAt(at); + if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == 0x0b) { + at++; + } else if (c == '#') { + while (at < p.length() && !lineTerminator(p.charAt(at), false)) { + at++; + } + } else { + return; + } + } + } + + /// `X|Y|Z`, up to a closing parenthesis or the end. + private Piece alternatives() { + List all = new ArrayList(); + all.add(sequence()); + while (at < p.length() && p.charAt(at) == '|') { + at++; + all.add(sequence()); + } + if (all.size() == 1) { + return all.get(0); + } + // Alternatives of one character each are one character class, + // which repeats without recursion. + CharSet merged = new CharSet(0); + boolean mergeable = true; + for (Piece piece : all) { + Node n = piece.first; + if (piece.first != piece.last) { //NOPMD CompareObjectsWithEquals - one node, by identity + mergeable = false; + } else if (n instanceof Char) { + merged.add(((Char) n).c); + } else if (n instanceof InClass) { + merged.unite(((InClass) n).set); + } else { + mergeable = false; + } + } + if (mergeable) { + return Piece.of(new InClass(merged), 1); + } + Node[] heads = new Node[all.size()]; + Pass join = new Pass(); + int min = Integer.MAX_VALUE; + int max = 0; + for (int k = 0; k < heads.length; k++) { + Piece piece = all.get(k); + heads[k] = piece.first; + piece.last.next = join; + min = Math.min(min, piece.min); + max = max < 0 || piece.max < 0 ? -1 : Math.max(max, piece.max); + } + Piece branch = new Piece(new Branch(heads), join, min, max); + branch.deterministic = false; + return branch; + } + + private Piece sequence() { + Seq out = new Seq(); + while (true) { + skipComments(); + if (at >= p.length()) { + return out.piece(); + } + char c = p.charAt(at); + if (c == '|' || c == ')') { + return out.piece(); + } + if (c == '\\' && at + 1 < p.length() && p.charAt(at + 1) == 'Q') { + at += 2; + int end = p.indexOf("\\E", at); + if (end < 0) { + end = p.length(); + } + // A quantifier after the quotation applies to its last + // character, so that one goes through the usual path. + for (; at < end - 1; at++) { + out.then(literal(p.charAt(at))); + } + if (at < end) { + Piece last = literal(p.charAt(at)); + at = Math.min(end + 2, p.length()); + out.then(quantified(last)); + } else { + at = Math.min(end + 2, p.length()); + } + continue; + } + if (c == '\\' && at + 1 < p.length() && p.charAt(at + 1) == 'E') { + at += 2; + continue; + } + out.then(quantified(atom())); + } + } + + private Piece atom() { + char c = p.charAt(at); + switch (c) { + case '(': + return group(); + case '[': + at++; + return Piece.of(new InClass(characterClass()), 1); + case '.': + at++; + return Piece.of(new Any(has(DOTALL), has(UNIX_LINES)), 1); + case '^': + at++; + return Piece.of(new Anchor(has(MULTILINE) ? Anchor.LINE_START : Anchor.BEGIN, has(UNIX_LINES)), 0); + case '$': + at++; + return Piece.of(new Anchor(has(MULTILINE) ? Anchor.LINE_END : Anchor.END_OR_FINAL_TERMINATOR, + has(UNIX_LINES)), 0); + case '\\': + at++; + return escape(); + case '*': + case '+': + case '?': + throw error("Dangling meta character '" + c + "'"); + case '{': + throw error("Illegal repetition"); + default: + at++; + if (c >= 0xd800 && c <= 0xdbff && at < p.length() && p.charAt(at) >= 0xdc00 + && p.charAt(at) <= 0xdfff) { + // A code point written out is one atom, so a + // quantifier after it repeats both halves. + Piece pair = Piece.of(new Char(c), 1); + pair.then(Piece.of(new Char(p.charAt(at++)), 1)); + return pair; + } + return literal(c); + } + } + + private Piece quantified(Piece piece) { + skipComments(); + if (at >= p.length()) { + return piece; + } + int min; + int max; + char c = p.charAt(at); + if (c == '*') { + min = 0; + max = -1; + at++; + } else if (c == '+') { + min = 1; + max = -1; + at++; + } else if (c == '?') { + min = 0; + max = 1; + at++; + } else if (c == '{') { + at++; + int start = at; + min = number(); + if (at == start) { + throw error("Illegal repetition"); + } + max = min; + if (at < p.length() && p.charAt(at) == ',') { + at++; + if (at < p.length() && p.charAt(at) == '}') { + max = -1; + } else { + int from = at; + max = number(); + if (at == from) { + throw error("Illegal repetition"); + } + if (max < min) { + throw error("Illegal repetition range"); + } + } + } + if (at >= p.length() || p.charAt(at) != '}') { + throw error("Unclosed counted closure"); + } + at++; + } else { + return piece; + } + int mode = GREEDY; + if (at < p.length() && p.charAt(at) == '?') { + mode = RELUCTANT; + at++; + } else if (at < p.length() && p.charAt(at) == '+') { + mode = POSSESSIVE; + at++; + } + int lowest = (int) Math.min(1000000000L, (long) piece.min * min); + int highest = max < 0 || piece.max < 0 ? -1 : (int) Math.min(1000000000L, (long) piece.max * max); + if (max == 0) { + highest = 0; + } + Piece out; + if (min == 0 && piece.max == 0 && piece.deterministic && piece.first instanceof GroupOpen) { + // A group that can only be empty, repeated from zero times: + // the JDK takes no round of it, so the group stays unset. + out = Piece.empty(); + } else if (piece.first == piece.last && piece.first instanceof Single) { //NOPMD CompareObjectsWithEquals + out = new Piece(null, null, lowest, highest); + out.first = new SingleLoop((Single) piece.first, min, max, mode); + out.last = out.first; + } else { + Loop loop = new Loop(loops++, min, max, mode == RELUCTANT); + loop.body = piece.first; + piece.last.next = new LoopTail(loop); + if (mode == POSSESSIVE) { + loop.next = new SubEnd(); + Node atomic = new Atomic(loop); + out = new Piece(atomic, atomic, lowest, highest); + } else { + out = new Piece(loop, loop, lowest, highest); + } + } + out.deterministic = piece.deterministic && min == max; + skipComments(); + if (at < p.length()) { + char after = p.charAt(at); + if (after == '*' || after == '+' || after == '?') { + throw error("Dangling meta character '" + after + "'"); + } + if (after == '{') { + return quantified(out); + } + } + return out; + } + + private int number() { + long n = 0; + while (at < p.length() && p.charAt(at) >= '0' && p.charAt(at) <= '9') { + n = n * 10 + (p.charAt(at) - '0'); + if (n > Integer.MAX_VALUE) { + throw error("Illegal repetition range"); + } + at++; + } + return (int) n; + } + + private Piece group() { + at++; + int saved = flags; + Piece out; + if (at < p.length() && p.charAt(at) == '?') { + at++; + if (at >= p.length()) { + throw error("Unknown inline modifier"); + } + char c = p.charAt(at); + if (c == ':') { + at++; + out = body(); + } else if (c == '=' || c == '!') { + at++; + out = look(false, c == '!'); + } else if (c == '>') { + at++; + Piece inner = body(); + inner.last.next = new SubEnd(); + Node atomic = new Atomic(inner.first); + out = new Piece(atomic, atomic, inner.min, inner.max); + } else if (c == '<') { + at++; + if (at < p.length() && (p.charAt(at) == '=' || p.charAt(at) == '!')) { + boolean negative = p.charAt(at) == '!'; + at++; + out = look(true, negative); + } else { + String name = groupName('>'); + if (names.containsKey(name)) { + throw error("Named capturing group <" + name + "> is already defined"); + } + int group = ++groups; + names.put(name, Integer.valueOf(group)); + out = capture(group); + } + } else { + // (?flags) for the rest of the enclosing group, or + // (?flags:X) for X alone. + inlineFlags(); + if (at >= p.length()) { + throw error("Unknown inline modifier"); + } + if (p.charAt(at) == ')') { + at++; + return Piece.empty(); + } + if (p.charAt(at) != ':') { + throw error("Unknown inline modifier"); + } + at++; + out = body(); + } + } else { + out = capture(++groups); + } + flags = saved; + return out; + } + + private Piece body() { + Piece inner = alternatives(); + if (at >= p.length() || p.charAt(at) != ')') { + throw error("Unclosed group"); + } + at++; + return inner; + } + + private Piece capture(int group) { + Piece out = Piece.of(new GroupOpen(group), 0); + out.then(body()); + out.then(Piece.of(new GroupClose(group), 0)); + return out; + } + + private Piece look(boolean behind, boolean negative) { + Piece inner = body(); + if (behind && inner.max < 0) { + throw error("Look-behind group does not have an obvious maximum length"); + } + inner.last.next = new SubEnd(); + return Piece.of(new Look(inner.first, behind, negative, inner.min, inner.max), 0); + } + + private String groupName(char close) { + int start = at; + while (at < p.length() && p.charAt(at) != close) { + char c = p.charAt(at); + boolean letter = asciiLetter(c); + if (!(letter || at > start && c >= '0' && c <= '9')) { + throw error(at == start ? "capturing group name does not start with a Latin letter" + : "named capturing group is missing trailing '" + close + "'"); + } + at++; + } + if (at >= p.length()) { + throw error("named capturing group is missing trailing '" + close + "'"); + } + if (at == start) { + throw error("named capturing group has 0 length name"); + } + String name = p.substring(start, at); + at++; + return name; + } + + private void inlineFlags() { + boolean on = true; + while (at < p.length()) { + char c = p.charAt(at); + int flag; + switch (c) { + case 'i': + flag = CASE_INSENSITIVE; + break; + case 'm': + flag = MULTILINE; + break; + case 's': + flag = DOTALL; + break; + case 'd': + flag = UNIX_LINES; + break; + case 'u': + flag = UNICODE_CASE; + break; + case 'x': + flag = COMMENTS; + break; + case 'c': + throw unsupported("The inline flag (?c), canonical equivalence,"); + case 'U': + throw unsupported("The inline flag (?U), Unicode character classes,"); + case '-': + if (!on) { + return; + } + on = false; + at++; + continue; + default: + return; + } + flags = on ? flags | flag : flags & ~flag; + at++; + } + } + + private int hex(int digits) { + if (at + digits > p.length()) { + throw error("Illegal hexadecimal escape sequence"); + } + int v = 0; + for (int k = 0; k < digits; k++) { + int d = Character.digit(p.charAt(at++), 16); + if (d < 0) { + throw error("Illegal hexadecimal escape sequence"); + } + v = v * 16 + d; + } + return v; + } + + /// The character an escape names, with `at` after the letter that + /// follows the backslash; -1 when the letter is not one of those. + private int escapedChar(char e) { + switch (e) { + case 't': + return '\t'; + case 'n': + return '\n'; + case 'r': + return '\r'; + case 'f': + return '\f'; + case 'a': + return 7; + case 'e': + return 0x1b; + case '0': { + int v = 0; + int digits = 0; + while (at < p.length() && digits < 3 && p.charAt(at) >= '0' && p.charAt(at) <= '7') { + int d = p.charAt(at) - '0'; + if (digits == 2 && v > 0x1f) { + break; + } + v = v * 8 + d; + digits++; + at++; + } + if (digits == 0) { + throw error("Illegal octal escape sequence"); + } + return v; + } + case 'x': + if (at < p.length() && p.charAt(at) == '{') { + at++; + int v = 0; + int start = at; + while (at < p.length() && p.charAt(at) != '}') { + int d = Character.digit(p.charAt(at), 16); + if (d < 0 || v > 0x10ffff) { + throw error("Illegal hexadecimal escape sequence"); + } + v = v * 16 + d; + at++; + } + if (at >= p.length() || at == start || v > 0x10ffff) { + throw error("Illegal hexadecimal escape sequence"); + } + at++; + return v; + } + return hex(2); + case 'u': + if (at + 4 > p.length()) { + throw error("Illegal Unicode escape sequence"); + } + for (int k = 0; k < 4; k++) { + if (Character.digit(p.charAt(at + k), 16) < 0) { + throw error("Illegal Unicode escape sequence"); + } + } + return hex(4); + case 'c': + if (at >= p.length()) { + throw error("Illegal control escape sequence"); + } + return p.charAt(at++) ^ 64; + default: + return -1; + } + } + + /// The class an escape names, or null. + private CharSet escapedClass(char e) { + switch (e) { + case 'd': + return new CharSet(0).add('0', '9'); + case 'D': + return new CharSet(0).add('0', '9').not(); + case 'w': + return word(); + case 'W': + return word().not(); + case 's': + return new CharSet(0).add(' ').add('\t', '\r'); + case 'S': + return new CharSet(0).add(' ').add('\t', '\r').not(); + case 'h': + return horizontal(); + case 'H': + return horizontal().not(); + case 'v': + return vertical(); + case 'V': + return vertical().not(); + case 'p': + case 'P': { + CharSet set = property(); + return e == 'P' ? set.not() : set; + } + default: + return null; + } + } + + private static CharSet word() { + return new CharSet(0).add('a', 'z').add('A', 'Z').add('0', '9').add('_'); + } + + private static CharSet horizontal() { + return new CharSet(0).add(' ').add('\t').add(0xa0).add(0x1680).add(0x180e).add(0x2000, 0x200a) + .add(0x202f).add(0x205f).add(0x3000); + } + + private static CharSet vertical() { + return new CharSet(0).add('\n', '\r').add(0x85).add(0x2028, 0x2029); + } + + private CharSet property() { + String name; + if (at < p.length() && p.charAt(at) == '{') { + int close = p.indexOf('}', at); + if (close < 0) { + throw error("Unclosed character family"); + } + name = p.substring(at + 1, close); + at = close + 1; + } else if (at < p.length()) { + name = String.valueOf(p.charAt(at++)); + } else { + throw error("Illegal character family"); + } + CharSet s = new CharSet(0); + // Under CASE_INSENSITIVE the case classes match either case, as + // they have on the JDK since 16 (JDK-8214245): the ASCII ones become + // the ASCII letters, the Unicode ones any cased letter. + boolean anyCase = has(CASE_INSENSITIVE); + if ("Lower".equals(name)) { + return anyCase ? s.add('a', 'z').add('A', 'Z') : s.add('a', 'z'); + } + if ("Upper".equals(name)) { + return anyCase ? s.add('a', 'z').add('A', 'Z') : s.add('A', 'Z'); + } + if ("ASCII".equals(name)) { + return s.add(0, 0x7f); + } + if ("Alpha".equals(name)) { + return s.add('a', 'z').add('A', 'Z'); + } + if ("Digit".equals(name)) { + return s.add('0', '9'); + } + if ("Alnum".equals(name)) { + return s.add('a', 'z').add('A', 'Z').add('0', '9'); + } + if ("Punct".equals(name)) { + return s.add('!', '/').add(':', '@').add('[', '`').add('{', '~'); + } + if ("Graph".equals(name)) { + return s.add('!', '~'); + } + if ("Print".equals(name)) { + return s.add(' ', '~'); + } + if ("Blank".equals(name)) { + return s.add(' ').add('\t'); + } + if ("Cntrl".equals(name)) { + return s.add(0, 0x1f).add(0x7f); + } + if ("XDigit".equals(name)) { + return s.add('0', '9').add('a', 'f').add('A', 'F'); + } + if ("Space".equals(name)) { + return s.add(' ').add('\t', '\r'); + } + if ("javaLowerCase".equals(name) || "Ll".equals(name) || "IsLl".equals(name) + || "IsLowercase".equals(name)) { + return anyCase ? s.kind(CharSet.LOWER | CharSet.UPPER | CharSet.TITLE) : s.kind(CharSet.LOWER); + } + if ("javaUpperCase".equals(name) || "Lu".equals(name) || "IsLu".equals(name) + || "IsUppercase".equals(name)) { + return anyCase ? s.kind(CharSet.LOWER | CharSet.UPPER | CharSet.TITLE) : s.kind(CharSet.UPPER); + } + if ("javaWhitespace".equals(name) || "IsWhite_Space".equals(name) || "IsWhiteSpace".equals(name)) { + return s.kind(CharSet.WHITESPACE); + } + if ("javaLetter".equals(name) || "L".equals(name) || "IsL".equals(name) || "IsLetter".equals(name) + || "IsAlphabetic".equals(name) || "javaAlphabetic".equals(name)) { + return s.kind(CharSet.LETTER); + } + if ("javaLetterOrDigit".equals(name)) { + return s.kind(CharSet.LETTER_OR_DIGIT); + } + if ("javaDigit".equals(name) || "N".equals(name) || "Nd".equals(name) || "IsN".equals(name) + || "IsNd".equals(name) || "IsDigit".equals(name)) { + return s.kind(CharSet.DIGIT); + } + throw unsupported("The character property {" + name + "}"); + } + + private Piece escape() { + if (at >= p.length()) { + throw error("Unexpected internal error"); + } + char e = p.charAt(at++); + boolean unix = has(UNIX_LINES); + switch (e) { + case 'A': + return Piece.of(new Anchor(Anchor.BEGIN, unix), 0); + case 'z': + return Piece.of(new Anchor(Anchor.END, unix), 0); + case 'Z': + return Piece.of(new Anchor(Anchor.END_OR_FINAL_TERMINATOR, unix), 0); + case 'G': + return Piece.of(new Anchor(Anchor.LAST_MATCH, unix), 0); + case 'b': + if (at < p.length() && p.charAt(at) == '{') { + throw unsupported("The boundary \\b{...}"); + } + return Piece.of(new Anchor(Anchor.WORD, unix), 0); + case 'B': + return Piece.of(new Anchor(Anchor.NOT_WORD, unix), 0); + case 'X': + throw unsupported("The grapheme cluster \\X"); + case 'N': + throw unsupported("The named character \\N{...}"); + case 'R': { + // \r\n as one, or any single line end; atomic, so the + // pair is never split to let what follows take the \n. + Piece crlf = Piece.of(new Char('\r'), 1); + crlf.then(Piece.of(new Char('\n'), 1)); + Piece one = Piece.of(new InClass(vertical()), 1); + Pass join = new Pass(); + crlf.last.next = join; + one.last.next = join; + join.next = new SubEnd(); + Node atomic = new Atomic(new Branch(new Node[] {crlf.first, one.first})); + return new Piece(atomic, atomic, 1, 2); + } + case 'k': { + if (at >= p.length() || p.charAt(at) != '<') { + throw error("\\k is not followed by '<' for named capturing group"); + } + at++; + String name = groupName('>'); + Integer group = names.get(name); + if (group == null) { + throw error("named capturing group <" + name + "> does not exist"); + } + return Piece.of(new BackReference(group.intValue(), has(CASE_INSENSITIVE), has(UNICODE_CASE)), 0) + .unbounded(); + } + default: + break; + } + if (e >= '1' && e <= '9') { + int group = e - '0'; + while (at < p.length() && p.charAt(at) >= '0' && p.charAt(at) <= '9') { + int more = group * 10 + (p.charAt(at) - '0'); + if (more > groups) { + break; + } + group = more; + at++; + } + return Piece.of(new BackReference(group, has(CASE_INSENSITIVE), has(UNICODE_CASE)), 0).unbounded(); + } + CharSet set = escapedClass(e); + if (set != null) { + return Piece.of(new InClass(set), 1); + } + int c = escapedChar(e); + if (c >= 0) { + return literal(c); + } + if (asciiLetter(e)) { + at--; + throw error("Illegal/unsupported escape sequence"); + } + return literal(e); + } + + /// A class, with `at` after its opening bracket; consumes the + /// closing one. + private CharSet characterClass() { + boolean negate = false; + if (at < p.length() && p.charAt(at) == '^') { + negate = true; + at++; + } + CharSet set = classBody(); + set.negate = negate; + return set; + } + + private CharSet classBody() { + CharSet set = newSet(); + boolean first = true; + while (true) { + if (has(COMMENTS)) { + skipComments(); + } + if (at >= p.length()) { + throw error("Unclosed character class"); + } + char c = p.charAt(at); + if (c == ']' && !first) { + at++; + return set; + } + first = false; + if (c == '[') { + at++; + set.unite(characterClass()); + continue; + } + if (c == '&' && at + 1 < p.length() && p.charAt(at + 1) == '&') { + at += 2; + if (at < p.length() && p.charAt(at) == ']') { + // Nothing to intersect with. + continue; + } + // Everything after && is the other operand, up to the + // bracket that closes this class. + CharSet right = classBody(); + if (set.isEmpty()) { + return right; + } + set.and = right; + return set; + } + int lo; + if (c == '\\') { + at++; + if (at >= p.length()) { + throw error("Unclosed character class"); + } + char e = p.charAt(at++); + if (e == 'Q') { + int end = p.indexOf("\\E", at); + if (end < 0) { + throw error("Unclosed character class"); + } + for (; at < end; at++) { + set.add(p.charAt(at)); + } + at = end + 2; + continue; + } + if (e == 'E') { + continue; + } + CharSet named = escapedClass(e); + if (named != null) { + set.unite(named); + continue; + } + lo = escapedChar(e); + if (lo < 0) { + if (asciiLetter(e) || e >= '0' && e <= '9') { + at--; + throw error("Illegal/unsupported escape sequence"); + } + lo = e; + } + } else { + at++; + lo = c; + } + int hi = lo; + if (at + 1 < p.length() && p.charAt(at) == '-' && p.charAt(at + 1) != ']' + && p.charAt(at + 1) != '[') { + at++; + char h = p.charAt(at++); + if (h == '\\') { + if (at >= p.length()) { + throw error("Unclosed character class"); + } + char e = p.charAt(at++); + hi = escapedChar(e); + if (hi < 0) { + if (asciiLetter(e)) { + at--; + throw error("Illegal character range"); + } + hi = e; + } + } else { + hi = h; + } + if (hi < lo) { + throw error("Illegal character range"); + } + } + if (hi > 0xffff) { + throw unsupported("A character above U+FFFF in a character class"); + } + set.add(lo, hi); + } + } + } +} diff --git a/CodenameOne/src/com/codename1/util/regex/PatternSyntaxException.java b/CodenameOne/src/com/codename1/util/regex/PatternSyntaxException.java new file mode 100644 index 00000000000..6319c57348d --- /dev/null +++ b/CodenameOne/src/com/codename1/util/regex/PatternSyntaxException.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.util.regex; + +/// The same shape as `java.util.regex.PatternSyntaxException`: a pattern that is not one, or +/// that uses a construct this runtime does not have -- the description says +/// which. +public class PatternSyntaxException extends IllegalArgumentException { + + private final String description; + private final String pattern; + private final int index; + + public PatternSyntaxException(String description, String regex, int index) { + this.description = description; + this.pattern = regex; + this.index = index; + } + + public int getIndex() { + return index; + } + + public String getDescription() { + return description; + } + + public String getPattern() { + return pattern; + } + + @Override + public String getMessage() { + StringBuilder sb = new StringBuilder(); + sb.append(description); + if (index >= 0) { + sb.append(" near index ").append(index); + } + sb.append('\n').append(pattern); + if (index >= 0 && pattern != null && index < pattern.length()) { + sb.append('\n'); + for (int i = 0; i < index; i++) { + sb.append(' '); + } + sb.append('^'); + } + return sb.toString(); + } +} diff --git a/CodenameOne/src/com/codename1/util/regex/StringRegex.java b/CodenameOne/src/com/codename1/util/regex/StringRegex.java new file mode 100644 index 00000000000..784a2302ad4 --- /dev/null +++ b/CodenameOne/src/com/codename1/util/regex/StringRegex.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.util.regex; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +/// The methods of `java.lang.String` that take a regular expression, over +/// this package's [Pattern]: `matches`, `split`, `replaceAll` and +/// `replaceFirst`, with the JDK's syntax. Each takes the string as its +/// first argument, so `s.split(";")` becomes `StringRegex.split(s, ";")`. +public final class StringRegex { + + /// The pattern compiled last. Code that splits every line of a file by + /// the same expression compiles it once. + private static final AtomicReference LAST = new AtomicReference(); + + private StringRegex() { + } + + private static Pattern pattern(String regex) { + Pattern last = LAST.get(); + if (last != null && last.pattern().equals(regex)) { + return last; + } + Pattern compiled = Pattern.compile(regex); + LAST.set(compiled); + return compiled; + } + + /// The character a separator is, when it is one plain character or one + /// escaped: -1 when it has to be compiled. + private static int plain(String regex) { + if (regex.length() == 1) { + char c = regex.charAt(0); + return ".$|()[{^?*+\\".indexOf(c) < 0 ? c : -1; + } + if (regex.length() == 2 && regex.charAt(0) == '\\') { + char c = regex.charAt(1); + boolean alnum = c >= '0' && c <= '9' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z'; + return alnum || c >= 0xd800 && c <= 0xdfff ? -1 : c; + } + return -1; + } + + public static boolean matches(String s, String regex) { + return pattern(regex).matcher(s).matches(); + } + + public static String[] split(String s, String regex) { + return split(s, regex, 0); + } + + public static String[] split(String s, String regex, int limit) { + int c = plain(regex); + if (c < 0) { + return pattern(regex).split(s, limit); + } + List list = new ArrayList(); + int off = 0; + boolean limited = limit > 0; + int next = s.indexOf(c, off); + while (next >= 0 && (!limited || list.size() < limit - 1)) { + list.add(s.substring(off, next)); + off = next + 1; + next = s.indexOf(c, off); + } + if (off == 0) { + return new String[] {s}; + } + list.add(s.substring(off)); + int size = list.size(); + if (limit == 0) { + while (size > 0 && list.get(size - 1).length() == 0) { + size--; + } + } + String[] out = new String[size]; + for (int i = 0; i < size; i++) { + out[i] = list.get(i); + } + return out; + } + + public static String replaceAll(String s, String regex, String replacement) { + return pattern(regex).matcher(s).replaceAll(replacement); + } + + public static String replaceFirst(String s, String regex, String replacement) { + return pattern(regex).matcher(s).replaceFirst(replacement); + } +} diff --git a/CodenameOne/src/com/codename1/util/regex/package-info.java b/CodenameOne/src/com/codename1/util/regex/package-info.java index af29c46f25a..2eb4f175b50 100644 --- a/CodenameOne/src/com/codename1/util/regex/package-info.java +++ b/CodenameOne/src/com/codename1/util/regex/package-info.java @@ -1,8 +1,30 @@ -/* - Document : package - Created on : Oct 11, 2007, 10:38:26 AM - Author : Shai Almog -*/ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ -/// A simple regular expression API that is portable across all platforms +/// Regular expressions that are portable across all platforms. +/// +/// [Pattern] and [Matcher] have the shape and the syntax of +/// `java.util.regex`, and [StringRegex] holds the regular expression methods +/// of `java.lang.String` over them. [RE] is the older, smaller evaluator with +/// its own dialect. package com.codename1.util.regex; diff --git a/NOTICE b/NOTICE index dc50ab27df2..28a66ac3365 100644 --- a/NOTICE +++ b/NOTICE @@ -129,3 +129,37 @@ ASM is distributed under the BSD 3-Clause license: CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +================================================================================ +GroupLayout +================================================================================ + +The class com.codename1.desktopcompat.javax.swing.GroupLayout in +maven/swing-compat is adapted from Codename One's own port of the group layout, +CodenameOne/src/com/codename1/ui/layouts/GroupLayout.java, which is derived from +the GroupLayout Oracle published for the Java platform. The adaptation gives the +class the javax.swing API -- its method names, its Alignment enumeration and the +LayoutStyle.ComponentPlacement gap types -- reads minimum and maximum sizes from +the components, and retargets the layout at the Swing compatibility layer's own +java.awt component classes. The original copyright and license notice is +retained in the source file. + +The file is distributed under the GNU General Public License version 2 with the +"Classpath" exception, the license of Codename One itself (see LICENSE): + + Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved. + +================================================================================ +FloatingDecimal digit generation +================================================================================ + +The class java.text.LegacyDoubleDigits, in vm/JavaAPI/src/java/text and +Ports/CLDC11/src/java/text, is a rewrite of the digit generation in OpenJDK's +sun.misc.FloatingDecimal (BinaryToASCIIBuffer.dtoa) and the FDBigInteger +arithmetic it uses, reduced to what java.text.DecimalFormat needs so that the +class formats a double with the digits the JDK's DecimalFormat produces. The +file is distributed under the GNU General Public License version 2 with the +"Classpath" exception, the license of Codename One itself (see LICENSE), and +keeps OpenJDK's notice: + + Copyright (c) 1996, 2013, Oracle and/or its affiliates. All rights reserved. diff --git a/Ports/Android/spotbugs-exclude.xml b/Ports/Android/spotbugs-exclude.xml index 3174b422459..d866bfc9950 100644 --- a/Ports/Android/spotbugs-exclude.xml +++ b/Ports/Android/spotbugs-exclude.xml @@ -567,4 +567,32 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + diff --git a/Ports/Android/src/com/codename1/impl/JdkHost.java b/Ports/Android/src/com/codename1/impl/JdkHost.java new file mode 100644 index 00000000000..ae07bd506c4 --- /dev/null +++ b/Ports/Android/src/com/codename1/impl/JdkHost.java @@ -0,0 +1,480 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.Map; + +/// The services the Java class library asks of the platform it runs on. +/// +/// Internal API. The `java.*` classes of the ParparVM runtime (`vm/JavaAPI`) +/// need a file system, preferences, the network, a log and an event thread, +/// and on a device those belong to Codename One, which the class library +/// cannot name: it is translated alone for the translator itself, the backend +/// and the VM tests. So the class library calls this class, and whoever runs +/// it installs the implementation that fits: +/// +/// - a Codename One application gets `com.codename1.impl.CoreJdkHost`, which +/// delegates to `FileSystemStorage`, `Preferences`, `NetworkManager`, `Log` +/// and `Display`. The ParparVM ports install it from their `init`. +/// - a program translated without Codename One (the self-hosted translator, +/// the backend, a VM test) gets `PosixJdkHost`, which reaches the POSIX file +/// natives through `java.io.File`. +/// - anything else gets the defaults below, which are plain Java and touch no +/// native method, so an application target that never installs a host +/// cannot link the file natives through this class. +/// +/// On Java SE and Android the real JDK runs and nothing calls this class. +/// Copies of it ship in those ports only so that `CoreJdkHost`, which is part +/// of the core sources, always has its superclass. +/// +/// The file is kept byte for byte identical in `vm/JavaAPI`, `Ports/CLDC11`, +/// `Ports/JavaSE` and `Ports/Android`; `CldcJavaApiParityTest` holds them +/// to that. It may use only API all four class libraries have. +/// +/// Paths are in the host's own form: absolute, as [#appHome] answers them. +/// Turning a relative path into one is the caller's job. +public abstract class JdkHost { + /// [#log] level of a debug message. + public static final int LOG_DEBUG = 1; + /// [#log] level of an informational message. + public static final int LOG_INFO = 2; + /// [#log] level of a warning. + public static final int LOG_WARNING = 3; + /// [#log] level of an error. + public static final int LOG_ERROR = 4; + + private static final JdkHost DEFAULTS = new Defaults(); + private static JdkHost current; + private static Registry registry; + private static Map memoryPreferences; + + /// Subclasses are installed with [#install]. + protected JdkHost() { + } + + /// The installed host, or the plain Java defaults when none was installed. + public static JdkHost get() { + JdkHost h = current; + return h == null ? DEFAULTS : h; + } + + /// Makes `host` the one [#get] answers; null restores the defaults. + /// Called once, while the program starts and before it starts a thread + /// of its own. + public static void install(JdkHost host) { + current = host; + } + + /// Makes `r` the registry [#providers] and [#newInstance] read. An + /// application's generated start-up code installs it. + public static void setRegistry(Registry r) { + registry = r; + } + + /// The registry [#setRegistry] installed, or null. + public static Registry getRegistry() { + return registry; + } + + // ------------------------------------------------------------------ + // Files + + /// What is known of `path`, or null when nothing is there. + public FileStat fileStat(String path) { + return null; + } + + /// The names of the entries of the directory `path`, or null when it is + /// not a directory. + public String[] fileList(String path) throws IOException { + return null; + } + + /// Opens the file `path` for reading. + public InputStream fileRead(String path) throws IOException { + throw new IOException("No file system: " + path); + } + + /// Opens the file `path` for writing, at its end when `append` is true + /// and from empty otherwise, creating it when it is not there. + public OutputStream fileWrite(String path, boolean append) throws IOException { + throw new IOException("No file system: " + path); + } + + /// Creates the directory `path`; false when it was not created, also + /// when something was there already. + public boolean fileMkdir(String path) { + return false; + } + + /// Deletes the file or empty directory `path`; false when it is still + /// there or never was. + public boolean fileDelete(String path) { + return false; + } + + /// Moves `from` to `to`, which must not exist. False when it did not + /// happen; a host may refuse a move to another directory, and the + /// caller then copies. + public boolean fileRename(String from, String to) { + return false; + } + + /// The application's home directory, where a relative path resolves. + public String appHome() { + return "/"; + } + + /// A directory for disposable files, or null when the platform has no + /// separate one and [#appHome] serves. + public String cachesDir() { + return null; + } + + /// The roots of the file system. + public String[] fileRoots() { + return new String[] {appHome()}; + } + + /// The size in bytes of the volume holding `path`, or 0 when unknown. + public long fileTotalSpace(String path) { + return 0L; + } + + /// The bytes still free for this program on the volume holding `path`, + /// or 0 when unknown. + public long fileUsableSpace(String path) { + return 0L; + } + + /// The bytes free on the volume holding `path`, counting what only a + /// privileged user may use; [#fileUsableSpace] unless the host knows + /// better. + public long fileFreeSpace(String path) { + return fileUsableSpace(path); + } + + /// [#fileTest]: something is at the path. + public static final int FILE_EXISTS = 1; + /// [#fileTest]: the path is a directory. + public static final int FILE_DIRECTORY = 2; + /// [#fileTest]: the path is a file that is not a directory. + public static final int FILE_REGULAR = 3; + /// [#fileTest] and [#fileSet]: the path may be read. + public static final int FILE_READABLE = 4; + /// [#fileTest] and [#fileSet]: the path may be written. + public static final int FILE_WRITABLE = 5; + /// [#fileTest] and [#fileSet]: the path may be executed, or for a + /// directory, entered. + public static final int FILE_EXECUTABLE = 6; + /// [#fileTest]: the path is hidden. + public static final int FILE_HIDDEN = 7; + + /// Whether `path` has the property `what`, one of the `FILE_` constants. + /// + /// The defaults read [#fileStat]: a file system without permissions lets + /// everything that exists be read and written, a directory be entered and + /// nothing be run, and hides a name that starts with a dot. + public boolean fileTest(String path, int what) { + if (what == FILE_HIDDEN) { + int slash = path.lastIndexOf('/'); + return path.startsWith(".", slash + 1); + } + FileStat st = fileStat(path); + if (st == null) { + return false; + } + if (what == FILE_DIRECTORY || what == FILE_EXECUTABLE) { + return st.isDirectory(); + } + if (what == FILE_REGULAR) { + return !st.isDirectory(); + } + return true; + } + + /// Grants (`on`) or takes away the permission `what` on `path`, one of + /// [#FILE_READABLE], [#FILE_WRITABLE] and [#FILE_EXECUTABLE]; false when + /// the path is not left that way. + /// + /// The defaults change nothing, so they answer whether the path already is + /// as asked, by the rules of [#fileTest]. + public boolean fileSet(String path, int what, boolean on) { + FileStat st = fileStat(path); + if (st == null) { + return false; + } + if (what == FILE_EXECUTABLE) { + return on == st.isDirectory(); + } + return on; + } + + /// The length in bytes of the file `path`; 0 for a directory or when + /// nothing is there. + public long fileLength(String path) { + FileStat st = fileStat(path); + return st == null || st.isDirectory() ? 0L : st.length(); + } + + /// When `path` was last modified, in milliseconds since the epoch; 0 when + /// nothing is there. + public long fileLastModified(String path) { + FileStat st = fileStat(path); + return st == null ? 0L : st.lastModified(); + } + + /// Creates the empty file `path` when nothing is there; false when + /// something was. + /// + /// The default checks and then creates, which is not atomic: two threads + /// racing on one path can both be answered true. + public boolean fileCreate(String path) throws IOException { + if (fileStat(path) != null) { + return false; + } + fileWrite(path, false).close(); + return true; + } + + /// Moves `from` over `to`, replacing the file there. False when it did not + /// happen, which is all the defaults answer; the caller then copies. + public boolean fileReplace(String from, String to) { + return false; + } + + /// The absolute `path` with symbolic links, `.` and `..` resolved, or + /// null when the host cannot resolve links and the caller should resolve + /// the dots by itself. + public String fileCanonicalPath(String path) throws IOException { + return null; + } + + // ------------------------------------------------------------------ + // Preferences + + /// The stored preference `key`, or null. The defaults keep preferences + /// in memory, for this run only. + public String preference(String key) { + return memoryPreference(key, null, false); + } + + /// Stores `value` under `key`. + public void setPreference(String key, String value) { + memoryPreference(key, value, true); + } + + /// Removes the preference `key`. + public void deletePreference(String key) { + memoryPreference(key, null, true); + } + + /// Reads `key` from the in-memory preferences, or, when `write` is true, + /// stores `value` there (null removes it). One lock: the class library + /// runs on several threads where a host leaves this to the defaults. + private static synchronized String memoryPreference(String key, String value, boolean write) { + if (memoryPreferences == null) { + memoryPreferences = new HashMap(); + } + if (!write) { + return memoryPreferences.get(key); + } + if (value == null) { + return memoryPreferences.remove(key); + } + return memoryPreferences.put(key, value); + } + + // ------------------------------------------------------------------ + // Network, log and process + + /// Reads `url` with an HTTP GET. A missing resource (404, 410) throws + /// `java.io.FileNotFoundException`, any other failure an `IOException`. + public InputStream openUrl(String url) throws IOException { + throw new IOException("No network: " + url); + } + + /// Logs `message` and `thrown`, either of which may be null, at `level` + /// ([#LOG_DEBUG] to [#LOG_ERROR]). + public void log(int level, String message, Throwable thrown) { + if (message != null) { + System.err.println(message); + } + if (thrown != null) { + thrown.printStackTrace(); + } + } + + /// The platform's answer for the system property `key`, or null. The + /// class library answers the properties it defines itself. + public String property(String key) { + return null; + } + + /// Asks the platform to close the application, after the shutdown hooks + /// ran; `Runtime.exit` ends the process itself if this returns. The + /// defaults leave it to that. + public void exit(int status) { + } + + /// Called when the program installs a default uncaught exception + /// handler, so that a host whose UI thread catches what its tasks throw + /// can hand that to `Thread.getDefaultUncaughtExceptionHandler()` too. + /// Called on every such install; the defaults do nothing. + public void watchUncaughtExceptions() { + } + + // ------------------------------------------------------------------ + // Threads + + /// Whether the caller is the UI thread, which must not block. + public boolean isUiThread() { + return false; + } + + /// Runs `task`, which blocks, where blocking is allowed: off the UI + /// thread while that thread keeps serving events, or right here. + public void blockUi(Runnable task) { + task.run(); + } + + // ------------------------------------------------------------------ + // Resources, services and locale + + /// Opens the application resource `name` (a path such as + /// `META-INF/services/x`, without a leading slash), or null. + public InputStream resource(String name) { + return null; + } + + /// The class names of the providers of `service` the application + /// declares, from the [Registry]; empty when there is none. + public String[] providers(String service) { + Registry r = registry; + String[] names = r == null ? null : r.providers(service); + return names == null ? new String[0] : names; + } + + /// A new instance of the application class `className`, made by the + /// [Registry] without reflection; null when it cannot make one. + public Object newInstance(String className) { + Registry r = registry; + return r == null ? null : r.newInstance(className); + } + + /// The class of the application class `className`, as the [Registry] + /// knows it, so that its type can be tested without creating one; null + /// when it does not know the class. + public Class type(String className) { + Registry r = registry; + return r == null ? null : r.type(className); + } + + /// The decimal separator, grouping separator and currency symbol of the + /// device's locale, any of them null when unknown; or null when the + /// platform cannot tell. + public String[] localeNumberSymbols() { + return null; + } + + // ------------------------------------------------------------------ + // Security + + /// Fills `out` with cryptographically secure random bytes. + public void randomBytes(byte[] out) { + throw new UnsupportedOperationException("No secure random source"); + } + + /// A new digest for `algorithm` (`MD5`, `SHA-1`, `SHA-256` and the like), + /// or null when the platform has none. + public Digest digest(String algorithm) { + return null; + } + + /// The attributes [#fileStat] reports. + public static final class FileStat { + private final boolean directory; + private final long length; + private final long lastModified; + + /// A file or, when `directory` is true, a directory. + public FileStat(boolean directory, long length, long lastModified) { + this.directory = directory; + this.length = length; + this.lastModified = lastModified; + } + + /// Whether it is a directory. + public boolean isDirectory() { + return directory; + } + + /// The length in bytes of a file; 0 for a directory. + public long length() { + return length; + } + + /// When it was last modified, in milliseconds since the epoch, or 0. + public long lastModified() { + return lastModified; + } + } + + /// A running message digest, as [#digest] makes it. + public interface Digest { + /// Feeds `length` bytes of `data` from `offset`. + void update(byte[] data, int offset, int length); + + /// Finishes the digest and resets it. + byte[] digest(); + + /// Discards what was fed. + void reset(); + + /// The length of the digest in bytes. + int length(); + } + + /// What an application declares for `java.util.ServiceLoader` and + /// `java.util.ResourceBundle`, generated with the application. Once one + /// is installed it is the whole answer: the class library creates no + /// class from its name and reads no services file. + public interface Registry { + /// The provider class names declared for `service`, or null. + String[] providers(String service); + + /// A new instance of `className`, or null. + Object newInstance(String className); + + /// The class `className`, or null when the registry does not know it. + Class type(String className); + } + + /// The host [#get] answers when none was installed. + private static final class Defaults extends JdkHost { + } +} diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java b/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java index adafaae440f..0dd3de1e955 100644 --- a/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java +++ b/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java @@ -1824,6 +1824,9 @@ public void setContext(Context c) { @Override public void init(Object m) { + // cn1res: URLs and flattened resources for applications built from + // desktop or Android sources, which ART cannot resolve on its own. + CompatResourceSupport.install(); // NOTE: Do not explicitly set the PlayServices instance to anything other than // an instance of the base PlayServices class. The Build Server will automatically // swap this for the appropriate subclass depending on the playServicesVersion of diff --git a/Ports/Android/src/com/codename1/impl/android/CompatResourceSupport.java b/Ports/Android/src/com/codename1/impl/android/CompatResourceSupport.java new file mode 100644 index 00000000000..364039cdc00 --- /dev/null +++ b/Ports/Android/src/com/codename1/impl/android/CompatResourceSupport.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.android; + +import com.codename1.impl.compat.Resources; + +import java.io.ByteArrayInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.util.Collections; +import java.util.Enumeration; + +/// What an application built from desktop or Android sources needs of the +/// runtime this port runs on to find the resources its build flattened +/// ([com.codename1.impl.compat.Resources]). +/// +/// - The JDK's `URL` does not know the `cn1res:` scheme, so the URLs +/// `Resources` answers are made here with a handler that reads them back +/// through it. +/// - The JDK's `ResourceBundle` and `ServiceLoader` look a nested name up +/// through a class loader, so the loader installed here asks the +/// application's own loader first and `Resources` after it. +/// +/// The JavaSE port carries the same class for the same reasons. +final class CompatResourceSupport { + private static final String SCHEME = "cn1res"; + + private CompatResourceSupport() { + } + + /// Installs both into `Resources`. Idempotent. + static void install() { + Resources.cn1SetUrlFactory(new Factory()); + if (Resources.cn1ClassLoader() == null) { + ClassLoader parent = Resources.class.getClassLoader(); + Resources.cn1SetClassLoader(new FlatResourceLoader(parent)); + } + } + + private static URL create(String encodedPath) throws MalformedURLException { + return new URL(SCHEME, null, -1, encodedPath, Handler.INSTANCE); + } + + private static final class Factory implements Resources.UrlFactory { + @Override + public URL create(String encodedPath) throws IOException { + return CompatResourceSupport.create(encodedPath); + } + } + + private static final class Handler extends URLStreamHandler { + static final Handler INSTANCE = new Handler(); + + @Override + protected URLConnection openConnection(URL u) { + return new Connection(u); + } + } + + private static final class Connection extends URLConnection { + Connection(URL url) { + super(url); + } + + @Override + public void connect() { + connected = true; + } + + @Override + public InputStream getInputStream() throws IOException { + InputStream in = Resources.openUrlPath(getURL().getPath()); + if (in == null) { + throw new FileNotFoundException(getURL().toExternalForm()); + } + return in; + } + } + + /// The application's loader, then the resources the build flattened. + private static final class FlatResourceLoader extends ClassLoader { + FlatResourceLoader(ClassLoader parent) { + super(parent); + } + + @Override + protected URL findResource(String name) { + if (name == null || name.startsWith("/") || !Resources.exists(name)) { + return null; + } + return Resources.getSystemResource(name); + } + + @Override + protected Enumeration findResources(String name) { + URL url = findResource(name); + if (url == null) { + return Collections.emptyEnumeration(); + } + return Collections.enumeration(Collections.singletonList(url)); + } + + /// A services file is the generated registry's list once one is + /// installed, and nothing else: the files the build flattened would + /// shadow one another, and would name providers that do not ship. + @Override + public Enumeration getResources(String name) throws IOException { + if (name != null && name.startsWith(Resources.SERVICES)) { + String file = Resources.cn1ServicesFile(name.substring(Resources.SERVICES.length())); + if (file != null) { + URL url = new URL(SCHEME, null, -1, "/" + name, new TextHandler(file)); + return Collections.enumeration(Collections.singletonList(url)); + } + } + return super.getResources(name); + } + } + + /// Serves one text, as UTF-8, whatever the URL. + private static final class TextHandler extends URLStreamHandler { + private final String text; + + TextHandler(String text) { + this.text = text; + } + + @Override + protected URLConnection openConnection(URL u) { + return new URLConnection(u) { + @Override + public void connect() { + connected = true; + } + + @Override + public InputStream getInputStream() throws IOException { + return new ByteArrayInputStream(text.getBytes("UTF-8")); + } + }; + } + } +} diff --git a/Ports/CLDC11/src/com/codename1/impl/JdkHost.java b/Ports/CLDC11/src/com/codename1/impl/JdkHost.java new file mode 100644 index 00000000000..ae07bd506c4 --- /dev/null +++ b/Ports/CLDC11/src/com/codename1/impl/JdkHost.java @@ -0,0 +1,480 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.Map; + +/// The services the Java class library asks of the platform it runs on. +/// +/// Internal API. The `java.*` classes of the ParparVM runtime (`vm/JavaAPI`) +/// need a file system, preferences, the network, a log and an event thread, +/// and on a device those belong to Codename One, which the class library +/// cannot name: it is translated alone for the translator itself, the backend +/// and the VM tests. So the class library calls this class, and whoever runs +/// it installs the implementation that fits: +/// +/// - a Codename One application gets `com.codename1.impl.CoreJdkHost`, which +/// delegates to `FileSystemStorage`, `Preferences`, `NetworkManager`, `Log` +/// and `Display`. The ParparVM ports install it from their `init`. +/// - a program translated without Codename One (the self-hosted translator, +/// the backend, a VM test) gets `PosixJdkHost`, which reaches the POSIX file +/// natives through `java.io.File`. +/// - anything else gets the defaults below, which are plain Java and touch no +/// native method, so an application target that never installs a host +/// cannot link the file natives through this class. +/// +/// On Java SE and Android the real JDK runs and nothing calls this class. +/// Copies of it ship in those ports only so that `CoreJdkHost`, which is part +/// of the core sources, always has its superclass. +/// +/// The file is kept byte for byte identical in `vm/JavaAPI`, `Ports/CLDC11`, +/// `Ports/JavaSE` and `Ports/Android`; `CldcJavaApiParityTest` holds them +/// to that. It may use only API all four class libraries have. +/// +/// Paths are in the host's own form: absolute, as [#appHome] answers them. +/// Turning a relative path into one is the caller's job. +public abstract class JdkHost { + /// [#log] level of a debug message. + public static final int LOG_DEBUG = 1; + /// [#log] level of an informational message. + public static final int LOG_INFO = 2; + /// [#log] level of a warning. + public static final int LOG_WARNING = 3; + /// [#log] level of an error. + public static final int LOG_ERROR = 4; + + private static final JdkHost DEFAULTS = new Defaults(); + private static JdkHost current; + private static Registry registry; + private static Map memoryPreferences; + + /// Subclasses are installed with [#install]. + protected JdkHost() { + } + + /// The installed host, or the plain Java defaults when none was installed. + public static JdkHost get() { + JdkHost h = current; + return h == null ? DEFAULTS : h; + } + + /// Makes `host` the one [#get] answers; null restores the defaults. + /// Called once, while the program starts and before it starts a thread + /// of its own. + public static void install(JdkHost host) { + current = host; + } + + /// Makes `r` the registry [#providers] and [#newInstance] read. An + /// application's generated start-up code installs it. + public static void setRegistry(Registry r) { + registry = r; + } + + /// The registry [#setRegistry] installed, or null. + public static Registry getRegistry() { + return registry; + } + + // ------------------------------------------------------------------ + // Files + + /// What is known of `path`, or null when nothing is there. + public FileStat fileStat(String path) { + return null; + } + + /// The names of the entries of the directory `path`, or null when it is + /// not a directory. + public String[] fileList(String path) throws IOException { + return null; + } + + /// Opens the file `path` for reading. + public InputStream fileRead(String path) throws IOException { + throw new IOException("No file system: " + path); + } + + /// Opens the file `path` for writing, at its end when `append` is true + /// and from empty otherwise, creating it when it is not there. + public OutputStream fileWrite(String path, boolean append) throws IOException { + throw new IOException("No file system: " + path); + } + + /// Creates the directory `path`; false when it was not created, also + /// when something was there already. + public boolean fileMkdir(String path) { + return false; + } + + /// Deletes the file or empty directory `path`; false when it is still + /// there or never was. + public boolean fileDelete(String path) { + return false; + } + + /// Moves `from` to `to`, which must not exist. False when it did not + /// happen; a host may refuse a move to another directory, and the + /// caller then copies. + public boolean fileRename(String from, String to) { + return false; + } + + /// The application's home directory, where a relative path resolves. + public String appHome() { + return "/"; + } + + /// A directory for disposable files, or null when the platform has no + /// separate one and [#appHome] serves. + public String cachesDir() { + return null; + } + + /// The roots of the file system. + public String[] fileRoots() { + return new String[] {appHome()}; + } + + /// The size in bytes of the volume holding `path`, or 0 when unknown. + public long fileTotalSpace(String path) { + return 0L; + } + + /// The bytes still free for this program on the volume holding `path`, + /// or 0 when unknown. + public long fileUsableSpace(String path) { + return 0L; + } + + /// The bytes free on the volume holding `path`, counting what only a + /// privileged user may use; [#fileUsableSpace] unless the host knows + /// better. + public long fileFreeSpace(String path) { + return fileUsableSpace(path); + } + + /// [#fileTest]: something is at the path. + public static final int FILE_EXISTS = 1; + /// [#fileTest]: the path is a directory. + public static final int FILE_DIRECTORY = 2; + /// [#fileTest]: the path is a file that is not a directory. + public static final int FILE_REGULAR = 3; + /// [#fileTest] and [#fileSet]: the path may be read. + public static final int FILE_READABLE = 4; + /// [#fileTest] and [#fileSet]: the path may be written. + public static final int FILE_WRITABLE = 5; + /// [#fileTest] and [#fileSet]: the path may be executed, or for a + /// directory, entered. + public static final int FILE_EXECUTABLE = 6; + /// [#fileTest]: the path is hidden. + public static final int FILE_HIDDEN = 7; + + /// Whether `path` has the property `what`, one of the `FILE_` constants. + /// + /// The defaults read [#fileStat]: a file system without permissions lets + /// everything that exists be read and written, a directory be entered and + /// nothing be run, and hides a name that starts with a dot. + public boolean fileTest(String path, int what) { + if (what == FILE_HIDDEN) { + int slash = path.lastIndexOf('/'); + return path.startsWith(".", slash + 1); + } + FileStat st = fileStat(path); + if (st == null) { + return false; + } + if (what == FILE_DIRECTORY || what == FILE_EXECUTABLE) { + return st.isDirectory(); + } + if (what == FILE_REGULAR) { + return !st.isDirectory(); + } + return true; + } + + /// Grants (`on`) or takes away the permission `what` on `path`, one of + /// [#FILE_READABLE], [#FILE_WRITABLE] and [#FILE_EXECUTABLE]; false when + /// the path is not left that way. + /// + /// The defaults change nothing, so they answer whether the path already is + /// as asked, by the rules of [#fileTest]. + public boolean fileSet(String path, int what, boolean on) { + FileStat st = fileStat(path); + if (st == null) { + return false; + } + if (what == FILE_EXECUTABLE) { + return on == st.isDirectory(); + } + return on; + } + + /// The length in bytes of the file `path`; 0 for a directory or when + /// nothing is there. + public long fileLength(String path) { + FileStat st = fileStat(path); + return st == null || st.isDirectory() ? 0L : st.length(); + } + + /// When `path` was last modified, in milliseconds since the epoch; 0 when + /// nothing is there. + public long fileLastModified(String path) { + FileStat st = fileStat(path); + return st == null ? 0L : st.lastModified(); + } + + /// Creates the empty file `path` when nothing is there; false when + /// something was. + /// + /// The default checks and then creates, which is not atomic: two threads + /// racing on one path can both be answered true. + public boolean fileCreate(String path) throws IOException { + if (fileStat(path) != null) { + return false; + } + fileWrite(path, false).close(); + return true; + } + + /// Moves `from` over `to`, replacing the file there. False when it did not + /// happen, which is all the defaults answer; the caller then copies. + public boolean fileReplace(String from, String to) { + return false; + } + + /// The absolute `path` with symbolic links, `.` and `..` resolved, or + /// null when the host cannot resolve links and the caller should resolve + /// the dots by itself. + public String fileCanonicalPath(String path) throws IOException { + return null; + } + + // ------------------------------------------------------------------ + // Preferences + + /// The stored preference `key`, or null. The defaults keep preferences + /// in memory, for this run only. + public String preference(String key) { + return memoryPreference(key, null, false); + } + + /// Stores `value` under `key`. + public void setPreference(String key, String value) { + memoryPreference(key, value, true); + } + + /// Removes the preference `key`. + public void deletePreference(String key) { + memoryPreference(key, null, true); + } + + /// Reads `key` from the in-memory preferences, or, when `write` is true, + /// stores `value` there (null removes it). One lock: the class library + /// runs on several threads where a host leaves this to the defaults. + private static synchronized String memoryPreference(String key, String value, boolean write) { + if (memoryPreferences == null) { + memoryPreferences = new HashMap(); + } + if (!write) { + return memoryPreferences.get(key); + } + if (value == null) { + return memoryPreferences.remove(key); + } + return memoryPreferences.put(key, value); + } + + // ------------------------------------------------------------------ + // Network, log and process + + /// Reads `url` with an HTTP GET. A missing resource (404, 410) throws + /// `java.io.FileNotFoundException`, any other failure an `IOException`. + public InputStream openUrl(String url) throws IOException { + throw new IOException("No network: " + url); + } + + /// Logs `message` and `thrown`, either of which may be null, at `level` + /// ([#LOG_DEBUG] to [#LOG_ERROR]). + public void log(int level, String message, Throwable thrown) { + if (message != null) { + System.err.println(message); + } + if (thrown != null) { + thrown.printStackTrace(); + } + } + + /// The platform's answer for the system property `key`, or null. The + /// class library answers the properties it defines itself. + public String property(String key) { + return null; + } + + /// Asks the platform to close the application, after the shutdown hooks + /// ran; `Runtime.exit` ends the process itself if this returns. The + /// defaults leave it to that. + public void exit(int status) { + } + + /// Called when the program installs a default uncaught exception + /// handler, so that a host whose UI thread catches what its tasks throw + /// can hand that to `Thread.getDefaultUncaughtExceptionHandler()` too. + /// Called on every such install; the defaults do nothing. + public void watchUncaughtExceptions() { + } + + // ------------------------------------------------------------------ + // Threads + + /// Whether the caller is the UI thread, which must not block. + public boolean isUiThread() { + return false; + } + + /// Runs `task`, which blocks, where blocking is allowed: off the UI + /// thread while that thread keeps serving events, or right here. + public void blockUi(Runnable task) { + task.run(); + } + + // ------------------------------------------------------------------ + // Resources, services and locale + + /// Opens the application resource `name` (a path such as + /// `META-INF/services/x`, without a leading slash), or null. + public InputStream resource(String name) { + return null; + } + + /// The class names of the providers of `service` the application + /// declares, from the [Registry]; empty when there is none. + public String[] providers(String service) { + Registry r = registry; + String[] names = r == null ? null : r.providers(service); + return names == null ? new String[0] : names; + } + + /// A new instance of the application class `className`, made by the + /// [Registry] without reflection; null when it cannot make one. + public Object newInstance(String className) { + Registry r = registry; + return r == null ? null : r.newInstance(className); + } + + /// The class of the application class `className`, as the [Registry] + /// knows it, so that its type can be tested without creating one; null + /// when it does not know the class. + public Class type(String className) { + Registry r = registry; + return r == null ? null : r.type(className); + } + + /// The decimal separator, grouping separator and currency symbol of the + /// device's locale, any of them null when unknown; or null when the + /// platform cannot tell. + public String[] localeNumberSymbols() { + return null; + } + + // ------------------------------------------------------------------ + // Security + + /// Fills `out` with cryptographically secure random bytes. + public void randomBytes(byte[] out) { + throw new UnsupportedOperationException("No secure random source"); + } + + /// A new digest for `algorithm` (`MD5`, `SHA-1`, `SHA-256` and the like), + /// or null when the platform has none. + public Digest digest(String algorithm) { + return null; + } + + /// The attributes [#fileStat] reports. + public static final class FileStat { + private final boolean directory; + private final long length; + private final long lastModified; + + /// A file or, when `directory` is true, a directory. + public FileStat(boolean directory, long length, long lastModified) { + this.directory = directory; + this.length = length; + this.lastModified = lastModified; + } + + /// Whether it is a directory. + public boolean isDirectory() { + return directory; + } + + /// The length in bytes of a file; 0 for a directory. + public long length() { + return length; + } + + /// When it was last modified, in milliseconds since the epoch, or 0. + public long lastModified() { + return lastModified; + } + } + + /// A running message digest, as [#digest] makes it. + public interface Digest { + /// Feeds `length` bytes of `data` from `offset`. + void update(byte[] data, int offset, int length); + + /// Finishes the digest and resets it. + byte[] digest(); + + /// Discards what was fed. + void reset(); + + /// The length of the digest in bytes. + int length(); + } + + /// What an application declares for `java.util.ServiceLoader` and + /// `java.util.ResourceBundle`, generated with the application. Once one + /// is installed it is the whole answer: the class library creates no + /// class from its name and reads no services file. + public interface Registry { + /// The provider class names declared for `service`, or null. + String[] providers(String service); + + /// A new instance of `className`, or null. + Object newInstance(String className); + + /// The class `className`, or null when the registry does not know it. + Class type(String className); + } + + /// The host [#get] answers when none was installed. + private static final class Defaults extends JdkHost { + } +} diff --git a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/BufferedInputStream.java b/Ports/CLDC11/src/java/io/BufferedInputStream.java similarity index 68% rename from maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/BufferedInputStream.java rename to Ports/CLDC11/src/java/io/BufferedInputStream.java index c054bb0d052..6c7efca43ef 100644 --- a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/BufferedInputStream.java +++ b/Ports/CLDC11/src/java/io/BufferedInputStream.java @@ -20,26 +20,35 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.androidcompat.jdk; +package java.io; -import java.io.IOException; -import java.io.InputStream; - -/// `java.io.BufferedInputStream` for the Codename One runtime, with mark and -/// reset support. +/// Reads from another stream through a buffer, so that many small reads +/// become few large ones, and supports `mark` and `reset`. +/// +/// Like most streams, it is for one thread at a time. public class BufferedInputStream extends FilterInputStream { - /// Null once the stream is closed, which every read checks. + /// The buffer; null once the stream is closed, which every read checks. protected byte[] buf; + /// How many bytes of [#buf] are valid. protected int count; + /// The index in [#buf] of the next byte to read. protected int pos; + /// The index in [#buf] of the mark, or -1 when there is none. protected int markpos = -1; + /// How far past the mark a read may go before the mark is dropped. protected int marklimit; + /// Buffers `in` with an 8192 byte buffer. public BufferedInputStream(InputStream in) { this(in, 8192); } + /// Buffers `in` with a buffer of `size` bytes. + /// + /// #### Throws + /// + /// - `IllegalArgumentException`: when `size` is not positive public BufferedInputStream(InputStream in, int size) { super(in); if (size <= 0) { @@ -54,6 +63,8 @@ private void ensureOpen() throws IOException { } } + /// Refills the buffer once it is consumed, keeping the bytes from a + /// valid mark on and growing the buffer up to the mark limit. private void fill() throws IOException { if (markpos < 0) { pos = 0; @@ -67,7 +78,14 @@ private void fill() throws IOException { markpos = -1; pos = 0; } else { - byte[] grown = new byte[Math.min(buf.length * 2, Math.max(marklimit, buf.length + 1))]; + int grownSize = buf.length * 2; + if (grownSize > marklimit) { + grownSize = marklimit; + } + if (grownSize <= buf.length) { + grownSize = buf.length + 1; + } + byte[] grown = new byte[grownSize]; System.arraycopy(buf, 0, grown, 0, pos); buf = grown; } @@ -91,12 +109,36 @@ public int read() throws IOException { return buf[pos++] & 0xff; } + /// Reads as the JDK does: on past the buffer while the stream underneath + /// has bytes available without blocking, so a small buffer does not make + /// every read a short one. @Override public int read(byte[] b, int off, int len) throws IOException { ensureOpen(); + if (off < 0 || len < 0 || len > b.length - off) { + throw new IndexOutOfBoundsException(); + } if (len == 0) { return 0; } + int total = 0; + while (true) { + int n = readOnce(b, off + total, len - total); + if (n <= 0) { + return total == 0 ? n : total; + } + total += n; + if (total >= len) { + return total; + } + InputStream input = in; + if (input != null && input.available() <= 0) { + return total; + } + } + } + + private int readOnce(byte[] b, int off, int len) throws IOException { int avail = count - pos; if (avail <= 0) { if (len >= buf.length && markpos < 0) { @@ -108,7 +150,7 @@ public int read(byte[] b, int off, int len) throws IOException { return -1; } } - int n = Math.min(avail, len); + int n = avail < len ? avail : len; System.arraycopy(buf, pos, b, off, n); pos += n; return n; @@ -131,7 +173,7 @@ public long skip(long n) throws IOException { return 0; } } - long skipped = Math.min(avail, n); + long skipped = avail < n ? avail : n; pos += (int) skipped; return skipped; } diff --git a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/BufferedOutputStream.java b/Ports/CLDC11/src/java/io/BufferedOutputStream.java similarity index 85% rename from maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/BufferedOutputStream.java rename to Ports/CLDC11/src/java/io/BufferedOutputStream.java index 02494565279..7494991e8e4 100644 --- a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/BufferedOutputStream.java +++ b/Ports/CLDC11/src/java/io/BufferedOutputStream.java @@ -20,25 +20,31 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.androidcompat.jdk; +package java.io; -import java.io.IOException; -import java.io.OutputStream; - -/// `java.io.BufferedOutputStream` for the Codename One runtime. +/// Writes to another stream through a buffer, so that many small writes +/// become few large ones. public class BufferedOutputStream extends FilterOutputStream { + /// The buffer. protected byte[] buf; + /// How many bytes of [#buf] are waiting to be written. protected int count; /// Set by [#close()]. The buffer and the underlying stream are kept, so /// without it a later write would land in the buffer and a second close /// could hand those bytes to a stream whose own close is a no-op. private boolean closed; + /// Buffers `out` with an 8192 byte buffer. public BufferedOutputStream(OutputStream out) { this(out, 8192); } + /// Buffers `out` with a buffer of `size` bytes. + /// + /// #### Throws + /// + /// - `IllegalArgumentException`: when `size` is not positive public BufferedOutputStream(OutputStream out, int size) { super(out); if (size <= 0) { @@ -72,6 +78,9 @@ public void write(int b) throws IOException { @Override public void write(byte[] b, int off, int len) throws IOException { ensureOpen(); + if (off < 0 || len < 0 || len > b.length - off) { + throw new IndexOutOfBoundsException(); + } if (len >= buf.length) { flushBuffer(); out.write(b, off, len); diff --git a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/BufferedReader.java b/Ports/CLDC11/src/java/io/BufferedReader.java similarity index 72% rename from maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/BufferedReader.java rename to Ports/CLDC11/src/java/io/BufferedReader.java index dd5b62fdb4c..c3d0ccc494c 100644 --- a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/BufferedReader.java +++ b/Ports/CLDC11/src/java/io/BufferedReader.java @@ -20,21 +20,18 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.androidcompat.jdk; +package java.io; -import java.io.IOException; -import java.io.Reader; - -/// `java.io.BufferedReader` for the Codename One runtime, which has no -/// buffered reader. Android code is compiled against the JDK class and the -/// build's remap step points it here, together with the compatibility -/// runtime's own uses. +/// Reads text from another reader through a buffer, and reads it a line at +/// a time. A line ends at `\n`, `\r` or `\r\n`. +/// +/// Like most readers, it is for one thread at a time. public class BufferedReader extends Reader { - private final Reader in; private static final int UNMARKED = -1; private static final int INVALIDATED = -2; + private final Reader in; /// Null once the reader is closed, which every operation checks. private char[] buf; private int pos; @@ -45,11 +42,18 @@ public class BufferedReader extends Reader { private int readAheadLimit; private boolean markedSkipLf; + /// Buffers `in` with an 8192 character buffer. public BufferedReader(Reader in) { this(in, 8192); } + /// Buffers `in` with a buffer of `size` characters. + /// + /// #### Throws + /// + /// - `IllegalArgumentException`: when `size` is not positive public BufferedReader(Reader in, int size) { + super(in); if (size <= 0) { throw new IllegalArgumentException("Buffer size <= 0"); } @@ -57,15 +61,15 @@ public BufferedReader(Reader in, int size) { this.buf = new char[size]; } - /// Refills the buffer once it is consumed. A valid mark keeps the - /// characters from the mark on at the start of the buffer, growing it up - /// to the read-ahead limit; reading past that limit drops the mark. private void ensureOpen() throws IOException { if (buf == null) { throw new IOException("Stream closed"); } } + /// Refills the buffer once it is consumed. A valid mark keeps the + /// characters from the mark on at the start of the buffer, growing it up + /// to the read-ahead limit; reading past that limit drops the mark. private boolean fill() throws IOException { int dst = 0; if (markedChar >= 0) { @@ -117,6 +121,9 @@ public int read() throws IOException { @Override public int read(char[] cbuf, int off, int len) throws IOException { ensureOpen(); + if (off < 0 || len < 0 || len > cbuf.length - off) { + throw new IndexOutOfBoundsException(); + } if (len == 0) { return 0; } @@ -134,6 +141,11 @@ public int read(char[] cbuf, int off, int len) throws IOException { return n; } + /// Reads a line of text, without its terminator. + /// + /// #### Returns + /// + /// the line, or null at the end of the text public String readLine() throws IOException { ensureOpen(); StringBuilder sb = null; @@ -179,6 +191,9 @@ public boolean ready() throws IOException { @Override public long skip(long n) throws IOException { + if (n < 0) { + throw new IllegalArgumentException("skip value is negative"); + } ensureOpen(); long skipped = 0; while (skipped < n && read() >= 0) { @@ -221,4 +236,59 @@ public void close() throws IOException { buf = null; in.close(); } + + /// The remaining lines, read as the stream asks for them. A read that + /// fails surfaces as an UncheckedIOException; closing the stream does + /// not close this reader, as in the JDK. + public java.util.stream.Stream lines() { + return java.util.stream.StreamSupport.stream( + java.util.Spliterators.spliteratorUnknownSize(new LineIterator(this), + java.util.Spliterator.ORDERED | java.util.Spliterator.NONNULL), false); + } + + /// Reads one line ahead, so `hasNext` can answer without losing one. + private static final class LineIterator implements java.util.Iterator { + private final BufferedReader reader; + private String next; + private boolean done; + + LineIterator(BufferedReader reader) { + this.reader = reader; + } + + @Override + public boolean hasNext() { + if (next != null) { + return true; + } + if (done) { + return false; + } + try { + next = reader.readLine(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + if (next == null) { + done = true; + return false; + } + return true; + } + + @Override + public String next() { + if (!hasNext()) { + throw new java.util.NoSuchElementException(); + } + String line = next; + next = null; + return line; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } } diff --git a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/BufferedWriter.java b/Ports/CLDC11/src/java/io/BufferedWriter.java similarity index 85% rename from maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/BufferedWriter.java rename to Ports/CLDC11/src/java/io/BufferedWriter.java index 6402631b82f..89377b497b1 100644 --- a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/BufferedWriter.java +++ b/Ports/CLDC11/src/java/io/BufferedWriter.java @@ -20,12 +20,9 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.androidcompat.jdk; +package java.io; -import java.io.IOException; -import java.io.Writer; - -/// `java.io.BufferedWriter` for the Codename One runtime; see [BufferedReader]. +/// Writes text to another writer through a buffer. public class BufferedWriter extends Writer { private final Writer out; @@ -36,11 +33,21 @@ public class BufferedWriter extends Writer { /// could hand that data to a writer whose own close is a no-op. private boolean closed; + /// Buffers `out` with an 8192 character buffer. public BufferedWriter(Writer out) { this(out, 8192); } + /// Buffers `out` with a buffer of `size` characters. + /// + /// #### Throws + /// + /// - `IllegalArgumentException`: when `size` is not positive public BufferedWriter(Writer out, int size) { + super(out); + if (size <= 0) { + throw new IllegalArgumentException("Buffer size <= 0"); + } this.out = out; this.buf = new char[size]; } @@ -70,6 +77,9 @@ public void write(int c) throws IOException { @Override public void write(char[] cbuf, int off, int len) throws IOException { ensureOpen(); + if (off < 0 || len < 0 || len > cbuf.length - off) { + throw new IndexOutOfBoundsException(); + } if (len >= buf.length) { flushBuffer(); out.write(cbuf, off, len); @@ -90,6 +100,7 @@ public void write(String s, int off, int len) throws IOException { write(c, 0, len); } + /// Writes a line separator, `\n`. public void newLine() throws IOException { write('\n'); } diff --git a/Ports/CLDC11/src/java/io/ByteArrayOutputStream.java b/Ports/CLDC11/src/java/io/ByteArrayOutputStream.java index 59ca3736a2b..545490821d5 100644 --- a/Ports/CLDC11/src/java/io/ByteArrayOutputStream.java +++ b/Ports/CLDC11/src/java/io/ByteArrayOutputStream.java @@ -83,4 +83,16 @@ public void write(int b){ return; //TODO codavaj!! } + @Deprecated + public String toString(int hibyte) { + return null; + } + + /// The contents decoded with `charset`. + public synchronized String toString(java.nio.charset.Charset charset) { + return null; + } + + public synchronized void writeTo(OutputStream out) throws IOException { + } } diff --git a/Ports/CLDC11/src/java/io/Closeable.java b/Ports/CLDC11/src/java/io/Closeable.java new file mode 100644 index 00000000000..529e9a57652 --- /dev/null +++ b/Ports/CLDC11/src/java/io/Closeable.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.io; + +/// A source or destination of data that holds something until it is closed: +/// every stream, reader and writer. +public interface Closeable extends AutoCloseable { + /// Closes this and releases what it holds. Closing it again has no effect. + @Override + void close() throws IOException; +} diff --git a/Ports/CLDC11/src/java/io/Externalizable.java b/Ports/CLDC11/src/java/io/Externalizable.java new file mode 100644 index 00000000000..b4134541904 --- /dev/null +++ b/Ports/CLDC11/src/java/io/Externalizable.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.io; + +/// An object that writes and reads its own state through [ObjectOutput] and +/// [ObjectInput]. +public interface Externalizable extends Serializable { + + /// Restores this object's state from `in`. + void readExternal(ObjectInput in) throws IOException, ClassNotFoundException; + + /// Writes this object's state to `out`. + void writeExternal(ObjectOutput out) throws IOException; +} diff --git a/Ports/CLDC11/src/java/io/File.java b/Ports/CLDC11/src/java/io/File.java new file mode 100644 index 00000000000..1fece0caed6 --- /dev/null +++ b/Ports/CLDC11/src/java/io/File.java @@ -0,0 +1,763 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.io; + +import com.codename1.impl.JdkHost; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +/// A path in the file system, and the operations on the file or directory at +/// it. +/// +/// Every operation goes to the platform's file system through +/// `com.codename1.impl.JdkHost`: on a device that is `FileSystemStorage`, in a +/// program translated without Codename One it is the native file system. +/// +/// #### Paths +/// +/// A path is kept with duplicate separators collapsed and no trailing one, +/// and a backslash is read as a separator. It is absolute when it starts with +/// `/`, with a drive (`C:/`), or with the `file:` scheme Codename One names its +/// files with. A relative path is resolved against the application's home +/// directory, which in a program run from a shell is the working directory. +/// +/// #### What differs from a desktop file system +/// +/// - Where the file system has no permissions, every file that exists may be +/// read and written, a directory may be entered and nothing may be run; the +/// permission setters answer whether the file already is as asked. +/// - [#setLastModified(long)] answers false: no platform lets it be set. +/// - [#deleteOnExit()] does nothing. +/// - Where the file system cannot resolve symbolic links, +/// [#getCanonicalPath()] answers the absolute path with `.` and `..` +/// resolved. +public class File implements Serializable, Comparable { + + private static final long serialVersionUID = 1L; + + /// The separator of the names in a path. + public static final char separatorChar = '/'; + /// [#separatorChar] as a string. + public static final String separator = "/"; + /// The separator of the paths in a list of them. + public static final char pathSeparatorChar = ':'; + /// [#pathSeparatorChar] as a string. + public static final String pathSeparator = ":"; + + private static final String SCHEME = "file:"; + + private final String path; + + /// The file at `pathname`. + /// + /// #### Throws + /// + /// - `NullPointerException`: when `pathname` is null + public File(String pathname) { + if (pathname == null) { + throw new NullPointerException(); + } + this.path = normalize(pathname); + } + + /// The file `child` in the directory `parent`. A null `parent` makes it + /// `new File(child)`, an empty one the root. + /// + /// #### Throws + /// + /// - `NullPointerException`: when `child` is null + public File(String parent, String child) { + if (child == null) { + throw new NullPointerException(); + } + if (parent == null) { + this.path = normalize(child); + } else if (parent.length() == 0) { + this.path = join(separator, child); + } else { + this.path = join(normalize(parent), child); + } + } + + /// The file `child` in the directory `parent`. A null `parent` makes it + /// `new File(child)`, an empty one the root. + /// + /// #### Throws + /// + /// - `NullPointerException`: when `child` is null + public File(File parent, String child) { + if (child == null) { + throw new NullPointerException(); + } + if (parent == null) { + this.path = normalize(child); + } else if (parent.path.length() == 0) { + this.path = join(separator, child); + } else { + this.path = join(parent.path, child); + } + } + + /// The file a `file:` URI names. + /// + /// #### Throws + /// + /// - `IllegalArgumentException`: when `uri` is not an absolute, + /// hierarchical `file:` URI with a path and without an authority, a query + /// or a fragment + public File(URI uri) { + if (!uri.isAbsolute()) { + throw new IllegalArgumentException("URI is not absolute"); + } + if (uri.isOpaque()) { + throw new IllegalArgumentException("URI is not hierarchical"); + } + if (!"file".equalsIgnoreCase(uri.getScheme())) { + throw new IllegalArgumentException("URI scheme is not \"file\""); + } + if (present(uri.getRawAuthority())) { + throw new IllegalArgumentException("URI has an authority component"); + } + if (present(uri.getRawFragment())) { + throw new IllegalArgumentException("URI has a fragment component"); + } + if (present(uri.getRawQuery())) { + throw new IllegalArgumentException("URI has a query component"); + } + String p = uri.getPath(); + if (p == null || p.length() == 0) { + throw new IllegalArgumentException("URI path component is empty"); + } + this.path = normalize(p); + } + + /// Whether a URI has the component. An empty one counts as none: the + /// runtime's `URI` answers an empty authority for `file:///x`, where the + /// JDK's answers null. + private static boolean present(String component) { + return component != null && component.length() > 0; + } + + private static JdkHost host() { + return JdkHost.get(); + } + + /// How many leading characters of `p` are its root: the scheme and its + /// slashes, `/`, a drive with its separator, or none. + private static int rootLength(String p) { + if (p.startsWith(SCHEME)) { + int i = SCHEME.length(); + while (i < p.length() && p.charAt(i) == '/') { + i++; + } + return i; + } + if (p.startsWith("/")) { + return 1; + } + if (p.length() >= 3 && p.charAt(1) == ':' && p.charAt(2) == '/' && isLetter(p.charAt(0))) { + return 3; + } + return 0; + } + + private static boolean isLetter(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); + } + + /// Reads a backslash as a separator, collapses duplicate separators + /// outside the scheme and drops a trailing one, as `java.io.File` does. + private static String normalize(String p) { + StringBuilder sb = new StringBuilder(p.length()); + int start = 0; + if (p.startsWith("file://")) { + sb.append("file://"); + start = 7; + } + char prev = 0; + for (int i = start; i < p.length(); i++) { + char c = p.charAt(i); + if (c == '\\') { + c = '/'; + } + if (c == '/' && prev == '/') { + continue; + } + sb.append(c); + prev = c; + } + int min = start + 1; + while (sb.length() > min && sb.charAt(sb.length() - 1) == '/') { + sb.setLength(sb.length() - 1); + } + return sb.toString(); + } + + private static String join(String parent, String child) { + String c = normalize(child); + if (parent.length() == 0) { + return c; + } + if (c.startsWith("/")) { + c = c.substring(1); + } + if (c.length() == 0) { + return parent; + } + return parent.endsWith("/") ? parent + c : parent + "/" + c; + } + + /// The path the host is asked about: this one when it is absolute, else + /// resolved against the application's home directory. + String hostPath() { + if (path.startsWith("file:/") || (rootLength(path) > 0 && !path.startsWith(SCHEME))) { + return path; + } + // A relative path, a "file:" with no slash after it included: a + // device's file system throws for anything but an absolute path. + String rel = path.startsWith(SCHEME) ? path.substring(SCHEME.length()) : path; + String home = host().appHome(); + if (rel.length() == 0) { + return home; + } + return home.endsWith("/") ? home + rel : home + "/" + rel; + } + + /// The last name of the path, or the whole path when it has one name. + public String getName() { + int i = path.lastIndexOf('/'); + return i < 0 ? path : path.substring(i + 1); + } + + /// The path as it was given, normalized. + public String getPath() { + return path; + } + + /// The path resolved against the application's home directory when it is + /// relative. + public String getAbsolutePath() { + return hostPath(); + } + + /// The file at [#getAbsolutePath()]. + public File getAbsoluteFile() { + return new File(getAbsolutePath()); + } + + /// The absolute path with symbolic links, `.` and `..` resolved. Code + /// checks a canonical path against a directory to reject a name like + /// `root/../outside`, so where the file system cannot resolve links the + /// dots are still resolved; a `..` at the root stays at the root. + public String getCanonicalPath() throws IOException { + String abs = hostPath(); + String canonical = host().fileCanonicalPath(abs); + return canonical != null ? canonical : resolveDots(abs); + } + + /// The file at [#getCanonicalPath()]. + public File getCanonicalFile() throws IOException { + return new File(getCanonicalPath()); + } + + private static String resolveDots(String p) { + int start = rootLength(p); + ArrayList segs = new ArrayList(); + int s = start; + for (int i = start; i <= p.length(); i++) { + if (i == p.length() || p.charAt(i) == '/') { + String seg = p.substring(s, i); + if (seg.equals("..")) { + if (!segs.isEmpty()) { + segs.remove(segs.size() - 1); + } + } else if (seg.length() > 0 && !seg.equals(".")) { + segs.add(seg); + } + s = i + 1; + } + } + StringBuilder sb = new StringBuilder(p.length()); + sb.append(p.substring(0, start)); + for (int i = 0; i < segs.size(); i++) { + if (i > 0) { + sb.append('/'); + } + sb.append(segs.get(i)); + } + return sb.toString(); + } + + /// The path without its last name, or null when it has no parent. + public String getParent() { + int i = path.lastIndexOf('/'); + if (i < 0) { + return null; + } + int root = rootLength(path); + if (i < root) { + return path.length() > root ? path.substring(0, root) : null; + } + if (i == root - 1) { + return path.length() > root ? path.substring(0, root) : null; + } + return path.substring(0, i); + } + + /// The file at [#getParent()], or null. + public File getParentFile() { + String p = getParent(); + return p == null ? null : new File(p); + } + + /// Whether the path is absolute. + public boolean isAbsolute() { + return rootLength(path) > 0; + } + + /// Whether something is at the path. + public boolean exists() { + return host().fileTest(hostPath(), JdkHost.FILE_EXISTS); + } + + /// Whether the path is a directory. + public boolean isDirectory() { + return host().fileTest(hostPath(), JdkHost.FILE_DIRECTORY); + } + + /// Whether the path is a file that is not a directory. + public boolean isFile() { + return host().fileTest(hostPath(), JdkHost.FILE_REGULAR); + } + + /// Whether the file is hidden; on most file systems, whether its name + /// starts with a dot. + public boolean isHidden() { + return host().fileTest(hostPath(), JdkHost.FILE_HIDDEN); + } + + /// Whether the file may be read. + public boolean canRead() { + return host().fileTest(hostPath(), JdkHost.FILE_READABLE); + } + + /// Whether the file may be written. + public boolean canWrite() { + return host().fileTest(hostPath(), JdkHost.FILE_WRITABLE); + } + + /// Whether the file may be run, or the directory entered. + public boolean canExecute() { + return host().fileTest(hostPath(), JdkHost.FILE_EXECUTABLE); + } + + /// Lets the file be read, or not; false when it is not left as asked. + public boolean setReadable(boolean readable, boolean ownerOnly) { + return host().fileSet(hostPath(), JdkHost.FILE_READABLE, readable); + } + + /// Lets the file be read, or not; false when it is not left as asked. + public boolean setReadable(boolean readable) { + return setReadable(readable, true); + } + + /// Lets the file be written, or not; false when it is not left as asked. + public boolean setWritable(boolean writable, boolean ownerOnly) { + return host().fileSet(hostPath(), JdkHost.FILE_WRITABLE, writable); + } + + /// Lets the file be written, or not; false when it is not left as asked. + public boolean setWritable(boolean writable) { + return setWritable(writable, true); + } + + /// Lets the file be run, or not; false when it is not left as asked. + public boolean setExecutable(boolean executable, boolean ownerOnly) { + return host().fileSet(hostPath(), JdkHost.FILE_EXECUTABLE, executable); + } + + /// Lets the file be run, or not; false when it is not left as asked. + public boolean setExecutable(boolean executable) { + return setExecutable(executable, true); + } + + /// Stops the file being written; false when it still can be. + public boolean setReadOnly() { + return host().fileSet(hostPath(), JdkHost.FILE_WRITABLE, false); + } + + /// Answers false: no platform lets a file's time be set. + /// + /// #### Throws + /// + /// - `IllegalArgumentException`: when `time` is negative + public boolean setLastModified(long time) { + if (time < 0) { + throw new IllegalArgumentException("Negative time"); + } + return false; + } + + /// The length of the file in bytes; 0 for a directory or when nothing is + /// there. + public long length() { + return host().fileLength(hostPath()); + } + + /// When the file was last modified, in milliseconds since the epoch; 0 + /// when nothing is there. + public long lastModified() { + return host().fileLastModified(hostPath()); + } + + /// The size of the volume holding the file, or 0 when unknown. + public long getTotalSpace() { + return host().fileTotalSpace(hostPath()); + } + + /// The bytes free on the volume holding the file, or 0 when unknown. + public long getFreeSpace() { + return host().fileFreeSpace(hostPath()); + } + + /// The bytes this program may still write on the volume holding the + /// file, or 0 when unknown. + public long getUsableSpace() { + return host().fileUsableSpace(hostPath()); + } + + /// Creates the file, empty, when nothing is at the path. + /// + /// #### Returns + /// + /// true when it was created, false when something was there + /// + /// #### Throws + /// + /// - `IOException`: when the directory it would be in does not exist, or + /// the file could not be created + public boolean createNewFile() throws IOException { + File parent = getParentFile(); + if (parent != null && !parent.isDirectory()) { + throw new IOException("No such file or directory: " + getParent()); + } + return host().fileCreate(hostPath()); + } + + /// Deletes the file, or the directory when it is empty. + /// + /// #### Returns + /// + /// whether it was deleted + public boolean delete() { + return host().fileDelete(hostPath()); + } + + /// Does nothing: nothing is deleted when the program ends. + public void deleteOnExit() { + } + + /// Creates the directory, in a parent that must exist. + /// + /// #### Returns + /// + /// whether it was created + public boolean mkdir() { + return host().fileMkdir(hostPath()); + } + + /// Creates the directory and the parents it needs. + /// + /// #### Returns + /// + /// whether it was created; false also when it already existed + public boolean mkdirs() { + if (exists()) { + return false; + } + if (mkdir()) { + return true; + } + File p = getParentFile(); + return p != null && (p.mkdirs() || p.exists()) && mkdir(); + } + + /// Moves the file or directory to `dest`. A file replaces a file already + /// there. Where the platform cannot move a file in place, it is copied and + /// the original deleted; when the original will not go, the copy is taken + /// back unless it replaced something. + /// + /// #### Returns + /// + /// whether it was moved + /// + /// #### Throws + /// + /// - `NullPointerException`: when `dest` is null + public boolean renameTo(File dest) { + if (dest == null) { + throw new NullPointerException(); + } + JdkHost h = host(); + String from = hostPath(); + String to = dest.hostPath(); + if (!h.fileTest(from, JdkHost.FILE_EXISTS)) { + return false; + } + // A file renamed onto itself is already where it was asked to be. + // The copy below would open the same file for reading and writing, + // truncating it, and then delete it. + if (resolveDots(from).equals(resolveDots(to))) { + return true; + } + boolean replacing = h.fileTest(to, JdkHost.FILE_EXISTS); + if (replacing ? h.fileReplace(from, to) : h.fileRename(from, to)) { + return true; + } + if (h.fileTest(from, JdkHost.FILE_DIRECTORY) || (replacing && h.fileTest(to, JdkHost.FILE_DIRECTORY))) { + return false; + } + try { + InputStream in = h.fileRead(from); + try { + OutputStream out = h.fileWrite(to, false); + try { + byte[] buffer = new byte[8192]; + int n; + while ((n = in.read(buffer, 0, buffer.length)) >= 0) { + out.write(buffer, 0, n); + } + } finally { + out.close(); + } + } finally { + in.close(); + } + } catch (IOException e) { + return false; + } + // The move only happened if the source is gone. When it cannot be + // removed the rename fails, and a copy that created the destination + // is taken back so the file is left where it was; one that replaced + // an existing destination stays, as deleting it would lose both. + h.fileDelete(from); + if (h.fileTest(from, JdkHost.FILE_EXISTS)) { + if (!replacing) { + h.fileDelete(to); + } + return false; + } + return true; + } + + /// The names of the entries of the directory, or null when this is not a + /// directory. + public String[] list() { + String[] names; + try { + names = host().fileList(hostPath()); + } catch (IOException e) { + return null; + } + if (names == null) { + return null; + } + String[] out = new String[names.length]; + for (int i = 0; i < names.length; i++) { + String n = names[i]; + while (n.endsWith("/")) { + n = n.substring(0, n.length() - 1); + } + int slash = n.lastIndexOf('/'); + out[i] = slash < 0 ? n : n.substring(slash + 1); + } + return out; + } + + /// The names of the entries of the directory that `filter` accepts, or + /// null when this is not a directory. A null `filter` accepts all. + public String[] list(FilenameFilter filter) { + String[] names = list(); + if (names == null || filter == null) { + return names; + } + List out = new ArrayList(); + for (int i = 0; i < names.length; i++) { + if (filter.accept(this, names[i])) { + out.add(names[i]); + } + } + return out.toArray(new String[out.size()]); + } + + /// The entries of the directory, or null when this is not a directory. + public File[] listFiles() { + return files(list()); + } + + /// The entries of the directory whose names `filter` accepts, or null when + /// this is not a directory. A null `filter` accepts all. + public File[] listFiles(FilenameFilter filter) { + return files(list(filter)); + } + + /// The entries of the directory that `filter` accepts, or null when this + /// is not a directory. A null `filter` accepts all. + public File[] listFiles(FileFilter filter) { + File[] all = listFiles(); + if (all == null || filter == null) { + return all; + } + List out = new ArrayList(); + for (int i = 0; i < all.length; i++) { + if (filter.accept(all[i])) { + out.add(all[i]); + } + } + return out.toArray(new File[out.size()]); + } + + private File[] files(String[] names) { + if (names == null) { + return null; + } + File[] out = new File[names.length]; + for (int i = 0; i < names.length; i++) { + out[i] = new File(this, names[i]); + } + return out; + } + + /// Creates an empty file named `prefix`, a number and `suffix` in the + /// directory for disposable files. + public static File createTempFile(String prefix, String suffix) throws IOException { + return createTempFile(prefix, suffix, null); + } + + /// Creates an empty file named `prefix`, a number and `suffix` (`.tmp` + /// when null) in `directory`, or in the directory for disposable files + /// when that is null. Nothing deletes it. + /// + /// #### Throws + /// + /// - `IllegalArgumentException`: when `prefix` is shorter than three + /// characters + /// - `IOException`: when the file could not be created + public static File createTempFile(String prefix, String suffix, File directory) throws IOException { + if (prefix.length() < 3) { + throw new IllegalArgumentException("Prefix string too short"); + } + File dir = directory; + if (dir == null) { + JdkHost h = host(); + String caches = h.cachesDir(); + dir = new File(caches != null ? caches : h.appHome()); + } + String s = suffix == null ? ".tmp" : suffix; + long n = System.currentTimeMillis(); + while (true) { + File f = new File(dir, prefix + n + s); + if (f.createNewFile()) { + return f; + } + if (!f.exists()) { + throw new IOException("Unable to create temporary file " + f.getPath()); + } + n++; + } + } + + /// The roots of the file system. + public static File[] listRoots() { + String[] roots = host().fileRoots(); + File[] out = new File[roots == null ? 0 : roots.length]; + for (int i = 0; i < out.length; i++) { + out[i] = new File(roots[i]); + } + return out; + } + + /// Compares the paths. + @Override + public int compareTo(File other) { + return path.compareTo(other.path); + } + + /// Whether `o` is a file with the same path. + @Override + public boolean equals(Object o) { + return o instanceof File && ((File) o).path.equals(path); + } + + @Override + public int hashCode() { + return path.hashCode() ^ 1234321; + } + + /// The path. + @Override + public String toString() { + return path; + } + + /// This file as a `java.nio.file.Path`. + public Path toPath() { + return Paths.get(path); + } + + /// The absolute path, without the scheme and with a single leading + /// separator, as a URI's path. + private String uriPath() { + String abs = getAbsolutePath(); + if (abs.startsWith(SCHEME)) { + int at = SCHEME.length(); + while (at + 1 < abs.length() && abs.charAt(at) == '/' && abs.charAt(at + 1) == '/') { + at++; + } + abs = abs.substring(at); + } + if (!abs.startsWith("/")) { + abs = "/" + abs; + } + return abs; + } + + /// The `file:` URI of the absolute path, ending in a separator when the + /// file is a directory. + public URI toURI() { + String p = uriPath(); + if (!p.endsWith("/") && isDirectory()) { + p = p + "/"; + } + try { + return new URI("file", null, p, null, null); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e.getMessage()); + } + } +} diff --git a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FileFilter.java b/Ports/CLDC11/src/java/io/FileFilter.java similarity index 91% rename from maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FileFilter.java rename to Ports/CLDC11/src/java/io/FileFilter.java index 327b03d7df0..815ce200515 100644 --- a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FileFilter.java +++ b/Ports/CLDC11/src/java/io/FileFilter.java @@ -20,9 +20,11 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.androidcompat.jdk; +package java.io; -/// `java.io.FileFilter` for the Codename One runtime. +/// Decides which files `File.listFiles(FileFilter)` answers. public interface FileFilter { + + /// Whether `pathname` is wanted. boolean accept(File pathname); } diff --git a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FileInputStream.java b/Ports/CLDC11/src/java/io/FileInputStream.java similarity index 57% rename from maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FileInputStream.java rename to Ports/CLDC11/src/java/io/FileInputStream.java index afc79341676..c775a53861b 100644 --- a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FileInputStream.java +++ b/Ports/CLDC11/src/java/io/FileInputStream.java @@ -20,57 +20,99 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.androidcompat.jdk; +package java.io; -import com.codename1.io.FileSystemStorage; +import com.codename1.impl.JdkHost; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; - -/// `java.io.FileInputStream` for the Codename One runtime, reading through -/// `FileSystemStorage`. +/// Reads the bytes of a file, from the platform's file system through +/// `com.codename1.impl.JdkHost`. +/// +/// Like most streams, it is for one thread at a time; closing it twice, or +/// from another thread, is safe. public class FileInputStream extends InputStream { private final InputStream in; + private boolean closed; + /// Opens the file at `name`. + /// + /// #### Throws + /// + /// - `FileNotFoundException`: when it is not there, is a directory or + /// cannot be read public FileInputStream(String name) throws FileNotFoundException { this(new File(name)); } + /// Opens `file`. + /// + /// #### Throws + /// + /// - `FileNotFoundException`: when it is not there, is a directory or + /// cannot be read public FileInputStream(File file) throws FileNotFoundException { - if (!file.isFile()) { - throw new FileNotFoundException(file.getPath() + " (No such file or directory)"); + if (file == null) { + throw new NullPointerException(); } try { - in = FileSystemStorage.getInstance().openInputStream(file.storagePath()); + in = JdkHost.get().fileRead(file.hostPath()); + } catch (FileNotFoundException e) { + throw e; } catch (IOException e) { throw new FileNotFoundException(file.getPath() + " (" + e.getMessage() + ")"); } + if (in == null) { + throw new FileNotFoundException(file.getPath()); + } + } + + private void checkOpen() throws IOException { + if (closed) { + throw new IOException("Stream closed"); + } } @Override public int read() throws IOException { + checkOpen(); return in.read(); } + @Override + public int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } + @Override public int read(byte[] b, int off, int len) throws IOException { + if (off < 0 || len < 0 || len > b.length - off) { + throw new IndexOutOfBoundsException(); + } + checkOpen(); + if (len == 0) { + return 0; + } return in.read(b, off, len); } @Override public long skip(long n) throws IOException { - return in.skip(n); + checkOpen(); + return n <= 0 ? 0 : in.skip(n); } @Override public int available() throws IOException { + checkOpen(); return in.available(); } @Override public void close() throws IOException { + if (closed) { + return; + } + closed = true; in.close(); } } diff --git a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FileOutputStream.java b/Ports/CLDC11/src/java/io/FileOutputStream.java similarity index 54% rename from maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FileOutputStream.java rename to Ports/CLDC11/src/java/io/FileOutputStream.java index 93ffcd3b620..675e607e40e 100644 --- a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FileOutputStream.java +++ b/Ports/CLDC11/src/java/io/FileOutputStream.java @@ -20,71 +20,110 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.androidcompat.jdk; +package java.io; -import com.codename1.io.FileSystemStorage; +import com.codename1.impl.JdkHost; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.OutputStream; - -/// `java.io.FileOutputStream` for the Codename One runtime, writing through -/// `FileSystemStorage`. Appending opens the storage stream at the file's -/// current length. +/// Writes the bytes of a file, to the platform's file system through +/// `com.codename1.impl.JdkHost`. The file is created when it is not there. +/// +/// Like most streams, it is for one thread at a time; closing it twice, or +/// from another thread, is safe. public class FileOutputStream extends OutputStream { private final OutputStream out; + private boolean closed; + /// Opens the file at `name`, emptying it. + /// + /// #### Throws + /// + /// - `FileNotFoundException`: when it is a directory or cannot be written public FileOutputStream(String name) throws FileNotFoundException { this(new File(name), false); } + /// Opens the file at `name`, at its end when `append` is true and emptied + /// otherwise. + /// + /// #### Throws + /// + /// - `FileNotFoundException`: when it is a directory or cannot be written public FileOutputStream(String name, boolean append) throws FileNotFoundException { this(new File(name), append); } + /// Opens `file`, emptying it. + /// + /// #### Throws + /// + /// - `FileNotFoundException`: when it is a directory or cannot be written public FileOutputStream(File file) throws FileNotFoundException { this(file, false); } + /// Opens `file`, at its end when `append` is true and emptied otherwise. + /// + /// #### Throws + /// + /// - `FileNotFoundException`: when it is a directory or cannot be written public FileOutputStream(File file, boolean append) throws FileNotFoundException { - if (file.isDirectory()) { - throw new FileNotFoundException(file.getPath() + " (Is a directory)"); - } - File parent = file.getParentFile(); - if (parent != null && !parent.isDirectory()) { - throw new FileNotFoundException(file.getPath() + " (No such file or directory)"); + if (file == null) { + throw new NullPointerException(); } try { - FileSystemStorage fs = FileSystemStorage.getInstance(); - String p = file.storagePath(); - if (append && fs.exists(p)) { - out = fs.openOutputStream(p, (int) fs.getLength(p)); - } else { - out = fs.openOutputStream(p); - } + out = JdkHost.get().fileWrite(file.hostPath(), append); + } catch (FileNotFoundException e) { + throw e; } catch (IOException e) { throw new FileNotFoundException(file.getPath() + " (" + e.getMessage() + ")"); } + if (out == null) { + throw new FileNotFoundException(file.getPath()); + } + } + + private void checkOpen() throws IOException { + if (closed) { + throw new IOException("Stream closed"); + } } @Override public void write(int b) throws IOException { + checkOpen(); out.write(b); } + @Override + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + @Override public void write(byte[] b, int off, int len) throws IOException { + if (off < 0 || len < 0 || len > b.length - off) { + throw new IndexOutOfBoundsException(); + } + checkOpen(); + if (len == 0) { + return; + } out.write(b, off, len); } @Override public void flush() throws IOException { + checkOpen(); out.flush(); } @Override public void close() throws IOException { + if (closed) { + return; + } + closed = true; out.close(); } } diff --git a/Ports/CLDC11/src/java/io/FileReader.java b/Ports/CLDC11/src/java/io/FileReader.java new file mode 100644 index 00000000000..ac492f1e623 --- /dev/null +++ b/Ports/CLDC11/src/java/io/FileReader.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.io; + +import java.nio.charset.Charset; + +/// Reads the text of a file, in UTF-8 unless a charset is given. +public class FileReader extends InputStreamReader { + + /// Opens the file at `fileName`. + /// + /// #### Throws + /// + /// - `FileNotFoundException`: when it is not there, is a directory or + /// cannot be read + public FileReader(String fileName) throws FileNotFoundException { + super(new FileInputStream(fileName)); + } + + /// Opens `file`. + /// + /// #### Throws + /// + /// - `FileNotFoundException`: when it is not there, is a directory or + /// cannot be read + public FileReader(File file) throws FileNotFoundException { + super(new FileInputStream(file)); + } + + /// Opens the file at `fileName`, reading `charset`. + public FileReader(String fileName, Charset charset) throws IOException { + super(new FileInputStream(fileName), charset.name()); + } + + /// Opens `file`, reading `charset`. + public FileReader(File file, Charset charset) throws IOException { + super(new FileInputStream(file), charset.name()); + } +} diff --git a/Ports/CLDC11/src/java/io/FileWriter.java b/Ports/CLDC11/src/java/io/FileWriter.java new file mode 100644 index 00000000000..e5415f4be95 --- /dev/null +++ b/Ports/CLDC11/src/java/io/FileWriter.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.io; + +import java.nio.charset.Charset; + +/// Writes text to a file, in UTF-8 unless a charset is given. +public class FileWriter extends OutputStreamWriter { + + /// Opens the file at `fileName`, emptying it. + public FileWriter(String fileName) throws IOException { + super(new FileOutputStream(fileName)); + } + + /// Opens the file at `fileName`, at its end when `append` is true. + public FileWriter(String fileName, boolean append) throws IOException { + super(new FileOutputStream(fileName, append)); + } + + /// Opens `file`, emptying it. + public FileWriter(File file) throws IOException { + super(new FileOutputStream(file)); + } + + /// Opens `file`, at its end when `append` is true. + public FileWriter(File file, boolean append) throws IOException { + super(new FileOutputStream(file, append)); + } + + /// Opens the file at `fileName`, emptying it, writing `charset`. + public FileWriter(String fileName, Charset charset) throws IOException { + super(new FileOutputStream(fileName), charset.name()); + } + + /// Opens the file at `fileName`, at its end when `append` is true, + /// writing `charset`. + public FileWriter(String fileName, Charset charset, boolean append) throws IOException { + super(new FileOutputStream(fileName, append), charset.name()); + } + + /// Opens `file`, emptying it, writing `charset`. + public FileWriter(File file, Charset charset) throws IOException { + super(new FileOutputStream(file), charset.name()); + } + + /// Opens `file`, at its end when `append` is true, writing `charset`. + public FileWriter(File file, Charset charset, boolean append) throws IOException { + super(new FileOutputStream(file, append), charset.name()); + } +} diff --git a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FilenameFilter.java b/Ports/CLDC11/src/java/io/FilenameFilter.java similarity index 89% rename from maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FilenameFilter.java rename to Ports/CLDC11/src/java/io/FilenameFilter.java index 2a9deb7efa7..6c2a83faae9 100644 --- a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FilenameFilter.java +++ b/Ports/CLDC11/src/java/io/FilenameFilter.java @@ -20,9 +20,11 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.androidcompat.jdk; +package java.io; -/// `java.io.FilenameFilter` for the Codename One runtime. +/// Decides which names `File.list(FilenameFilter)` answers. public interface FilenameFilter { + + /// Whether the entry `name` of the directory `dir` is wanted. boolean accept(File dir, String name); } diff --git a/Ports/CLDC11/src/java/io/InputStream.java b/Ports/CLDC11/src/java/io/InputStream.java index 998892d6f87..4c20a9e8eef 100644 --- a/Ports/CLDC11/src/java/io/InputStream.java +++ b/Ports/CLDC11/src/java/io/InputStream.java @@ -25,7 +25,7 @@ /// This abstract class is the superclass of all classes representing an input stream of bytes. /// Applications that need to define a subclass of InputStream must always provide a method that returns the next byte of input. /// Since: JDK1.0, CLDC 1.0 See Also:ByteArrayInputStream, DataInputStream, read(), OutputStream -public abstract class InputStream implements AutoCloseable { +public abstract class InputStream implements Closeable { public InputStream(){ //TODO codavaj!! } @@ -96,4 +96,85 @@ public long skip(long n) throws java.io.IOException{ return 0l; //TODO codavaj!! } + /// Reads everything left in this stream, until its end. The stream is + /// not closed. + /// + /// #### Returns + /// + /// the bytes read, an empty array when the stream was at its end + public byte[] readAllBytes() throws IOException { + return readNBytes(Integer.MAX_VALUE); + } + + /// Reads up to `len` bytes, fewer only when the stream ends first. + /// + /// #### Parameters + /// + /// - `len`: the most bytes to read + /// + /// #### Returns + /// + /// the bytes read + /// + /// #### Throws + /// + /// - `IllegalArgumentException`: when `len` is negative + public byte[] readNBytes(int len) throws IOException { + if (len < 0) { + throw new IllegalArgumentException("len < 0"); + } + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] chunk = new byte[8192]; + int left = len; + while (left > 0) { + int n = read(chunk, 0, left < chunk.length ? left : chunk.length); + if (n < 0) { + break; + } + out.write(chunk, 0, n); + left -= n; + } + return out.toByteArray(); + } + + /// Reads into `b` from `off` until `len` bytes arrived or the stream + /// ended, where a single `read` may stop short. + /// + /// #### Returns + /// + /// how many bytes were read, 0 at the end of the stream + public int readNBytes(byte[] b, int off, int len) throws IOException { + if (off < 0 || len < 0 || len > b.length - off) { + throw new IndexOutOfBoundsException(); + } + int total = 0; + while (total < len) { + int n = read(b, off + total, len - total); + if (n < 0) { + break; + } + total += n; + } + return total; + } + + /// Copies everything left in this stream to `out`, closing neither. + /// + /// #### Returns + /// + /// how many bytes were copied + public long transferTo(OutputStream out) throws IOException { + if (out == null) { + throw new NullPointerException("out"); + } + byte[] chunk = new byte[8192]; + long total = 0; + int n = read(chunk, 0, chunk.length); + while (n >= 0) { + out.write(chunk, 0, n); + total += n; + n = read(chunk, 0, chunk.length); + } + return total; + } } diff --git a/Ports/CLDC11/src/java/io/InputStreamReader.java b/Ports/CLDC11/src/java/io/InputStreamReader.java index 9769621f7bf..85ae2bce44c 100644 --- a/Ports/CLDC11/src/java/io/InputStreamReader.java +++ b/Ports/CLDC11/src/java/io/InputStreamReader.java @@ -79,4 +79,12 @@ public long skip(long n) throws java.io.IOException{ return 0l; //TODO codavaj!! } + /// An InputStreamReader that decodes with `cs`. + public InputStreamReader(java.io.InputStream is, java.nio.charset.Charset cs) { + } + + /// The historical name of the encoding: `UTF8`, `ASCII`, `ISO8859_1`. + public String getEncoding() { + return null; + } } diff --git a/Ports/CLDC11/src/java/io/ObjectInput.java b/Ports/CLDC11/src/java/io/ObjectInput.java new file mode 100644 index 00000000000..56644b2bc7f --- /dev/null +++ b/Ports/CLDC11/src/java/io/ObjectInput.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.io; + +/// Reads objects as well as primitive values. Codename One has no object +/// serialization of its own; this type exists so code written against it +/// compiles, and an implementation supplies the reading. +public interface ObjectInput extends DataInput, AutoCloseable { + + /// Reads the next object. + Object readObject() throws ClassNotFoundException, IOException; + + /// Reads one byte, or -1 at the end of the input. + int read() throws IOException; + + /// Reads into `b`, returning how many bytes were read or -1 at the end. + int read(byte[] b) throws IOException; + + /// Reads into `b` from `off`, returning how many bytes were read or -1 + /// at the end. + int read(byte[] b, int off, int len) throws IOException; + + /// Skips up to `n` bytes, returning how many were skipped. + long skip(long n) throws IOException; + + /// How many bytes can be read without blocking. + int available() throws IOException; + + /// Closes the input. + @Override + void close() throws IOException; +} diff --git a/Ports/CLDC11/src/java/io/ObjectOutput.java b/Ports/CLDC11/src/java/io/ObjectOutput.java new file mode 100644 index 00000000000..252854e3fe6 --- /dev/null +++ b/Ports/CLDC11/src/java/io/ObjectOutput.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.io; + +/// Writes objects as well as primitive values. Codename One has no object +/// serialization of its own; this type exists so code written against it +/// compiles, and an implementation supplies the writing. +public interface ObjectOutput extends DataOutput, AutoCloseable { + + /// Writes `obj`. + void writeObject(Object obj) throws IOException; + + /// Writes any buffered output. + void flush() throws IOException; + + /// Closes the output. + @Override + void close() throws IOException; +} diff --git a/Ports/CLDC11/src/java/io/OutputStream.java b/Ports/CLDC11/src/java/io/OutputStream.java index 68c5a269213..4f32b74a651 100644 --- a/Ports/CLDC11/src/java/io/OutputStream.java +++ b/Ports/CLDC11/src/java/io/OutputStream.java @@ -25,7 +25,7 @@ /// This abstract class is the superclass of all classes representing an output stream of bytes. An output stream accepts output bytes and sends them to some sink. /// Applications that need to define a subclass of OutputStream must always provide at least a method that writes one byte of output. /// Since: JDK1.0, CLDC 1.0 See Also:ByteArrayOutputStream, DataOutputStream, InputStream, write(int) -public abstract class OutputStream implements AutoCloseable { +public abstract class OutputStream implements Closeable, Flushable { public OutputStream(){ //TODO codavaj!! } diff --git a/Ports/CLDC11/src/java/io/OutputStreamWriter.java b/Ports/CLDC11/src/java/io/OutputStreamWriter.java index 4e666f37c08..8815d30f195 100644 --- a/Ports/CLDC11/src/java/io/OutputStreamWriter.java +++ b/Ports/CLDC11/src/java/io/OutputStreamWriter.java @@ -64,4 +64,12 @@ public void write(java.lang.String str, int off, int len) throws java.io.IOExcep return; //TODO codavaj!! } + /// An OutputStreamWriter that encodes with `cs`. + public OutputStreamWriter(java.io.OutputStream os, java.nio.charset.Charset cs) { + } + + /// The historical name of the encoding: `UTF8`, `ASCII`, `ISO8859_1`. + public String getEncoding() { + return null; + } } diff --git a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/PrintWriter.java b/Ports/CLDC11/src/java/io/PrintWriter.java similarity index 68% rename from maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/PrintWriter.java rename to Ports/CLDC11/src/java/io/PrintWriter.java index 3d7a4ab82e7..189a18ea044 100644 --- a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/PrintWriter.java +++ b/Ports/CLDC11/src/java/io/PrintWriter.java @@ -20,16 +20,9 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.androidcompat.jdk; +package java.io; -import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.UnsupportedEncodingException; -import java.io.Writer; - -/// `java.io.PrintWriter` for the Codename One runtime. Like the JDK class it -/// never throws: an I/O error is recorded and reported by `checkError`. +/// Prints formatted text to a writer or a stream. It never throws: an I/O error is recorded and reported by `checkError`. public class PrintWriter extends Writer { protected Writer out; @@ -53,12 +46,44 @@ public PrintWriter(OutputStream out, boolean autoFlush) { this(utf8(out), autoFlush); } - public PrintWriter(String fileName) throws java.io.FileNotFoundException { - this(utf8(new FileOutputStream(fileName)), false); + /// Writes to the file `fileName` in UTF-8, creating it or truncating it. + public PrintWriter(String fileName) throws FileNotFoundException { + this(new BufferedWriter(utf8(new FileOutputStream(fileName))), false); + } + + /// Writes to the file `fileName` in the charset `csn`. + public PrintWriter(String fileName, String csn) throws FileNotFoundException, UnsupportedEncodingException { + this(encoded(new File(fileName), csn), false); + } + + /// Writes to the file `fileName` in the charset `cs`. + public PrintWriter(String fileName, java.nio.charset.Charset cs) throws IOException { + this(encoded(new File(fileName), cs.name()), false); + } + + /// Writes to `file` in UTF-8, creating it or truncating it. + public PrintWriter(File file) throws FileNotFoundException { + this(new BufferedWriter(utf8(new FileOutputStream(file))), false); + } + + /// Writes to `file` in the charset `csn`. + public PrintWriter(File file, String csn) throws FileNotFoundException, UnsupportedEncodingException { + this(encoded(file, csn), false); } - public PrintWriter(File file) throws java.io.FileNotFoundException { - this(utf8(new FileOutputStream(file)), false); + /// Writes to `file` in the charset `cs`. + public PrintWriter(File file, java.nio.charset.Charset cs) throws IOException { + this(encoded(file, cs.name()), false); + } + + /// Checks the charset before the file is opened, so an unknown name + /// leaves the file as it was, as the JDK does. + private static Writer encoded(File file, String csn) throws FileNotFoundException, UnsupportedEncodingException { + if (csn == null) { + throw new NullPointerException("charsetName"); + } + "".getBytes(csn); + return new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), csn)); } private static Writer utf8(OutputStream out) { @@ -261,4 +286,32 @@ public PrintWriter append(char c) { print(c); return this; } + + /// Writes `format` filled in with `args`, as `String.format` does. + /// + /// #### Returns + /// + /// this writer + public PrintWriter printf(String format, Object... args) { + return format(format, args); + } + + /// Writes `format` filled in with `args`, as `String.format` does. + /// + /// #### Returns + /// + /// this writer + public PrintWriter format(String format, Object... args) { + write(String.format(format, args)); + if (autoFlush) { + flush(); + } + return this; + } + + public PrintWriter append(CharSequence csq, int start, int end) { + CharSequence cs = csq == null ? "null" : csq; + print(cs.subSequence(start, end).toString()); + return this; + } } diff --git a/Ports/CLDC11/src/java/io/Reader.java b/Ports/CLDC11/src/java/io/Reader.java index 094f8088aba..7f61a332c68 100644 --- a/Ports/CLDC11/src/java/io/Reader.java +++ b/Ports/CLDC11/src/java/io/Reader.java @@ -24,7 +24,7 @@ package java.io; /// Abstract class for reading character streams. The only methods that a subclass must implement are read(char[], int, int) and close(). Most subclasses, however, will override some of the methods defined here in order to provide higher efficiency, additional functionality, or both. /// Since: JDK1.1, CLDC 1.0 See Also:InputStreamReader, Writer -public abstract class Reader implements AutoCloseable { +public abstract class Reader implements Closeable { /// The object used to synchronize operations on this stream. For efficiency, a character-stream object may use an object other than itself to protect critical sections. A subclass should therefore use the object in this field rather than this or a synchronized method. protected java.lang.Object lock; @@ -81,4 +81,23 @@ public long skip(long n) throws java.io.IOException{ return 0l; //TODO codavaj!! } + /// Copies everything left in this reader to `out`, closing neither. + /// + /// #### Returns + /// + /// how many characters were copied + public long transferTo(Writer out) throws IOException { + if (out == null) { + throw new NullPointerException("out"); + } + char[] chunk = new char[8192]; + long total = 0; + int n = read(chunk, 0, chunk.length); + while (n >= 0) { + out.write(chunk, 0, n); + total += n; + n = read(chunk, 0, chunk.length); + } + return total; + } } diff --git a/Ports/CLDC11/src/java/io/UncheckedIOException.java b/Ports/CLDC11/src/java/io/UncheckedIOException.java new file mode 100644 index 00000000000..71864bc40b2 --- /dev/null +++ b/Ports/CLDC11/src/java/io/UncheckedIOException.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.io; + +/// `java.io.UncheckedIOException`: an +/// `IOException` carried through code that cannot declare it, as a lambda or +/// a constructor that loads a resource does. +public class UncheckedIOException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + /// Wraps an exception with a message of its own. + public UncheckedIOException(String message, IOException cause) { + super(message, required(cause)); + } + + /// Wraps an exception, taking its description as the message. + public UncheckedIOException(IOException cause) { + super(required(cause).toString(), cause); + } + + private static IOException required(IOException cause) { + if (cause == null) { + throw new NullPointerException(); + } + return cause; + } + + /// Returns the exception this one carries. + @Override + public IOException getCause() { + Throwable cause = super.getCause(); + return cause instanceof IOException ? (IOException) cause : null; + } +} diff --git a/Ports/CLDC11/src/java/io/Writer.java b/Ports/CLDC11/src/java/io/Writer.java index 99b31b10fbb..43063a2b5cc 100644 --- a/Ports/CLDC11/src/java/io/Writer.java +++ b/Ports/CLDC11/src/java/io/Writer.java @@ -24,7 +24,7 @@ package java.io; /// Abstract class for writing to character streams. The only methods that a subclass must implement are write(char[], int, int), flush(), and close(). Most subclasses, however, will override some of the methods defined here in order to provide higher efficiency, additional functionality, or both. /// Since: JDK1.1, CLDC 1.0 See Also:OutputStreamWriter, Reader -public abstract class Writer implements AutoCloseable { +public abstract class Writer implements Appendable, Closeable, Flushable { /// The object used to synchronize operations on this stream. For efficiency, a character-stream object may use an object other than itself to protect critical sections. A subclass should therefore use the object in this field rather than this or a synchronized method. protected java.lang.Object lock; @@ -39,6 +39,21 @@ protected Writer(java.lang.Object lock){ //TODO codavaj!! } + /// Writes `csq`, or "null" when it is null. + public Writer append(CharSequence csq) throws IOException { + return this; + } + + /// Writes `c`. + public Writer append(char c) throws IOException { + return this; + } + + /// Writes the characters of `csq` from `start` up to `end`. + public Writer append(CharSequence csq, int start, int end) throws IOException { + return this; + } + /// Close the stream, flushing it first. Once a stream has been closed, further write() or flush() invocations will cause an IOException to be thrown. Closing a previously-closed stream, however, has no effect. public abstract void close() throws java.io.IOException; diff --git a/Ports/CLDC11/src/java/lang/Boolean.java b/Ports/CLDC11/src/java/lang/Boolean.java index b06e8237fc0..55523ffa128 100644 --- a/Ports/CLDC11/src/java/lang/Boolean.java +++ b/Ports/CLDC11/src/java/lang/Boolean.java @@ -24,7 +24,7 @@ package java.lang; /// The Boolean class wraps a value of the primitive type boolean in an object. An object of type Boolean contains a single field whose type is boolean. /// Since: JDK1.0, CLDC 1.0 -public final class Boolean{ +public final class Boolean implements Comparable { /// The Boolean object corresponding to the primitive value false. public static final java.lang.Boolean FALSE=null; @@ -81,4 +81,46 @@ public static boolean parseBoolean(final String s) { public int compareTo(final Boolean b2) { return 0; } + + public static final Class TYPE = null; + + /// A Boolean of `parseBoolean(s)`. + @Deprecated + public Boolean(String s) { + } + + /// `"true"` or `"false"`. + public static String toString(boolean b) { + return null; + } + + /// 1231 for true, 1237 for false, as the JDK hashes them. + public static int hashCode(boolean value) { + return 0; + } + + /// 0 when equal, otherwise positive when `x` is true. + public static int compare(boolean x, boolean y) { + return 0; + } + + /// `a && b`. + public static boolean logicalAnd(boolean a, boolean b) { + return false; + } + + /// `a || b`. + public static boolean logicalOr(boolean a, boolean b) { + return false; + } + + /// `a ^ b`. + public static boolean logicalXor(boolean a, boolean b) { + return false; + } + + /// Whether the system property `name` is `"true"`, ignoring case. + public static boolean getBoolean(String name) { + return false; + } } diff --git a/Ports/CLDC11/src/java/lang/Byte.java b/Ports/CLDC11/src/java/lang/Byte.java index 76bac16f985..597ae4a0489 100644 --- a/Ports/CLDC11/src/java/lang/Byte.java +++ b/Ports/CLDC11/src/java/lang/Byte.java @@ -105,11 +105,59 @@ public double doubleValue() { return 0.0; } - public int compare(byte b1, byte b2) { + public static int compare(byte b1, byte b2) { return b1 < b2 ? -1 : b1 > b2 ? 1 : 0; } public int compareTo(Byte b) { return compare(this, b); } + + /// The size of a byte in bytes. + public static final int BYTES = 1; + + /// A Byte of `parseByte(s)`. + @Deprecated + public Byte(String s) throws NumberFormatException { + } + + /// The decimal string of `b`. + public static String toString(byte b) { + return null; + } + + /// A Byte of `parseByte(s)`. + public static Byte valueOf(String s) throws NumberFormatException { + return null; + } + + /// A Byte of `parseByte(s, radix)`. + public static Byte valueOf(String s, int radix) throws NumberFormatException { + return null; + } + + /// Decodes a decimal, hexadecimal or octal byte, as `Integer.decode`. + public static Byte decode(String nm) throws NumberFormatException { + return null; + } + + /// The value itself. + public static int hashCode(byte value) { + return 0; + } + + /// Compares the two as unsigned values. + public static int compareUnsigned(byte x, byte y) { + return 0; + } + + /// The value as an unsigned int. + public static int toUnsignedInt(byte x) { + return 0; + } + + /// The value as an unsigned long. + public static long toUnsignedLong(byte x) { + return 0; + } } diff --git a/Ports/CLDC11/src/java/lang/CharSequence.java b/Ports/CLDC11/src/java/lang/CharSequence.java index a84f1d9c02c..e7aef96a2b8 100644 --- a/Ports/CLDC11/src/java/lang/CharSequence.java +++ b/Ports/CLDC11/src/java/lang/CharSequence.java @@ -81,4 +81,26 @@ public interface CharSequence { /// /// a string based on this sequence. public String toString(); + + /// Whether the sequence has no characters. + default boolean isEmpty() { + return false; + } + + /// The sequence's `char` values, zero-extended to `int`. Stubbed in + /// the CLDC11 subset; the implementation comes from the platform. + default java.util.stream.IntStream chars() { + return null; + } + + /// The sequence's code points. Stubbed in the CLDC11 subset. + default java.util.stream.IntStream codePoints() { + return null; + } + + /// Compares two sequences lexicographically, char by char, the shorter + /// first when one is a prefix of the other. + public static int compare(CharSequence cs1, CharSequence cs2) { + return 0; + } } diff --git a/Ports/CLDC11/src/java/lang/Character.java b/Ports/CLDC11/src/java/lang/Character.java index 00335b8a6db..8b118932737 100644 --- a/Ports/CLDC11/src/java/lang/Character.java +++ b/Ports/CLDC11/src/java/lang/Character.java @@ -26,7 +26,7 @@ /// In addition, this class provides several methods for determining the type of a character and converting characters from uppercase to lowercase and vice versa. /// Character information is based on the Unicode Standard, version 3.0. However, in order to reduce footprint, by default the character property and case conversion operations in CLDC are available only for the ISO Latin-1 range of characters. Other Unicode character blocks can be supported as necessary. /// Since: JDK1.0, CLDC 1.0 -public final class Character{ +public final class Character implements Comparable { /// The maximum radix available for conversion to and from Strings. /// See Also:Integer.toString(int, int), Integer.valueOf(java.lang.String), Constant Field Values public static final int MAX_RADIX=36; @@ -1167,4 +1167,325 @@ public static char toTitleCase(char c) { return (char) 0; } + public static final Class TYPE = char.class; + + public static final byte UNASSIGNED = 0; + + public static final byte UPPERCASE_LETTER = 1; + + public static final byte LOWERCASE_LETTER = 2; + + public static final byte TITLECASE_LETTER = 3; + + public static final byte MODIFIER_LETTER = 4; + + public static final byte OTHER_LETTER = 5; + + public static final byte NON_SPACING_MARK = 6; + + public static final byte ENCLOSING_MARK = 7; + + public static final byte COMBINING_SPACING_MARK = 8; + + public static final byte DECIMAL_DIGIT_NUMBER = 9; + + public static final byte LETTER_NUMBER = 10; + + public static final byte OTHER_NUMBER = 11; + + public static final byte SPACE_SEPARATOR = 12; + + public static final byte LINE_SEPARATOR = 13; + + public static final byte PARAGRAPH_SEPARATOR = 14; + + public static final byte CONTROL = 15; + + public static final byte FORMAT = 16; + + public static final byte PRIVATE_USE = 17; + + public static final byte SURROGATE = 19; + + public static final byte DASH_PUNCTUATION = 20; + + public static final byte START_PUNCTUATION = 21; + + public static final byte END_PUNCTUATION = 22; + + public static final byte CONNECTOR_PUNCTUATION = 23; + + public static final byte OTHER_PUNCTUATION = 24; + + public static final byte MATH_SYMBOL = 25; + + public static final byte CURRENCY_SYMBOL = 26; + + public static final byte MODIFIER_SYMBOL = 27; + + public static final byte OTHER_SYMBOL = 28; + + public static final byte INITIAL_QUOTE_PUNCTUATION = 29; + + public static final byte FINAL_QUOTE_PUNCTUATION = 30; + + public static final byte DIRECTIONALITY_UNDEFINED = -1; + + public static final byte DIRECTIONALITY_LEFT_TO_RIGHT = 0; + + public static final byte DIRECTIONALITY_RIGHT_TO_LEFT = 1; + + public static final byte DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC = 2; + + public static final byte DIRECTIONALITY_EUROPEAN_NUMBER = 3; + + public static final byte DIRECTIONALITY_EUROPEAN_NUMBER_SEPARATOR = 4; + + public static final byte DIRECTIONALITY_EUROPEAN_NUMBER_TERMINATOR = 5; + + public static final byte DIRECTIONALITY_ARABIC_NUMBER = 6; + + public static final byte DIRECTIONALITY_COMMON_NUMBER_SEPARATOR = 7; + + public static final byte DIRECTIONALITY_NONSPACING_MARK = 8; + + public static final byte DIRECTIONALITY_BOUNDARY_NEUTRAL = 9; + + public static final byte DIRECTIONALITY_PARAGRAPH_SEPARATOR = 10; + + public static final byte DIRECTIONALITY_SEGMENT_SEPARATOR = 11; + + public static final byte DIRECTIONALITY_WHITESPACE = 12; + + public static final byte DIRECTIONALITY_OTHER_NEUTRALS = 13; + + public static final byte DIRECTIONALITY_LEFT_TO_RIGHT_EMBEDDING = 14; + + public static final byte DIRECTIONALITY_LEFT_TO_RIGHT_OVERRIDE = 15; + + public static final byte DIRECTIONALITY_RIGHT_TO_LEFT_EMBEDDING = 16; + + public static final byte DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE = 17; + + public static final byte DIRECTIONALITY_POP_DIRECTIONAL_FORMAT = 18; + + public static int digit(int codePoint, int radix) { + return 0; + } + + public static boolean isLetterOrDigit(char ch) { + return false; + } + + public static boolean isLetterOrDigit(int codePoint) { + return false; + } + + public static boolean isLetter(char ch) { + return false; + } + + public static boolean isLetter(int codePoint) { + return false; + } + + public static boolean isIdentifierIgnorable(char ch) { + return false; + } + + public static boolean isIdentifierIgnorable(int codePoint) { + return false; + } + + public static boolean isDigit(int codePoint) { + return false; + } + + public static boolean isLowerCase(int codePoint) { + return false; + } + + public static boolean isUpperCase(int codePoint) { + return false; + } + + public static int toLowerCase(int codePoint) { + return 0; + } + + public static int toUpperCase(int codePoint) { + return 0; + } + + public static char highSurrogate(int codePoint) { + return 0; + } + + public static char lowSurrogate(int codePoint) { + return 0; + } + + /// Whether the code point is a Unicode space separator, line separator or + /// paragraph separator. + public static boolean isSpaceChar(int codePoint) { + return false; + } + + public static byte getDirectionality(char ch) { + return 0; + } + + public static int getType(char c) { + return 0; + } + + public static boolean isBmpCodePoint(int codePoint) { + return false; + } + + public static boolean isSurrogate(char ch) { + return false; + } + + public static int getType(int codePoint) { + return 0; + } + + public int compareTo(Character another) { + return 0; + } + + /// The size of a char in bytes. + public static final int BYTES = 2; + + /// A one-char string of `c`. + public static String toString(char c) { + return null; + } + + /// The string of the code point: one char, or a surrogate pair. + /// + /// #### Throws + /// + /// - `IllegalArgumentException`: if it is not a valid code point + public static String toString(int codePoint) { + return null; + } + + /// The char itself, as the JDK hashes it. + public static int hashCode(char value) { + return 0; + } + + /// `x - y`. + public static int compare(char x, char y) { + return 0; + } + + /// Whether the char is an ISO control character: U+0000 to U+001F or + /// U+007F to U+009F. + public static boolean isISOControl(char ch) { + return false; + } + + /// Whether the code point is an ISO control character. + public static boolean isISOControl(int codePoint) { + return false; + } + + /// The char of `digit` in `radix`, a lower case letter from 10 on, or + /// U+0000 when either is out of range. + public static char forDigit(int digit, int radix) { + return 0; + } + + /// The value of a digit, or of a Latin letter read as a digit of base + /// 36, or -1. + public static int getNumericValue(char ch) { + return 0; + } + + /// The value of a digit, or of a Latin letter read as a digit of base + /// 36, or -1. + public static int getNumericValue(int codePoint) { + return 0; + } + + /// Whether the code point is alphabetic. This runtime answers as for + /// `isLetter`. + public static boolean isAlphabetic(int codePoint) { + return false; + } + + /// Whether the char has a category in this runtime's classification. + public static boolean isDefined(char ch) { + return false; + } + + /// Whether the code point has a category in this runtime's + /// classification. Above Latin-1 that classification is partial, so an + /// assigned symbol it does not know answers false. + public static boolean isDefined(int codePoint) { + return false; + } + + /// Whether the code point is a CJK ideograph. + public static boolean isIdeographic(int codePoint) { + return false; + } + + /// Whether the char is a title case letter. + public static boolean isTitleCase(char ch) { + return false; + } + + /// Whether the code point is a title case letter. + public static boolean isTitleCase(int codePoint) { + return false; + } + + /// Whether the char is mirrored in right-to-left text: the ASCII + /// brackets and the guillemets. + public static boolean isMirrored(char ch) { + return false; + } + + /// Whether the code point is mirrored in right-to-left text. This + /// runtime knows the ASCII brackets and the guillemets. + public static boolean isMirrored(int codePoint) { + return false; + } + + /// As `isJavaIdentifierStart(char)`. + @Deprecated + public static boolean isJavaLetter(char ch) { + return false; + } + + /// As `isJavaIdentifierPart(char)`. + @Deprecated + public static boolean isJavaLetterOrDigit(char ch) { + return false; + } + + /// Whether the char may start a Unicode identifier. + public static boolean isUnicodeIdentifierStart(char ch) { + return false; + } + + /// Whether the code point may start a Unicode identifier: a letter. + public static boolean isUnicodeIdentifierStart(int codePoint) { + return false; + } + + /// Whether the char may continue a Unicode identifier. + public static boolean isUnicodeIdentifierPart(char ch) { + return false; + } + + /// Whether the code point may continue a Unicode identifier: a letter, a + /// digit, connecting punctuation or an ignorable character. + public static boolean isUnicodeIdentifierPart(int codePoint) { + return false; + } } diff --git a/Ports/CLDC11/src/java/lang/Class.java b/Ports/CLDC11/src/java/lang/Class.java index ae8200a31c2..8c9ac59a227 100644 --- a/Ports/CLDC11/src/java/lang/Class.java +++ b/Ports/CLDC11/src/java/lang/Class.java @@ -260,4 +260,10 @@ public boolean isLocalClass() { public boolean isRecord() { return this != java.lang.Record.class && java.lang.Record.class.isAssignableFrom(this); } + + /// The module this class is in: the unnamed module, for every class, as + /// for a class the JDK loaded from the class path. + public Module getModule() { + return Module.unnamed(); + } } diff --git a/Ports/CLDC11/src/java/lang/Double.java b/Ports/CLDC11/src/java/lang/Double.java index cf095463712..e454cf167da 100644 --- a/Ports/CLDC11/src/java/lang/Double.java +++ b/Ports/CLDC11/src/java/lang/Double.java @@ -206,6 +206,56 @@ public static int compare(double d1, double d2) { public int compareTo(Double d) { return compare(this, d); } - - + + public static final int MAX_EXPONENT = 1023; + + public static final int MIN_EXPONENT = -1022; + + public static final int SIZE = 64; + + public static long doubleToRawLongBits(double value) { + return 0; + } + + public static double sum(double a, double b) { + return 0; + } + + public static double max(double a, double b) { + return 0; + } + + public static double min(double a, double b) { + return 0; + } + + /// The size of a double in bytes. + public static final int BYTES = 8; + + /// The bits of precision of a double's significand. + public static final int PRECISION = 53; + + /// The smallest positive normal double, 2 to the power -1022. + public static final double MIN_NORMAL = 0x1.0p-1022; + + /// A Double of `parseDouble(s)`. + @Deprecated + public Double(String s) throws NumberFormatException { + } + + /// Whether `d` is neither infinite nor NaN. + public static boolean isFinite(double d) { + return false; + } + + /// The hash the JDK gives `value`: its bits folded to an int. + public static int hashCode(double value) { + return 0; + } + + /// The value as a hexadecimal floating point literal, as the JDK writes + /// it: `0x1.8p1` for 3.0, `0x0.0p0` for zero. + public static String toHexString(double d) { + return null; + } } diff --git a/Ports/CLDC11/src/java/lang/Float.java b/Ports/CLDC11/src/java/lang/Float.java index 723dcbf780e..5766d8efe86 100644 --- a/Ports/CLDC11/src/java/lang/Float.java +++ b/Ports/CLDC11/src/java/lang/Float.java @@ -192,4 +192,61 @@ public static int compare(float f1, float f2) { public int compareTo(Float f) { return compare(this, f); } + + public static final Class TYPE = null; + + public static int floatToRawIntBits(float value) { + return 0; + } + + public static float sum(float a, float b) { + return 0; + } + + public static float max(float a, float b) { + return 0; + } + + public static float min(float a, float b) { + return 0; + } + + /// The size of a float in bytes. + public static final int BYTES = 4; + + /// The size of a float in bits. + public static final int SIZE = 32; + + /// The bits of precision of a float's significand. + public static final int PRECISION = 24; + + /// The largest exponent of a finite float. + public static final int MAX_EXPONENT = 127; + + /// The smallest exponent of a normal float. + public static final int MIN_EXPONENT = -126; + + /// The smallest positive normal float, 2 to the power -126. + public static final float MIN_NORMAL = 0x1.0p-126f; + + /// A Float of `parseFloat(s)`. + @Deprecated + public Float(String s) throws NumberFormatException { + } + + /// Whether `f` is neither infinite nor NaN. + public static boolean isFinite(float f) { + return false; + } + + /// The hash the JDK gives `value`: its bits. + public static int hashCode(float value) { + return 0; + } + + /// The value as a hexadecimal floating point literal, as the JDK writes + /// it. + public static String toHexString(float f) { + return null; + } } diff --git a/Ports/CLDC11/src/java/lang/Integer.java b/Ports/CLDC11/src/java/lang/Integer.java index 9cda08451cf..27d9bd25d5d 100644 --- a/Ports/CLDC11/src/java/lang/Integer.java +++ b/Ports/CLDC11/src/java/lang/Integer.java @@ -235,4 +235,155 @@ public static int numberOfLeadingZeros(int i) { } return 32 - n - 1; } + + public static int rotateLeft(int i, int distance) { + return 0; + } + + public static int sum(int a, int b) { + return 0; + } + + public static int max(int a, int b) { + return 0; + } + + public static int min(int a, int b) { + return 0; + } + + /// The size of an int in bytes. + public static final int BYTES = 4; + + /// The size of an int in bits. + public static final int SIZE = 32; + + /// An Integer of `parseInt(s)`. + @Deprecated + public Integer(String s) throws NumberFormatException { + } + + /// The value itself. + public static int hashCode(int value) { + return 0; + } + + /// The number of one bits. + public static int bitCount(int i) { + return 0; + } + + /// The highest one bit alone, or 0. + public static int highestOneBit(int i) { + return 0; + } + + /// The lowest one bit alone, or 0. + public static int lowestOneBit(int i) { + return 0; + } + + /// The number of zero bits below the lowest one bit; 32 for zero. + public static int numberOfTrailingZeros(int i) { + return 0; + } + + /// The bits in reverse order. + public static int reverse(int i) { + return 0; + } + + /// The bytes in reverse order. + public static int reverseBytes(int i) { + return 0; + } + + /// The bits rotated right by `distance`. + public static int rotateRight(int i, int distance) { + return 0; + } + + /// Compares the two as unsigned values. + public static int compareUnsigned(int x, int y) { + return 0; + } + + /// The value as an unsigned long. + public static long toUnsignedLong(int x) { + return 0; + } + + /// The unsigned quotient. + public static int divideUnsigned(int dividend, int divisor) { + return 0; + } + + /// The unsigned remainder. + public static int remainderUnsigned(int dividend, int divisor) { + return 0; + } + + /// The value as an unsigned decimal string. + public static String toUnsignedString(int i) { + return null; + } + + /// The value as an unsigned string in `radix`. + public static String toUnsignedString(int i, int radix) { + return null; + } + + /// Parses an unsigned decimal int. + public static int parseUnsignedInt(String s) throws NumberFormatException { + return 0; + } + + /// Parses an unsigned int in `radix`. + public static int parseUnsignedInt(String s, int radix) throws NumberFormatException { + return 0; + } + + /// Parses `s` from `beginIndex` to `endIndex` as a signed int. + public static int parseInt(CharSequence s, int beginIndex, int endIndex, int radix) + throws NumberFormatException { + return 0; + } + + /// Parses `s` from `beginIndex` to `endIndex` as an unsigned int. + public static int parseUnsignedInt(CharSequence s, int beginIndex, int endIndex, int radix) + throws NumberFormatException { + return 0; + } + + /// Decodes a decimal, `0x`, `0X`, `#` or leading-zero octal int, with an + /// optional sign. + public static Integer decode(String nm) throws NumberFormatException { + return null; + } + + /// The system property `nm` as an Integer, or null. + public static Integer getInteger(String nm) { + return null; + } + + /// The system property `nm` as an Integer, or `val`. + public static Integer getInteger(String nm, int val) { + return null; + } + + /// The system property `nm` decoded as an Integer, or `val` when it is + /// unset or not a number. + public static Integer getInteger(String nm, Integer val) { + return null; + } + + /// The bits of `i` selected by `mask`, packed to the low end. + public static int compress(int i, int mask) { + return 0; + } + + /// The low bits of `i` spread to the positions of the one bits of `mask`. + public static int expand(int i, int mask) { + return 0; + } } diff --git a/Ports/CLDC11/src/java/lang/Iterable.java b/Ports/CLDC11/src/java/lang/Iterable.java index 4c19f8cea0f..7b47dda2fe0 100644 --- a/Ports/CLDC11/src/java/lang/Iterable.java +++ b/Ports/CLDC11/src/java/lang/Iterable.java @@ -39,4 +39,9 @@ public interface Iterable { /// from the platform's JDK at runtime. default void forEach(Consumer action) { } + + /// The elements, as a [java.util.Spliterator] whose size is not known. + default java.util.Spliterator spliterator() { + return java.util.Spliterators.spliteratorUnknownSize(iterator(), 0); + } } diff --git a/Ports/CLDC11/src/java/lang/Long.java b/Ports/CLDC11/src/java/lang/Long.java index 442dd006c9c..5d00927a026 100644 --- a/Ports/CLDC11/src/java/lang/Long.java +++ b/Ports/CLDC11/src/java/lang/Long.java @@ -139,4 +139,185 @@ public static int compare(long l1, long l2) { public int compareTo(Long l) { return compare(this, l); } + + public static int hashCode(long value) { + return 0; + } + + public static long sum(long a, long b) { + return 0; + } + + public static long max(long a, long b) { + return 0; + } + + public static long min(long a, long b) { + return 0; + } + + /// The size of a long in bytes. + public static final int BYTES = 8; + + /// The size of a long in bits. + public static final int SIZE = 64; + + /// A Long of `parseLong(s)`. + @Deprecated + public Long(String s) throws NumberFormatException { + } + + /// A Long of `parseLong(s)`. + public static Long valueOf(String s) throws NumberFormatException { + return null; + } + + /// A Long of `parseLong(s, radix)`. + public static Long valueOf(String s, int radix) throws NumberFormatException { + return null; + } + + /// The number of one bits. + public static int bitCount(long i) { + return 0; + } + + /// The highest one bit alone, or 0. + public static long highestOneBit(long i) { + return 0; + } + + /// The lowest one bit alone, or 0. + public static long lowestOneBit(long i) { + return 0; + } + + /// The number of zero bits above the highest one bit; 64 for zero. + public static int numberOfLeadingZeros(long i) { + return 0; + } + + /// The number of zero bits below the lowest one bit; 64 for zero. + public static int numberOfTrailingZeros(long i) { + return 0; + } + + /// The bits in reverse order. + public static long reverse(long i) { + return 0; + } + + /// The bytes in reverse order. + public static long reverseBytes(long i) { + return 0; + } + + /// The bits rotated left by `distance`. + public static long rotateLeft(long i, int distance) { + return 0; + } + + /// The bits rotated right by `distance`. + public static long rotateRight(long i, int distance) { + return 0; + } + + /// -1, 0 or 1 by the sign of `i`. + public static int signum(long i) { + return 0; + } + + /// The value as unsigned hexadecimal digits. + public static String toHexString(long i) { + return null; + } + + /// The value as unsigned octal digits. + public static String toOctalString(long i) { + return null; + } + + /// The value as unsigned binary digits. + public static String toBinaryString(long i) { + return null; + } + + /// Compares the two as unsigned values. + public static int compareUnsigned(long x, long y) { + return 0; + } + + /// The unsigned quotient. + public static long divideUnsigned(long dividend, long divisor) { + return 0; + } + + /// The unsigned remainder. + public static long remainderUnsigned(long dividend, long divisor) { + return 0; + } + + /// The value as an unsigned decimal string. + public static String toUnsignedString(long i) { + return null; + } + + /// The value as an unsigned string in `radix`. + public static String toUnsignedString(long i, int radix) { + return null; + } + + /// Parses an unsigned decimal long. + public static long parseUnsignedLong(String s) throws NumberFormatException { + return 0; + } + + /// Parses an unsigned long in `radix`. + public static long parseUnsignedLong(String s, int radix) throws NumberFormatException { + return 0; + } + + /// Parses `s` from `beginIndex` to `endIndex` as a signed long. + public static long parseLong(CharSequence s, int beginIndex, int endIndex, int radix) + throws NumberFormatException { + return 0; + } + + /// Parses `s` from `beginIndex` to `endIndex` as an unsigned long. + public static long parseUnsignedLong(CharSequence s, int beginIndex, int endIndex, int radix) + throws NumberFormatException { + return 0; + } + + /// Decodes a decimal, `0x`, `0X`, `#` or leading-zero octal long, with an + /// optional sign. + public static Long decode(String nm) throws NumberFormatException { + return null; + } + + /// The system property `nm` as a Long, or null. + public static Long getLong(String nm) { + return null; + } + + /// The system property `nm` as a Long, or `val`. + public static Long getLong(String nm, long val) { + return null; + } + + /// The system property `nm` decoded as a Long, or `val` when it is unset + /// or not a number. + public static Long getLong(String nm, Long val) { + return null; + } + + /// The bits of `i` selected by `mask`, packed to the low end. + public static long compress(long i, long mask) { + return 0; + } + + /// The low bits of `i` spread to the positions of the one bits of `mask`. + public static long expand(long i, long mask) { + return 0; + } } diff --git a/Ports/CLDC11/src/java/lang/MatchException.java b/Ports/CLDC11/src/java/lang/MatchException.java new file mode 100644 index 00000000000..eb3e1aab5dd --- /dev/null +++ b/Ports/CLDC11/src/java/lang/MatchException.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.lang; + +/// `java.lang.MatchException`: what a `switch` over patterns throws when no +/// case matched a value it was compiled to cover -- a sealed type or an enum +/// that gained a member after the switch was compiled -- or when a record's +/// accessor failed while a pattern took the record apart. +/// +/// javac names it in the code of every exhaustive pattern switch, so the +/// class has to exist on a device for such a switch to load at all. +public final class MatchException extends RuntimeException { + /// A failed match, with what caused it or null. + public MatchException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/Ports/CLDC11/src/java/lang/Math.java b/Ports/CLDC11/src/java/lang/Math.java index 8129b172ad8..6211893d782 100644 --- a/Ports/CLDC11/src/java/lang/Math.java +++ b/Ports/CLDC11/src/java/lang/Math.java @@ -231,6 +231,314 @@ public static double nextAfter(double start, double direction) { public static float nextAfter(float start, double direction) { return 0; } - - + + /// Twice pi, the ratio of a circle's circumference to its radius. + public static final double TAU = 2.0 * PI; + + /// A pseudo-random double in `[0, 1)`. + public static double random() { + return 0; + } + + /// -1.0, 0.0 or 1.0 by the sign of `d`; a zero or NaN is returned as it is. + public static double signum(double d) { + return 0; + } + + /// -1.0f, 0.0f or 1.0f by the sign of `f`; a zero or NaN is returned as it is. + public static float signum(float f) { + return 0; + } + + /// `magnitude` with the sign bit of `sign`. + public static double copySign(double magnitude, double sign) { + return 0; + } + + /// `magnitude` with the sign bit of `sign`. + public static float copySign(float magnitude, float sign) { + return 0; + } + + /// The unbiased exponent of `d`: 1024 for NaN and the infinities, -1023 + /// for zero and the subnormals. + public static int getExponent(double d) { + return 0; + } + + /// The unbiased exponent of `f`: 128 for NaN and the infinities, -127 for + /// zero and the subnormals. + public static int getExponent(float f) { + return 0; + } + + /// The distance from `d` to the next double of larger magnitude. + public static double ulp(double d) { + return 0; + } + + /// The distance from `f` to the next float of larger magnitude. + public static float ulp(float f) { + return 0; + } + + /// The double next to `d` towards positive infinity. + public static double nextUp(double d) { + return 0; + } + + /// The float next to `f` towards positive infinity. + public static float nextUp(float f) { + return 0; + } + + /// The float next to `f` towards negative infinity. + public static float nextDown(float f) { + return 0; + } + + /// `d` times 2 to the power `scaleFactor`, rounded once. + public static double scalb(double d, int scaleFactor) { + return 0; + } + + /// `f` times 2 to the power `scaleFactor`, rounded once. + public static float scalb(float f, int scaleFactor) { + return 0; + } + + /// The square root of `x * x + y * y`, without overflow in between. + public static double hypot(double x, double y) { + return 0; + } + + /// The cube root of `a`. + public static double cbrt(double a) { + return 0; + } + + /// The whole number nearest `a`; a tie goes to the even one. + public static double rint(double a) { + return 0; + } + + /// The hyperbolic sine of `x`. + public static double sinh(double x) { + return 0; + } + + /// The hyperbolic cosine of `x`. + public static double cosh(double x) { + return 0; + } + + /// The hyperbolic tangent of `x`. + public static double tanh(double x) { + return 0; + } + + /// The largest int not greater than `x / y`. + public static int floorDiv(int x, int y) { + return 0; + } + + /// The largest long not greater than `x / y`. + public static long floorDiv(long x, int y) { + return 0; + } + + /// The largest long not greater than `x / y`. + public static long floorDiv(long x, long y) { + return 0; + } + + /// `x - floorDiv(x, y) * y`: the remainder with the sign of `y`. + public static int floorMod(int x, int y) { + return 0; + } + + /// `x - floorDiv(x, y) * y`: the remainder with the sign of `y`. + public static int floorMod(long x, int y) { + return 0; + } + + /// `x - floorDiv(x, y) * y`: the remainder with the sign of `y`. + public static long floorMod(long x, long y) { + return 0; + } + + /// The smallest int not less than `x / y`. + public static int ceilDiv(int x, int y) { + return 0; + } + + /// The smallest long not less than `x / y`. + public static long ceilDiv(long x, int y) { + return 0; + } + + /// The smallest long not less than `x / y`. + public static long ceilDiv(long x, long y) { + return 0; + } + + /// `x - ceilDiv(x, y) * y`. + public static int ceilMod(int x, int y) { + return 0; + } + + /// `x - ceilDiv(x, y) * y`. + public static int ceilMod(long x, int y) { + return 0; + } + + /// `x - ceilDiv(x, y) * y`. + public static long ceilMod(long x, long y) { + return 0; + } + + /// `x / y`, throwing on the one quotient that overflows. + public static int divideExact(int x, int y) { + return 0; + } + + /// `x / y`, throwing on the one quotient that overflows. + public static long divideExact(long x, long y) { + return 0; + } + + /// `floorDiv(x, y)`, throwing on the one quotient that overflows. + public static int floorDivExact(int x, int y) { + return 0; + } + + /// `floorDiv(x, y)`, throwing on the one quotient that overflows. + public static long floorDivExact(long x, long y) { + return 0; + } + + /// `ceilDiv(x, y)`, throwing on the one quotient that overflows. + public static int ceilDivExact(int x, int y) { + return 0; + } + + /// `ceilDiv(x, y)`, throwing on the one quotient that overflows. + public static long ceilDivExact(long x, long y) { + return 0; + } + + /// `x + y`, throwing when it overflows. + public static int addExact(int x, int y) { + return 0; + } + + /// `x + y`, throwing when it overflows. + public static long addExact(long x, long y) { + return 0; + } + + /// `x - y`, throwing when it overflows. + public static int subtractExact(int x, int y) { + return 0; + } + + /// `x - y`, throwing when it overflows. + public static long subtractExact(long x, long y) { + return 0; + } + + /// `x * y`, throwing when it overflows. + public static int multiplyExact(int x, int y) { + return 0; + } + + /// `x * y`, throwing when it overflows. + public static long multiplyExact(long x, int y) { + return 0; + } + + /// `x * y`, throwing when it overflows. + public static long multiplyExact(long x, long y) { + return 0; + } + + /// The exact product of two ints. + public static long multiplyFull(int x, int y) { + return 0; + } + + /// The high 64 bits of the 128-bit product of two signed longs. + public static long multiplyHigh(long x, long y) { + return 0; + } + + /// The high 64 bits of the 128-bit product of two unsigned longs. + public static long unsignedMultiplyHigh(long x, long y) { + return 0; + } + + /// `a + 1`, throwing when it overflows. + public static int incrementExact(int a) { + return 0; + } + + /// `a + 1`, throwing when it overflows. + public static long incrementExact(long a) { + return 0; + } + + /// `a - 1`, throwing when it overflows. + public static int decrementExact(int a) { + return 0; + } + + /// `a - 1`, throwing when it overflows. + public static long decrementExact(long a) { + return 0; + } + + /// `-a`, throwing when it overflows. + public static int negateExact(int a) { + return 0; + } + + /// `-a`, throwing when it overflows. + public static long negateExact(long a) { + return 0; + } + + /// The value as an int, throwing when it does not fit. + public static int toIntExact(long value) { + return 0; + } + + /// The absolute value, throwing for `Integer.MIN_VALUE`, which has none. + public static int absExact(int a) { + return 0; + } + + /// The absolute value, throwing for `Long.MIN_VALUE`, which has none. + public static long absExact(long a) { + return 0; + } + + /// `value` clamped to `[min, max]`, as an int. + public static int clamp(long value, int min, int max) { + return 0; + } + + /// `value` clamped to `[min, max]`. + public static long clamp(long value, long min, long max) { + return 0; + } + + /// `value` clamped to `[min, max]`. + public static double clamp(double value, double min, double max) { + return 0; + } + + /// `value` clamped to `[min, max]`. + public static float clamp(float value, float min, float max) { + return 0; + } } diff --git a/Ports/CLDC11/src/java/lang/Module.java b/Ports/CLDC11/src/java/lang/Module.java new file mode 100644 index 00000000000..3ab4b321139 --- /dev/null +++ b/Ports/CLDC11/src/java/lang/Module.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.lang; + +/// The module a class is in, as far as an application without modules has +/// one: the single unnamed module every class of it is in. +/// +/// [Class#getModule()] answers it for every class. It has no name, no layer +/// and no descriptor, which is what the JDK answers for a class loaded from +/// the class path, and what code that asks in order to choose between the +/// module path and the class path needs to hear. Nothing else of the module +/// system is here. +public final class Module { + + // Made by this class's own initializer, which only a getModule() call runs. + private static final Module UNNAMED = new Module(); + + private Module() { + } + + /// The one module. + static Module unnamed() { + return UNNAMED; + } + + /// False: the one module there is has no name. + public boolean isNamed() { + return false; + } + + /// Null, as for every unnamed module. + public String getName() { + return null; + } + + /// Null: an unnamed module is in no layer. + public ModuleLayer getLayer() { + return null; + } + + @Override + public String toString() { + return "unnamed module @" + Integer.toHexString(hashCode()); + } +} diff --git a/Ports/CLDC11/src/java/lang/ModuleLayer.java b/Ports/CLDC11/src/java/lang/ModuleLayer.java new file mode 100644 index 00000000000..282fba9c41a --- /dev/null +++ b/Ports/CLDC11/src/java/lang/ModuleLayer.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.lang; + +/// `java.lang.ModuleLayer`, as a type only. An application without modules +/// has no layer: [Module#getLayer()] answers null and nothing creates an +/// instance of this class. It exists so that code which asks for a class's +/// layer, and takes the class path route when there is none, links. +public final class ModuleLayer { + + private ModuleLayer() { + } +} diff --git a/Ports/CLDC11/src/java/lang/Runtime.java b/Ports/CLDC11/src/java/lang/Runtime.java index 9f9571703db..1f33fd15bb9 100644 --- a/Ports/CLDC11/src/java/lang/Runtime.java +++ b/Ports/CLDC11/src/java/lang/Runtime.java @@ -26,6 +26,34 @@ /// An application cannot create its own instance of this class. /// Since: JDK1.0, CLDC 1.0 See Also:getRuntime() public class Runtime{ + private Runtime() { + } + + /// Ends the process at once, without running the shutdown hooks. + public void halt(int status) { + throw new RuntimeException("Compile-time stub"); + } + + /// Registers a hook that `exit` runs before the application closes. + public void addShutdownHook(Thread hook) { + throw new RuntimeException("Compile-time stub"); + } + + /// Unregisters a hook; whether it was registered. + public boolean removeShutdownHook(Thread hook) { + throw new RuntimeException("Compile-time stub"); + } + + /// The number of processors available to the program. + public int availableProcessors() { + throw new RuntimeException("Compile-time stub"); + } + + /// The most memory the program may use. + public long maxMemory() { + throw new RuntimeException("Compile-time stub"); + } + /// Terminates the currently running Java application. This method never returns normally. /// The argument serves as a status code; by convention, a nonzero status code indicates abnormal termination. public void exit(int status){ diff --git a/Ports/CLDC11/src/java/lang/Short.java b/Ports/CLDC11/src/java/lang/Short.java index d2fd8c381a8..5b71ae296dd 100644 --- a/Ports/CLDC11/src/java/lang/Short.java +++ b/Ports/CLDC11/src/java/lang/Short.java @@ -110,4 +110,62 @@ public int compareTo(Short another) { public static int compare(short s1, short s2) { return s1 < s2 ? -1 : s1 > s2 ? 1 : 0; } + + public static final Class TYPE = null; + + /// The size of a short in bytes. + public static final int BYTES = 2; + + /// The size of a short in bits. + public static final int SIZE = 16; + + /// A Short of `parseShort(s)`. + @Deprecated + public Short(String s) throws NumberFormatException { + } + + /// The decimal string of `s`. + public static String toString(short s) { + return null; + } + + /// A Short of `parseShort(s)`. + public static Short valueOf(String s) throws NumberFormatException { + return null; + } + + /// A Short of `parseShort(s, radix)`. + public static Short valueOf(String s, int radix) throws NumberFormatException { + return null; + } + + /// Decodes a decimal, hexadecimal or octal short, as `Integer.decode`. + public static Short decode(String nm) throws NumberFormatException { + return null; + } + + /// The value itself. + public static int hashCode(short value) { + return 0; + } + + /// Compares the two as unsigned values. + public static int compareUnsigned(short x, short y) { + return 0; + } + + /// The value as an unsigned int. + public static int toUnsignedInt(short x) { + return 0; + } + + /// The value as an unsigned long. + public static long toUnsignedLong(short x) { + return 0; + } + + /// The bytes in reverse order. + public static short reverseBytes(short i) { + return 0; + } } diff --git a/Ports/CLDC11/src/java/lang/StrictMath.java b/Ports/CLDC11/src/java/lang/StrictMath.java new file mode 100644 index 00000000000..57309f6a248 --- /dev/null +++ b/Ports/CLDC11/src/java/lang/StrictMath.java @@ -0,0 +1,472 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +package java.lang; + +/// The JDK's StrictMath. Every member answers exactly as the `Math` member +/// of the same name: this runtime has one implementation of each function, +/// so the results are the same on every platform it runs on, which is what +/// StrictMath promises. +public final class StrictMath { + private StrictMath() { + } + + /// As `Math.E`. + public static final double E = Math.E; + + /// As `Math.PI`. + public static final double PI = Math.PI; + + /// As `Math.TAU`. + public static final double TAU = Math.TAU; + + /// As `Math.abs`. + public static double abs(double a) { + return 0; + } + + /// As `Math.abs`. + public static float abs(float a) { + return 0; + } + + /// As `Math.abs`. + public static int abs(int a) { + return 0; + } + + /// As `Math.abs`. + public static long abs(long a) { + return 0; + } + + /// As `Math.ceil`. + public static double ceil(double a) { + return 0; + } + + /// As `Math.cos`. + public static double cos(double a) { + return 0; + } + + /// As `Math.floor`. + public static double floor(double a) { + return 0; + } + + /// As `Math.max`. + public static double max(double a, double b) { + return 0; + } + + /// As `Math.max`. + public static float max(float a, float b) { + return 0; + } + + /// As `Math.max`. + public static int max(int a, int b) { + return 0; + } + + /// As `Math.max`. + public static long max(long a, long b) { + return 0; + } + + /// As `Math.min`. + public static double min(double a, double b) { + return 0; + } + + /// As `Math.min`. + public static float min(float a, float b) { + return 0; + } + + /// As `Math.min`. + public static int min(int a, int b) { + return 0; + } + + /// As `Math.min`. + public static long min(long a, long b) { + return 0; + } + + /// As `Math.sin`. + public static double sin(double a) { + return 0; + } + + /// As `Math.sqrt`. + public static double sqrt(double a) { + return 0; + } + + /// As `Math.tan`. + public static double tan(double a) { + return 0; + } + + /// As `Math.toDegrees`. + public static double toDegrees(double a) { + return 0; + } + + /// As `Math.toRadians`. + public static double toRadians(double a) { + return 0; + } + + /// As `Math.round`. + public static long round(double a) { + return 0; + } + + /// As `Math.round`. + public static int round(float a) { + return 0; + } + + /// As `Math.nextDown`. + public static double nextDown(double a) { + return 0; + } + + /// As `Math.nextAfter`. + public static double nextAfter(double a, double b) { + return 0; + } + + /// As `Math.nextAfter`. + public static float nextAfter(float a, double b) { + return 0; + } + + /// As `Math.random`. + public static double random() { + return 0; + } + + /// As `Math.signum`. + public static double signum(double a) { + return 0; + } + + /// As `Math.signum`. + public static float signum(float a) { + return 0; + } + + /// As `Math.copySign`. + public static double copySign(double a, double b) { + return 0; + } + + /// As `Math.copySign`. + public static float copySign(float a, float b) { + return 0; + } + + /// As `Math.getExponent`. + public static int getExponent(double a) { + return 0; + } + + /// As `Math.getExponent`. + public static int getExponent(float a) { + return 0; + } + + /// As `Math.ulp`. + public static double ulp(double a) { + return 0; + } + + /// As `Math.ulp`. + public static float ulp(float a) { + return 0; + } + + /// As `Math.nextUp`. + public static double nextUp(double a) { + return 0; + } + + /// As `Math.nextUp`. + public static float nextUp(float a) { + return 0; + } + + /// As `Math.nextDown`. + public static float nextDown(float a) { + return 0; + } + + /// As `Math.scalb`. + public static double scalb(double a, int b) { + return 0; + } + + /// As `Math.scalb`. + public static float scalb(float a, int b) { + return 0; + } + + /// As `Math.hypot`. + public static double hypot(double a, double b) { + return 0; + } + + /// As `Math.cbrt`. + public static double cbrt(double a) { + return 0; + } + + /// As `Math.rint`. + public static double rint(double a) { + return 0; + } + + /// As `Math.sinh`. + public static double sinh(double a) { + return 0; + } + + /// As `Math.cosh`. + public static double cosh(double a) { + return 0; + } + + /// As `Math.tanh`. + public static double tanh(double a) { + return 0; + } + + /// As `Math.floorDiv`. + public static int floorDiv(int a, int b) { + return 0; + } + + /// As `Math.floorDiv`. + public static long floorDiv(long a, int b) { + return 0; + } + + /// As `Math.floorDiv`. + public static long floorDiv(long a, long b) { + return 0; + } + + /// As `Math.floorMod`. + public static int floorMod(int a, int b) { + return 0; + } + + /// As `Math.floorMod`. + public static int floorMod(long a, int b) { + return 0; + } + + /// As `Math.floorMod`. + public static long floorMod(long a, long b) { + return 0; + } + + /// As `Math.ceilDiv`. + public static int ceilDiv(int a, int b) { + return 0; + } + + /// As `Math.ceilDiv`. + public static long ceilDiv(long a, int b) { + return 0; + } + + /// As `Math.ceilDiv`. + public static long ceilDiv(long a, long b) { + return 0; + } + + /// As `Math.ceilMod`. + public static int ceilMod(int a, int b) { + return 0; + } + + /// As `Math.ceilMod`. + public static int ceilMod(long a, int b) { + return 0; + } + + /// As `Math.ceilMod`. + public static long ceilMod(long a, long b) { + return 0; + } + + /// As `Math.divideExact`. + public static int divideExact(int a, int b) { + return 0; + } + + /// As `Math.divideExact`. + public static long divideExact(long a, long b) { + return 0; + } + + /// As `Math.floorDivExact`. + public static int floorDivExact(int a, int b) { + return 0; + } + + /// As `Math.floorDivExact`. + public static long floorDivExact(long a, long b) { + return 0; + } + + /// As `Math.ceilDivExact`. + public static int ceilDivExact(int a, int b) { + return 0; + } + + /// As `Math.ceilDivExact`. + public static long ceilDivExact(long a, long b) { + return 0; + } + + /// As `Math.addExact`. + public static int addExact(int a, int b) { + return 0; + } + + /// As `Math.addExact`. + public static long addExact(long a, long b) { + return 0; + } + + /// As `Math.subtractExact`. + public static int subtractExact(int a, int b) { + return 0; + } + + /// As `Math.subtractExact`. + public static long subtractExact(long a, long b) { + return 0; + } + + /// As `Math.multiplyExact`. + public static int multiplyExact(int a, int b) { + return 0; + } + + /// As `Math.multiplyExact`. + public static long multiplyExact(long a, int b) { + return 0; + } + + /// As `Math.multiplyExact`. + public static long multiplyExact(long a, long b) { + return 0; + } + + /// As `Math.multiplyFull`. + public static long multiplyFull(int a, int b) { + return 0; + } + + /// As `Math.multiplyHigh`. + public static long multiplyHigh(long a, long b) { + return 0; + } + + /// As `Math.unsignedMultiplyHigh`. + public static long unsignedMultiplyHigh(long a, long b) { + return 0; + } + + /// As `Math.incrementExact`. + public static int incrementExact(int a) { + return 0; + } + + /// As `Math.incrementExact`. + public static long incrementExact(long a) { + return 0; + } + + /// As `Math.decrementExact`. + public static int decrementExact(int a) { + return 0; + } + + /// As `Math.decrementExact`. + public static long decrementExact(long a) { + return 0; + } + + /// As `Math.negateExact`. + public static int negateExact(int a) { + return 0; + } + + /// As `Math.negateExact`. + public static long negateExact(long a) { + return 0; + } + + /// As `Math.toIntExact`. + public static int toIntExact(long a) { + return 0; + } + + /// As `Math.absExact`. + public static int absExact(int a) { + return 0; + } + + /// As `Math.absExact`. + public static long absExact(long a) { + return 0; + } + + /// As `Math.clamp`. + public static int clamp(long a, int b, int c) { + return 0; + } + + /// As `Math.clamp`. + public static long clamp(long a, long b, long c) { + return 0; + } + + /// As `Math.clamp`. + public static double clamp(double a, double b, double c) { + return 0; + } + + /// As `Math.clamp`. + public static float clamp(float a, float b, float c) { + return 0; + } +} diff --git a/Ports/CLDC11/src/java/lang/String.java b/Ports/CLDC11/src/java/lang/String.java index 853d60a73fc..30e1bf8fb62 100644 --- a/Ports/CLDC11/src/java/lang/String.java +++ b/Ports/CLDC11/src/java/lang/String.java @@ -68,14 +68,10 @@ public String(byte[] bytes, int off, int len, java.lang.String enc) throws java. } /// Construct a new String by converting the specified array of bytes using the specified character encoding. The length of the new String is a function of the encoding, and hence may not be equal to the length of the byte array. - /// bytes - The bytes to be converted into charactersenc - The name of a supported character encoding - /// - If the named encoding is not supported - /// - /// #### Since - /// - /// 8.0 - public String(byte[] bytes, java.nio.charset.Charset charset) throws java.io.UnsupportedEncodingException { - this(bytes, 0, bytes.length, charset.name()); + /// Unlike the constructor taking a charset name, this throws no checked + /// exception. + public String(byte[] bytes, java.nio.charset.Charset charset) { + this(bytes, 0, bytes.length, charset); } /// Construct a new String by converting the specified array of bytes using the specified character encoding. The length of the new String is a function of the encoding, and hence may not be equal to the length of the byte array. @@ -186,8 +182,9 @@ public byte[] getBytes(java.lang.String enc) throws java.io.UnsupportedEncodingE } /// Convert this String into bytes according to the specified character encoding, storing the result into a new byte array. - public byte[] getBytes(Charset charset) throws java.io.UnsupportedEncodingException { - return getBytes(charset.name()); + /// Unlike `getBytes(String)`, this throws no checked exception. + public byte[] getBytes(Charset charset) { + return null; } /// Copies characters from this string into the destination character array. @@ -425,7 +422,8 @@ public static java.lang.String valueOf(char c){ return null; //TODO codavaj!! } - java.lang.String valueOf(char[] data){ + /// Returns the string representation of the char array argument. + public static java.lang.String valueOf(char[] data){ return null; //TODO codavaj!! } @@ -519,4 +517,118 @@ public boolean regionMatches(int thisStart, String string, int start, int length return false; } + /// A string of the code points `codePoints[offset]` to + /// `codePoints[offset + count - 1]`. + public String(int[] codePoints, int offset, int count) { + } + + /// Decodes `length` bytes of `bytes` from `offset` with `charset`. + public String(byte[] bytes, int offset, int length, java.nio.charset.Charset charset) { + } + + /// The code point at `index`. + public int codePointAt(int index) { + return 0; + } + + /// The code point that ends just before `index`. + public int codePointBefore(int index) { + return 0; + } + + /// The number of code points between `beginIndex` and `endIndex`. + public int codePointCount(int beginIndex, int endIndex) { + return 0; + } + + /// The index `codePointOffset` code points away from `index`. + public int offsetByCodePoints(int index, int codePointOffset) { + return 0; + } + + /// The first index of `ch` in `[beginIndex, endIndex)`, or -1. + public int indexOf(int ch, int beginIndex, int endIndex) { + return -1; + } + + /// The first index of `str` that lies wholly in `[beginIndex, endIndex)`, or -1. + public int indexOf(String str, int beginIndex, int endIndex) { + return -1; + } + + /// The elements with `delimiter` between every two. + public static String join(CharSequence delimiter, CharSequence... elements) { + return null; + } + + /// The elements with `delimiter` between every two. + public static String join(CharSequence delimiter, Iterable elements) { + return null; + } + + /// As `format(String, Object...)`. + public static String format(java.util.Locale l, String format, Object... args) { + return null; + } + + /// As `toLowerCase()`. + public java.lang.String toLowerCase(java.util.Locale locale) { + return null; + } + + /// `String.format(this, args)`. + public String formatted(Object... args) { + return null; + } + + /// Whether the string is empty or holds nothing but white space. + public boolean isBlank() { + return false; + } + + /// The string without its leading and trailing white space. + public String strip() { + return null; + } + + /// The string without its leading white space. + public String stripLeading() { + return null; + } + + /// The string without its trailing white space. + public String stripTrailing() { + return null; + } + + /// The string `count` times over. + public String repeat(int count) { + return null; + } + + /// The lines of the string, without their terminators. + public java.util.stream.Stream lines() { + return null; + } + + /// Every line indented by `n` spaces, or outdented for a negative `n`. + public String indent(int n) { + return null; + } + + /// The string with the indentation common to its lines removed. + public String stripIndent() { + return null; + } + + /// The string with its escape sequences translated. + public String translateEscapes() { + return null; + } + + /// `f.apply(this)`. + public R transform(java.util.function.Function f) { + return null; + } + } diff --git a/Ports/CLDC11/src/java/lang/StringBuffer.java b/Ports/CLDC11/src/java/lang/StringBuffer.java index 38e02416a46..10342c3d1d7 100644 --- a/Ports/CLDC11/src/java/lang/StringBuffer.java +++ b/Ports/CLDC11/src/java/lang/StringBuffer.java @@ -31,7 +31,7 @@ /// In general, if sb refers to an instance of a StringBuffer, then sb.append(x) has the same effect as sb.insert(sb.length(),x). /// Every string buffer has a capacity. As long as the length of the character sequence contained in the string buffer does not exceed the capacity, it is not necessary to allocate a new internal buffer array. If the internal buffer overflows, it is automatically made larger. /// Since: JDK1.0, CLDC 1.0 See Also:ByteArrayOutputStream, String -public final class StringBuffer implements CharSequence { +public final class StringBuffer implements CharSequence, Appendable, Comparable { /// Constructs a string buffer with no characters in it and an initial capacity of 16 characters. public StringBuffer(){ //TODO codavaj!! @@ -63,7 +63,7 @@ public java.lang.StringBuffer append(char c){ return null; //TODO codavaj!! } - java.lang.StringBuffer append(char[] str){ + public java.lang.StringBuffer append(char[] str){ return null; //TODO codavaj!! } @@ -158,7 +158,7 @@ public java.lang.StringBuffer insert(int offset, char c){ return null; //TODO codavaj!! } - java.lang.StringBuffer insert(int offset, char[] str){ + public java.lang.StringBuffer insert(int offset, char[] str){ return null; //TODO codavaj!! } @@ -239,9 +239,9 @@ public java.lang.String toString(){ return null; //TODO codavaj!! } - public StringBuffer StringBuffer(java.lang.CharSequence cs) { - return new StringBuffer(cs.toString()); - } + /// A StringBuffer holding the characters of `cs`. + public StringBuffer(java.lang.CharSequence cs) { + } public void trimToSize() { // do nothing: according to the 1.5 javadoc, @@ -273,5 +273,88 @@ public CharSequence subSequence(int start, int end) { return null; } + /// Appends the contents of `sb`, or `"null"`. + public synchronized StringBuffer append(StringBuffer sb) { + return null; + } + + /// Inserts `len` chars of `str` from `offset` at `index`. + public synchronized StringBuffer insert(int index, char[] str, int offset, int len) { + return null; + } + + /// Appends the code point, as one char or as a surrogate pair. + public synchronized StringBuffer appendCodePoint(int codePoint) { + return null; + } + + /// The code point at `index`. + public synchronized int codePointAt(int index) { + return 0; + } + + /// The code point that ends just before `index`. + public synchronized int codePointBefore(int index) { + return 0; + } + + /// The number of code points between `beginIndex` and `endIndex`. + public synchronized int codePointCount(int beginIndex, int endIndex) { + return 0; + } + + /// The index `codePointOffset` code points away from `index`. + public synchronized int offsetByCodePoints(int index, int codePointOffset) { + return 0; + } + + /// The first index of `str`, or -1. + public synchronized int indexOf(String str) { + return 0; + } + + /// The first index of `str` at or after `fromIndex`, or -1. + public synchronized int indexOf(String str, int fromIndex) { + return 0; + } + + /// The last index of `str`, or -1. + public synchronized int lastIndexOf(String str) { + return 0; + } + + /// The last index of `str` at or before `fromIndex`, or -1. + public synchronized int lastIndexOf(String str, int fromIndex) { + return 0; + } + + /// Replaces the characters from `start` to `end` with `str`. + public synchronized StringBuffer replace(int start, int end, String str) { + return null; + } + + /// The characters from `start` to the end. + public synchronized String substring(int start) { + return null; + } + /// The characters from `start` to `end`. + public synchronized String substring(int start, int end) { + return null; + } + + /// Appends the code point `count` times. + public synchronized StringBuffer repeat(int codePoint, int count) { + return null; + } + + /// Appends `cs` `count` times. + public synchronized StringBuffer repeat(CharSequence cs, int count) { + return null; + } + + /// Compares the contents lexicographically. + public synchronized int compareTo(StringBuffer another) { + return 0; + } } diff --git a/Ports/CLDC11/src/java/lang/StringBuilder.java b/Ports/CLDC11/src/java/lang/StringBuilder.java index f7d3ac90e1e..46130b048a2 100644 --- a/Ports/CLDC11/src/java/lang/StringBuilder.java +++ b/Ports/CLDC11/src/java/lang/StringBuilder.java @@ -31,7 +31,7 @@ /// In general, if sb refers to an instance of a StringBuilder, then sb.append(x) has the same effect as sb.insert(sb.length(),x). /// Every string builder has a capacity. As long as the length of the character sequence contained in the string builder does not exceed the capacity, it is not necessary to allocate a new internal builder array. If the internal builder overflows, it is automatically made larger. /// Since: JDK1.0, CLDC 1.0 See Also:ByteArrayOutputStream, String -public final class StringBuilder implements CharSequence { +public final class StringBuilder implements CharSequence, Appendable, Comparable { /// Constructs a string builder with no characters in it and an initial capacity of 16 characters. public StringBuilder(){ //TODO codavaj!! @@ -171,7 +171,7 @@ public java.lang.StringBuilder insert(int offset, char c){ return null; //TODO codavaj!! } - java.lang.StringBuilder insert(int offset, char[] str){ + public java.lang.StringBuilder insert(int offset, char[] str){ return null; //TODO codavaj!! } @@ -286,5 +286,87 @@ public CharSequence subSequence(int start, int end) { return null; } + public StringBuilder insert(int index, char[] value, int offset, int length) { + return null; + } + + /// The characters from `start` to the end, as a String. + public String substring(int start) { + return null; + } + + /// The characters from `start` to `end`, as a String. + public String substring(int start, int end) { + return null; + } + + /// Appends the code point, as one char or as a surrogate pair. + /// + /// #### Throws + /// + /// - `IllegalArgumentException`: if it is not a valid code point + public StringBuilder appendCodePoint(int codePoint) { + return null; + } + + /// The code point at `index`. + public int codePointAt(int index) { + return 0; + } + /// The code point that ends just before `index`. + public int codePointBefore(int index) { + return 0; + } + + /// The number of code points between `beginIndex` and `endIndex`. + public int codePointCount(int beginIndex, int endIndex) { + return 0; + } + + /// The index `codePointOffset` code points away from `index`. + public int offsetByCodePoints(int index, int codePointOffset) { + return 0; + } + + /// The first index of `str`, or -1. + public int indexOf(String str) { + return 0; + } + + /// The first index of `str` at or after `fromIndex`, or -1. + public int indexOf(String str, int fromIndex) { + return 0; + } + + /// The last index of `str`, or -1. + public int lastIndexOf(String str) { + return 0; + } + + /// The last index of `str` at or before `fromIndex`, or -1. + public int lastIndexOf(String str, int fromIndex) { + return 0; + } + + /// Replaces the characters from `start` to `end` (clamped to the length) + /// with `str`. + public StringBuilder replace(int start, int end, String str) { + return null; + } + + /// Appends the code point `count` times. + public StringBuilder repeat(int codePoint, int count) { + return null; + } + + /// Appends `cs` (or `"null"`) `count` times. + public StringBuilder repeat(CharSequence cs, int count) { + return null; + } + + /// Compares the contents lexicographically. + public int compareTo(StringBuilder another) { + return 0; + } } diff --git a/Ports/CLDC11/src/java/lang/System.java b/Ports/CLDC11/src/java/lang/System.java index 359682fac8c..9ceee404836 100644 --- a/Ports/CLDC11/src/java/lang/System.java +++ b/Ports/CLDC11/src/java/lang/System.java @@ -92,6 +92,22 @@ public static String clearProperty(String key) { throw new RuntimeException("Compile-time stub"); } + /// The value of the environment variable `name`, or null when it is not + /// set. + public static String getenv(String name) { + throw new RuntimeException("Compile-time stub"); + } + + /// The environment as a map; empty, since the runtime cannot list it. + public static java.util.Map getenv() { + throw new RuntimeException("Compile-time stub"); + } + + /// The line separator, a single line feed. + public static String lineSeparator() { + throw new RuntimeException("Compile-time stub"); + } + /// Returns the same hashcode for the given object as would be returned by the default method hashCode(), whether or not the given object's class overrides hashCode(). The hashcode for the null reference is zero. public static int identityHashCode(java.lang.Object x){ return 0; //TODO codavaj!! diff --git a/Ports/CLDC11/src/java/lang/Thread.java b/Ports/CLDC11/src/java/lang/Thread.java index d3eb877d8d7..2f87fe5ea7e 100644 --- a/Ports/CLDC11/src/java/lang/Thread.java +++ b/Ports/CLDC11/src/java/lang/Thread.java @@ -128,6 +128,63 @@ public final void join() throws java.lang.InterruptedException{ return; //TODO codavaj!! } + /// Waits at most `millis` milliseconds for this thread to die; 0 waits + /// for as long as it takes. + public final synchronized void join(long millis) throws java.lang.InterruptedException { + throw new RuntimeException("Compile-time stub"); + } + + /// Waits at most `millis` milliseconds plus `nanos` nanoseconds for this + /// thread to die. + public final synchronized void join(long millis, int nanos) throws java.lang.InterruptedException { + throw new RuntimeException("Compile-time stub"); + } + + /// Sleeps for `millis` milliseconds plus `nanos` nanoseconds. + public static void sleep(long millis, int nanos) throws java.lang.InterruptedException { + throw new RuntimeException("Compile-time stub"); + } + + /// Changes this thread's name. + public final void setName(String name) { + throw new RuntimeException("Compile-time stub"); + } + + /// A number that identifies this thread for its lifetime. + public long getId() { + throw new RuntimeException("Compile-time stub"); + } + + /// Handles what a thread's run method throws. + public interface UncaughtExceptionHandler { + /// Called with the thread and what its run method threw. + void uncaughtException(Thread t, Throwable e); + } + + /// Sets the handler for what this thread's run method throws; null + /// leaves it to the default handler. + public void setUncaughtExceptionHandler(UncaughtExceptionHandler handler) { + throw new RuntimeException("Compile-time stub"); + } + + /// This thread's handler, or the default handler when it has none. + public UncaughtExceptionHandler getUncaughtExceptionHandler() { + throw new RuntimeException("Compile-time stub"); + } + + /// Sets the handler for what any thread without its own handler throws. + /// In a Codename One application it also receives what an event dispatch + /// thread task throws, which then no longer reaches the platform's own + /// error handling. + public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler handler) { + throw new RuntimeException("Compile-time stub"); + } + + /// The handler `setDefaultUncaughtExceptionHandler` set, or null. + public static UncaughtExceptionHandler getDefaultUncaughtExceptionHandler() { + throw new RuntimeException("Compile-time stub"); + } + /// If this thread was constructed using a separate Runnable run object, then that Runnable object's run method is called; otherwise, this method does nothing and returns. /// Subclasses of Thread should override this method. public void run(){ diff --git a/Ports/CLDC11/src/java/lang/Throwable.java b/Ports/CLDC11/src/java/lang/Throwable.java index f4b408fc18b..ee96e063edf 100644 --- a/Ports/CLDC11/src/java/lang/Throwable.java +++ b/Ports/CLDC11/src/java/lang/Throwable.java @@ -92,6 +92,10 @@ public void printStackTrace(java.io.PrintStream s){ return; //TODO codavaj!! } + /// Prints this throwable and its backtrace to the given writer. + public void printStackTrace(java.io.PrintWriter s){ + } + /// Returns a short description of this Throwable object. If this Throwable object was /// with an error message string, then the result is the concatenation of three strings: The name of the actual class of this object ": " (a colon and a space) The result of the /// method for this object If this Throwable object was diff --git a/Ports/CLDC11/src/java/lang/reflect/InvocationTargetException.java b/Ports/CLDC11/src/java/lang/reflect/InvocationTargetException.java new file mode 100644 index 00000000000..77619898d8b --- /dev/null +++ b/Ports/CLDC11/src/java/lang/reflect/InvocationTargetException.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.lang.reflect; + +/// `java.lang.reflect.InvocationTargetException` for the Codename One +/// runtime. A device has no reflective invocation, so nothing here throws +/// it on its own account: it exists for the code that declares, catches or +/// wraps it, `EventQueue.invokeAndWait` and `SwingUtilities.invokeAndWait` +/// first among them. +public class InvocationTargetException extends Exception { + + private static final long serialVersionUID = 1L; + + private final Throwable target; + + protected InvocationTargetException() { + super((Throwable) null); + this.target = null; + } + + public InvocationTargetException(Throwable target) { + super((Throwable) null); + this.target = target; + } + + public InvocationTargetException(Throwable target, String s) { + super(s, null); + this.target = target; + } + + public Throwable getTargetException() { + return target; + } + + @Override + public Throwable getCause() { + return target; + } +} diff --git a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FileReader.java b/Ports/CLDC11/src/java/net/MalformedURLException.java similarity index 66% rename from maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FileReader.java rename to Ports/CLDC11/src/java/net/MalformedURLException.java index 7a1eb72a7ee..26ebc362f97 100644 --- a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FileReader.java +++ b/Ports/CLDC11/src/java/net/MalformedURLException.java @@ -20,21 +20,21 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.androidcompat.jdk; +package java.net; -import java.io.FileNotFoundException; -import java.io.InputStreamReader; -import java.io.UnsupportedEncodingException; +import java.io.IOException; -/// `java.io.FileReader` for the Codename One runtime. Reads UTF-8, Android's -/// default charset. -public class FileReader extends InputStreamReader { +/// A string +/// could not be parsed as a URL, or names a protocol nothing handles. +public class MalformedURLException extends IOException { - public FileReader(String fileName) throws FileNotFoundException, UnsupportedEncodingException { - super(new FileInputStream(fileName), "UTF-8"); + private static final long serialVersionUID = 1L; + + public MalformedURLException() { + super(); } - public FileReader(File file) throws FileNotFoundException, UnsupportedEncodingException { - super(new FileInputStream(file), "UTF-8"); + public MalformedURLException(String msg) { + super(msg); } } diff --git a/Ports/CLDC11/src/java/net/URI.java b/Ports/CLDC11/src/java/net/URI.java index ce424b8efd2..248417ebb81 100644 --- a/Ports/CLDC11/src/java/net/URI.java +++ b/Ports/CLDC11/src/java/net/URI.java @@ -443,6 +443,9 @@ protected void parseSchemeSpecificPart(String ssp, boolean encode) throws URISyn if (ssp == null) { throw new URISyntaxException(ssp, "Invalid scheme specific part"); } + if (scheme != null && ssp.length() == 0) { + throw new URISyntaxException(scheme + SCHEME_SEPARATOR, "Expected scheme-specific part"); + } if (scheme != null && ssp.charAt(0) != PATH_SEPARATOR) { this.opaque = true; this.schemeSpecificPart = ssp; @@ -459,8 +462,15 @@ protected void parseSchemeSpecificPart(String ssp, boolean encode) throws URISyn s = s.substring(0, index); } - index = (s.startsWith(AUTHORITY_MARKER)) ? 2 : 0; - index = s.indexOf(PATH_SEPARATOR, index); + // Only a "//" opens an authority; without one the remainder is all + // path (see vm/JavaAPI's URI, which this mirrors). + if (!s.startsWith(AUTHORITY_MARKER)) { + if (s.length() > 0) { + setPath(s, encode); + } + return; + } + index = s.indexOf(PATH_SEPARATOR, AUTHORITY_MARKER.length()); if (index != -1) { setPath(s.substring(index), encode); s = s.substring(0, index); diff --git a/Ports/CLDC11/src/java/net/URL.java b/Ports/CLDC11/src/java/net/URL.java new file mode 100644 index 00000000000..7aa1ce27997 --- /dev/null +++ b/Ports/CLDC11/src/java/net/URL.java @@ -0,0 +1,736 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.net; + +import com.codename1.impl.JdkHost; + +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +/// A parsed URL, and a way to read what it points at. +/// +/// Parsing follows the JDK class, including the resolution of a relative +/// reference against a context URL. The protocols accepted are `http`, +/// `https`, `file`, `ftp`, `jar`, `mailto` and `cn1res`; any other throws +/// `MalformedURLException`, as an unknown protocol does on the JDK. +/// +/// #### Reading +/// +/// [#openStream()] reads: +/// +/// - `file:` through `java.io.FileInputStream`. +/// - `http:` and `https:` through the platform's network, which +/// `com.codename1.impl.JdkHost` reaches (a Codename One `ConnectionRequest` +/// in an application), run to completion before the stream is returned: +/// the stream is over the whole downloaded body, so it suits configuration +/// files and images, not endless or very large responses. +/// - `cn1res:` from the application's packaged resources, through the same +/// host. +/// +/// #### The `cn1res` scheme +/// +/// `cn1res:/images/logo.png` names the resource `/images/logo.png` of the +/// application, the one `Class.getResourceAsStream("/images/logo.png")` +/// opens. The scheme exists because the device has no `Class.getResource`: +/// a later step of the build rewrites calls to `Class.getResource(String)` so +/// that they return a URL of this scheme, and code that goes on to call +/// `openStream()`, `toString()` or `toExternalForm()` on the result keeps +/// working. A `cn1res` URL has no host; its path is the absolute resource +/// name. +/// +/// #### Differences from the JDK +/// +/// - No `openConnection`, `getContent` or stream handler factories: the +/// device has no `URLConnection`. +/// - `equals` and `hashCode` compare host names as text, ignoring case, and +/// never resolve them. +public final class URL implements java.io.Serializable { + + private static final long serialVersionUID = 1L; + + private final String protocol; + private final String host; + private final int port; + private final String authority; + private final String userInfo; + private final String path; + private final String query; + private final String ref; + + public URL(String spec) throws MalformedURLException { + this(null, spec); + } + + public URL(String protocol, String host, String file) throws MalformedURLException { + this(protocol, host, -1, file); + } + + public URL(String protocol, String host, int port, String file) throws MalformedURLException { + String scheme = asciiLower(protocol); + if (!isKnownProtocol(scheme)) { + throw new MalformedURLException("unknown protocol: " + protocol); + } + this.protocol = scheme; + if (host != null) { + String h = host; + if (h.indexOf(':') >= 0 && !h.startsWith("[")) { + h = "[" + h + "]"; + } + if (port < -1) { + throw new MalformedURLException("Invalid port number :" + port); + } + this.host = h; + this.port = port; + this.authority = port == -1 ? h : h + ":" + port; + } else { + this.host = null; + this.port = -1; + this.authority = null; + } + this.userInfo = null; + String rest = file; + int hash = rest.indexOf('#'); + this.ref = hash < 0 ? null : rest.substring(hash + 1); + if (hash >= 0) { + rest = rest.substring(0, hash); + } + int question = rest.lastIndexOf('?'); + if (question >= 0) { + this.query = rest.substring(question + 1); + this.path = rest.substring(0, question); + } else { + this.query = null; + this.path = rest; + } + } + + /// Parses `spec`, resolving it against `context` when it is a relative + /// reference; `context` may be `null`. + public URL(URL context, String spec) throws MalformedURLException { + if (spec == null) { + throw new MalformedURLException("no protocol: null"); + } + int limit = spec.length(); + while (limit > 0 && spec.charAt(limit - 1) <= ' ') { + limit--; + } + int start = 0; + while (start < limit && spec.charAt(start) <= ' ') { + start++; + } + if (spec.regionMatches(true, start, "url:", 0, 4)) { + start += 4; + } + boolean isRef = start < spec.length() && spec.charAt(start) == '#'; + String newProtocol = null; + for (int i = start; !isRef && i < limit; i++) { + char c = spec.charAt(i); + if (c == '/') { + break; + } + if (c == ':') { + String s = asciiLower(spec.substring(start, i)); + if (isValidProtocol(s)) { + newProtocol = s; + start = i + 1; + } + break; + } + } + + String p = newProtocol; + String h = null; + int prt = -1; + String auth = null; + String user = null; + String pth = null; + String qry = null; + String rf = null; + boolean isRelative = false; + if (context != null && (newProtocol == null || newProtocol.equals(context.protocol))) { + // A scheme repeated in front of a relative reference to a + // hierarchical URL is ignored. + if (context.path != null && context.path.startsWith("/")) { + newProtocol = null; + } + if (newProtocol == null) { + p = context.protocol; + auth = context.authority; + user = context.userInfo; + h = context.host; + prt = context.port; + pth = context.path; + isRelative = true; + } + } + if (p == null) { + throw new MalformedURLException("no protocol: " + spec); + } + if (!isKnownProtocol(p)) { + throw new MalformedURLException("unknown protocol: " + p); + } + int hash = spec.indexOf('#', start); + if (hash >= 0) { + rf = spec.substring(hash + 1, limit); + limit = hash; + } + if (isRelative && start == limit) { + // An empty reference is the context itself. + qry = context.query; + if (rf == null) { + rf = context.ref; + } + } + + if ("jar".equals(p) || "mailto".equals(p)) { + // Not hierarchical: everything after the scheme is the file. + String file; + if ("mailto".equals(p)) { + file = spec.substring(start, limit); + if (file.trim().length() == 0) { + throw new MalformedURLException("No email address"); + } + } else if (hash == start && isRelative) { + file = context.getFile(); + } else { + file = jarFile(isRelative ? context.getFile() : null, spec.substring(start, limit), + newProtocol != null); + } + if ("mailto".equals(p)) { + // As in the JDK, an address has no fragment. + rf = null; + } + int question = file.lastIndexOf('?'); + this.protocol = p; + this.host = ""; + this.port = -1; + this.authority = null; + this.userInfo = null; + this.path = question < 0 ? file : file.substring(0, question); + this.query = question < 0 ? null : file.substring(question + 1); + this.ref = rf; + return; + } + + // From here on: the authority, then the path and the query. + String s = spec; + boolean isRelPath = false; + boolean queryOnly = false; + if (start < limit) { + int queryStart = s.indexOf('?'); + queryOnly = queryStart == start; + if (queryStart != -1 && queryStart < limit) { + qry = s.substring(queryStart + 1, limit); + limit = queryStart; + s = s.substring(0, queryStart); + } + } + boolean fourSlashes = start <= limit - 4 && s.charAt(start) == '/' && s.charAt(start + 1) == '/' + && s.charAt(start + 2) == '/' && s.charAt(start + 3) == '/'; + if (!fourSlashes && start <= limit - 2 && s.charAt(start) == '/' && s.charAt(start + 1) == '/') { + start += 2; + int end = s.indexOf('/', start); + if (end < 0 || end > limit) { + end = s.indexOf('?', start); + if (end < 0 || end > limit) { + end = limit; + } + } + auth = s.substring(start, end); + h = auth; + int at = auth.indexOf('@'); + if (at != -1) { + if (at != auth.lastIndexOf('@')) { + user = null; + h = null; + } else { + user = auth.substring(0, at); + h = auth.substring(at + 1); + } + } else { + user = null; + } + prt = -1; + if (h != null) { + String portText = null; + if (h.length() > 0 && h.charAt(0) == '[') { + int close = h.indexOf(']'); + if (close <= 2) { + throw new MalformedURLException("Invalid authority field: " + auth); + } + if (h.length() > close + 1) { + if (h.charAt(close + 1) != ':') { + throw new MalformedURLException("Invalid authority field: " + auth); + } + portText = h.substring(close + 2); + } + h = h.substring(0, close + 1); + } else { + int colon = h.indexOf(':'); + if (colon >= 0) { + portText = h.substring(colon + 1); + h = h.substring(0, colon); + } + } + if (portText != null && portText.length() > 0) { + prt = parsePort(portText); + } + } else { + h = ""; + } + start = end; + if (auth.length() > 0) { + // With an authority, the path comes from the reference alone. + pth = ""; + } + } + if (h == null) { + h = ""; + } + if (start < limit) { + if (s.charAt(start) == '/') { + pth = s.substring(start, limit); + } else if (pth != null && pth.length() > 0) { + isRelPath = true; + int slash = pth.lastIndexOf('/'); + String separator = slash == -1 && auth != null ? "/" : ""; + pth = pth.substring(0, slash + 1) + separator + s.substring(start, limit); + } else { + String separator = auth != null ? "/" : ""; + pth = separator + s.substring(start, limit); + } + } else if (queryOnly && pth != null) { + int slash = pth.lastIndexOf('/'); + if (slash < 0) { + slash = 0; + } + pth = pth.substring(0, slash) + "/"; + } + if (pth == null) { + pth = ""; + } + if (isRelPath) { + pth = removeDots(pth); + } + this.protocol = p; + this.host = h; + this.port = prt; + this.authority = auth; + this.userInfo = user; + this.path = pth; + this.query = qry; + this.ref = rf; + } + + /// The index of the `/` of the last `!/` in a `jar:` file, or -1. + private static int indexOfBangSlash(String file) { + int bang = file.lastIndexOf('!'); + while (bang >= 0) { + if (bang != file.length() - 1 && file.charAt(bang + 1) == '/') { + return bang + 1; + } + bang = bang == 0 ? -1 : file.lastIndexOf('!', bang - 1); + } + return -1; + } + + /// The file of a `jar:` URL: an archive's own URL, `!/`, and the entry. + /// A relative reference is resolved inside the archive, never out of it. + private static String jarFile(String contextFile, String part, boolean absolute) + throws MalformedURLException { + if (absolute) { + int bangSlash = indexOfBangSlash(part); + if (bangSlash < 0) { + throw new MalformedURLException("no !/ in spec"); + } + try { + new URL(part.substring(0, bangSlash - 1)); + } catch (MalformedURLException e) { + throw new MalformedURLException("invalid url: " + part + " (" + e.getMessage() + ")"); + } + return part; + } + if (contextFile == null) { + throw new MalformedURLException("no !/ in spec"); + } + String file = contextFile; + if (part.startsWith("/")) { + int bangSlash = indexOfBangSlash(file); + if (bangSlash < 0) { + throw new MalformedURLException("malformed context url: no !/"); + } + file = file.substring(0, bangSlash); + } else if (!file.endsWith("/")) { + int lastSlash = file.lastIndexOf('/'); + if (lastSlash < 0) { + throw new MalformedURLException("malformed context url"); + } + file = file.substring(0, lastSlash + 1); + } + file = file + part; + int bangSlash = indexOfBangSlash(file); + if (bangSlash < 0) { + throw new MalformedURLException("no !/ in spec"); + } + return file.substring(0, bangSlash) + canonizeEntry(file.substring(bangSlash)); + } + + /// Resolves `.` and `..` in the entry part of a `jar:` file. + private static String canonizeEntry(String entry) { + String file = entry; + int i = file.indexOf("/../"); + while (i >= 0) { + int lim = i > 0 ? file.lastIndexOf('/', i - 1) : -1; + file = lim >= 0 ? file.substring(0, lim) + file.substring(i + 3) : file.substring(i + 3); + i = file.indexOf("/../"); + } + i = file.indexOf("/./"); + while (i >= 0) { + file = file.substring(0, i) + file.substring(i + 2); + i = file.indexOf("/./"); + } + while (file.endsWith("/..")) { + i = file.indexOf("/.."); + int lim = i > 0 ? file.lastIndexOf('/', i - 1) : -1; + file = lim >= 0 ? file.substring(0, lim + 1) : file.substring(0, i); + } + if (file.endsWith("/.")) { + file = file.substring(0, file.length() - 1); + } + return file; + } + + /// A port as the JDK reads one: a decimal `int` with an optional sign, + /// of which -1, "no port", is the only negative accepted. + private static int parsePort(String text) throws MalformedURLException { + int from = 0; + boolean negative = false; + if (text.charAt(0) == '-' || text.charAt(0) == '+') { + negative = text.charAt(0) == '-'; + from = 1; + } + long value = 0; + if (text.length() == from || text.length() - from > 10) { + throw new MalformedURLException("For input string: \"" + text + "\""); + } + for (int i = from; i < text.length(); i++) { + char c = text.charAt(i); + if (c < '0' || c > '9') { + throw new MalformedURLException("For input string: \"" + text + "\""); + } + value = value * 10 + (c - '0'); + } + if (negative) { + value = -value; + } + if (value > Integer.MAX_VALUE || value < Integer.MIN_VALUE) { + throw new MalformedURLException("For input string: \"" + text + "\""); + } + if (value < -1) { + throw new MalformedURLException("Invalid port number :" + value); + } + return (int) value; + } + + /// Resolves the `.` and `..` segments a relative reference brought into + /// a path. + private static String removeDots(String in) { + String path = in; + int i = path.indexOf("/./"); + while (i >= 0) { + path = path.substring(0, i) + path.substring(i + 2); + i = path.indexOf("/./"); + } + i = path.indexOf("/../"); + while (i >= 0) { + // "/a/b/../c" becomes "/a/c"; "/../../a" has nothing to cancel + // and stays. + int previous = i > 0 ? path.lastIndexOf('/', i - 1) : -1; + if (previous >= 0 && path.indexOf("/../", previous) != 0) { + path = path.substring(0, previous) + path.substring(i + 3); + i = path.indexOf("/../"); + } else { + i = path.indexOf("/../", i + 3); + } + } + while (path.endsWith("/..")) { + i = path.indexOf("/.."); + int previous = i > 0 ? path.lastIndexOf('/', i - 1) : -1; + if (previous < 0) { + break; + } + path = path.substring(0, previous + 1); + } + if (path.startsWith("./") && path.length() > 2) { + path = path.substring(2); + } + if (path.endsWith("/.")) { + path = path.substring(0, path.length() - 1); + } + return path; + } + + /// Lower case for the ASCII letters only. A scheme is ASCII by + /// specification, and `String.toLowerCase()` follows the device's locale. + private static String asciiLower(String s) { + StringBuilder sb = null; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c >= 'A' && c <= 'Z') { + if (sb == null) { + sb = new StringBuilder(s); + } + sb.setCharAt(i, (char) (c + ('a' - 'A'))); + } + } + return sb == null ? s : sb.toString(); + } + + private static boolean isAsciiLetter(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); + } + + private static boolean isValidProtocol(String protocol) { + int len = protocol.length(); + if (len < 1 || !isAsciiLetter(protocol.charAt(0))) { + return false; + } + for (int i = 1; i < len; i++) { + char c = protocol.charAt(i); + boolean digit = c >= '0' && c <= '9'; + if (!isAsciiLetter(c) && !digit && c != '.' && c != '+' && c != '-') { + return false; + } + } + return true; + } + + private static boolean isKnownProtocol(String protocol) { + return "http".equals(protocol) || "https".equals(protocol) || "file".equals(protocol) + || "cn1res".equals(protocol) || "ftp".equals(protocol) || "jar".equals(protocol) + || "mailto".equals(protocol); + } + + public String getProtocol() { + return protocol; + } + + public String getHost() { + return host; + } + + public int getPort() { + return port; + } + + public int getDefaultPort() { + if ("http".equals(protocol)) { + return 80; + } + if ("https".equals(protocol)) { + return 443; + } + if ("ftp".equals(protocol)) { + return 21; + } + return -1; + } + + public String getAuthority() { + return authority; + } + + public String getUserInfo() { + return userInfo; + } + + public String getPath() { + return path; + } + + public String getQuery() { + return query; + } + + public String getRef() { + return ref; + } + + public String getFile() { + return query == null ? path : path + "?" + query; + } + + public String toExternalForm() { + StringBuilder sb = new StringBuilder(); + sb.append(protocol).append(':'); + if (authority != null && authority.length() > 0) { + sb.append("//").append(authority); + } + if (path != null) { + sb.append(path); + } + if (query != null) { + sb.append('?').append(query); + } + if (ref != null) { + sb.append('#').append(ref); + } + return sb.toString(); + } + + @Override + public String toString() { + return toExternalForm(); + } + + public URI toURI() throws URISyntaxException { + return new URI(toString()); + } + + private static boolean same(String a, String b) { + return a == null ? b == null : a.equals(b); + } + + private int effectivePort() { + return port != -1 ? port : getDefaultPort(); + } + + /// Whether the two URLs name the same resource, the fragment aside. + public boolean sameFile(URL other) { + if (other == null) { + return false; + } + boolean hostsEqual = host == null || other.host == null + ? host == null && other.host == null + : host.equalsIgnoreCase(other.host); + return protocol.equals(other.protocol) && same(getFile(), other.getFile()) + && effectivePort() == other.effectivePort() && hostsEqual; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof URL)) { + return false; + } + URL other = (URL) obj; + return same(ref, other.ref) && sameFile(other); + } + + @Override + public int hashCode() { + int h = protocol.hashCode(); + if (host != null) { + h += asciiLower(host).hashCode(); + } + String file = getFile(); + if (file != null) { + h += file.hashCode(); + } + h += effectivePort(); + if (ref != null) { + h += ref.hashCode(); + } + return h; + } + + private static int hex(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'a' && c <= 'f') { + return c - 'a' + 10; + } + if (c >= 'A' && c <= 'F') { + return c - 'A' + 10; + } + return -1; + } + + /// Replaces each `%XX` by the byte it stands for and reads the result + /// as UTF-8. A `%` not followed by two hex digits is kept. + private static String percentDecode(String s) throws IOException { + if (s.indexOf('%') < 0) { + return s; + } + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + int i = 0; + while (i < s.length()) { + if (s.charAt(i) == '%' && i + 2 < s.length() && hex(s.charAt(i + 1)) >= 0 + && hex(s.charAt(i + 2)) >= 0) { + bytes.write((hex(s.charAt(i + 1)) << 4) | hex(s.charAt(i + 2))); + i += 3; + } else { + // Up to the next escape in one piece, so that a surrogate + // pair is encoded as the one character it is. + int next = s.indexOf('%', i + 1); + if (next < 0) { + next = s.length(); + } + bytes.write(s.substring(i, next).getBytes("UTF-8")); + i = next; + } + } + return new String(bytes.toByteArray(), "UTF-8"); + } + + /// Opens the resource for reading; see the class description for what + /// each protocol reads. + /// + /// #### Throws + /// + /// - `FileNotFoundException`: if the file or resource does not exist, or + /// the server answers 404 or 410 + /// + /// - `IOException`: if the request fails, the server answers another + /// error status, or the protocol cannot be read on the device + public InputStream openStream() throws IOException { + if ("file".equals(protocol)) { + throw new RuntimeException("Compile-time stub"); + } + if ("cn1res".equals(protocol)) { + // Through the host, which knows the flat name the build shipped + // the resource under; the path here is the one it had on a desktop. + String name = percentDecode(path); + InputStream in = JdkHost.get().resource(name.startsWith("/") ? name.substring(1) : name); + if (in == null) { + throw new FileNotFoundException(toExternalForm()); + } + return in; + } + if ("http".equals(protocol) || "https".equals(protocol)) { + return openHttp(); + } + throw new IOException("Cannot read the " + protocol + " protocol: " + toExternalForm()); + } + + private InputStream openHttp() throws IOException { + StringBuilder address = new StringBuilder(); + address.append(protocol).append("://").append(authority == null ? "" : authority).append(path); + if (query != null) { + address.append('?').append(query); + } + return JdkHost.get().openUrl(address.toString()); + } +} diff --git a/Ports/CLDC11/src/java/net/URLDecoder.java b/Ports/CLDC11/src/java/net/URLDecoder.java new file mode 100644 index 00000000000..d65cea4ddc9 --- /dev/null +++ b/Ports/CLDC11/src/java/net/URLDecoder.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.net; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; + +/// A string out of its +/// `application/x-www-form-urlencoded` form. A plus is a space and a run of +/// `%XX` is the bytes of some characters. +public final class URLDecoder { + + private URLDecoder() { + } + + /// Decodes as UTF-8, where the JDK uses the platform's default. + public static String decode(String s) { + return decodeAs(s, "UTF-8"); + } + + public static String decode(String s, String enc) throws UnsupportedEncodingException { + if (enc == null) { + throw new NullPointerException("charsetName"); + } + if (enc.length() == 0) { + throw new UnsupportedEncodingException("URLDecoder: empty string enc parameter"); + } + return decodeAs(s, URLEncoder.charsetName(enc)); + } + + public static String decode(String s, Charset charset) { + return decodeAs(s, charset.name()); + } + + private static int hex(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'a' && c <= 'f') { + return c - 'a' + 10; + } + if (c >= 'A' && c <= 'F') { + return c - 'A' + 10; + } + return -1; + } + + private static String decodeAs(String s, String charset) { + int n = s.length(); + StringBuilder out = new StringBuilder(n); + byte[] bytes = null; + int i = 0; + while (i < n) { + char c = s.charAt(i); + if (c == '+') { + out.append(' '); + i++; + } else if (c == '%') { + if (bytes == null) { + bytes = new byte[(n - i) / 3]; + } + int count = 0; + while (i + 2 < n && c == '%') { + int high = hex(s.charAt(i + 1)); + int low = hex(s.charAt(i + 2)); + if (high < 0 || low < 0) { + throw new IllegalArgumentException( + "URLDecoder: Illegal hex characters in escape (%) pattern - Error at index " + + (high < 0 ? 0 : 1) + " in: \"" + s.substring(i + 1, i + 3) + "\""); + } + bytes[count++] = (byte) (high * 16 + low); + i += 3; + if (i < n) { + c = s.charAt(i); + } + } + if (i < n && c == '%') { + throw new IllegalArgumentException("URLDecoder: Incomplete trailing escape (%) pattern"); + } + try { + out.append(new String(bytes, 0, count, charset)); + } catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException(charset); + } + } else { + out.append(c); + i++; + } + } + return out.toString(); + } +} diff --git a/Ports/CLDC11/src/java/net/URLEncoder.java b/Ports/CLDC11/src/java/net/URLEncoder.java new file mode 100644 index 00000000000..3d65ea379b5 --- /dev/null +++ b/Ports/CLDC11/src/java/net/URLEncoder.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.net; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; + +/// The `application/x-www-form-urlencoded` form of a +/// string. Letters, digits and `.-*_` stay, a space is a plus, and every +/// other character is the bytes of its encoding as `%XX`. +public final class URLEncoder { + private static final String HEX = "0123456789ABCDEF"; + + private URLEncoder() { + } + + static String charsetName(String enc) throws UnsupportedEncodingException { + if (enc == null) { + throw new NullPointerException("charsetName"); + } + try { + return Charset.forName(enc).name(); + } catch (IllegalArgumentException e) { + throw new UnsupportedEncodingException(enc); + } + } + + private static boolean plain(char c) { + return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' + || c == '.' || c == '-' || c == '*' || c == '_'; + } + + /// Encodes in UTF-8, where the JDK uses the platform's default. + public static String encode(String s) { + return encodeAs(s, "UTF-8"); + } + + public static String encode(String s, String enc) throws UnsupportedEncodingException { + return encodeAs(s, charsetName(enc)); + } + + public static String encode(String s, Charset charset) { + return encodeAs(s, charset.name()); + } + + private static String encodeAs(String s, String charset) { + int n = s.length(); + StringBuilder out = new StringBuilder(n + 16); + int i = 0; + while (i < n) { + char c = s.charAt(i); + if (plain(c)) { + out.append(c); + i++; + } else if (c == ' ') { + out.append('+'); + i++; + } else { + // The run of characters to escape is encoded as one piece, + // so the two halves of a surrogate pair stay together. + int start = i; + while (i < n && !plain(s.charAt(i)) && s.charAt(i) != ' ') { + i++; + } + byte[] bytes; + try { + bytes = s.substring(start, i).getBytes(charset); + } catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException(charset); + } + for (int b = 0; b < bytes.length; b++) { + out.append('%').append(HEX.charAt((bytes[b] >> 4) & 0xF)).append(HEX.charAt(bytes[b] & 0xF)); + } + } + } + return out.toString(); + } +} diff --git a/Ports/CLDC11/src/java/nio/charset/Charset.java b/Ports/CLDC11/src/java/nio/charset/Charset.java index c851a1c5eff..a10e1024f78 100644 --- a/Ports/CLDC11/src/java/nio/charset/Charset.java +++ b/Ports/CLDC11/src/java/nio/charset/Charset.java @@ -22,28 +22,192 @@ */ package java.nio.charset; -/// Added this for Kotlin +import java.util.Collections; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +/// A named character encoding. This runtime has the three charsets every +/// Java platform must: UTF-8, US-ASCII and ISO-8859-1, under their canonical +/// names and the JDK's aliases. Encoding and decoding go through the +/// String constructors and `getBytes` that take a Charset, and the readers +/// and writers of `java.io`. public class Charset implements Comparable { - private String name; + private static final String[] UTF8_ALIASES = {"unicode-1-1-utf-8", "UTF8"}; + private static final String[] USASCII_ALIASES = { + "iso-ir-6", "ANSI_X3.4-1986", "ISO_646.irv:1991", "ASCII", "iso_646.irv:1983", "ANSI_X3.4-1968", + "646", "default", "cp367", "csASCII", "us", "ISO646-US", "IBM367", "ascii7" + }; + private static final String[] ISO88591_ALIASES = { + "819", "ISO8859-1", "l1", "ISO_8859-1:1987", "ISO_8859-1", "8859_1", "iso-ir-100", "latin1", + "cp819", "ISO8859_1", "IBM819", "ISO_8859_1", "IBM-819", "csISOLatin1" + }; + private static final Charset UTF8 = new SimpleCharset("UTF-8", UTF8_ALIASES); + private static final Charset USASCII = new SimpleCharset("US-ASCII", USASCII_ALIASES); + private static final Charset ISO88591 = new SimpleCharset("ISO-8859-1", ISO88591_ALIASES); + + private final String name; + private final String[] aliases; + protected Charset(String canonicalName, String[] aliases) { + checkName(canonicalName); name = canonicalName; + this.aliases = aliases == null ? new String[0] : aliases; } - - public int compareTo(Charset another) { - return name.compareTo(another.name); + + /// Compares canonical names, ignoring case. + public final int compareTo(Charset another) { + return name.compareToIgnoreCase(another.name); } - + + /// The canonical name. public final String name() { return name; } + /// The canonical name. public String displayName() { return name; } - - public static Charset forName(String name) { - throw new UnsupportedOperationException("Charset.forName not implemented on this platform"); + + /// The canonical name, whatever the locale. + public String displayName(Locale locale) { + return name; + } + + /// The aliases of the charset. + public final Set aliases() { + HashSet out = new HashSet(); + for (int i = 0; i < aliases.length; i++) { + out.add(aliases[i]); + } + return Collections.unmodifiableSet(out); + } + + /// Whether the charset is registered with IANA; the three this runtime + /// has are. + public final boolean isRegistered() { + return true; + } + + /// Whether the charset can encode; the three this runtime has can. + public boolean canEncode() { + return true; + } + + /// Whether `obj` is a charset of the same canonical name. + public final boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Charset)) { + return false; + } + return name.equals(((Charset) obj).name); + } + + /// The hash of the canonical name. + public final int hashCode() { + return name.hashCode(); + } + + /// The canonical name. + public final String toString() { + return name; + } + + /// UTF-8, the default charset of this runtime on every platform. + public static Charset defaultCharset() { + return UTF8; + } + + /// Whether `charsetName` names a charset this runtime has. + /// + /// #### Throws + /// + /// - `IllegalCharsetNameException`: if the name is not a legal charset name + /// - `IllegalArgumentException`: if it is null + public static boolean isSupported(String charsetName) { + return lookup(charsetName) != null; + } + + /// The charset `charsetName` names, by canonical name or alias, in any + /// case. + /// + /// #### Throws + /// + /// - `IllegalCharsetNameException`: if the name is not a legal charset name + /// - `UnsupportedCharsetException`: if this runtime has no such charset + /// - `IllegalArgumentException`: if it is null + public static Charset forName(String charsetName) { + Charset cs = lookup(charsetName); + if (cs == null) { + throw new UnsupportedCharsetException(charsetName); + } + return cs; + } + + /// The charset `charsetName` names, or `fallback` when the name is + /// illegal or names a charset this runtime does not have. + public static Charset forName(String charsetName, Charset fallback) { + try { + Charset cs = lookup(charsetName); + return cs == null ? fallback : cs; + } catch (IllegalCharsetNameException e) { + return fallback; + } + } + + private static Charset lookup(String charsetName) { + if (charsetName == null) { + throw new IllegalArgumentException("Null charset name"); + } + checkName(charsetName); + if (matches(UTF8, charsetName)) { + return UTF8; + } + if (matches(ISO88591, charsetName)) { + return ISO88591; + } + if (matches(USASCII, charsetName)) { + return USASCII; + } + return null; + } + + // Charset names are ASCII; equalsIgnoreCase folds them without the + // device's locale. + private static boolean matches(Charset cs, String charsetName) { + if (cs.name.equalsIgnoreCase(charsetName)) { + return true; + } + for (int i = 0; i < cs.aliases.length; i++) { + if (cs.aliases[i].equalsIgnoreCase(charsetName)) { + return true; + } + } + return false; + } + + private static void checkName(String s) { + int n = s.length(); + if (n == 0) { + throw new IllegalCharsetNameException(s); + } + for (int i = 0; i < n; i++) { + char c = s.charAt(i); + boolean ok = c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || c >= '0' && c <= '9' + || i > 0 && (c == '-' || c == '+' || c == ':' || c == '_' || c == '.'); + if (!ok) { + throw new IllegalCharsetNameException(s); + } + } + } + + private static final class SimpleCharset extends Charset { + private SimpleCharset(String canonicalName, String[] aliases) { + super(canonicalName, aliases); + } } - } diff --git a/Ports/CLDC11/src/java/nio/charset/IllegalCharsetNameException.java b/Ports/CLDC11/src/java/nio/charset/IllegalCharsetNameException.java new file mode 100644 index 00000000000..34c7a73e179 --- /dev/null +++ b/Ports/CLDC11/src/java/nio/charset/IllegalCharsetNameException.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +package java.nio.charset; + +/// Thrown for a string that is not a legal charset name. +public class IllegalCharsetNameException extends IllegalArgumentException { + + private final String charsetName; + + /// An exception naming `charsetName`. + public IllegalCharsetNameException(String charsetName) { + super(String.valueOf(charsetName)); + this.charsetName = charsetName; + } + + /// The charset name this exception is about. + public String getCharsetName() { + return charsetName; + } +} diff --git a/Ports/CLDC11/src/java/nio/charset/StandardCharsets.java b/Ports/CLDC11/src/java/nio/charset/StandardCharsets.java index 8b094883b05..0d98f66ddc1 100644 --- a/Ports/CLDC11/src/java/nio/charset/StandardCharsets.java +++ b/Ports/CLDC11/src/java/nio/charset/StandardCharsets.java @@ -6,6 +6,19 @@ * published by the Free Software Foundation. Codename One designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. */ package java.nio.charset; @@ -14,7 +27,7 @@ public final class StandardCharsets { private StandardCharsets() { } - public static final Charset UTF_8 = new Charset("UTF-8", new String[0]); - public static final Charset US_ASCII = new Charset("US-ASCII", new String[0]); - public static final Charset ISO_8859_1 = new Charset("ISO-8859-1", new String[0]); + public static final Charset UTF_8 = Charset.forName("UTF-8"); + public static final Charset US_ASCII = Charset.forName("US-ASCII"); + public static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1"); } diff --git a/Ports/CLDC11/src/java/nio/charset/UnsupportedCharsetException.java b/Ports/CLDC11/src/java/nio/charset/UnsupportedCharsetException.java new file mode 100644 index 00000000000..2b0016396df --- /dev/null +++ b/Ports/CLDC11/src/java/nio/charset/UnsupportedCharsetException.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +package java.nio.charset; + +/// Thrown for a legal charset name this runtime has no charset for. +public class UnsupportedCharsetException extends IllegalArgumentException { + + private final String charsetName; + + /// An exception naming `charsetName`. + public UnsupportedCharsetException(String charsetName) { + super(String.valueOf(charsetName)); + this.charsetName = charsetName; + } + + /// The charset name this exception is about. + public String getCharsetName() { + return charsetName; + } +} diff --git a/Ports/CLDC11/src/java/nio/file/AccessDeniedException.java b/Ports/CLDC11/src/java/nio/file/AccessDeniedException.java new file mode 100644 index 00000000000..4982149cd2c --- /dev/null +++ b/Ports/CLDC11/src/java/nio/file/AccessDeniedException.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.nio.file; + +/// The file system refused the operation. +public class AccessDeniedException extends FileSystemException { + private static final long serialVersionUID = 1L; + + public AccessDeniedException(String file) { + super(file); + } + + public AccessDeniedException(String file, String other, String reason) { + super(file, other, reason); + } +} diff --git a/Ports/CLDC11/src/java/nio/file/CopyOption.java b/Ports/CLDC11/src/java/nio/file/CopyOption.java new file mode 100644 index 00000000000..22912454d71 --- /dev/null +++ b/Ports/CLDC11/src/java/nio/file/CopyOption.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.nio.file; + +/// How a file is +/// copied or moved. [StandardCopyOption] has the values. +public interface CopyOption { +} diff --git a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FileWriter.java b/Ports/CLDC11/src/java/nio/file/DirectoryIteratorException.java similarity index 62% rename from maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FileWriter.java rename to Ports/CLDC11/src/java/nio/file/DirectoryIteratorException.java index 351a499d050..e78d5f72b8c 100644 --- a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FileWriter.java +++ b/Ports/CLDC11/src/java/nio/file/DirectoryIteratorException.java @@ -20,28 +20,30 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.androidcompat.jdk; +package java.nio.file; import java.io.IOException; -import java.io.OutputStreamWriter; -/// `java.io.FileWriter` for the Codename One runtime. Writes UTF-8, Android's -/// default charset. -public class FileWriter extends OutputStreamWriter { +/// An `IOException` met while iterating a directory, which an iterator +/// cannot throw as it is. +public final class DirectoryIteratorException extends RuntimeException { + private static final long serialVersionUID = 1L; + private final IOException cause; - public FileWriter(String fileName) throws IOException { - super(new FileOutputStream(fileName), "UTF-8"); + public DirectoryIteratorException(IOException cause) { + super(text(cause)); + this.cause = cause; } - public FileWriter(String fileName, boolean append) throws IOException { - super(new FileOutputStream(fileName, append), "UTF-8"); + private static String text(IOException cause) { + if (cause == null) { + throw new NullPointerException(); + } + return cause.toString(); } - public FileWriter(File file) throws IOException { - super(new FileOutputStream(file), "UTF-8"); - } - - public FileWriter(File file, boolean append) throws IOException { - super(new FileOutputStream(file, append), "UTF-8"); + @Override + public IOException getCause() { + return cause; } } diff --git a/Ports/CLDC11/src/java/nio/file/DirectoryNotEmptyException.java b/Ports/CLDC11/src/java/nio/file/DirectoryNotEmptyException.java new file mode 100644 index 00000000000..de3cc35e360 --- /dev/null +++ b/Ports/CLDC11/src/java/nio/file/DirectoryNotEmptyException.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.nio.file; + +/// A directory that has entries cannot be deleted or replaced. +public class DirectoryNotEmptyException extends FileSystemException { + private static final long serialVersionUID = 1L; + + public DirectoryNotEmptyException(String file) { + super(file); + } +} diff --git a/Ports/CLDC11/src/java/nio/file/DirectoryStream.java b/Ports/CLDC11/src/java/nio/file/DirectoryStream.java new file mode 100644 index 00000000000..51353de80d3 --- /dev/null +++ b/Ports/CLDC11/src/java/nio/file/DirectoryStream.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.nio.file; + +import java.io.IOException; + +/// The entries +/// of a directory, read when the stream was opened. It can be iterated once. +public interface DirectoryStream extends AutoCloseable, Iterable { + + /// Decides which entries a directory stream answers. + interface Filter { + boolean accept(T entry) throws IOException; + } + + @Override + void close() throws IOException; +} diff --git a/Ports/CLDC11/src/java/nio/file/FileAlreadyExistsException.java b/Ports/CLDC11/src/java/nio/file/FileAlreadyExistsException.java new file mode 100644 index 00000000000..9c21b957266 --- /dev/null +++ b/Ports/CLDC11/src/java/nio/file/FileAlreadyExistsException.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.nio.file; + +/// Something is already at the path. +public class FileAlreadyExistsException extends FileSystemException { + private static final long serialVersionUID = 1L; + + public FileAlreadyExistsException(String file) { + super(file); + } + + public FileAlreadyExistsException(String file, String other, String reason) { + super(file, other, reason); + } +} diff --git a/Ports/CLDC11/src/java/nio/file/FileSystemException.java b/Ports/CLDC11/src/java/nio/file/FileSystemException.java new file mode 100644 index 00000000000..d44771bb04a --- /dev/null +++ b/Ports/CLDC11/src/java/nio/file/FileSystemException.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.nio.file; + +import java.io.IOException; + +/// An operation on a file failed; names the file, the other file of a copy +/// or move, and the reason. +public class FileSystemException extends IOException { + private static final long serialVersionUID = 1L; + private final String file; + private final String other; + private final String reason; + + public FileSystemException(String file) { + this(file, null, null); + } + + public FileSystemException(String file, String other, String reason) { + super((String) null); + this.file = file; + this.other = other; + this.reason = reason; + } + + public String getFile() { + return file; + } + + public String getOtherFile() { + return other; + } + + public String getReason() { + return reason; + } + + @Override + public String getMessage() { + if (file == null && other == null) { + return reason; + } + StringBuilder sb = new StringBuilder(); + if (file != null) { + sb.append(file); + } + if (other != null) { + sb.append(" -> ").append(other); + } + if (reason != null) { + sb.append(": ").append(reason); + } + return sb.toString(); + } +} diff --git a/Ports/CLDC11/src/java/nio/file/FileVisitOption.java b/Ports/CLDC11/src/java/nio/file/FileVisitOption.java new file mode 100644 index 00000000000..ebf4637b044 --- /dev/null +++ b/Ports/CLDC11/src/java/nio/file/FileVisitOption.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.nio.file; + +/// Accepted +/// and without effect: symbolic links are never followed. +public enum FileVisitOption { + FOLLOW_LINKS +} diff --git a/Ports/CLDC11/src/java/nio/file/FileVisitResult.java b/Ports/CLDC11/src/java/nio/file/FileVisitResult.java new file mode 100644 index 00000000000..8b575a51b35 --- /dev/null +++ b/Ports/CLDC11/src/java/nio/file/FileVisitResult.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.nio.file; + +/// What a +/// [FileVisitor] tells `Files.walkFileTree` to do next. +public enum FileVisitResult { + CONTINUE, + TERMINATE, + SKIP_SUBTREE, + SKIP_SIBLINGS +} diff --git a/Ports/CLDC11/src/java/nio/file/FileVisitor.java b/Ports/CLDC11/src/java/nio/file/FileVisitor.java new file mode 100644 index 00000000000..5ca1e9d5944 --- /dev/null +++ b/Ports/CLDC11/src/java/nio/file/FileVisitor.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.nio.file; + +import java.io.IOException; +import java.nio.file.attribute.BasicFileAttributes; + +/// What +/// `Files.walkFileTree` calls for each directory and file it reaches. +public interface FileVisitor { + + FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs) throws IOException; + + FileVisitResult visitFile(T file, BasicFileAttributes attrs) throws IOException; + + FileVisitResult visitFileFailed(T file, IOException exc) throws IOException; + + FileVisitResult postVisitDirectory(T dir, IOException exc) throws IOException; +} diff --git a/Ports/CLDC11/src/java/nio/file/Files.java b/Ports/CLDC11/src/java/nio/file/Files.java new file mode 100644 index 00000000000..f57e710cabe --- /dev/null +++ b/Ports/CLDC11/src/java/nio/file/Files.java @@ -0,0 +1,1087 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.nio.file; + +import com.codename1.impl.JdkHost; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.UncheckedIOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.FileTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.function.BiPredicate; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/// Operations on files, directories and their contents, over `java.io.File` +/// and the file streams, so over the platform's file system as +/// `com.codename1.impl.JdkHost` provides it. +/// +/// #### What differs from a desktop file system +/// +/// - A directory's entries come back sorted by name. The JDK promises no +/// order, and a device's file systems disagree with each other. +/// - `newDirectoryStream` reads the entries when it is called, not as the +/// result is consumed. +/// - Nothing is a symbolic link, and a `LinkOption` changes nothing. +/// - The device keeps one time per file. `getLastModifiedTime` answers it, +/// and so do the creation and access times of [BasicFileAttributes]. +/// - A file is executable when it is a directory, which is all "executable" +/// can mean where no program can be started. +/// - A temporary file goes to the caches directory where the device has +/// one, and to the application's home otherwise. Nothing deletes it. +/// - Text is UTF-8 unless a `Charset` says otherwise; undecodable bytes are +/// replaced rather than reported. +/// +/// #### What is not provided +/// +/// Channels (`newByteChannel`), POSIX permissions, owners, file stores, hard +/// and symbolic links, `probeContentType`, `setLastModifiedTime` and the +/// attribute views by name. A device has no equivalent, and a call to any of +/// them is a build error. +/// +/// `lines`, `list`, `walk` and `find` are lazy, as in the JDK: `lines` reads +/// the file as the stream asks for lines, and `walk` lists a directory only +/// when it is about to enter it, so `walk(root).limit(1)` lists nothing. A +/// failure found that late surfaces as an `UncheckedIOException`. Close the +/// stream `lines` answers (try-with-resources) to close the file. +public final class Files { + + private static final String LINE = "\n"; + + private Files() { + } + + private static File file(Path path) { + if (path == null) { + throw new NullPointerException(); + } + return path.toFile(); + } + + private static boolean has(Object[] options, Object option) { + if (options != null) { + for (Object o : options) { + if (o == null) { + throw new NullPointerException(); + } + if (o == option) { + return true; + } + } + } + return false; + } + + // ---- what is there ---- + + public static boolean exists(Path path, LinkOption... options) { + return file(path).exists(); + } + + public static boolean notExists(Path path, LinkOption... options) { + return !file(path).exists(); + } + + public static boolean isDirectory(Path path, LinkOption... options) { + return file(path).isDirectory(); + } + + public static boolean isRegularFile(Path path, LinkOption... options) { + return file(path).isFile(); + } + + public static boolean isSymbolicLink(Path path) { + file(path); + return false; + } + + public static boolean isReadable(Path path) { + return file(path).exists(); + } + + public static boolean isWritable(Path path) { + return file(path).exists(); + } + + public static boolean isExecutable(Path path) { + return file(path).isDirectory(); + } + + public static boolean isHidden(Path path) throws IOException { + return file(path).isHidden(); + } + + public static boolean isSameFile(Path path, Path path2) throws IOException { + if (path.equals(path2)) { + return true; + } + return path.toRealPath().equals(path2.toRealPath()); + } + + private static File existing(Path path) throws IOException { + File f = file(path); + if (!f.exists()) { + throw new NoSuchFileException(path.toString()); + } + return f; + } + + public static long size(Path path) throws IOException { + return existing(path).length(); + } + + public static FileTime getLastModifiedTime(Path path, LinkOption... options) throws IOException { + return FileTime.fromMillis(existing(path).lastModified()); + } + + /// The attributes of `path`. `type` has to be `BasicFileAttributes.class`, + /// the one set a device has. + @SuppressWarnings("unchecked") + public static A readAttributes(Path path, Class type, LinkOption... options) + throws IOException { + if (type != BasicFileAttributes.class) { + throw new UnsupportedOperationException("Only BasicFileAttributes can be read on a device"); + } + return (A) attributes(existing(path)); + } + + private static BasicFileAttributes attributes(File f) { + final boolean directory = f.isDirectory(); + final long size = f.length(); + final FileTime time = FileTime.fromMillis(f.lastModified()); + return new BasicFileAttributes() { + @Override + public FileTime lastModifiedTime() { + return time; + } + + @Override + public FileTime lastAccessTime() { + return time; + } + + @Override + public FileTime creationTime() { + return time; + } + + @Override + public boolean isRegularFile() { + return !directory; + } + + @Override + public boolean isDirectory() { + return directory; + } + + @Override + public boolean isSymbolicLink() { + return false; + } + + @Override + public boolean isOther() { + return false; + } + + @Override + public long size() { + return size; + } + + @Override + public Object fileKey() { + return null; + } + }; + } + + // ---- streams ---- + + public static InputStream newInputStream(Path path, OpenOption... options) throws IOException { + if (has(options, StandardOpenOption.WRITE) || has(options, StandardOpenOption.APPEND)) { + throw new UnsupportedOperationException("An input stream cannot be opened for writing"); + } + File f = existing(path); + if (f.isDirectory()) { + throw new IOException(path + ": Is a directory"); + } + return new FileInputStream(f); + } + + public static OutputStream newOutputStream(Path path, OpenOption... options) throws IOException { + if (has(options, StandardOpenOption.READ)) { + throw new IllegalArgumentException("READ not allowed"); + } + boolean none = options == null || options.length == 0; + boolean append = has(options, StandardOpenOption.APPEND); + if (append && has(options, StandardOpenOption.TRUNCATE_EXISTING)) { + throw new IllegalArgumentException("APPEND + TRUNCATE_EXISTING not allowed"); + } + File f = file(path); + boolean exists = f.exists(); + if (has(options, StandardOpenOption.CREATE_NEW)) { + if (exists) { + throw new FileAlreadyExistsException(path.toString()); + } + } else if (!exists && !none && !has(options, StandardOpenOption.CREATE)) { + throw new NoSuchFileException(path.toString()); + } + if (f.isDirectory()) { + throw new FileSystemException(path.toString(), null, "Is a directory"); + } + File parent = f.getParentFile(); + if (parent != null && !parent.isDirectory()) { + throw new NoSuchFileException(path.toString()); + } + // Without APPEND or TRUNCATE_EXISTING the JDK overwrites from the + // start and leaves a longer file's tail in place. A device stream + // replaces the file, so the tail is put back when there is one. + byte[] tail = null; + if (exists && !none && !append && !has(options, StandardOpenOption.TRUNCATE_EXISTING)) { + tail = readAllBytes(path); + } + // CREATE_NEW claims the path with the exclusive create before opening + // it: the check above alone lets two callers both pass and the later + // open erase what the first one wrote. + if (has(options, StandardOpenOption.CREATE_NEW) && !f.createNewFile()) { + throw new FileAlreadyExistsException(path.toString()); + } + OutputStream out = new FileOutputStream(f, append); + return tail == null || tail.length == 0 ? out : new Overwrite(out, tail); + } + + /// A stream that writes over the start of what a file held and keeps the + /// rest: what opening a file with `WRITE` alone means. + private static final class Overwrite extends OutputStream { + private final OutputStream out; + private final byte[] old; + private long written; + private boolean closed; + + Overwrite(OutputStream out, byte[] old) { + this.out = out; + this.old = old; + } + + @Override + public void write(int b) throws IOException { + out.write(b); + written++; + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + out.write(b, off, len); + written += len; + } + + @Override + public void flush() throws IOException { + out.flush(); + } + + @Override + public void close() throws IOException { + if (!closed) { + closed = true; + if (written < old.length) { + out.write(old, (int) written, old.length - (int) written); + } + out.close(); + } + } + } + + public static BufferedReader newBufferedReader(Path path, Charset cs) throws IOException { + return new BufferedReader(new InputStreamReader(newInputStream(path), cs.name())); + } + + public static BufferedReader newBufferedReader(Path path) throws IOException { + return newBufferedReader(path, StandardCharsets.UTF_8); + } + + public static BufferedWriter newBufferedWriter(Path path, Charset cs, OpenOption... options) throws IOException { + return new BufferedWriter(new OutputStreamWriter(newOutputStream(path, options), cs.name())); + } + + public static BufferedWriter newBufferedWriter(Path path, OpenOption... options) throws IOException { + return newBufferedWriter(path, StandardCharsets.UTF_8, options); + } + + // ---- reading ---- + + public static byte[] readAllBytes(Path path) throws IOException { + InputStream in = newInputStream(path); + try { + // Read here rather than by InputStream.readAllBytes, which a Java 8 + // class library lacks: the differential tests run this class over + // the JDK's own streams. + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buf = new byte[8192]; + int n; + while ((n = in.read(buf, 0, buf.length)) > 0) { + out.write(buf, 0, n); + } + return out.toByteArray(); + } finally { + in.close(); + } + } + + public static String readString(Path path) throws IOException { + return readString(path, StandardCharsets.UTF_8); + } + + public static String readString(Path path, Charset cs) throws IOException { + return new String(readAllBytes(path), cs.name()); + } + + public static List readAllLines(Path path) throws IOException { + return readAllLines(path, StandardCharsets.UTF_8); + } + + /// The lines of the file. A line ends at `\n`, `\r` or `\r\n`, and a + /// file that ends with a terminator has no empty last line. + public static List readAllLines(Path path, Charset cs) throws IOException { + String text = readString(path, cs); + List lines = new ArrayList(); + int n = text.length(); + int start = 0; + for (int i = 0; i < n; i++) { + char c = text.charAt(i); + if (c == '\n' || c == '\r') { + lines.add(text.substring(start, i)); + if (c == '\r' && i + 1 < n && text.charAt(i + 1) == '\n') { + i++; + } + start = i + 1; + } + } + if (start < n) { + lines.add(text.substring(start)); + } + return lines; + } + + // ---- writing ---- + + public static Path write(Path path, byte[] bytes, OpenOption... options) throws IOException { + if (bytes == null) { + throw new NullPointerException(); + } + OutputStream out = newOutputStream(path, options); + try { + out.write(bytes, 0, bytes.length); + } finally { + out.close(); + } + return path; + } + + public static Path write(Path path, Iterable lines, Charset cs, OpenOption... options) + throws IOException { + if (lines == null) { + throw new NullPointerException(); + } + StringBuilder sb = new StringBuilder(); + for (CharSequence line : lines) { + sb.append(line.toString()).append(LINE); + } + return write(path, sb.toString().getBytes(cs.name()), options); + } + + public static Path write(Path path, Iterable lines, OpenOption... options) + throws IOException { + return write(path, lines, StandardCharsets.UTF_8, options); + } + + public static Path writeString(Path path, CharSequence csq, OpenOption... options) throws IOException { + return writeString(path, csq, StandardCharsets.UTF_8, options); + } + + public static Path writeString(Path path, CharSequence csq, Charset cs, OpenOption... options) + throws IOException { + if (csq == null) { + throw new NullPointerException(); + } + return write(path, csq.toString().getBytes(cs.name()), options); + } + + // ---- creating and deleting ---- + + public static Path createFile(Path path, FileAttribute... attrs) throws IOException { + File f = file(path); + if (f.exists()) { + throw new FileAlreadyExistsException(path.toString()); + } + File parent = f.getParentFile(); + if (parent != null && !parent.isDirectory()) { + throw new NoSuchFileException(path.toString()); + } + // Claimed with the exclusive create, so of two callers racing for + // the path exactly one succeeds and nothing is truncated. + if (!f.createNewFile()) { + throw new FileAlreadyExistsException(path.toString()); + } + return path; + } + + public static Path createDirectory(Path dir, FileAttribute... attrs) throws IOException { + File f = file(dir); + if (f.exists()) { + throw new FileAlreadyExistsException(dir.toString()); + } + File parent = f.getParentFile(); + if (parent != null && !parent.isDirectory()) { + throw new NoSuchFileException(dir.toString()); + } + if (!f.mkdir()) { + // mkdir is the atomic claim: a caller that lost the race for the + // name is told so, as createFile tells it. + if (f.exists()) { + throw new FileAlreadyExistsException(dir.toString()); + } + throw new IOException("Could not create the directory " + dir); + } + return dir; + } + + public static Path createDirectories(Path dir, FileAttribute... attrs) throws IOException { + File f = file(dir); + if (f.isDirectory()) { + return dir; + } + if (f.exists()) { + throw new FileAlreadyExistsException(dir.toString()); + } + f.mkdirs(); + if (!f.isDirectory()) { + throw new IOException("Could not create the directory " + dir); + } + return dir; + } + + private static Path temporaryDirectory() { + JdkHost h = JdkHost.get(); + String caches = h.cachesDir(); + return PathImpl.of(caches != null ? caches : h.appHome()); + } + + private static Path temporary(Path dir, String prefix, String suffix, boolean directory) throws IOException { + Path parent = dir == null ? temporaryDirectory() : dir; + String p = prefix == null ? "" : prefix; + long n = System.currentTimeMillis(); + while (true) { + Path candidate = parent.resolve(p + n + suffix); + if (!file(candidate).exists()) { + // Another caller can claim the same name between the check and + // the create; the create is exclusive, so the loser moves on. + try { + return directory ? createDirectory(candidate) : createFile(candidate); + } catch (FileAlreadyExistsException e) { + // Taken meanwhile: try the next name. + } + } + n++; + } + } + + public static Path createTempFile(Path dir, String prefix, String suffix, FileAttribute... attrs) + throws IOException { + if (dir == null) { + throw new NullPointerException(); + } + return temporary(dir, prefix, suffix == null ? ".tmp" : suffix, false); + } + + public static Path createTempFile(String prefix, String suffix, FileAttribute... attrs) throws IOException { + return temporary(null, prefix, suffix == null ? ".tmp" : suffix, false); + } + + public static Path createTempDirectory(Path dir, String prefix, FileAttribute... attrs) throws IOException { + if (dir == null) { + throw new NullPointerException(); + } + return temporary(dir, prefix, "", true); + } + + public static Path createTempDirectory(String prefix, FileAttribute... attrs) throws IOException { + return temporary(null, prefix, "", true); + } + + public static void delete(Path path) throws IOException { + File f = existing(path); + if (f.isDirectory()) { + String[] entries = f.list(); + if (entries != null && entries.length > 0) { + throw new DirectoryNotEmptyException(path.toString()); + } + } + if (!f.delete()) { + throw new IOException("Could not delete " + path); + } + } + + public static boolean deleteIfExists(Path path) throws IOException { + if (!file(path).exists()) { + return false; + } + delete(path); + return true; + } + + // ---- copying and moving ---- + + /// Clears the way for a copy or a move to `target`, or says why not. + private static void replace(Path target, CopyOption[] options) throws IOException { + if (file(target).exists()) { + if (!has(options, StandardCopyOption.REPLACE_EXISTING)) { + throw new FileAlreadyExistsException(target.toString()); + } + delete(target); + } + } + + private static void transfer(File from, File to) throws IOException { + InputStream in = new FileInputStream(from); + try { + OutputStream out = new FileOutputStream(to, false); + try { + pump(in, out); + } finally { + out.close(); + } + } finally { + in.close(); + } + } + + private static long pump(InputStream in, OutputStream out) throws IOException { + byte[] buffer = new byte[8192]; + long total = 0; + int n; + while ((n = in.read(buffer, 0, buffer.length)) >= 0) { + out.write(buffer, 0, n); + total += n; + } + return total; + } + + /// Copies a file, or creates an empty directory where `source` is one: + /// a directory's entries are not copied, as in the JDK. + public static Path copy(Path source, Path target, CopyOption... options) throws IOException { + File from = existing(source); + if (file(target).exists() && isSameFile(source, target)) { + return target; + } + replace(target, options); + if (from.isDirectory()) { + createDirectory(target); + } else { + File to = file(target); + File parent = to.getParentFile(); + if (parent != null && !parent.isDirectory()) { + throw new NoSuchFileException(target.toString()); + } + transfer(from, to); + } + return target; + } + + public static long copy(InputStream in, Path target, CopyOption... options) throws IOException { + if (in == null) { + throw new NullPointerException(); + } + replace(target, options); + OutputStream out = newOutputStream(target, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE); + try { + return pump(in, out); + } finally { + out.close(); + } + } + + public static long copy(Path source, OutputStream out) throws IOException { + if (out == null) { + throw new NullPointerException(); + } + InputStream in = newInputStream(source); + try { + return pump(in, out); + } finally { + in.close(); + } + } + + private static void copyTree(File from, File to) throws IOException { + if (from.isDirectory()) { + if (!to.mkdir()) { + throw new IOException("Could not create the directory " + to.getPath()); + } + String[] names = from.list(); + if (names != null) { + for (String name : names) { + copyTree(new File(from, name), new File(to, name)); + } + } + } else { + transfer(from, to); + } + } + + /// Deletes what a move has copied. An entry that will not go is an + /// error: answering normally would report a move that left the source, + /// or half of it, where it was. + private static void deleteTree(File f) throws IOException { + if (f.isDirectory()) { + String[] names = f.list(); + if (names != null) { + for (String name : names) { + deleteTree(new File(f, name)); + } + } + } + if (!f.delete() && f.exists()) { + throw new IOException("Moved " + f.getPath() + " but could not delete the original"); + } + } + + /// Moves a file or a directory with everything in it. Where the device + /// cannot rename in place the tree is copied and the original deleted. + public static Path move(Path source, Path target, CopyOption... options) throws IOException { + File from = existing(source); + if (file(target).exists() && isSameFile(source, target)) { + return target; + } + replace(target, options); + File to = file(target); + File parent = to.getParentFile(); + if (parent != null && !parent.isDirectory()) { + throw new NoSuchFileException(target.toString()); + } + if (!from.renameTo(to)) { + // No copy fallback for a regular file: File.renameTo already is + // one -- outside its directory it copies the bytes and deletes + // the original -- so a rename that failed is a copy or a delete + // that failed, and trying the same again would fail the same way. + if (!from.isDirectory()) { + throw new IOException("Could not move " + source + " to " + target); + } + copyTree(from, to); + deleteTree(from); + } + return target; + } + + /// The position of the first byte two files differ in, or -1 when they + /// hold the same bytes. Both are read a block at a time, as the JDK + /// reads them, so two large files that differ early are not loaded + /// whole, and a file larger than an array can be compared at all. + public static long mismatch(Path path, Path path2) throws IOException { + if (isSameFile(path, path2)) { + return -1L; + } + InputStream a = newInputStream(path); + try { + InputStream b = newInputStream(path2); + try { + byte[] bufA = new byte[8192]; + byte[] bufB = new byte[8192]; + long at = 0; + while (true) { + int na = fill(a, bufA); + int nb = fill(b, bufB); + int n = Math.min(na, nb); + for (int i = 0; i < n; i++) { + if (bufA[i] != bufB[i]) { + return at + i; + } + } + if (na != nb) { + return at + n; + } + if (na < bufA.length) { + return -1L; + } + at += n; + } + } finally { + b.close(); + } + } finally { + a.close(); + } + } + + /// Reads into `buf` until it is full or the stream ends; how many bytes + /// were read. A stream may answer fewer than asked before its end. + private static int fill(InputStream in, byte[] buf) throws IOException { + int total = 0; + while (total < buf.length) { + int n = in.read(buf, total, buf.length - total); + if (n < 0) { + break; + } + total += n; + } + return total; + } + + // ---- directories ---- + + private static List entries(Path dir) throws IOException { + File f = existing(dir); + if (!f.isDirectory()) { + throw new NotDirectoryException(dir.toString()); + } + String[] names = f.list(); + List out = new ArrayList(); + if (names != null) { + Arrays.sort(names); + for (String name : names) { + out.add(dir.resolve(name)); + } + } + return out; + } + + /// The lines of `path` as UTF-8. See [#lines(Path, Charset)]. + public static Stream lines(Path path) throws IOException { + return lines(path, StandardCharsets.UTF_8); + } + + /// The lines of `path`, read as the stream asks for them: `findFirst` + /// reads the start of a large file, not all of it. The file is opened + /// here, so a missing one throws now, and closing the stream closes it. + public static Stream lines(Path path, Charset cs) throws IOException { + BufferedReader reader = newBufferedReader(path, cs); + return reader.lines().onClose(new CloseReader(reader)); + } + + /// The entries of `dir`, sorted by name. + public static Stream list(Path dir) throws IOException { + return lazy(entries(dir).iterator()); + } + + /// The paths under `start`, `start` first, depth first, to `maxDepth` + /// levels below it. + public static Stream walk(Path start, int maxDepth, FileVisitOption... options) throws IOException { + if (maxDepth < 0) { + throw new IllegalArgumentException("'maxDepth' is negative"); + } + existing(start); + return lazy(new Walker(start, maxDepth)); + } + + /// The paths under `start`, at every depth. See + /// [#walk(Path, int, FileVisitOption...)]. + public static Stream walk(Path start, FileVisitOption... options) throws IOException { + return walk(start, Integer.MAX_VALUE, options); + } + + /// The paths [#walk(Path, int, FileVisitOption...)] would answer for + /// which `matcher` holds, given each path's attributes. + public static Stream find(Path start, int maxDepth, BiPredicate matcher, + FileVisitOption... options) throws IOException { + if (maxDepth < 0) { + throw new IllegalArgumentException("'maxDepth' is negative"); + } + if (matcher == null) { + throw new NullPointerException(); + } + existing(start); + return lazy(new Finder(new Walker(start, maxDepth), matcher)); + } + + private static Stream lazy(Iterator it) { + return StreamSupport.stream(Spliterators.spliteratorUnknownSize(it, + Spliterator.ORDERED | Spliterator.NONNULL), false); + } + + private static final class CloseReader implements Runnable { + private final BufferedReader reader; + + CloseReader(BufferedReader reader) { + this.reader = reader; + } + + @Override + public void run() { + try { + reader.close(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } + + /// Depth first, discovering as it goes: a directory is listed when the + /// walk advances past it, never before, and a failure to list it + /// surfaces then. + private static final class Walker implements Iterator { + /// The entries still to visit, one iterator per open directory. + private final List> levels = new ArrayList>(); + private final int maxDepth; + private Path pending; + private int pendingDepth; + /// The path answered last, entered on the next advance if it is a + /// directory within the depth limit. + private Path last; + private int lastDepth; + + Walker(Path start, int maxDepth) { + this.maxDepth = maxDepth; + this.pending = start; + } + + @Override + public boolean hasNext() { + if (pending != null) { + return true; + } + if (last != null && lastDepth < maxDepth && file(last).isDirectory()) { + try { + levels.add(entries(last).iterator()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + last = null; + while (!levels.isEmpty()) { + Iterator top = levels.get(levels.size() - 1); + if (top.hasNext()) { + pending = top.next(); + pendingDepth = levels.size(); + return true; + } + levels.remove(levels.size() - 1); + } + return false; + } + + @Override + public Path next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + Path p = pending; + pending = null; + last = p; + lastDepth = pendingDepth; + return p; + } + } + + private static final class Finder implements Iterator { + private final Iterator all; + private final BiPredicate matcher; + private Path next; + + Finder(Iterator all, BiPredicate matcher) { + this.all = all; + this.matcher = matcher; + } + + @Override + public boolean hasNext() { + while (next == null && all.hasNext()) { + Path p = all.next(); + if (matcher.test(p, attributes(file(p)))) { + next = p; + } + } + return next != null; + } + + @Override + public Path next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + Path p = next; + next = null; + return p; + } + } + + public static Path walkFileTree(Path start, FileVisitor visitor) throws IOException { + return walkFileTree(start, null, Integer.MAX_VALUE, visitor); + } + + public static Path walkFileTree(Path start, Set options, int maxDepth, + FileVisitor visitor) throws IOException { + if (maxDepth < 0) { + throw new IllegalArgumentException("'maxDepth' is negative"); + } + if (visitor == null) { + throw new NullPointerException(); + } + visit(start, 0, maxDepth, visitor); + return start; + } + + private static FileVisitResult checked(FileVisitResult result) { + if (result == null) { + throw new NullPointerException(); + } + return result; + } + + /// Visits `path` and what is under it. Answers `TERMINATE` to stop the + /// walk, `SKIP_SIBLINGS` to stop the directory it is an entry of, and + /// `CONTINUE` otherwise. + private static FileVisitResult visit(Path path, int depth, int maxDepth, FileVisitor visitor) + throws IOException { + File f = file(path); + if (!f.exists()) { + return checked(visitor.visitFileFailed(path, new NoSuchFileException(path.toString()))); + } + BasicFileAttributes attrs = attributes(f); + if (!attrs.isDirectory() || depth >= maxDepth) { + return checked(visitor.visitFile(path, attrs)); + } + FileVisitResult pre = checked(visitor.preVisitDirectory(path, attrs)); + if (pre == FileVisitResult.SKIP_SUBTREE) { + return FileVisitResult.CONTINUE; + } + if (pre != FileVisitResult.CONTINUE) { + return pre; + } + for (Path entry : entries(path)) { + FileVisitResult result = visit(entry, depth + 1, maxDepth, visitor); + if (result == FileVisitResult.TERMINATE) { + return result; + } + if (result == FileVisitResult.SKIP_SIBLINGS) { + break; + } + } + FileVisitResult post = checked(visitor.postVisitDirectory(path, null)); + return post == FileVisitResult.SKIP_SUBTREE ? FileVisitResult.CONTINUE : post; + } + + public static DirectoryStream newDirectoryStream(Path dir) throws IOException { + return new Listing(entries(dir)); + } + + public static DirectoryStream newDirectoryStream(Path dir, DirectoryStream.Filter filter) + throws IOException { + if (filter == null) { + throw new NullPointerException(); + } + List out = new ArrayList(); + for (Path entry : entries(dir)) { + if (filter.accept(entry)) { + out.add(entry); + } + } + return new Listing(out); + } + + /// The entries of `dir` whose names match `glob`: `*` and `?` for any + /// characters and any one, `[a-c]` and `[!a-c]` for a set of them, + /// `{one,two}` for alternatives, and a backslash to take the next + /// character literally. + public static DirectoryStream newDirectoryStream(Path dir, String glob) throws IOException { + if (glob == null) { + throw new NullPointerException(); + } + List out = new ArrayList(); + for (Path entry : entries(dir)) { + String name = entry.getFileName().toString(); + if (Glob.matches(glob, 0, glob.length(), name, 0)) { + out.add(entry); + } + } + return new Listing(out); + } + + private static final class Listing implements DirectoryStream { + private final List entries; + private boolean iterated; + private boolean closed; + + Listing(List entries) { + this.entries = entries; + } + + @Override + public Iterator iterator() { + if (closed) { + throw new IllegalStateException("Directory stream is closed"); + } + if (iterated) { + throw new IllegalStateException("Iterator already obtained"); + } + iterated = true; + final Iterator it = entries.iterator(); + return new Iterator() { + @Override + public boolean hasNext() { + return !closed && it.hasNext(); + } + + @Override + public Path next() { + if (closed) { + throw new NoSuchElementException(); + } + return it.next(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + @Override + public void close() { + closed = true; + } + } +} diff --git a/Ports/CLDC11/src/java/nio/file/Glob.java b/Ports/CLDC11/src/java/nio/file/Glob.java new file mode 100644 index 00000000000..33b36151901 --- /dev/null +++ b/Ports/CLDC11/src/java/nio/file/Glob.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.nio.file; + +/// The glob syntax of `Files.newDirectoryStream(dir, glob)`, matched against +/// one file name. +final class Glob { + + private Glob() { + } + + /// Whether `name` from `ni` on matches `glob` between `gi` and `gEnd`. + static boolean matches(String glob, int gi, int gEnd, String name, int ni) { + int n = name.length(); + while (gi < gEnd) { + char g = glob.charAt(gi); + if (g == '*') { + while (gi < gEnd && glob.charAt(gi) == '*') { + gi++; + } + if (gi == gEnd) { + return true; + } + for (int k = ni; k <= n; k++) { + if (matches(glob, gi, gEnd, name, k)) { + return true; + } + } + return false; + } + if (g == '{') { + int close = closing(glob, gi, gEnd); + if (close < 0) { + throw new IllegalArgumentException("Missing '}' in glob: " + glob); + } + String rest = glob.substring(close + 1, gEnd); + int start = gi + 1; + for (int i = start; i <= close; i++) { + char c = glob.charAt(i); + if (c == '\\') { + i++; + } else if (c == ',' || i == close) { + String alternative = glob.substring(start, i) + rest; + if (matches(alternative, 0, alternative.length(), name, ni)) { + return true; + } + start = i + 1; + } + } + return false; + } + if (ni >= n) { + return false; + } + char c = name.charAt(ni); + if (g == '?') { + gi++; + } else if (g == '[') { + int close = gi + 1; + boolean negate = close < gEnd && glob.charAt(close) == '!'; + if (negate) { + close++; + } + int first = close; + boolean found = false; + while (close < gEnd && (glob.charAt(close) != ']' || close == first)) { + char lo = glob.charAt(close); + char hi = lo; + if (close + 2 < gEnd && glob.charAt(close + 1) == '-' && glob.charAt(close + 2) != ']') { + hi = glob.charAt(close + 2); + close += 2; + } + found = found || (c >= lo && c <= hi); + close++; + } + if (close >= gEnd) { + throw new IllegalArgumentException("Missing ']' in glob: " + glob); + } + if (found == negate) { + return false; + } + gi = close + 1; + } else { + if (g == '\\') { + gi++; + if (gi >= gEnd) { + throw new IllegalArgumentException("No character to escape in glob: " + glob); + } + g = glob.charAt(gi); + } + if (g != c) { + return false; + } + gi++; + } + ni++; + } + return ni == n; + } + + private static int closing(String glob, int open, int end) { + for (int i = open + 1; i < end; i++) { + char c = glob.charAt(i); + if (c == '\\') { + i++; + } else if (c == '}') { + return i; + } + } + return -1; + } +} diff --git a/Ports/CLDC11/src/java/nio/file/InvalidPathException.java b/Ports/CLDC11/src/java/nio/file/InvalidPathException.java new file mode 100644 index 00000000000..b2f91cbc64c --- /dev/null +++ b/Ports/CLDC11/src/java/nio/file/InvalidPathException.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.nio.file; + +/// A string that cannot be made a path. +public class InvalidPathException extends IllegalArgumentException { + private static final long serialVersionUID = 1L; + private final String input; + private final String reason; + private final int index; + + public InvalidPathException(String input, String reason, int index) { + super(reason); + if (input == null || reason == null) { + throw new NullPointerException(); + } + if (index < -1) { + throw new IllegalArgumentException(); + } + this.input = input; + this.reason = reason; + this.index = index; + } + + public InvalidPathException(String input, String reason) { + this(input, reason, -1); + } + + public String getInput() { + return input; + } + + public String getReason() { + return reason; + } + + public int getIndex() { + return index; + } + + @Override + public String getMessage() { + StringBuilder sb = new StringBuilder(reason); + if (index > -1) { + sb.append(" at index ").append(index); + } + return sb.append(": ").append(input).toString(); + } +} diff --git a/Ports/CLDC11/src/java/nio/file/LinkOption.java b/Ports/CLDC11/src/java/nio/file/LinkOption.java new file mode 100644 index 00000000000..b3e7d920389 --- /dev/null +++ b/Ports/CLDC11/src/java/nio/file/LinkOption.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.nio.file; + +/// The device's +/// file system is not asked about symbolic links, so the option is accepted +/// wherever the JDK takes it and changes nothing. +public enum LinkOption implements OpenOption, CopyOption { + NOFOLLOW_LINKS +} diff --git a/Ports/CLDC11/src/java/nio/file/NoSuchFileException.java b/Ports/CLDC11/src/java/nio/file/NoSuchFileException.java new file mode 100644 index 00000000000..30cdb47343f --- /dev/null +++ b/Ports/CLDC11/src/java/nio/file/NoSuchFileException.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.nio.file; + +/// Nothing is at the path. +public class NoSuchFileException extends FileSystemException { + private static final long serialVersionUID = 1L; + + public NoSuchFileException(String file) { + super(file); + } + + public NoSuchFileException(String file, String other, String reason) { + super(file, other, reason); + } +} diff --git a/Ports/CLDC11/src/java/nio/file/NotDirectoryException.java b/Ports/CLDC11/src/java/nio/file/NotDirectoryException.java new file mode 100644 index 00000000000..cead4086d50 --- /dev/null +++ b/Ports/CLDC11/src/java/nio/file/NotDirectoryException.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.nio.file; + +/// The path is not a directory. +public class NotDirectoryException extends FileSystemException { + private static final long serialVersionUID = 1L; + + public NotDirectoryException(String file) { + super(file); + } +} diff --git a/Ports/CLDC11/src/java/nio/file/OpenOption.java b/Ports/CLDC11/src/java/nio/file/OpenOption.java new file mode 100644 index 00000000000..02550233ff4 --- /dev/null +++ b/Ports/CLDC11/src/java/nio/file/OpenOption.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.nio.file; + +/// How a file is +/// opened. [StandardOpenOption] has the values. +public interface OpenOption { +} diff --git a/Ports/CLDC11/src/java/nio/file/Path.java b/Ports/CLDC11/src/java/nio/file/Path.java new file mode 100644 index 00000000000..db53f9b0d1b --- /dev/null +++ b/Ports/CLDC11/src/java/nio/file/Path.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.nio.file; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.util.Iterator; + +/// A path in the platform's file system, the one `java.io.File` reaches. +/// +/// #### What a path is here +/// +/// A sequence of names separated by `/`, after an optional root. A backslash +/// is read as a separator. The root is `/`, or -- because Codename One names +/// its files `file:///...` and `java.io.File.getAbsolutePath()` answers that +/// form -- the `file:` scheme with the slashes after it. A relative path is +/// resolved against the application's home directory when it is used, which +/// is also what [#toAbsolutePath()] answers. +/// +/// The methods that only rearrange names (`resolve`, `relativize`, +/// `normalize`, `getParent`, `startsWith`, ...) answer what the JDK answers +/// on a Unix file system. +/// +/// #### What is not provided +/// +/// `getFileSystem()` and `register(...)`: there is one file system and it +/// cannot be watched. [#toRealPath] is `File.getCanonicalPath()`: where the +/// file system cannot resolve symbolic links it answers the absolute path +/// with `.` and `..` resolved. +public interface Path extends Comparable, Iterable { + + /// The path `first`, with each of `more` appended as further names. + static Path of(String first, String... more) { + return PathImpl.of(first, more); + } + + /// The path a `file:` URI names. + static Path of(URI uri) { + return PathImpl.of(uri); + } + + boolean isAbsolute(); + + Path getRoot(); + + Path getFileName(); + + Path getParent(); + + int getNameCount(); + + Path getName(int index); + + Path subpath(int beginIndex, int endIndex); + + boolean startsWith(Path other); + + boolean startsWith(String other); + + boolean endsWith(Path other); + + boolean endsWith(String other); + + Path normalize(); + + Path resolve(Path other); + + Path resolve(String other); + + Path resolveSibling(Path other); + + Path resolveSibling(String other); + + Path relativize(Path other); + + URI toUri(); + + Path toAbsolutePath(); + + /// The absolute path with `.` and `..` resolved, and symbolic links where + /// the file system can. Throws [NoSuchFileException] when nothing is there. + Path toRealPath(LinkOption... options) throws IOException; + + File toFile(); + + @Override + Iterator iterator(); + + @Override + int compareTo(Path other); +} diff --git a/Ports/CLDC11/src/java/nio/file/PathImpl.java b/Ports/CLDC11/src/java/nio/file/PathImpl.java new file mode 100644 index 00000000000..d229c310493 --- /dev/null +++ b/Ports/CLDC11/src/java/nio/file/PathImpl.java @@ -0,0 +1,515 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.nio.file; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +/// The one implementation of [Path]: a normalized path string, split into a +/// root and names when a method needs them. +final class PathImpl implements Path { + + private static final String[] NO_NAMES = new String[0]; + private static final String SCHEME = "file:"; + + /// Separators collapsed, no trailing separator unless the path is a root. + private final String path; + private final int rootLength; + private String[] names; + + private PathImpl(String path) { + this.path = path; + this.rootLength = rootLength(path); + } + + static PathImpl of(String first, String[] more) { + if (first == null) { + throw new NullPointerException(); + } + String joined = first; + if (more != null && more.length > 0) { + StringBuilder sb = new StringBuilder(first); + for (String m : more) { + if (m == null) { + throw new NullPointerException(); + } + if (m.length() > 0) { + if (sb.length() > 0) { + sb.append('/'); + } + sb.append(m); + } + } + joined = sb.toString(); + } + if (joined.indexOf('\0') >= 0) { + throw new InvalidPathException(joined, "Nul character not allowed"); + } + return new PathImpl(clean(joined)); + } + + static PathImpl of(String path) { + return of(path, null); + } + + static PathImpl of(URI uri) { + String scheme = uri.getScheme(); + if (scheme == null || !"file".equalsIgnoreCase(scheme)) { + throw new IllegalArgumentException("URI scheme is not \"file\""); + } + // Only what names a file on this device is a path, as in the JDK: + // a host, a query or a fragment is not dropped, it is refused, so + // that file://server/share/x does not quietly become /share/x. + if (uri.isOpaque()) { + throw new IllegalArgumentException("URI is not hierarchical"); + } + if (present(uri.getRawAuthority())) { + throw new IllegalArgumentException("URI has an authority component"); + } + if (present(uri.getRawFragment())) { + throw new IllegalArgumentException("URI has a fragment component"); + } + if (present(uri.getRawQuery())) { + throw new IllegalArgumentException("URI has a query component"); + } + String p = uri.getPath(); + if (p == null || p.length() == 0) { + throw new IllegalArgumentException("URI has no path component"); + } + return of(p); + } + + /// Whether a URI has the component. An empty one counts as none: the + /// device's `URI` answers an empty authority for `file:///x`, where the + /// JDK's answers null. + private static boolean present(String component) { + return component != null && component.length() > 0; + } + + /// How many leading characters are the root: one for `/`, the scheme and + /// its slashes for a `file:` path, none for a relative path. + private static int rootLength(String p) { + if (p.startsWith(SCHEME)) { + int i = SCHEME.length(); + while (i < p.length() && p.charAt(i) == '/') { + i++; + } + return i; + } + return p.startsWith("/") ? 1 : 0; + } + + /// Collapses runs of separators and drops a trailing one, leaving the + /// slashes of a `file:` root as they were written. + private static String clean(String p) { + int root = 0; + StringBuilder sb = new StringBuilder(p.length()); + if (p.startsWith(SCHEME)) { + root = rootLength(p); + sb.append(p.substring(0, root)); + } + boolean separator = false; + for (int i = root; i < p.length(); i++) { + char c = p.charAt(i); + if (c == '\\') { + c = '/'; + } + if (c == '/') { + if (sb.length() == 0) { + sb.append('/'); + root = 1; + } else { + separator = sb.length() > root; + } + continue; + } + if (separator) { + sb.append('/'); + separator = false; + } + sb.append(c); + } + return sb.toString(); + } + + private String[] names() { + if (names == null) { + if (path.length() == rootLength) { + // The empty path is one empty name; a root has none. + names = rootLength == 0 ? new String[] {""} : NO_NAMES; + } else { + List out = new ArrayList(); + int start = rootLength; + for (int i = rootLength; i <= path.length(); i++) { + if (i == path.length() || path.charAt(i) == '/') { + out.add(path.substring(start, i)); + start = i + 1; + } + } + names = out.toArray(new String[out.size()]); + } + } + return names; + } + + private boolean isEmpty() { + return path.length() == 0; + } + + private String root() { + return path.substring(0, rootLength); + } + + private static PathImpl build(String root, String[] names, int from, int to) { + StringBuilder sb = new StringBuilder(root); + for (int i = from; i < to; i++) { + if (i > from) { + sb.append('/'); + } + sb.append(names[i]); + } + return new PathImpl(sb.toString()); + } + + private static PathImpl impl(Path other) { + if (other == null) { + throw new NullPointerException(); + } + if (other instanceof PathImpl) { + return (PathImpl) other; + } + return of(other.toString()); + } + + @Override + public boolean isAbsolute() { + return rootLength > 0; + } + + @Override + public Path getRoot() { + return rootLength == 0 ? null : new PathImpl(root()); + } + + @Override + public Path getFileName() { + String[] n = names(); + return n.length == 0 ? null : new PathImpl(n[n.length - 1]); + } + + @Override + public Path getParent() { + String[] n = names(); + if (n.length == 0) { + return null; + } + if (n.length == 1) { + return rootLength == 0 ? null : new PathImpl(root()); + } + return build(root(), n, 0, n.length - 1); + } + + @Override + public int getNameCount() { + return names().length; + } + + @Override + public Path getName(int index) { + String[] n = names(); + if (index < 0 || index >= n.length) { + throw new IllegalArgumentException(); + } + return new PathImpl(n[index]); + } + + @Override + public Path subpath(int beginIndex, int endIndex) { + String[] n = names(); + if (beginIndex < 0 || beginIndex >= n.length || endIndex > n.length || beginIndex >= endIndex) { + throw new IllegalArgumentException(); + } + return build("", n, beginIndex, endIndex); + } + + @Override + public boolean startsWith(Path other) { + PathImpl o = impl(other); + if (!root().equals(o.root())) { + return false; + } + String[] a = names(); + String[] b = o.names(); + if (b.length > a.length) { + return false; + } + for (int i = 0; i < b.length; i++) { + if (!a[i].equals(b[i])) { + return false; + } + } + return true; + } + + @Override + public boolean startsWith(String other) { + return startsWith(of(other)); + } + + @Override + public boolean endsWith(Path other) { + PathImpl o = impl(other); + if (o.rootLength > 0) { + return equals(o); + } + String[] a = names(); + String[] b = o.names(); + if (b.length > a.length) { + return false; + } + for (int i = 0; i < b.length; i++) { + if (!a[a.length - b.length + i].equals(b[i])) { + return false; + } + } + return true; + } + + @Override + public boolean endsWith(String other) { + return endsWith(of(other)); + } + + @Override + public Path normalize() { + String[] n = names(); + List out = new ArrayList(n.length); + for (String name : n) { + if (".".equals(name) || name.length() == 0) { + continue; + } + if ("..".equals(name)) { + int last = out.size() - 1; + if (last >= 0 && !"..".equals(out.get(last))) { + out.remove(last); + continue; + } + if (rootLength > 0) { + // Above the root there is the root. + continue; + } + } + out.add(name); + } + if (out.size() == n.length) { + return this; + } + return build(root(), out.toArray(new String[out.size()]), 0, out.size()); + } + + @Override + public Path resolve(Path other) { + PathImpl o = impl(other); + if (o.rootLength > 0 || isEmpty()) { + return o; + } + if (o.isEmpty()) { + return this; + } + return new PathImpl(path.length() == rootLength ? path + o.path : path + "/" + o.path); + } + + @Override + public Path resolve(String other) { + return resolve(of(other)); + } + + @Override + public Path resolveSibling(Path other) { + PathImpl o = impl(other); + Path parent = getParent(); + return parent == null ? o : parent.resolve(o); + } + + @Override + public Path resolveSibling(String other) { + return resolveSibling(of(other)); + } + + private boolean hasDots() { + if (isEmpty()) { + return false; + } + for (String name : names()) { + if (".".equals(name) || "..".equals(name)) { + return true; + } + } + return false; + } + + @Override + public Path relativize(Path other) { + PathImpl o = impl(other); + if (!root().equals(o.root())) { + throw new IllegalArgumentException("'other' is different type of Path"); + } + if (path.equals(o.path)) { + return new PathImpl(""); + } + // A `.` or `..` anywhere has both sides normalized first, as the + // JDK has done since it stopped answering `../../b` for `a/..` + // against `b`: a name that cancels the one before it is no step + // away from anything. What is left of a `..` afterwards leads the + // path, cancels nothing, and is refused below. + PathImpl from = this; + if (!isEmpty() && (hasDots() || o.hasDots())) { + from = impl(normalize()); + o = impl(o.normalize()); + } + String[] a = from.isEmpty() ? NO_NAMES : from.names(); + String[] b = o.isEmpty() ? NO_NAMES : o.names(); + int common = 0; + while (common < a.length && common < b.length && a[common].equals(b[common])) { + common++; + } + StringBuilder sb = new StringBuilder(); + for (int i = common; i < a.length; i++) { + if ("..".equals(a[i])) { + throw new IllegalArgumentException("Unable to compute relative path from " + this + " to " + other); + } + if (sb.length() > 0) { + sb.append('/'); + } + sb.append(".."); + } + for (int i = common; i < b.length; i++) { + if (sb.length() > 0) { + sb.append('/'); + } + sb.append(b[i]); + } + return new PathImpl(sb.toString()); + } + + @Override + public URI toUri() { + // The storage path is "file://" and an absolute path, as typed: a + // space, '#' or '%' in a file's name is not legal in a URI as it + // stands. Handing the path over as a component has it escaped. + String storage = toFile().getAbsolutePath(); + int at = storage.startsWith(SCHEME) ? SCHEME.length() : 0; + while (at + 1 < storage.length() && storage.charAt(at) == '/' && storage.charAt(at + 1) == '/') { + at++; + } + String uriPath = storage.substring(at); + // A directory's URI ends in '/', as the JDK's does, so that resolving + // a name against it lands inside the directory rather than beside it. + if (!uriPath.endsWith("/") && toFile().isDirectory()) { + uriPath = uriPath + "/"; + } + try { + return new URI("file", null, uriPath, null, null); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e.getMessage()); + } + } + + @Override + public Path toAbsolutePath() { + return rootLength > 0 ? this : new PathImpl(clean(toFile().getAbsolutePath())); + } + + @Override + public Path toRealPath(LinkOption... options) throws IOException { + File file = toFile(); + if (!file.exists()) { + throw new NoSuchFileException(path); + } + return new PathImpl(clean(file.getCanonicalPath())); + } + + @Override + public File toFile() { + return new File(path); + } + + @Override + public Iterator iterator() { + return new Names(names()); + } + + /// The names of a path, each as a path of its own. + private static final class Names implements Iterator { + private final String[] n; + private int next; + + Names(String[] n) { + this.n = n; + } + + @Override + public boolean hasNext() { + return next < n.length; + } + + @Override + public Path next() { + if (next >= n.length) { + throw new NoSuchElementException(); + } + return new PathImpl(n[next++]); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + @Override + public int compareTo(Path other) { + return path.compareTo(impl(other).path); + } + + @Override + public boolean equals(Object o) { + return o instanceof PathImpl && ((PathImpl) o).path.equals(path); + } + + @Override + public int hashCode() { + return path.hashCode(); + } + + @Override + public String toString() { + return path; + } +} diff --git a/Ports/CLDC11/src/java/nio/file/Paths.java b/Ports/CLDC11/src/java/nio/file/Paths.java new file mode 100644 index 00000000000..afb6f8e9d12 --- /dev/null +++ b/Ports/CLDC11/src/java/nio/file/Paths.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.nio.file; + +import java.net.URI; + +/// Makes paths; see [Path]. +public final class Paths { + + private Paths() { + } + + public static Path get(String first, String... more) { + return PathImpl.of(first, more); + } + + public static Path get(URI uri) { + return PathImpl.of(uri); + } +} diff --git a/Ports/CLDC11/src/java/nio/file/SimpleFileVisitor.java b/Ports/CLDC11/src/java/nio/file/SimpleFileVisitor.java new file mode 100644 index 00000000000..ac7e9451e32 --- /dev/null +++ b/Ports/CLDC11/src/java/nio/file/SimpleFileVisitor.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.nio.file; + +import java.io.IOException; +import java.nio.file.attribute.BasicFileAttributes; + +/// A +/// [FileVisitor] that visits everything and rethrows what failed. +public class SimpleFileVisitor implements FileVisitor { + + protected SimpleFileVisitor() { + } + + @Override + public FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs) throws IOException { + if (dir == null || attrs == null) { + throw new NullPointerException(); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(T file, BasicFileAttributes attrs) throws IOException { + if (file == null || attrs == null) { + throw new NullPointerException(); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(T file, IOException exc) throws IOException { + if (file == null) { + throw new NullPointerException(); + } + throw exc; + } + + @Override + public FileVisitResult postVisitDirectory(T dir, IOException exc) throws IOException { + if (dir == null) { + throw new NullPointerException(); + } + if (exc != null) { + throw exc; + } + return FileVisitResult.CONTINUE; + } +} diff --git a/Ports/CLDC11/src/java/nio/file/StandardCopyOption.java b/Ports/CLDC11/src/java/nio/file/StandardCopyOption.java new file mode 100644 index 00000000000..e7f564abb0e --- /dev/null +++ b/Ports/CLDC11/src/java/nio/file/StandardCopyOption.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.nio.file; + +/// How a file is copied or moved. +/// `REPLACE_EXISTING` is honoured; `COPY_ATTRIBUTES` and `ATOMIC_MOVE` are +/// accepted and have no effect. +public enum StandardCopyOption implements CopyOption { + REPLACE_EXISTING, + COPY_ATTRIBUTES, + ATOMIC_MOVE +} diff --git a/Ports/CLDC11/src/java/nio/file/StandardOpenOption.java b/Ports/CLDC11/src/java/nio/file/StandardOpenOption.java new file mode 100644 index 00000000000..b86be1e717a --- /dev/null +++ b/Ports/CLDC11/src/java/nio/file/StandardOpenOption.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.nio.file; + +/// How a file is opened. +/// +/// `READ`, `WRITE`, `APPEND`, `TRUNCATE_EXISTING`, `CREATE` and `CREATE_NEW` +/// mean what they mean in the JDK. `DELETE_ON_CLOSE`, `SPARSE`, `SYNC` and +/// `DSYNC` are accepted and have no effect: a device writes a file when the +/// stream is closed and has nothing to say about how. +public enum StandardOpenOption implements OpenOption { + READ, + WRITE, + APPEND, + TRUNCATE_EXISTING, + CREATE, + CREATE_NEW, + DELETE_ON_CLOSE, + SPARSE, + SYNC, + DSYNC +} diff --git a/Ports/CLDC11/src/java/nio/file/attribute/BasicFileAttributes.java b/Ports/CLDC11/src/java/nio/file/attribute/BasicFileAttributes.java new file mode 100644 index 00000000000..df3cab26b9b --- /dev/null +++ b/Ports/CLDC11/src/java/nio/file/attribute/BasicFileAttributes.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.nio.file.attribute; + +/// What the device's file system knows about a file. +/// +/// The three times are all the time of the last modification, which is the +/// only one the device records. Nothing is a symbolic link, and there is no +/// file key. +public interface BasicFileAttributes { + + FileTime lastModifiedTime(); + + FileTime lastAccessTime(); + + FileTime creationTime(); + + boolean isRegularFile(); + + boolean isDirectory(); + + boolean isSymbolicLink(); + + boolean isOther(); + + long size(); + + Object fileKey(); +} diff --git a/Ports/CLDC11/src/java/nio/file/attribute/FileAttribute.java b/Ports/CLDC11/src/java/nio/file/attribute/FileAttribute.java new file mode 100644 index 00000000000..9ea9cdfd1fe --- /dev/null +++ b/Ports/CLDC11/src/java/nio/file/attribute/FileAttribute.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.nio.file.attribute; + +/// The +/// methods that create a file take these and a device has none to set, so +/// the type exists for the empty array every such call passes. +public interface FileAttribute { + + String name(); + + T value(); +} diff --git a/Ports/CLDC11/src/java/nio/file/attribute/FileTime.java b/Ports/CLDC11/src/java/nio/file/attribute/FileTime.java new file mode 100644 index 00000000000..8924d06279d --- /dev/null +++ b/Ports/CLDC11/src/java/nio/file/attribute/FileTime.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.nio.file.attribute; + +import java.time.Instant; +import java.util.concurrent.TimeUnit; + +/// A file's +/// time stamp, kept to the millisecond, which is as fine as the device's +/// file system reports it. +public final class FileTime implements Comparable { + + private final long millis; + + private FileTime(long millis) { + this.millis = millis; + } + + public static FileTime fromMillis(long value) { + return new FileTime(value); + } + + public static FileTime from(long value, TimeUnit unit) { + return new FileTime(unit.toMillis(value)); + } + + public static FileTime from(Instant instant) { + return new FileTime(instant.toEpochMilli()); + } + + public long toMillis() { + return millis; + } + + public long to(TimeUnit unit) { + return unit.convert(millis, TimeUnit.MILLISECONDS); + } + + public Instant toInstant() { + return Instant.ofEpochMilli(millis); + } + + @Override + public int compareTo(FileTime other) { + return millis < other.millis ? -1 : millis == other.millis ? 0 : 1; + } + + @Override + public boolean equals(Object o) { + return o instanceof FileTime && ((FileTime) o).millis == millis; + } + + @Override + public int hashCode() { + return (int) (millis ^ (millis >>> 32)); + } + + private static long floorDiv(long x, long y) { + long q = x / y; + return (x % y != 0 && (x < 0) != (y < 0)) ? q - 1 : q; + } + + private static void pad(StringBuilder sb, long value, int width) { + String digits = Long.toString(value); + for (int i = digits.length(); i < width; i++) { + sb.append('0'); + } + sb.append(digits); + } + + /// The time in ISO 8601, in UTC, as the JDK writes it: + /// `2024-02-29T12:30:05.25Z`, the fraction left out when it is zero. + @Override + public String toString() { + long days = floorDiv(millis, 86400000L); + long ofDay = millis - floorDiv(millis, 86400000L) * 86400000L; + // Days since 1970-01-01 to a civil date, in the proleptic Gregorian + // calendar: eras of 400 years, counted from the first of March. + long z = days + 719468L; + long era = floorDiv(z, 146097L); + long doe = z - era * 146097L; + long yoe = (doe - doe / 1460L + doe / 36524L - doe / 146096L) / 365L; + long doy = doe - (365L * yoe + yoe / 4L - yoe / 100L); + long mp = (5L * doy + 2L) / 153L; + long day = doy - (153L * mp + 2L) / 5L + 1L; + long month = mp < 10 ? mp + 3 : mp - 9; + long year = yoe + era * 400L + (month <= 2 ? 1 : 0); + StringBuilder sb = new StringBuilder(30); + if (year < 0) { + sb.append('-'); + pad(sb, -year, 4); + } else { + pad(sb, year, 4); + } + sb.append('-'); + pad(sb, month, 2); + sb.append('-'); + pad(sb, day, 2); + sb.append('T'); + pad(sb, ofDay / 3600000L, 2); + sb.append(':'); + pad(sb, ofDay / 60000L % 60L, 2); + sb.append(':'); + pad(sb, ofDay / 1000L % 60L, 2); + long fraction = ofDay % 1000L; + if (fraction != 0) { + sb.append('.'); + pad(sb, fraction, 3); + while (sb.charAt(sb.length() - 1) == '0') { + sb.setLength(sb.length() - 1); + } + } + return sb.append('Z').toString(); + } +} diff --git a/Ports/CLDC11/src/java/nio/file/attribute/package-info.java b/Ports/CLDC11/src/java/nio/file/attribute/package-info.java new file mode 100644 index 00000000000..465f850991f --- /dev/null +++ b/Ports/CLDC11/src/java/nio/file/attribute/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +/// The attributes of a file: its type, size and time stamp. +package java.nio.file.attribute; diff --git a/Ports/CLDC11/src/java/nio/file/package-info.java b/Ports/CLDC11/src/java/nio/file/package-info.java new file mode 100644 index 00000000000..17825f5ee4f --- /dev/null +++ b/Ports/CLDC11/src/java/nio/file/package-info.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +/// Paths, and the operations on the files and directories they name, over +/// the platform's file system as `java.io.File` reaches it. +/// +/// `Files` reads, writes, copies, moves and walks; `Path` and `Paths` make +/// and take apart paths. The methods of `Files` that answer a stream of +/// lines or paths are not provided; `readAllLines`, `newDirectoryStream` and +/// `walkFileTree` do the same work. +package java.nio.file; diff --git a/Ports/CLDC11/src/java/security/GeneralSecurityException.java b/Ports/CLDC11/src/java/security/GeneralSecurityException.java new file mode 100644 index 00000000000..f5550cbc02a --- /dev/null +++ b/Ports/CLDC11/src/java/security/GeneralSecurityException.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.security; + +/// `java.security.GeneralSecurityException`. +public class GeneralSecurityException extends Exception { + + public GeneralSecurityException() { + } + + public GeneralSecurityException(String message) { + super(message); + } + + public GeneralSecurityException(String message, Throwable cause) { + super(message, cause); + } + + public GeneralSecurityException(Throwable cause) { + super(cause == null ? null : cause.toString(), cause); + } +} diff --git a/Ports/CLDC11/src/java/security/MessageDigest.java b/Ports/CLDC11/src/java/security/MessageDigest.java new file mode 100644 index 00000000000..bb1dda4b26d --- /dev/null +++ b/Ports/CLDC11/src/java/security/MessageDigest.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.security; + +import com.codename1.impl.JdkHost; + +/// A message digest over the platform's own, which +/// `com.codename1.impl.JdkHost` reaches: MD5, SHA-1, SHA-224, SHA-256, +/// SHA-384 and SHA-512 in a Codename One application. Any algorithm the +/// platform lacks is a `NoSuchAlgorithmException`, as it is on a JDK without +/// a provider for it. +/// +/// There are no providers here: `getInstance` with a provider argument, +/// `getProvider` and a subclass of this class are not offered. +public final class MessageDigest { + private final String algorithm; + private final JdkHost.Digest hash; + + private MessageDigest(String algorithm, JdkHost.Digest hash) { + this.algorithm = algorithm; + this.hash = hash; + } + + public static MessageDigest getInstance(String algorithm) throws NoSuchAlgorithmException { + if (algorithm == null) { + throw new NullPointerException("null algorithm name"); + } + String name = null; + if ("MD5".equalsIgnoreCase(algorithm)) { + name = "MD5"; + } else if ("SHA-1".equalsIgnoreCase(algorithm) || "SHA1".equalsIgnoreCase(algorithm) + || "SHA".equalsIgnoreCase(algorithm)) { + name = "SHA-1"; + } else if ("SHA-224".equalsIgnoreCase(algorithm) || "SHA224".equalsIgnoreCase(algorithm)) { + name = "SHA-224"; + } else if ("SHA-256".equalsIgnoreCase(algorithm) || "SHA256".equalsIgnoreCase(algorithm)) { + name = "SHA-256"; + } else if ("SHA-384".equalsIgnoreCase(algorithm) || "SHA384".equalsIgnoreCase(algorithm)) { + name = "SHA-384"; + } else if ("SHA-512".equalsIgnoreCase(algorithm) || "SHA512".equalsIgnoreCase(algorithm)) { + name = "SHA-512"; + } + JdkHost.Digest digest = name == null ? null : JdkHost.get().digest(name); + if (digest == null) { + throw new NoSuchAlgorithmException(algorithm + " MessageDigest not available"); + } + return new MessageDigest(algorithm, digest); + } + + public void update(byte input) { + hash.update(new byte[] {input}, 0, 1); + } + + public void update(byte[] input) { + hash.update(input, 0, input.length); + } + + public void update(byte[] input, int offset, int len) { + if (input == null) { + throw new IllegalArgumentException("No input buffer given"); + } + if (offset < 0 || len < 0 || input.length - offset < len) { + throw new IllegalArgumentException("Input buffer too short"); + } + hash.update(input, offset, len); + } + + /// The digest of everything given so far; the next one starts empty. + public byte[] digest() { + byte[] out = hash.digest(); + hash.reset(); + return out; + } + + public byte[] digest(byte[] input) { + update(input); + return digest(); + } + + public void reset() { + hash.reset(); + } + + public String getAlgorithm() { + return algorithm; + } + + public int getDigestLength() { + return hash.length(); + } + + /// Whether two digests are the same, in a time that does not depend + /// on where they first differ. + public static boolean isEqual(byte[] a, byte[] b) { + if (a == b) { + return true; + } + if (a == null || b == null) { + return false; + } + if (b.length == 0) { + return a.length == 0; + } + int diff = a.length ^ b.length; + for (int i = 0; i < a.length; i++) { + diff |= a[i] ^ b[i % b.length]; + } + return diff == 0; + } + + @Override + public String toString() { + return algorithm + " Message Digest from the platform, \n"; + } +} diff --git a/Ports/CLDC11/src/java/security/NoSuchAlgorithmException.java b/Ports/CLDC11/src/java/security/NoSuchAlgorithmException.java new file mode 100644 index 00000000000..ff0379e7a62 --- /dev/null +++ b/Ports/CLDC11/src/java/security/NoSuchAlgorithmException.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.security; + +/// `java.security.NoSuchAlgorithmException`. +public class NoSuchAlgorithmException extends GeneralSecurityException { + + public NoSuchAlgorithmException() { + } + + public NoSuchAlgorithmException(String message) { + super(message); + } + + public NoSuchAlgorithmException(String message, Throwable cause) { + super(message, cause); + } + + public NoSuchAlgorithmException(Throwable cause) { + super(cause); + } +} diff --git a/Ports/CLDC11/src/java/security/SecureRandom.java b/Ports/CLDC11/src/java/security/SecureRandom.java new file mode 100644 index 00000000000..fbb1e83997b --- /dev/null +++ b/Ports/CLDC11/src/java/security/SecureRandom.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.security; + +import com.codename1.impl.JdkHost; + +import java.util.Random; + +/// A cryptographically strong random number generator over the platform's +/// own source of secure random bytes, which `com.codename1.impl.JdkHost` +/// reaches: the one `com.codename1.security.SecureRandom` reads in a Codename +/// One application, `/dev/urandom` for a program translated without it. +/// +/// A seed adds nothing: the platform's generator seeds itself, and +/// `setSeed` is accepted and ignored rather than made to weaken it. +public class SecureRandom extends Random { + private static final long serialVersionUID = 1L; + + public SecureRandom() { + super(0); + } + + /// The seed is ignored; see the class description. + public SecureRandom(byte[] seed) { + super(0); + } + + /// The platform's generator, which never blocks. + public static SecureRandom getInstanceStrong() { + return new SecureRandom(); + } + + @Override + public void setSeed(long seed) { + // Ignored: see the class description. + } + + /// Ignored: see the class description. + public void setSeed(byte[] seed) { + } + + public void nextBytes(byte[] bytes) { + JdkHost.get().randomBytes(bytes); + } + + /// `numBytes` fresh random bytes, where the JDK answers seed material. + public byte[] generateSeed(int numBytes) { + if (numBytes < 0) { + throw new IllegalArgumentException("numBytes cannot be negative"); + } + byte[] out = new byte[numBytes]; + JdkHost.get().randomBytes(out); + return out; + } + + @Override + protected int next(int bits) { + byte[] four = new byte[4]; + JdkHost.get().randomBytes(four); + int value = (four[0] & 0xFF) << 24 | (four[1] & 0xFF) << 16 | (four[2] & 0xFF) << 8 | (four[3] & 0xFF); + return bits >= 32 ? value : value >>> (32 - bits); + } + + public String getAlgorithm() { + return "NativePRNG"; + } +} diff --git a/Ports/CLDC11/src/java/text/ChoiceFormat.java b/Ports/CLDC11/src/java/text/ChoiceFormat.java new file mode 100644 index 00000000000..f88312cacc5 --- /dev/null +++ b/Ports/CLDC11/src/java/text/ChoiceFormat.java @@ -0,0 +1,337 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.text; + +/// Picks a string by the range a number falls in. It is what the `choice` element of a +/// [MessageFormat] pattern uses to write plurals. +/// +/// A pattern is a list of `limit#text` pairs separated by `|`, the limits +/// ascending: `0#no files|1#one file|1<{0} files`. A number selects the last +/// pair whose limit it reaches; `#` includes the limit itself and `<` starts +/// just above it. A number below the first limit selects the first pair. A +/// single quote makes the characters up to the next one literal, and two in a +/// row stand for one quote. +public class ChoiceFormat extends NumberFormat { + + private static final long serialVersionUID = 1L; + + private static final char LESS_OR_EQUAL = (char) 0x2264; + private static final String INFINITY = String.valueOf((char) 0x221E); + + private static final long SIGN_BIT = 0x8000000000000000L; + private static final long POSITIVE_INFINITY_BITS = 0x7FF0000000000000L; + + private double[] choiceLimits = new double[0]; + private String[] choiceFormats = new String[0]; + + public ChoiceFormat(String newPattern) { + parsePattern(newPattern); + } + + public ChoiceFormat(double[] limits, String[] formats) { + store(limits, formats); + } + + public void applyPattern(String newPattern) { + parsePattern(newPattern); + } + + private void parsePattern(String newPattern) { + StringBuilder limitText = new StringBuilder(); + StringBuilder formatText = new StringBuilder(); + double[] limits = new double[8]; + String[] formats = new String[8]; + int count = 0; + boolean inFormat = false; + boolean inQuote = false; + double startValue = 0; + double oldStartValue = Double.NaN; + for (int i = 0; i < newPattern.length(); i++) { + char ch = newPattern.charAt(i); + StringBuilder current = inFormat ? formatText : limitText; + if (ch == '\'') { + if (i + 1 < newPattern.length() && newPattern.charAt(i + 1) == ch) { + current.append(ch); + i++; + } else { + inQuote = !inQuote; + } + } else if (inQuote) { + current.append(ch); + } else if (ch == '<' || ch == '#' || ch == LESS_OR_EQUAL) { + if (limitText.length() == 0) { + throw new IllegalArgumentException("A choice has no limit: " + newPattern); + } + startValue = parseLimit(limitText.toString().trim(), newPattern); + if (ch == '<' && !Double.isInfinite(startValue)) { + startValue = nextDouble(startValue); + } + if (startValue <= oldStartValue) { + throw new IllegalArgumentException("Choice limits are not ascending: " + newPattern); + } + limitText.setLength(0); + inFormat = true; + } else if (ch == '|') { + if (count == limits.length) { + double[] moreLimits = new double[count * 2]; + String[] moreFormats = new String[count * 2]; + System.arraycopy(limits, 0, moreLimits, 0, count); + System.arraycopy(formats, 0, moreFormats, 0, count); + limits = moreLimits; + formats = moreFormats; + } + limits[count] = startValue; + formats[count] = formatText.toString(); + count++; + oldStartValue = startValue; + formatText.setLength(0); + inFormat = false; + } else { + current.append(ch); + } + } + if (inFormat) { + if (count == limits.length) { + double[] moreLimits = new double[count + 1]; + String[] moreFormats = new String[count + 1]; + System.arraycopy(limits, 0, moreLimits, 0, count); + System.arraycopy(formats, 0, moreFormats, 0, count); + limits = moreLimits; + formats = moreFormats; + } + limits[count] = startValue; + formats[count] = formatText.toString(); + count++; + } + choiceLimits = new double[count]; + System.arraycopy(limits, 0, choiceLimits, 0, count); + choiceFormats = new String[count]; + System.arraycopy(formats, 0, choiceFormats, 0, count); + } + + private static double parseLimit(String text, String pattern) { + if (text.equals(INFINITY)) { + return Double.POSITIVE_INFINITY; + } + if (text.equals("-" + INFINITY)) { + return Double.NEGATIVE_INFINITY; + } + try { + return Double.parseDouble(text); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("A choice limit is not a number: " + pattern); + } + } + + private static double distanceToInteger(double value) { + return Math.abs(value - Math.floor(value + 0.5)); + } + + public String toPattern() { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < choiceLimits.length; i++) { + if (i != 0) { + result.append('|'); + } + // Either "limit#" or "justBelow<" says the same thing; the one + // that is closer to a whole number is the one that was written. + double limit = choiceLimits[i]; + double less = previousDouble(limit); + if (distanceToInteger(limit) < distanceToInteger(less)) { + appendLimit(result, limit); + result.append('#'); + } else { + appendLimit(result, less); + result.append('<'); + } + String text = choiceFormats[i]; + boolean needQuote = text.indexOf('<') >= 0 || text.indexOf('#') >= 0 + || text.indexOf(LESS_OR_EQUAL) >= 0 || text.indexOf('|') >= 0; + if (needQuote) { + result.append('\''); + } + for (int k = 0; k < text.length(); k++) { + char c = text.charAt(k); + result.append(c); + if (c == '\'') { + result.append(c); + } + } + if (needQuote) { + result.append('\''); + } + } + return result.toString(); + } + + private static void appendLimit(StringBuilder out, double limit) { + if (limit == Double.POSITIVE_INFINITY) { + out.append(INFINITY); + } else if (limit == Double.NEGATIVE_INFINITY) { + out.append('-').append(INFINITY); + } else { + out.append(Double.toString(limit)); + } + } + + public void setChoices(double[] limits, String[] formats) { + store(limits, formats); + } + + private void store(double[] limits, String[] formats) { + if (limits.length != formats.length) { + throw new IllegalArgumentException("Array and limit arrays must be of the same length."); + } + choiceLimits = limits.clone(); + choiceFormats = formats.clone(); + } + + public double[] getLimits() { + return choiceLimits.clone(); + } + + public Object[] getFormats() { + return choiceFormats.clone(); + } + + @Override + public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition status) { + return format((double) number, toAppendTo, status); + } + + @Override + public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition status) { + if (choiceFormats.length == 0) { + return toAppendTo; + } + int i = 0; + // Written so that not-a-number, which is below nothing, selects the + // first choice. + while (i < choiceLimits.length && number >= choiceLimits[i]) { + i++; + } + i--; + if (i < 0) { + i = 0; + } + return toAppendTo.append(choiceFormats[i]); + } + + /// Finds the choice whose text starts at the position, the longest if + /// several do, and answers its limit as a `Double`. When none does, the + /// error index is set and the answer is not-a-number. + @Override + public Number parse(String text, ParsePosition status) { + int start = status.getIndex(); + int furthest = start; + double bestNumber = Double.NaN; + for (int i = 0; i < choiceFormats.length; i++) { + String candidate = choiceFormats[i]; + if (text.regionMatches(start, candidate, 0, candidate.length())) { + int end = start + candidate.length(); + if (end > furthest) { + furthest = end; + bestNumber = choiceLimits[i]; + if (furthest == text.length()) { + break; + } + } + } + } + status.setIndex(furthest); + if (furthest == start) { + status.setErrorIndex(furthest); + } + return Double.valueOf(bestNumber); + } + + public static final double nextDouble(double d) { + return nextDouble(d, true); + } + + public static final double previousDouble(double d) { + return nextDouble(d, false); + } + + /// The `double` next to `d`: the next greater when `positive`, else the + /// next smaller. + public static double nextDouble(double d, boolean positive) { + if (Double.isNaN(d)) { + return d; + } + if (d == 0.0) { + double smallest = Double.longBitsToDouble(1L); + return positive ? smallest : -smallest; + } + long bits = Double.doubleToLongBits(d); + long magnitude = bits & ~SIGN_BIT; + if ((bits > 0) == positive) { + if (magnitude != POSITIVE_INFINITY_BITS) { + magnitude += 1; + } + } else { + magnitude -= 1; + } + return Double.longBitsToDouble(magnitude | (bits & SIGN_BIT)); + } + + /// A copy of this format, built from copies of its arrays: + /// `Object.clone()` answers `null` on the device for anything but an + /// array. + @Override + public Object clone() { + ChoiceFormat copy = new ChoiceFormat(choiceLimits, choiceFormats); + copyNumberFormatTo(copy); + return copy; + } + + @Override + public int hashCode() { + int result = choiceLimits.length; + if (choiceFormats.length > 0) { + result ^= choiceFormats[choiceFormats.length - 1].hashCode(); + } + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof ChoiceFormat) || getClass() != obj.getClass()) { + return false; + } + ChoiceFormat other = (ChoiceFormat) obj; + if (choiceLimits.length != other.choiceLimits.length) { + return false; + } + for (int i = 0; i < choiceLimits.length; i++) { + if (Double.doubleToLongBits(choiceLimits[i]) != Double.doubleToLongBits(other.choiceLimits[i]) + || !choiceFormats[i].equals(other.choiceFormats[i])) { + return false; + } + } + return true; + } +} diff --git a/Ports/CLDC11/src/java/text/DateFormat.java b/Ports/CLDC11/src/java/text/DateFormat.java index a67682db9f9..2ba03ff4789 100644 --- a/Ports/CLDC11/src/java/text/DateFormat.java +++ b/Ports/CLDC11/src/java/text/DateFormat.java @@ -64,20 +64,49 @@ public class DateFormat extends Format { } - /// Format a given object. + /// Format a given object, a date or a number of milliseconds since the epoch. + /// + /// #### Parameters /// - /// Parameter `obj`: object to be formatted. + /// - `obj`: object to be formatted. + /// + /// - `toAppendTo`: buffer to which to append output. + /// + /// - `fieldPosition`: not updated: no field is reported. /// /// #### Returns /// - /// formatted object. + /// `toAppendTo`. /// /// #### Throws /// - /// - `IllegalArgumentException`: of the source can not be formatted. + /// - `IllegalArgumentException`: if the source is neither a date nor a number. @Override - public String format(Object obj) throws IllegalArgumentException { - return format(obj, new StringBuffer()); + public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition fieldPosition) { + if (obj instanceof Date) { + return format((Date) obj, toAppendTo, fieldPosition); + } + if (obj instanceof Number) { + return format(new Date(((Number) obj).longValue()), toAppendTo, fieldPosition); + } + throw new IllegalArgumentException("Cannot format given Object as a Date"); + } + + /// Format a given date. + /// + /// #### Parameters + /// + /// - `date`: date to be formatted. + /// + /// - `toAppendTo`: buffer to which to append output. + /// + /// - `fieldPosition`: not updated: no field is reported. + /// + /// #### Returns + /// + /// `toAppendTo`. + public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition) { + return toAppendTo.append(format(date)); } /// Format a given object. @@ -127,16 +156,44 @@ String format(Date source, StringBuffer toAppendTo) { return null; } - /// NOT IMPLEMENTED - use SimpleDateFormat for parsing instead. + /// Parse a date from the beginning of the source. NOT IMPLEMENTED for the + /// style based formats - use SimpleDateFormat for parsing instead. @Override public Object parseObject(String source) throws ParseException { - // can't parse because we don't know the L10NManagers templates - throw new ParseException("Not implemented", 0); + return parse(source); } - /// NOT IMPLEMENTED - use SimpleDateFormat for parsing instead. + /// Parse a date from the beginning of the source. NOT IMPLEMENTED for the + /// style based formats - use SimpleDateFormat for parsing instead. public Date parse(String source) throws ParseException { - return (Date) parseObject(source); + return parseAt(source, 0, null); + } + + /// Parse a date starting at a position. On success the index of the + /// position is moved past the date; on failure it is left alone, its + /// error index is set and null is returned. + public Date parse(String source, ParsePosition pos) { + int[] end = new int[1]; + try { + Date result = parseAt(source, pos.getIndex(), end); + pos.setIndex(end[0]); + return result; + } catch (ParseException e) { + pos.setErrorIndex(e.getErrorOffset()); + return null; + } + } + + @Override + public Object parseObject(String source, ParsePosition pos) { + return parse(source, pos); + } + + /// Parse a date at index start of source and, when end is not null, store + /// in end[0] the index just past it. + Date parseAt(String source, int start, int[] end) throws ParseException { + // can't parse because we don't know the L10NManagers templates + throw new ParseException("Not implemented", start); } /// Get a DateFormat instance with default style for date/time (SHORT/SHORT). diff --git a/Ports/CLDC11/src/java/text/DecimalFormat.java b/Ports/CLDC11/src/java/text/DecimalFormat.java new file mode 100644 index 00000000000..84c1769b1ce --- /dev/null +++ b/Ports/CLDC11/src/java/text/DecimalFormat.java @@ -0,0 +1,1463 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.text; + +/// Formats and parses decimal numbers according to a pattern. +/// +/// #### Pattern syntax +/// +/// A pattern is a positive subpattern, optionally followed by `;` and a +/// negative subpattern of which only the prefix and suffix are used. Each +/// subpattern is a prefix, a number and a suffix. In the number: +/// +/// - `0` is a digit that is always written, `#` a digit written only when +/// it is significant. +/// - `.` is the decimal separator and `,` the grouping separator; the +/// distance from the last `,` to the end of the integer part is the group +/// size. +/// - `E` followed by one or more `0` selects scientific notation, with at +/// least that many exponent digits. With more integer digit positions than +/// `0`s among them (`##0.###E0`), the exponent is kept a multiple of the +/// number of integer positions. +/// +/// In a prefix or suffix, `%` multiplies the number by 100 and writes a +/// percent sign, the per mille sign multiplies by 1000, the currency sign +/// writes the currency symbol (doubled, the international currency code), and +/// `-` writes the minus sign. A single quote makes the characters up to the +/// next one literal, and two in a row stand for one quote. +/// +/// The characters actually written -- separators, minus, percent, currency +/// -- come from the format's [DecimalFormatSymbols]. +/// +/// #### Rounding +/// +/// Rounding is half-even, the JDK default, and like the JDK it is decided by +/// the exact binary value of a `double`: `0.125` is a true tie and formats as +/// `0.12` under `0.00`, while `0.15`, which as a `double` lies just below the +/// decimal `0.15`, formats as `0.1` under `0.0`. +/// +/// The JDK departs from half-even in two places, and so does this class, +/// because a last digit that differs from the desktop's is worse than either +/// answer: a whole number rounds a tie up in scientific notation (`12345` +/// under `0.###E0` is `1.235E4`), and `5E-4`, `5E-5` and so on round down +/// when the 5 is the first digit dropped (`0.0005` under `0.000` is `0.000`). +/// +/// #### Differences from the JDK +/// +/// - No `setRoundingMode`, `setCurrency`, `setParseBigDecimal` or +/// `formatToCharacterIterator`: the device has none of the types they name. +/// - No localized patterns (`applyLocalizedPattern`, `toLocalizedPattern`). +public class DecimalFormat extends NumberFormat { + + private static final long serialVersionUID = 1L; + + /// The currency sign of a pattern. Not written as a character literal: + /// the sources are ASCII. + static final char CURRENCY_SIGN = (char) 0xA4; + + private static final char PER_MILLE_SIGN = (char) 0x2030; + private static final char QUOTE = '\''; + + /// A `double` has no more integer digits than this, nor fraction digits + /// than the next; the JDK caps the settings there for a `double` as well. + private static final int DOUBLE_INTEGER_DIGITS = 309; + private static final int DOUBLE_FRACTION_DIGITS = 340; + + private DecimalFormatSymbols symbols; + + // The affixes as the pattern gave them, in an internal form where a quote + // precedes each character to be replaced by a symbol and '' is a quote. + private String posPrefixPattern = ""; + private String posSuffixPattern = ""; + private String negPrefixPattern = "'-"; + private String negSuffixPattern = ""; + + // The same affixes with the symbols filled in. + private String positivePrefix = ""; + private String positiveSuffix = ""; + private String negativePrefix = "-"; + private String negativeSuffix = ""; + + private int multiplier = 1; + private int groupingSize = 3; + private boolean decimalSeparatorAlwaysShown; + private boolean useExponentialNotation; + private int minExponentDigits; + private boolean currencyFormat; + + public DecimalFormat() { + this("#,##0.###", DecimalFormatSymbols.getInstance()); + } + + public DecimalFormat(String pattern) { + this(pattern, DecimalFormatSymbols.getInstance()); + } + + public DecimalFormat(String pattern, DecimalFormatSymbols symbols) { + if (symbols == null) { + throw new NullPointerException(); + } + Object copy = symbols.clone(); + this.symbols = copy instanceof DecimalFormatSymbols ? (DecimalFormatSymbols) copy : symbols; + parsePattern(pattern); + } + + // ------------------------------------------------------------------ + // Digits + // ------------------------------------------------------------------ + + /// A non-negative decimal number: `count` significant digits, with the + /// decimal point `decimalAt` digits from the left (so the value is + /// 0.d1d2... times ten to the `decimalAt`). No trailing zeros; zero is no + /// digits at all. + private static final class Digits { + /// The digits are the number: a tie goes to the even neighbour. + static final int TIE_EVEN = 0; + /// The digits are the shortest text of a `double`: a tie is settled + /// by where the `double` really lies, and is even only if it lies + /// exactly there. + static final int TIE_BY_VALUE = 1; + static final int TIE_UP = 2; + static final int TIE_DOWN = 3; + + private char[] d = new char[24]; + private int count; + private int decimalAt; + + private void append(char c) { + if (count == d.length) { + char[] bigger = new char[d.length * 2]; + System.arraycopy(d, 0, bigger, 0, count); + d = bigger; + } + d[count++] = c; + } + + private void trim() { + while (count > 0 && d[count - 1] == '0') { + count--; + } + if (count == 0) { + decimalAt = 0; + } + } + + /// Takes plain decimal digits, an integer. + void setInteger(String digits) { + count = 0; + int i = 0; + while (i < digits.length() && digits.charAt(i) == '0') { + i++; + } + decimalAt = digits.length() - i; + for (; i < digits.length(); i++) { + append(digits.charAt(i)); + } + trim(); + } + + /// Takes the digits of a finite, non-negative `double` as the JDK's + /// `DecimalFormat` sees them. Those are NOT the shortest digits + /// `Double.toString` writes (`1.0E23` is `9.999999999999999E22` + /// here); see `LegacyDoubleDigits` for why. + void setDouble(double value) { + count = 0; + decimalAt = 0; + // Zero has no digits. So does NaN, which reaches here from an + // infinity times a multiplier of zero. + if (!(value > 0.0)) { + return; + } + LegacyDoubleDigits converted = new LegacyDoubleDigits(value); + for (int i = 0; i < converted.count; i++) { + append(converted.digits[converted.first + i]); + } + decimalAt = converted.decExponent; + trim(); + } + + /// Multiplies by a positive integer. + void multiply(int by) { + if (count == 0) { + return; + } + char[] out = new char[count + 11]; + int at = out.length; + long carry = 0; + for (int i = count - 1; i >= 0; i--) { + long v = (long) (d[i] - '0') * by + carry; + out[--at] = (char) ('0' + (int) (v % 10)); + carry = v / 10; + } + while (carry > 0) { + out[--at] = (char) ('0' + (int) (carry % 10)); + carry = carry / 10; + } + int produced = out.length - at; + decimalAt += produced - count; + count = 0; + for (int i = at; i < out.length; i++) { + append(out[i]); + } + trim(); + } + + /// Keeps the first `keep` digits, rounding half-even. `tie` says how + /// digits that end in exactly a half are settled; `value` is the + /// `double` they were taken from, for [#TIE_BY_VALUE]. + void round(int keep, int tie, double value) { + if (keep >= count) { + return; + } + if (keep < 0) { + count = 0; + decimalAt = 0; + return; + } + if (roundsUp(keep, tie, value)) { + int i = keep - 1; + while (i >= 0 && d[i] == '9') { + i--; + } + if (i < 0) { + d[0] = '1'; + count = 1; + decimalAt++; + } else { + d[i]++; + count = i + 1; + } + } else { + count = keep; + } + trim(); + } + + private boolean roundsUp(int keep, int tie, double value) { + char next = d[keep]; + if (next != '5') { + return next > '5'; + } + if (keep + 1 < count) { + // Past the 5 there is something, and it is not zero. + return true; + } + // The digits end in exactly a half. + if (tie == TIE_UP) { + return true; + } + if (tie == TIE_DOWN) { + return false; + } + // A long is those digits. A double is only the nearest double to + // them, so the true value is compared against the half it + // appears to be. + int side = tie == TIE_EVEN ? 0 : compareToDigits(value); + if (side != 0) { + return side > 0; + } + return keep > 0 && ((d[keep - 1] - '0') & 1) == 1; + } + + /// The sign of `value` minus the number these digits spell. + private int compareToDigits(double value) { + long bits = Double.doubleToLongBits(value); + int rawExponent = (int) ((bits >> 52) & 0x7ffL); + long mantissa = bits & 0xfffffffffffffL; + int binaryExponent; + if (rawExponent == 0) { + binaryExponent = -1074; + } else { + mantissa |= 1L << 52; + binaryExponent = rawExponent - 1075; + } + // value = mantissa * 2^binaryExponent, digits = n * 10^decimalExponent. + // Negative powers move to the other side, leaving integers. + Magnitude left = Magnitude.ofLong(mantissa); + Magnitude right = Magnitude.ofDigits(d, count); + int decimalExponent = decimalAt - count; + if (binaryExponent > 0) { + left.shiftLeft(binaryExponent); + } else if (binaryExponent < 0) { + right.shiftLeft(-binaryExponent); + } + if (decimalExponent > 0) { + right.multiplyByPowerOfTen(decimalExponent); + } else if (decimalExponent < 0) { + left.multiplyByPowerOfTen(-decimalExponent); + } + return left.compareTo(right); + } + } + + /// A non-negative integer of any size, just enough of one to compare a + /// `double` with the decimal digits it was written as: the device has no + /// `java.math`. Little-endian limbs in base one billion, so a limb times + /// any factor up to two to the 30th plus a carry fits a `long`. + private static final class Magnitude { + private static final int BASE = 1000000000; + private static final int SHIFT_STEP = 30; + + private int[] limbs = new int[4]; + private int length; + + static Magnitude ofLong(long value) { + Magnitude m = new Magnitude(); + long v = value; + while (v > 0) { + m.push((int) (v % BASE)); + v = v / BASE; + } + return m; + } + + /// The integer the first `count` ASCII digits of `digits` spell. + static Magnitude ofDigits(char[] digits, int count) { + Magnitude m = new Magnitude(); + int end = count; + while (end > 0) { + int start = Math.max(0, end - 9); + int limb = 0; + for (int i = start; i < end; i++) { + limb = limb * 10 + (digits[i] - '0'); + } + m.push(limb); + end = start; + } + m.normalize(); + return m; + } + + private void push(int limb) { + if (length == limbs.length) { + int[] bigger = new int[limbs.length * 2]; + System.arraycopy(limbs, 0, bigger, 0, length); + limbs = bigger; + } + limbs[length++] = limb; + } + + private void normalize() { + while (length > 0 && limbs[length - 1] == 0) { + length--; + } + } + + /// Multiplies by a factor from 1 to two to the 30th. + private void multiplySmall(int factor) { + long carry = 0; + for (int i = 0; i < length; i++) { + long v = (long) limbs[i] * factor + carry; + limbs[i] = (int) (v % BASE); + carry = v / BASE; + } + while (carry > 0) { + push((int) (carry % BASE)); + carry = carry / BASE; + } + } + + void shiftLeft(int bits) { + int left = bits; + while (left >= SHIFT_STEP) { + multiplySmall(1 << SHIFT_STEP); + left -= SHIFT_STEP; + } + if (left > 0) { + multiplySmall(1 << left); + } + } + + void multiplyByPowerOfTen(int exponent) { + int left = exponent; + while (left >= 9) { + multiplySmall(BASE); + left -= 9; + } + int factor = 1; + for (int i = 0; i < left; i++) { + factor *= 10; + } + if (factor > 1) { + multiplySmall(factor); + } + } + + int compareTo(Magnitude other) { + if (length != other.length) { + return length < other.length ? -1 : 1; + } + for (int i = length - 1; i >= 0; i--) { + if (limbs[i] != other.limbs[i]) { + return limbs[i] < other.limbs[i] ? -1 : 1; + } + } + return 0; + } + } + + // ------------------------------------------------------------------ + // Formatting + // ------------------------------------------------------------------ + + @Override + public StringBuffer format(double number, StringBuffer result, FieldPosition fieldPosition) { + fieldPosition.setBeginIndex(0); + fieldPosition.setEndIndex(0); + if (Double.isNaN(number)) { + int begin = result.length(); + result.append(symbols.getNaN()); + if (fieldPosition.getField() == INTEGER_FIELD) { + fieldPosition.setBeginIndex(begin); + fieldPosition.setEndIndex(result.length()); + } + return result; + } + // The sign bit, so that negative zero is negative. + boolean negative = (Double.doubleToLongBits(number) < 0) ^ (multiplier < 0); + double value = number; + if (multiplier != 1) { + value = value * multiplier; + } + if (Double.isInfinite(value)) { + result.append(negative ? negativePrefix : positivePrefix); + int begin = result.length(); + result.append(symbols.getInfinity()); + if (fieldPosition.getField() == INTEGER_FIELD) { + fieldPosition.setBeginIndex(begin); + fieldPosition.setEndIndex(result.length()); + } + result.append(negative ? negativeSuffix : positiveSuffix); + return result; + } + value = Math.abs(value); + int maxInt = Math.min(getMaximumIntegerDigits(), DOUBLE_INTEGER_DIGITS); + int minInt = Math.min(getMinimumIntegerDigits(), DOUBLE_INTEGER_DIGITS); + int maxFrac = Math.min(getMaximumFractionDigits(), DOUBLE_FRACTION_DIGITS); + int minFrac = Math.min(getMinimumFractionDigits(), DOUBLE_FRACTION_DIGITS); + Digits digits = new Digits(); + digits.setDouble(value); + // Two places where the JDK does not round half-even, kept because + // output that differs from the desktop's in the last digit is worse + // than either answer. A whole number below two to the 63rd rounds a + // tie up (12345 under 0.###E0 is 1.235E4), and 5E-4, 5E-5, ... round + // down when the 5 is the first digit dropped (0.0005 under 0.000 is + // 0.000, though 0.005 under 0.00 is 0.01). + int tie = Digits.TIE_BY_VALUE; + if (value < 9.2E18 && (double) (long) value == value) { + tie = Digits.TIE_UP; + } + if (useExponentialNotation) { + digits.round(maxInt + maxFrac, tie, value); + } else { + int keep = digits.decimalAt + maxFrac; + if (keep == 0 && value < 1.0E-3 && !isCommonShape(maxInt, minInt, maxFrac, minFrac)) { + tie = Digits.TIE_DOWN; + } + digits.round(keep, tie, value); + } + return subformat(result, fieldPosition, digits, negative, false, maxInt, minInt, maxFrac, minFrac); + } + + /// The shapes the JDK formats by a separate, exact route -- the default + /// number and currency patterns -- where neither exception applies. + private boolean isCommonShape(int maxInt, int minInt, int maxFrac, int minFrac) { + if (!isGroupingUsed() || groupingSize != 3 || multiplier != 1 || decimalSeparatorAlwaysShown + || minInt != 1 || maxInt < 10) { + return false; + } + return currencyFormat ? minFrac == 2 && maxFrac == 2 : minFrac == 0 && maxFrac == 3; + } + + @Override + public StringBuffer format(long number, StringBuffer result, FieldPosition fieldPosition) { + fieldPosition.setBeginIndex(0); + fieldPosition.setEndIndex(0); + boolean negative = number < 0; + String text = Long.toString(number); + Digits digits = new Digits(); + digits.setInteger(negative ? text.substring(1) : text); + if (multiplier != 1) { + if (multiplier == 0) { + digits.setInteger("0"); + } else if (multiplier < 0) { + negative = !negative; + if (multiplier == Integer.MIN_VALUE) { + digits.multiply(1 << 30); + digits.multiply(2); + } else { + digits.multiply(-multiplier); + } + } else { + digits.multiply(multiplier); + } + } + int maxInt = getMaximumIntegerDigits(); + int minInt = getMinimumIntegerDigits(); + int maxFrac = getMaximumFractionDigits(); + int minFrac = getMinimumFractionDigits(); + if (useExponentialNotation) { + int significant = maxInt + maxFrac; + if (significant > 0) { + digits.round(significant, Digits.TIE_EVEN, 0); + } + } + if (digits.count == 0) { + // An integer zero has no sign. + negative = false; + } + return subformat(result, fieldPosition, digits, negative, true, maxInt, minInt, maxFrac, minFrac); + } + + private char digitChar(char asciiDigit) { + return (char) (asciiDigit - '0' + symbols.getZeroDigit()); + } + + private StringBuffer subformat(StringBuffer result, FieldPosition fieldPosition, Digits digits, + boolean negative, boolean isInteger, + int maxInt, int minInt, int maxFrac, int minFrac) { + char zero = symbols.getZeroDigit(); + char decimal = currencyFormat ? symbols.getMonetaryDecimalSeparator() : symbols.getDecimalSeparator(); + result.append(negative ? negativePrefix : positivePrefix); + int integerBegin = result.length(); + int integerEnd = -1; + int fractionBegin = -1; + if (useExponentialNotation) { + // The exponent is what leaves the wanted number of integer + // digits; with more integer positions than required digits it is + // also kept a multiple of the number of positions. + int exponent = digits.decimalAt; + int repeat = maxInt; + int minimumIntegerDigits = minInt; + if (repeat > 1 && repeat > minInt) { + if (exponent >= 1) { + exponent = ((exponent - 1) / repeat) * repeat; + } else { + exponent = ((exponent - repeat) / repeat) * repeat; + } + minimumIntegerDigits = 1; + } else { + exponent -= minimumIntegerDigits; + } + int minimumDigits = minInt + minFrac; + if (minimumDigits < 0) { + minimumDigits = Integer.MAX_VALUE; + } + int integerDigits = digits.count == 0 ? minimumIntegerDigits : digits.decimalAt - exponent; + if (minimumDigits < integerDigits) { + minimumDigits = integerDigits; + } + int totalDigits = Math.max(digits.count, minimumDigits); + boolean wroteDecimal = false; + for (int i = 0; i < totalDigits; i++) { + if (i == integerDigits) { + integerEnd = result.length(); + result.append(decimal); + wroteDecimal = true; + fractionBegin = result.length(); + } + result.append(i < digits.count ? digitChar(digits.d[i]) : zero); + } + if (decimalSeparatorAlwaysShown && totalDigits == integerDigits) { + integerEnd = result.length(); + result.append(decimal); + wroteDecimal = true; + fractionBegin = result.length(); + } + if (integerEnd < 0) { + integerEnd = result.length(); + } + if (!wroteDecimal) { + fractionBegin = result.length(); + } + int fractionEnd = result.length(); + result.append(symbols.getExponentSeparator()); + if (digits.count == 0) { + exponent = 0; + } + if (exponent < 0) { + exponent = -exponent; + result.append(symbols.getMinusSign()); + } + String exponentText = Integer.toString(exponent); + for (int i = exponentText.length(); i < minExponentDigits; i++) { + result.append(zero); + } + for (int i = 0; i < exponentText.length(); i++) { + result.append(digitChar(exponentText.charAt(i))); + } + setField(fieldPosition, integerBegin, integerEnd, fractionBegin, fractionEnd); + } else { + int count = minInt; + int digitIndex = 0; + if (digits.decimalAt > 0 && count < digits.decimalAt) { + count = digits.decimalAt; + } + // With fewer integer digits allowed than the number has, the + // least significant ones are the ones written: 1997 under a + // maximum of two is "97". + if (count > maxInt) { + count = maxInt; + digitIndex = digits.decimalAt - count; + } + boolean grouping = isGroupingUsed() && groupingSize > 0; + for (int i = count - 1; i >= 0; i--) { + if (i < digits.decimalAt && digitIndex < digits.count) { + result.append(digitChar(digits.d[digitIndex++])); + } else { + result.append(zero); + } + // groupingSize is checked positive just above. + if (grouping && i > 0 && i % groupingSize == 0) { + result.append(symbols.getGroupingSeparator()); + } + } + boolean fractionPresent = minFrac > 0 || (!isInteger && digitIndex < digits.count); + if (!fractionPresent && result.length() == integerBegin) { + result.append(zero); + } + integerEnd = result.length(); + if (decimalSeparatorAlwaysShown || fractionPresent) { + result.append(decimal); + } + fractionBegin = result.length(); + for (int i = 0; i < maxFrac; i++) { + if (i >= minFrac && (isInteger || digitIndex >= digits.count)) { + break; + } + // Zeros between the decimal point and the first digit. + if (-1 - i > digits.decimalAt - 1) { + result.append(zero); + continue; + } + if (!isInteger && digitIndex < digits.count) { + result.append(digitChar(digits.d[digitIndex++])); + } else { + result.append(zero); + } + } + setField(fieldPosition, integerBegin, integerEnd, fractionBegin, result.length()); + } + result.append(negative ? negativeSuffix : positiveSuffix); + return result; + } + + private static void setField(FieldPosition fieldPosition, int integerBegin, int integerEnd, + int fractionBegin, int fractionEnd) { + if (fieldPosition.getField() == INTEGER_FIELD) { + fieldPosition.setBeginIndex(integerBegin); + fieldPosition.setEndIndex(integerEnd); + } else if (fieldPosition.getField() == FRACTION_FIELD) { + fieldPosition.setBeginIndex(fractionBegin); + fieldPosition.setEndIndex(fractionEnd); + } + } + + // ------------------------------------------------------------------ + // Parsing + // ------------------------------------------------------------------ + + private int digitValue(char c) { + int v = c - symbols.getZeroDigit(); + if (v >= 0 && v <= 9) { + return v; + } + v = c - '0'; + return v >= 0 && v <= 9 ? v : -1; + } + + private static boolean matches(String text, int at, String part) { + return text.regionMatches(at, part, 0, part.length()); + } + + @Override + public Number parse(String text, ParsePosition pos) { + int start = pos.getIndex(); + int position = start; + String nan = symbols.getNaN(); + if (nan.length() > 0 && matches(text, position, nan)) { + pos.setIndex(position + nan.length()); + return Double.valueOf(Double.NaN); + } + boolean gotPositive = matches(text, position, positivePrefix); + boolean gotNegative = matches(text, position, negativePrefix); + if (gotPositive && gotNegative) { + if (positivePrefix.length() > negativePrefix.length()) { + gotNegative = false; + } else if (positivePrefix.length() < negativePrefix.length()) { + gotPositive = false; + } + } + if (gotPositive) { + position += positivePrefix.length(); + } else if (gotNegative) { + position += negativePrefix.length(); + } else { + pos.setErrorIndex(position); + return null; + } + + boolean infinite = false; + StringBuilder digits = new StringBuilder(); + int decimalAt = 0; + String infinity = symbols.getInfinity(); + if (infinity.length() > 0 && matches(text, position, infinity)) { + position += infinity.length(); + infinite = true; + } else { + char decimal = currencyFormat ? symbols.getMonetaryDecimalSeparator() : symbols.getDecimalSeparator(); + char grouping = symbols.getGroupingSeparator(); + String exponentText = symbols.getExponentSeparator(); + boolean sawDecimal = false; + boolean sawDigit = false; + int exponent = 0; + int digitCount = 0; + int backup = -1; + for (; position < text.length(); position++) { + char ch = text.charAt(position); + int digit = digitValue(ch); + if (digit >= 0) { + backup = -1; + sawDigit = true; + if (digit == 0 && digits.length() == 0) { + // Leading zeros carry no digits, but after the + // decimal point they shift it. + if (sawDecimal) { + decimalAt--; + } + } else { + digitCount++; + digits.append((char) ('0' + digit)); + } + } else if (ch == decimal) { + if (isParseIntegerOnly() || sawDecimal) { + break; + } + decimalAt = digitCount; + sawDecimal = true; + } else if (ch == grouping && isGroupingUsed()) { + if (sawDecimal) { + break; + } + // A grouping separator must be followed by a digit; if + // it is not, the number ended before it. + backup = position; + } else if (exponentText.length() > 0 && matches(text, position, exponentText)) { + int at = position + exponentText.length(); + boolean negativeExponent = false; + if (at < text.length() && text.charAt(at) == symbols.getMinusSign()) { + negativeExponent = true; + at++; + } + int exponentStart = at; + long parsed = 0; + while (at < text.length() && digitValue(text.charAt(at)) >= 0) { + if (parsed < 100000000L) { + parsed = parsed * 10 + digitValue(text.charAt(at)); + } + at++; + } + if (at > exponentStart) { + exponent = (int) (negativeExponent ? -parsed : parsed); + position = at; + } + break; + } else { + break; + } + } + if (backup != -1) { + position = backup; + } + if (!sawDecimal) { + decimalAt = digitCount; + } + decimalAt += exponent; + if (!sawDigit) { + // As in the JDK, a prefix with no number after it is reported + // at the start of the prefix. + pos.setErrorIndex(start); + return null; + } + } + + if (gotPositive) { + gotPositive = matches(text, position, positiveSuffix); + } + if (gotNegative) { + gotNegative = matches(text, position, negativeSuffix); + } + if (gotPositive && gotNegative) { + if (positiveSuffix.length() > negativeSuffix.length()) { + gotNegative = false; + } else if (positiveSuffix.length() < negativeSuffix.length()) { + gotPositive = false; + } + } + if (gotPositive == gotNegative) { + pos.setErrorIndex(position); + return null; + } + position += gotPositive ? positiveSuffix.length() : negativeSuffix.length(); + pos.setIndex(position); + + if (infinite) { + boolean positiveResult = gotPositive == (multiplier >= 0); + return Double.valueOf(positiveResult ? Double.POSITIVE_INFINITY : Double.NEGATIVE_INFINITY); + } + return toNumber(digits, decimalAt, gotPositive); + } + + private static final String LONG_MIN_DIGITS = "9223372036854775808"; + + /// Whether these digits, an integer with `decimalAt` digits before the + /// point, fit a `long` of the given sign. + private static boolean fitsLong(StringBuilder digits, int decimalAt, boolean positive) { + int count = digits.length(); + if (decimalAt < count || decimalAt > LONG_MIN_DIGITS.length()) { + return false; + } + if (decimalAt < LONG_MIN_DIGITS.length()) { + return true; + } + for (int i = 0; i < count; i++) { + char mine = digits.charAt(i); + char limit = LONG_MIN_DIGITS.charAt(i); + if (mine > limit) { + return false; + } + if (mine < limit) { + return true; + } + } + if (count < decimalAt) { + return true; + } + // Exactly the magnitude of Long.MIN_VALUE. + return !positive; + } + + private Number toNumber(StringBuilder digits, int decimalAt, boolean positive) { + while (digits.length() > 0 && digits.charAt(digits.length() - 1) == '0') { + digits.setLength(digits.length() - 1); + } + boolean gotDouble; + long longResult = 0; + double doubleResult = 0.0; + if (digits.length() == 0) { + if (positive || isParseIntegerOnly()) { + gotDouble = false; + } else { + gotDouble = true; + doubleResult = -0.0; + } + } else if (fitsLong(digits, decimalAt, positive)) { + gotDouble = false; + StringBuilder sb = new StringBuilder(); + if (!positive) { + sb.append('-'); + } + sb.append(digits.toString()); + for (int i = digits.length(); i < decimalAt; i++) { + sb.append('0'); + } + longResult = Long.parseLong(sb.toString()); + } else if (decimalAt > 309) { + // At least ten to the 309th, past Double.MAX_VALUE: the JDK + // answers an infinity. Settled here rather than handed to + // Double.parseDouble, which on some ports answers 0.0 for an + // exponent this large. + gotDouble = true; + doubleResult = positive ? Double.POSITIVE_INFINITY : Double.NEGATIVE_INFINITY; + } else if (decimalAt < -323) { + // Below ten to the -324th, under half of Double.MIN_VALUE: the + // JDK answers a zero of the parsed sign. + gotDouble = true; + doubleResult = positive ? 0.0 : -0.0; + } else { + gotDouble = true; + StringBuilder sb = new StringBuilder(); + if (!positive) { + sb.append('-'); + } + sb.append(digits.charAt(0)).append('.'); + if (digits.length() > 1) { + sb.append(digits.toString().substring(1)); + } else { + sb.append('0'); + } + sb.append('E').append(decimalAt - 1); + doubleResult = Double.parseDouble(sb.toString()); + } + // A multiplier of zero formats everything as zero; there is nothing + // to divide back by. + if (multiplier != 1 && multiplier != 0) { + if (gotDouble) { + doubleResult = doubleResult / multiplier; + } else if (longResult % multiplier == 0) { + longResult = longResult / multiplier; + } else { + doubleResult = (double) longResult / multiplier; + gotDouble = true; + } + if (gotDouble) { + long whole = (long) doubleResult; + boolean negativeZero = doubleResult == 0.0 && Double.doubleToLongBits(doubleResult) < 0; + if (doubleResult == (double) whole && !negativeZero) { + gotDouble = false; + longResult = whole; + } else if (isParseIntegerOnly()) { + gotDouble = false; + longResult = whole; + } + } + } + if (gotDouble) { + return Double.valueOf(doubleResult); + } + return Long.valueOf(longResult); + } + + // ------------------------------------------------------------------ + // Pattern + // ------------------------------------------------------------------ + + public void applyPattern(String pattern) { + parsePattern(pattern); + } + + private static IllegalArgumentException malformed(String why, String pattern) { + return new IllegalArgumentException(why + " in pattern \"" + pattern + '"'); + } + + private static boolean isNumberChar(char ch) { + return ch == '#' || ch == '0' || ch == ',' || ch == '.'; + } + + private void parsePattern(String pattern) { + if (pattern == null) { + throw new NullPointerException(); + } + int len = pattern.length(); + boolean gotNegative = false; + boolean exponential = false; + boolean currency = false; + int exponentDigits = 0; + int newMultiplier = 1; + String newPosPrefix = ""; + String newPosSuffix = ""; + String newNegPrefix = ""; + String newNegSuffix = ""; + int digitLeftCount = 0; + int zeroDigitCount = 0; + int digitRightCount = 0; + int decimalPos = -1; + int groupingCount = -1; + int numberLength = 0; + + int start = 0; + for (int part = 0; part < 2 && start < len; part++) { + boolean positive = part == 0; + StringBuilder prefix = new StringBuilder(); + StringBuilder suffix = new StringBuilder(); + StringBuilder affix = prefix; + int phase = 0; + boolean inQuote = false; + boolean sawSeparator = false; + int partMultiplier = 1; + int toSkip = numberLength; + int pos = start; + while (pos < len) { + char ch = pattern.charAt(pos); + if (phase == 1) { + if (!positive) { + // Only the affixes of the negative subpattern count; + // its number is skipped, as long as the positive one. + if (toSkip > 0) { + toSkip--; + pos++; + continue; + } + phase = 2; + affix = suffix; + continue; + } + if (ch == '#') { + if (zeroDigitCount > 0) { + digitRightCount++; + } else { + digitLeftCount++; + } + if (groupingCount >= 0 && decimalPos < 0) { + groupingCount++; + } + } else if (ch == '0') { + if (digitRightCount > 0) { + throw malformed("Unexpected '0'", pattern); + } + zeroDigitCount++; + if (groupingCount >= 0 && decimalPos < 0) { + groupingCount++; + } + } else if (ch == ',') { + groupingCount = 0; + } else if (ch == '.') { + if (decimalPos >= 0) { + throw malformed("Multiple decimal separators", pattern); + } + decimalPos = digitLeftCount + zeroDigitCount + digitRightCount; + } else if (ch == 'E') { + if (exponential) { + throw malformed("Multiple exponential symbols", pattern); + } + exponential = true; + exponentDigits = 0; + while (pos + 1 < len && pattern.charAt(pos + 1) == '0') { + exponentDigits++; + numberLength++; + pos++; + } + if (digitLeftCount + zeroDigitCount < 1 || exponentDigits < 1) { + throw malformed("Malformed exponential pattern", pattern); + } + numberLength++; + pos++; + phase = 2; + affix = suffix; + continue; + } else { + phase = 2; + affix = suffix; + continue; + } + numberLength++; + pos++; + continue; + } + // A prefix or a suffix. + if (inQuote) { + if (ch == QUOTE) { + if (pos + 1 < len && pattern.charAt(pos + 1) == QUOTE) { + pos++; + affix.append("''"); + } else { + inQuote = false; + } + pos++; + continue; + } + } else if (isNumberChar(ch)) { + // In a suffix too: the JDK goes back to reading the + // number, so "0.0x0" is "0.00x", and this does the same + // rather than reject a pattern the desktop accepts. + if (phase == 2 && !positive) { + pos = len; + continue; + } + phase = 1; + continue; + } else if (ch == CURRENCY_SIGN) { + boolean doubled = pos + 1 < len && pattern.charAt(pos + 1) == CURRENCY_SIGN; + if (doubled) { + pos++; + } + currency = true; + affix.append(QUOTE).append(CURRENCY_SIGN); + if (doubled) { + affix.append(CURRENCY_SIGN); + } + pos++; + continue; + } else if (ch == QUOTE) { + if (pos + 1 < len && pattern.charAt(pos + 1) == QUOTE) { + pos++; + affix.append("''"); + } else { + inQuote = true; + } + pos++; + continue; + } else if (ch == ';') { + if (phase == 0 || !positive) { + throw malformed("Unquoted special character ';'", pattern); + } + sawSeparator = true; + pos++; + break; + } else if (ch == '%' || ch == PER_MILLE_SIGN) { + if (partMultiplier != 1) { + throw malformed("Too many percent/per mille characters", pattern); + } + partMultiplier = ch == '%' ? 100 : 1000; + if (positive) { + newMultiplier = partMultiplier; + } + affix.append(QUOTE).append(ch); + pos++; + continue; + } else if (ch == '-') { + affix.append(QUOTE).append('-'); + pos++; + continue; + } + affix.append(ch); + pos++; + } + if (inQuote) { + throw malformed("Unterminated quote", pattern); + } + if (positive) { + newPosPrefix = prefix.toString(); + newPosSuffix = suffix.toString(); + } else { + newNegPrefix = prefix.toString(); + newNegSuffix = suffix.toString(); + gotNegative = true; + } + // Without a separator the whole pattern is read again as the + // negative one. Its affixes then equal the positive ones, which + // is what selects the default minus sign below. + start = sawSeparator ? pos : 0; + } + + if (len == 0) { + useExponentialNotation = false; + minExponentDigits = 0; + currencyFormat = false; + decimalSeparatorAlwaysShown = false; + setMinimumIntegerDigits(0); + setMaximumIntegerDigits(Integer.MAX_VALUE); + setMinimumFractionDigits(0); + setMaximumFractionDigits(Integer.MAX_VALUE); + } else { + if (zeroDigitCount == 0 && digitLeftCount > 0 && decimalPos >= 0) { + // "#.##", "#." and ".#" have one digit that is always + // written; it is the one next to the decimal point. + int n = decimalPos; + if (n == 0) { + n++; + } + digitRightCount = digitLeftCount - n; + digitLeftCount = n - 1; + zeroDigitCount = 1; + } + if ((decimalPos < 0 && digitRightCount > 0) + || (decimalPos >= 0 + && (decimalPos < digitLeftCount || decimalPos > digitLeftCount + zeroDigitCount)) + || groupingCount == 0) { + throw malformed("Malformed pattern", pattern); + } + int digitTotalCount = digitLeftCount + zeroDigitCount + digitRightCount; + if (decimalPos > digitTotalCount) { + decimalPos = digitTotalCount; + } + int effectiveDecimalPos = decimalPos >= 0 ? decimalPos : digitTotalCount; + useExponentialNotation = exponential; + minExponentDigits = exponentDigits; + // Reset first, so that neither setter is clamped by a previous + // pattern's limits. + setMinimumIntegerDigits(0); + setMinimumFractionDigits(0); + int minInt = effectiveDecimalPos - digitLeftCount; + setMaximumIntegerDigits(exponential ? digitLeftCount + minInt : Integer.MAX_VALUE); + setMinimumIntegerDigits(minInt); + setMaximumFractionDigits(decimalPos >= 0 ? digitTotalCount - decimalPos : 0); + setMinimumFractionDigits(decimalPos >= 0 ? digitLeftCount + zeroDigitCount - decimalPos : 0); + setGroupingUsed(groupingCount > 0); + groupingSize = groupingCount > 0 ? groupingCount : 0; + multiplier = newMultiplier; + currencyFormat = currency; + decimalSeparatorAlwaysShown = decimalPos == 0 || decimalPos == digitTotalCount; + } + posPrefixPattern = newPosPrefix; + posSuffixPattern = newPosSuffix; + // No negative subpattern, or one that says nothing new, means a + // minus sign in front of the positive one. + if (!gotNegative || (newNegPrefix.equals(newPosPrefix) && newNegSuffix.equals(newPosSuffix))) { + negPrefixPattern = "'-" + newPosPrefix; + negSuffixPattern = newPosSuffix; + } else { + negPrefixPattern = newNegPrefix; + negSuffixPattern = newNegSuffix; + } + expandAffixes(); + } + + private void expandAffixes() { + positivePrefix = expandAffix(posPrefixPattern); + positiveSuffix = expandAffix(posSuffixPattern); + negativePrefix = expandAffix(negPrefixPattern); + negativeSuffix = expandAffix(negSuffixPattern); + } + + private String expandAffix(String internal) { + if (internal.indexOf(QUOTE) < 0) { + return internal; + } + StringBuilder sb = new StringBuilder(); + int i = 0; + while (i < internal.length()) { + char c = internal.charAt(i++); + if (c != QUOTE || i >= internal.length()) { + sb.append(c); + continue; + } + c = internal.charAt(i++); + if (c == CURRENCY_SIGN) { + if (i < internal.length() && internal.charAt(i) == CURRENCY_SIGN) { + i++; + sb.append(symbols.getInternationalCurrencySymbol()); + } else { + sb.append(symbols.getCurrencySymbol()); + } + } else if (c == '%') { + sb.append(symbols.getPercent()); + } else if (c == PER_MILLE_SIGN) { + sb.append(symbols.getPerMill()); + } else if (c == '-') { + sb.append(symbols.getMinusSign()); + } else { + sb.append(c); + } + } + return sb.toString(); + } + + private static String literalAffix(String literal) { + if (literal.indexOf(QUOTE) < 0) { + return literal; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < literal.length(); i++) { + char c = literal.charAt(i); + sb.append(c); + if (c == QUOTE) { + sb.append(QUOTE); + } + } + return sb.toString(); + } + + private static boolean needsQuoting(char c) { + return isNumberChar(c) || c == ';' || c == '%' || c == PER_MILLE_SIGN || c == CURRENCY_SIGN + || c == '-'; + } + + private static void appendAffixPattern(StringBuilder out, String internal) { + int i = 0; + while (i < internal.length()) { + char c = internal.charAt(i++); + if (c == QUOTE && i < internal.length()) { + c = internal.charAt(i++); + if (c == QUOTE) { + out.append("''"); + } else { + out.append(c); + if (c == CURRENCY_SIGN && i < internal.length() && internal.charAt(i) == CURRENCY_SIGN) { + out.append(CURRENCY_SIGN); + i++; + } + } + } else if (needsQuoting(c)) { + out.append(QUOTE).append(c).append(QUOTE); + } else { + out.append(c); + } + } + } + + private void appendNumberPattern(StringBuilder out) { + int minInt = getMinimumIntegerDigits(); + // Capped: an empty pattern allows any number of fraction digits, and + // spelling that out exhausts memory, as it does in the JDK. + int maxFrac = Math.min(getMaximumFractionDigits(), DOUBLE_FRACTION_DIGITS); + int minFrac = Math.min(getMinimumFractionDigits(), DOUBLE_FRACTION_DIGITS); + boolean grouping = isGroupingUsed() && groupingSize > 0; + int digitCount = useExponentialNotation + ? getMaximumIntegerDigits() + : Math.max(groupingSize, minInt) + 1; + for (int i = digitCount; i > 0; i--) { + // groupingSize is checked positive just above. + if (i != digitCount && grouping && i % groupingSize == 0) { + out.append(','); + } + out.append(i <= minInt ? '0' : '#'); + } + if (maxFrac > 0 || decimalSeparatorAlwaysShown) { + out.append('.'); + } + for (int i = 0; i < maxFrac; i++) { + out.append(i < minFrac ? '0' : '#'); + } + if (useExponentialNotation) { + out.append('E'); + for (int i = 0; i < minExponentDigits; i++) { + out.append('0'); + } + } + } + + /// A pattern that produces this format. It is equivalent to the pattern + /// that was applied, not necessarily the same text: `0.00` comes back as + /// `#0.00`. + public String toPattern() { + StringBuilder out = new StringBuilder(); + appendAffixPattern(out, posPrefixPattern); + appendNumberPattern(out); + appendAffixPattern(out, posSuffixPattern); + boolean defaultNegative = negSuffixPattern.equals(posSuffixPattern) + && negPrefixPattern.equals("'-" + posPrefixPattern); + if (!defaultNegative) { + out.append(';'); + appendAffixPattern(out, negPrefixPattern); + appendNumberPattern(out); + appendAffixPattern(out, negSuffixPattern); + } + return out.toString(); + } + + // ------------------------------------------------------------------ + // Properties + // ------------------------------------------------------------------ + + /// A copy of the symbols in use; changing it changes nothing until it + /// is passed to [#setDecimalFormatSymbols(DecimalFormatSymbols)]. + public DecimalFormatSymbols getDecimalFormatSymbols() { + Object copy = symbols.clone(); + return copy instanceof DecimalFormatSymbols ? (DecimalFormatSymbols) copy : symbols; + } + + public void setDecimalFormatSymbols(DecimalFormatSymbols newSymbols) { + Object copy = newSymbols.clone(); + if (copy instanceof DecimalFormatSymbols) { + symbols = (DecimalFormatSymbols) copy; + expandAffixes(); + } + } + + public String getPositivePrefix() { + return positivePrefix; + } + + public void setPositivePrefix(String newValue) { + positivePrefix = newValue; + posPrefixPattern = literalAffix(newValue); + } + + public String getNegativePrefix() { + return negativePrefix; + } + + public void setNegativePrefix(String newValue) { + negativePrefix = newValue; + negPrefixPattern = literalAffix(newValue); + } + + public String getPositiveSuffix() { + return positiveSuffix; + } + + public void setPositiveSuffix(String newValue) { + positiveSuffix = newValue; + posSuffixPattern = literalAffix(newValue); + } + + public String getNegativeSuffix() { + return negativeSuffix; + } + + public void setNegativeSuffix(String newValue) { + negativeSuffix = newValue; + negSuffixPattern = literalAffix(newValue); + } + + public int getMultiplier() { + return multiplier; + } + + public void setMultiplier(int newValue) { + multiplier = newValue; + } + + public int getGroupingSize() { + return groupingSize; + } + + public void setGroupingSize(int newValue) { + groupingSize = Math.max(0, newValue); + } + + public boolean isDecimalSeparatorAlwaysShown() { + return decimalSeparatorAlwaysShown; + } + + public void setDecimalSeparatorAlwaysShown(boolean newValue) { + decimalSeparatorAlwaysShown = newValue; + } + + /// A copy of this format. Written out field by field: `Object.clone()` + /// answers `null` on the device for anything but an array. + @Override + public Object clone() { + DecimalFormat copy = new DecimalFormat("", symbols); + copyNumberFormatTo(copy); + copy.posPrefixPattern = posPrefixPattern; + copy.posSuffixPattern = posSuffixPattern; + copy.negPrefixPattern = negPrefixPattern; + copy.negSuffixPattern = negSuffixPattern; + copy.positivePrefix = positivePrefix; + copy.positiveSuffix = positiveSuffix; + copy.negativePrefix = negativePrefix; + copy.negativeSuffix = negativeSuffix; + copy.multiplier = multiplier; + copy.groupingSize = groupingSize; + copy.decimalSeparatorAlwaysShown = decimalSeparatorAlwaysShown; + copy.useExponentialNotation = useExponentialNotation; + copy.minExponentDigits = minExponentDigits; + copy.currencyFormat = currencyFormat; + return copy; + } + + @Override + public boolean equals(Object obj) { + if (!super.equals(obj) || !(obj instanceof DecimalFormat)) { + return false; + } + DecimalFormat other = (DecimalFormat) obj; + return positivePrefix.equals(other.positivePrefix) + && positiveSuffix.equals(other.positiveSuffix) + && negativePrefix.equals(other.negativePrefix) + && negativeSuffix.equals(other.negativeSuffix) + && multiplier == other.multiplier + && groupingSize == other.groupingSize + && decimalSeparatorAlwaysShown == other.decimalSeparatorAlwaysShown + && useExponentialNotation == other.useExponentialNotation + && minExponentDigits == other.minExponentDigits + && symbols.equals(other.symbols); + } + + @Override + public int hashCode() { + return super.hashCode() * 37 + positivePrefix.hashCode(); + } +} diff --git a/Ports/CLDC11/src/java/text/DecimalFormatSymbols.java b/Ports/CLDC11/src/java/text/DecimalFormatSymbols.java new file mode 100644 index 00000000000..4941d026547 --- /dev/null +++ b/Ports/CLDC11/src/java/text/DecimalFormatSymbols.java @@ -0,0 +1,294 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.text; + +import com.codename1.impl.JdkHost; + +import java.util.Locale; + +/// The characters a [DecimalFormat] writes and reads -- decimal and +/// grouping separators, minus sign, percent, currency symbol and so on. +/// +/// #### Where the symbols come from +/// +/// The device library carries no locale data of its own. What it knows about +/// the user's locale -- the decimal and grouping separators and the currency +/// symbol -- it is told by the platform, which on a device reads them off +/// numbers Codename One's `L10NManager` formatted. Everything the platform +/// cannot tell (percent, per mille, the exponent separator, the infinity and +/// not-a-number texts) is as in the US English locale. +/// +/// A request that names `en_US` gets the US English symbols outright. Any +/// other locale gets the device's symbols, since the device cannot format for +/// a locale other than its own. Before Codename One is initialized, and where +/// the platform answers nothing usable, the symbols are US English. +public class DecimalFormatSymbols implements Cloneable, java.io.Serializable { + + private static final long serialVersionUID = 1L; + + private char zeroDigit = '0'; + private char groupingSeparator = ','; + private char decimalSeparator = '.'; + private char perMill = (char) 0x2030; + private char percent = '%'; + private char digit = '#'; + private char patternSeparator = ';'; + private String infinity = String.valueOf((char) 0x221E); + private String nan = String.valueOf((char) 0xFFFD); + private char minusSign = '-'; + private String currencySymbol = "$"; + private String intlCurrencySymbol = "USD"; + private char monetarySeparator = '.'; + private String exponentSeparator = "E"; + + public DecimalFormatSymbols() { + this(Locale.getDefault()); + } + + public DecimalFormatSymbols(Locale locale) { + if (locale == null) { + throw new NullPointerException(); + } + if (!isUsEnglish(locale)) { + readDeviceSymbols(); + } + } + + public static final DecimalFormatSymbols getInstance() { + return new DecimalFormatSymbols(); + } + + public static final DecimalFormatSymbols getInstance(Locale locale) { + return new DecimalFormatSymbols(locale); + } + + private static boolean isUsEnglish(Locale locale) { + return "en".equals(locale.getLanguage()) && "US".equals(locale.getCountry()); + } + + private void readDeviceSymbols() { + String[] device; + try { + device = JdkHost.get().localeNumberSymbols(); + } catch (RuntimeException e) { + // A port whose localization cannot answer leaves the US English + // symbols in place, which is the documented fallback. + return; + } + if (device == null || device.length < 3) { + return; + } + String decimal = device[0]; + if (decimal != null && decimal.length() == 1) { + decimalSeparator = decimal.charAt(0); + monetarySeparator = decimalSeparator; + } + String grouping = device[1]; + if (grouping != null && grouping.length() == 1 && grouping.charAt(0) != decimalSeparator) { + groupingSeparator = grouping.charAt(0); + } else if (decimalSeparator == ',') { + // A locale that writes a decimal comma and no grouping on this + // platform still must not group with the same character. + groupingSeparator = '.'; + } + String currency = device[2]; + if (currency != null && currency.length() > 0) { + currencySymbol = currency; + intlCurrencySymbol = currency; + } + } + + public char getZeroDigit() { + return zeroDigit; + } + + public void setZeroDigit(char zeroDigit) { + this.zeroDigit = zeroDigit; + } + + public char getGroupingSeparator() { + return groupingSeparator; + } + + public void setGroupingSeparator(char groupingSeparator) { + this.groupingSeparator = groupingSeparator; + } + + public char getDecimalSeparator() { + return decimalSeparator; + } + + public void setDecimalSeparator(char decimalSeparator) { + this.decimalSeparator = decimalSeparator; + } + + public char getPerMill() { + return perMill; + } + + public void setPerMill(char perMill) { + this.perMill = perMill; + } + + public char getPercent() { + return percent; + } + + public void setPercent(char percent) { + this.percent = percent; + } + + public char getDigit() { + return digit; + } + + public void setDigit(char digit) { + this.digit = digit; + } + + public char getPatternSeparator() { + return patternSeparator; + } + + public void setPatternSeparator(char patternSeparator) { + this.patternSeparator = patternSeparator; + } + + public String getInfinity() { + return infinity; + } + + public void setInfinity(String infinity) { + this.infinity = infinity; + } + + public String getNaN() { + return nan; + } + + public void setNaN(String nan) { + this.nan = nan; + } + + public char getMinusSign() { + return minusSign; + } + + public void setMinusSign(char minusSign) { + this.minusSign = minusSign; + } + + public String getCurrencySymbol() { + return currencySymbol; + } + + public void setCurrencySymbol(String currency) { + currencySymbol = currency; + } + + public String getInternationalCurrencySymbol() { + return intlCurrencySymbol; + } + + public void setInternationalCurrencySymbol(String currencyCode) { + intlCurrencySymbol = currencyCode; + } + + public char getMonetaryDecimalSeparator() { + return monetarySeparator; + } + + public void setMonetaryDecimalSeparator(char sep) { + monetarySeparator = sep; + } + + public String getExponentSeparator() { + return exponentSeparator; + } + + public void setExponentSeparator(String exp) { + if (exp == null) { + throw new NullPointerException(); + } + exponentSeparator = exp; + } + + /// A copy of these symbols. Written out field by field: `Object.clone()` + /// answers `null` on the device for anything but an array. + @Override + public Object clone() { + DecimalFormatSymbols copy = new DecimalFormatSymbols(new Locale("en", "US")); + copy.zeroDigit = zeroDigit; + copy.groupingSeparator = groupingSeparator; + copy.decimalSeparator = decimalSeparator; + copy.perMill = perMill; + copy.percent = percent; + copy.digit = digit; + copy.patternSeparator = patternSeparator; + copy.infinity = infinity; + copy.nan = nan; + copy.minusSign = minusSign; + copy.currencySymbol = currencySymbol; + copy.intlCurrencySymbol = intlCurrencySymbol; + copy.monetarySeparator = monetarySeparator; + copy.exponentSeparator = exponentSeparator; + return copy; + } + + private static boolean same(String a, String b) { + return a == null ? b == null : a.equals(b); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof DecimalFormatSymbols)) { + return false; + } + DecimalFormatSymbols other = (DecimalFormatSymbols) obj; + return zeroDigit == other.zeroDigit + && groupingSeparator == other.groupingSeparator + && decimalSeparator == other.decimalSeparator + && percent == other.percent + && perMill == other.perMill + && digit == other.digit + && minusSign == other.minusSign + && patternSeparator == other.patternSeparator + && monetarySeparator == other.monetarySeparator + && same(infinity, other.infinity) + && same(nan, other.nan) + && same(currencySymbol, other.currencySymbol) + && same(intlCurrencySymbol, other.intlCurrencySymbol) + && same(exponentSeparator, other.exponentSeparator); + } + + @Override + public int hashCode() { + int result = zeroDigit; + result = result * 37 + groupingSeparator; + result = result * 37 + decimalSeparator; + return result; + } +} diff --git a/Ports/CLDC11/src/java/text/FieldPosition.java b/Ports/CLDC11/src/java/text/FieldPosition.java new file mode 100644 index 00000000000..aedd19d8be3 --- /dev/null +++ b/Ports/CLDC11/src/java/text/FieldPosition.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.text; + +/// Names a field of formatted output and receives the range of characters that field took. +/// `NumberFormat.INTEGER_FIELD` and `NumberFormat.FRACTION_FIELD` are the +/// fields the number formats here report. +public class FieldPosition { + + private final int field; + private int beginIndex; + private int endIndex; + + public FieldPosition(int field) { + this.field = field; + } + + public int getField() { + return field; + } + + public int getBeginIndex() { + return beginIndex; + } + + public int getEndIndex() { + return endIndex; + } + + public void setBeginIndex(int bi) { + beginIndex = bi; + } + + public void setEndIndex(int ei) { + endIndex = ei; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof FieldPosition)) { + return false; + } + FieldPosition other = (FieldPosition) obj; + return field == other.field && beginIndex == other.beginIndex && endIndex == other.endIndex; + } + + @Override + public int hashCode() { + return (field << 24) | (beginIndex << 16) | endIndex; + } + + @Override + public String toString() { + return getClass().getName() + "[field=" + field + ",attribute=null,beginIndex=" + beginIndex + + ",endIndex=" + endIndex + ']'; + } +} diff --git a/Ports/CLDC11/src/java/text/Format.java b/Ports/CLDC11/src/java/text/Format.java index b2bc4c1bc94..e3838fef3d4 100644 --- a/Ports/CLDC11/src/java/text/Format.java +++ b/Ports/CLDC11/src/java/text/Format.java @@ -6,55 +6,76 @@ * published by the Free Software Foundation. Codename One designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. - * + * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). - * + * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Codename One through http://www.codenameone.com/ if you + * + * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ package java.text; -/// An abstract class for parsing and formatting localisation sensitive information, compatible with JDK 6. +/// The base of the formats that turn locale sensitive values into text and +/// back: [DateFormat], [NumberFormat] and [MessageFormat]. /// -/// #### See also +/// A subclass implements [#format(Object, StringBuffer, FieldPosition)] and +/// [#parseObject(String, ParsePosition)]; the one-argument forms are written +/// over them, as in the JDK. /// -/// - [http://docs.oracle.com/javase/6/docs/api/java/text/Format.html](http://docs.oracle.com/javase/6/docs/api/java/text/Format.html) -public abstract class Format implements Cloneable { - /// Format an object. - /// - /// #### Parameters - /// - /// - `source`: object to be formatted to text. - /// - /// #### Returns - /// - /// formatted text. - /// - /// #### Throws - /// - /// - `IllegalArgumentException`: if the source can not be formatted. - public abstract String format(Object source) throws IllegalArgumentException; +/// #### Differences from the JDK +/// +/// - No `clone` here: `Object.clone()` answers `null` on the device for +/// anything but an array, so every format of this package declares a +/// `clone` that copies its own fields. +/// - No `formatToCharacterIterator`: the device has no +/// `AttributedCharacterIterator`. +public abstract class Format implements Cloneable, java.io.Serializable { + + private static final long serialVersionUID = 1L; + + protected Format() { + } + + /// Formats an object to text. + /// + /// #### Throws + /// + /// - `IllegalArgumentException`: if this format cannot format `obj` + public final String format(Object obj) { + return format(obj, new StringBuffer(), new FieldPosition(0)).toString(); + } + + /// Formats an object, appending the text to `toAppendTo`, and reports the + /// range the field `pos` names took. + /// + /// #### Throws + /// + /// - `IllegalArgumentException`: if this format cannot format `obj` + public abstract StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos); + + /// Parses an object from `source` starting at `pos`. On success the index + /// of `pos` is moved past what was read; on failure it is left alone, the + /// error index is set and `null` is returned. + public abstract Object parseObject(String source, ParsePosition pos); - /// Parse an string to an object. - /// - /// #### Parameters - /// - /// - `source`: document to be parsed. - /// - /// #### Returns - /// - /// parsed object. - /// - /// #### Throws - /// - /// - `ParseException`: if the source could not be parsed. - public abstract Object parseObject(String source) throws ParseException; -} \ No newline at end of file + /// Parses an object from the beginning of `source`. + /// + /// #### Throws + /// + /// - `ParseException`: if nothing at the beginning of `source` parses + public Object parseObject(String source) throws ParseException { + ParsePosition pos = new ParsePosition(0); + Object result = parseObject(source, pos); + if (pos.getIndex() == 0) { + throw new ParseException("Format.parseObject(String) failed", pos.getErrorIndex()); + } + return result; + } +} diff --git a/Ports/CLDC11/src/java/text/LegacyDoubleDigits.java b/Ports/CLDC11/src/java/text/LegacyDoubleDigits.java new file mode 100644 index 00000000000..fa571f1d145 --- /dev/null +++ b/Ports/CLDC11/src/java/text/LegacyDoubleDigits.java @@ -0,0 +1,547 @@ +/* + * Copyright (c) 1996, 2013, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * Codename One note: this is a rewrite of the digit generation in OpenJDK's + * FloatingDecimal.BinaryToASCIIBuffer.dtoa (and the arithmetic it needs from + * FDBigInteger), reduced to what DecimalFormat uses and written against the + * JDK subset vm/JavaAPI and Ports/CLDC11 both provide. The decisions -- which + * path a value takes, the stopping tests, the rounding of the last digit -- + * are OpenJDK's and are kept exactly, quirks included, because reproducing + * the JDK's output is the whole point of the class. + */ +package java.text; + +/// The decimal digits the JDK's `DecimalFormat` starts from when it formats a +/// `double`. +/// +/// That is NOT `Double.toString`. Since JDK-4511638 `Double.toString` writes +/// the shortest string that round-trips, but `DecimalFormat` still takes its +/// digits from the older `FloatingDecimal` conversion, which is not always the +/// shortest: `1.0E23` comes out as `9.999999999999999E22`, and a value above +/// two to the 53rd whose shortest form ends in zeros keeps its seventeenth +/// digit (`7.2057594037927936E16`). Formatting from the shortest digits +/// therefore disagrees with the desktop in the last digits of large numbers, +/// so this class reproduces the old conversion. +/// +/// The conversion picks one of three routes and the routes are NOT +/// equivalent at their edges (`int` and `long` arithmetic use a strict +/// stopping test where the big-integer route uses an inclusive one), so all +/// three are kept rather than doing everything with big integers. +final class LegacyDoubleDigits { + private static final int EXP_SHIFT = 52; + private static final long FRACT_HOB = 1L << EXP_SHIFT; + private static final long EXP_ONE = 1023L << EXP_SHIFT; + private static final long SIGNIF_BIT_MASK = FRACT_HOB - 1; + private static final int MAX_SMALL_BIN_EXP = 62; + private static final int MIN_SMALL_BIN_EXP = -(63 / 3); + + private static final int[] SMALL_5_POW = new int[14]; + private static final long[] LONG_5_POW = new long[27]; + + static { + SMALL_5_POW[0] = 1; + for (int i = 1; i < SMALL_5_POW.length; i++) { + SMALL_5_POW[i] = SMALL_5_POW[i - 1] * 5; + } + LONG_5_POW[0] = 1L; + for (int i = 1; i < LONG_5_POW.length; i++) { + LONG_5_POW[i] = LONG_5_POW[i - 1] * 5L; + } + } + + /// Approximately `ceil(log2(5^i))`, as OpenJDK tabulates it; the route + /// choice depends on these exact values. + private static final int[] N_5_BITS = { + 0, 3, 5, 7, 10, 12, 14, 17, 19, 21, 24, 26, 28, 31, 33, 35, 38, 40, 42, 45, 47, 49, 52, 54, 56, 59, 61 + }; + + /// `insignificantDigits(1L << i)`: how many low decimal digits of an + /// integer value are below its precision. + private static final int[] INSIGNIFICANT_DIGITS_NUMBER = { + 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 3, + 4, 4, 4, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, + 8, 8, 8, 9, 9, 9, 9, 10, 10, 10, 11, 11, 11, + 12, 12, 12, 12, 13, 13, 13, 14, 14, 14, + 15, 15, 15, 15, 16, 16, 16, 17, 17, 17, + 18, 18, 18, 19 + }; + + /// The digits, `digits[first]` to `digits[first + count - 1]`. + final char[] digits = new char[24]; + int first; + int count; + /// The position of the decimal point relative to the first digit: + /// the value is `0.d1d2d3... * 10^decExponent`. + int decExponent; + + /// Converts a finite, strictly positive value. + LegacyDoubleDigits(double value) { + long bits = Double.doubleToLongBits(value); + long fractBits = bits & SIGNIF_BIT_MASK; + int binExp = (int) ((bits >> EXP_SHIFT) & 0x7ffL); + int nSignificantBits; + if (binExp == 0) { + // Subnormal: normalize so the high-order bit sits at EXP_SHIFT. + int leadingZeros = Long.numberOfLeadingZeros(fractBits); + int shift = leadingZeros - (63 - EXP_SHIFT); + fractBits <<= shift; + binExp = 1 - shift; + nSignificantBits = 64 - leadingZeros; + } else { + fractBits |= FRACT_HOB; + nSignificantBits = EXP_SHIFT + 1; + } + binExp -= 1023; + dtoa(binExp, fractBits, nSignificantBits); + } + + private void developLongDigits(int decExp, long lvalue, int insignificantDigits) { + if (insignificantDigits != 0) { + // Drop the digits below the value's precision, rounding. + long pow10 = LONG_5_POW[insignificantDigits] << insignificantDigits; + long residue = lvalue % pow10; + lvalue /= pow10; + decExp += insignificantDigits; + if (residue >= (pow10 >> 1)) { + lvalue++; + } + } + int digitno = digits.length - 1; + int c = (int) (lvalue % 10L); + lvalue /= 10L; + while (c == 0) { + decExp++; + c = (int) (lvalue % 10L); + lvalue /= 10L; + } + while (lvalue != 0L) { + digits[digitno--] = (char) (c + '0'); + decExp++; + c = (int) (lvalue % 10L); + lvalue /= 10L; + } + digits[digitno] = (char) (c + '0'); + decExponent = decExp + 1; + first = digitno; + count = digits.length - digitno; + } + + private void dtoa(int binExp, long fractBits, int nSignificantBits) { + final int tailZeros = Long.numberOfTrailingZeros(fractBits); + final int nFractBits = EXP_SHIFT + 1 - tailZeros; + int nTinyBits = Math.max(0, nFractBits - binExp - 1); + if (binExp <= MAX_SMALL_BIN_EXP && binExp >= MIN_SMALL_BIN_EXP + && nTinyBits < LONG_5_POW.length && nFractBits + N_5_BITS[nTinyBits] < 64 + && nTinyBits == 0) { + // An integer that fits a long: its digits, less the ones below + // its precision. + int insignificant = 0; + if (binExp > nSignificantBits) { + int p2 = binExp - nSignificantBits - 1; + if (p2 > 1 && p2 < INSIGNIFICANT_DIGITS_NUMBER.length) { + insignificant = INSIGNIFICANT_DIGITS_NUMBER[p2]; + } + } + if (binExp >= EXP_SHIFT) { + fractBits <<= (binExp - EXP_SHIFT); + } else { + fractBits >>>= (EXP_SHIFT - binExp); + } + developLongDigits(0, fractBits, insignificant); + return; + } + // value = (B / S) * 10^decExp with 1 <= B / S < 10, and M is half an + // ULP scaled like B; digits are developed until the remainder is + // within M of either neighbour. + int decExp = estimateDecExp(fractBits, binExp); + int b5 = Math.max(0, -decExp); + int b2 = b5 + nTinyBits + binExp; + int s5 = Math.max(0, decExp); + int s2 = s5 + nTinyBits; + int m5 = b5; + int m2 = b2 - nSignificantBits; + + fractBits >>>= tailZeros; + b2 -= nFractBits - 1; + int common2factor = Math.min(b2, s2); + b2 -= common2factor; + s2 -= common2factor; + m2 -= common2factor; + // Below an exact power of two the next smaller double is only half + // as far away. + if (nFractBits == 1) { + m2 -= 1; + } + if (m2 < 0) { + b2 -= m2; + s2 -= m2; + m2 = 0; + } + + int ndigit = 0; + boolean low; + boolean high; + long lowDigitDifference; + int q; + int bBits = nFractBits + b2 + (b5 < N_5_BITS.length ? N_5_BITS[b5] : b5 * 3); + int tenSBits = s2 + 1 + (s5 + 1 < N_5_BITS.length ? N_5_BITS[s5 + 1] : (s5 + 1) * 3); + if (bBits < 64 && tenSBits < 64) { + if (bBits < 32 && tenSBits < 32) { + int b = ((int) fractBits * SMALL_5_POW[b5]) << b2; + int s = SMALL_5_POW[s5] << s2; + int m = SMALL_5_POW[m5] << m2; + int tens = s * 10; + q = b / s; + b = 10 * (b % s); + m *= 10; + low = b < m; + high = b + m > tens; + if (q == 0 && !high) { + // The exponent estimate was one too high. + decExp--; + } else { + digits[ndigit++] = (char) ('0' + q); + } + // Java's E-form always shows a digit after the point, so + // outside the plain-notation range a second digit is forced. + if (decExp < -3 || decExp >= 8) { + high = false; + low = false; + } + while (!low && !high) { + q = b / s; + b = 10 * (b % s); + m *= 10; + if (m > 0) { + low = b < m; + high = b + m > tens; + } else { + // m overflowed, so it certainly exceeds both tests. + low = true; + high = true; + } + digits[ndigit++] = (char) ('0' + q); + } + lowDigitDifference = (b << 1) - tens; + } else { + long b = (fractBits * LONG_5_POW[b5]) << b2; + long s = LONG_5_POW[s5] << s2; + long m = LONG_5_POW[m5] << m2; + long tens = s * 10L; + q = (int) (b / s); + b = 10L * (b % s); + m *= 10L; + low = b < m; + high = b + m > tens; + if (q == 0 && !high) { + decExp--; + } else { + digits[ndigit++] = (char) ('0' + q); + } + if (decExp < -3 || decExp >= 8) { + high = false; + low = false; + } + while (!low && !high) { + q = (int) (b / s); + b = 10 * (b % s); + m *= 10; + if (m > 0L) { + low = b < m; + high = b + m > tens; + } else { + low = true; + high = true; + } + digits[ndigit++] = (char) ('0' + q); + } + lowDigitDifference = (b << 1) - tens; + } + } else { + // OpenJDK normalizes these by a common power of two first to + // speed its division; that scales every comparison equally, so + // it is left out. Note the HIGH test here is inclusive (>=), + // unlike the int and long routes above. + Big sVal = Big.pow52(s5, s2); + Big bVal = Big.pow52(0, 0); + bVal.multiplyLong(fractBits); + bVal.multiplyPow5(b5); + bVal.shiftLeft(b2); + Big mVal = Big.pow52(m5 + 1, m2 + 1); + Big tenSVal = Big.pow52(s5 + 1, s2 + 1); + q = bVal.quoRemIteration(sVal); + low = bVal.compareTo(mVal) < 0; + high = tenSVal.compareToSum(bVal, mVal) <= 0; + if (q == 0 && !high) { + decExp--; + } else { + digits[ndigit++] = (char) ('0' + q); + } + if (decExp < -3 || decExp >= 8) { + high = false; + low = false; + } + while (!low && !high) { + q = bVal.quoRemIteration(sVal); + mVal.multiplySmall(10); + low = bVal.compareTo(mVal) < 0; + high = tenSVal.compareToSum(bVal, mVal) <= 0; + digits[ndigit++] = (char) ('0' + q); + } + if (high && low) { + bVal.shiftLeft(1); + lowDigitDifference = bVal.compareTo(tenSVal); + } else { + lowDigitDifference = 0L; + } + } + decExponent = decExp + 1; + first = 0; + count = ndigit; + // The last digit is rounded by the stopping condition. + if (high) { + if (low) { + if (lowDigitDifference == 0L) { + if ((digits[first + count - 1] & 1) != 0) { + roundup(); + } + } else if (lowDigitDifference > 0) { + roundup(); + } + } else { + roundup(); + } + } + } + + private void roundup() { + int i = first + count - 1; + int q = digits[i]; + if (q == '9') { + while (q == '9' && i > first) { + digits[i] = '0'; + q = digits[--i]; + } + if (q == '9') { + decExponent += 1; + digits[first] = '1'; + return; + } + } + digits[i] = (char) (q + 1); + } + + /// Estimates `floor(log10(value))` from a linear approximation of + /// `log10` over the mantissa; it may come out one too high, which the + /// first digit corrects. + private static int estimateDecExp(long fractBits, int binExp) { + double d2 = Double.longBitsToDouble(EXP_ONE | (fractBits & SIGNIF_BIT_MASK)); + double d = (d2 - 1.5D) * 0.289529654D + 0.176091259 + (double) binExp * 0.301029995663981; + long dBits = Double.doubleToLongBits(d); + int exponent = (int) ((dBits >> EXP_SHIFT) & 0x7ffL) - 1023; + boolean isNegative = dBits < 0; + if (exponent >= 0 && exponent < 52) { + long mask = SIGNIF_BIT_MASK >> exponent; + int r = (int) (((dBits & SIGNIF_BIT_MASK) | FRACT_HOB) >> (EXP_SHIFT - exponent)); + return isNegative ? (((mask & dBits) == 0L) ? -r : -r - 1) : r; + } else if (exponent < 0) { + return ((dBits & ~(1L << 63)) == 0) ? 0 : (isNegative ? -1 : 0); + } else { + return (int) d; + } + } + + /// A non-negative integer of any size: little-endian 32-bit words held + /// in an `int[]`, worked on through `long`s. Only the operations the + /// digit loop needs. + private static final class Big { + private static final long MASK = 0xffffffffL; + private int[] words = new int[8]; + private int length; + + static Big pow52(int p5, int p2) { + Big b = new Big(); + b.words[0] = 1; + b.length = 1; + b.multiplyPow5(p5); + b.shiftLeft(p2); + return b; + } + + private void ensure(int size) { + if (size > words.length) { + int[] bigger = new int[Math.max(size, words.length * 2)]; + System.arraycopy(words, 0, bigger, 0, length); + words = bigger; + } + } + + private void trim() { + while (length > 0 && words[length - 1] == 0) { + length--; + } + } + + /// Multiplies by a non-negative factor below two to the 31st. + void multiplySmall(int factor) { + long carry = 0; + for (int i = 0; i < length; i++) { + long v = (words[i] & MASK) * factor + carry; + words[i] = (int) v; + carry = v >>> 32; + } + if (carry != 0) { + ensure(length + 1); + words[length++] = (int) carry; + } + } + + /// Multiplies by a non-negative long. + void multiplyLong(long factor) { + int lowFactor = (int) (factor & 0x7fffffffL); + long high = factor >>> 31; + if (high == 0) { + multiplySmall(lowFactor); + return; + } + Big highPart = copy(); + highPart.multiplyLong(high); + highPart.shiftLeft(31); + multiplySmall(lowFactor); + add(highPart); + } + + void multiplyPow5(int p5) { + int left = p5; + while (left >= 13) { + multiplySmall(SMALL_5_POW[13]); + left -= 13; + } + if (left > 0) { + multiplySmall(SMALL_5_POW[left]); + } + } + + void shiftLeft(int bits) { + if (length == 0 || bits == 0) { + return; + } + int wordShift = bits >>> 5; + int bitShift = bits & 31; + ensure(length + wordShift + 1); + if (bitShift == 0) { + // The extra top word is counted below; a word left over from + // an earlier, larger value must not survive in it. + words[length + wordShift] = 0; + for (int i = length - 1; i >= 0; i--) { + words[i + wordShift] = words[i]; + } + } else { + words[length + wordShift] = words[length - 1] >>> (32 - bitShift); + for (int i = length - 1; i > 0; i--) { + words[i + wordShift] = (words[i] << bitShift) | (words[i - 1] >>> (32 - bitShift)); + } + words[wordShift] = words[0] << bitShift; + } + for (int i = 0; i < wordShift; i++) { + words[i] = 0; + } + length += wordShift + 1; + trim(); + } + + private Big copy() { + Big c = new Big(); + c.ensure(length); + System.arraycopy(words, 0, c.words, 0, length); + c.length = length; + return c; + } + + void add(Big other) { + int size = Math.max(length, other.length); + ensure(size + 1); + long carry = 0; + for (int i = 0; i < size; i++) { + long a = i < length ? (words[i] & MASK) : 0L; + long b = i < other.length ? (other.words[i] & MASK) : 0L; + long v = a + b + carry; + words[i] = (int) v; + carry = v >>> 32; + } + length = size; + if (carry != 0) { + words[length++] = (int) carry; + } + } + + /// Subtracts a value no greater than this one. + private void subtract(Big other) { + long borrow = 0; + for (int i = 0; i < length; i++) { + long b = i < other.length ? (other.words[i] & MASK) : 0L; + long v = (words[i] & MASK) - b - borrow; + words[i] = (int) v; + borrow = v < 0 ? 1 : 0; + } + trim(); + } + + int compareTo(Big other) { + if (length != other.length) { + return length < other.length ? -1 : 1; + } + for (int i = length - 1; i >= 0; i--) { + long a = words[i] & MASK; + long b = other.words[i] & MASK; + if (a != b) { + return a < b ? -1 : 1; + } + } + return 0; + } + + /// The sign of this minus `(x + y)`. + int compareToSum(Big x, Big y) { + Big sum = x.copy(); + sum.add(y); + return compareTo(sum); + } + + /// `q = this / s; this = 10 * (this % s)`, returning `q`. The + /// quotient is a single decimal digit, so repeated subtraction is + /// both exact and cheap. + int quoRemIteration(Big s) { + int q = 0; + while (compareTo(s) >= 0) { + subtract(s); + q++; + } + multiplySmall(10); + return q; + } + } +} diff --git a/Ports/CLDC11/src/java/text/MessageFormat.java b/Ports/CLDC11/src/java/text/MessageFormat.java new file mode 100644 index 00000000000..66f627a0ca5 --- /dev/null +++ b/Ports/CLDC11/src/java/text/MessageFormat.java @@ -0,0 +1,627 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.text; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +/// Builds a message from a pattern and a list of arguments, and reads the +/// arguments back out of a message. +/// +/// #### Pattern syntax +/// +/// Text is copied as it is, and each `{...}` element is replaced by an +/// argument: +/// +/// - `{0}`: the argument at that index. A `Number` is formatted with +/// `NumberFormat.getInstance`, a `Date` with the short date and time +/// format, a `String` is copied, anything else contributes `toString()`. +/// - `{0,number}`, `{0,number,integer}`, `{0,number,percent}`, +/// `{0,number,currency}`, or `{0,number,#,##0.00}` with a [DecimalFormat] +/// pattern. +/// - `{0,date}` and `{0,time}`, optionally followed by `short`, `medium`, +/// `long` or `full`, or by a `SimpleDateFormat` pattern: +/// `{0,date,yyyy-MM-dd}`. The device's own `java.text.DateFormat` does the +/// formatting, so the named styles produce what the device produces. +/// - `{0,choice,0#none|1#one|1<{0} items}` with a [ChoiceFormat] pattern. +/// If the chosen text has elements of its own, they are formatted against +/// the same arguments. +/// +/// A single quote makes the characters up to the next one literal, which is +/// how a `{` is written (`'{'`), and two quotes in a row stand for one. An +/// apostrophe in a message must therefore be doubled: `it''s`. +/// +/// An argument that is `null` is written as `null`, and an element whose +/// index is past the end of the arguments is written back as it was, `{2}`. +/// +/// #### Differences from the JDK +/// +/// - `toPattern` returns the elements as they were written rather than a +/// canonical rewrite of them. +/// - Date and time elements ignore the locale passed to the constructor: the +/// device formats dates for its own locale only. +public class MessageFormat extends Format { + + private static final long serialVersionUID = 1L; + + private static final int TYPE_NONE = 0; + private static final int TYPE_NUMBER = 1; + private static final int TYPE_DATE = 2; + private static final int TYPE_TIME = 3; + private static final int TYPE_CHOICE = 4; + + private Locale locale; + + /// The literal text before each element, and after the last one: always + /// one longer than the element arrays. + private String[] literals = new String[] {""}; + private int[] argumentNumbers = new int[0]; + private Format[] formats = new Format[0]; + private String[] elementTexts = new String[0]; + + public MessageFormat(String pattern) { + this(pattern, Locale.getDefault()); + } + + public MessageFormat(String pattern, Locale locale) { + this.locale = locale; + parsePattern(pattern); + } + + public void setLocale(Locale locale) { + this.locale = locale; + } + + public Locale getLocale() { + return locale; + } + + public void applyPattern(String pattern) { + parsePattern(pattern); + } + + private void parsePattern(String pattern) { + List newLiterals = new ArrayList(); + List newNumbers = new ArrayList(); + List newFormats = new ArrayList(); + List newTexts = new ArrayList(); + StringBuilder raw = new StringBuilder(); + // The parts of the element being read: index, type, style. + StringBuilder[] parts = new StringBuilder[3]; + int part = -1; + boolean inQuote = false; + int braceDepth = 0; + for (int i = 0; i < pattern.length(); i++) { + char ch = pattern.charAt(i); + if (part < 0) { + if (ch == '\'') { + if (i + 1 < pattern.length() && pattern.charAt(i + 1) == '\'') { + raw.append(ch); + i++; + } else { + inQuote = !inQuote; + } + } else if (ch == '{' && !inQuote) { + part = 0; + parts[0] = new StringBuilder(); + parts[1] = null; + parts[2] = null; + } else { + raw.append(ch); + } + continue; + } + if (inQuote) { + // Inside an element the quotes are the sub-pattern's own. + parts[part].append(ch); + if (ch == '\'') { + inQuote = false; + } + continue; + } + if (ch == ',') { + if (part < 2) { + part++; + parts[part] = new StringBuilder(); + } else { + parts[part].append(ch); + } + } else if (ch == '{') { + braceDepth++; + parts[part].append(ch); + } else if (ch == '}') { + if (braceDepth == 0) { + newLiterals.add(raw.toString()); + raw.setLength(0); + String type = parts[1] == null ? "" : parts[1].toString(); + String style = parts[2] == null ? "" : parts[2].toString(); + int number = parseArgumentNumber(parts[0].toString()); + newNumbers.add(Integer.valueOf(number)); + Format made = makeFormat(type, style); + newFormats.add(made); + newTexts.add(number + describe(typeOf(type), style, made)); + part = -1; + } else { + braceDepth--; + parts[part].append(ch); + } + } else if (ch == ' ') { + // Leading spaces of the type are not part of it. + if (part != 1 || parts[1].length() > 0) { + parts[part].append(ch); + } + } else { + if (ch == '\'') { + inQuote = true; + } + parts[part].append(ch); + } + } + // Only an element left open at depth zero is refused. A pattern that + // ends inside a NESTED brace ("{0,choice,0#{") is accepted by every + // JDK from 8 to 25 -- java.text.MessageFormat tests braceStack == 0 + // at this point too -- so refusing it would reject what the desktop + // accepts. MessageFormatDifferentialTest.rejectsWhatTheJdkRejects + // holds both sides to it. + if (braceDepth == 0 && part >= 0) { + throw new IllegalArgumentException("Unmatched braces in the pattern."); + } + newLiterals.add(raw.toString()); + int n = newNumbers.size(); + literals = newLiterals.toArray(new String[newLiterals.size()]); + argumentNumbers = new int[n]; + for (int i = 0; i < n; i++) { + argumentNumbers[i] = newNumbers.get(i).intValue(); + } + formats = newFormats.toArray(new Format[n]); + elementTexts = newTexts.toArray(new String[n]); + } + + private static int parseArgumentNumber(String text) { + boolean negative = text.startsWith("-"); + int from = negative ? 1 : 0; + if (text.length() == from || text.length() - from > 9) { + throw new IllegalArgumentException("can't parse argument number: " + text); + } + int value = 0; + for (int i = from; i < text.length(); i++) { + char c = text.charAt(i); + if (c < '0' || c > '9') { + throw new IllegalArgumentException("can't parse argument number: " + text); + } + value = value * 10 + (c - '0'); + } + if (negative) { + throw new IllegalArgumentException("negative argument number: " + text); + } + return value; + } + + private static int typeOf(String type) { + String t = type.trim(); + if (t.length() == 0) { + return TYPE_NONE; + } + if (t.equalsIgnoreCase("number")) { + return TYPE_NUMBER; + } + if (t.equalsIgnoreCase("date")) { + return TYPE_DATE; + } + if (t.equalsIgnoreCase("time")) { + return TYPE_TIME; + } + if (t.equalsIgnoreCase("choice")) { + return TYPE_CHOICE; + } + throw new IllegalArgumentException("unknown format type: " + type); + } + + /// The date style a keyword names, or -1 when the text is a pattern. + private static int dateStyle(String style) { + String s = style.trim(); + if (s.length() == 0) { + return DateFormat.DEFAULT; + } + if (s.equalsIgnoreCase("short")) { + return DateFormat.SHORT; + } + if (s.equalsIgnoreCase("medium")) { + return DateFormat.DEFAULT; + } + if (s.equalsIgnoreCase("long")) { + return DateFormat.LONG; + } + if (s.equalsIgnoreCase("full")) { + return DateFormat.FULL; + } + return -1; + } + + private Format makeFormat(String type, String style) { + int kind = typeOf(type); + String keyword = style.trim(); + if (kind == TYPE_NUMBER) { + if (keyword.length() == 0) { + return NumberFormat.getInstance(locale); + } + if (keyword.equalsIgnoreCase("currency")) { + return NumberFormat.getCurrencyInstance(locale); + } + if (keyword.equalsIgnoreCase("percent")) { + return NumberFormat.getPercentInstance(locale); + } + if (keyword.equalsIgnoreCase("integer")) { + return NumberFormat.getIntegerInstance(locale); + } + return new DecimalFormat(style, DecimalFormatSymbols.getInstance(locale)); + } + if (kind == TYPE_DATE) { + int dateStyle = dateStyle(style); + return dateStyle >= 0 ? DateFormat.getDateInstance(dateStyle) : new SimpleDateFormat(style); + } + if (kind == TYPE_TIME) { + int timeStyle = dateStyle(style); + return timeStyle >= 0 ? DateFormat.getTimeInstance(timeStyle) : new SimpleDateFormat(style); + } + if (kind == TYPE_CHOICE) { + try { + return new ChoiceFormat(style); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Choice Pattern incorrect: " + style); + } + } + return null; + } + + /// The type and style of an element as `toPattern` writes them: not the + /// text that was read but, as in the JDK, a description of the format it + /// produced -- `{0, number, #.##}` comes back as `{0,number,#0.##}`. + private String describe(int kind, String style, Format made) { + if (kind == TYPE_NUMBER) { + if (made.equals(NumberFormat.getInstance(locale))) { + return ",number"; + } + if (made.equals(NumberFormat.getCurrencyInstance(locale))) { + return ",number,currency"; + } + if (made.equals(NumberFormat.getPercentInstance(locale))) { + return ",number,percent"; + } + if (made.equals(NumberFormat.getIntegerInstance(locale))) { + return ",number,integer"; + } + return made instanceof DecimalFormat ? ",number," + ((DecimalFormat) made).toPattern() : ",number"; + } + if (kind == TYPE_CHOICE) { + return made instanceof ChoiceFormat ? ",choice," + ((ChoiceFormat) made).toPattern() : ",choice"; + } + if (kind == TYPE_DATE || kind == TYPE_TIME) { + String name = kind == TYPE_DATE ? ",date" : ",time"; + int dateStyle = dateStyle(style); + if (dateStyle == DateFormat.SHORT) { + return name + ",short"; + } + if (dateStyle == DateFormat.LONG) { + return name + ",long"; + } + if (dateStyle == DateFormat.FULL) { + return name + ",full"; + } + // A pattern of either type is a date pattern to the JDK. + return dateStyle >= 0 ? name : ",date," + style; + } + return ""; + } + + private static void appendLiteral(StringBuilder out, String text) { + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (c == '\'') { + out.append("''"); + } else if (c == '{') { + out.append("'{'"); + } else { + out.append(c); + } + } + } + + public String toPattern() { + StringBuilder out = new StringBuilder(); + for (int i = 0; i < argumentNumbers.length; i++) { + appendLiteral(out, literals[i]); + out.append('{').append(elementTexts[i]).append('}'); + } + appendLiteral(out, literals[argumentNumbers.length]); + return out.toString(); + } + + public static String format(String pattern, Object... arguments) { + return new MessageFormat(pattern).format(arguments); + } + + /// Formats an array of arguments, which `arguments` must be. + /// + /// #### Throws + /// + /// - `IllegalArgumentException`: if `arguments` is not an `Object[]`, or + /// an argument is not of the type its element formats + @Override + public final StringBuffer format(Object arguments, StringBuffer result, FieldPosition pos) { + if (arguments == null || arguments instanceof Object[]) { + return format((Object[]) arguments, result, pos); + } + throw new IllegalArgumentException("The arguments of a message are an Object[]"); + } + + public final StringBuffer format(Object[] arguments, StringBuffer result, FieldPosition pos) { + for (int i = 0; i < argumentNumbers.length; i++) { + result.append(literals[i]); + int number = argumentNumbers[i]; + if (arguments == null || number >= arguments.length) { + result.append('{').append(number).append('}'); + continue; + } + Object argument = arguments[number]; + Format format = formats[i]; + if (argument == null) { + result.append("null"); + } else if (format instanceof ChoiceFormat) { + String chosen = ((ChoiceFormat) format).format(argument); + if (chosen.indexOf('{') >= 0) { + new MessageFormat(chosen, locale).format(arguments, result, pos); + } else { + result.append(chosen); + } + } else if (format instanceof NumberFormat) { + result.append(((NumberFormat) format).format(argument)); + } else if (format instanceof DateFormat) { + result.append(formatDate((DateFormat) format, argument)); + } else if (format != null) { + result.append(format.format(argument)); + } else if (argument instanceof Number) { + result.append(NumberFormat.getInstance(locale).format(argument)); + } else if (argument instanceof Date) { + DateFormat dateTime = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT); + result.append(dateTime.format((Date) argument)); + } else if (argument instanceof String) { + result.append((String) argument); + } else { + result.append(String.valueOf(argument.toString())); + } + } + result.append(literals[argumentNumbers.length]); + return result; + } + + private static String formatDate(DateFormat format, Object argument) { + if (argument instanceof Date) { + return format.format((Date) argument); + } + if (argument instanceof Number) { + return format.format(new Date(((Number) argument).longValue())); + } + throw new IllegalArgumentException("Cannot format given Object as a Date"); + } + + /// The formats of the elements, in the order the elements appear in the + /// pattern; `null` for an element that has none. + public Format[] getFormats() { + Format[] copy = new Format[formats.length]; + System.arraycopy(formats, 0, copy, 0, formats.length); + return copy; + } + + /// The formats of the arguments, indexed by argument number; `null` for + /// an argument no element formats. Where several elements name the same + /// argument, the last one's format is the one returned. + public Format[] getFormatsByArgumentIndex() { + Format[] result = new Format[maximumArgumentNumber() + 1]; + for (int i = 0; i < argumentNumbers.length; i++) { + result[argumentNumbers[i]] = formats[i]; + } + return result; + } + + private int maximumArgumentNumber() { + int max = -1; + for (int i = 0; i < argumentNumbers.length; i++) { + max = Math.max(max, argumentNumbers[i]); + } + return max; + } + + /// Sets the format of the element at `formatElementIndex`, counted in + /// the order the elements appear in the pattern. + /// + /// #### Throws + /// + /// - `ArrayIndexOutOfBoundsException`: if there is no such element + public void setFormat(int formatElementIndex, Format newFormat) { + if (formatElementIndex < 0 || formatElementIndex >= formats.length) { + throw new ArrayIndexOutOfBoundsException(formatElementIndex); + } + formats[formatElementIndex] = newFormat; + elementTexts[formatElementIndex] = argumentNumbers[formatElementIndex] + describeFormat(newFormat); + } + + /// Sets the formats of the elements in pattern order; extra formats are + /// ignored and elements past the end of `newFormats` are left alone. + public void setFormats(Format[] newFormats) { + int n = Math.min(newFormats.length, formats.length); + for (int i = 0; i < n; i++) { + setFormat(i, newFormats[i]); + } + } + + /// Sets the format of every element that formats argument + /// `argumentIndex`. + public void setFormatByArgumentIndex(int argumentIndex, Format newFormat) { + for (int i = 0; i < argumentNumbers.length; i++) { + if (argumentNumbers[i] == argumentIndex) { + setFormat(i, newFormat); + } + } + } + + /// Sets the formats of the arguments, indexed by argument number. + public void setFormatsByArgumentIndex(Format[] newFormats) { + for (int i = 0; i < argumentNumbers.length; i++) { + if (argumentNumbers[i] < newFormats.length) { + setFormat(i, newFormats[argumentNumbers[i]]); + } + } + } + + /// How `toPattern` writes an element whose format was set rather than + /// read from the pattern: as the JDK does, by recognising the format. + private String describeFormat(Format f) { + if (f instanceof ChoiceFormat) { + return describe(TYPE_CHOICE, "", f); + } + if (f instanceof NumberFormat) { + return describe(TYPE_NUMBER, "", f); + } + if (f instanceof DateFormat) { + String[] names = {",short", "", ",long", ",full"}; + int[] styles = {DateFormat.SHORT, DateFormat.MEDIUM, DateFormat.LONG, DateFormat.FULL}; + for (int i = 0; i < styles.length; i++) { + if (f.equals(DateFormat.getDateInstance(styles[i]))) { + return ",date" + names[i]; + } + if (f.equals(DateFormat.getTimeInstance(styles[i]))) { + return ",time" + names[i]; + } + } + if (f instanceof SimpleDateFormat) { + return ",date," + ((SimpleDateFormat) f).toPattern(); + } + } + return ""; + } + + /// Reads the arguments back out of `source`, starting at `pos`: the + /// literal text must match exactly, an element with a format is read by + /// that format, and an element without one takes the text up to the + /// literal that follows it. The result is indexed by argument number. On + /// failure `pos` is left alone, its error index is set, and `null` is + /// returned. + public Object[] parse(String source, ParsePosition pos) { + if (source == null) { + return new Object[0]; + } + Object[] result = new Object[maximumArgumentNumber() + 1]; + int at = pos.getIndex(); + ParsePosition inner = new ParsePosition(0); + for (int i = 0; i < argumentNumbers.length; i++) { + String literal = literals[i]; + if (!source.regionMatches(at, literal, 0, literal.length())) { + pos.setErrorIndex(at); + return null; + } + at += literal.length(); + Format format = formats[i]; + if (format == null) { + // The text runs to the literal after this element; with + // none, to the end of the source. + String next = literals[i + 1]; + int end = next.length() == 0 ? source.length() : source.indexOf(next, at); + if (end < 0) { + pos.setErrorIndex(at); + return null; + } + String value = source.substring(at, end); + if (!value.equals("{" + argumentNumbers[i] + "}")) { + result[argumentNumbers[i]] = value; + } + at = end; + } else { + inner.setIndex(at); + Object value = format.parseObject(source, inner); + if (inner.getIndex() == at) { + pos.setErrorIndex(at); + return null; + } + result[argumentNumbers[i]] = value; + at = inner.getIndex(); + } + } + String last = literals[argumentNumbers.length]; + if (!source.regionMatches(at, last, 0, last.length())) { + pos.setErrorIndex(at); + return null; + } + pos.setIndex(at + last.length()); + return result; + } + + /// Reads the arguments back out of the beginning of `source`. + /// + /// #### Throws + /// + /// - `ParseException`: if `source` does not match the pattern + public Object[] parse(String source) throws ParseException { + ParsePosition pos = new ParsePosition(0); + Object[] result = parse(source, pos); + if (pos.getIndex() == 0) { + throw new ParseException("MessageFormat parse error!", pos.getErrorIndex()); + } + return result; + } + + @Override + public Object parseObject(String source, ParsePosition pos) { + return parse(source, pos); + } + + /// A copy of this format, built by applying its pattern again: + /// `Object.clone()` answers `null` on the device for anything but an + /// array. + @Override + public Object clone() { + return new MessageFormat(toPattern(), locale); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass() || !(obj instanceof MessageFormat)) { + return false; + } + MessageFormat other = (MessageFormat) obj; + if (locale == null ? other.locale != null : !locale.equals(other.locale)) { + return false; + } + return toPattern().equals(other.toPattern()); + } + + @Override + public int hashCode() { + return toPattern().hashCode(); + } +} diff --git a/Ports/CLDC11/src/java/text/NumberFormat.java b/Ports/CLDC11/src/java/text/NumberFormat.java new file mode 100644 index 00000000000..2ca8abdca38 --- /dev/null +++ b/Ports/CLDC11/src/java/text/NumberFormat.java @@ -0,0 +1,260 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.text; + +import java.util.Locale; + +/// The base of the number formats, and the factory for the usual ones. +/// +/// The factory methods answer a [DecimalFormat] with these patterns: +/// +/// - `getInstance` / `getNumberInstance`: `#,##0.###` +/// - `getIntegerInstance`: `#,##0`, rounding to a whole number and parsing +/// only the integer part +/// - `getPercentInstance`: `#,##0%` +/// - `getCurrencyInstance`: the currency symbol followed by `#,##0.00`, and a +/// minus sign before the symbol for a negative amount +/// +/// The separators and the currency symbol are those of +/// [DecimalFormatSymbols]; read there how a locale is honoured. +/// +/// #### Differences from the JDK +/// +/// - Rounding is always half-even, the JDK's default. There is no +/// `setRoundingMode`, as the device has no `java.math.RoundingMode`. +/// - There is no `getCurrency`/`setCurrency`, as the device has no +/// `java.util.Currency`. +public abstract class NumberFormat extends Format { + + private static final long serialVersionUID = 1L; + + /// Names the integer part of a formatted number to a [FieldPosition]. + public static final int INTEGER_FIELD = 0; + + /// Names the fraction part of a formatted number to a [FieldPosition]. + public static final int FRACTION_FIELD = 1; + + private boolean groupingUsed = true; + private int maximumIntegerDigits = 40; + private int minimumIntegerDigits = 1; + private int maximumFractionDigits = 3; + private int minimumFractionDigits; + private boolean parseIntegerOnly; + + protected NumberFormat() { + } + + public static final NumberFormat getInstance() { + return getNumberInstance(Locale.getDefault()); + } + + public static NumberFormat getInstance(Locale inLocale) { + return getNumberInstance(inLocale); + } + + public static final NumberFormat getNumberInstance() { + return getNumberInstance(Locale.getDefault()); + } + + public static NumberFormat getNumberInstance(Locale inLocale) { + return new DecimalFormat("#,##0.###", DecimalFormatSymbols.getInstance(inLocale)); + } + + public static final NumberFormat getIntegerInstance() { + return getIntegerInstance(Locale.getDefault()); + } + + public static NumberFormat getIntegerInstance(Locale inLocale) { + DecimalFormat format = new DecimalFormat("#,##0", DecimalFormatSymbols.getInstance(inLocale)); + format.setParseIntegerOnly(true); + return format; + } + + public static final NumberFormat getPercentInstance() { + return getPercentInstance(Locale.getDefault()); + } + + public static NumberFormat getPercentInstance(Locale inLocale) { + return new DecimalFormat("#,##0%", DecimalFormatSymbols.getInstance(inLocale)); + } + + public static final NumberFormat getCurrencyInstance() { + return getCurrencyInstance(Locale.getDefault()); + } + + public static NumberFormat getCurrencyInstance(Locale inLocale) { + String pattern = DecimalFormat.CURRENCY_SIGN + "#,##0.00"; + return new DecimalFormat(pattern, DecimalFormatSymbols.getInstance(inLocale)); + } + + /// Formats a `Number`: an integral type as a `long`, anything else as a + /// `double`. + /// + /// #### Throws + /// + /// - `IllegalArgumentException`: if `number` is not a `Number` + @Override + public StringBuffer format(Object number, StringBuffer toAppendTo, FieldPosition pos) { + if (number instanceof Long || number instanceof Integer || number instanceof Short + || number instanceof Byte) { + return format(((Number) number).longValue(), toAppendTo, pos); + } + if (number instanceof Number) { + return format(((Number) number).doubleValue(), toAppendTo, pos); + } + throw new IllegalArgumentException("Cannot format given Object as a Number"); + } + + public final String format(double number) { + return format(number, new StringBuffer(), new FieldPosition(0)).toString(); + } + + public final String format(long number) { + return format(number, new StringBuffer(), new FieldPosition(0)).toString(); + } + + public abstract StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos); + + public abstract StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos); + + /// Parses a number starting at `parsePosition`, which is advanced past + /// what was read. On failure the position is left alone, its error index + /// is set, and `null` is returned. The result is a `Long` when the text is + /// a whole number that fits one, and a `Double` otherwise. + public abstract Number parse(String source, ParsePosition parsePosition); + + public Number parse(String source) throws ParseException { + ParsePosition parsePosition = new ParsePosition(0); + Number result = parse(source, parsePosition); + if (parsePosition.getIndex() == 0) { + throw new ParseException("Unparseable number: \"" + source + "\"", parsePosition.getErrorIndex()); + } + return result; + } + + @Override + public final Object parseObject(String source, ParsePosition pos) { + return parse(source, pos); + } + + public boolean isParseIntegerOnly() { + return parseIntegerOnly; + } + + public void setParseIntegerOnly(boolean value) { + parseIntegerOnly = value; + } + + public boolean isGroupingUsed() { + return groupingUsed; + } + + public void setGroupingUsed(boolean newValue) { + groupingUsed = newValue; + } + + public int getMaximumIntegerDigits() { + return maximumIntegerDigits; + } + + public void setMaximumIntegerDigits(int newValue) { + maximumIntegerDigits = Math.max(0, newValue); + if (minimumIntegerDigits > maximumIntegerDigits) { + minimumIntegerDigits = maximumIntegerDigits; + } + } + + public int getMinimumIntegerDigits() { + return minimumIntegerDigits; + } + + public void setMinimumIntegerDigits(int newValue) { + minimumIntegerDigits = Math.max(0, newValue); + if (minimumIntegerDigits > maximumIntegerDigits) { + maximumIntegerDigits = minimumIntegerDigits; + } + } + + public int getMaximumFractionDigits() { + return maximumFractionDigits; + } + + public void setMaximumFractionDigits(int newValue) { + maximumFractionDigits = Math.max(0, newValue); + if (maximumFractionDigits < minimumFractionDigits) { + minimumFractionDigits = maximumFractionDigits; + } + } + + public int getMinimumFractionDigits() { + return minimumFractionDigits; + } + + public void setMinimumFractionDigits(int newValue) { + minimumFractionDigits = Math.max(0, newValue); + if (maximumFractionDigits < minimumFractionDigits) { + maximumFractionDigits = minimumFractionDigits; + } + } + + /// Copies the settings this class holds into `target`, for the `clone` + /// of a subclass: `Object.clone()` answers `null` on the device. + final void copyNumberFormatTo(NumberFormat target) { + target.groupingUsed = groupingUsed; + target.maximumIntegerDigits = maximumIntegerDigits; + target.minimumIntegerDigits = minimumIntegerDigits; + target.maximumFractionDigits = maximumFractionDigits; + target.minimumFractionDigits = minimumFractionDigits; + target.parseIntegerOnly = parseIntegerOnly; + } + + /// A copy of this format. Every subclass in this package implements it + /// by copying its fields. + @Override + public abstract Object clone(); + + @Override + public int hashCode() { + return maximumIntegerDigits * 37 + maximumFractionDigits; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (this == obj) { + return true; + } + if (getClass() != obj.getClass() || !(obj instanceof NumberFormat)) { + return false; + } + NumberFormat other = (NumberFormat) obj; + return maximumIntegerDigits == other.maximumIntegerDigits + && minimumIntegerDigits == other.minimumIntegerDigits + && maximumFractionDigits == other.maximumFractionDigits + && minimumFractionDigits == other.minimumFractionDigits + && groupingUsed == other.groupingUsed + && parseIntegerOnly == other.parseIntegerOnly; + } +} diff --git a/Ports/CLDC11/src/java/text/ParsePosition.java b/Ports/CLDC11/src/java/text/ParsePosition.java new file mode 100644 index 00000000000..7f3327e3c36 --- /dev/null +++ b/Ports/CLDC11/src/java/text/ParsePosition.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.text; + +/// Where a parse starts, where it stopped, and where it failed. +public class ParsePosition { + + private int index; + private int errorIndex = -1; + + public ParsePosition(int index) { + this.index = index; + } + + public int getIndex() { + return index; + } + + public void setIndex(int index) { + this.index = index; + } + + public void setErrorIndex(int ei) { + errorIndex = ei; + } + + public int getErrorIndex() { + return errorIndex; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ParsePosition)) { + return false; + } + ParsePosition other = (ParsePosition) obj; + return index == other.index && errorIndex == other.errorIndex; + } + + @Override + public int hashCode() { + return (errorIndex << 16) | index; + } + + @Override + public String toString() { + return getClass().getName() + "[index=" + index + ",errorIndex=" + errorIndex + ']'; + } +} diff --git a/Ports/CLDC11/src/java/text/SimpleDateFormat.java b/Ports/CLDC11/src/java/text/SimpleDateFormat.java index 485b17502ed..810a35dd5b2 100644 --- a/Ports/CLDC11/src/java/text/SimpleDateFormat.java +++ b/Ports/CLDC11/src/java/text/SimpleDateFormat.java @@ -354,12 +354,20 @@ String leftPad(int v, int size) { */ @Override public Date parse(String source) throws ParseException { + return parseAt(source, 0, null); + } + + @Override + Date parseAt(String source, int start, int[] end) throws ParseException { if (pattern == null) { - return super.parse(source); + return super.parseAt(source, start, end); } - int startIndex = 0; + int startIndex = start; // parse based on GMT timezone for handling offsets Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone(GMT)); + // A field the pattern does not parse takes its value from the epoch, + // 1970-01-01 00:00:00.000, as in the JDK -- never from the current time. + calendar.setTime(new Date(0L)); int tzMinutes = -1; List pattern = getPatternTokens(); for (int i = 0; i < pattern.size(); i++) { @@ -452,6 +460,9 @@ public Date parse(String source) throws ParseException { startIndex += s.length(); } } + if (end != null) { + end[0] = startIndex; + } TimeZone localTimezone = Calendar.getInstance().getTimeZone(); calendar.setTimeZone(localTimezone); // If timezone offset not part of date, the date passed will be treated @@ -616,7 +627,7 @@ String readNumber(String source, int ofs, String token, boolean adjacent) { char ch = source.charAt(i); if (isNumeric(ch) == false) { // empty string would be invalid number - if (i == 0) { + if (i == ofs) { return null; } return source.substring(ofs, i); @@ -789,7 +800,7 @@ String readMonth(String source, int ofs, String token, boolean adjacent) { if (adjacent) { return source.substring(ofs, ofs + token.length()); } - if (isNumeric(source.charAt(0))) { + if (ofs < source.length() && isNumeric(source.charAt(ofs))) { return readNumber(source, ofs, token, adjacent); } } @@ -834,6 +845,9 @@ String readMonth(String source, int ofs, String token, boolean adjacent) { /// /// - DateFormatSymbols int parseMonth(String month, int offset) throws ParseException { + if (month == null) { + throwInvalid("month", offset); + } if (month.length() < 3) { return (parseNumber(month, offset, "month", 1, 12) - 1) + Calendar.JANUARY; } diff --git a/Ports/CLDC11/src/java/time/Clock.java b/Ports/CLDC11/src/java/time/Clock.java index 85d04e71d48..c6e942aeecf 100644 --- a/Ports/CLDC11/src/java/time/Clock.java +++ b/Ports/CLDC11/src/java/time/Clock.java @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ package java.time; public abstract class Clock { @@ -5,6 +27,9 @@ public abstract class Clock { public abstract Instant instant(); + /// This clock, reporting its instants in `zone`. + public abstract Clock withZone(ZoneId zone); + public long millis() { return instant().toEpochMilli(); } @@ -17,10 +42,32 @@ public static Clock systemDefaultZone() { return new SystemClock(ZoneId.systemDefault()); } + /// The system clock, seen in `zone`. + public static Clock system(ZoneId zone) { + if (zone == null) { + throw new NullPointerException("zone"); + } + return new SystemClock(zone); + } + public static Clock fixed(Instant fixedInstant, ZoneId zone) { return new FixedClock(fixedInstant, zone); } + /// A clock that reads `base` moved on by `offsetDuration`. + public static Clock offset(Clock base, Duration offsetDuration) { + if (base == null) { + throw new NullPointerException("baseClock"); + } + if (offsetDuration == null) { + throw new NullPointerException("offsetDuration"); + } + if (offsetDuration.isZero()) { + return base; + } + return new OffsetClock(base, offsetDuration); + } + private static final class SystemClock extends Clock { private final ZoneId zone; @@ -35,6 +82,22 @@ public ZoneId getZone() { public Instant instant() { return Instant.now(); } + + public Clock withZone(ZoneId zone) { + return zone.equals(this.zone) ? this : new SystemClock(zone); + } + + public boolean equals(Object obj) { + return obj instanceof SystemClock && zone.equals(((SystemClock) obj).zone); + } + + public int hashCode() { + return zone.hashCode() + 1; + } + + public String toString() { + return "SystemClock[" + zone + "]"; + } } private static final class FixedClock extends Clock { @@ -53,5 +116,63 @@ public ZoneId getZone() { public Instant instant() { return instant; } + + public Clock withZone(ZoneId zone) { + return zone.equals(this.zone) ? this : new FixedClock(instant, zone); + } + + public boolean equals(Object obj) { + if (!(obj instanceof FixedClock)) { + return false; + } + FixedClock other = (FixedClock) obj; + return instant.equals(other.instant) && zone.equals(other.zone); + } + + public int hashCode() { + return instant.hashCode() ^ zone.hashCode(); + } + + public String toString() { + return "FixedClock[" + instant + "," + zone + "]"; + } + } + + private static final class OffsetClock extends Clock { + private final Clock base; + private final Duration offset; + + private OffsetClock(Clock base, Duration offset) { + this.base = base; + this.offset = offset; + } + + public ZoneId getZone() { + return base.getZone(); + } + + public Instant instant() { + return base.instant().plus(offset); + } + + public Clock withZone(ZoneId zone) { + return zone.equals(base.getZone()) ? this : new OffsetClock(base.withZone(zone), offset); + } + + public boolean equals(Object obj) { + if (!(obj instanceof OffsetClock)) { + return false; + } + OffsetClock other = (OffsetClock) obj; + return base.equals(other.base) && offset.equals(other.offset); + } + + public int hashCode() { + return base.hashCode() ^ offset.hashCode(); + } + + public String toString() { + return "OffsetClock[" + base + "," + offset + "]"; + } } } diff --git a/Ports/CLDC11/src/java/time/DateTimeSupport.java b/Ports/CLDC11/src/java/time/DateTimeSupport.java index ff219a6e3f2..d3e4b356d15 100644 --- a/Ports/CLDC11/src/java/time/DateTimeSupport.java +++ b/Ports/CLDC11/src/java/time/DateTimeSupport.java @@ -1,225 +1,71 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ package java.time; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.Date; -import java.util.Locale; import java.util.TimeZone; +/// Where `java.time` meets the platform's time zone rules. final class DateTimeSupport { - static final long MILLIS_PER_SECOND = 1000L; - static final long MILLIS_PER_DAY = 86400000L; - static final long SECONDS_PER_DAY = 86400L; - static final long NANOS_PER_SECOND = 1000000000L; - static final long NANOS_PER_MILLI = 1000000L; - static final long NANOS_PER_DAY = 86400000000000L; - - private static final long DAYS_0000_TO_1970 = 719528L; - private DateTimeSupport() { } - static int floorDiv(int x, int y) { - int r = x / y; - if ((x ^ y) < 0 && (r * y != x)) { - r--; - } - return r; - } - - static long floorDiv(long x, long y) { - long r = x / y; - if ((x ^ y) < 0 && (r * y != x)) { - r--; - } - return r; - } - - static int floorMod(int x, int y) { - return x - floorDiv(x, y) * y; - } - - static long floorMod(long x, long y) { - return x - floorDiv(x, y) * y; - } - - static boolean isLeapYear(int year) { - return ((year & 3) == 0) && ((year % 100) != 0 || (year % 400) == 0); - } - - static int lengthOfMonth(int year, int month) { - switch (month) { - case 2: - return isLeapYear(year) ? 29 : 28; - case 4: - case 6: - case 9: - case 11: - return 30; - default: - return 31; - } - } - - static long toEpochDay(int year, int month, int dayOfMonth) { - long y = year; - long m = month; - long total = 365L * y; - if (y >= 0) { - total += (y + 3) / 4 - (y + 99) / 100 + (y + 399) / 400; - } else { - total -= y / -4 - y / -100 + y / -400; - } - total += ((367 * m - 362) / 12); - total += dayOfMonth - 1; - if (m > 2) { - total--; - if (!isLeapYear(year)) { - total--; - } - } - return total - DAYS_0000_TO_1970; - } - - static int[] epochDayToDate(long epochDay) { - long zeroDay = epochDay + DAYS_0000_TO_1970; - zeroDay -= 60; - long adjust = 0; - if (zeroDay < 0) { - long adjustCycles = (zeroDay + 1) / 146097 - 1; - adjust = adjustCycles * 400; - zeroDay += -adjustCycles * 146097; - } - long yearEst = (400 * zeroDay + 591) / 146097; - long doyEst = zeroDay - (365 * yearEst + yearEst / 4 - yearEst / 100 + yearEst / 400); - if (doyEst < 0) { - yearEst--; - doyEst = zeroDay - (365 * yearEst + yearEst / 4 - yearEst / 100 + yearEst / 400); - } - yearEst += adjust; - int marchDoy0 = (int) doyEst; - int marchMonth0 = (marchDoy0 * 5 + 2) / 153; - int month = (marchMonth0 + 2) % 12 + 1; - int day = marchDoy0 - (marchMonth0 * 306 + 5) / 10 + 1; - yearEst += marchMonth0 / 10; - return new int[] { (int) yearEst, month, day }; - } - - static void checkDate(int year, int month, int day) { - if (month < 1 || month > 12) { - throw new IllegalArgumentException("Invalid month: " + month); - } - int maxDay = lengthOfMonth(year, month); - if (day < 1 || day > maxDay) { - throw new IllegalArgumentException("Invalid day: " + day); - } - } - - static void checkTime(int hour, int minute, int second, int nano) { - if (hour < 0 || hour > 23) { - throw new IllegalArgumentException("Invalid hour: " + hour); - } - if (minute < 0 || minute > 59) { - throw new IllegalArgumentException("Invalid minute: " + minute); - } - if (second < 0 || second > 59) { - throw new IllegalArgumentException("Invalid second: " + second); - } - if (nano < 0 || nano >= NANOS_PER_SECOND) { - throw new IllegalArgumentException("Invalid nano: " + nano); - } - } - - static long toEpochSecond(LocalDate date, LocalTime time, ZoneOffset offset) { - long days = date.toEpochDay(); - long secs = days * SECONDS_PER_DAY + time.toSecondOfDay(); - return secs - offset.getTotalSeconds(); - } - - static int millisOfSecond(int nano) { - return nano / 1000000; - } - - static Calendar newCalendar(TimeZone tz) { - Calendar out = Calendar.getInstance(tz); - return out; - } - static LocalDateTime localDateTimeFromInstant(Instant instant, ZoneId zone) { ZoneOffset offset = offsetFromInstant(instant, zone); - long localSecond = instant.getEpochSecond() + offset.getTotalSeconds(); - long epochDay = floorDiv(localSecond, SECONDS_PER_DAY); - int secondOfDay = (int) floorMod(localSecond, SECONDS_PER_DAY); - LocalDate date = LocalDate.ofEpochDay(epochDay); - LocalTime time = LocalTime.ofNanoOfDay(secondOfDay * NANOS_PER_SECOND + instant.getNano()); - return LocalDateTime.of(date, time); + return LocalDateTime.ofEpochSecond(instant.getEpochSecond(), instant.getNano(), offset); } + /// The offset `zone` has at `instant`. static ZoneOffset offsetFromInstant(Instant instant, ZoneId zone) { + ZoneOffset fixed = zone.fixedOffset(); + if (fixed != null) { + // A fixed offset already is the answer; round-tripping it through + // the host time zone database can only lose or invert it. + return fixed; + } TimeZone tz = zone.toTimeZone(); - Calendar cal = newCalendar(TimeZone.getTimeZone("GMT")); - cal.setTime(new Date(instant.toEpochMilli())); - int offsetMillis = tz.getOffset( - 1, - cal.get(Calendar.YEAR), - cal.get(Calendar.MONTH), - cal.get(Calendar.DAY_OF_MONTH), - cal.get(Calendar.DAY_OF_WEEK), - ((cal.get(Calendar.HOUR_OF_DAY) * 60 + cal.get(Calendar.MINUTE)) * 60 + cal.get(Calendar.SECOND)) * 1000 - + cal.get(Calendar.MILLISECOND)); + // Ask the zone for its offset at this instant rather than reading the + // fields back out of a Calendar. Calendar reconstructs the local time + // from a raw offset plus a fixed one-hour daylight guess, which loses + // the saving on the desktop ports (Europe/Berlin in June came back as + // UTC), while TimeZone.getOffset consults the platform's own rules. + // getOffset takes LOCAL STANDARD time fields, so the instant is + // turned into local standard time first by adding the raw offset. + // Worked in seconds, not milliseconds: the instants java.time accepts + // reach past what a long counts in milliseconds. + long localStandard = instant.getEpochSecond() + tz.getRawOffset() / 1000; + long epochDay = TimeArithmetic.floorDiv(localStandard, 86400L); + int millisOfDay = (int) TimeArithmetic.floorMod(localStandard, 86400L) * 1000 + + instant.getNano() / 1000000; + LocalDate utcDate = LocalDate.ofEpochDay(epochDay); + // Calendar.SUNDAY is 1 and epoch day 0 was a Thursday. + int dayOfWeek = (int) TimeArithmetic.floorMod(epochDay + 4, 7) + 1; + // getOffset takes an era plus a year *of that era*, not a proleptic ISO + // year: ISO 0 is 1 BC, ISO -1 is 2 BC, hence 1 - isoYear. + int isoYear = utcDate.getYear(); + int era = isoYear > 0 ? 1 /* GregorianCalendar.AD */ : 0 /* GregorianCalendar.BC */; + int yearOfEra = isoYear > 0 ? isoYear : 1 - isoYear; + int offsetMillis = tz.getOffset(era, yearOfEra, + utcDate.getMonthValue() - 1, utcDate.getDayOfMonth(), dayOfWeek, millisOfDay); return ZoneOffset.ofTotalSeconds(offsetMillis / 1000); } - - static SimpleDateFormat newFormat(String pattern, ZoneId zone, Locale locale) { - SimpleDateFormat sdf = new SimpleDateFormat(pattern); - return sdf; - } - - static String formatPattern(String pattern, TemporalCarrier carrier, Locale locale) { - ZoneId zone = carrier.getZoneForFormatting(); - SimpleDateFormat sdf = newFormat(pattern, zone, locale); - TimeZone original = TimeZone.getDefault(); - try { - if (zone != null) { - TimeZone.setDefault(zone.toTimeZone()); - } - return sdf.format(new Date(carrier.toInstant().toEpochMilli())); - } finally { - TimeZone.setDefault(original); - } - } - - static ParsedPatternResult parsePattern(String text, String pattern, ZoneId defaultZone, Locale locale) { - TimeZone original = TimeZone.getDefault(); - try { - if (defaultZone != null) { - TimeZone.setDefault(defaultZone.toTimeZone()); - } - SimpleDateFormat sdf = newFormat(pattern, defaultZone, locale); - Date date = sdf.parse(text); - Instant instant = Instant.ofEpochMilli(date.getTime()); - ZoneId zone = defaultZone == null ? ZoneOffset.UTC : defaultZone; - return new ParsedPatternResult(instant, zone); - } catch (ParseException err) { - throw new java.time.format.DateTimeParseException(err.getMessage(), text, 0); - } finally { - TimeZone.setDefault(original); - } - } - - interface TemporalCarrier { - Instant toInstant(); - ZoneId getZoneForFormatting(); - } - - static final class ParsedPatternResult { - public final Instant instant; - public final ZoneId zone; - - ParsedPatternResult(Instant instant, ZoneId zone) { - this.instant = instant; - this.zone = zone; - } - } } diff --git a/Ports/CLDC11/src/java/time/DayOfWeek.java b/Ports/CLDC11/src/java/time/DayOfWeek.java new file mode 100644 index 00000000000..95ba8eaa2c7 --- /dev/null +++ b/Ports/CLDC11/src/java/time/DayOfWeek.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.time; + +import java.time.temporal.TemporalAccessor; + +/// The seven days of the week, Monday first as ISO counts them. +public enum DayOfWeek implements TemporalAccessor { + /// Monday, day 1. + MONDAY, + /// Tuesday, day 2. + TUESDAY, + /// Wednesday, day 3. + WEDNESDAY, + /// Thursday, day 4. + THURSDAY, + /// Friday, day 5. + FRIDAY, + /// Saturday, day 6. + SATURDAY, + /// Sunday, day 7. + SUNDAY; + + private static final DayOfWeek[] ALL = values(); + + /// The day numbered `dayOfWeek`, 1 for Monday to 7 for Sunday. + public static DayOfWeek of(int dayOfWeek) { + if (dayOfWeek < 1 || dayOfWeek > 7) { + throw new DateTimeException("Invalid value for DayOfWeek: " + dayOfWeek); + } + return ALL[dayOfWeek - 1]; + } + + /// 1 for Monday to 7 for Sunday. + public int getValue() { + return ordinal() + 1; + } + + /// The day `days` later, around the week. + public DayOfWeek plus(long days) { + int amount = (int) (days % 7); + return ALL[(ordinal() + amount + 7) % 7]; + } + + /// The day `days` earlier, around the week. + public DayOfWeek minus(long days) { + return plus(-(days % 7)); + } +} diff --git a/Ports/CLDC11/src/java/time/Duration.java b/Ports/CLDC11/src/java/time/Duration.java index 9c56a302ff7..4b15161d750 100644 --- a/Ports/CLDC11/src/java/time/Duration.java +++ b/Ports/CLDC11/src/java/time/Duration.java @@ -23,8 +23,24 @@ package java.time; import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoUnit; +import java.time.temporal.Temporal; +import java.time.temporal.TemporalAmount; +import java.time.temporal.TemporalUnit; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/// An amount of time in seconds and nanoseconds, such as 34.5 seconds. +/// +/// Arithmetic is exact: a result longer than a `Duration` holds throws +/// `ArithmeticException` rather than wrapping around, as the JDK's does. +public final class Duration implements TemporalAmount, Comparable { + /// A duration of zero. + public static final Duration ZERO = new Duration(0L, 0); + + private static final long NANOS_PER_SECOND = TimeArithmetic.NANOS_PER_SECOND; -public final class Duration implements Comparable { private final long seconds; private final int nanos; @@ -33,30 +49,105 @@ private Duration(long seconds, int nanos) { this.nanos = nanos; } + private static Duration create(long seconds, int nanos) { + if ((seconds | nanos) == 0) { + return ZERO; + } + return new Duration(seconds, nanos); + } + + /// A duration of `days` standard 24 hour days. public static Duration ofDays(long days) { - return ofSeconds(days * 86400L); + return create(TimeArithmetic.multiplyExact(days, 86400L), 0); } + /// A duration of `hours` hours. public static Duration ofHours(long hours) { - return ofSeconds(hours * 3600L); + return create(TimeArithmetic.multiplyExact(hours, 3600L), 0); } + /// A duration of `minutes` minutes. public static Duration ofMinutes(long minutes) { - return ofSeconds(minutes * 60L); + return create(TimeArithmetic.multiplyExact(minutes, 60L), 0); } + /// A duration of `seconds` seconds. public static Duration ofSeconds(long seconds) { - return new Duration(seconds, 0); + return create(seconds, 0); } + /// A duration of `seconds` seconds adjusted by `nanoAdjustment` + /// nanoseconds, which may take it across any number of seconds. public static Duration ofSeconds(long seconds, long nanoAdjustment) { - long secs = seconds + DateTimeSupport.floorDiv(nanoAdjustment, DateTimeSupport.NANOS_PER_SECOND); - int nanos = (int) DateTimeSupport.floorMod(nanoAdjustment, DateTimeSupport.NANOS_PER_SECOND); - return new Duration(secs, nanos); + long secs = TimeArithmetic.addExact(seconds, TimeArithmetic.floorDiv(nanoAdjustment, NANOS_PER_SECOND)); + int nos = (int) TimeArithmetic.floorMod(nanoAdjustment, NANOS_PER_SECOND); + return create(secs, nos); } + /// A duration of `millis` milliseconds. public static Duration ofMillis(long millis) { - return ofSeconds(DateTimeSupport.floorDiv(millis, 1000L), DateTimeSupport.floorMod(millis, 1000L) * 1000000L); + long secs = millis / 1000; + int mos = (int) (millis % 1000); + if (mos < 0) { + mos += 1000; + secs--; + } + return create(secs, mos * 1000000); + } + + /// A duration of `nanos` nanoseconds. + public static Duration ofNanos(long nanos) { + long secs = nanos / NANOS_PER_SECOND; + int nos = (int) (nanos % NANOS_PER_SECOND); + if (nos < 0) { + nos += (int) NANOS_PER_SECOND; + secs--; + } + return create(secs, nos); + } + + /// A duration of `amount` of `unit`. The unit must have an exact length: + /// a day or anything shorter. + public static Duration of(long amount, TemporalUnit unit) { + return ZERO.plus(amount, unit); + } + + /// The duration `amount` adds up to. Every unit it is made of must have + /// an exact length. + public static Duration from(TemporalAmount amount) { + if (amount == null) { + throw new NullPointerException("amount"); + } + Duration duration = ZERO; + List units = amount.getUnits(); + for (int i = 0; i < units.size(); i++) { + TemporalUnit unit = units.get(i); + duration = duration.plus(amount.get(unit), unit); + } + return duration; + } + + /// The duration from one value to another of the same kind, negative + /// when `endExclusive` comes first. + public static Duration between(Temporal startInclusive, Temporal endExclusive) { + try { + return ofNanos(startInclusive.until(endExclusive, ChronoUnit.NANOS)); + } catch (DateTimeException ex) { + return betweenInSeconds(startInclusive, endExclusive); + } catch (ArithmeticException ex) { + return betweenInSeconds(startInclusive, endExclusive); + } + } + + private static Duration betweenInSeconds(Temporal startInclusive, Temporal endExclusive) { + long secs = startInclusive.until(endExclusive, ChronoUnit.SECONDS); + long nanoDiff = TimeArithmetic.nanoOfSecond(endExclusive) - TimeArithmetic.nanoOfSecond(startInclusive); + if (secs > 0 && nanoDiff < 0) { + secs++; + } else if (secs < 0 && nanoDiff > 0) { + secs--; + } + return ofSeconds(secs, nanoDiff); } public static Duration parse(CharSequence text) { @@ -126,18 +217,18 @@ public static Duration parse(CharSequence text) { throw invalidDuration(value); } if (unit == 'D' && !time && !foundDays && fractionStart < 0) { - seconds += amount * 86400L; + seconds = parsedSum(value, seconds, amount, 86400L); foundDays = true; } else if (unit == 'H' && time && lastTimeUnit < 1 && fractionStart < 0) { - seconds += amount * 3600L; + seconds = parsedSum(value, seconds, amount, 3600L); lastTimeUnit = 1; foundTime = true; } else if (unit == 'M' && time && lastTimeUnit < 2 && fractionStart < 0) { - seconds += amount * 60L; + seconds = parsedSum(value, seconds, amount, 60L); lastTimeUnit = 2; foundTime = true; } else if (unit == 'S' && time && lastTimeUnit < 3) { - seconds += amount; + seconds = parsedSum(value, seconds, amount, 1L); lastTimeUnit = 3; foundTime = true; if (fractionStart >= 0) { @@ -161,46 +252,374 @@ public static Duration parse(CharSequence text) { if (!found || (time && !foundTime)) { throw invalidDuration(value); } - return ofSeconds(overallSign * seconds, overallSign * (long) nanos); + try { + return ofSeconds(TimeArithmetic.multiplyExact(overallSign, seconds), overallSign * (long) nanos); + } catch (ArithmeticException e) { + throw invalidDuration(value); + } + } + + /// `seconds` plus `amount` units of `unitSeconds` each. A field too large + /// for a duration is refused as the JDK refuses it, rather than wrapping + /// around to an unrelated one. + private static long parsedSum(String value, long seconds, long amount, long unitSeconds) { + try { + return TimeArithmetic.addExact(seconds, TimeArithmetic.multiplyExact(amount, unitSeconds)); + } catch (ArithmeticException e) { + throw invalidDuration(value); + } } private static DateTimeParseException invalidDuration(String value) { return new DateTimeParseException("Text cannot be parsed to a Duration", value, 0); } + /// The amount of `unit` held: the seconds or the nanoseconds. + @Override + public long get(TemporalUnit unit) { + if (unit == ChronoUnit.SECONDS) { + return seconds; + } + if (unit == ChronoUnit.NANOS) { + return nanos; + } + throw TimeArithmetic.unsupported(unit); + } + + /// Seconds and nanoseconds, the two units a duration is made of. + @Override + public List getUnits() { + List units = new ArrayList(2); + units.add(ChronoUnit.SECONDS); + units.add(ChronoUnit.NANOS); + return Collections.unmodifiableList(units); + } + + @Override + public Temporal addTo(Temporal temporal) { + if (seconds != 0) { + temporal = temporal.plus(seconds, ChronoUnit.SECONDS); + } + if (nanos != 0) { + temporal = temporal.plus(nanos, ChronoUnit.NANOS); + } + return temporal; + } + + @Override + public Temporal subtractFrom(Temporal temporal) { + if (seconds != 0) { + temporal = temporal.minus(seconds, ChronoUnit.SECONDS); + } + if (nanos != 0) { + temporal = temporal.minus(nanos, ChronoUnit.NANOS); + } + return temporal; + } + + /// Whether this is zero long. + public boolean isZero() { + return (seconds | nanos) == 0; + } + + /// Whether this is shorter than zero. + public boolean isNegative() { + return seconds < 0; + } + + /// The whole seconds, rounded towards negative infinity. public long getSeconds() { return seconds; } + /// The nanoseconds within the second, 0 to 999,999,999. public int getNano() { return nanos; } + /// This duration with `seconds` whole seconds. + public Duration withSeconds(long seconds) { + return create(seconds, nanos); + } + + /// This duration with `nanoOfSecond` nanoseconds within the second. + public Duration withNanos(int nanoOfSecond) { + if (nanoOfSecond < 0 || nanoOfSecond >= NANOS_PER_SECOND) { + throw new DateTimeException("Invalid value for NanoOfSecond: " + nanoOfSecond); + } + return create(seconds, nanoOfSecond); + } + + public Duration plus(Duration duration) { + return plus(duration.getSeconds(), duration.getNano()); + } + + /// This duration plus `amountToAdd` of `unit`, which must have an exact + /// length. + public Duration plus(long amountToAdd, TemporalUnit unit) { + if (unit == null) { + throw new NullPointerException("unit"); + } + if (unit == ChronoUnit.DAYS) { + return plus(TimeArithmetic.multiplyExact(amountToAdd, 86400L), 0); + } + if (unit.isDurationEstimated()) { + throw new java.time.temporal.UnsupportedTemporalTypeException( + "Unit must not have an estimated duration"); + } + if (amountToAdd == 0) { + return this; + } + if (unit instanceof ChronoUnit) { + ChronoUnit u = (ChronoUnit) unit; + if (u == ChronoUnit.NANOS) { + return plusNanos(amountToAdd); + } + if (u == ChronoUnit.MICROS) { + return plusSeconds((amountToAdd / (1000000L * 1000)) * 1000) + .plusNanos((amountToAdd % (1000000L * 1000)) * 1000); + } + if (u == ChronoUnit.MILLIS) { + return plusMillis(amountToAdd); + } + if (u == ChronoUnit.SECONDS) { + return plusSeconds(amountToAdd); + } + return plusSeconds(TimeArithmetic.multiplyExact(TimeArithmetic.seconds(u), amountToAdd)); + } + Duration duration = unit.getDuration().multipliedBy(amountToAdd); + return plusSeconds(duration.getSeconds()).plusNanos(duration.getNano()); + } + + public Duration plusDays(long daysToAdd) { + return plus(TimeArithmetic.multiplyExact(daysToAdd, 86400L), 0); + } + + public Duration plusHours(long hoursToAdd) { + return plus(TimeArithmetic.multiplyExact(hoursToAdd, 3600L), 0); + } + + public Duration plusMinutes(long minutesToAdd) { + return plus(TimeArithmetic.multiplyExact(minutesToAdd, 60L), 0); + } + + public Duration plusSeconds(long secondsToAdd) { + return plus(secondsToAdd, 0); + } + + public Duration plusMillis(long millisToAdd) { + return plus(millisToAdd / 1000, (millisToAdd % 1000) * 1000000); + } + + public Duration plusNanos(long nanosToAdd) { + return plus(0, nanosToAdd); + } + + private Duration plus(long secondsToAdd, long nanosToAdd) { + if ((secondsToAdd | nanosToAdd) == 0) { + return this; + } + long epochSec = TimeArithmetic.addExact(seconds, secondsToAdd); + epochSec = TimeArithmetic.addExact(epochSec, nanosToAdd / NANOS_PER_SECOND); + nanosToAdd = nanosToAdd % NANOS_PER_SECOND; + long nanoAdjustment = nanos + nanosToAdd; + return ofSeconds(epochSec, nanoAdjustment); + } + + public Duration minus(Duration duration) { + long secsToSubtract = duration.getSeconds(); + int nanosToSubtract = duration.getNano(); + if (secsToSubtract == Long.MIN_VALUE) { + return plus(Long.MAX_VALUE, -nanosToSubtract).plus(1, 0); + } + return plus(-secsToSubtract, -nanosToSubtract); + } + + public Duration minus(long amountToSubtract, TemporalUnit unit) { + return amountToSubtract == Long.MIN_VALUE ? plus(Long.MAX_VALUE, unit).plus(1, unit) + : plus(-amountToSubtract, unit); + } + + public Duration minusDays(long daysToSubtract) { + return daysToSubtract == Long.MIN_VALUE ? plusDays(Long.MAX_VALUE).plusDays(1) : plusDays(-daysToSubtract); + } + + public Duration minusHours(long hoursToSubtract) { + return hoursToSubtract == Long.MIN_VALUE ? plusHours(Long.MAX_VALUE).plusHours(1) + : plusHours(-hoursToSubtract); + } + + public Duration minusMinutes(long minutesToSubtract) { + return minutesToSubtract == Long.MIN_VALUE ? plusMinutes(Long.MAX_VALUE).plusMinutes(1) + : plusMinutes(-minutesToSubtract); + } + + public Duration minusSeconds(long secondsToSubtract) { + return secondsToSubtract == Long.MIN_VALUE ? plusSeconds(Long.MAX_VALUE).plusSeconds(1) + : plusSeconds(-secondsToSubtract); + } + + public Duration minusMillis(long millisToSubtract) { + return millisToSubtract == Long.MIN_VALUE ? plusMillis(Long.MAX_VALUE).plusMillis(1) + : plusMillis(-millisToSubtract); + } + + public Duration minusNanos(long nanosToSubtract) { + return nanosToSubtract == Long.MIN_VALUE ? plusNanos(Long.MAX_VALUE).plusNanos(1) + : plusNanos(-nanosToSubtract); + } + + /// This duration multiplied by `multiplicand`, or `ArithmeticException` + /// when the product is longer than a duration holds. + public Duration multipliedBy(long multiplicand) { + if (multiplicand == 1) { + return this; + } + // A negative duration borrows a second first, so its seconds and + // nanoseconds share a sign and neither partial product overflows while + // the whole fits; the factor is split at a billion so the nanosecond + // product fits a long. + long secs = seconds; + long nos = nanos; + if (secs < 0 && nos > 0) { + secs++; + nos -= NANOS_PER_SECOND; + } + long high = TimeArithmetic.floorDiv(multiplicand, NANOS_PER_SECOND); + long rest = nos * TimeArithmetic.floorMod(multiplicand, NANOS_PER_SECOND); + // The nanosecond terms are summed first: together they are the carry + // of nanos * factor, which is never larger than the factor, while the + // first alone can push a result that fits past the range. + long carry = TimeArithmetic.addExact(nos * high, TimeArithmetic.floorDiv(rest, NANOS_PER_SECOND)); + long total = TimeArithmetic.addExact(TimeArithmetic.multiplyExact(secs, multiplicand), carry); + return ofSeconds(total, TimeArithmetic.floorMod(rest, NANOS_PER_SECOND)); + } + + /// This duration divided by `divisor`, the fraction of a nanosecond + /// dropped towards zero. Exact for anything shorter than 292 years; a + /// longer one throws `ArithmeticException`. + public Duration dividedBy(long divisor) { + if (divisor == 0) { + throw new ArithmeticException("Cannot divide by zero"); + } + if (divisor == 1) { + return this; + } + return ofNanos(toNanos() / divisor); + } + + /// This duration with its sign reversed. The most negative duration has + /// no positive counterpart and throws `ArithmeticException`. + public Duration negated() { + return multipliedBy(-1); + } + + /// This duration without its sign. + public Duration abs() { + return isNegative() ? negated() : this; + } + + /// The whole days, of 24 hours. + public long toDays() { + return seconds / 86400L; + } + + /// The whole hours. + public long toHours() { + return seconds / 3600L; + } + + /// The whole minutes. + public long toMinutes() { + return seconds / 60L; + } + + /// The whole seconds; the same as `getSeconds()`. + public long toSeconds() { + return seconds; + } + + /// The whole milliseconds, or `ArithmeticException` past what a long + /// holds. public long toMillis() { - return seconds * 1000L + nanos / 1000000L; + long tempSeconds = seconds; + long tempNanos = nanos; + if (tempSeconds < 0) { + // A negative duration borrows a second first, so the most negative + // value a long holds is reached without an intermediate overflow. + tempSeconds = tempSeconds + 1; + tempNanos = tempNanos - NANOS_PER_SECOND; + } + long millis = TimeArithmetic.multiplyExact(tempSeconds, 1000L); + return TimeArithmetic.addExact(millis, tempNanos / 1000000L); + } + + /// The whole nanoseconds, or `ArithmeticException` past what a long + /// holds (about 292 years). + public long toNanos() { + long tempSeconds = seconds; + long tempNanos = nanos; + if (tempSeconds < 0) { + tempSeconds = tempSeconds + 1; + tempNanos = tempNanos - NANOS_PER_SECOND; + } + long totalNanos = TimeArithmetic.multiplyExact(tempSeconds, NANOS_PER_SECOND); + return TimeArithmetic.addExact(totalNanos, tempNanos); + } + + /// The whole days; the same as `toDays()`. + public long toDaysPart() { + return seconds / 86400L; + } + + /// The hours left over after the whole days. + public int toHoursPart() { + return (int) (toHours() % 24); + } + + /// The minutes left over after the whole hours. + public int toMinutesPart() { + return (int) (toMinutes() % 60); + } + + /// The seconds left over after the whole minutes. + public int toSecondsPart() { + return (int) (seconds % 60); } - public Duration plus(Duration other) { - return ofSeconds(seconds + other.seconds, nanos + other.nanos); + /// The milliseconds within the second. + public int toMillisPart() { + return nanos / 1000000; } - public Duration minus(Duration other) { - return ofSeconds(seconds - other.seconds, nanos - other.nanos); + /// The nanoseconds within the second; the same as `getNano()`. + public int toNanosPart() { + return nanos; } + @Override public int compareTo(Duration other) { if (seconds != other.seconds) { return seconds < other.seconds ? -1 : 1; } - return nanos < other.nanos ? -1 : nanos > other.nanos ? 1 : 0; + return nanos - other.nanos; } + @Override public boolean equals(Object obj) { - return obj instanceof Duration && compareTo((Duration) obj) == 0; + if (this == obj) { + return true; + } + if (!(obj instanceof Duration)) { + return false; + } + Duration other = (Duration) obj; + return seconds == other.seconds && nanos == other.nanos; } + @Override public int hashCode() { - return (int) (seconds ^ (seconds >>> 32)) + nanos * 31; + return ((int) (seconds ^ (seconds >>> 32))) + (51 * nanos); } public String toString() { diff --git a/Ports/CLDC11/src/java/time/Instant.java b/Ports/CLDC11/src/java/time/Instant.java index 8ac4e01fdb2..1a58de06765 100644 --- a/Ports/CLDC11/src/java/time/Instant.java +++ b/Ports/CLDC11/src/java/time/Instant.java @@ -1,9 +1,49 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ package java.time; import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.time.temporal.Temporal; import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalAmount; +import java.time.temporal.TemporalUnit; + +/// A moment on the time line, in seconds and nanoseconds from +/// 1970-01-01T00:00:00Z. +public final class Instant implements Temporal, Comparable { + private static final long MIN_SECOND = -31557014167219200L; + private static final long MAX_SECOND = 31556889864403199L; + + /// 1970-01-01T00:00:00Z. + public static final Instant EPOCH = new Instant(0L, 0); + + /// The earliest instant, -1000000000-01-01T00:00Z. + public static final Instant MIN = new Instant(MIN_SECOND, 0); + + /// The latest instant, 1000000000-12-31T23:59:59.999999999Z. + public static final Instant MAX = new Instant(MAX_SECOND, 999999999); -public final class Instant implements Comparable, TemporalAccessor { private final long epochSecond; private final int nano; @@ -12,28 +52,67 @@ private Instant(long epochSecond, int nano) { this.nano = nano; } + private static Instant create(long seconds, int nanoOfSecond) { + if ((seconds | nanoOfSecond) == 0) { + return EPOCH; + } + if (seconds < MIN_SECOND || seconds > MAX_SECOND) { + throw TimeArithmetic.instantOutOfRange(); + } + return new Instant(seconds, nanoOfSecond); + } + public static Instant now() { return ofEpochMilli(System.currentTimeMillis()); } + /// The moment now on `clock`. + public static Instant now(Clock clock) { + return clock.instant(); + } + public static Instant ofEpochMilli(long epochMilli) { - long secs = DateTimeSupport.floorDiv(epochMilli, 1000L); - int nanos = (int) (DateTimeSupport.floorMod(epochMilli, 1000L) * 1000000L); - return new Instant(secs, nanos); + long secs = TimeArithmetic.floorDiv(epochMilli, 1000L); + int mos = (int) TimeArithmetic.floorMod(epochMilli, 1000L); + return create(secs, mos * 1000000); } public static Instant ofEpochSecond(long epochSecond) { - return new Instant(epochSecond, 0); + return create(epochSecond, 0); } public static Instant ofEpochSecond(long epochSecond, long nanoAdjustment) { - long secs = epochSecond + DateTimeSupport.floorDiv(nanoAdjustment, DateTimeSupport.NANOS_PER_SECOND); - int nanos = (int) DateTimeSupport.floorMod(nanoAdjustment, DateTimeSupport.NANOS_PER_SECOND); - return new Instant(secs, nanos); + long secs = TimeArithmetic.addExact(epochSecond, + TimeArithmetic.floorDiv(nanoAdjustment, TimeArithmetic.NANOS_PER_SECOND)); + int nos = (int) TimeArithmetic.floorMod(nanoAdjustment, TimeArithmetic.NANOS_PER_SECOND); + return create(secs, nos); + } + + /// The moment `temporal` is. + public static Instant from(TemporalAccessor temporal) { + if (temporal instanceof Instant) { + return (Instant) temporal; + } + if (temporal instanceof ZonedDateTime) { + return ((ZonedDateTime) temporal).toInstant(); + } + if (temporal instanceof OffsetDateTime) { + return ((OffsetDateTime) temporal).toInstant(); + } + if (temporal == null) { + throw new NullPointerException("temporal"); + } + throw new DateTimeException("Unable to obtain Instant from TemporalAccessor: " + temporal + + " of type " + temporal.getClass().getName()); } public static Instant parse(CharSequence text) { - return (Instant) DateTimeFormatter.ISO_INSTANT.parse(text); + TemporalAccessor parsed = DateTimeFormatter.ISO_INSTANT.parse(text); + try { + return from(parsed); + } catch (DateTimeException ex) { + throw TimeArithmetic.unparsable(text, ex); + } } public long getEpochSecond() { @@ -44,50 +123,200 @@ public int getNano() { return nano; } + /// The milliseconds since the epoch, or `ArithmeticException` past what + /// a long holds. public long toEpochMilli() { - return epochSecond * 1000L + nano / 1000000L; + if (epochSecond < 0 && nano > 0) { + long millis = TimeArithmetic.multiplyExact(epochSecond + 1, 1000L); + long adjustment = nano / 1000000 - 1000; + return TimeArithmetic.addExact(millis, adjustment); + } + long millis = TimeArithmetic.multiplyExact(epochSecond, 1000L); + return TimeArithmetic.addExact(millis, nano / 1000000); + } + + @Override + public boolean isSupported(TemporalUnit unit) { + if (unit instanceof ChronoUnit) { + return unit.isTimeBased() || unit == ChronoUnit.DAYS; + } + return unit != null && unit.isSupportedBy(this); + } + + @Override + public Instant plus(long amountToAdd, TemporalUnit unit) { + if (unit instanceof ChronoUnit) { + ChronoUnit u = (ChronoUnit) unit; + if (u == ChronoUnit.NANOS) { + return plusNanos(amountToAdd); + } + if (u == ChronoUnit.MICROS) { + return plus(amountToAdd / 1000000, (amountToAdd % 1000000) * 1000); + } + if (u == ChronoUnit.MILLIS) { + return plusMillis(amountToAdd); + } + if (u == ChronoUnit.SECONDS) { + return plusSeconds(amountToAdd); + } + if (u == ChronoUnit.MINUTES || u == ChronoUnit.HOURS || u == ChronoUnit.HALF_DAYS + || u == ChronoUnit.DAYS) { + return plusSeconds(TimeArithmetic.multiplyExact(amountToAdd, TimeArithmetic.seconds(u))); + } + throw TimeArithmetic.unsupported(unit); + } + if (unit == null) { + throw new NullPointerException("unit"); + } + return unit.addTo(this, amountToAdd); + } + + @Override + public Instant minus(long amountToSubtract, TemporalUnit unit) { + return amountToSubtract == Long.MIN_VALUE ? plus(Long.MAX_VALUE, unit).plus(1, unit) + : plus(-amountToSubtract, unit); + } + + @Override + public Instant plus(TemporalAmount amount) { + return (Instant) amount.addTo(this); + } + + @Override + public Instant minus(TemporalAmount amount) { + return (Instant) amount.subtractFrom(this); } public Instant plusSeconds(long secondsToAdd) { - return ofEpochSecond(epochSecond + secondsToAdd, nano); + return plus(secondsToAdd, 0); } public Instant plusMillis(long millisToAdd) { - return ofEpochMilli(toEpochMilli() + millisToAdd); + return plus(millisToAdd / 1000, (millisToAdd % 1000) * 1000000); + } + + public Instant plusNanos(long nanosToAdd) { + return plus(0, nanosToAdd); + } + + private Instant plus(long secondsToAdd, long nanosToAdd) { + if ((secondsToAdd | nanosToAdd) == 0) { + return this; + } + long epochSec = TimeArithmetic.addExact(epochSecond, secondsToAdd); + epochSec = TimeArithmetic.addExact(epochSec, nanosToAdd / TimeArithmetic.NANOS_PER_SECOND); + nanosToAdd = nanosToAdd % TimeArithmetic.NANOS_PER_SECOND; + long nanoAdjustment = nano + nanosToAdd; + return ofEpochSecond(epochSec, nanoAdjustment); } public Instant minusSeconds(long secondsToSubtract) { + if (secondsToSubtract == Long.MIN_VALUE) { + return plusSeconds(Long.MAX_VALUE).plusSeconds(1); + } return plusSeconds(-secondsToSubtract); } public Instant minusMillis(long millisToSubtract) { + if (millisToSubtract == Long.MIN_VALUE) { + return plusMillis(Long.MAX_VALUE).plusMillis(1); + } return plusMillis(-millisToSubtract); } + public Instant minusNanos(long nanosToSubtract) { + if (nanosToSubtract == Long.MIN_VALUE) { + return plusNanos(Long.MAX_VALUE).plusNanos(1); + } + return plusNanos(-nanosToSubtract); + } + + /// This moment with everything smaller than `unit` dropped. The unit has + /// to divide a day. + public Instant truncatedTo(TemporalUnit unit) { + if (unit == ChronoUnit.NANOS) { + return this; + } + long dur = TimeArithmetic.truncationNanos(unit); + long nod = (epochSecond % TimeArithmetic.SECONDS_PER_DAY) * TimeArithmetic.NANOS_PER_SECOND + nano; + long result = TimeArithmetic.floorDiv(nod, dur) * dur; + return plusNanos(result - nod); + } + + /// The whole units of `unit` from this moment to `endExclusive`. + @Override + public long until(Temporal endExclusive, TemporalUnit unit) { + Instant end = from(endExclusive); + if (unit instanceof ChronoUnit) { + ChronoUnit u = (ChronoUnit) unit; + if (u.compareTo(ChronoUnit.DAYS) > 0) { + throw TimeArithmetic.unsupported(unit); + } + if (u == ChronoUnit.NANOS || u == ChronoUnit.MICROS) { + // Counted in nanoseconds first, as the JDK does: more than + // 292 years apart is an overflow in either unit. + long secsDiff = TimeArithmetic.subtractExact(end.epochSecond, epochSecond); + long nanos = TimeArithmetic.addExact(TimeArithmetic.multiplyExact(secsDiff, + TimeArithmetic.NANOS_PER_SECOND), end.nano - nano); + return u == ChronoUnit.NANOS ? nanos : nanos / 1000; + } + if (u == ChronoUnit.MILLIS) { + // Between the two moments each cut to its millisecond. + return TimeArithmetic.subtractExact(end.toEpochMilli(), toEpochMilli()); + } + return TimeArithmetic.timeUntil(epochSecond, nano, end.epochSecond, end.nano, u); + } + if (unit == null) { + throw new NullPointerException("unit"); + } + return unit.between(this, end); + } + + /// This moment in `zone`. + public ZonedDateTime atZone(ZoneId zone) { + return ZonedDateTime.ofInstant(this, zone); + } + + /// This moment at `offset`. + public OffsetDateTime atOffset(ZoneOffset offset) { + return OffsetDateTime.ofInstant(this, offset); + } + + public boolean isAfter(Instant otherInstant) { + return compareTo(otherInstant) > 0; + } + + public boolean isBefore(Instant otherInstant) { + return compareTo(otherInstant) < 0; + } + + @Override public int compareTo(Instant other) { if (epochSecond != other.epochSecond) { return epochSecond < other.epochSecond ? -1 : 1; } - return nano < other.nano ? -1 : nano > other.nano ? 1 : 0; + return nano - other.nano; } + @Override public boolean equals(Object obj) { - return obj instanceof Instant && compareTo((Instant) obj) == 0; + if (this == obj) { + return true; + } + if (!(obj instanceof Instant)) { + return false; + } + Instant other = (Instant) obj; + return epochSecond == other.epochSecond && nano == other.nano; } + @Override public int hashCode() { - return (int) (epochSecond ^ (epochSecond >>> 32)) + nano * 51; + return ((int) (epochSecond ^ (epochSecond >>> 32))) + 51 * nano; } + @Override public String toString() { return DateTimeFormatter.ISO_INSTANT.format(this); } - - Instant toInstant() { - return this; - } - - ZoneId getZoneForFormatting() { - return ZoneOffset.UTC; - } } diff --git a/Ports/CLDC11/src/java/time/LocalDate.java b/Ports/CLDC11/src/java/time/LocalDate.java index 35440aaaa50..daffa6894c9 100644 --- a/Ports/CLDC11/src/java/time/LocalDate.java +++ b/Ports/CLDC11/src/java/time/LocalDate.java @@ -1,43 +1,200 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ package java.time; +import java.time.chrono.ChronoLocalDate; import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.time.temporal.Temporal; import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalAmount; +import java.time.temporal.TemporalUnit; +import java.util.stream.Stream; + +/// A date of the ISO calendar with no time and no zone, such as 2007-12-03. +public final class LocalDate implements ChronoLocalDate { + /// -999999999-01-01, the earliest date. + public static final LocalDate MIN = new LocalDate(-999999999, 1, 1); + /// +999999999-12-31, the latest date. + public static final LocalDate MAX = new LocalDate(999999999, 12, 31); + /// 1970-01-01. + public static final LocalDate EPOCH = new LocalDate(1970, 1, 1); + + private static final int YEAR_LIMIT = 999999999; + private static final long DAYS_PER_CYCLE = 146097L; + private static final long DAYS_0000_TO_1970 = (DAYS_PER_CYCLE * 5L) - (30L * 365L + 7L); -public final class LocalDate implements Comparable, TemporalAccessor { private final int year; - private final int month; - private final int dayOfMonth; + private final short month; + private final short day; private LocalDate(int year, int month, int dayOfMonth) { - DateTimeSupport.checkDate(year, month, dayOfMonth); this.year = year; - this.month = month; - this.dayOfMonth = dayOfMonth; + this.month = (short) month; + this.day = (short) dayOfMonth; + } + + private static boolean leap(long year) { + return ((year & 3) == 0) && ((year % 100) != 0 || (year % 400) == 0); + } + + private static int monthLength(int year, int month) { + return Month.of(month).length(leap(year)); + } + + private static LocalDate create(int year, int month, int dayOfMonth) { + if (dayOfMonth > 28) { + int dom = monthLength(year, month); + if (dayOfMonth > dom) { + if (dayOfMonth == 29) { + throw new DateTimeException("Invalid date 'February 29' as '" + year + "' is not a leap year"); + } + throw new DateTimeException("Invalid date '" + Month.of(month).name() + " " + dayOfMonth + "'"); + } + } + return new LocalDate(year, month, dayOfMonth); + } + + /// The date, with the day brought back to the last one the month has. + private static LocalDate resolvePreviousValid(int year, int month, int day) { + int last = monthLength(year, month); + return new LocalDate(year, month, day > last ? last : day); + } + + private static int checkYear(long year) { + return (int) TimeArithmetic.check("Year", -YEAR_LIMIT, YEAR_LIMIT, year); } public static LocalDate now() { return now(Clock.systemDefaultZone()); } + /// The date today in `zone`. + public static LocalDate now(ZoneId zone) { + return now(Clock.system(zone)); + } + public static LocalDate now(Clock clock) { - return LocalDateTime.ofInstant(clock.instant(), clock.getZone()).toLocalDate(); + return ofInstant(clock.instant(), clock.getZone()); } public static LocalDate of(int year, int month, int dayOfMonth) { - return new LocalDate(year, month, dayOfMonth); + checkYear(year); + TimeArithmetic.check("MonthOfYear", 1, 12, month); + TimeArithmetic.check("DayOfMonth", 1, 31, dayOfMonth); + return create(year, month, dayOfMonth); + } + + public static LocalDate of(int year, Month month, int dayOfMonth) { + if (month == null) { + throw new NullPointerException("month"); + } + return of(year, month.getValue(), dayOfMonth); + } + + /// The `dayOfYear`th day of `year`. + public static LocalDate ofYearDay(int year, int dayOfYear) { + checkYear(year); + TimeArithmetic.check("DayOfYear", 1, 366, dayOfYear); + boolean leap = leap(year); + if (dayOfYear == 366 && !leap) { + throw new DateTimeException("Invalid date 'DayOfYear 366' as '" + year + "' is not a leap year"); + } + Month moy = Month.of((dayOfYear - 1) / 31 + 1); + int monthEnd = moy.firstDayOfYear(leap) + moy.length(leap) - 1; + if (dayOfYear > monthEnd) { + moy = moy.plus(1); + } + int dom = dayOfYear - moy.firstDayOfYear(leap) + 1; + return new LocalDate(year, moy.getValue(), dom); + } + + /// The date `instant` falls on in `zone`. + public static LocalDate ofInstant(Instant instant, ZoneId zone) { + return LocalDateTime.ofInstant(instant, zone).toLocalDate(); } public static LocalDate ofEpochDay(long epochDay) { - int[] parts = DateTimeSupport.epochDayToDate(epochDay); - return of(parts[0], parts[1], parts[2]); + TimeArithmetic.check("EpochDay", -365243219162L, 365241780471L, epochDay); + long zeroDay = epochDay + DAYS_0000_TO_1970; + // find the march-based year + zeroDay -= 60; + long adjust = 0; + if (zeroDay < 0) { + long adjustCycles = (zeroDay + 1) / DAYS_PER_CYCLE - 1; + adjust = adjustCycles * 400; + zeroDay += -adjustCycles * DAYS_PER_CYCLE; + } + long yearEst = (400 * zeroDay + 591) / DAYS_PER_CYCLE; + long doyEst = zeroDay - (365 * yearEst + yearEst / 4 - yearEst / 100 + yearEst / 400); + if (doyEst < 0) { + yearEst--; + doyEst = zeroDay - (365 * yearEst + yearEst / 4 - yearEst / 100 + yearEst / 400); + } + yearEst += adjust; + int marchDoy0 = (int) doyEst; + int marchMonth0 = (marchDoy0 * 5 + 2) / 153; + int month = (marchMonth0 + 2) % 12 + 1; + int dom = marchDoy0 - (marchMonth0 * 306 + 5) / 10 + 1; + yearEst += marchMonth0 / 10; + return new LocalDate(checkYear(yearEst), month, dom); + } + + /// The date of `temporal`. + public static LocalDate from(TemporalAccessor temporal) { + if (temporal instanceof LocalDate) { + return (LocalDate) temporal; + } + if (temporal instanceof LocalDateTime) { + return ((LocalDateTime) temporal).toLocalDate(); + } + if (temporal instanceof ZonedDateTime) { + return ((ZonedDateTime) temporal).toLocalDate(); + } + if (temporal instanceof OffsetDateTime) { + return ((OffsetDateTime) temporal).toLocalDate(); + } + if (temporal == null) { + throw new NullPointerException("temporal"); + } + throw new DateTimeException("Unable to obtain LocalDate from TemporalAccessor: " + temporal + + " of type " + temporal.getClass().getName()); } public static LocalDate parse(CharSequence text) { - return (LocalDate) DateTimeFormatter.ISO_LOCAL_DATE.parse(text); + return parse(text, DateTimeFormatter.ISO_LOCAL_DATE); } public static LocalDate parse(CharSequence text, DateTimeFormatter formatter) { - return (LocalDate) formatter.parse(text); + if (formatter == null) { + throw new NullPointerException("formatter"); + } + TemporalAccessor parsed = formatter.parse(text); + try { + return from(parsed); + } catch (DateTimeException ex) { + throw TimeArithmetic.unparsable(text, ex); + } } public int getYear() { @@ -48,43 +205,285 @@ public int getMonthValue() { return month; } + /// The month as a `Month`. + public Month getMonth() { + return Month.of(month); + } + public int getDayOfMonth() { - return dayOfMonth; + return day; + } + + /// The day of the year, 1 for January 1st. + public int getDayOfYear() { + return getMonth().firstDayOfYear(isLeapYear()) + day - 1; + } + + /// The day of the week. + public DayOfWeek getDayOfWeek() { + int dow0 = (int) TimeArithmetic.floorMod(toEpochDay() + 3, 7); + return DayOfWeek.of(dow0 + 1); } + @Override public boolean isLeapYear() { - return DateTimeSupport.isLeapYear(year); + return leap(year); } + @Override public int lengthOfMonth() { - return DateTimeSupport.lengthOfMonth(year, month); + return monthLength(year, month); } + @Override + public int lengthOfYear() { + return isLeapYear() ? 366 : 365; + } + + @Override public long toEpochDay() { - return DateTimeSupport.toEpochDay(year, month, dayOfMonth); + long y = year; + long m = month; + long total = 0; + total += 365 * y; + if (y >= 0) { + total += (y + 3) / 4 - (y + 99) / 100 + (y + 399) / 400; + } else { + total -= y / -4 - y / -100 + y / -400; + } + total += ((367 * m - 362) / 12); + total += day - 1; + if (m > 2) { + total--; + if (!isLeapYear()) { + total--; + } + } + return total - DAYS_0000_TO_1970; + } + + private long getProlepticMonth() { + return (year * 12L + month - 1); + } + + public LocalDate withYear(int year) { + if (this.year == year) { + return this; + } + checkYear(year); + return resolvePreviousValid(year, month, day); + } + + public LocalDate withMonth(int month) { + if (this.month == month) { + return this; + } + TimeArithmetic.check("MonthOfYear", 1, 12, month); + return resolvePreviousValid(year, month, day); + } + + public LocalDate withDayOfMonth(int dayOfMonth) { + if (this.day == dayOfMonth) { + return this; + } + return of(year, month, dayOfMonth); + } + + public LocalDate withDayOfYear(int dayOfYear) { + if (getDayOfYear() == dayOfYear) { + return this; + } + return ofYearDay(year, dayOfYear); + } + + @Override + public boolean isSupported(TemporalUnit unit) { + if (unit instanceof ChronoUnit) { + return unit.isDateBased(); + } + return unit != null && unit.isSupportedBy(this); + } + + @Override + public LocalDate plus(long amountToAdd, TemporalUnit unit) { + if (unit instanceof ChronoUnit) { + ChronoUnit u = (ChronoUnit) unit; + if (u == ChronoUnit.DAYS) { + return plusDays(amountToAdd); + } + if (u == ChronoUnit.WEEKS) { + return plusWeeks(amountToAdd); + } + if (u == ChronoUnit.MONTHS) { + return plusMonths(amountToAdd); + } + if (u == ChronoUnit.YEARS) { + return plusYears(amountToAdd); + } + if (u == ChronoUnit.DECADES) { + return plusYears(TimeArithmetic.multiplyExact(amountToAdd, 10)); + } + if (u == ChronoUnit.CENTURIES) { + return plusYears(TimeArithmetic.multiplyExact(amountToAdd, 100)); + } + if (u == ChronoUnit.MILLENNIA) { + return plusYears(TimeArithmetic.multiplyExact(amountToAdd, 1000)); + } + if (u == ChronoUnit.ERAS) { + // The two eras of the ISO calendar: year 1 and later, and before. + long era = TimeArithmetic.addExact(year >= 1 ? 1 : 0, amountToAdd); + TimeArithmetic.check("Era", 0, 1, era); + return amountToAdd == 0 ? this : resolvePreviousValid(1 - year, month, day); + } + throw TimeArithmetic.unsupported(unit); + } + if (unit == null) { + throw new NullPointerException("unit"); + } + return unit.addTo(this, amountToAdd); + } + + @Override + public LocalDate minus(long amountToSubtract, TemporalUnit unit) { + return amountToSubtract == Long.MIN_VALUE ? plus(Long.MAX_VALUE, unit).plus(1, unit) + : plus(-amountToSubtract, unit); + } + + @Override + public LocalDate plus(TemporalAmount amount) { + return (LocalDate) amount.addTo(this); + } + + @Override + public LocalDate minus(TemporalAmount amount) { + return (LocalDate) amount.subtractFrom(this); } public LocalDate plusDays(long daysToAdd) { - return ofEpochDay(toEpochDay() + daysToAdd); + if (daysToAdd == 0) { + return this; + } + return ofEpochDay(TimeArithmetic.addExact(toEpochDay(), daysToAdd)); + } + + public LocalDate plusWeeks(long weeksToAdd) { + return plusDays(TimeArithmetic.multiplyExact(weeksToAdd, 7)); } public LocalDate plusMonths(long monthsToAdd) { + if (monthsToAdd == 0) { + return this; + } long monthCount = year * 12L + (month - 1); long calcMonths = monthCount + monthsToAdd; - int newYear = (int) DateTimeSupport.floorDiv(calcMonths, 12); - int newMonth = (int) DateTimeSupport.floorMod(calcMonths, 12) + 1; - int newDay = Math.min(dayOfMonth, DateTimeSupport.lengthOfMonth(newYear, newMonth)); - return of(newYear, newMonth, newDay); + int newYear = checkYear(TimeArithmetic.floorDiv(calcMonths, 12)); + int newMonth = (int) TimeArithmetic.floorMod(calcMonths, 12) + 1; + return resolvePreviousValid(newYear, newMonth, day); } public LocalDate plusYears(long yearsToAdd) { - int newYear = (int) (year + yearsToAdd); - int newDay = Math.min(dayOfMonth, DateTimeSupport.lengthOfMonth(newYear, month)); - return of(newYear, month, newDay); + if (yearsToAdd == 0) { + return this; + } + int newYear = checkYear(year + yearsToAdd); + return resolvePreviousValid(newYear, month, day); } public LocalDate minusDays(long daysToSubtract) { - return plusDays(-daysToSubtract); + return daysToSubtract == Long.MIN_VALUE ? plusDays(Long.MAX_VALUE).plusDays(1) : plusDays(-daysToSubtract); + } + + public LocalDate minusWeeks(long weeksToSubtract) { + return weeksToSubtract == Long.MIN_VALUE ? plusWeeks(Long.MAX_VALUE).plusWeeks(1) + : plusWeeks(-weeksToSubtract); + } + + public LocalDate minusMonths(long monthsToSubtract) { + return monthsToSubtract == Long.MIN_VALUE ? plusMonths(Long.MAX_VALUE).plusMonths(1) + : plusMonths(-monthsToSubtract); + } + + public LocalDate minusYears(long yearsToSubtract) { + return yearsToSubtract == Long.MIN_VALUE ? plusYears(Long.MAX_VALUE).plusYears(1) + : plusYears(-yearsToSubtract); + } + + /// The whole units of `unit` from this date to the date of `endExclusive`. + @Override + public long until(Temporal endExclusive, TemporalUnit unit) { + LocalDate end = from(endExclusive); + if (unit instanceof ChronoUnit) { + ChronoUnit u = (ChronoUnit) unit; + if (u == ChronoUnit.DAYS) { + return end.toEpochDay() - toEpochDay(); + } + if (u == ChronoUnit.WEEKS) { + return (end.toEpochDay() - toEpochDay()) / 7; + } + if (u == ChronoUnit.MONTHS) { + return monthsUntil(end); + } + if (u == ChronoUnit.YEARS) { + return monthsUntil(end) / 12; + } + if (u == ChronoUnit.DECADES) { + return monthsUntil(end) / 120; + } + if (u == ChronoUnit.CENTURIES) { + return monthsUntil(end) / 1200; + } + if (u == ChronoUnit.MILLENNIA) { + return monthsUntil(end) / 12000; + } + if (u == ChronoUnit.ERAS) { + return (end.year >= 1 ? 1 : 0) - (year >= 1 ? 1 : 0); + } + throw TimeArithmetic.unsupported(unit); + } + if (unit == null) { + throw new NullPointerException("unit"); + } + return unit.between(this, end); + } + + long monthsUntil(LocalDate end) { + long packed1 = getProlepticMonth() * 32L + day; + long packed2 = end.getProlepticMonth() * 32L + end.day; + return (packed2 - packed1) / 32; + } + + /// The years, months and days from this date to `endDateExclusive`. + public Period until(ChronoLocalDate endDateExclusive) { + LocalDate end = from(endDateExclusive); + long totalMonths = end.getProlepticMonth() - getProlepticMonth(); + int days = end.day - day; + if (totalMonths > 0 && days < 0) { + totalMonths--; + LocalDate calcDate = plusMonths(totalMonths); + days = (int) (end.toEpochDay() - calcDate.toEpochDay()); + } else if (totalMonths < 0 && days > 0) { + totalMonths++; + days -= end.lengthOfMonth(); + } + long years = totalMonths / 12; + int months = (int) (totalMonths % 12); + return Period.of(TimeArithmetic.toIntExact(years), months, days); + } + + /// The dates from this one up to and not including `endExclusive`, a day + /// apart. + public Stream datesUntil(LocalDate endExclusive) { + long end = endExclusive.toEpochDay(); + long start = toEpochDay(); + if (end < start) { + throw new IllegalArgumentException(endExclusive + " < " + this); + } + // Made in full: the class library has no lazy stream source yet. + LocalDate[] all = new LocalDate[TimeArithmetic.toIntExact(end - start)]; + for (int i = 0; i < all.length; i++) { + all[i] = ofEpochDay(start + i); + } + return Stream.of(all); } public LocalDateTime atTime(int hour, int minute) { @@ -103,6 +502,11 @@ public LocalDateTime atTime(LocalTime time) { return LocalDateTime.of(this, time); } + /// Midnight at the start of this date. + public LocalDateTime atStartOfDay() { + return LocalDateTime.of(this, LocalTime.MIDNIGHT); + } + public ZonedDateTime atStartOfDay(ZoneId zone) { return ZonedDateTime.of(this, LocalTime.MIDNIGHT, zone); } @@ -111,25 +515,82 @@ public String format(DateTimeFormatter formatter) { return formatter.format(this); } - public int compareTo(LocalDate other) { - if (year != other.year) { - return year < other.year ? -1 : 1; + private int compareTo0(LocalDate otherDate) { + int cmp = year - otherDate.year; + if (cmp == 0) { + cmp = month - otherDate.month; + if (cmp == 0) { + cmp = day - otherDate.day; + } + } + return cmp; + } + + @Override + public int compareTo(ChronoLocalDate other) { + if (other instanceof LocalDate) { + return compareTo0((LocalDate) other); + } + long a = toEpochDay(); + long b = other.toEpochDay(); + return a < b ? -1 : a > b ? 1 : 0; + } + + @Override + public boolean isAfter(ChronoLocalDate other) { + if (other instanceof LocalDate) { + return compareTo0((LocalDate) other) > 0; } - if (month != other.month) { - return month < other.month ? -1 : 1; + return toEpochDay() > other.toEpochDay(); + } + + @Override + public boolean isBefore(ChronoLocalDate other) { + if (other instanceof LocalDate) { + return compareTo0((LocalDate) other) < 0; } - return dayOfMonth < other.dayOfMonth ? -1 : dayOfMonth > other.dayOfMonth ? 1 : 0; + return toEpochDay() < other.toEpochDay(); } + @Override + public boolean isEqual(ChronoLocalDate other) { + if (other instanceof LocalDate) { + return compareTo0((LocalDate) other) == 0; + } + return toEpochDay() == other.toEpochDay(); + } + + @Override public boolean equals(Object obj) { - return obj instanceof LocalDate && compareTo((LocalDate) obj) == 0; + if (this == obj) { + return true; + } + return obj instanceof LocalDate && compareTo0((LocalDate) obj) == 0; } + @Override public int hashCode() { - return year * 37 + month * 13 + dayOfMonth; + return (year & 0xFFFFF800) ^ ((year << 11) + (month << 6) + day); } + /// The JDK's text, `uuuu-MM-dd`, with a sign on a year past 9999. + @Override public String toString() { - return DateTimeFormatter.ISO_LOCAL_DATE.format(this); + StringBuilder buf = new StringBuilder(12); + int absYear = year < 0 ? -year : year; + if (absYear < 1000) { + if (year < 0) { + buf.append('-'); + } + String digits = Integer.toString(absYear + 10000); + buf.append(digits.substring(1)); + } else { + if (year > 9999) { + buf.append('+'); + } + buf.append(year); + } + return buf.append(month < 10 ? "-0" : "-").append((int) month).append(day < 10 ? "-0" : "-") + .append((int) day).toString(); } } diff --git a/Ports/CLDC11/src/java/time/LocalDateTime.java b/Ports/CLDC11/src/java/time/LocalDateTime.java index d19cfed30d0..423c0384185 100644 --- a/Ports/CLDC11/src/java/time/LocalDateTime.java +++ b/Ports/CLDC11/src/java/time/LocalDateTime.java @@ -1,9 +1,47 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ package java.time; +import java.time.chrono.ChronoLocalDateTime; import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.time.temporal.Temporal; import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalAmount; +import java.time.temporal.TemporalUnit; + +/// A date and a time of day with no zone, such as 2007-12-03T10:15:30. +public final class LocalDateTime implements ChronoLocalDateTime { + /// The earliest date and time. + public static final LocalDateTime MIN = new LocalDateTime(LocalDate.MIN, LocalTime.MIN); + /// The latest date and time. + public static final LocalDateTime MAX = new LocalDateTime(LocalDate.MAX, LocalTime.MAX); + + private static final long NANOS_PER_SECOND = TimeArithmetic.NANOS_PER_SECOND; + private static final long NANOS_PER_MINUTE = 60L * NANOS_PER_SECOND; + private static final long NANOS_PER_HOUR = 60L * NANOS_PER_MINUTE; + private static final long NANOS_PER_DAY = TimeArithmetic.NANOS_PER_DAY; -public final class LocalDateTime implements Comparable, TemporalAccessor { private final LocalDate date; private final LocalTime time; @@ -12,15 +50,33 @@ private LocalDateTime(LocalDate date, LocalTime time) { this.time = time; } + private LocalDateTime with(LocalDate newDate, LocalTime newTime) { + if (date == newDate && time == newTime) { + return this; + } + return new LocalDateTime(newDate, newTime); + } + public static LocalDateTime now() { return now(Clock.systemDefaultZone()); } + /// The date and time now in `zone`. + public static LocalDateTime now(ZoneId zone) { + return now(Clock.system(zone)); + } + public static LocalDateTime now(Clock clock) { return ofInstant(clock.instant(), clock.getZone()); } public static LocalDateTime of(LocalDate date, LocalTime time) { + if (date == null) { + throw new NullPointerException("date"); + } + if (time == null) { + throw new NullPointerException("time"); + } return new LocalDateTime(date, time); } @@ -36,22 +92,75 @@ public static LocalDateTime of(int year, int month, int dayOfMonth, int hour, in return of(LocalDate.of(year, month, dayOfMonth), LocalTime.of(hour, minute, second, nano)); } + public static LocalDateTime of(int year, Month month, int dayOfMonth, int hour, int minute) { + return of(LocalDate.of(year, month, dayOfMonth), LocalTime.of(hour, minute)); + } + + public static LocalDateTime of(int year, Month month, int dayOfMonth, int hour, int minute, int second) { + return of(LocalDate.of(year, month, dayOfMonth), LocalTime.of(hour, minute, second)); + } + + public static LocalDateTime of(int year, Month month, int dayOfMonth, int hour, int minute, int second, + int nano) { + return of(LocalDate.of(year, month, dayOfMonth), LocalTime.of(hour, minute, second, nano)); + } + public static LocalDateTime ofInstant(Instant instant, ZoneId zone) { return DateTimeSupport.localDateTimeFromInstant(instant, zone); } + /// The date and time `epochSecond` seconds and `nanoOfSecond` nanoseconds + /// after 1970-01-01T00:00 at `offset`. + public static LocalDateTime ofEpochSecond(long epochSecond, int nanoOfSecond, ZoneOffset offset) { + TimeArithmetic.check("NanoOfSecond", 0, 999999999, nanoOfSecond); + long localSecond = epochSecond + offset.getTotalSeconds(); + long localEpochDay = TimeArithmetic.floorDiv(localSecond, TimeArithmetic.SECONDS_PER_DAY); + int secsOfDay = (int) TimeArithmetic.floorMod(localSecond, TimeArithmetic.SECONDS_PER_DAY); + LocalDate date = LocalDate.ofEpochDay(localEpochDay); + LocalTime time = LocalTime.ofNanoOfDay(secsOfDay * NANOS_PER_SECOND + nanoOfSecond); + return new LocalDateTime(date, time); + } + + /// The date and time of `temporal`. + public static LocalDateTime from(TemporalAccessor temporal) { + if (temporal instanceof LocalDateTime) { + return (LocalDateTime) temporal; + } + if (temporal instanceof ZonedDateTime) { + return ((ZonedDateTime) temporal).toLocalDateTime(); + } + if (temporal instanceof OffsetDateTime) { + return ((OffsetDateTime) temporal).toLocalDateTime(); + } + if (temporal == null) { + throw new NullPointerException("temporal"); + } + throw new DateTimeException("Unable to obtain LocalDateTime from TemporalAccessor: " + temporal + + " of type " + temporal.getClass().getName()); + } + public static LocalDateTime parse(CharSequence text) { - return (LocalDateTime) DateTimeFormatter.ISO_LOCAL_DATE_TIME.parse(text); + return parse(text, DateTimeFormatter.ISO_LOCAL_DATE_TIME); } public static LocalDateTime parse(CharSequence text, DateTimeFormatter formatter) { - return (LocalDateTime) formatter.parse(text); + if (formatter == null) { + throw new NullPointerException("formatter"); + } + TemporalAccessor parsed = formatter.parse(text); + try { + return from(parsed); + } catch (DateTimeException ex) { + throw TimeArithmetic.unparsable(text, ex); + } } + @Override public LocalDate toLocalDate() { return date; } + @Override public LocalTime toLocalTime() { return time; } @@ -64,10 +173,22 @@ public int getMonthValue() { return date.getMonthValue(); } + public Month getMonth() { + return date.getMonth(); + } + public int getDayOfMonth() { return date.getDayOfMonth(); } + public int getDayOfYear() { + return date.getDayOfYear(); + } + + public DayOfWeek getDayOfWeek() { + return date.getDayOfWeek(); + } + public int getHour() { return time.getHour(); } @@ -84,58 +205,332 @@ public int getNano() { return time.getNano(); } + public LocalDateTime withYear(int year) { + return with(date.withYear(year), time); + } + + public LocalDateTime withMonth(int month) { + return with(date.withMonth(month), time); + } + + public LocalDateTime withDayOfMonth(int dayOfMonth) { + return with(date.withDayOfMonth(dayOfMonth), time); + } + + public LocalDateTime withDayOfYear(int dayOfYear) { + return with(date.withDayOfYear(dayOfYear), time); + } + + public LocalDateTime withHour(int hour) { + return with(date, time.withHour(hour)); + } + + public LocalDateTime withMinute(int minute) { + return with(date, time.withMinute(minute)); + } + + public LocalDateTime withSecond(int second) { + return with(date, time.withSecond(second)); + } + + public LocalDateTime withNano(int nanoOfSecond) { + return with(date, time.withNano(nanoOfSecond)); + } + + /// This value with everything smaller than `unit` set to zero. The unit + /// has to divide a day. + public LocalDateTime truncatedTo(TemporalUnit unit) { + return with(date, time.truncatedTo(unit)); + } + + @Override + public boolean isSupported(TemporalUnit unit) { + if (unit instanceof ChronoUnit) { + return unit != ChronoUnit.FOREVER; + } + return unit != null && unit.isSupportedBy(this); + } + + @Override + public LocalDateTime plus(long amountToAdd, TemporalUnit unit) { + if (unit instanceof ChronoUnit) { + ChronoUnit u = (ChronoUnit) unit; + if (u == ChronoUnit.NANOS) { + return plusNanos(amountToAdd); + } + if (u == ChronoUnit.MICROS) { + return plusDays(amountToAdd / 86400000000L).plusNanos((amountToAdd % 86400000000L) * 1000); + } + if (u == ChronoUnit.MILLIS) { + return plusDays(amountToAdd / 86400000L).plusNanos((amountToAdd % 86400000L) * 1000000); + } + if (u == ChronoUnit.SECONDS) { + return plusSeconds(amountToAdd); + } + if (u == ChronoUnit.MINUTES) { + return plusMinutes(amountToAdd); + } + if (u == ChronoUnit.HOURS) { + return plusHours(amountToAdd); + } + if (u == ChronoUnit.HALF_DAYS) { + // The JDK's own arithmetic, 256 half days to a step. + return plusDays(amountToAdd / 256).plusHours((amountToAdd % 256) * 12); + } + return with(date.plus(amountToAdd, unit), time); + } + if (unit == null) { + throw new NullPointerException("unit"); + } + return unit.addTo(this, amountToAdd); + } + + @Override + public LocalDateTime minus(long amountToSubtract, TemporalUnit unit) { + return amountToSubtract == Long.MIN_VALUE ? plus(Long.MAX_VALUE, unit).plus(1, unit) + : plus(-amountToSubtract, unit); + } + + @Override + public LocalDateTime plus(TemporalAmount amount) { + if (amount instanceof Period) { + Period periodToAdd = (Period) amount; + return with(date.plus(periodToAdd), time); + } + if (amount == null) { + throw new NullPointerException("amountToAdd"); + } + return (LocalDateTime) amount.addTo(this); + } + + @Override + public LocalDateTime minus(TemporalAmount amount) { + if (amount instanceof Period) { + Period periodToSubtract = (Period) amount; + return with(date.minus(periodToSubtract), time); + } + if (amount == null) { + throw new NullPointerException("amountToSubtract"); + } + return (LocalDateTime) amount.subtractFrom(this); + } + + public LocalDateTime plusYears(long years) { + return with(date.plusYears(years), time); + } + + public LocalDateTime plusMonths(long months) { + return with(date.plusMonths(months), time); + } + + public LocalDateTime plusWeeks(long weeks) { + return with(date.plusWeeks(weeks), time); + } + public LocalDateTime plusDays(long days) { - return of(date.plusDays(days), time); + return with(date.plusDays(days), time); } public LocalDateTime plusHours(long hours) { - long totalNanos = time.toNanoOfDay() + hours * 3600L * DateTimeSupport.NANOS_PER_SECOND; - long dayAdjust = DateTimeSupport.floorDiv(totalNanos, DateTimeSupport.NANOS_PER_DAY); - long nanoOfDay = DateTimeSupport.floorMod(totalNanos, DateTimeSupport.NANOS_PER_DAY); - return of(date.plusDays(dayAdjust), LocalTime.ofNanoOfDay(nanoOfDay)); + return plusWithOverflow(date, hours, 0, 0, 0, 1); } public LocalDateTime plusMinutes(long minutes) { - return plusHours(minutes / 60).plusSeconds((minutes % 60) * 60); + return plusWithOverflow(date, 0, minutes, 0, 0, 1); } public LocalDateTime plusSeconds(long seconds) { - long totalNanos = time.toNanoOfDay() + seconds * DateTimeSupport.NANOS_PER_SECOND; - long dayAdjust = DateTimeSupport.floorDiv(totalNanos, DateTimeSupport.NANOS_PER_DAY); - long nanoOfDay = DateTimeSupport.floorMod(totalNanos, DateTimeSupport.NANOS_PER_DAY); - return of(date.plusDays(dayAdjust), LocalTime.ofNanoOfDay(nanoOfDay)); + return plusWithOverflow(date, 0, 0, seconds, 0, 1); + } + + public LocalDateTime plusNanos(long nanos) { + return plusWithOverflow(date, 0, 0, 0, nanos, 1); + } + + public LocalDateTime minusYears(long years) { + return years == Long.MIN_VALUE ? plusYears(Long.MAX_VALUE).plusYears(1) : plusYears(-years); + } + + public LocalDateTime minusMonths(long months) { + return months == Long.MIN_VALUE ? plusMonths(Long.MAX_VALUE).plusMonths(1) : plusMonths(-months); + } + + public LocalDateTime minusWeeks(long weeks) { + return weeks == Long.MIN_VALUE ? plusWeeks(Long.MAX_VALUE).plusWeeks(1) : plusWeeks(-weeks); } - public Instant toInstant(ZoneOffset offset) { - return Instant.ofEpochSecond(DateTimeSupport.toEpochSecond(date, time, offset), time.getNano()); + public LocalDateTime minusDays(long days) { + return days == Long.MIN_VALUE ? plusDays(Long.MAX_VALUE).plusDays(1) : plusDays(-days); + } + + public LocalDateTime minusHours(long hours) { + return plusWithOverflow(date, hours, 0, 0, 0, -1); + } + + public LocalDateTime minusMinutes(long minutes) { + return plusWithOverflow(date, 0, minutes, 0, 0, -1); + } + + public LocalDateTime minusSeconds(long seconds) { + return plusWithOverflow(date, 0, 0, seconds, 0, -1); + } + + public LocalDateTime minusNanos(long nanos) { + return plusWithOverflow(date, 0, 0, 0, nanos, -1); + } + + /// The time added a unit at a time, each taken apart into whole days and + /// the rest first, so no step overflows while the result fits. + private LocalDateTime plusWithOverflow(LocalDate newDate, long hours, long minutes, long seconds, long nanos, + int sign) { + if ((hours | minutes | seconds | nanos) == 0) { + return with(newDate, time); + } + long totDays = nanos / NANOS_PER_DAY + seconds / 86400L + minutes / 1440L + hours / 24L; + totDays *= sign; + long totNanos = nanos % NANOS_PER_DAY + (seconds % 86400L) * NANOS_PER_SECOND + + (minutes % 1440L) * NANOS_PER_MINUTE + (hours % 24L) * NANOS_PER_HOUR; + long curNoD = time.toNanoOfDay(); + totNanos = totNanos * sign + curNoD; + totDays += TimeArithmetic.floorDiv(totNanos, NANOS_PER_DAY); + long newNoD = TimeArithmetic.floorMod(totNanos, NANOS_PER_DAY); + LocalTime newTime = newNoD == curNoD ? time : LocalTime.ofNanoOfDay(newNoD); + return with(newDate.plusDays(totDays), newTime); + } + + /// The whole units of `unit` from this value to `endExclusive`. + @Override + public long until(Temporal endExclusive, TemporalUnit unit) { + LocalDateTime end = from(endExclusive); + if (unit instanceof ChronoUnit) { + ChronoUnit u = (ChronoUnit) unit; + if (u.isTimeBased()) { + long amount = end.date.toEpochDay() - date.toEpochDay(); + if (amount == 0) { + return time.until(end.time, unit); + } + long timePart = end.time.toNanoOfDay() - time.toNanoOfDay(); + if (amount > 0) { + amount--; + timePart += NANOS_PER_DAY; + } else { + amount++; + timePart -= NANOS_PER_DAY; + } + if (u == ChronoUnit.NANOS) { + amount = TimeArithmetic.multiplyExact(amount, NANOS_PER_DAY); + } else if (u == ChronoUnit.MICROS) { + amount = TimeArithmetic.multiplyExact(amount, 86400000000L); + timePart = timePart / 1000; + } else if (u == ChronoUnit.MILLIS) { + amount = TimeArithmetic.multiplyExact(amount, 86400000L); + timePart = timePart / 1000000; + } else if (u == ChronoUnit.SECONDS) { + amount = TimeArithmetic.multiplyExact(amount, 86400L); + timePart = timePart / NANOS_PER_SECOND; + } else if (u == ChronoUnit.MINUTES) { + amount = TimeArithmetic.multiplyExact(amount, 1440L); + timePart = timePart / NANOS_PER_MINUTE; + } else if (u == ChronoUnit.HOURS) { + amount = TimeArithmetic.multiplyExact(amount, 24L); + timePart = timePart / NANOS_PER_HOUR; + } else { + amount = TimeArithmetic.multiplyExact(amount, 2L); + timePart = timePart / (NANOS_PER_HOUR * 12); + } + return TimeArithmetic.addExact(amount, timePart); + } + LocalDate endDate = end.date; + if (endDate.isAfter(date) && end.time.isBefore(time)) { + endDate = endDate.minusDays(1); + } else if (endDate.isBefore(date) && end.time.isAfter(time)) { + endDate = endDate.plusDays(1); + } + return date.until(endDate, unit); + } + if (unit == null) { + throw new NullPointerException("unit"); + } + return unit.between(this, end); + } + + /// This date and time in `zone`. + public ZonedDateTime atZone(ZoneId zone) { + return ZonedDateTime.of(this, zone); + } + + /// This date and time at `offset`. + public OffsetDateTime atOffset(ZoneOffset offset) { + return OffsetDateTime.of(this, offset); } public String format(DateTimeFormatter formatter) { return formatter.format(this); } - public int compareTo(LocalDateTime other) { + private int compareTo0(LocalDateTime other) { int cmp = date.compareTo(other.date); - return cmp != 0 ? cmp : time.compareTo(other.time); + if (cmp == 0) { + cmp = time.compareTo(other.time); + } + return cmp; } - public boolean equals(Object obj) { - return obj instanceof LocalDateTime && compareTo((LocalDateTime) obj) == 0; + @Override + public int compareTo(ChronoLocalDateTime other) { + if (other instanceof LocalDateTime) { + return compareTo0((LocalDateTime) other); + } + int cmp = toLocalDate().compareTo(other.toLocalDate()); + if (cmp == 0) { + cmp = toLocalTime().compareTo(other.toLocalTime()); + } + return cmp; } - public int hashCode() { - return date.hashCode() * 31 + time.hashCode(); + @Override + public boolean isAfter(ChronoLocalDateTime other) { + if (other instanceof LocalDateTime) { + return compareTo0((LocalDateTime) other) > 0; + } + return compareTo(other) > 0; } - public String toString() { - return DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(this); + @Override + public boolean isBefore(ChronoLocalDateTime other) { + if (other instanceof LocalDateTime) { + return compareTo0((LocalDateTime) other) < 0; + } + return compareTo(other) < 0; } - Instant toInstant() { - return toInstant(ZoneOffset.UTC); + @Override + public boolean isEqual(ChronoLocalDateTime other) { + if (other instanceof LocalDateTime) { + return compareTo0((LocalDateTime) other) == 0; + } + return compareTo(other) == 0; } - ZoneId getZoneForFormatting() { - return ZoneOffset.UTC; + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof LocalDateTime)) { + return false; + } + LocalDateTime other = (LocalDateTime) obj; + return date.equals(other.date) && time.equals(other.time); + } + + @Override + public int hashCode() { + return date.hashCode() ^ time.hashCode(); + } + + @Override + public String toString() { + return date.toString() + 'T' + time.toString(); } } diff --git a/Ports/CLDC11/src/java/time/LocalTime.java b/Ports/CLDC11/src/java/time/LocalTime.java index 51d76969676..41bd7496b5d 100644 --- a/Ports/CLDC11/src/java/time/LocalTime.java +++ b/Ports/CLDC11/src/java/time/LocalTime.java @@ -1,10 +1,49 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ package java.time; import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.time.temporal.Temporal; import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalAmount; +import java.time.temporal.TemporalUnit; -public final class LocalTime implements Comparable, TemporalAccessor { +/// A time of day with no date and no zone, such as 10:15:30. +public final class LocalTime implements Temporal, Comparable { + /// 00:00. public static final LocalTime MIDNIGHT = new LocalTime(0, 0, 0, 0); + /// 00:00, the earliest time of day. + public static final LocalTime MIN = MIDNIGHT; + /// 12:00. + public static final LocalTime NOON = new LocalTime(12, 0, 0, 0); + /// 23:59:59.999999999, the latest time of day. + public static final LocalTime MAX = new LocalTime(23, 59, 59, 999999999); + + private static final long NANOS_PER_SECOND = TimeArithmetic.NANOS_PER_SECOND; + private static final long NANOS_PER_MINUTE = 60L * NANOS_PER_SECOND; + private static final long NANOS_PER_HOUR = 60L * NANOS_PER_MINUTE; + private static final long NANOS_PER_DAY = TimeArithmetic.NANOS_PER_DAY; private final int hour; private final int minute; @@ -12,53 +51,103 @@ public final class LocalTime implements Comparable, TemporalAccessor private final int nano; private LocalTime(int hour, int minute, int second, int nano) { - DateTimeSupport.checkTime(hour, minute, second, nano); this.hour = hour; this.minute = minute; this.second = second; this.nano = nano; } + private static LocalTime create(int hour, int minute, int second, int nano) { + if ((minute | second | nano) == 0 && hour == 0) { + return MIDNIGHT; + } + return new LocalTime(hour, minute, second, nano); + } + public static LocalTime now() { return now(Clock.systemDefaultZone()); } + /// The time of day now in `zone`. + public static LocalTime now(ZoneId zone) { + return now(Clock.system(zone)); + } + public static LocalTime now(Clock clock) { return LocalDateTime.ofInstant(clock.instant(), clock.getZone()).toLocalTime(); } public static LocalTime of(int hour, int minute) { - return new LocalTime(hour, minute, 0, 0); + return of(hour, minute, 0, 0); } public static LocalTime of(int hour, int minute, int second) { - return new LocalTime(hour, minute, second, 0); + return of(hour, minute, second, 0); } - public static LocalTime of(int hour, int minute, int second, int nano) { - return new LocalTime(hour, minute, second, nano); + public static LocalTime of(int hour, int minute, int second, int nanoOfSecond) { + TimeArithmetic.check("HourOfDay", 0, 23, hour); + TimeArithmetic.check("MinuteOfHour", 0, 59, minute); + TimeArithmetic.check("SecondOfMinute", 0, 59, second); + TimeArithmetic.check("NanoOfSecond", 0, 999999999, nanoOfSecond); + return create(hour, minute, second, nanoOfSecond); } public static LocalTime ofSecondOfDay(long secondOfDay) { + TimeArithmetic.check("SecondOfDay", 0, 86399, secondOfDay); int hours = (int) (secondOfDay / 3600); - int minutes = (int) ((secondOfDay % 3600) / 60); - int seconds = (int) (secondOfDay % 60); - return of(hours, minutes, seconds); + secondOfDay -= hours * 3600L; + int minutes = (int) (secondOfDay / 60); + secondOfDay -= minutes * 60L; + return create(hours, minutes, (int) secondOfDay, 0); } public static LocalTime ofNanoOfDay(long nanoOfDay) { - long secondOfDay = nanoOfDay / DateTimeSupport.NANOS_PER_SECOND; - int nanos = (int) (nanoOfDay % DateTimeSupport.NANOS_PER_SECOND); - LocalTime base = ofSecondOfDay(secondOfDay); - return of(base.hour, base.minute, base.second, nanos); + TimeArithmetic.check("NanoOfDay", 0, NANOS_PER_DAY - 1, nanoOfDay); + int hours = (int) (nanoOfDay / NANOS_PER_HOUR); + nanoOfDay -= hours * NANOS_PER_HOUR; + int minutes = (int) (nanoOfDay / NANOS_PER_MINUTE); + nanoOfDay -= minutes * NANOS_PER_MINUTE; + int seconds = (int) (nanoOfDay / NANOS_PER_SECOND); + nanoOfDay -= seconds * NANOS_PER_SECOND; + return create(hours, minutes, seconds, (int) nanoOfDay); + } + + /// The time of day of `temporal`. + public static LocalTime from(TemporalAccessor temporal) { + if (temporal instanceof LocalTime) { + return (LocalTime) temporal; + } + if (temporal instanceof LocalDateTime) { + return ((LocalDateTime) temporal).toLocalTime(); + } + if (temporal instanceof ZonedDateTime) { + return ((ZonedDateTime) temporal).toLocalTime(); + } + if (temporal instanceof OffsetDateTime) { + return ((OffsetDateTime) temporal).toLocalTime(); + } + if (temporal == null) { + throw new NullPointerException("temporal"); + } + throw new DateTimeException("Unable to obtain LocalTime from TemporalAccessor: " + temporal + + " of type " + temporal.getClass().getName()); } public static LocalTime parse(CharSequence text) { - return (LocalTime) DateTimeFormatter.ISO_LOCAL_TIME.parse(text); + return parse(text, DateTimeFormatter.ISO_LOCAL_TIME); } public static LocalTime parse(CharSequence text, DateTimeFormatter formatter) { - return (LocalTime) formatter.parse(text); + if (formatter == null) { + throw new NullPointerException("formatter"); + } + TemporalAccessor parsed = formatter.parse(text); + try { + return from(parsed); + } catch (DateTimeException ex) { + throw TimeArithmetic.unparsable(text, ex); + } } public int getHour() { @@ -82,50 +171,268 @@ public int toSecondOfDay() { } public long toNanoOfDay() { - return toSecondOfDay() * DateTimeSupport.NANOS_PER_SECOND + nano; + return hour * NANOS_PER_HOUR + minute * NANOS_PER_MINUTE + second * NANOS_PER_SECOND + nano; + } + + /// This time of day on `date`. + public LocalDateTime atDate(LocalDate date) { + return LocalDateTime.of(date, this); + } + + public LocalTime withHour(int hour) { + if (this.hour == hour) { + return this; + } + TimeArithmetic.check("HourOfDay", 0, 23, hour); + return create(hour, minute, second, nano); + } + + public LocalTime withMinute(int minute) { + if (this.minute == minute) { + return this; + } + TimeArithmetic.check("MinuteOfHour", 0, 59, minute); + return create(hour, minute, second, nano); + } + + public LocalTime withSecond(int second) { + if (this.second == second) { + return this; + } + TimeArithmetic.check("SecondOfMinute", 0, 59, second); + return create(hour, minute, second, nano); + } + + public LocalTime withNano(int nanoOfSecond) { + if (this.nano == nanoOfSecond) { + return this; + } + TimeArithmetic.check("NanoOfSecond", 0, 999999999, nanoOfSecond); + return create(hour, minute, second, nanoOfSecond); + } + + /// This time with everything smaller than `unit` set to zero. The unit + /// has to divide a day. + public LocalTime truncatedTo(TemporalUnit unit) { + if (unit == ChronoUnit.NANOS) { + return this; + } + long dur = TimeArithmetic.truncationNanos(unit); + long nod = toNanoOfDay(); + return ofNanoOfDay((nod / dur) * dur); + } + + @Override + public boolean isSupported(TemporalUnit unit) { + if (unit instanceof ChronoUnit) { + return unit.isTimeBased(); + } + return unit != null && unit.isSupportedBy(this); + } + + @Override + public LocalTime plus(long amountToAdd, TemporalUnit unit) { + if (unit instanceof ChronoUnit) { + ChronoUnit u = (ChronoUnit) unit; + if (u == ChronoUnit.NANOS) { + return plusNanos(amountToAdd); + } + if (u == ChronoUnit.MICROS) { + return plusNanos((amountToAdd % 86400000000L) * 1000); + } + if (u == ChronoUnit.MILLIS) { + return plusNanos((amountToAdd % 86400000L) * 1000000); + } + if (u == ChronoUnit.SECONDS) { + return plusSeconds(amountToAdd); + } + if (u == ChronoUnit.MINUTES) { + return plusMinutes(amountToAdd); + } + if (u == ChronoUnit.HOURS) { + return plusHours(amountToAdd); + } + if (u == ChronoUnit.HALF_DAYS) { + return plusHours((amountToAdd % 2) * 12); + } + throw TimeArithmetic.unsupported(unit); + } + if (unit == null) { + throw new NullPointerException("unit"); + } + return unit.addTo(this, amountToAdd); + } + + @Override + public LocalTime minus(long amountToSubtract, TemporalUnit unit) { + return amountToSubtract == Long.MIN_VALUE ? plus(Long.MAX_VALUE, unit).plus(1, unit) + : plus(-amountToSubtract, unit); + } + + @Override + public LocalTime plus(TemporalAmount amount) { + return (LocalTime) amount.addTo(this); + } + + @Override + public LocalTime minus(TemporalAmount amount) { + return (LocalTime) amount.subtractFrom(this); } public LocalTime plusHours(long hoursToAdd) { - return ofNanoOfDay(DateTimeSupport.floorMod(toNanoOfDay() + hoursToAdd * 3600L * DateTimeSupport.NANOS_PER_SECOND, - DateTimeSupport.NANOS_PER_DAY)); + if (hoursToAdd == 0) { + return this; + } + int newHour = ((int) (hoursToAdd % 24) + hour + 24) % 24; + return create(newHour, minute, second, nano); } public LocalTime plusMinutes(long minutesToAdd) { - return ofNanoOfDay(DateTimeSupport.floorMod(toNanoOfDay() + minutesToAdd * 60L * DateTimeSupport.NANOS_PER_SECOND, - DateTimeSupport.NANOS_PER_DAY)); + if (minutesToAdd == 0) { + return this; + } + int mofd = hour * 60 + minute; + int newMofd = ((int) (minutesToAdd % 1440) + mofd + 1440) % 1440; + if (mofd == newMofd) { + return this; + } + return create(newMofd / 60, newMofd % 60, second, nano); + } + + public LocalTime plusSeconds(long secondstoAdd) { + if (secondstoAdd == 0) { + return this; + } + int sofd = toSecondOfDay(); + int newSofd = ((int) (secondstoAdd % 86400) + sofd + 86400) % 86400; + if (sofd == newSofd) { + return this; + } + return create(newSofd / 3600, (newSofd / 60) % 60, newSofd % 60, nano); } - public LocalTime plusSeconds(long secondsToAdd) { - return ofNanoOfDay(DateTimeSupport.floorMod(toNanoOfDay() + secondsToAdd * DateTimeSupport.NANOS_PER_SECOND, - DateTimeSupport.NANOS_PER_DAY)); + public LocalTime plusNanos(long nanosToAdd) { + if (nanosToAdd == 0) { + return this; + } + long nofd = toNanoOfDay(); + long newNofd = ((nanosToAdd % NANOS_PER_DAY) + nofd + NANOS_PER_DAY) % NANOS_PER_DAY; + if (nofd == newNofd) { + return this; + } + return ofNanoOfDay(newNofd); + } + + public LocalTime minusHours(long hoursToSubtract) { + return plusHours(-(hoursToSubtract % 24)); + } + + public LocalTime minusMinutes(long minutesToSubtract) { + return plusMinutes(-(minutesToSubtract % 1440)); + } + + public LocalTime minusSeconds(long secondsToSubtract) { + return plusSeconds(-(secondsToSubtract % 86400)); + } + + public LocalTime minusNanos(long nanosToSubtract) { + return plusNanos(-(nanosToSubtract % NANOS_PER_DAY)); + } + + /// The whole units of `unit` from this time to `endExclusive`, within + /// one day. + @Override + public long until(Temporal endExclusive, TemporalUnit unit) { + LocalTime end = from(endExclusive); + if (unit instanceof ChronoUnit) { + long nanosUntil = end.toNanoOfDay() - toNanoOfDay(); + ChronoUnit u = (ChronoUnit) unit; + if (u == ChronoUnit.NANOS) { + return nanosUntil; + } + if (u == ChronoUnit.MICROS) { + return nanosUntil / 1000; + } + if (u == ChronoUnit.MILLIS) { + return nanosUntil / 1000000; + } + if (u == ChronoUnit.SECONDS) { + return nanosUntil / NANOS_PER_SECOND; + } + if (u == ChronoUnit.MINUTES) { + return nanosUntil / NANOS_PER_MINUTE; + } + if (u == ChronoUnit.HOURS) { + return nanosUntil / NANOS_PER_HOUR; + } + if (u == ChronoUnit.HALF_DAYS) { + return nanosUntil / (12 * NANOS_PER_HOUR); + } + throw TimeArithmetic.unsupported(unit); + } + if (unit == null) { + throw new NullPointerException("unit"); + } + return unit.between(this, end); } public String format(DateTimeFormatter formatter) { return formatter.format(this); } + public boolean isAfter(LocalTime other) { + return compareTo(other) > 0; + } + + public boolean isBefore(LocalTime other) { + return compareTo(other) < 0; + } + + @Override public int compareTo(LocalTime other) { - if (hour != other.hour) { - return hour < other.hour ? -1 : 1; - } - if (minute != other.minute) { - return minute < other.minute ? -1 : 1; - } - if (second != other.second) { - return second < other.second ? -1 : 1; - } - return nano < other.nano ? -1 : nano > other.nano ? 1 : 0; + long a = toNanoOfDay(); + long b = other.toNanoOfDay(); + return a < b ? -1 : a > b ? 1 : 0; } + @Override public boolean equals(Object obj) { - return obj instanceof LocalTime && compareTo((LocalTime) obj) == 0; + if (this == obj) { + return true; + } + if (!(obj instanceof LocalTime)) { + return false; + } + LocalTime other = (LocalTime) obj; + return hour == other.hour && minute == other.minute && second == other.second && nano == other.nano; } + @Override public int hashCode() { - return hour * 3600 + minute * 60 + second + nano; + long nod = toNanoOfDay(); + return (int) (nod ^ (nod >>> 32)); } + /// The JDK's text: `HH:mm`, then `:ss` when there are seconds or a + /// fraction, then the fraction in as many groups of three digits as it + /// needs. + @Override public String toString() { - return DateTimeFormatter.ISO_LOCAL_TIME.format(this); + StringBuilder buf = new StringBuilder(18); + buf.append(hour < 10 ? "0" : "").append(hour).append(minute < 10 ? ":0" : ":").append(minute); + if (second > 0 || nano > 0) { + buf.append(second < 10 ? ":0" : ":").append(second); + if (nano > 0) { + buf.append('.'); + if (nano % 1000000 == 0) { + buf.append(Integer.toString((nano / 1000000) + 1000).substring(1)); + } else if (nano % 1000 == 0) { + buf.append(Integer.toString((nano / 1000) + 1000000).substring(1)); + } else { + buf.append(Integer.toString(nano + 1000000000).substring(1)); + } + } + } + return buf.toString(); } } diff --git a/Ports/CLDC11/src/java/time/Month.java b/Ports/CLDC11/src/java/time/Month.java new file mode 100644 index 00000000000..3d2ef98b516 --- /dev/null +++ b/Ports/CLDC11/src/java/time/Month.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.time; + +import java.time.temporal.TemporalAccessor; + +/// The twelve months of the ISO calendar. +public enum Month implements TemporalAccessor { + /// January, month 1. + JANUARY, + /// February, month 2. + FEBRUARY, + /// March, month 3. + MARCH, + /// April, month 4. + APRIL, + /// May, month 5. + MAY, + /// June, month 6. + JUNE, + /// July, month 7. + JULY, + /// August, month 8. + AUGUST, + /// September, month 9. + SEPTEMBER, + /// October, month 10. + OCTOBER, + /// November, month 11. + NOVEMBER, + /// December, month 12. + DECEMBER; + + private static final Month[] ALL = values(); + + /// The month numbered `month`, 1 for January to 12 for December. + public static Month of(int month) { + if (month < 1 || month > 12) { + throw new DateTimeException("Invalid value for MonthOfYear: " + month); + } + return ALL[month - 1]; + } + + /// 1 for January to 12 for December. + public int getValue() { + return ordinal() + 1; + } + + /// The month `months` later, around the year. + public Month plus(long months) { + int amount = (int) (months % 12); + return ALL[(ordinal() + amount + 12) % 12]; + } + + /// The month `months` earlier, around the year. + public Month minus(long months) { + return plus(-(months % 12)); + } + + /// The number of days of this month in a leap year or another. + public int length(boolean leapYear) { + // Not a switch: one over an enum compiles to a class that catches + // NoSuchFieldError, which a device does not have. + if (this == FEBRUARY) { + return leapYear ? 29 : 28; + } + if (this == APRIL || this == JUNE || this == SEPTEMBER || this == NOVEMBER) { + return 30; + } + return 31; + } + + /// The fewest days this month can have. + public int minLength() { + return length(false); + } + + /// The most days this month can have. + public int maxLength() { + return length(true); + } + + /// The day of the year this month starts on, 1 for January. + public int firstDayOfYear(boolean leapYear) { + int day = 1; + for (int i = 0; i < ordinal(); i++) { + day += ALL[i].length(leapYear); + } + return day; + } + + /// The first month of the quarter this month is in. + public Month firstMonthOfQuarter() { + return ALL[ordinal() / 3 * 3]; + } +} diff --git a/Ports/CLDC11/src/java/time/OffsetDateTime.java b/Ports/CLDC11/src/java/time/OffsetDateTime.java index 5de661396cf..5e175e1f420 100644 --- a/Ports/CLDC11/src/java/time/OffsetDateTime.java +++ b/Ports/CLDC11/src/java/time/OffsetDateTime.java @@ -1,17 +1,69 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ package java.time; import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.time.temporal.Temporal; import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalAmount; +import java.time.temporal.TemporalUnit; + +/// A date and time with a fixed offset from UTC, such as +/// 2007-12-03T10:15:30+01:00. +public final class OffsetDateTime implements Temporal, Comparable { + /// The earliest date and time, at the largest offset. + public static final OffsetDateTime MIN = new OffsetDateTime(LocalDateTime.MIN, ZoneOffset.MAX); + + /// The latest date and time, at the smallest offset. + public static final OffsetDateTime MAX = new OffsetDateTime(LocalDateTime.MAX, ZoneOffset.MIN); -public final class OffsetDateTime implements Comparable, TemporalAccessor { private final LocalDateTime dateTime; private final ZoneOffset offset; private OffsetDateTime(LocalDateTime dateTime, ZoneOffset offset) { + if (dateTime == null) { + throw new NullPointerException("dateTime"); + } + if (offset == null) { + throw new NullPointerException("offset"); + } this.dateTime = dateTime; this.offset = offset; } + public static OffsetDateTime now() { + return now(Clock.systemDefaultZone()); + } + + public static OffsetDateTime now(ZoneId zone) { + return now(Clock.system(zone)); + } + + public static OffsetDateTime now(Clock clock) { + return ofInstant(clock.instant(), clock.getZone()); + } + public static OffsetDateTime of(LocalDate date, LocalTime time, ZoneOffset offset) { return new OffsetDateTime(LocalDateTime.of(date, time), offset); } @@ -20,24 +72,67 @@ public static OffsetDateTime of(LocalDateTime dateTime, ZoneOffset offset) { return new OffsetDateTime(dateTime, offset); } + public static OffsetDateTime of(int year, int month, int dayOfMonth, int hour, int minute, int second, + int nanoOfSecond, ZoneOffset offset) { + return new OffsetDateTime(LocalDateTime.of(year, month, dayOfMonth, hour, minute, second, nanoOfSecond), + offset); + } + public static OffsetDateTime ofInstant(Instant instant, ZoneId zone) { + if (instant == null) { + throw new NullPointerException("instant"); + } + if (zone == null) { + throw new NullPointerException("zone"); + } ZoneOffset offset = DateTimeSupport.offsetFromInstant(instant, zone); - LocalDateTime local = LocalDateTime.ofInstant(instant, zone); - return new OffsetDateTime(local, offset); + return new OffsetDateTime(LocalDateTime.ofEpochSecond(instant.getEpochSecond(), instant.getNano(), offset), + offset); + } + + /// The date, time and offset of `temporal`. + public static OffsetDateTime from(TemporalAccessor temporal) { + if (temporal instanceof OffsetDateTime) { + return (OffsetDateTime) temporal; + } + if (temporal instanceof ZonedDateTime) { + return ((ZonedDateTime) temporal).toOffsetDateTime(); + } + if (temporal == null) { + throw new NullPointerException("temporal"); + } + throw new DateTimeException("Unable to obtain OffsetDateTime from TemporalAccessor: " + temporal + + " of type " + temporal.getClass().getName()); } public static OffsetDateTime parse(CharSequence text) { - return (OffsetDateTime) DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(text); + return parse(text, DateTimeFormatter.ISO_OFFSET_DATE_TIME); } public static OffsetDateTime parse(CharSequence text, DateTimeFormatter formatter) { - return (OffsetDateTime) formatter.parse(text); + if (formatter == null) { + throw new NullPointerException("formatter"); + } + TemporalAccessor parsed = formatter.parse(text); + try { + return from(parsed); + } catch (DateTimeException ex) { + throw TimeArithmetic.unparsable(text, ex); + } } public LocalDateTime toLocalDateTime() { return dateTime; } + public LocalDate toLocalDate() { + return dateTime.toLocalDate(); + } + + public LocalTime toLocalTime() { + return dateTime.toLocalTime(); + } + public ZoneOffset getOffset() { return offset; } @@ -46,28 +141,285 @@ public Instant toInstant() { return dateTime.toInstant(offset); } + public long toEpochSecond() { + return dateTime.toEpochSecond(offset); + } + + /// This date and time, as a moment in `offset`'s own zone. + public ZonedDateTime toZonedDateTime() { + return ZonedDateTime.of(dateTime, offset); + } + + /// The same moment in `zone`. + public ZonedDateTime atZoneSameInstant(ZoneId zone) { + return ZonedDateTime.ofInstant(dateTime, offset, zone); + } + + /// The same moment at another offset. + public OffsetDateTime withOffsetSameInstant(ZoneOffset offset) { + if (offset.equals(this.offset)) { + return this; + } + int difference = offset.getTotalSeconds() - this.offset.getTotalSeconds(); + return new OffsetDateTime(dateTime.plusSeconds(difference), offset); + } + + /// The same wall clock time at another offset. + public OffsetDateTime withOffsetSameLocal(ZoneOffset offset) { + return offset.equals(this.offset) ? this : new OffsetDateTime(dateTime, offset); + } + + public int getYear() { + return dateTime.getYear(); + } + + public int getMonthValue() { + return dateTime.getMonthValue(); + } + + public Month getMonth() { + return dateTime.getMonth(); + } + + public int getDayOfMonth() { + return dateTime.getDayOfMonth(); + } + + public int getDayOfYear() { + return dateTime.getDayOfYear(); + } + + public DayOfWeek getDayOfWeek() { + return dateTime.getDayOfWeek(); + } + + public int getHour() { + return dateTime.getHour(); + } + + public int getMinute() { + return dateTime.getMinute(); + } + + public int getSecond() { + return dateTime.getSecond(); + } + + public int getNano() { + return dateTime.getNano(); + } + + private OffsetDateTime with(LocalDateTime newDateTime) { + return newDateTime == dateTime ? this : new OffsetDateTime(newDateTime, offset); + } + + public OffsetDateTime withYear(int year) { + return with(dateTime.withYear(year)); + } + + public OffsetDateTime withMonth(int month) { + return with(dateTime.withMonth(month)); + } + + public OffsetDateTime withDayOfMonth(int dayOfMonth) { + return with(dateTime.withDayOfMonth(dayOfMonth)); + } + + public OffsetDateTime withDayOfYear(int dayOfYear) { + return with(dateTime.withDayOfYear(dayOfYear)); + } + + public OffsetDateTime withHour(int hour) { + return with(dateTime.withHour(hour)); + } + + public OffsetDateTime withMinute(int minute) { + return with(dateTime.withMinute(minute)); + } + + public OffsetDateTime withSecond(int second) { + return with(dateTime.withSecond(second)); + } + + public OffsetDateTime withNano(int nanoOfSecond) { + return with(dateTime.withNano(nanoOfSecond)); + } + + public OffsetDateTime truncatedTo(TemporalUnit unit) { + return with(dateTime.truncatedTo(unit)); + } + + @Override + public boolean isSupported(TemporalUnit unit) { + if (unit instanceof ChronoUnit) { + return unit != ChronoUnit.FOREVER; + } + return unit != null && unit.isSupportedBy(this); + } + + @Override + public OffsetDateTime plus(long amountToAdd, TemporalUnit unit) { + if (unit instanceof ChronoUnit) { + return with(dateTime.plus(amountToAdd, unit)); + } + if (unit == null) { + throw new NullPointerException("unit"); + } + return unit.addTo(this, amountToAdd); + } + + @Override + public OffsetDateTime minus(long amountToSubtract, TemporalUnit unit) { + return amountToSubtract == Long.MIN_VALUE ? plus(Long.MAX_VALUE, unit).plus(1, unit) + : plus(-amountToSubtract, unit); + } + + @Override + public OffsetDateTime plus(TemporalAmount amount) { + if (amount == null) { + throw new NullPointerException("amountToAdd"); + } + return (OffsetDateTime) amount.addTo(this); + } + + @Override + public OffsetDateTime minus(TemporalAmount amount) { + if (amount == null) { + throw new NullPointerException("amountToSubtract"); + } + return (OffsetDateTime) amount.subtractFrom(this); + } + + public OffsetDateTime plusYears(long years) { + return with(dateTime.plusYears(years)); + } + + public OffsetDateTime plusMonths(long months) { + return with(dateTime.plusMonths(months)); + } + + public OffsetDateTime plusWeeks(long weeks) { + return with(dateTime.plusWeeks(weeks)); + } + + public OffsetDateTime plusDays(long days) { + return with(dateTime.plusDays(days)); + } + + public OffsetDateTime plusHours(long hours) { + return with(dateTime.plusHours(hours)); + } + + public OffsetDateTime plusMinutes(long minutes) { + return with(dateTime.plusMinutes(minutes)); + } + + public OffsetDateTime plusSeconds(long seconds) { + return with(dateTime.plusSeconds(seconds)); + } + + public OffsetDateTime plusNanos(long nanos) { + return with(dateTime.plusNanos(nanos)); + } + + public OffsetDateTime minusYears(long years) { + return with(dateTime.minusYears(years)); + } + + public OffsetDateTime minusMonths(long months) { + return with(dateTime.minusMonths(months)); + } + + public OffsetDateTime minusWeeks(long weeks) { + return with(dateTime.minusWeeks(weeks)); + } + + public OffsetDateTime minusDays(long days) { + return with(dateTime.minusDays(days)); + } + + public OffsetDateTime minusHours(long hours) { + return with(dateTime.minusHours(hours)); + } + + public OffsetDateTime minusMinutes(long minutes) { + return with(dateTime.minusMinutes(minutes)); + } + + public OffsetDateTime minusSeconds(long seconds) { + return with(dateTime.minusSeconds(seconds)); + } + + public OffsetDateTime minusNanos(long nanos) { + return with(dateTime.minusNanos(nanos)); + } + + @Override + public long until(Temporal endExclusive, TemporalUnit unit) { + OffsetDateTime end = from(endExclusive); + if (unit instanceof ChronoUnit) { + end = end.withOffsetSameInstant(offset); + return dateTime.until(end.dateTime, unit); + } + if (unit == null) { + throw new NullPointerException("unit"); + } + return unit.between(this, end); + } + public String format(DateTimeFormatter formatter) { return formatter.format(this); } + public boolean isAfter(OffsetDateTime other) { + long a = toEpochSecond(); + long b = other.toEpochSecond(); + return a > b || (a == b && getNano() > other.getNano()); + } + + public boolean isBefore(OffsetDateTime other) { + long a = toEpochSecond(); + long b = other.toEpochSecond(); + return a < b || (a == b && getNano() < other.getNano()); + } + + public boolean isEqual(OffsetDateTime other) { + return toEpochSecond() == other.toEpochSecond() && getNano() == other.getNano(); + } + + /// Orders by moment, and moments that are equal by wall clock time. public int compareTo(OffsetDateTime other) { - int cmp = toInstant().compareTo(other.toInstant()); - return cmp != 0 ? cmp : dateTime.compareTo(other.dateTime); + if (offset.equals(other.offset)) { + return dateTime.compareTo(other.dateTime); + } + long a = toEpochSecond(); + long b = other.toEpochSecond(); + int cmp = a < b ? -1 : (a > b ? 1 : 0); + if (cmp == 0) { + cmp = getNano() - other.getNano(); + if (cmp == 0) { + cmp = dateTime.compareTo(other.dateTime); + } + } + return cmp; } public boolean equals(Object obj) { - return obj instanceof OffsetDateTime && compareTo((OffsetDateTime) obj) == 0; + if (this == obj) { + return true; + } + if (!(obj instanceof OffsetDateTime)) { + return false; + } + OffsetDateTime other = (OffsetDateTime) obj; + return dateTime.equals(other.dateTime) && offset.equals(other.offset); } public int hashCode() { - return dateTime.hashCode() * 31 + offset.hashCode(); + return dateTime.hashCode() ^ offset.hashCode(); } public String toString() { - return DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(this); - } - - ZoneId getZoneForFormatting() { - return offset; + return dateTime.toString() + offset.toString(); } } diff --git a/Ports/CLDC11/src/java/time/Period.java b/Ports/CLDC11/src/java/time/Period.java index 3fab3574b7c..77b61b2ab6c 100644 --- a/Ports/CLDC11/src/java/time/Period.java +++ b/Ports/CLDC11/src/java/time/Period.java @@ -1,6 +1,42 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ package java.time; -public final class Period { +import java.time.temporal.ChronoUnit; +import java.time.temporal.Temporal; +import java.time.temporal.TemporalAmount; +import java.time.temporal.TemporalUnit; +import java.time.temporal.UnsupportedTemporalTypeException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/// An amount of calendar time in years, months and days, such as +/// "2 years, 3 months and 4 days". The three are kept apart and never +/// converted into one another except by [#normalized]. +public final class Period implements TemporalAmount { + public static final Period ZERO = new Period(0, 0, 0); + private final int years; private final int months; private final int days; @@ -11,20 +47,157 @@ private Period(int years, int months, int days) { this.days = days; } - public static Period of(int years, int months, int days) { + private static Period create(int years, int months, int days) { + if ((years | months | days) == 0) { + return ZERO; + } return new Period(years, months, days); } + public static Period of(int years, int months, int days) { + return create(years, months, days); + } + public static Period ofDays(int days) { - return new Period(0, 0, days); + return create(0, 0, days); + } + + public static Period ofWeeks(int weeks) { + return create(0, 0, TimeArithmetic.toIntExact(TimeArithmetic.multiplyExact((long) weeks, 7L))); } public static Period ofMonths(int months) { - return new Period(0, months, 0); + return create(0, months, 0); } public static Period ofYears(int years) { - return new Period(years, 0, 0); + return create(years, 0, 0); + } + + /// The years, months and days from `startDateInclusive` to + /// `endDateExclusive`. + public static Period between(LocalDate startDateInclusive, LocalDate endDateExclusive) { + return startDateInclusive.until(endDateExclusive); + } + + /// The period of `amount`, which must be in years, months and days. + public static Period from(TemporalAmount amount) { + if (amount instanceof Period) { + return (Period) amount; + } + if (amount == null) { + throw new NullPointerException("amount"); + } + int years = 0; + int months = 0; + int days = 0; + List units = amount.getUnits(); + for (int i = 0; i < units.size(); i++) { + TemporalUnit unit = units.get(i); + long value = amount.get(unit); + if (unit == ChronoUnit.YEARS) { + years = TimeArithmetic.toIntExact(value); + } else if (unit == ChronoUnit.MONTHS) { + months = TimeArithmetic.toIntExact(value); + } else if (unit == ChronoUnit.DAYS) { + days = TimeArithmetic.toIntExact(value); + } else { + throw new DateTimeException("Unit must be Years, Months or Days, but was " + unit); + } + } + return create(years, months, days); + } + + /// Reads `PnYnMnWnD`: each section optional but at least one present, + /// each number signed, the letters in either case, and a leading sign + /// negating the whole. Weeks are added to the days. + public static Period parse(CharSequence text) { + if (text == null) { + throw new NullPointerException("text"); + } + String s = text.toString(); + int pos = 0; + boolean negate = false; + if (pos < s.length() && (s.charAt(pos) == '+' || s.charAt(pos) == '-')) { + negate = s.charAt(pos) == '-'; + pos++; + } + if (pos >= s.length() || (s.charAt(pos) != 'P' && s.charAt(pos) != 'p')) { + throw unparsablePeriod(s, null); + } + pos++; + String order = "YMWD"; + long[] values = new long[4]; + int next = 0; + boolean any = false; + try { + while (pos < s.length()) { + int start = pos; + if (s.charAt(pos) == '+' || s.charAt(pos) == '-') { + pos++; + } + int digits = pos; + while (pos < s.length() && s.charAt(pos) >= '0' && s.charAt(pos) <= '9') { + pos++; + } + if (pos == digits || pos >= s.length()) { + throw unparsablePeriod(s, null); + } + char letter = Character.toUpperCase(s.charAt(pos)); + int slot = order.indexOf(letter, next); + if (slot < 0) { + throw unparsablePeriod(s, null); + } + values[slot] = Integer.parseInt(s.substring(s.charAt(start) == '+' ? start + 1 : start, pos)); + next = slot + 1; + any = true; + pos++; + } + } catch (NumberFormatException ex) { + throw unparsablePeriod(s, ex); + } + if (!any) { + throw unparsablePeriod(s, null); + } + if (negate) { + for (int i = 0; i < values.length; i++) { + if (values[i] == Integer.MIN_VALUE) { + throw unparsablePeriod(s, new ArithmeticException("integer overflow")); + } + values[i] = -values[i]; + } + } + // Weeks join the days outside the reading, so an overflow there is + // the arithmetic's own exception, as the JDK's is. + int days = TimeArithmetic.toIntExact(values[3] + TimeArithmetic.toIntExact(values[2] * 7)); + return create((int) values[0], (int) values[1], days); + } + + private static java.time.format.DateTimeParseException unparsablePeriod(String text, Throwable cause) { + return new java.time.format.DateTimeParseException("Text cannot be parsed to a Period", text, 0, cause); + } + + @Override + public long get(TemporalUnit unit) { + if (unit == ChronoUnit.YEARS) { + return years; + } + if (unit == ChronoUnit.MONTHS) { + return months; + } + if (unit == ChronoUnit.DAYS) { + return days; + } + throw new UnsupportedTemporalTypeException("Unsupported unit: " + unit); + } + + @Override + public List getUnits() { + ArrayList units = new ArrayList(3); + units.add(ChronoUnit.YEARS); + units.add(ChronoUnit.MONTHS); + units.add(ChronoUnit.DAYS); + return Collections.unmodifiableList(units); } public int getYears() { @@ -39,19 +212,166 @@ public int getDays() { return days; } - public LocalDate addTo(LocalDate date) { - return date.plusYears(years).plusMonths(months).plusDays(days); + public boolean isZero() { + return this == ZERO || (years | months | days) == 0; + } + + public boolean isNegative() { + return years < 0 || months < 0 || days < 0; + } + + public Period withYears(int years) { + return years == this.years ? this : create(years, months, days); + } + + public Period withMonths(int months) { + return months == this.months ? this : create(years, months, days); + } + + public Period withDays(int days) { + return days == this.days ? this : create(years, months, days); + } + + /// This period plus `amountToAdd`, which must be in years, months and + /// days; each unit is added to its own. + public Period plus(TemporalAmount amountToAdd) { + Period other = from(amountToAdd); + return create(TimeArithmetic.toIntExact(TimeArithmetic.addExact((long) years, (long) other.years)), + TimeArithmetic.toIntExact(TimeArithmetic.addExact((long) months, (long) other.months)), + TimeArithmetic.toIntExact(TimeArithmetic.addExact((long) days, (long) other.days))); + } + + public Period minus(TemporalAmount amountToSubtract) { + Period other = from(amountToSubtract); + return create(TimeArithmetic.toIntExact(TimeArithmetic.subtractExact((long) years, (long) other.years)), + TimeArithmetic.toIntExact(TimeArithmetic.subtractExact((long) months, (long) other.months)), + TimeArithmetic.toIntExact(TimeArithmetic.subtractExact((long) days, (long) other.days))); + } + + public Period plusYears(long yearsToAdd) { + return yearsToAdd == 0 ? this + : create(TimeArithmetic.toIntExact(TimeArithmetic.addExact((long) years, yearsToAdd)), months, days); + } + + public Period plusMonths(long monthsToAdd) { + return monthsToAdd == 0 ? this + : create(years, TimeArithmetic.toIntExact(TimeArithmetic.addExact((long) months, monthsToAdd)), days); + } + + public Period plusDays(long daysToAdd) { + return daysToAdd == 0 ? this + : create(years, months, TimeArithmetic.toIntExact(TimeArithmetic.addExact((long) days, daysToAdd))); + } + + public Period minusYears(long yearsToSubtract) { + return yearsToSubtract == Long.MIN_VALUE ? plusYears(Long.MAX_VALUE).plusYears(1) + : plusYears(-yearsToSubtract); + } + + public Period minusMonths(long monthsToSubtract) { + return monthsToSubtract == Long.MIN_VALUE ? plusMonths(Long.MAX_VALUE).plusMonths(1) + : plusMonths(-monthsToSubtract); + } + + public Period minusDays(long daysToSubtract) { + return daysToSubtract == Long.MIN_VALUE ? plusDays(Long.MAX_VALUE).plusDays(1) + : plusDays(-daysToSubtract); + } + + public Period multipliedBy(int scalar) { + if (this == ZERO || scalar == 1) { + return this; + } + return create(TimeArithmetic.toIntExact(TimeArithmetic.multiplyExact((long) years, (long) scalar)), + TimeArithmetic.toIntExact(TimeArithmetic.multiplyExact((long) months, (long) scalar)), + TimeArithmetic.toIntExact(TimeArithmetic.multiplyExact((long) days, (long) scalar))); + } + + public Period negated() { + return multipliedBy(-1); + } + + /// The years and months rebalanced so that the months lie within + /// -11 to 11 with the sign of the years; the days are left as they are. + public Period normalized() { + long totalMonths = toTotalMonths(); + long splitYears = totalMonths / 12; + int splitMonths = (int) (totalMonths % 12); + if (splitYears == years && splitMonths == months) { + return this; + } + return create(TimeArithmetic.toIntExact(splitYears), splitMonths, days); + } + + public long toTotalMonths() { + return years * 12L + months; + } + + /// Adds the years and months together, then the days, as the JDK does: + /// a date at the end of a month stays valid in between. + @Override + public Temporal addTo(Temporal temporal) { + if (months == 0) { + if (years != 0) { + temporal = temporal.plus(years, ChronoUnit.YEARS); + } + } else { + long totalMonths = toTotalMonths(); + if (totalMonths != 0) { + temporal = temporal.plus(totalMonths, ChronoUnit.MONTHS); + } + } + if (days != 0) { + temporal = temporal.plus(days, ChronoUnit.DAYS); + } + return temporal; + } + + @Override + public Temporal subtractFrom(Temporal temporal) { + if (months == 0) { + if (years != 0) { + temporal = temporal.minus(years, ChronoUnit.YEARS); + } + } else { + long totalMonths = toTotalMonths(); + if (totalMonths != 0) { + temporal = temporal.minus(totalMonths, ChronoUnit.MONTHS); + } + } + if (days != 0) { + temporal = temporal.minus(days, ChronoUnit.DAYS); + } + return temporal; + } + + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Period)) { + return false; + } + Period other = (Period) obj; + return years == other.years && months == other.months && days == other.days; + } + + public int hashCode() { + return years + ((months << 8) | (months >>> 24)) + ((days << 16) | (days >>> 16)); } public String toString() { - StringBuffer sb = new StringBuffer("P"); + if (this == ZERO || (years | months | days) == 0) { + return "P0D"; + } + StringBuilder sb = new StringBuilder("P"); if (years != 0) { sb.append(years).append('Y'); } if (months != 0) { sb.append(months).append('M'); } - if (days != 0 || (years == 0 && months == 0)) { + if (days != 0) { sb.append(days).append('D'); } return sb.toString(); diff --git a/Ports/CLDC11/src/java/time/TimeArithmetic.java b/Ports/CLDC11/src/java/time/TimeArithmetic.java new file mode 100644 index 00000000000..ca3f529cbba --- /dev/null +++ b/Ports/CLDC11/src/java/time/TimeArithmetic.java @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.time; + +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalUnit; +import java.time.temporal.UnsupportedTemporalTypeException; + +/// Exact arithmetic and unit tables the date and time classes share. The +/// exact operations throw `ArithmeticException("long overflow")` where the +/// JDK's `Math.*Exact` do, rather than wrapping around. +final class TimeArithmetic { + static final long NANOS_PER_SECOND = 1000000000L; + static final long SECONDS_PER_DAY = 86400L; + static final long NANOS_PER_DAY = SECONDS_PER_DAY * NANOS_PER_SECOND; + + /// The whole seconds of each `ChronoUnit`, by ordinal; 0 below a second. + private static final long[] UNIT_SECONDS = {0L, 0L, 0L, 1L, 60L, 3600L, 43200L, 86400L, 604800L, + 2629746L, 31556952L, 315569520L, 3155695200L, 31556952000L, 31556952000000000L, Long.MAX_VALUE}; + + /// The nanoseconds in one unit below a second, by ordinal; 0 above. + private static final long[] UNIT_NANOS = {1L, 1000L, 1000000L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, + 0L, 0L, 0L}; + + private TimeArithmetic() { + } + + static long floorDiv(long a, long b) { + long q = a / b; + return (a % b != 0 && ((a < 0) != (b < 0))) ? q - 1 : q; + } + + static long floorMod(long a, long b) { + return a - floorDiv(a, b) * b; + } + + static long multiplyExact(long a, long b) { + long r = a * b; + long absA = a < 0 ? -a : a; + long absB = b < 0 ? -b : b; + // MIN_VALUE * -1 is tested before the division: ParparVM divides in + // C, where MIN_VALUE / -1 is undefined and traps (SIGFPE) on x86. + if (((absA | absB) >>> 31) != 0 + && (a == Long.MIN_VALUE && b == -1 || b != 0 && r / b != a)) { + throw new ArithmeticException("long overflow"); + } + return r; + } + + static long addExact(long a, long b) { + long r = a + b; + if (((a ^ r) & (b ^ r)) < 0) { + throw new ArithmeticException("long overflow"); + } + return r; + } + + static long subtractExact(long a, long b) { + long r = a - b; + if (((a ^ b) & (a ^ r)) < 0) { + throw new ArithmeticException("long overflow"); + } + return r; + } + + static int toIntExact(long value) { + if ((int) value != value) { + throw new ArithmeticException("integer overflow"); + } + return (int) value; + } + + /// `unit` as a `ChronoUnit`, or the JDK's refusal of a unit it cannot add. + /// The length in nanoseconds of a unit to truncate a time to. Any unit + /// will do, a custom one included, as long as its duration divides a + /// standard day: that is the JDK's rule, and the reason it reads the + /// duration rather than requiring a [ChronoUnit]. + static long truncationNanos(TemporalUnit unit) { + java.time.Duration length = unit.getDuration(); + if (length.getSeconds() > SECONDS_PER_DAY) { + throw new UnsupportedTemporalTypeException("Unit is too large to be used for truncation"); + } + long dur = length.toNanos(); + if (dur <= 0 || NANOS_PER_DAY % dur != 0) { + throw new UnsupportedTemporalTypeException("Unit must divide into a standard day without remainder"); + } + return dur; + } + + static ChronoUnit chrono(TemporalUnit unit) { + if (unit instanceof ChronoUnit) { + return (ChronoUnit) unit; + } + if (unit == null) { + throw new NullPointerException("unit"); + } + throw unsupported(unit); + } + + static UnsupportedTemporalTypeException unsupported(Object unit) { + return new UnsupportedTemporalTypeException("Unsupported unit: " + unit); + } + + /// The whole seconds in one `unit`, 0 for the units shorter than a second. + static long seconds(ChronoUnit unit) { + return UNIT_SECONDS[unit.ordinal()]; + } + + /// The nanoseconds in one `unit` shorter than a second, 0 for the others. + static long nanos(ChronoUnit unit) { + return UNIT_NANOS[unit.ordinal()]; + } + + /// The whole units between two moments given as seconds and nanoseconds, + /// the fraction dropped towards zero. + static long timeUntil(long s1, long n1, long s2, long n2, ChronoUnit unit) { + long seconds = s2 - s1; + long nanos = n2 - n1; + if (seconds > 0 && nanos < 0) { + seconds--; + nanos += NANOS_PER_SECOND; + } else if (seconds < 0 && nanos > 0) { + seconds++; + nanos -= NANOS_PER_SECOND; + } + long perNanos = nanos(unit); + if (perNanos != 0) { + return addExact(multiplyExact(seconds, NANOS_PER_SECOND / perNanos), nanos / perNanos); + } + return seconds / seconds(unit); + } + + /// The nanosecond of the second of a value that has a time, 0 for one + /// that has none. + static int nanoOfSecond(Object value) { + if (value instanceof Instant) { + return ((Instant) value).getNano(); + } + if (value instanceof LocalTime) { + return ((LocalTime) value).getNano(); + } + if (value instanceof LocalDateTime) { + return ((LocalDateTime) value).getNano(); + } + if (value instanceof ZonedDateTime) { + return ((ZonedDateTime) value).getNano(); + } + if (value instanceof OffsetDateTime) { + return ((OffsetDateTime) value).toLocalDateTime().getNano(); + } + return 0; + } + + /// `value`, or the JDK's `DateTimeException` for a field outside + /// `min` to `max`. + static long check(String field, long min, long max, long value) { + if (value < min || value > max) { + throw new DateTimeException("Invalid value for " + field + " (valid values " + min + " - " + max + + "): " + value); + } + return value; + } + + static DateTimeException instantOutOfRange() { + return new DateTimeException("Instant exceeds minimum or maximum instant"); + } + + /// What `parse(text, formatter)` throws when the text was read but does + /// not make the value asked for. + static java.time.format.DateTimeParseException unparsable(CharSequence text, DateTimeException cause) { + return new java.time.format.DateTimeParseException("Text '" + text + "' could not be parsed: " + + cause.getMessage(), text, 0, cause); + } +} diff --git a/Ports/CLDC11/src/java/time/ZoneId.java b/Ports/CLDC11/src/java/time/ZoneId.java index 31f638e1c78..958a9b6c2ea 100644 --- a/Ports/CLDC11/src/java/time/ZoneId.java +++ b/Ports/CLDC11/src/java/time/ZoneId.java @@ -1,51 +1,165 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ package java.time; import java.util.TimeZone; +/// A time zone: either a fixed [ZoneOffset], or a region such as +/// `Europe/Paris` whose offsets come from the platform's time zone rules. +/// +/// As in the JDK, `ZoneId.of("UTC")` and `ZoneId.of("GMT")` are regions +/// named that way, not [ZoneOffset#UTC] (whose id is `Z`), and +/// `ZoneId.of("UTC+01:00")` keeps its prefix in its id. public class ZoneId { private final String id; + /// The offset of a region that never changes it (`UTC`, `GMT`, `UT` + /// and those followed by an offset), or null. + private final ZoneOffset fixed; ZoneId(String id) { + this(id, null); + } + + private ZoneId(String id, ZoneOffset fixed) { this.id = id; + this.fixed = fixed; } public static ZoneId of(String zoneId) { if (zoneId == null) { - throw new NullPointerException(); - } - if ("Z".equals(zoneId) || "UTC".equals(zoneId) || "GMT".equals(zoneId)) { - return ZoneOffset.UTC; + throw new NullPointerException("zoneId"); } - if (zoneId.startsWith("+") || zoneId.startsWith("-")) { + if (zoneId.length() <= 1 || zoneId.startsWith("+") || zoneId.startsWith("-")) { return ZoneOffset.of(zoneId); } - if (zoneId.startsWith("UTC+") || zoneId.startsWith("UTC-")) { - return ZoneOffset.of(zoneId.substring(3)); + if (zoneId.startsWith("UTC") || zoneId.startsWith("GMT")) { + return ofPrefixed(zoneId, 3); } - if (zoneId.startsWith("GMT+") || zoneId.startsWith("GMT-")) { - return ZoneOffset.of(zoneId.substring(3)); + if (zoneId.startsWith("UT")) { + return ofPrefixed(zoneId, 2); } - return new ZoneId(zoneId); + for (int i = 0; i < zoneId.length(); i++) { + char c = zoneId.charAt(i); + boolean ok = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') + || c == '~' || c == '/' || c == '.' || c == '_' || c == '+' || c == '-'; + if (!ok) { + throw new DateTimeException("Invalid ID for region-based ZoneId, invalid format: " + zoneId); + } + } + // A well-formed region ID is accepted without checking it names a + // real region, where the JDK throws ZoneRulesException. That is + // deliberate: a device has no zone database to check against -- the + // rules come from the OS one lookup at a time, through TimeZone, and + // TimeZone.getAvailableIDs() answers only GMT and the default zone -- + // so validating would mean a new native on every port for an error + // case. Its rules are whatever java.util.TimeZone.getTimeZone gives + // the same ID, which -- on the JDK as here -- does not throw for an + // unknown one either. + return new ZoneId(zoneId, null); + } + + private static ZoneId ofPrefixed(String zoneId, int prefixLength) { + String prefix = zoneId.substring(0, prefixLength); + if (zoneId.length() == prefixLength) { + return new ZoneId(prefix, ZoneOffset.UTC); + } + char sign = zoneId.charAt(prefixLength); + if (sign != '+' && sign != '-') { + // GMT0 and the like: a region of its own. + return of0(zoneId); + } + ZoneOffset offset; + try { + offset = ZoneOffset.of(zoneId.substring(prefixLength)); + } catch (DateTimeException ex) { + throw new DateTimeException("Invalid ID for offset-based ZoneId: " + zoneId); + } + if (offset.getTotalSeconds() == 0) { + return new ZoneId(prefix, ZoneOffset.UTC); + } + return new ZoneId(prefix + offset.getId(), offset); + } + + private static ZoneId of0(String zoneId) { + return new ZoneId(zoneId, "GMT0".equals(zoneId) ? ZoneOffset.UTC : null); } public static ZoneId systemDefault() { return of(TimeZone.getDefault().getID()); } + /// The zone `prefix` followed by `offset`: `UTC+01:00` for `UTC`, + /// just `UTC` for a zero offset. + public static ZoneId ofOffset(String prefix, ZoneOffset offset) { + if (prefix == null || offset == null) { + throw new NullPointerException(); + } + if (prefix.length() == 0) { + return offset; + } + if (!prefix.equals("GMT") && !prefix.equals("UTC") && !prefix.equals("UT")) { + throw new IllegalArgumentException("prefix should be GMT, UTC or UT, is: " + prefix); + } + if (offset.getTotalSeconds() == 0) { + return new ZoneId(prefix, ZoneOffset.UTC); + } + return new ZoneId(prefix + offset.getId(), offset); + } + public String getId() { return id; } + /// This zone as a [ZoneOffset] when its offset never changes, and + /// itself otherwise. + public ZoneId normalized() { + ZoneOffset f = fixedOffset(); + return f != null ? f : this; + } + + /// The offset this zone always has, or null for a region whose offset + /// changes. + ZoneOffset fixedOffset() { + return fixed; + } + + /// The platform time zone with this zone's rules. TimeZone toTimeZone() { - if (this instanceof ZoneOffset) { - ZoneOffset offset = (ZoneOffset) this; - return TimeZone.getTimeZone(offset.getId().equals("Z") ? "GMT" : "GMT" + offset.getId()); + ZoneOffset offset = fixedOffset(); + if (offset != null) { + return TimeZone.getTimeZone(offset.getTotalSeconds() == 0 ? "GMT" : "GMT" + offset.getId()); } return TimeZone.getTimeZone(id); } public boolean equals(Object obj) { - return obj instanceof ZoneId && id.equals(((ZoneId) obj).id); + if (this == obj) { + return true; + } + if (!(obj instanceof ZoneId) || obj instanceof ZoneOffset) { + return false; + } + return id.equals(((ZoneId) obj).id); } public int hashCode() { diff --git a/Ports/CLDC11/src/java/time/ZoneOffset.java b/Ports/CLDC11/src/java/time/ZoneOffset.java index c66eca48d06..4c10c59e4a5 100644 --- a/Ports/CLDC11/src/java/time/ZoneOffset.java +++ b/Ports/CLDC11/src/java/time/ZoneOffset.java @@ -1,7 +1,36 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ package java.time; -public final class ZoneOffset extends ZoneId { +import java.time.temporal.TemporalAccessor; + +/// A fixed offset from UTC, between -18:00 and +18:00. +public final class ZoneOffset extends ZoneId implements TemporalAccessor, Comparable { + private static final int MAX_SECONDS = 18 * 3600; + public static final ZoneOffset UTC = new ZoneOffset(0); + public static final ZoneOffset MIN = new ZoneOffset(-MAX_SECONDS); + public static final ZoneOffset MAX = new ZoneOffset(MAX_SECONDS); private final int totalSeconds; @@ -10,83 +39,183 @@ private ZoneOffset(int totalSeconds) { this.totalSeconds = totalSeconds; } + /// Reads `Z`, `+h`, `+hh`, `+hh:mm`, `+hhmm`, `+hh:mm:ss` and + /// `+hhmmss`, with `-` in place of `+` for a negative offset. public static ZoneOffset of(String offsetId) { if (offsetId == null) { - throw new NullPointerException(); + throw new NullPointerException("offsetId"); } if ("Z".equals(offsetId)) { return UTC; } String text = offsetId; - char sign = text.charAt(0); - if (sign != '+' && sign != '-') { - throw new IllegalArgumentException("Invalid offset: " + offsetId); - } - text = text.substring(1); int hours; - int minutes = 0; - if (text.indexOf(':') > 0) { - String[] parts = split(text, ':'); - hours = Integer.parseInt(parts[0]); - minutes = Integer.parseInt(parts[1]); - } else if (text.length() == 2) { - hours = Integer.parseInt(text); - } else if (text.length() == 4) { - hours = Integer.parseInt(text.substring(0, 2)); - minutes = Integer.parseInt(text.substring(2)); - } else { - throw new IllegalArgumentException("Invalid offset: " + offsetId); - } - int total = hours * 3600 + minutes * 60; - if (sign == '-') { - total = -total; - } - return ofTotalSeconds(total); + int minutes; + int seconds; + switch (text.length()) { + case 2: + text = text.charAt(0) + "0" + text.charAt(1); + hours = parseNumber(offsetId, text, 1, false); + minutes = 0; + seconds = 0; + break; + case 3: + hours = parseNumber(offsetId, text, 1, false); + minutes = 0; + seconds = 0; + break; + case 5: + hours = parseNumber(offsetId, text, 1, false); + minutes = parseNumber(offsetId, text, 3, false); + seconds = 0; + break; + case 6: + hours = parseNumber(offsetId, text, 1, false); + minutes = parseNumber(offsetId, text, 4, true); + seconds = 0; + break; + case 7: + hours = parseNumber(offsetId, text, 1, false); + minutes = parseNumber(offsetId, text, 3, false); + seconds = parseNumber(offsetId, text, 5, false); + break; + case 9: + hours = parseNumber(offsetId, text, 1, false); + minutes = parseNumber(offsetId, text, 4, true); + seconds = parseNumber(offsetId, text, 7, true); + break; + default: + throw new DateTimeException("Invalid ID for ZoneOffset, invalid format: " + offsetId); + } + char first = text.charAt(0); + if (first != '+' && first != '-') { + throw new DateTimeException("Invalid ID for ZoneOffset, plus/minus not found when expected: " + + offsetId); + } + if (first == '-') { + return ofHoursMinutesSeconds(-hours, -minutes, -seconds); + } + return ofHoursMinutesSeconds(hours, minutes, seconds); } - private static String[] split(String value, char ch) { - int pos = value.indexOf(ch); - return new String[] { value.substring(0, pos), value.substring(pos + 1) }; + private static int parseNumber(String offsetId, String text, int pos, boolean precededByColon) { + if (precededByColon && text.charAt(pos - 1) != ':') { + throw new DateTimeException("Invalid ID for ZoneOffset, colon not found when expected: " + offsetId); + } + char ch1 = text.charAt(pos); + char ch2 = text.charAt(pos + 1); + if (ch1 < '0' || ch1 > '9' || ch2 < '0' || ch2 > '9') { + throw new DateTimeException("Invalid ID for ZoneOffset, non numeric characters found: " + offsetId); + } + return (ch1 - 48) * 10 + (ch2 - 48); } public static ZoneOffset ofHours(int hours) { - return ofTotalSeconds(hours * 3600); + return ofHoursMinutesSeconds(hours, 0, 0); } public static ZoneOffset ofHoursMinutes(int hours, int minutes) { - int sign = hours < 0 || minutes < 0 ? -1 : 1; - return ofTotalSeconds(hours * 3600 + sign * Math.abs(minutes) * 60); + return ofHoursMinutesSeconds(hours, minutes, 0); + } + + /// The offset of `hours`, `minutes` and `seconds`, which must all have + /// the same sign. + public static ZoneOffset ofHoursMinutesSeconds(int hours, int minutes, int seconds) { + if (hours < -18 || hours > 18) { + throw new DateTimeException("Zone offset hours not in valid range: value " + hours + + " is not in the range -18 to 18"); + } + if (hours > 0) { + if (minutes < 0 || seconds < 0) { + throw new DateTimeException("Zone offset minutes and seconds must be positive because hours is positive"); + } + } else if (hours < 0) { + if (minutes > 0 || seconds > 0) { + throw new DateTimeException("Zone offset minutes and seconds must be negative because hours is negative"); + } + } else if ((minutes > 0 && seconds < 0) || (minutes < 0 && seconds > 0)) { + throw new DateTimeException("Zone offset minutes and seconds must have the same sign"); + } + if (minutes < -59 || minutes > 59) { + throw new DateTimeException("Zone offset minutes not in valid range: value " + minutes + + " is not in the range -59 to 59"); + } + if (seconds < -59 || seconds > 59) { + throw new DateTimeException("Zone offset seconds not in valid range: value " + seconds + + " is not in the range -59 to 59"); + } + if (Math.abs(hours) == 18 && (minutes | seconds) != 0) { + throw new DateTimeException("Zone offset not in valid range: -18:00 to +18:00"); + } + return ofTotalSeconds(hours * 3600 + minutes * 60 + seconds); } public static ZoneOffset ofTotalSeconds(int totalSeconds) { + if (totalSeconds < -MAX_SECONDS || totalSeconds > MAX_SECONDS) { + throw new DateTimeException("Zone offset not in valid range: -18:00 to +18:00"); + } if (totalSeconds == 0) { return UTC; } return new ZoneOffset(totalSeconds); } + /// The offset of `temporal`. + public static ZoneOffset from(TemporalAccessor temporal) { + if (temporal instanceof ZoneOffset) { + return (ZoneOffset) temporal; + } + if (temporal instanceof ZonedDateTime) { + return ((ZonedDateTime) temporal).getOffset(); + } + if (temporal instanceof OffsetDateTime) { + return ((OffsetDateTime) temporal).getOffset(); + } + if (temporal == null) { + throw new NullPointerException("temporal"); + } + throw new DateTimeException("Unable to obtain ZoneOffset from TemporalAccessor: " + temporal + + " of type " + temporal.getClass().getName()); + } + public int getTotalSeconds() { return totalSeconds; } + @Override + ZoneOffset fixedOffset() { + return this; + } + + /// Larger offsets sort first, as in the JDK: an offset further east is + /// an earlier moment for the same wall clock time. + public int compareTo(ZoneOffset other) { + return other.totalSeconds - totalSeconds; + } + + public boolean equals(Object obj) { + return obj == this || (obj instanceof ZoneOffset && ((ZoneOffset) obj).totalSeconds == totalSeconds); + } + + public int hashCode() { + return totalSeconds; + } + private static String buildId(int totalSeconds) { if (totalSeconds == 0) { return "Z"; } int abs = Math.abs(totalSeconds); int hours = abs / 3600; - int minutes = (abs % 3600) / 60; - StringBuffer sb = new StringBuffer(); + int minutes = (abs / 60) % 60; + int seconds = abs % 60; + StringBuilder sb = new StringBuilder(); sb.append(totalSeconds < 0 ? '-' : '+'); - if (hours < 10) { - sb.append('0'); - } - sb.append(hours); - sb.append(':'); - if (minutes < 10) { - sb.append('0'); + sb.append(hours < 10 ? "0" : "").append(hours); + sb.append(minutes < 10 ? ":0" : ":").append(minutes); + if (seconds != 0) { + sb.append(seconds < 10 ? ":0" : ":").append(seconds); } - sb.append(minutes); return sb.toString(); } } diff --git a/Ports/CLDC11/src/java/time/ZonedDateTime.java b/Ports/CLDC11/src/java/time/ZonedDateTime.java index 7fe868a58bf..ec21defa796 100644 --- a/Ports/CLDC11/src/java/time/ZonedDateTime.java +++ b/Ports/CLDC11/src/java/time/ZonedDateTime.java @@ -1,94 +1,528 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ package java.time; +import java.time.chrono.ChronoZonedDateTime; import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.time.temporal.Temporal; import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalAmount; +import java.time.temporal.TemporalUnit; -public final class ZonedDateTime implements Comparable, TemporalAccessor { +/// A date and a time of day in a time zone, such as +/// `2007-12-03T10:15:30+01:00[Europe/Paris]`. +/// +/// Adding a date-based amount moves the wall clock and keeps the offset where +/// the zone allows it; adding a time-based amount moves the instant. Across a +/// change of offset the two differ, as they do in the JDK. +public final class ZonedDateTime implements ChronoZonedDateTime { private final LocalDateTime dateTime; private final ZoneId zone; private final ZoneOffset offset; - private ZonedDateTime(LocalDateTime dateTime, ZoneId zone, ZoneOffset offset) { + private ZonedDateTime(LocalDateTime dateTime, ZoneOffset offset, ZoneId zone) { this.dateTime = dateTime; - this.zone = zone; this.offset = offset; + this.zone = zone; } public static ZonedDateTime now() { return now(Clock.systemDefaultZone()); } + /// The date and time now in `zone`. + public static ZonedDateTime now(ZoneId zone) { + return now(Clock.system(zone)); + } + public static ZonedDateTime now(Clock clock) { return ofInstant(clock.instant(), clock.getZone()); } public static ZonedDateTime of(LocalDate date, LocalTime time, ZoneId zone) { - LocalDateTime ldt = LocalDateTime.of(date, time); - Instant instant = ldt.toInstant(DateTimeSupport.offsetFromInstant(ldt.toInstant(ZoneOffset.UTC), zone)); - return ofInstant(instant, zone); + return of(LocalDateTime.of(date, time), zone); + } + + public static ZonedDateTime of(LocalDateTime localDateTime, ZoneId zone) { + return ofLocal(localDateTime, zone, null); + } + + public static ZonedDateTime of(int year, int month, int dayOfMonth, int hour, int minute, int second, + int nanoOfSecond, ZoneId zone) { + return ofLocal(LocalDateTime.of(year, month, dayOfMonth, hour, minute, second, nanoOfSecond), zone, null); + } + + /// `localDateTime` in `zone`. A wall clock time the zone skips is moved + /// forward by the length of the gap; one it passes twice takes + /// `preferredOffset` when that is one of its two offsets, and the earlier + /// offset otherwise. + public static ZonedDateTime ofLocal(LocalDateTime localDateTime, ZoneId zone, ZoneOffset preferredOffset) { + if (localDateTime == null) { + throw new NullPointerException("localDateTime"); + } + if (zone == null) { + throw new NullPointerException("zone"); + } + if (zone instanceof ZoneOffset) { + return new ZonedDateTime(localDateTime, (ZoneOffset) zone, zone); + } + long local = localDateTime.toEpochSecond(ZoneOffset.UTC); + // The offsets a day either side; a zone changes its offset far less + // often than that, so these are the offset before any transition + // touching this wall clock time and the offset after it. + ZoneOffset before = offsetAt(local - 86400L, zone); + ZoneOffset after = offsetAt(local + 86400L, zone); + boolean beforeValid = offsetAt(local - before.getTotalSeconds(), zone).equals(before); + boolean afterValid = offsetAt(local - after.getTotalSeconds(), zone).equals(after); + if (before.equals(after) || (beforeValid && !afterValid)) { + return new ZonedDateTime(localDateTime, before, zone); + } + if (afterValid && !beforeValid) { + return new ZonedDateTime(localDateTime, after, zone); + } + if (beforeValid) { + // An overlap: the wall clock time happens at both offsets. + ZoneOffset chosen = after.equals(preferredOffset) ? after : before; + return new ZonedDateTime(localDateTime, chosen, zone); + } + // A gap: the wall clock time never happens; move past it. + LocalDateTime shifted = localDateTime.plusSeconds(after.getTotalSeconds() - before.getTotalSeconds()); + return new ZonedDateTime(shifted, after, zone); + } + + /// The two offsets of the overlap this wall clock time falls in, earlier + /// first, or null when it falls in none. + private ZoneOffset[] overlap() { + if (zone instanceof ZoneOffset) { + return null; + } + long local = dateTime.toEpochSecond(ZoneOffset.UTC); + ZoneOffset before = offsetAt(local - 86400L, zone); + ZoneOffset after = offsetAt(local + 86400L, zone); + if (before.equals(after) || !offsetAt(local - before.getTotalSeconds(), zone).equals(before) + || !offsetAt(local - after.getTotalSeconds(), zone).equals(after)) { + return null; + } + return new ZoneOffset[] {before, after}; } - public static ZonedDateTime of(LocalDateTime dateTime, ZoneId zone) { - Instant instant = dateTime.toInstant(DateTimeSupport.offsetFromInstant(dateTime.toInstant(ZoneOffset.UTC), zone)); - return ofInstant(instant, zone); + /// At a wall clock time the zone passes twice, the first of the two + /// moments; otherwise this. + public ZonedDateTime withEarlierOffsetAtOverlap() { + ZoneOffset[] both = overlap(); + return both == null || both[0].equals(offset) ? this : new ZonedDateTime(dateTime, both[0], zone); + } + + /// At a wall clock time the zone passes twice, the second of the two + /// moments; otherwise this. + public ZonedDateTime withLaterOffsetAtOverlap() { + ZoneOffset[] both = overlap(); + return both == null || both[1].equals(offset) ? this : new ZonedDateTime(dateTime, both[1], zone); + } + + private static ZoneOffset offsetAt(long epochSecond, ZoneId zone) { + return DateTimeSupport.offsetFromInstant(Instant.ofEpochSecond(epochSecond), zone); } public static ZonedDateTime ofInstant(Instant instant, ZoneId zone) { + if (instant == null) { + throw new NullPointerException("instant"); + } + if (zone == null) { + throw new NullPointerException("zone"); + } + return create(instant.getEpochSecond(), instant.getNano(), zone); + } + + /// The moment `localDateTime` is at `offset`, in `zone`. + public static ZonedDateTime ofInstant(LocalDateTime localDateTime, ZoneOffset offset, ZoneId zone) { + return create(localDateTime.toEpochSecond(offset), localDateTime.getNano(), zone); + } + + private static ZonedDateTime create(long epochSecond, int nanoOfSecond, ZoneId zone) { + Instant instant = Instant.ofEpochSecond(epochSecond, nanoOfSecond); ZoneOffset offset = DateTimeSupport.offsetFromInstant(instant, zone); - LocalDateTime local = LocalDateTime.ofInstant(instant, zone); - return new ZonedDateTime(local, zone, offset); + LocalDateTime ldt = LocalDateTime.ofEpochSecond(epochSecond, nanoOfSecond, offset); + return new ZonedDateTime(ldt, offset, zone); + } + + /// The date and time and zone of `temporal`. + public static ZonedDateTime from(TemporalAccessor temporal) { + if (temporal instanceof ZonedDateTime) { + return (ZonedDateTime) temporal; + } + if (temporal instanceof OffsetDateTime) { + return ((OffsetDateTime) temporal).toZonedDateTime(); + } + if (temporal == null) { + throw new NullPointerException("temporal"); + } + throw new DateTimeException("Unable to obtain ZonedDateTime from TemporalAccessor: " + temporal + + " of type " + temporal.getClass().getName()); } public static ZonedDateTime parse(CharSequence text) { - return (ZonedDateTime) DateTimeFormatter.ISO_ZONED_DATE_TIME.parse(text); + return parse(text, DateTimeFormatter.ISO_ZONED_DATE_TIME); } public static ZonedDateTime parse(CharSequence text, DateTimeFormatter formatter) { - return (ZonedDateTime) formatter.parse(text); + if (formatter == null) { + throw new NullPointerException("formatter"); + } + TemporalAccessor parsed = formatter.parse(text); + try { + return from(parsed); + } catch (DateTimeException ex) { + throw TimeArithmetic.unparsable(text, ex); + } } + @Override public LocalDateTime toLocalDateTime() { return dateTime; } + @Override + public LocalDate toLocalDate() { + return dateTime.toLocalDate(); + } + + @Override + public LocalTime toLocalTime() { + return dateTime.toLocalTime(); + } + + @Override public ZoneId getZone() { return zone; } + @Override public ZoneOffset getOffset() { return offset; } + @Override public Instant toInstant() { - return dateTime.toInstant(offset); + return Instant.ofEpochSecond(toEpochSecond(), dateTime.getNano()); + } + + @Override + public long toEpochSecond() { + return dateTime.toEpochSecond(offset); + } + + /// This date and time with its offset and no zone. + public OffsetDateTime toOffsetDateTime() { + return OffsetDateTime.of(dateTime, offset); + } + + public int getYear() { + return dateTime.getYear(); + } + + public int getMonthValue() { + return dateTime.getMonthValue(); + } + + public Month getMonth() { + return dateTime.getMonth(); + } + + public int getDayOfMonth() { + return dateTime.getDayOfMonth(); + } + + public int getDayOfYear() { + return dateTime.getDayOfYear(); + } + + public DayOfWeek getDayOfWeek() { + return dateTime.getDayOfWeek(); + } + + public int getHour() { + return dateTime.getHour(); + } + + public int getMinute() { + return dateTime.getMinute(); + } + + public int getSecond() { + return dateTime.getSecond(); + } + + public int getNano() { + return dateTime.getNano(); + } + + /// The same moment in `zone`. + public ZonedDateTime withZoneSameInstant(ZoneId zone) { + if (zone == null) { + throw new NullPointerException("zone"); + } + return this.zone.equals(zone) ? this : create(dateTime.toEpochSecond(offset), dateTime.getNano(), zone); + } + + /// The same wall clock time in `zone`, keeping the offset if it can. + public ZonedDateTime withZoneSameLocal(ZoneId zone) { + if (zone == null) { + throw new NullPointerException("zone"); + } + return this.zone.equals(zone) ? this : ofLocal(dateTime, zone, offset); + } + + private ZonedDateTime resolveLocal(LocalDateTime newDateTime) { + return ofLocal(newDateTime, zone, offset); + } + + private ZonedDateTime resolveInstant(LocalDateTime newDateTime) { + return ofInstant(newDateTime, offset, zone); + } + + public ZonedDateTime withYear(int year) { + return resolveLocal(dateTime.withYear(year)); + } + + public ZonedDateTime withMonth(int month) { + return resolveLocal(dateTime.withMonth(month)); + } + + public ZonedDateTime withDayOfMonth(int dayOfMonth) { + return resolveLocal(dateTime.withDayOfMonth(dayOfMonth)); + } + + public ZonedDateTime withDayOfYear(int dayOfYear) { + return resolveLocal(dateTime.withDayOfYear(dayOfYear)); + } + + /// This date and time with its zone replaced by its current offset. + public ZonedDateTime withFixedOffsetZone() { + return zone.equals(offset) ? this : new ZonedDateTime(dateTime, offset, offset); + } + + public ZonedDateTime withHour(int hour) { + return resolveLocal(dateTime.withHour(hour)); + } + + public ZonedDateTime withMinute(int minute) { + return resolveLocal(dateTime.withMinute(minute)); + } + + public ZonedDateTime withSecond(int second) { + return resolveLocal(dateTime.withSecond(second)); + } + + public ZonedDateTime withNano(int nanoOfSecond) { + return resolveLocal(dateTime.withNano(nanoOfSecond)); + } + + public ZonedDateTime truncatedTo(TemporalUnit unit) { + return resolveLocal(dateTime.truncatedTo(unit)); + } + + @Override + public boolean isSupported(TemporalUnit unit) { + if (unit instanceof ChronoUnit) { + return unit != ChronoUnit.FOREVER; + } + return unit != null && unit.isSupportedBy(this); + } + + @Override + public ZonedDateTime plus(long amountToAdd, TemporalUnit unit) { + if (unit instanceof ChronoUnit) { + if (unit.isDateBased()) { + return resolveLocal(dateTime.plus(amountToAdd, unit)); + } + return resolveInstant(dateTime.plus(amountToAdd, unit)); + } + if (unit == null) { + throw new NullPointerException("unit"); + } + return unit.addTo(this, amountToAdd); + } + + @Override + public ZonedDateTime minus(long amountToSubtract, TemporalUnit unit) { + return amountToSubtract == Long.MIN_VALUE ? plus(Long.MAX_VALUE, unit).plus(1, unit) + : plus(-amountToSubtract, unit); + } + + @Override + public ZonedDateTime plus(TemporalAmount amount) { + if (amount instanceof Period) { + return resolveLocal(dateTime.plus(amount)); + } + if (amount == null) { + throw new NullPointerException("amountToAdd"); + } + return (ZonedDateTime) amount.addTo(this); + } + + @Override + public ZonedDateTime minus(TemporalAmount amount) { + if (amount instanceof Period) { + return resolveLocal(dateTime.minus(amount)); + } + if (amount == null) { + throw new NullPointerException("amountToSubtract"); + } + return (ZonedDateTime) amount.subtractFrom(this); + } + + public ZonedDateTime plusYears(long years) { + return resolveLocal(dateTime.plusYears(years)); + } + + public ZonedDateTime plusMonths(long months) { + return resolveLocal(dateTime.plusMonths(months)); + } + + public ZonedDateTime plusWeeks(long weeks) { + return resolveLocal(dateTime.plusWeeks(weeks)); + } + + public ZonedDateTime plusDays(long days) { + return resolveLocal(dateTime.plusDays(days)); + } + + public ZonedDateTime plusHours(long hours) { + return resolveInstant(dateTime.plusHours(hours)); + } + + public ZonedDateTime plusMinutes(long minutes) { + return resolveInstant(dateTime.plusMinutes(minutes)); + } + + public ZonedDateTime plusSeconds(long seconds) { + return resolveInstant(dateTime.plusSeconds(seconds)); + } + + public ZonedDateTime plusNanos(long nanos) { + return resolveInstant(dateTime.plusNanos(nanos)); + } + + public ZonedDateTime minusYears(long years) { + return years == Long.MIN_VALUE ? plusYears(Long.MAX_VALUE).plusYears(1) : plusYears(-years); + } + + public ZonedDateTime minusMonths(long months) { + return months == Long.MIN_VALUE ? plusMonths(Long.MAX_VALUE).plusMonths(1) : plusMonths(-months); + } + + public ZonedDateTime minusWeeks(long weeks) { + return weeks == Long.MIN_VALUE ? plusWeeks(Long.MAX_VALUE).plusWeeks(1) : plusWeeks(-weeks); + } + + public ZonedDateTime minusDays(long days) { + return days == Long.MIN_VALUE ? plusDays(Long.MAX_VALUE).plusDays(1) : plusDays(-days); + } + + public ZonedDateTime minusHours(long hours) { + return hours == Long.MIN_VALUE ? plusHours(Long.MAX_VALUE).plusHours(1) : plusHours(-hours); + } + + public ZonedDateTime minusMinutes(long minutes) { + return minutes == Long.MIN_VALUE ? plusMinutes(Long.MAX_VALUE).plusMinutes(1) : plusMinutes(-minutes); + } + + public ZonedDateTime minusSeconds(long seconds) { + return seconds == Long.MIN_VALUE ? plusSeconds(Long.MAX_VALUE).plusSeconds(1) : plusSeconds(-seconds); + } + + public ZonedDateTime minusNanos(long nanos) { + return nanos == Long.MIN_VALUE ? plusNanos(Long.MAX_VALUE).plusNanos(1) : plusNanos(-nanos); + } + + /// The whole units of `unit` from this value to `endExclusive`: date + /// units on the wall clock of this zone, time units on the time line. + @Override + public long until(Temporal endExclusive, TemporalUnit unit) { + ZonedDateTime end = from(endExclusive); + if (unit instanceof ChronoUnit) { + if (unit.isDateBased()) { + end = end.withZoneSameInstant(zone); + return dateTime.until(end.dateTime, unit); + } + LocalDateTime endHere = LocalDateTime.ofEpochSecond(end.toEpochSecond(), end.getNano(), offset); + return dateTime.until(endHere, unit); + } + if (unit == null) { + throw new NullPointerException("unit"); + } + return unit.between(this, end); } public String format(DateTimeFormatter formatter) { return formatter.format(this); } - public int compareTo(ZonedDateTime other) { - int cmp = toInstant().compareTo(other.toInstant()); - if (cmp != 0) { - return cmp; + @Override + public int compareTo(ChronoZonedDateTime other) { + long a = toEpochSecond(); + long b = other.toEpochSecond(); + if (a != b) { + return a < b ? -1 : 1; + } + int cmp = toLocalTime().getNano() - other.toLocalTime().getNano(); + if (cmp == 0) { + cmp = toLocalDateTime().compareTo(other.toLocalDateTime()); + if (cmp == 0) { + cmp = getZone().getId().compareTo(other.getZone().getId()); + } } - cmp = dateTime.compareTo(other.dateTime); - return cmp != 0 ? cmp : zone.getId().compareTo(other.zone.getId()); + return cmp; } + @Override public boolean equals(Object obj) { - return obj instanceof ZonedDateTime && compareTo((ZonedDateTime) obj) == 0; + if (this == obj) { + return true; + } + if (!(obj instanceof ZonedDateTime)) { + return false; + } + ZonedDateTime other = (ZonedDateTime) obj; + return dateTime.equals(other.dateTime) && offset.equals(other.offset) && zone.equals(other.zone); } + @Override public int hashCode() { - return dateTime.hashCode() * 31 + zone.hashCode(); + int z = zone.hashCode(); + return dateTime.hashCode() ^ offset.hashCode() ^ ((z << 3) | (z >>> 29)); } + /// The JDK's text: the date and time, the offset, and the zone in + /// brackets when it is a region rather than the offset itself. + @Override public String toString() { - return DateTimeFormatter.ISO_ZONED_DATE_TIME.format(this); - } - - ZoneId getZoneForFormatting() { - return zone; + String str = dateTime.toString() + offset.toString(); + if (!offset.equals(zone)) { + str += '[' + zone.toString() + ']'; + } + return str; } } diff --git a/Ports/CLDC11/src/java/time/chrono/ChronoLocalDate.java b/Ports/CLDC11/src/java/time/chrono/ChronoLocalDate.java new file mode 100644 index 00000000000..1af4fd36deb --- /dev/null +++ b/Ports/CLDC11/src/java/time/chrono/ChronoLocalDate.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.time.chrono; + +import java.time.temporal.Temporal; + +/// A date of a calendar. The ISO calendar's `LocalDate` is the one there is. +public interface ChronoLocalDate extends Temporal, Comparable { + /// The days since 1970-01-01. + long toEpochDay(); + + /// Whether the year of this date is a leap year. + boolean isLeapYear(); + + /// The days in the month of this date. + int lengthOfMonth(); + + /// The days in the year of this date. + default int lengthOfYear() { + return isLeapYear() ? 366 : 365; + } + + /// Whether this date comes after `other`. + default boolean isAfter(ChronoLocalDate other) { + return toEpochDay() > other.toEpochDay(); + } + + /// Whether this date comes before `other`. + default boolean isBefore(ChronoLocalDate other) { + return toEpochDay() < other.toEpochDay(); + } + + /// Whether this date is the same day as `other`. + default boolean isEqual(ChronoLocalDate other) { + return toEpochDay() == other.toEpochDay(); + } +} diff --git a/Ports/CLDC11/src/java/time/chrono/ChronoLocalDateTime.java b/Ports/CLDC11/src/java/time/chrono/ChronoLocalDateTime.java new file mode 100644 index 00000000000..0f6651198ac --- /dev/null +++ b/Ports/CLDC11/src/java/time/chrono/ChronoLocalDateTime.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.time.chrono; + +import java.time.Instant; +import java.time.LocalTime; +import java.time.ZoneOffset; +import java.time.temporal.Temporal; + +/// A date and a time of day of a calendar, with no zone. +public interface ChronoLocalDateTime + extends Temporal, Comparable> { + /// The date. + D toLocalDate(); + + /// The time of day. + LocalTime toLocalTime(); + + /// The seconds since 1970-01-01T00:00Z of this date and time at `offset`. + default long toEpochSecond(ZoneOffset offset) { + long epochDay = toLocalDate().toEpochDay(); + long secs = epochDay * 86400L + toLocalTime().toSecondOfDay(); + return secs - offset.getTotalSeconds(); + } + + /// The moment this date and time is at `offset`. + default Instant toInstant(ZoneOffset offset) { + return Instant.ofEpochSecond(toEpochSecond(offset), toLocalTime().getNano()); + } + + /// Whether this comes after `other` on the local time line. + default boolean isAfter(ChronoLocalDateTime other) { + long thisDay = toLocalDate().toEpochDay(); + long otherDay = other.toLocalDate().toEpochDay(); + return thisDay > otherDay + || (thisDay == otherDay && toLocalTime().toNanoOfDay() > other.toLocalTime().toNanoOfDay()); + } + + /// Whether this comes before `other` on the local time line. + default boolean isBefore(ChronoLocalDateTime other) { + long thisDay = toLocalDate().toEpochDay(); + long otherDay = other.toLocalDate().toEpochDay(); + return thisDay < otherDay + || (thisDay == otherDay && toLocalTime().toNanoOfDay() < other.toLocalTime().toNanoOfDay()); + } + + /// Whether this is the same date and time as `other`. + default boolean isEqual(ChronoLocalDateTime other) { + return toLocalTime().toNanoOfDay() == other.toLocalTime().toNanoOfDay() + && toLocalDate().toEpochDay() == other.toLocalDate().toEpochDay(); + } +} diff --git a/Ports/CLDC11/src/java/time/chrono/ChronoZonedDateTime.java b/Ports/CLDC11/src/java/time/chrono/ChronoZonedDateTime.java new file mode 100644 index 00000000000..3b875999605 --- /dev/null +++ b/Ports/CLDC11/src/java/time/chrono/ChronoZonedDateTime.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.time.chrono; + +import java.time.Instant; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.temporal.Temporal; + +/// A date and a time of day in a time zone. +public interface ChronoZonedDateTime + extends Temporal, Comparable> { + /// The date and time on the wall clock. + ChronoLocalDateTime toLocalDateTime(); + + /// The offset from UTC. + ZoneOffset getOffset(); + + /// The zone. + ZoneId getZone(); + + /// The date. + default D toLocalDate() { + return toLocalDateTime().toLocalDate(); + } + + /// The time of day. + default LocalTime toLocalTime() { + return toLocalDateTime().toLocalTime(); + } + + /// The seconds since 1970-01-01T00:00Z. + default long toEpochSecond() { + return toLocalDateTime().toEpochSecond(getOffset()); + } + + /// The moment this is. + default Instant toInstant() { + return Instant.ofEpochSecond(toEpochSecond(), toLocalTime().getNano()); + } + + /// Whether this moment comes after `other`. + default boolean isAfter(ChronoZonedDateTime other) { + long thisSecond = toEpochSecond(); + long otherSecond = other.toEpochSecond(); + return thisSecond > otherSecond + || (thisSecond == otherSecond && toLocalTime().getNano() > other.toLocalTime().getNano()); + } + + /// Whether this moment comes before `other`. + default boolean isBefore(ChronoZonedDateTime other) { + long thisSecond = toEpochSecond(); + long otherSecond = other.toEpochSecond(); + return thisSecond < otherSecond + || (thisSecond == otherSecond && toLocalTime().getNano() < other.toLocalTime().getNano()); + } + + /// Whether this is the same moment as `other`. + default boolean isEqual(ChronoZonedDateTime other) { + return toEpochSecond() == other.toEpochSecond() + && toLocalTime().getNano() == other.toLocalTime().getNano(); + } +} diff --git a/Ports/CLDC11/src/java/time/format/DateTimeFormatter.java b/Ports/CLDC11/src/java/time/format/DateTimeFormatter.java index f7a25bd2d1c..fbf37066dab 100644 --- a/Ports/CLDC11/src/java/time/format/DateTimeFormatter.java +++ b/Ports/CLDC11/src/java/time/format/DateTimeFormatter.java @@ -22,8 +22,8 @@ */ package java.time.format; -import java.text.ParseException; -import java.text.SimpleDateFormat; +import java.time.DateTimeException; +import java.time.DayOfWeek; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; @@ -33,305 +33,1599 @@ import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.temporal.TemporalAccessor; -import java.util.Date; +import java.time.temporal.TemporalQuery; +import java.time.temporal.UnsupportedTemporalTypeException; +import java.util.ArrayList; import java.util.Locale; -import java.util.TimeZone; +/// Formats and parses dates and times: the ISO formats as constants, and +/// any other through [#ofPattern(String)]. +/// +/// Patterns follow the JDK's letters and counts. Supported: `G` era, `u` +/// year, `y` year of era, `M`/`L` month, `d` day of month, `D` day of +/// year, `Q`/`q` quarter, `E` day of week, `a` AM/PM, `h`, `K`, `k`, `H` +/// hours, `m` minutes, `s` seconds, `S` fraction of a second, `n` nano of +/// second, `N` nano of day, `A` milli of day, `VV` zone id, `z` (the zone +/// id), `O`, `X`, `x` and `Z` offsets, quoted text, and `[...]` optional +/// sections. Text is English whatever the locale. The week-based letters +/// (`Y`, `w`, `W`, `e`, `c`, `F`, `g`), `v`, `B` and `p` are refused. +/// +/// A pattern parses as the JDK's smart resolver does: a day of month past +/// the end of its month becomes the last day, an hour alone is that hour +/// on the dot, and a two-digit year is one of 2000 to 2099. The ISO +/// formats resolve strictly. public final class DateTimeFormatter { - private static final int TYPE_PATTERN = 0; - private static final int TYPE_ISO_LOCAL_DATE = 1; - private static final int TYPE_ISO_LOCAL_TIME = 2; - private static final int TYPE_ISO_LOCAL_DATE_TIME = 3; - private static final int TYPE_ISO_OFFSET_DATE_TIME = 4; - private static final int TYPE_ISO_ZONED_DATE_TIME = 5; - private static final int TYPE_ISO_INSTANT = 6; - - public static final DateTimeFormatter ISO_LOCAL_DATE = new DateTimeFormatter(TYPE_ISO_LOCAL_DATE, null, null); - public static final DateTimeFormatter ISO_LOCAL_TIME = new DateTimeFormatter(TYPE_ISO_LOCAL_TIME, null, null); - public static final DateTimeFormatter ISO_LOCAL_DATE_TIME = new DateTimeFormatter(TYPE_ISO_LOCAL_DATE_TIME, null, null); - public static final DateTimeFormatter ISO_OFFSET_DATE_TIME = new DateTimeFormatter(TYPE_ISO_OFFSET_DATE_TIME, null, null); - public static final DateTimeFormatter ISO_ZONED_DATE_TIME = new DateTimeFormatter(TYPE_ISO_ZONED_DATE_TIME, null, null); - public static final DateTimeFormatter ISO_INSTANT = new DateTimeFormatter(TYPE_ISO_INSTANT, null, null); - - private final int type; + private static final int KIND_PATTERN = 0; + private static final int KIND_LOCAL_DATE = 1; + private static final int KIND_LOCAL_TIME = 2; + private static final int KIND_LOCAL_DATE_TIME = 3; + private static final int KIND_OFFSET_DATE_TIME = 4; + private static final int KIND_ZONED_DATE_TIME = 5; + private static final int KIND_INSTANT = 6; + + private static final long SECONDS_PER_10000_YEARS = 146097L * 25L * 86400L; + private static final long SECONDS_0000_TO_1970 = ((146097L * 5L) - (30L * 365L + 7L)) * 86400L; + + public static final DateTimeFormatter ISO_LOCAL_DATE = new DateTimeFormatter(KIND_LOCAL_DATE, null, null, + null, null); + public static final DateTimeFormatter ISO_LOCAL_TIME = new DateTimeFormatter(KIND_LOCAL_TIME, null, null, + null, null); + public static final DateTimeFormatter ISO_LOCAL_DATE_TIME = new DateTimeFormatter(KIND_LOCAL_DATE_TIME, null, + null, null, null); + public static final DateTimeFormatter ISO_OFFSET_DATE_TIME = new DateTimeFormatter(KIND_OFFSET_DATE_TIME, null, + null, null, null); + public static final DateTimeFormatter ISO_ZONED_DATE_TIME = new DateTimeFormatter(KIND_ZONED_DATE_TIME, null, + null, null, null); + public static final DateTimeFormatter ISO_INSTANT = new DateTimeFormatter(KIND_INSTANT, null, null, null, null); + + private final int kind; private final String pattern; + private final Token[] tokens; private final Locale locale; + private final ZoneId zone; - private DateTimeFormatter(int type, String pattern, Locale locale) { - this.type = type; + private DateTimeFormatter(int kind, String pattern, Token[] tokens, Locale locale, ZoneId zone) { + this.kind = kind; this.pattern = pattern; + this.tokens = tokens; this.locale = locale; + this.zone = zone; } public static DateTimeFormatter ofPattern(String pattern) { - return new DateTimeFormatter(TYPE_PATTERN, pattern, null); + return ofPattern(pattern, Locale.getDefault()); } public static DateTimeFormatter ofPattern(String pattern, Locale locale) { - return new DateTimeFormatter(TYPE_PATTERN, pattern, locale); + if (pattern == null) { + throw new NullPointerException("pattern"); + } + if (locale == null) { + throw new NullPointerException("locale"); + } + return new DateTimeFormatter(KIND_PATTERN, pattern, compile(pattern), locale, null); + } + + /// This formatter with `zone` overriding the zone of what it formats and + /// supplying one to what it parses; null removes the override. + public DateTimeFormatter withZone(ZoneId zone) { + if (zone == null ? this.zone == null : zone.equals(this.zone)) { + return this; + } + return new DateTimeFormatter(kind, pattern, tokens, locale, zone); + } + + /// The zone given to [#withZone(ZoneId)], or null. + public ZoneId getZone() { + return zone; + } + + public DateTimeFormatter withLocale(Locale locale) { + if (locale == null) { + throw new NullPointerException("locale"); + } + if (locale.equals(this.locale)) { + return this; + } + return new DateTimeFormatter(kind, pattern, tokens, locale, zone); + } + + public Locale getLocale() { + return locale == null ? Locale.getDefault() : locale; } + // ---- formatting ---- + public String format(TemporalAccessor temporal) { if (temporal == null) { - throw new NullPointerException(); - } - switch (type) { - case TYPE_PATTERN: - return formatPattern(temporal); - case TYPE_ISO_LOCAL_DATE: - return formatLocalDate((LocalDate) temporal); - case TYPE_ISO_LOCAL_TIME: - return formatLocalTime((LocalTime) temporal); - case TYPE_ISO_LOCAL_DATE_TIME: - return formatLocalDate((LocalDateTime) temporal) + "T" + formatLocalTime(((LocalDateTime) temporal).toLocalTime()); - case TYPE_ISO_OFFSET_DATE_TIME: - return formatLocalDateTime(((OffsetDateTime) temporal).toLocalDateTime()) + formatOffset(((OffsetDateTime) temporal).getOffset()); - case TYPE_ISO_ZONED_DATE_TIME: - ZonedDateTime zdt = (ZonedDateTime) temporal; - return formatLocalDateTime(zdt.toLocalDateTime()) + formatOffset(zdt.getOffset()) + "[" + zdt.getZone().getId() + "]"; - case TYPE_ISO_INSTANT: - return formatInstant((Instant) temporal); + throw new NullPointerException("temporal"); + } + Fields f = Fields.of(temporal, zone); + StringBuilder out = new StringBuilder(32); + switch (kind) { + case KIND_PATTERN: + formatPattern(f, out); + break; + case KIND_LOCAL_DATE: + isoDate(f, out); + break; + case KIND_LOCAL_TIME: + isoTime(f, out); + break; + case KIND_LOCAL_DATE_TIME: + isoDate(f, out); + out.append('T'); + isoTime(f, out); + break; + case KIND_OFFSET_DATE_TIME: + isoDate(f, out); + out.append('T'); + isoTime(f, out); + out.append(f.offset("OffsetSeconds").getId()); + break; + case KIND_ZONED_DATE_TIME: + isoDate(f, out); + out.append('T'); + isoTime(f, out); + out.append(f.offset("OffsetSeconds").getId()); + if (f.zone != null && !(f.zone instanceof ZoneOffset)) { + out.append('[').append(f.zone.getId()).append(']'); + } + break; default: - throw new IllegalStateException(); + isoInstant(f, out); + break; } + return out.toString(); } - public TemporalAccessor parse(CharSequence text) { - switch (type) { - case TYPE_ISO_LOCAL_DATE: - return parseLocalDate(text.toString()); - case TYPE_ISO_LOCAL_TIME: - return parseLocalTime(text.toString()); - case TYPE_ISO_LOCAL_DATE_TIME: - return parseLocalDateTime(text.toString()); - case TYPE_ISO_OFFSET_DATE_TIME: - return parseOffsetDateTime(text.toString()); - case TYPE_ISO_ZONED_DATE_TIME: - return parseZonedDateTime(text.toString()); - case TYPE_ISO_INSTANT: - return parseInstant(text.toString()); - case TYPE_PATTERN: - return parseLocalDateTime(text.toString()); - default: - throw new IllegalStateException(); + private static void isoDate(Fields f, StringBuilder out) { + LocalDate date = f.date("Year"); + appendYear(date.getYear(), out); + out.append('-'); + pad(out, date.getMonthValue(), 2); + out.append('-'); + pad(out, date.getDayOfMonth(), 2); + } + + /// A year as the ISO formats print it: at least four digits, a sign + /// when negative, and a `+` when more than four digits. + private static void appendYear(int year, StringBuilder out) { + if (year < 0) { + out.append('-'); + pad(out, -(long) year, 4); + } else { + if (year > 9999) { + out.append('+'); + } + pad(out, year, 4); + } + } + + private static void isoTime(Fields f, StringBuilder out) { + LocalTime time = f.time("HourOfDay"); + pad(out, time.getHour(), 2); + out.append(':'); + pad(out, time.getMinute(), 2); + out.append(':'); + pad(out, time.getSecond(), 2); + int nano = time.getNano(); + if (nano != 0) { + String digits = String.valueOf(1000000000 + nano).substring(1); + int end = 9; + while (digits.charAt(end - 1) == '0') { + end--; + } + out.append('.').append(digits.substring(0, end)); } } - LocalDate parseLocalDate(String text) { - if (type == TYPE_PATTERN) { - ParsedPatternResult parsed = parsePattern(text, pattern, ZoneOffset.UTC, locale); - return LocalDateTime.ofInstant(parsed.instant, ZoneOffset.UTC).toLocalDate(); + private static void isoInstant(Fields f, StringBuilder out) { + if (f.epochSecond == null) { + throw new UnsupportedTemporalTypeException("Unsupported field: InstantSeconds"); } - if (type != TYPE_ISO_LOCAL_DATE) { - throw new DateTimeParseException("Formatter does not produce LocalDate", text, 0); + // An instant reaches a year past what LocalDate holds, so the year + // is split off in whole 10000 year cycles first, as the JDK does. + long seconds = f.epochSecond.longValue(); + long shifted = seconds + SECONDS_0000_TO_1970; + long cycles = shifted / SECONDS_PER_10000_YEARS; + if (shifted < 0 && cycles * SECONDS_PER_10000_YEARS != shifted) { + cycles--; } - return LocalDate.of(parseInt(text, 0, 4), parseInt(text, 5, 7), parseInt(text, 8, 10)); + long within = seconds - cycles * SECONDS_PER_10000_YEARS; + LocalDateTime utc = LocalDateTime.ofEpochSecond(within, f.nano, ZoneOffset.UTC); + appendYear((int) (cycles * 10000 + utc.getYear()), out); + out.append('-'); + pad(out, utc.getMonthValue(), 2); + out.append('-'); + pad(out, utc.getDayOfMonth(), 2); + out.append('T'); + pad(out, utc.getHour(), 2); + out.append(':'); + pad(out, utc.getMinute(), 2); + out.append(':'); + pad(out, utc.getSecond(), 2); + int nano = utc.getNano(); + if (nano != 0) { + // Whole groups of three digits: millis, micros or nanos. + String digits = String.valueOf(1000000000 + nano).substring(1); + if (nano % 1000000 == 0) { + digits = digits.substring(0, 3); + } else if (nano % 1000 == 0) { + digits = digits.substring(0, 6); + } + out.append('.').append(digits); + } + out.append('Z'); } - LocalTime parseLocalTime(String text) { - if (type == TYPE_PATTERN) { - ParsedPatternResult parsed = parsePattern(text, pattern, ZoneOffset.UTC, locale); - return LocalDateTime.ofInstant(parsed.instant, ZoneOffset.UTC).toLocalTime(); + private void formatPattern(Fields f, StringBuilder out) { + ArrayList starts = new ArrayList(); + int i = 0; + while (i < tokens.length) { + Token t = tokens[i]; + if (t.kind == Token.OPTIONAL_START) { + starts.add(Integer.valueOf(out.length())); + i++; + continue; + } + if (t.kind == Token.OPTIONAL_END) { + starts.remove(starts.size() - 1); + i++; + continue; + } + if (t.kind == Token.LITERAL) { + out.append(t.text); + i++; + continue; + } + try { + formatField(t, f, out); + i++; + } catch (UnsupportedTemporalTypeException missing) { + if (starts.isEmpty()) { + throw missing; + } + // A field an optional section needs is missing: drop the + // section, whatever of it was printed already included. + out.setLength(starts.remove(starts.size() - 1).intValue()); + i = skipSection(i); + } } - if (type != TYPE_ISO_LOCAL_TIME) { - throw new DateTimeParseException("Formatter does not produce LocalTime", text, 0); + } + + /// The index after the end of the optional section `i` is inside. + private int skipSection(int i) { + int depth = 1; + i++; + while (i < tokens.length && depth > 0) { + if (tokens[i].kind == Token.OPTIONAL_START) { + depth++; + } else if (tokens[i].kind == Token.OPTIONAL_END) { + depth--; + } + i++; } - int hour = parseInt(text, 0, 2); - int minute = parseInt(text, 3, 5); - int second = text.length() >= 8 ? parseInt(text, 6, 8) : 0; - int nano = 0; - int dot = text.indexOf('.'); - if (dot > 0) { - String frac = text.substring(dot + 1); - while (frac.length() < 9) { - frac += "0"; + return i; + } + + private static void formatField(Token t, Fields f, StringBuilder out) { + int count = t.count; + switch (t.letter) { + case 'G': { + int year = f.date("Era").getYear(); + text(out, count, year >= 1 ? ERA_FULL[1] : ERA_FULL[0], year >= 1 ? "AD" : "BC"); + return; + } + case 'u': + case 'y': { + int year = f.date(t.letter == 'u' ? "Year" : "YearOfEra").getYear(); + long value = t.letter == 'y' && year < 1 ? 1L - year : year; + if (count == 2) { + pad(out, Math.abs(value) % 100, 2); + } else if (count < 4) { + number(out, value, count, 19, SIGN_NORMAL, "Year"); + } else { + number(out, value, count, 19, SIGN_EXCEEDS_PAD, "Year"); + } + return; + } + case 'M': + case 'L': { + int month = f.date("MonthOfYear").getMonthValue(); + if (count >= 3) { + text(out, count, MONTHS[month - 1], null); + } else { + small(out, month, count); + } + return; + } + case 'Q': + case 'q': { + int quarter = (f.date("QuarterOfYear").getMonthValue() - 1) / 3 + 1; + if (count == 3) { + out.append('Q').append(quarter); + } else if (count == 4) { + out.append(QUARTERS[quarter - 1]).append(" quarter"); + } else if (count == 5) { + out.append(quarter); + } else { + small(out, quarter, count); + } + return; + } + case 'd': + small(out, f.date("DayOfMonth").getDayOfMonth(), count); + return; + case 'D': { + int doy = f.date("DayOfYear").getDayOfYear(); + if (count == 2) { + number(out, doy, 2, 3, SIGN_NOT_NEGATIVE, "DayOfYear"); + } else { + small(out, doy, count); + } + return; } - if (frac.length() > 9) { - frac = frac.substring(0, 9); + case 'E': { + DayOfWeek dow = f.dayOfWeek(); + text(out, count, DAYS[dow.getValue() - 1], null); + return; } - nano = Integer.parseInt(frac); + case 'a': + out.append(f.time("AmPmOfDay").getHour() < 12 ? "AM" : "PM"); + return; + case 'h': { + int h = f.time("ClockHourOfAmPm").getHour() % 12; + small(out, h == 0 ? 12 : h, count); + return; + } + case 'K': + small(out, f.time("HourOfAmPm").getHour() % 12, count); + return; + case 'k': { + int h = f.time("ClockHourOfDay").getHour(); + small(out, h == 0 ? 24 : h, count); + return; + } + case 'H': + small(out, f.time("HourOfDay").getHour(), count); + return; + case 'm': + small(out, f.time("MinuteOfHour").getMinute(), count); + return; + case 's': + small(out, f.time("SecondOfMinute").getSecond(), count); + return; + case 'S': + out.append(String.valueOf(1000000000 + f.nanoOfSecond()).substring(1, 1 + count)); + return; + case 'n': + small(out, f.nanoOfSecond(), count); + return; + case 'N': + small(out, f.time("NanoOfDay").toNanoOfDay(), count); + return; + case 'A': + small(out, f.time("MilliOfDay").toNanoOfDay() / 1000000L, count); + return; + case 'V': + case 'z': + if (f.zone == null) { + throw new UnsupportedTemporalTypeException("Unable to extract ZoneId from temporal"); + } + out.append(f.zone.getId()); + return; + case 'O': + localizedOffset(out, f.offset("OffsetSeconds").getTotalSeconds(), count == 4); + return; + case 'X': + offset(out, f.offset("OffsetSeconds").getTotalSeconds(), count == 1 ? 1 : count, "Z"); + return; + case 'x': + offset(out, f.offset("OffsetSeconds").getTotalSeconds(), count, OFFSET_ZERO[count - 1]); + return; + case 'Z': + if (count == 4) { + localizedOffset(out, f.offset("OffsetSeconds").getTotalSeconds(), true); + } else if (count == 5) { + offset(out, f.offset("OffsetSeconds").getTotalSeconds(), 5, "Z"); + } else { + offset(out, f.offset("OffsetSeconds").getTotalSeconds(), 2, "+0000"); + } + return; + default: + throw new IllegalStateException(String.valueOf(t.letter)); } - return LocalTime.of(hour, minute, second, nano); } - LocalDateTime parseLocalDateTime(String text) { - if (type == TYPE_PATTERN) { - ParsedPatternResult parsed = parsePattern(text, pattern, ZoneOffset.UTC, locale); - return LocalDateTime.ofInstant(parsed.instant, ZoneOffset.UTC); + private static final String[] MONTHS = {"January", "February", "March", "April", "May", "June", "July", + "August", "September", "October", "November", "December"}; + private static final String[] DAYS = {"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", + "Sunday"}; + private static final String[] ERA_FULL = {"Before Christ", "Anno Domini"}; + private static final String[] QUARTERS = {"1st", "2nd", "3rd", "4th"}; + private static final String[] OFFSET_ZERO = {"+00", "+0000", "+00:00", "+0000", "+00:00"}; + + private static final int SIGN_NORMAL = 0; + private static final int SIGN_NOT_NEGATIVE = 1; + private static final int SIGN_EXCEEDS_PAD = 2; + + /// Text in the style the count asks for: short (1 to 3), full (4) or + /// narrow (5). `shortForm` stands in when the short form is not the + /// first three letters. + private static void text(StringBuilder out, int count, String full, String shortForm) { + if (count == 4) { + out.append(full); + } else if (count == 5) { + out.append(full.charAt(0)); + } else { + out.append(shortForm != null ? shortForm : full.substring(0, 3)); } - if (type != TYPE_ISO_LOCAL_DATE_TIME) { - throw new DateTimeParseException("Formatter does not produce LocalDateTime", text, 0); + } + + /// A field of one letter (any width) or two (exactly two digits). + private static void small(StringBuilder out, long value, int count) { + if (count == 1) { + out.append(value); + } else { + pad(out, value, count); } - int t = text.indexOf('T'); - return LocalDateTime.of((LocalDate) ISO_LOCAL_DATE.parse(text.substring(0, t)), - (LocalTime) ISO_LOCAL_TIME.parse(text.substring(t + 1))); } - OffsetDateTime parseOffsetDateTime(String text) { - if (type == TYPE_PATTERN) { - ParsedPatternResult parsed = parsePattern(text, pattern, ZoneOffset.UTC, locale); - return OffsetDateTime.ofInstant(parsed.instant, parsed.zone); + private static void number(StringBuilder out, long value, int minWidth, int maxWidth, int sign, String field) { + String digits = String.valueOf(Math.abs(value)); + if (value == Long.MIN_VALUE || digits.length() > maxWidth) { + throw new DateTimeException("Field " + field + " cannot be printed as the value " + value + + " exceeds the maximum print width of " + maxWidth); } - if (type != TYPE_ISO_OFFSET_DATE_TIME) { - throw new DateTimeParseException("Formatter does not produce OffsetDateTime", text, 0); + if (value < 0) { + if (sign == SIGN_NOT_NEGATIVE) { + throw new DateTimeException("Field " + field + " cannot be printed as the value " + value + + " cannot be negative according to the SignStyle"); + } + out.append('-'); + } else if (sign == SIGN_EXCEEDS_PAD && minWidth < 19 && digits.length() > minWidth) { + out.append('+'); } - int idx = Math.max(text.lastIndexOf('+'), text.lastIndexOf('-')); - if (text.endsWith("Z")) { - idx = text.length() - 1; + for (int i = digits.length(); i < minWidth; i++) { + out.append('0'); } - LocalDateTime ldt = (LocalDateTime) ISO_LOCAL_DATE_TIME.parse(text.substring(0, idx)); - ZoneOffset offset = text.endsWith("Z") ? ZoneOffset.UTC : ZoneOffset.of(text.substring(idx)); - return OffsetDateTime.of(ldt, offset); + out.append(digits); } - ZonedDateTime parseZonedDateTime(String text) { - if (type == TYPE_PATTERN) { - ParsedPatternResult parsed = parsePattern(text, pattern, ZoneId.systemDefault(), locale); - return ZonedDateTime.ofInstant(parsed.instant, parsed.zone); + private static void pad(StringBuilder out, long value, int width) { + String digits = String.valueOf(value); + for (int i = digits.length(); i < width; i++) { + out.append('0'); } - if (type != TYPE_ISO_ZONED_DATE_TIME) { - throw new DateTimeParseException("Formatter does not produce ZonedDateTime", text, 0); + out.append(digits); + } + + /// An offset as the `X`, `x` and `Z` letters print it. `style` is the + /// count of `X`: 1 `+HHmm`, 2 `+HHMM`, 3 `+HH:MM`, 4 `+HHMMss`, 5 + /// `+HH:MM:ss`. + private static void offset(StringBuilder out, int total, int style, String zeroText) { + if (total == 0) { + out.append(zeroText); + return; + } + int abs = Math.abs(total); + int hours = abs / 3600; + int minutes = (abs / 60) % 60; + int seconds = abs % 60; + boolean colon = style == 3 || style == 5; + out.append(total < 0 ? '-' : '+'); + pad(out, hours, 2); + if (style == 1 && minutes == 0) { + return; + } + if (colon) { + out.append(':'); } - int zoneStart = text.indexOf('['); - int offsetIdx = Math.max(text.lastIndexOf('+'), text.lastIndexOf('-')); - if (text.indexOf('Z') > 0 && (offsetIdx < 0 || text.indexOf('Z') < zoneStart)) { - offsetIdx = text.indexOf('Z'); + pad(out, minutes, 2); + if (style >= 4 && seconds != 0) { + if (colon) { + out.append(':'); + } + pad(out, seconds, 2); } - LocalDateTime ldt = (LocalDateTime) ISO_LOCAL_DATE_TIME.parse(text.substring(0, offsetIdx)); - ZoneOffset offset = text.charAt(offsetIdx) == 'Z' ? ZoneOffset.UTC : ZoneOffset.of(text.substring(offsetIdx, zoneStart)); - ZoneId zone = ZoneId.of(text.substring(zoneStart + 1, text.length() - 1)); - return ZonedDateTime.ofInstant(ldt.toInstant(offset), zone); } - Instant parseInstant(String text) { - if (type != TYPE_ISO_INSTANT) { - throw new DateTimeParseException("Formatter does not produce Instant", text, 0); + /// `GMT+8` (short) or `GMT+08:00` (full); `GMT` alone for zero. + private static void localizedOffset(StringBuilder out, int total, boolean full) { + out.append("GMT"); + if (total == 0) { + return; + } + int abs = Math.abs(total); + int hours = abs / 3600; + int minutes = (abs / 60) % 60; + int seconds = abs % 60; + out.append(total < 0 ? '-' : '+'); + if (full) { + pad(out, hours, 2); + out.append(':'); + pad(out, minutes, 2); + if (seconds != 0) { + out.append(':'); + pad(out, seconds, 2); + } + } else { + out.append(hours); + if (minutes != 0 || seconds != 0) { + out.append(':'); + pad(out, minutes, 2); + if (seconds != 0) { + out.append(':'); + pad(out, seconds, 2); + } + } } - OffsetDateTime odt = ISO_OFFSET_DATE_TIME.parseOffsetDateTime(text.endsWith("Z") - ? text.substring(0, text.length() - 1) + "+00:00" - : text); - return odt.toInstant(); } - private String formatPattern(TemporalAccessor temporal) { - ZoneId zone; - Instant instant; - if (temporal instanceof Instant) { - instant = (Instant) temporal; - zone = ZoneOffset.UTC; - } else if (temporal instanceof LocalDateTime) { - instant = ((LocalDateTime) temporal).toInstant(ZoneOffset.UTC); - zone = ZoneOffset.UTC; - } else if (temporal instanceof OffsetDateTime) { - OffsetDateTime offsetDateTime = (OffsetDateTime) temporal; - instant = offsetDateTime.toInstant(); - zone = offsetDateTime.getOffset(); - } else if (temporal instanceof ZonedDateTime) { - ZonedDateTime zonedDateTime = (ZonedDateTime) temporal; - instant = zonedDateTime.toInstant(); - zone = zonedDateTime.getZone(); - } else { - throw new IllegalArgumentException("Unsupported temporal type"); + // ---- parsing ---- + + public TemporalAccessor parse(CharSequence text) { + if (text == null) { + throw new NullPointerException("text"); + } + String s = text.toString(); + Parsed p = new Parsed(s); + switch (kind) { + case KIND_PATTERN: + parsePattern(p); + break; + case KIND_LOCAL_DATE: + parseIsoDate(p); + break; + case KIND_LOCAL_TIME: + parseIsoTime(p); + break; + case KIND_LOCAL_DATE_TIME: + parseIsoDateTime(p); + break; + case KIND_OFFSET_DATE_TIME: + parseIsoDateTime(p); + parseOffsetId(p); + break; + case KIND_ZONED_DATE_TIME: + parseIsoDateTime(p); + parseOffsetId(p); + if (p.pos < s.length() && s.charAt(p.pos) == '[') { + p.pos++; + parseZoneId(p); + p.literal(']'); + } + break; + default: + Instant instant = parseIsoInstant(p); + // With a zone of its own the formatter places the moment + // in it, so the result is a zoned date and time as well. + return zone == null ? instant : ZonedDateTime.ofInstant(instant, zone); + } + if (p.pos < s.length()) { + throw new DateTimeParseException("Text '" + s + "' could not be parsed, unparsed text found at index " + + p.pos, s, p.pos); } - SimpleDateFormat sdf = newFormat(pattern, zone, locale); - TimeZone original = TimeZone.getDefault(); try { - if (zone != null) { - TimeZone.setDefault(toTimeZone(zone)); + return p.resolve(zone, kind != KIND_PATTERN); + } catch (DateTimeParseException ex) { + throw ex; + } catch (DateTimeException ex) { + throw new DateTimeParseException("Text '" + s + "' could not be parsed: " + ex.getMessage(), s, 0, ex); + } + } + + /// Parses `text` and hands the result to `query`, as in + /// `formatter.parse(text, LocalDate::from)`. + public T parse(CharSequence text, TemporalQuery query) { + if (query == null) { + throw new NullPointerException("query"); + } + TemporalAccessor parsed = parse(text); + try { + return query.queryFrom(parsed); + } catch (DateTimeParseException ex) { + throw ex; + } catch (RuntimeException ex) { + String s = text.toString(); + throw new DateTimeParseException("Text '" + s + "' could not be parsed: " + ex.getMessage(), s, 0, ex); + } + } + + private static void parseIsoDate(Parsed p) { + int start = p.pos; + boolean negative = false; + boolean signed = false; + if (p.pos < p.text.length() && (p.text.charAt(p.pos) == '+' || p.text.charAt(p.pos) == '-')) { + negative = p.text.charAt(p.pos) == '-'; + signed = true; + p.pos++; + } + int digitsStart = p.pos; + long year = p.digits(4, 10); + if (p.pos - digitsStart > 4 && !signed) { + p.fail(start); + } + if (signed && !negative && p.pos - digitsStart <= 4) { + p.fail(start); + } + p.set(F_YEAR, negative ? -year : year); + p.literal('-'); + p.set(F_MONTH, p.digits(2, 2)); + p.literal('-'); + p.set(F_DAY, p.digits(2, 2)); + } + + private static void parseIsoTime(Parsed p) { + p.set(F_HOUR, p.digits(2, 2)); + p.literal(':'); + p.set(F_MINUTE, p.digits(2, 2)); + if (p.pos < p.text.length() && p.text.charAt(p.pos) == ':') { + p.pos++; + p.set(F_SECOND, p.digits(2, 2)); + if (p.pos < p.text.length() && p.text.charAt(p.pos) == '.') { + p.pos++; + int from = p.pos; + long fraction = p.digits(0, 9); + int width = p.pos - from; + for (int i = width; i < 9; i++) { + fraction *= 10; + } + p.set(F_NANO, fraction); } - return sdf.format(new Date(instant.toEpochMilli())); - } finally { - TimeZone.setDefault(original); } } - private static SimpleDateFormat newFormat(String pattern, ZoneId zone, Locale locale) { - return new SimpleDateFormat(pattern); + private static void parseIsoDateTime(Parsed p) { + parseIsoDate(p); + if (p.pos >= p.text.length() || (p.text.charAt(p.pos) != 'T' && p.text.charAt(p.pos) != 't')) { + p.fail(p.pos); + } + p.pos++; + parseIsoTime(p); + } + + /// `Z`, or an offset of hours with optional minutes and seconds, with + /// or without colons. + private static void parseOffsetId(Parsed p) { + if (p.pos < p.text.length() && (p.text.charAt(p.pos) == 'Z' || p.text.charAt(p.pos) == 'z')) { + p.pos++; + p.offset = ZoneOffset.UTC; + return; + } + p.offset = ZoneOffset.ofTotalSeconds(p.offsetSeconds()); } - private static ParsedPatternResult parsePattern(String text, String pattern, ZoneId defaultZone, Locale locale) { - TimeZone original = TimeZone.getDefault(); + private static void parseZoneId(Parsed p) { + int start = p.pos; + while (p.pos < p.text.length()) { + char c = p.text.charAt(p.pos); + if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '~' || c == '/' + || c == '.' || c == '_' || c == '+' || c == '-' || c == ':') { + p.pos++; + } else { + break; + } + } + if (p.pos == start) { + p.fail(start); + } try { - if (defaultZone != null) { - TimeZone.setDefault(toTimeZone(defaultZone)); + p.zone = ZoneId.of(p.text.substring(start, p.pos)); + } catch (DateTimeException ex) { + p.fail(start); + } + } + + private Instant parseIsoInstant(Parsed p) { + parseIsoDate(p); + if (p.pos >= p.text.length() || (p.text.charAt(p.pos) != 'T' && p.text.charAt(p.pos) != 't')) { + p.fail(p.pos); + } + p.pos++; + p.set(F_HOUR, p.digits(2, 2)); + p.literal(':'); + p.set(F_MINUTE, p.digits(2, 2)); + p.literal(':'); + p.set(F_SECOND, p.digits(2, 2)); + long nano = 0; + if (p.pos < p.text.length() && p.text.charAt(p.pos) == '.') { + p.pos++; + int from = p.pos; + nano = p.digits(0, 9); + for (int i = p.pos - from; i < 9; i++) { + nano *= 10; } - SimpleDateFormat sdf = newFormat(pattern, defaultZone, locale); - Date date = sdf.parse(text); - Instant instant = Instant.ofEpochMilli(date.getTime()); - ZoneId zone = defaultZone == null ? ZoneOffset.UTC : defaultZone; - return new ParsedPatternResult(instant, zone); - } catch (ParseException err) { - throw new DateTimeParseException(err.getMessage(), text, 0); - } finally { - TimeZone.setDefault(original); + } + parseOffsetId(p); + if (p.pos < p.text.length()) { + throw new DateTimeParseException("Text '" + p.text + "' could not be parsed, unparsed text found at index " + + p.pos, p.text, p.pos); + } + try { + int hour = (int) p.values[F_HOUR]; + int second = (int) p.values[F_SECOND]; + long dayAdjust = 0; + if (hour == 24 && p.values[F_MINUTE] == 0 && second == 0 && nano == 0) { + hour = 0; + dayAdjust = 1; + } + if (second == 60) { + // A leap second reads as the last second of its minute. + second = 59; + } + LocalDate date = LocalDate.of((int) p.values[F_YEAR], (int) p.values[F_MONTH], (int) p.values[F_DAY]) + .plusDays(dayAdjust); + LocalTime time = LocalTime.of(hour, (int) p.values[F_MINUTE], second, (int) nano); + return LocalDateTime.of(date, time).toInstant(p.offset); + } catch (DateTimeException ex) { + throw new DateTimeParseException("Text '" + p.text + "' could not be parsed: " + ex.getMessage(), p.text, + 0, ex); } } - private static final class ParsedPatternResult { - final Instant instant; - final ZoneId zone; + private void parsePattern(Parsed p) { + parseTokens(p, 0, tokens.length); + } - ParsedPatternResult(Instant instant, ZoneId zone) { - this.instant = instant; - this.zone = zone; + /// Parses tokens `from` to `to`; an optional section that fails is + /// undone and skipped. + private void parseTokens(Parsed p, int from, int to) { + int i = from; + while (i < to) { + Token t = tokens[i]; + if (t.kind == Token.OPTIONAL_START) { + int end = skipSection(i) - 1; + Parsed saved = p.copy(); + try { + parseTokens(p, i + 1, end); + } catch (DateTimeParseException ex) { + p.restore(saved); + } + i = end + 1; + continue; + } + if (t.kind == Token.OPTIONAL_END) { + i++; + continue; + } + if (t.kind == Token.LITERAL) { + for (int c = 0; c < t.text.length(); c++) { + p.literal(t.text.charAt(c)); + } + i++; + continue; + } + parseField(p, t, reserved(i + 1, to)); + i++; } } - private static int parseInt(String text, int start, int end) { - return Integer.parseInt(text.substring(start, end)); + /// The digits the fixed-width numbers directly after token `i` need, + /// which a variable-width number before them leaves for them. + private int reserved(int i, int to) { + int total = 0; + while (i < to && tokens[i].kind == Token.FIELD) { + int width = fixedWidth(tokens[i]); + if (width <= 0) { + break; + } + total += width; + i++; + } + return total; } - private static TimeZone toTimeZone(ZoneId zone) { - if (zone instanceof ZoneOffset) { - ZoneOffset offset = (ZoneOffset) zone; - return TimeZone.getTimeZone(offset.getId().equals("Z") ? "GMT" : "GMT" + offset.getId()); + /// The width of a number that is always printed with exactly that many + /// digits, or 0. + private static int fixedWidth(Token t) { + switch (t.letter) { + case 'y': + case 'u': + return t.count == 2 ? 2 : 0; + case 'M': + case 'L': + case 'Q': + case 'q': + return t.count == 2 ? 2 : 0; + case 'd': + case 'H': + case 'h': + case 'K': + case 'k': + case 'm': + case 's': + return t.count == 2 ? 2 : 0; + case 'D': + return t.count == 3 ? 3 : 0; + case 'S': + return t.count; + default: + return 0; } - return TimeZone.getTimeZone(zone.getId()); } - private static String pad(int value, int length) { - String s = String.valueOf(Math.abs(value)); - StringBuffer out = new StringBuffer(); - if (value < 0) { - out.append('-'); + private static void parseField(Parsed p, Token t, int reserve) { + int count = t.count; + switch (t.letter) { + case 'G': + p.set(F_ERA, p.oneOf(count == 4 ? ERA_FULL : (count == 5 ? new String[] {"B", "A"} + : new String[] {"BC", "AD"}))); + return; + case 'u': + case 'y': { + int field = t.letter == 'u' ? F_YEAR : F_YEAR_OF_ERA; + if (count == 2) { + p.set(field, 2000 + p.digits(2, 2)); + } else { + boolean negative = false; + int start = p.pos; + if (p.pos < p.text.length() && (p.text.charAt(p.pos) == '-' + || (count >= 4 && p.text.charAt(p.pos) == '+'))) { + negative = p.text.charAt(p.pos) == '-'; + p.pos++; + } + int digitsStart = p.pos; + long value = p.digits(count, Math.max(count, 19 - reserve), reserve); + if (count >= 4 && !negative && (p.pos - digitsStart > count) != (start != digitsStart)) { + p.fail(start); + } + if (negative && t.letter == 'y') { + p.fail(start); + } + p.set(field, negative ? -value : value); + } + return; + } + case 'M': + case 'L': + if (count >= 3) { + p.set(F_MONTH, 1 + p.oneOf(styled(MONTHS, count))); + } else { + p.set(F_MONTH, p.number(count, reserve)); + } + return; + case 'Q': + case 'q': + if (count == 3) { + p.literal('Q'); + p.set(F_QUARTER, p.digits(1, 1)); + } else if (count == 4) { + p.set(F_QUARTER, 1 + p.oneOf(new String[] {"1st quarter", "2nd quarter", "3rd quarter", + "4th quarter"})); + } else if (count == 5) { + p.set(F_QUARTER, p.digits(1, 1)); + } else { + p.set(F_QUARTER, p.number(count, reserve)); + } + return; + case 'd': + p.set(F_DAY, p.number(count, reserve)); + return; + case 'D': + p.set(F_DAY_OF_YEAR, count == 2 ? p.digits(2, 3, reserve) : p.number(count, reserve)); + return; + case 'E': + p.set(F_DAY_OF_WEEK, 1 + p.oneOf(styled(DAYS, count))); + return; + case 'a': + p.set(F_AMPM, p.oneOf(new String[] {"AM", "PM"})); + return; + case 'h': + p.set(F_CLOCK_HOUR_AMPM, p.number(count, reserve)); + return; + case 'K': + p.set(F_HOUR_AMPM, p.number(count, reserve)); + return; + case 'k': + p.set(F_CLOCK_HOUR, p.number(count, reserve)); + return; + case 'H': + p.set(F_HOUR, p.number(count, reserve)); + return; + case 'm': + p.set(F_MINUTE, p.number(count, reserve)); + return; + case 's': + p.set(F_SECOND, p.number(count, reserve)); + return; + case 'S': { + long fraction = p.digits(count, count); + for (int i = count; i < 9; i++) { + fraction *= 10; + } + p.set(F_NANO, fraction); + return; + } + case 'n': + p.set(F_NANO, p.number(count, reserve)); + return; + case 'N': + p.set(F_NANO_OF_DAY, p.number(count, reserve)); + return; + case 'A': + p.set(F_MILLI_OF_DAY, p.number(count, reserve)); + return; + case 'V': + case 'z': + parseZoneId(p); + return; + case 'O': { + int start = p.pos; + p.literal('G'); + p.literal('M'); + p.literal('T'); + if (p.pos < p.text.length() && (p.text.charAt(p.pos) == '+' || p.text.charAt(p.pos) == '-')) { + boolean negative = p.text.charAt(p.pos) == '-'; + p.pos++; + int hours = (int) p.digits(1, 2); + int minutes = 0; + int seconds = 0; + if (p.pos < p.text.length() && p.text.charAt(p.pos) == ':') { + p.pos++; + minutes = (int) p.digits(2, 2); + if (p.pos < p.text.length() && p.text.charAt(p.pos) == ':') { + p.pos++; + seconds = (int) p.digits(2, 2); + } + } + int total = hours * 3600 + minutes * 60 + seconds; + p.setOffset(negative ? -total : total, start); + } else { + p.setOffset(0, start); + } + return; + } + case 'X': + case 'Z': + if ((t.letter == 'X' || count == 5) && p.pos < p.text.length() && p.text.charAt(p.pos) == 'Z') { + p.pos++; + p.offset = ZoneOffset.UTC; + return; + } + p.setOffset(p.offsetSeconds(), p.pos); + return; + case 'x': + p.setOffset(p.offsetSeconds(), p.pos); + return; + default: + throw new IllegalStateException(String.valueOf(t.letter)); } - for (int i = s.length(); i < length; i++) { - out.append('0'); + } + + /// The names of `full` in the style `count` asks for. + private static String[] styled(String[] full, int count) { + String[] out = new String[full.length]; + for (int i = 0; i < full.length; i++) { + out[i] = count == 4 ? full[i] : (count == 5 ? full[i].substring(0, 1) : full[i].substring(0, 3)); } - out.append(s); - return out.toString(); + return out; } - private static String formatLocalDate(LocalDate date) { - return pad(date.getYear(), 4) + "-" + pad(date.getMonthValue(), 2) + "-" + pad(date.getDayOfMonth(), 2); + // ---- what was read, and what it makes ---- + + private static final int F_ERA = 0; + private static final int F_YEAR = 1; + private static final int F_YEAR_OF_ERA = 2; + private static final int F_MONTH = 3; + private static final int F_DAY = 4; + private static final int F_DAY_OF_YEAR = 5; + private static final int F_DAY_OF_WEEK = 6; + private static final int F_QUARTER = 7; + private static final int F_AMPM = 8; + private static final int F_HOUR = 9; + private static final int F_CLOCK_HOUR = 10; + private static final int F_HOUR_AMPM = 11; + private static final int F_CLOCK_HOUR_AMPM = 12; + private static final int F_MINUTE = 13; + private static final int F_SECOND = 14; + private static final int F_NANO = 15; + private static final int F_NANO_OF_DAY = 16; + private static final int F_MILLI_OF_DAY = 17; + private static final int FIELD_COUNT = 18; + + private static final String[] FIELD_NAMES = {"Era", "Year", "YearOfEra", "MonthOfYear", "DayOfMonth", + "DayOfYear", "DayOfWeek", "QuarterOfYear", "AmPmOfDay", "HourOfDay", "ClockHourOfDay", "HourOfAmPm", + "ClockHourOfAmPm", "MinuteOfHour", "SecondOfMinute", "NanoOfSecond", "NanoOfDay", "MilliOfDay"}; + private static final long[] FIELD_MIN = {0, -999999999L, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0}; + private static final long[] FIELD_MAX = {1, 999999999L, 1000000000L, 12, 31, 366, 7, 4, 1, 23, 24, 11, 12, 59, + 59, 999999999L, 86399999999999L, 86399999L}; + + /// The state of one parse: where it is in the text and every field and + /// offset read so far. + private static final class Parsed implements TemporalAccessor { + final String text; + int pos; + final long[] values = new long[FIELD_COUNT]; + final boolean[] present = new boolean[FIELD_COUNT]; + /// Whether the time read was 24:00, which ends the day it was read + /// with and so moves a date read beside it on by one. + private boolean endOfDay; + ZoneOffset offset; + ZoneId zone; + + Parsed(String text) { + this.text = text; + } + + Parsed copy() { + Parsed c = new Parsed(text); + c.pos = pos; + System.arraycopy(values, 0, c.values, 0, FIELD_COUNT); + System.arraycopy(present, 0, c.present, 0, FIELD_COUNT); + c.offset = offset; + c.zone = zone; + return c; + } + + void restore(Parsed saved) { + pos = saved.pos; + System.arraycopy(saved.values, 0, values, 0, FIELD_COUNT); + System.arraycopy(saved.present, 0, present, 0, FIELD_COUNT); + offset = saved.offset; + zone = saved.zone; + } + + void fail(int index) { + throw new DateTimeParseException("Text '" + text + "' could not be parsed at index " + index, text, index); + } + + void set(int field, long value) { + if (present[field] && values[field] != value) { + throw new DateTimeParseException("Text '" + text + "' could not be parsed: Conflict found: " + + FIELD_NAMES[field] + " " + values[field] + " differs from " + FIELD_NAMES[field] + " " + + value, text, 0); + } + values[field] = value; + present[field] = true; + } + + void setOffset(int seconds, int start) { + try { + offset = ZoneOffset.ofTotalSeconds(seconds); + } catch (DateTimeException ex) { + fail(start); + } + } + + void literal(char c) { + if (pos >= text.length() || text.charAt(pos) != c) { + fail(pos); + } + pos++; + } + + long digits(int min, int max) { + return digits(min, max, 0); + } + + /// Between `min` and `max` digits, leaving `reserve` of them for + /// the fixed-width numbers that follow when there are more. + long digits(int min, int max, int reserve) { + int start = pos; + int end = start; + while (end < text.length() && end - start < max && text.charAt(end) >= '0' && text.charAt(end) <= '9') { + end++; + } + if (reserve > 0 && end - start - reserve >= min) { + end -= reserve; + } + if (end - start < min) { + fail(start); + } + long value = 0; + for (int i = start; i < end; i++) { + value = value * 10 + (text.charAt(i) - '0'); + if (value < 0) { + fail(start); + } + } + pos = end; + return value; + } + + /// A number of one letter (one or more digits) or two (exactly two). + long number(int count, int reserve) { + return count == 1 ? digits(1, 19, reserve) : digits(count, count); + } + + int oneOf(String[] names) { + int best = -1; + int bestLength = 0; + for (int i = 0; i < names.length; i++) { + String n = names[i]; + if (n.length() > bestLength && text.regionMatches(pos, n, 0, n.length())) { + best = i; + bestLength = n.length(); + } + } + if (best < 0) { + fail(pos); + } + pos += bestLength; + return best; + } + + /// `+HH`, `+HHMM`, `+HH:MM`, `+HHMMss` or `+HH:MM:ss`. + int offsetSeconds() { + int start = pos; + if (pos >= text.length() || (text.charAt(pos) != '+' && text.charAt(pos) != '-')) { + fail(start); + } + boolean negative = text.charAt(pos) == '-'; + pos++; + int hours = (int) digits(2, 2); + int minutes = 0; + int seconds = 0; + boolean colon = pos < text.length() && text.charAt(pos) == ':'; + if (colon) { + pos++; + } + if (pos + 1 < text.length() && isDigit(pos) && isDigit(pos + 1)) { + minutes = (int) digits(2, 2); + if (colon ? pos < text.length() && text.charAt(pos) == ':' : true) { + int mark = pos; + if (colon) { + pos++; + } + if (pos + 1 < text.length() && isDigit(pos) && isDigit(pos + 1)) { + seconds = (int) digits(2, 2); + } else { + pos = mark; + } + } + } else if (colon) { + fail(pos); + } + if (minutes > 59 || seconds > 59) { + fail(start); + } + int total = hours * 3600 + minutes * 60 + seconds; + if (total > 18 * 3600) { + fail(start); + } + return negative ? -total : total; + } + + private boolean isDigit(int i) { + char c = text.charAt(i); + return c >= '0' && c <= '9'; + } + + private void check(int field, boolean strict) { + long v = values[field]; + if (!strict) { + // The smart resolver takes 24:00 as the end of the day and + // a clock hour of 0 as midnight, as the JDK's does. + if (field == F_HOUR && v == 24) { + return; + } + if ((field == F_CLOCK_HOUR || field == F_CLOCK_HOUR_AMPM) && v == 0) { + return; + } + } + if (present[field] && (v < FIELD_MIN[field] || v > FIELD_MAX[field])) { + throw new DateTimeException("Invalid value for " + FIELD_NAMES[field] + " (valid values " + + FIELD_MIN[field] + " - " + FIELD_MAX[field] + "): " + v); + } + } + + /// The date, time, offset and zone that were read, as the class + /// they make: a zoned or offset date and time, a date and time, a + /// date, a time, or this when they make none of those. + TemporalAccessor resolve(ZoneId overrideZone, boolean strict) { + for (int f = 0; f < FIELD_COUNT; f++) { + check(f, strict); + } + LocalDate date = resolveDate(strict); + LocalTime time = resolveTime(); + ZoneId z = zone != null ? zone : overrideZone; + if (date != null && time != null) { + LocalDateTime ldt = LocalDateTime.of(date, time); + if (endOfDay) { + ldt = ldt.plusDays(1); + } + if (offset != null) { + if (z == null) { + return OffsetDateTime.of(ldt, offset); + } + return ZonedDateTime.ofInstant(ldt.toInstant(offset), z); + } + if (z != null) { + return ZonedDateTime.of(ldt, z); + } + return ldt; + } + if (date != null) { + return date; + } + if (time != null) { + return time; + } + return this; + } + + private LocalDate resolveDate(boolean strict) { + Long year = null; + if (present[F_YEAR]) { + year = Long.valueOf(values[F_YEAR]); + } + if (present[F_YEAR_OF_ERA]) { + long yoe = values[F_YEAR_OF_ERA]; + long fromEra = !present[F_ERA] || values[F_ERA] == 1 ? yoe : 1 - yoe; + if (year != null && year.longValue() != fromEra) { + throw new DateTimeException("Conflict found: Year " + year + " differs from Year " + fromEra); + } + year = Long.valueOf(fromEra); + } else if (present[F_ERA] && year != null && (year.longValue() >= 1 ? 1 : 0) != values[F_ERA]) { + throw new DateTimeException("Conflict found: Era " + values[F_ERA] + " differs from year " + year); + } + LocalDate date = null; + if (year != null && present[F_MONTH] && present[F_DAY]) { + int y = (int) year.longValue(); + int m = (int) values[F_MONTH]; + int d = (int) values[F_DAY]; + if (!strict) { + int length = LocalDate.of(y, m, 1).lengthOfMonth(); + if (d > length) { + d = length; + } + } + date = LocalDate.of(y, m, d); + } else if (year != null && present[F_DAY_OF_YEAR]) { + date = LocalDate.ofYearDay((int) year.longValue(), (int) values[F_DAY_OF_YEAR]); + } + if (date != null) { + if (present[F_DAY_OF_WEEK] && date.getDayOfWeek().getValue() != values[F_DAY_OF_WEEK]) { + throw new DateTimeException("Conflict found: Field DayOfWeek " + date.getDayOfWeek().getValue() + + " differs from DayOfWeek " + values[F_DAY_OF_WEEK] + " derived from " + date); + } + if (present[F_QUARTER] && (date.getMonthValue() - 1) / 3 + 1 != values[F_QUARTER]) { + throw new DateTimeException("Conflict found: Field QuarterOfYear differs from " + date); + } + } + return date; + } + + private LocalTime resolveTime() { + Long hour = null; + if (present[F_HOUR]) { + hour = Long.valueOf(values[F_HOUR]); + } + if (present[F_CLOCK_HOUR]) { + long h = values[F_CLOCK_HOUR] == 24 ? 0 : values[F_CLOCK_HOUR]; + hour = conflict(hour, h); + } + if (present[F_AMPM]) { + if (present[F_CLOCK_HOUR_AMPM]) { + long h = values[F_CLOCK_HOUR_AMPM] == 12 ? 0 : values[F_CLOCK_HOUR_AMPM]; + hour = conflict(hour, values[F_AMPM] * 12 + h); + } + if (present[F_HOUR_AMPM]) { + hour = conflict(hour, values[F_AMPM] * 12 + values[F_HOUR_AMPM]); + } + } + if (hour == null) { + if (present[F_NANO_OF_DAY]) { + return LocalTime.ofNanoOfDay(values[F_NANO_OF_DAY]); + } + if (present[F_MILLI_OF_DAY]) { + return LocalTime.ofNanoOfDay(values[F_MILLI_OF_DAY] * 1000000L); + } + return null; + } + if (present[F_AMPM] && hour.longValue() / 12 != values[F_AMPM]) { + throw new DateTimeException("Conflict found: AmPmOfDay " + values[F_AMPM] + " differs from hour " + + hour); + } + boolean minute = present[F_MINUTE]; + boolean second = present[F_SECOND]; + boolean nano = present[F_NANO]; + if ((!minute && (second || nano)) || (minute && !second && nano)) { + return null; + } + if (hour.longValue() == 24) { + if ((minute && values[F_MINUTE] != 0) || (second && values[F_SECOND] != 0) + || (nano && values[F_NANO] != 0)) { + throw new DateTimeException("Invalid value for HourOfDay (valid values 0 - 23): 24"); + } + endOfDay = true; + return LocalTime.MIDNIGHT; + } + return LocalTime.of((int) hour.longValue(), minute ? (int) values[F_MINUTE] : 0, + second ? (int) values[F_SECOND] : 0, nano ? (int) values[F_NANO] : 0); + } + + private static Long conflict(Long hour, long value) { + if (hour != null && hour.longValue() != value) { + throw new DateTimeException("Conflict found: HourOfDay " + hour + " differs from HourOfDay " + value); + } + return Long.valueOf(value); + } + + public String toString() { + StringBuilder sb = new StringBuilder("{"); + boolean first = true; + for (int f = 0; f < FIELD_COUNT; f++) { + if (present[f]) { + if (!first) { + sb.append(", "); + } + sb.append(FIELD_NAMES[f]).append('=').append(values[f]); + first = false; + } + } + sb.append("},ISO"); + if (zone != null) { + sb.append(',').append(zone); + } else if (offset != null) { + sb.append(',').append(offset); + } + return sb.toString(); + } } - private static String formatLocalTime(LocalTime time) { - String base = pad(time.getHour(), 2) + ":" + pad(time.getMinute(), 2) + ":" + pad(time.getSecond(), 2); - if (time.getNano() == 0) { - return base; + // ---- what a value to format has ---- + + /// The date, time, offset and zone a value carries, after the + /// formatter's override zone is applied. + private static final class Fields { + LocalDate date; + LocalTime time; + ZoneOffset offsetValue; + ZoneId zone; + Long epochSecond; + int nano; + DayOfWeek dayOfWeek; + + static Fields of(TemporalAccessor temporal, ZoneId override) { + Fields f = new Fields(); + if (override != null) { + Instant instant = null; + if (temporal instanceof Instant) { + instant = (Instant) temporal; + } else if (temporal instanceof OffsetDateTime) { + instant = ((OffsetDateTime) temporal).toInstant(); + } else if (temporal instanceof ZonedDateTime && !((ZonedDateTime) temporal).getZone().equals(override)) { + instant = ((ZonedDateTime) temporal).toInstant(); + } + if (instant != null) { + temporal = ZonedDateTime.ofInstant(instant, override); + } + } + if (temporal instanceof ZonedDateTime) { + ZonedDateTime z = (ZonedDateTime) temporal; + f.date = z.toLocalDate(); + f.time = z.toLocalTime(); + f.offsetValue = z.getOffset(); + f.zone = z.getZone(); + f.epochSecond = Long.valueOf(z.toEpochSecond()); + f.nano = z.getNano(); + } else if (temporal instanceof OffsetDateTime) { + OffsetDateTime o = (OffsetDateTime) temporal; + f.date = o.toLocalDate(); + f.time = o.toLocalTime(); + f.offsetValue = o.getOffset(); + f.epochSecond = Long.valueOf(o.toEpochSecond()); + f.nano = o.getNano(); + } else if (temporal instanceof LocalDateTime) { + LocalDateTime l = (LocalDateTime) temporal; + f.date = l.toLocalDate(); + f.time = l.toLocalTime(); + f.nano = l.getNano(); + } else if (temporal instanceof LocalDate) { + f.date = (LocalDate) temporal; + } else if (temporal instanceof LocalTime) { + f.time = (LocalTime) temporal; + f.nano = f.time.getNano(); + } else if (temporal instanceof Instant) { + Instant i = (Instant) temporal; + f.epochSecond = Long.valueOf(i.getEpochSecond()); + f.nano = i.getNano(); + } else if (temporal instanceof DayOfWeek) { + f.dayOfWeek = (DayOfWeek) temporal; + } else if (temporal instanceof ZoneOffset) { + f.offsetValue = (ZoneOffset) temporal; + } + if (f.zone == null && override != null && !(temporal instanceof Instant)) { + f.zone = override; + } + return f; + } + + LocalDate date(String field) { + if (date == null) { + throw new UnsupportedTemporalTypeException("Unsupported field: " + field); + } + return date; + } + + LocalTime time(String field) { + if (time == null) { + throw new UnsupportedTemporalTypeException("Unsupported field: " + field); + } + return time; } - String frac = String.valueOf(1000000000L + time.getNano()).substring(1); - while (frac.endsWith("0")) { - frac = frac.substring(0, frac.length() - 1); + + ZoneOffset offset(String field) { + if (offsetValue == null) { + throw new UnsupportedTemporalTypeException("Unsupported field: " + field); + } + return offsetValue; + } + + DayOfWeek dayOfWeek() { + if (dayOfWeek != null) { + return dayOfWeek; + } + return date("DayOfWeek").getDayOfWeek(); + } + + int nanoOfSecond() { + if (time == null && epochSecond == null) { + throw new UnsupportedTemporalTypeException("Unsupported field: NanoOfSecond"); + } + return nano; } - return base + "." + frac; } - private static String formatLocalDate(LocalDateTime dateTime) { - return formatLocalDate(dateTime.toLocalDate()); + // ---- the pattern itself ---- + + private static final class Token { + static final int FIELD = 0; + static final int LITERAL = 1; + static final int OPTIONAL_START = 2; + static final int OPTIONAL_END = 3; + + final int kind; + final char letter; + final int count; + final String text; + + Token(int kind, char letter, int count, String text) { + this.kind = kind; + this.letter = letter; + this.count = count; + this.text = text; + } } - private static String formatLocalDateTime(LocalDateTime dateTime) { - return formatLocalDate(dateTime.toLocalDate()) + "T" + formatLocalTime(dateTime.toLocalTime()); + private static final String SUPPORTED = "GuyMLQqdDEahKkHmsSnNAVzOXxZ"; + private static final String KNOWN_UNSUPPORTED = "YwWecFgvBp"; + + private static Token[] compile(String pattern) { + ArrayList out = new ArrayList(); + int depth = 0; + int i = 0; + while (i < pattern.length()) { + char c = pattern.charAt(i); + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) { + int start = i; + while (i < pattern.length() && pattern.charAt(i) == c) { + i++; + } + int count = i - start; + if (KNOWN_UNSUPPORTED.indexOf(c) >= 0) { + throw new IllegalArgumentException("Pattern letter not supported here: " + c); + } + if (SUPPORTED.indexOf(c) < 0) { + throw new IllegalArgumentException("Unknown pattern letter: " + c); + } + validateCount(c, count); + out.add(new Token(Token.FIELD, c, count, null)); + } else if (c == '\'') { + int start = i++; + StringBuilder lit = new StringBuilder(); + boolean closed = false; + while (i < pattern.length()) { + char q = pattern.charAt(i); + if (q == '\'') { + if (i + 1 < pattern.length() && pattern.charAt(i + 1) == '\'') { + lit.append('\''); + i += 2; + continue; + } + closed = true; + i++; + break; + } + lit.append(q); + i++; + } + if (!closed) { + throw new IllegalArgumentException("Pattern ends with an incomplete string literal: " + pattern); + } + if (i - start == 2) { + lit.append('\''); + } + out.add(new Token(Token.LITERAL, '\0', 0, lit.toString())); + } else if (c == '[') { + depth++; + out.add(new Token(Token.OPTIONAL_START, '\0', 0, null)); + i++; + } else if (c == ']') { + if (depth == 0) { + throw new IllegalArgumentException("Pattern invalid as it contains ] without previous ["); + } + depth--; + out.add(new Token(Token.OPTIONAL_END, '\0', 0, null)); + i++; + } else if (c == '{' || c == '}' || c == '#') { + throw new IllegalArgumentException("Pattern includes reserved character: '" + c + "'"); + } else { + out.add(new Token(Token.LITERAL, '\0', 0, String.valueOf(c))); + i++; + } + } + while (depth-- > 0) { + out.add(new Token(Token.OPTIONAL_END, '\0', 0, null)); + } + return out.toArray(new Token[out.size()]); } - private static String formatOffset(ZoneOffset offset) { - return offset.getId(); + private static void validateCount(char c, int count) { + int max; + switch (c) { + case 'd': + case 'h': + case 'H': + case 'k': + case 'K': + case 'm': + case 's': + max = 2; + break; + case 'D': + max = 3; + break; + case 'a': + max = 1; + break; + case 'S': + max = 9; + break; + case 'z': + max = 4; + break; + case 'u': + case 'y': + case 'n': + case 'N': + case 'A': + max = 19; + break; + default: + max = 5; + break; + } + if (count > max) { + throw new IllegalArgumentException("Too many pattern letters: " + c); + } + if (c == 'V' && count != 2) { + throw new IllegalArgumentException("Pattern letter count must be 2: " + c); + } + if (c == 'O' && count != 1 && count != 4) { + throw new IllegalArgumentException("Pattern letter count must be 1 or 4: " + c); + } } - private static String formatInstant(Instant instant) { - LocalDateTime dateTime = LocalDateTime.ofInstant(instant, ZoneOffset.UTC); - return formatLocalDateTime(dateTime) + "Z"; + public String toString() { + switch (kind) { + case KIND_LOCAL_DATE: + return "ISO_LOCAL_DATE"; + case KIND_LOCAL_TIME: + return "ISO_LOCAL_TIME"; + case KIND_LOCAL_DATE_TIME: + return "ISO_LOCAL_DATE_TIME"; + case KIND_OFFSET_DATE_TIME: + return "ISO_OFFSET_DATE_TIME"; + case KIND_ZONED_DATE_TIME: + return "ISO_ZONED_DATE_TIME"; + case KIND_INSTANT: + return "ISO_INSTANT"; + default: + return pattern; + } } } diff --git a/Ports/CLDC11/src/java/time/temporal/ChronoUnit.java b/Ports/CLDC11/src/java/time/temporal/ChronoUnit.java new file mode 100644 index 00000000000..9ad00f84c95 --- /dev/null +++ b/Ports/CLDC11/src/java/time/temporal/ChronoUnit.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.time.temporal; + +import java.time.Duration; + +/// The units of the ISO calendar, with the JDK's order, names and lengths. +public enum ChronoUnit implements TemporalUnit { + /// A nanosecond. + NANOS("Nanos", 0L, 1), + /// A microsecond. + MICROS("Micros", 0L, 1000), + /// A millisecond. + MILLIS("Millis", 0L, 1000000), + /// A second. + SECONDS("Seconds", 1L, 0), + /// A minute. + MINUTES("Minutes", 60L, 0), + /// An hour. + HOURS("Hours", 3600L, 0), + /// Twelve hours. + HALF_DAYS("HalfDays", 43200L, 0), + /// A day. + DAYS("Days", 86400L, 0), + /// Seven days. + WEEKS("Weeks", 7 * 86400L, 0), + /// A month, estimated as a twelfth of a year. + MONTHS("Months", 31556952L / 12, 0), + /// A year, estimated as 365.2425 days. + YEARS("Years", 31556952L, 0), + /// Ten years. + DECADES("Decades", 31556952L * 10L, 0), + /// A hundred years. + CENTURIES("Centuries", 31556952L * 100L, 0), + /// A thousand years. + MILLENNIA("Millennia", 31556952L * 1000L, 0), + /// An era, estimated as a billion years. + ERAS("Eras", 31556952L * 1000000000L, 0), + /// Forever: longer than any other unit. + FOREVER("Forever", Long.MAX_VALUE, 999999999); + + private final String label; + private final long seconds; + private final int nanos; + + ChronoUnit(String label, long seconds, int nanos) { + this.label = label; + this.seconds = seconds; + this.nanos = nanos; + } + + @Override + public Duration getDuration() { + return Duration.ofSeconds(seconds, nanos); + } + + @Override + public boolean isDurationEstimated() { + return compareTo(DAYS) >= 0; + } + + @Override + public boolean isDateBased() { + return compareTo(DAYS) >= 0 && this != FOREVER; + } + + @Override + public boolean isTimeBased() { + return compareTo(DAYS) < 0; + } + + @Override + public boolean isSupportedBy(Temporal temporal) { + return temporal.isSupported(this); + } + + @Override + @SuppressWarnings("unchecked") + public R addTo(R temporal, long amount) { + return (R) temporal.plus(amount, this); + } + + @Override + public long between(Temporal temporal1Inclusive, Temporal temporal2Exclusive) { + return temporal1Inclusive.until(temporal2Exclusive, this); + } + + @Override + public String toString() { + return label; + } +} diff --git a/Ports/CLDC11/src/java/time/temporal/Temporal.java b/Ports/CLDC11/src/java/time/temporal/Temporal.java new file mode 100644 index 00000000000..3691dd73012 --- /dev/null +++ b/Ports/CLDC11/src/java/time/temporal/Temporal.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.time.temporal; + +/// A date, a time or a moment that can be moved by an amount of time and +/// measured against another of its kind. +public interface Temporal extends TemporalAccessor { + /// Whether this value can add or subtract `unit`. + boolean isSupported(TemporalUnit unit); + + /// This value moved forward by `amountToAdd` of `unit`. + Temporal plus(long amountToAdd, TemporalUnit unit); + + /// This value moved back by `amountToSubtract` of `unit`. + default Temporal minus(long amountToSubtract, TemporalUnit unit) { + if (amountToSubtract == Long.MIN_VALUE) { + return plus(Long.MAX_VALUE, unit).plus(1, unit); + } + return plus(-amountToSubtract, unit); + } + + /// This value moved forward by `amount`. + default Temporal plus(TemporalAmount amount) { + return amount.addTo(this); + } + + /// This value moved back by `amount`. + default Temporal minus(TemporalAmount amount) { + return amount.subtractFrom(this); + } + + /// The whole units of `unit` from this value to `endExclusive`. + long until(Temporal endExclusive, TemporalUnit unit); +} diff --git a/Ports/CLDC11/src/java/time/temporal/TemporalAccessor.java b/Ports/CLDC11/src/java/time/temporal/TemporalAccessor.java index 93d35a217ec..6eb2ce641fa 100644 --- a/Ports/CLDC11/src/java/time/temporal/TemporalAccessor.java +++ b/Ports/CLDC11/src/java/time/temporal/TemporalAccessor.java @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ package java.time.temporal; public interface TemporalAccessor { diff --git a/Ports/CLDC11/src/java/time/temporal/TemporalAmount.java b/Ports/CLDC11/src/java/time/temporal/TemporalAmount.java new file mode 100644 index 00000000000..5feea4d1152 --- /dev/null +++ b/Ports/CLDC11/src/java/time/temporal/TemporalAmount.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.time.temporal; + +import java.util.List; + +/// An amount of time, such as a `Duration` or a `Period`. +public interface TemporalAmount { + /// The amount of `unit` this holds. + long get(TemporalUnit unit); + + /// The units this amount is made of. + List getUnits(); + + /// `temporal` moved forward by this amount. + Temporal addTo(Temporal temporal); + + /// `temporal` moved back by this amount. + Temporal subtractFrom(Temporal temporal); +} diff --git a/Ports/CLDC11/src/java/time/temporal/TemporalQuery.java b/Ports/CLDC11/src/java/time/temporal/TemporalQuery.java new file mode 100644 index 00000000000..5d11d5100ca --- /dev/null +++ b/Ports/CLDC11/src/java/time/temporal/TemporalQuery.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.time.temporal; + +/// A question asked of a date or time, such as what +/// `formatter.parse(text, LocalDate::from)` takes as its second argument. +public interface TemporalQuery { + /// The answer for `temporal`. + R queryFrom(TemporalAccessor temporal); +} diff --git a/Ports/CLDC11/src/java/time/temporal/TemporalUnit.java b/Ports/CLDC11/src/java/time/temporal/TemporalUnit.java new file mode 100644 index 00000000000..4b894f379a0 --- /dev/null +++ b/Ports/CLDC11/src/java/time/temporal/TemporalUnit.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.time.temporal; + +import java.time.Duration; + +/// A unit time is measured in. `ChronoUnit` holds the units of the ISO +/// calendar. +public interface TemporalUnit { + /// The length of one unit; an estimate for the units a calendar decides. + Duration getDuration(); + + /// Whether the length is an estimate. + boolean isDurationEstimated(); + + /// Whether this is a unit of a date: a day or longer. + boolean isDateBased(); + + /// Whether this is a unit of a time of day: shorter than a day. + boolean isTimeBased(); + + /// Whether `temporal` can add or subtract this unit. + default boolean isSupportedBy(Temporal temporal) { + return temporal.isSupported(this); + } + + /// `temporal` moved by `amount` of this unit. + R addTo(R temporal, long amount); + + /// How many whole units lie between two values of the same kind. + long between(Temporal temporal1Inclusive, Temporal temporal2Exclusive); +} diff --git a/Ports/CLDC11/src/java/time/temporal/UnsupportedTemporalTypeException.java b/Ports/CLDC11/src/java/time/temporal/UnsupportedTemporalTypeException.java new file mode 100644 index 00000000000..b36e943c05b --- /dev/null +++ b/Ports/CLDC11/src/java/time/temporal/UnsupportedTemporalTypeException.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.time.temporal; + +import java.time.DateTimeException; + +/// Thrown when a date or time is asked for a unit it does not have. +public class UnsupportedTemporalTypeException extends DateTimeException { + /// An exception with `message`. + public UnsupportedTemporalTypeException(String message) { + super(message); + } + + /// An exception with `message`, caused by `cause`. + public UnsupportedTemporalTypeException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/Ports/CLDC11/src/java/util/Arrays.java b/Ports/CLDC11/src/java/util/Arrays.java index b919f41fd68..b0eeac2bd5d 100644 --- a/Ports/CLDC11/src/java/util/Arrays.java +++ b/Ports/CLDC11/src/java/util/Arrays.java @@ -4293,4 +4293,59 @@ public static short[] copyOf(short[] original, int len) { return copyOfRange(original, 0, len); } + + /// A sequential stream of the elements of `array`. + public static java.util.stream.Stream stream(T[] array) { + return java.util.stream.Stream.of(array); + } + + /// A sequential stream of the elements of `array` from `startInclusive` up + /// to, not including, `endExclusive`. + public static java.util.stream.Stream stream(T[] array, int startInclusive, int endExclusive) { + checkStreamRange(array.length, startInclusive, endExclusive); + return java.util.stream.Stream.of(array).skip(startInclusive).limit(endExclusive - startInclusive); + } + + /// A sequential stream of the elements of `array`. + public static java.util.stream.IntStream stream(int[] array) { + return java.util.stream.IntStream.of(array); + } + + /// A sequential stream of the elements of `array` from `startInclusive` up + /// to, not including, `endExclusive`. + public static java.util.stream.IntStream stream(int[] array, int startInclusive, int endExclusive) { + checkStreamRange(array.length, startInclusive, endExclusive); + return java.util.stream.IntStream.of(array).skip(startInclusive).limit(endExclusive - startInclusive); + } + + /// A sequential stream of the elements of `array`. + public static java.util.stream.LongStream stream(long[] array) { + return java.util.stream.LongStream.of(array); + } + + /// A sequential stream of the elements of `array` from `startInclusive` up + /// to, not including, `endExclusive`. + public static java.util.stream.LongStream stream(long[] array, int startInclusive, int endExclusive) { + checkStreamRange(array.length, startInclusive, endExclusive); + return java.util.stream.LongStream.of(array).skip(startInclusive).limit(endExclusive - startInclusive); + } + + /// A sequential stream of the elements of `array`. + public static java.util.stream.DoubleStream stream(double[] array) { + return java.util.stream.DoubleStream.of(array); + } + + /// A sequential stream of the elements of `array` from `startInclusive` up + /// to, not including, `endExclusive`. + public static java.util.stream.DoubleStream stream(double[] array, int startInclusive, int endExclusive) { + checkStreamRange(array.length, startInclusive, endExclusive); + return java.util.stream.DoubleStream.of(array).skip(startInclusive).limit(endExclusive - startInclusive); + } + + private static void checkStreamRange(int length, int startInclusive, int endExclusive) { + if (startInclusive < 0 || endExclusive > length || startInclusive > endExclusive) { + throw new ArrayIndexOutOfBoundsException("origin(" + startInclusive + ") > fence(" + endExclusive + ")"); + } + } + } diff --git a/Ports/CLDC11/src/java/util/Base64.java b/Ports/CLDC11/src/java/util/Base64.java new file mode 100644 index 00000000000..3d7e81f62e6 --- /dev/null +++ b/Ports/CLDC11/src/java/util/Base64.java @@ -0,0 +1,264 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util; + +/// `java.util.Base64`: the basic, URL-safe and MIME encodings of RFC 4648 +/// and RFC 2045, by the JDK's rules for padding and for what a decoder +/// refuses. +public final class Base64 { + private static final String BASIC = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + private static final String URL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + private static final byte[] CRLF = {'\r', '\n'}; + + private Base64() { + } + + public static Encoder getEncoder() { + return new Encoder(BASIC, 0, null, true); + } + + public static Encoder getUrlEncoder() { + return new Encoder(URL, 0, null, true); + } + + public static Encoder getMimeEncoder() { + return new Encoder(BASIC, 76, CRLF, true); + } + + public static Encoder getMimeEncoder(int lineLength, byte[] lineSeparator) { + if (lineSeparator == null) { + throw new NullPointerException(); + } + for (int i = 0; i < lineSeparator.length; i++) { + int b = lineSeparator[i] & 0xFF; + // The padding character too, as on the JDK: a '=' between two + // groups reads as the end of the data. + if (b == '=' || BASIC.indexOf((char) b) >= 0) { + throw new IllegalArgumentException("Illegal base64 line separator character 0x" + + Integer.toString(b, 16)); + } + } + // Rounded down to a multiple of four, as whole groups make a line. + int length = lineLength & ~3; + if (length <= 0) { + return new Encoder(BASIC, 0, null, true); + } + byte[] separator = new byte[lineSeparator.length]; + System.arraycopy(lineSeparator, 0, separator, 0, separator.length); + return new Encoder(BASIC, length, separator, true); + } + + public static Decoder getDecoder() { + return new Decoder(BASIC, false); + } + + public static Decoder getUrlDecoder() { + return new Decoder(URL, false); + } + + public static Decoder getMimeDecoder() { + return new Decoder(BASIC, true); + } + + /// `java.util.Base64.Encoder`. + public static final class Encoder { + private final String alphabet; + private final int lineLength; + private final byte[] separator; + private final boolean padding; + + Encoder(String alphabet, int lineLength, byte[] separator, boolean padding) { + this.alphabet = alphabet; + this.lineLength = lineLength; + this.separator = separator; + this.padding = padding; + } + + /// The same encoding without the `=` that fills the last group. + public Encoder withoutPadding() { + return padding ? new Encoder(alphabet, lineLength, separator, false) : this; + } + + private int length(int n) { + int length = padding ? 4 * ((n + 2) / 3) : 4 * (n / 3) + (n % 3 == 0 ? 0 : n % 3 + 1); + if (lineLength > 0 && length > 0) { + length += (length - 1) / lineLength * separator.length; + } + return length; + } + + public byte[] encode(byte[] src) { + byte[] out = new byte[length(src.length)]; + write(src, out); + return out; + } + + /// Encodes into `dst`, which must be large enough, and answers how + /// many bytes were written. + public int encode(byte[] src, byte[] dst) { + if (dst.length < length(src.length)) { + throw new IllegalArgumentException("Output byte array is too small for encoding all input bytes"); + } + return write(src, dst); + } + + public String encodeToString(byte[] src) { + byte[] out = encode(src); + char[] chars = new char[out.length]; + for (int i = 0; i < out.length; i++) { + chars[i] = (char) (out[i] & 0xFF); + } + return new String(chars); + } + + private int write(byte[] src, byte[] dst) { + int at = 0; + int onLine = 0; + int n = src.length; + for (int i = 0; i < n; i += 3) { + if (lineLength > 0 && onLine == lineLength) { + for (int s = 0; s < separator.length; s++) { + dst[at++] = separator[s]; + } + onLine = 0; + } + int left = n - i; + int bits = (src[i] & 0xFF) << 16; + if (left > 1) { + bits |= (src[i + 1] & 0xFF) << 8; + } + if (left > 2) { + bits |= src[i + 2] & 0xFF; + } + dst[at++] = (byte) alphabet.charAt((bits >> 18) & 0x3F); + dst[at++] = (byte) alphabet.charAt((bits >> 12) & 0x3F); + if (left > 1) { + dst[at++] = (byte) alphabet.charAt((bits >> 6) & 0x3F); + } else if (padding) { + dst[at++] = '='; + } + if (left > 2) { + dst[at++] = (byte) alphabet.charAt(bits & 0x3F); + } else if (padding) { + dst[at++] = '='; + } + onLine += 4; + } + return at; + } + } + + /// `java.util.Base64.Decoder`. + public static final class Decoder { + private final int[] values = new int[256]; + private final boolean mime; + + Decoder(String alphabet, boolean mime) { + this.mime = mime; + for (int i = 0; i < 256; i++) { + values[i] = -1; + } + for (int i = 0; i < alphabet.length(); i++) { + values[alphabet.charAt(i)] = i; + } + values['='] = -2; + } + + public byte[] decode(String src) { + byte[] bytes = new byte[src.length()]; + for (int i = 0; i < bytes.length; i++) { + char c = src.charAt(i); + bytes[i] = (byte) (c < 256 ? c : '?'); + } + return decode(bytes); + } + + public byte[] decode(byte[] src) { + byte[] out = new byte[src.length / 4 * 3 + 3]; + int n = read(src, out); + byte[] exact = new byte[n]; + System.arraycopy(out, 0, exact, 0, n); + return exact; + } + + /// Decodes into `dst`, which must be large enough, and answers how + /// many bytes were written. + public int decode(byte[] src, byte[] dst) { + byte[] out = decode(src); + if (dst.length < out.length) { + throw new IllegalArgumentException("Output byte array is too small for decoding all input bytes"); + } + System.arraycopy(out, 0, dst, 0, out.length); + return out.length; + } + + private int read(byte[] src, byte[] dst) { + int at = 0; + int bits = 0; + int shift = 18; + int sp = 0; + int end = src.length; + while (sp < end) { + int b = values[src[sp++] & 0xFF]; + if (b < 0) { + if (b == -2) { + // xx== or xxx=, and nothing else, ends the input. + if (shift == 6 && (sp == end || src[sp++] != '=') || shift == 18) { + throw new IllegalArgumentException("Input byte array has wrong 4-byte ending unit"); + } + break; + } + if (mime) { + continue; + } + throw new IllegalArgumentException("Illegal base64 character " + + Integer.toString(src[sp - 1], 16)); + } + bits |= b << shift; + shift -= 6; + if (shift < 0) { + dst[at++] = (byte) (bits >> 16); + dst[at++] = (byte) (bits >> 8); + dst[at++] = (byte) bits; + shift = 18; + bits = 0; + } + } + if (shift == 6) { + dst[at++] = (byte) (bits >> 16); + } else if (shift == 0) { + dst[at++] = (byte) (bits >> 16); + dst[at++] = (byte) (bits >> 8); + } else if (shift == 12) { + throw new IllegalArgumentException("Last unit does not have enough valid bits"); + } + while (sp < end) { + if (mime && values[src[sp++] & 0xFF] < 0) { + continue; + } + throw new IllegalArgumentException("Input byte array has incorrect ending byte at " + sp); + } + return at; + } + } +} diff --git a/Ports/CLDC11/src/java/util/Collection.java b/Ports/CLDC11/src/java/util/Collection.java index ba29f40a84e..5fb0e8953f2 100644 --- a/Ports/CLDC11/src/java/util/Collection.java +++ b/Ports/CLDC11/src/java/util/Collection.java @@ -17,6 +17,7 @@ package java.util; +import java.util.function.IntFunction; import java.util.function.Predicate; @@ -373,4 +374,28 @@ public interface Collection extends java.lang.Iterable { default boolean removeIf(Predicate filter) { return false; } + + /// A sequential stream of the elements, read when a terminal operation + /// asks for them, so elements added before then are seen. + default java.util.stream.Stream stream() { + return java.util.stream.StreamSupport.stream(spliterator(), false); + } + + /// The same as [#stream()]: streams here are sequential, which the JDK + /// allows a parallel stream to be. + default java.util.stream.Stream parallelStream() { + return java.util.stream.StreamSupport.stream(spliterator(), true); + } + + /// The elements, read from [#iterator()] when the first one is asked for. + @Override + default Spliterator spliterator() { + return Spliterators.spliterator(this, 0); + } + + /// The elements in an array `generator` makes, as `toArray(generator.apply(0))`. + default T[] toArray(IntFunction generator) { + return toArray(generator.apply(0)); + } + } diff --git a/Ports/CLDC11/src/java/util/Collections.java b/Ports/CLDC11/src/java/util/Collections.java index b8d36dcfb29..c91538197ba 100644 --- a/Ports/CLDC11/src/java/util/Collections.java +++ b/Ports/CLDC11/src/java/util/Collections.java @@ -2519,6 +2519,12 @@ public static final List emptyList() { return EMPTY_LIST; } + /// An empty, immutable `Iterator`. + @SuppressWarnings("unchecked") + public static final Iterator emptyIterator() { + return EMPTY_LIST.iterator(); + } + /// Returns a type-safe empty, immutable `Set`. /// /// #### Returns diff --git a/Ports/CLDC11/src/java/util/Comparator.java b/Ports/CLDC11/src/java/util/Comparator.java index 8a571fba418..3f0313d8567 100644 --- a/Ports/CLDC11/src/java/util/Comparator.java +++ b/Ports/CLDC11/src/java/util/Comparator.java @@ -78,4 +78,253 @@ public interface Comparator { /// /// - Object#equals public boolean equals(Object object); + + /// This ordering, reversed. + /// + /// @return the reversed comparator + default Comparator reversed() { + final Comparator self = this; + return new Comparator() { + public int compare(T a, T b) { + return self.compare(b, a); + } + }; + } + + /// This ordering, ties broken by `other`. + /// + /// @param other the tie breaker + /// @return the combined comparator + default Comparator thenComparing(final Comparator other) { + // Arguments are checked here, not when the comparator is first used, as on the JDK. + if (other == null) { + throw new NullPointerException(); + } + final Comparator self = this; + return new Comparator() { + public int compare(T a, T b) { + int r = self.compare(a, b); + return r != 0 ? r : other.compare(a, b); + } + }; + } + + /// This ordering, ties broken by the natural order of a key. + /// + /// @param keyExtractor extracts the key + /// @param the key type + /// @return the combined comparator + default > Comparator thenComparing( + java.util.function.Function keyExtractor) { + return thenComparing(Comparator.comparing(keyExtractor)); + } + + /// Compares by the natural order of a key. + /// + /// @param keyExtractor extracts the key + /// @param the compared type + /// @param the key type + /// @return the comparator + static > Comparator comparing( + final java.util.function.Function keyExtractor) { + if (keyExtractor == null) { + throw new NullPointerException(); + } + return new Comparator() { + public int compare(T a, T b) { + return keyExtractor.apply(a).compareTo(keyExtractor.apply(b)); + } + }; + } + + /// Compares by a key, using the given comparator for the keys. + /// + /// @param keyExtractor extracts the key + /// @param keyComparator orders the keys + /// @param the compared type + /// @param the key type + /// @return the comparator + static Comparator comparing(final java.util.function.Function keyExtractor, + final Comparator keyComparator) { + if (keyExtractor == null || keyComparator == null) { + throw new NullPointerException(); + } + return new Comparator() { + public int compare(T a, T b) { + return keyComparator.compare(keyExtractor.apply(a), keyExtractor.apply(b)); + } + }; + } + + /// The natural order of comparable values. + /// + /// @param the compared type + /// @return the comparator + @SuppressWarnings("unchecked") + static > Comparator naturalOrder() { + return (Comparator) NaturalOrder.INSTANCE; + } + + /// The reverse of the natural order. + /// + /// @param the compared type + /// @return the comparator + @SuppressWarnings("unchecked") + static > Comparator reverseOrder() { + // Not Collections.reverseOrder(): naming Collections here makes the translator + // load it, and with it classes (Collections.SetFromMap) whose presence the native + // collection traversal keys on, in every application. + return (Comparator) NaturalOrder.INSTANCE.reversed(); + } + + /// This ordering, ties broken by a key in the order `keyComparator` gives. + /// + /// @param keyExtractor extracts the key + /// @param keyComparator orders the keys + /// @param the key type + /// @return the combined comparator + default Comparator thenComparing(java.util.function.Function keyExtractor, + Comparator keyComparator) { + return thenComparing(Comparator.comparing(keyExtractor, keyComparator)); + } + + /// This ordering, ties broken by an `int` key. + /// + /// @param keyExtractor extracts the key + /// @return the combined comparator + default Comparator thenComparingInt(java.util.function.ToIntFunction keyExtractor) { + return thenComparing(Comparator.comparingInt(keyExtractor)); + } + + /// This ordering, ties broken by a `long` key. + /// + /// @param keyExtractor extracts the key + /// @return the combined comparator + default Comparator thenComparingLong(java.util.function.ToLongFunction keyExtractor) { + return thenComparing(Comparator.comparingLong(keyExtractor)); + } + + /// This ordering, ties broken by a `double` key, compared as [Double#compare] does. + /// + /// @param keyExtractor extracts the key + /// @return the combined comparator + default Comparator thenComparingDouble(java.util.function.ToDoubleFunction keyExtractor) { + return thenComparing(Comparator.comparingDouble(keyExtractor)); + } + + /// Compares by an `int` key. + /// + /// @param keyExtractor extracts the key + /// @param the compared type + /// @return the comparator + static Comparator comparingInt(final java.util.function.ToIntFunction keyExtractor) { + if (keyExtractor == null) { + throw new NullPointerException(); + } + return new Comparator() { + public int compare(T a, T b) { + int x = keyExtractor.applyAsInt(a); + int y = keyExtractor.applyAsInt(b); + return x < y ? -1 : x == y ? 0 : 1; + } + }; + } + + /// Compares by a `long` key. + /// + /// @param keyExtractor extracts the key + /// @param the compared type + /// @return the comparator + static Comparator comparingLong(final java.util.function.ToLongFunction keyExtractor) { + if (keyExtractor == null) { + throw new NullPointerException(); + } + return new Comparator() { + public int compare(T a, T b) { + long x = keyExtractor.applyAsLong(a); + long y = keyExtractor.applyAsLong(b); + return x < y ? -1 : x == y ? 0 : 1; + } + }; + } + + /// Compares by a `double` key, as [Double#compare] does: NaN above + /// everything and `-0.0` below `0.0`. + /// + /// @param keyExtractor extracts the key + /// @param the compared type + /// @return the comparator + static Comparator comparingDouble(final java.util.function.ToDoubleFunction keyExtractor) { + if (keyExtractor == null) { + throw new NullPointerException(); + } + return new Comparator() { + public int compare(T a, T b) { + return Double.compare(keyExtractor.applyAsDouble(a), keyExtractor.applyAsDouble(b)); + } + }; + } + + /// Orders null before every other value, and the rest by `comparator`; + /// with a null `comparator` the others are all equal. + /// + /// @param comparator orders the values that are not null, or null + /// @param the compared type + /// @return the comparator + static Comparator nullsFirst(Comparator comparator) { + return new NullOrder(true, comparator); + } + + /// Orders null after every other value, and the rest by `comparator`; + /// with a null `comparator` the others are all equal. + /// + /// @param comparator orders the values that are not null, or null + /// @param the compared type + /// @return the comparator + static Comparator nullsLast(Comparator comparator) { + return new NullOrder(false, comparator); + } +} + +/// The order [Comparator#nullsFirst] and [Comparator#nullsLast] hand out. Its +/// [#reversed] keeps null on the same side and reverses the rest, as the JDK's does. +final class NullOrder implements Comparator { + private final boolean nullFirst; + private final Comparator real; + + @SuppressWarnings("unchecked") + NullOrder(boolean nullFirst, Comparator real) { + this.nullFirst = nullFirst; + this.real = (Comparator) real; + } + + public int compare(T a, T b) { + if (a == null) { + return b == null ? 0 : nullFirst ? -1 : 1; + } + if (b == null) { + return nullFirst ? 1 : -1; + } + return real == null ? 0 : real.compare(a, b); + } + + public Comparator thenComparing(Comparator other) { + if (other == null) { + throw new NullPointerException(); + } + return new NullOrder(nullFirst, real == null ? other : real.thenComparing(other)); + } + + public Comparator reversed() { + return new NullOrder(!nullFirst, real == null ? null : real.reversed()); + } +} + +/// The natural ordering [Comparator#naturalOrder] hands out. +final class NaturalOrder implements Comparator> { + static final NaturalOrder INSTANCE = new NaturalOrder(); + + public int compare(Comparable a, Comparable b) { + return a.compareTo(b); + } } diff --git a/Ports/CLDC11/src/java/util/Date.java b/Ports/CLDC11/src/java/util/Date.java index 9c70c039957..ec382eda8fb 100644 --- a/Ports/CLDC11/src/java/util/Date.java +++ b/Ports/CLDC11/src/java/util/Date.java @@ -70,4 +70,83 @@ public int compareTo(Date arg0) { return 0; } + /// This date as an [java.time.Instant]. + public java.time.Instant toInstant() { + return java.time.Instant.ofEpochMilli(getTime()); + } + + /// The date of `instant`, which must fit in milliseconds since the + /// epoch. + public static Date from(java.time.Instant instant) { + try { + return new Date(instant.toEpochMilli()); + } catch (ArithmeticException ex) { + throw new IllegalArgumentException(ex.toString()); + } + } + + /// Whether this date is after `when`. + public boolean after(Date when) { + return getTime() > when.getTime(); + } + + /// Whether this date is before `when`. + public boolean before(Date when) { + return getTime() < when.getTime(); + } + + private int field(int field) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(this); + return calendar.get(field); + } + + /// The year less 1900, in the default time zone. + /// + /// @deprecated use `Calendar.get(Calendar.YEAR) - 1900` + public int getYear() { + return field(Calendar.YEAR) - 1900; + } + + /// The month, 0 for January, in the default time zone. + /// + /// @deprecated use `Calendar.get(Calendar.MONTH)` + public int getMonth() { + return field(Calendar.MONTH); + } + + /// The day of the month, in the default time zone. + /// + /// @deprecated use `Calendar.get(Calendar.DAY_OF_MONTH)` + public int getDate() { + return field(Calendar.DAY_OF_MONTH); + } + + /// The day of the week, 0 for Sunday, in the default time zone. + /// + /// @deprecated use `Calendar.get(Calendar.DAY_OF_WEEK)` + public int getDay() { + return field(Calendar.DAY_OF_WEEK) - Calendar.SUNDAY; + } + + /// The hour of the day, in the default time zone. + /// + /// @deprecated use `Calendar.get(Calendar.HOUR_OF_DAY)` + public int getHours() { + return field(Calendar.HOUR_OF_DAY); + } + + /// The minute of the hour, in the default time zone. + /// + /// @deprecated use `Calendar.get(Calendar.MINUTE)` + public int getMinutes() { + return field(Calendar.MINUTE); + } + + /// The second of the minute, in the default time zone. + /// + /// @deprecated use `Calendar.get(Calendar.SECOND)` + public int getSeconds() { + return field(Calendar.SECOND); + } } diff --git a/Ports/CLDC11/src/java/util/DoubleSum.java b/Ports/CLDC11/src/java/util/DoubleSum.java new file mode 100644 index 00000000000..02920bd9cb8 --- /dev/null +++ b/Ports/CLDC11/src/java/util/DoubleSum.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util; + +/// A running sum of `double` values that carries the rounding error of each +/// addition along (Kahan summation), so that adding many small values to a +/// large one does not lose them. +final class DoubleSum { + + private double sum; + private double compensation; + /// The plain sum, which is what says "infinite" when the compensated + /// one has turned into a NaN by subtracting two infinities. + private double simple; + + void add(double value) { + simple += value; + double corrected = value - compensation; + double next = sum + corrected; + compensation = (next - sum) - corrected; + sum = next; + } + + void add(DoubleSum other) { + double plain = simple + other.simple; + add(other.sum); + add(-other.compensation); + simple = plain; + } + + double value() { + double out = sum - compensation; + if (Double.isNaN(out) && Double.isInfinite(simple)) { + return simple; + } + return out; + } +} diff --git a/Ports/CLDC11/src/java/util/DoubleSummaryStatistics.java b/Ports/CLDC11/src/java/util/DoubleSummaryStatistics.java new file mode 100644 index 00000000000..4d219243bb2 --- /dev/null +++ b/Ports/CLDC11/src/java/util/DoubleSummaryStatistics.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util; + +import java.util.function.DoubleConsumer; + +/// The count, +/// sum, minimum, maximum and average of the values it was given. +public class DoubleSummaryStatistics implements DoubleConsumer { + + private long count; + private final DoubleSum sum = new DoubleSum(); + private double min = Double.POSITIVE_INFINITY; + private double max = Double.NEGATIVE_INFINITY; + + public DoubleSummaryStatistics() { + // Nothing seen yet. + } + + @Override + public void accept(double value) { + count++; + sum.add(value); + min = Math.min(min, value); + max = Math.max(max, value); + } + + public void combine(DoubleSummaryStatistics other) { + count += other.count; + sum.add(other.sum); + min = Math.min(min, other.min); + max = Math.max(max, other.max); + } + + public final long getCount() { + return count; + } + + public final double getSum() { + return sum.value(); + } + + public final double getMin() { + return min; + } + + public final double getMax() { + return max; + } + + public final double getAverage() { + return count > 0 ? (double) getSum() / count : 0.0d; + } +} diff --git a/Ports/CLDC11/src/java/util/EventObject.java b/Ports/CLDC11/src/java/util/EventObject.java new file mode 100644 index 00000000000..48f852f5505 --- /dev/null +++ b/Ports/CLDC11/src/java/util/EventObject.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util; + +/// `java.util.EventObject`: the root of every +/// listener event, carrying the object the event happened on. +public class EventObject implements java.io.Serializable { + + private static final long serialVersionUID = 1L; + + /// The object the event initially occurred on. + protected transient Object source; + + public EventObject(Object source) { + if (source == null) { + throw new IllegalArgumentException("null source"); + } + this.source = source; + } + + public Object getSource() { + return source; + } + + @Override + public String toString() { + return getClass().getName() + "[source=" + source + "]"; + } +} diff --git a/Ports/CLDC11/src/java/util/HexFormat.java b/Ports/CLDC11/src/java/util/HexFormat.java new file mode 100644 index 00000000000..d0c73c05d3e --- /dev/null +++ b/Ports/CLDC11/src/java/util/HexFormat.java @@ -0,0 +1,261 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util; + +/// `java.util.HexFormat`: bytes to hexadecimal text and back, with an +/// optional delimiter between bytes and a prefix and suffix around each. +public final class HexFormat { + private static final String LOWER = "0123456789abcdef"; + private static final String UPPER = "0123456789ABCDEF"; + private static final HexFormat PLAIN = new HexFormat("", "", "", false); + + private final String delimiter; + private final String prefix; + private final String suffix; + private final boolean upper; + + private HexFormat(String delimiter, String prefix, String suffix, boolean upper) { + if (delimiter == null || prefix == null || suffix == null) { + throw new NullPointerException(); + } + this.delimiter = delimiter; + this.prefix = prefix; + this.suffix = suffix; + this.upper = upper; + } + + /// Lowercase digits and nothing between or around the bytes. + public static HexFormat of() { + return PLAIN; + } + + public static HexFormat ofDelimiter(String delimiter) { + return new HexFormat(delimiter, "", "", false); + } + + public HexFormat withDelimiter(String delimiter) { + return new HexFormat(delimiter, prefix, suffix, upper); + } + + public HexFormat withPrefix(String prefix) { + return new HexFormat(delimiter, prefix, suffix, upper); + } + + public HexFormat withSuffix(String suffix) { + return new HexFormat(delimiter, prefix, suffix, upper); + } + + public HexFormat withUpperCase() { + return new HexFormat(delimiter, prefix, suffix, true); + } + + public HexFormat withLowerCase() { + return new HexFormat(delimiter, prefix, suffix, false); + } + + public String delimiter() { + return delimiter; + } + + public String prefix() { + return prefix; + } + + public String suffix() { + return suffix; + } + + public boolean isUpperCase() { + return upper; + } + + public String formatHex(byte[] bytes) { + return formatHex(bytes, 0, bytes.length); + } + + public String formatHex(byte[] bytes, int fromIndex, int toIndex) { + if (fromIndex < 0 || toIndex > bytes.length || fromIndex > toIndex) { + throw new IndexOutOfBoundsException("Range [" + fromIndex + ", " + toIndex + + ") out of bounds for length " + bytes.length); + } + String digits = upper ? UPPER : LOWER; + StringBuilder out = new StringBuilder(); + for (int i = fromIndex; i < toIndex; i++) { + if (i > fromIndex) { + out.append(delimiter); + } + out.append(prefix).append(digits.charAt((bytes[i] >> 4) & 0xF)).append(digits.charAt(bytes[i] & 0xF)) + .append(suffix); + } + return out.toString(); + } + + public byte[] parseHex(CharSequence string) { + return parseHex(string, 0, string.length()); + } + + public byte[] parseHex(CharSequence string, int fromIndex, int toIndex) { + if (fromIndex < 0 || toIndex > string.length() || fromIndex > toIndex) { + throw new IndexOutOfBoundsException("Range [" + fromIndex + ", " + toIndex + + ") out of bounds for length " + string.length()); + } + int length = toIndex - fromIndex; + if (length == 0) { + return new byte[0]; + } + int each = prefix.length() + 2 + suffix.length(); + int stride = each + delimiter.length(); + if ((length - each) % stride != 0) { + throw new IllegalArgumentException("extra or missing delimiters " + + "or values consisting of prefix, two hexadecimal digits, and suffix in the string"); + } + byte[] out = new byte[(length - each) / stride + 1]; + int at = fromIndex; + for (int i = 0; i < out.length; i++) { + if (i > 0) { + at = expect(string, at, delimiter, "delimiter"); + } + at = expect(string, at, prefix, "prefix"); + out[i] = (byte) (fromHexDigit(string.charAt(at)) * 16 + fromHexDigit(string.charAt(at + 1))); + at = expect(string, at + 2, suffix, "suffix"); + } + return out; + } + + private static int expect(CharSequence string, int at, String literal, String what) { + for (int i = 0; i < literal.length(); i++) { + if (string.charAt(at + i) != literal.charAt(i)) { + throw new IllegalArgumentException("found: \"" + string.subSequence(at, at + literal.length()) + + "\", expected: \"" + literal + "\", index: " + at + " ch: " + + Integer.toHexString(string.charAt(at))); + } + } + return at + literal.length(); + } + + public char toLowHexDigit(int value) { + return (upper ? UPPER : LOWER).charAt(value & 0xF); + } + + public char toHighHexDigit(int value) { + return (upper ? UPPER : LOWER).charAt((value >> 4) & 0xF); + } + + public String toHexDigits(byte value) { + return toHexDigits(value, 2); + } + + public String toHexDigits(char value) { + return toHexDigits(value, 4); + } + + public String toHexDigits(short value) { + return toHexDigits(value, 4); + } + + public String toHexDigits(int value) { + return toHexDigits(value, 8); + } + + public String toHexDigits(long value) { + return toHexDigits(value, 16); + } + + public String toHexDigits(long value, int digits) { + if (digits < 0 || digits > 16) { + throw new IllegalArgumentException("number of digits: " + digits); + } + char[] out = new char[digits]; + long v = value; + for (int i = digits - 1; i >= 0; i--) { + out[i] = toLowHexDigit((int) v); + v >>>= 4; + } + return new String(out); + } + + public static boolean isHexDigit(int ch) { + return ch >= '0' && ch <= '9' || ch >= 'a' && ch <= 'f' || ch >= 'A' && ch <= 'F'; + } + + public static int fromHexDigit(int ch) { + if (ch >= '0' && ch <= '9') { + return ch - '0'; + } + if (ch >= 'a' && ch <= 'f') { + return ch - 'a' + 10; + } + if (ch >= 'A' && ch <= 'F') { + return ch - 'A' + 10; + } + throw new NumberFormatException("not a hexadecimal digit: \"" + (char) ch + "\" = " + ch); + } + + public static int fromHexDigits(CharSequence string) { + int length = string.length(); + if (length > 8) { + throw new IllegalArgumentException("string length greater than 8: " + length); + } + int value = 0; + for (int i = 0; i < length; i++) { + value = (value << 4) + fromHexDigit(string.charAt(i)); + } + return value; + } + + public static long fromHexDigitsToLong(CharSequence string) { + int length = string.length(); + if (length > 16) { + throw new IllegalArgumentException("string length greater than 16: " + length); + } + long value = 0; + for (int i = 0; i < length; i++) { + value = (value << 4) + fromHexDigit(string.charAt(i)); + } + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof HexFormat)) { + return false; + } + HexFormat other = (HexFormat) o; + return upper == other.upper && delimiter.equals(other.delimiter) && prefix.equals(other.prefix) + && suffix.equals(other.suffix); + } + + @Override + public int hashCode() { + return (delimiter.hashCode() * 31 + prefix.hashCode()) * 31 + suffix.hashCode() + (upper ? 1 : 0); + } + + @Override + public String toString() { + return "uppercase: " + upper + ", delimiter: \"" + delimiter + "\", prefix: \"" + prefix + + "\", suffix: \"" + suffix + "\""; + } +} diff --git a/Ports/CLDC11/src/java/util/ImmutableCollections.java b/Ports/CLDC11/src/java/util/ImmutableCollections.java new file mode 100644 index 00000000000..a7e5309180e --- /dev/null +++ b/Ports/CLDC11/src/java/util/ImmutableCollections.java @@ -0,0 +1,467 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util; + +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; + +/// What `List.of`, `Set.of`, `Map.of`, their `copyOf` forms and `Map.entry` +/// answer: collections that reject null elements and every mutation, as the +/// JDK's do. A set and a map iterate in no particular order, which the JDK +/// leaves unspecified too; they are hashed rather than linked so an +/// application that never calls them does not keep `LinkedHashMap`. +final class ImmutableCollections { + + private ImmutableCollections() { + } + + /// A list of `elements`, which the caller hands over: nothing else may + /// hold the array. + static List list(Object[] elements) { + for (int i = 0; i < elements.length; i++) { + Objects.requireNonNull(elements[i]); + } + return new ImmutableList(elements); + } + + /// A set of `elements`. Two equal ones are an error when `strict`, as in + /// `Set.of`, and one element otherwise, as in `Set.copyOf`. + @SuppressWarnings("unchecked") + static Set set(Object[] elements, boolean strict) { + HashSet all = new HashSet(); + for (int i = 0; i < elements.length; i++) { + if (!all.add((E) Objects.requireNonNull(elements[i])) && strict) { + throw new IllegalArgumentException("duplicate element: " + elements[i]); + } + } + return new ImmutableSet(all); + } + + /// A map of `keysAndValues`, alternating. + @SuppressWarnings("unchecked") + static Map map(Object[] keysAndValues) { + HashMap all = new HashMap(); + for (int i = 0; i < keysAndValues.length; i += 2) { + K key = (K) Objects.requireNonNull(keysAndValues[i]); + V value = (V) Objects.requireNonNull(keysAndValues[i + 1]); + if (all.containsKey(key)) { + throw new IllegalArgumentException("duplicate key: " + key); + } + all.put(key, value); + } + return new ImmutableMap(all); + } + + /// A list of a copy of `elements`. + static List listCopy(Object[] elements) { + Object[] copy = new Object[elements.length]; + System.arraycopy(elements, 0, copy, 0, elements.length); + return list(copy); + } + + @SuppressWarnings("unchecked") + static List listCopyOf(Collection collection) { + if (collection instanceof ImmutableList) { + return (List) collection; + } + return list(collection.toArray()); + } + + @SuppressWarnings("unchecked") + static Set setCopyOf(Collection collection) { + if (collection instanceof ImmutableSet) { + return (Set) collection; + } + return set(collection.toArray(), false); + } + + @SuppressWarnings("unchecked") + static Map mapCopyOf(Map map) { + if (map instanceof ImmutableMap) { + return (Map) map; + } + Object[] flat = new Object[map.size() * 2]; + int at = 0; + for (Map.Entry e : map.entrySet()) { + flat[at++] = e.getKey(); + flat[at++] = e.getValue(); + } + return map(flat); + } + + static Map mapOfEntries(Map.Entry[] entries) { + Object[] flat = new Object[entries.length * 2]; + for (int i = 0; i < entries.length; i++) { + flat[i * 2] = entries[i].getKey(); + flat[i * 2 + 1] = entries[i].getValue(); + } + return map(flat); + } + + private static UnsupportedOperationException immutable() { + return new UnsupportedOperationException(); + } + + static final class ImmutableEntry implements Map.Entry { + + private final K key; + private final V value; + + ImmutableEntry(K key, V value) { + this.key = Objects.requireNonNull(key); + this.value = Objects.requireNonNull(value); + } + + @Override + public K getKey() { + return key; + } + + @Override + public V getValue() { + return value; + } + + @Override + public V setValue(V newValue) { + throw immutable(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Map.Entry)) { + return false; + } + Map.Entry other = (Map.Entry) o; + return key.equals(other.getKey()) && value.equals(other.getValue()); + } + + @Override + public int hashCode() { + return key.hashCode() ^ value.hashCode(); + } + + @Override + public String toString() { + return key + "=" + value; + } + } + + static final class ImmutableList extends AbstractList implements RandomAccess { + + private final Object[] elements; + + ImmutableList(Object[] elements) { + this.elements = elements; + } + + @Override + @SuppressWarnings("unchecked") + public E get(int index) { + if (index < 0 || index >= elements.length) { + throw new IndexOutOfBoundsException("Index " + index + " out of bounds for length " + + elements.length); + } + return (E) elements[index]; + } + + @Override + public int size() { + return elements.length; + } + + @Override + public boolean contains(Object o) { + return indexOf(o) >= 0; + } + + @Override + public int indexOf(Object o) { + Objects.requireNonNull(o); + for (int i = 0; i < elements.length; i++) { + if (o.equals(elements[i])) { + return i; + } + } + return -1; + } + + @Override + public int lastIndexOf(Object o) { + Objects.requireNonNull(o); + for (int i = elements.length - 1; i >= 0; i--) { + if (o.equals(elements[i])) { + return i; + } + } + return -1; + } + + @Override + public boolean add(E e) { + throw immutable(); + } + + @Override + public void add(int index, E e) { + throw immutable(); + } + + @Override + public boolean addAll(Collection c) { + throw immutable(); + } + + @Override + public boolean addAll(int index, Collection c) { + throw immutable(); + } + + @Override + public void clear() { + throw immutable(); + } + + @Override + public E remove(int index) { + throw immutable(); + } + + @Override + public boolean remove(Object o) { + throw immutable(); + } + + @Override + public boolean removeAll(Collection c) { + throw immutable(); + } + + @Override + public boolean retainAll(Collection c) { + throw immutable(); + } + + @Override + public boolean removeIf(Predicate filter) { + throw immutable(); + } + + @Override + public void replaceAll(UnaryOperator operator) { + throw immutable(); + } + + @Override + public void sort(Comparator c) { + throw immutable(); + } + + @Override + public E set(int index, E e) { + throw immutable(); + } + } + + static final class ImmutableSet extends AbstractSet { + + private final Set elements; + + ImmutableSet(Set elements) { + this.elements = elements; + } + + @Override + public Iterator iterator() { + Iterator it = elements.iterator(); + return new Iterator() { + @Override + public boolean hasNext() { + return it.hasNext(); + } + + @Override + public E next() { + return it.next(); + } + + @Override + public void remove() { + throw immutable(); + } + }; + } + + @Override + public int size() { + return elements.size(); + } + + @Override + public boolean contains(Object o) { + return elements.contains(Objects.requireNonNull(o)); + } + + @Override + public boolean add(E e) { + throw immutable(); + } + + @Override + public boolean addAll(Collection c) { + throw immutable(); + } + + @Override + public void clear() { + throw immutable(); + } + + @Override + public boolean remove(Object o) { + throw immutable(); + } + + @Override + public boolean removeAll(Collection c) { + throw immutable(); + } + + @Override + public boolean retainAll(Collection c) { + throw immutable(); + } + + @Override + public boolean removeIf(Predicate filter) { + throw immutable(); + } + } + + static final class ImmutableMap extends AbstractMap { + + private final Map entries; + private final Set> entrySet; + + ImmutableMap(HashMap entries) { + this.entries = entries; + HashSet> all = new HashSet>(); + for (Map.Entry e : entries.entrySet()) { + all.add(new ImmutableEntry(e.getKey(), e.getValue())); + } + this.entrySet = new ImmutableSet>(all); + } + + @Override + public Set> entrySet() { + return entrySet; + } + + @Override + public int size() { + return entries.size(); + } + + @Override + public V get(Object key) { + return entries.get(Objects.requireNonNull(key)); + } + + @Override + public boolean containsKey(Object key) { + return entries.containsKey(Objects.requireNonNull(key)); + } + + @Override + public boolean containsValue(Object value) { + return entries.containsValue(Objects.requireNonNull(value)); + } + + @Override + public V put(K key, V value) { + throw immutable(); + } + + @Override + public void putAll(Map map) { + throw immutable(); + } + + @Override + public V remove(Object key) { + throw immutable(); + } + + @Override + public void clear() { + throw immutable(); + } + + @Override + public V putIfAbsent(K key, V value) { + throw immutable(); + } + + @Override + public boolean remove(Object key, Object value) { + throw immutable(); + } + + @Override + public boolean replace(K key, V oldValue, V newValue) { + throw immutable(); + } + + @Override + public V replace(K key, V value) { + throw immutable(); + } + + @Override + public void replaceAll(BiFunction function) { + throw immutable(); + } + + @Override + public V computeIfAbsent(K key, Function mappingFunction) { + throw immutable(); + } + + @Override + public V computeIfPresent(K key, BiFunction remappingFunction) { + throw immutable(); + } + + @Override + public V compute(K key, BiFunction remappingFunction) { + throw immutable(); + } + + @Override + public V merge(K key, V value, BiFunction remappingFunction) { + throw immutable(); + } + } +} diff --git a/Ports/CLDC11/src/java/util/IntSummaryStatistics.java b/Ports/CLDC11/src/java/util/IntSummaryStatistics.java new file mode 100644 index 00000000000..377d6f95d71 --- /dev/null +++ b/Ports/CLDC11/src/java/util/IntSummaryStatistics.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util; + +import java.util.function.IntConsumer; + +/// The count, +/// sum, minimum, maximum and average of the values it was given. +public class IntSummaryStatistics implements IntConsumer { + + private long count; + private long sum; + private int min = Integer.MAX_VALUE; + private int max = Integer.MIN_VALUE; + + public IntSummaryStatistics() { + // Nothing seen yet. + } + + @Override + public void accept(int value) { + count++; + sum += value; + min = Math.min(min, value); + max = Math.max(max, value); + } + + public void combine(IntSummaryStatistics other) { + count += other.count; + sum += other.sum; + min = Math.min(min, other.min); + max = Math.max(max, other.max); + } + + public final long getCount() { + return count; + } + + public final long getSum() { + return sum; + } + + public final int getMin() { + return min; + } + + public final int getMax() { + return max; + } + + public final double getAverage() { + return count > 0 ? (double) getSum() / count : 0.0d; + } +} diff --git a/Ports/CLDC11/src/java/util/Iterator.java b/Ports/CLDC11/src/java/util/Iterator.java index 3726d017fbe..71f54878109 100644 --- a/Ports/CLDC11/src/java/util/Iterator.java +++ b/Ports/CLDC11/src/java/util/Iterator.java @@ -17,6 +17,8 @@ package java.util; +import java.util.function.Consumer; + /// An `Iterator` is used to sequence over a collection of objects. /// Conceptually, an iterator is always positioned between two elements of a /// collection. A fresh iterator is always positioned in front of the first @@ -56,17 +58,26 @@ public interface Iterator { /// - #hasNext public E next(); - /// Removes the last object returned by `next` from the collection. - /// This method can only be called once after `next` was called. + /// Removes the last element `next` answered from the collection being + /// iterated, which this iterator does not support unless it says otherwise. /// /// #### Throws /// - /// - `UnsupportedOperationException`: @throws UnsupportedOperationException - /// if removing is not supported by the collection being - /// iterated. + /// - `UnsupportedOperationException`: when removing is not supported /// - /// - `IllegalStateException`: @throws IllegalStateException - /// if `next` has not been called, or `remove` has - /// already been called after the last call to `next`. - public void remove(); + /// - `IllegalStateException`: when `next` has not been called, or `remove` + /// has already been called after the last call to `next` + default void remove() { + throw new UnsupportedOperationException("remove"); + } + + /// Hands each remaining element to `action`, in order. + default void forEachRemaining(Consumer action) { + if (action == null) { + throw new NullPointerException(); + } + while (hasNext()) { + action.accept(next()); + } + } } diff --git a/Ports/CLDC11/src/java/util/List.java b/Ports/CLDC11/src/java/util/List.java index 9c6a02154aa..34ec3e47a0d 100644 --- a/Ports/CLDC11/src/java/util/List.java +++ b/Ports/CLDC11/src/java/util/List.java @@ -446,4 +446,107 @@ default void replaceAll(UnaryOperator operator) { /// implementation comes from the platform's JDK at runtime. default void sort(Comparator c) { } + + /// Inserts `e` before the first element. Stubbed in the CLDC11 + /// subset; the implementation comes from the platform at runtime. + default void addFirst(E e) { + } + + /// Appends `e` after the last element. Stubbed in the CLDC11 subset. + default void addLast(E e) { + } + + /// The first element. Stubbed in the CLDC11 subset. + default E getFirst() { + return null; + } + + /// The last element. Stubbed in the CLDC11 subset. + default E getLast() { + return null; + } + + /// Removes and answers the first element. Stubbed in the CLDC11 subset. + default E removeFirst() { + return null; + } + + /// Removes and answers the last element. Stubbed in the CLDC11 subset. + default E removeLast() { + return null; + } + + /// A view of this list in the opposite order. Stubbed in the CLDC11 + /// subset. + default List reversed() { + return null; + } + + /// An unmodifiable list of no elements. + static List of() { + return ImmutableCollections.list(new Object[] {}); + } + + /// An unmodifiable list of the given elements, none of which may be null. + static List of(E e1) { + return ImmutableCollections.list(new Object[] {e1}); + } + + /// An unmodifiable list of the given elements, none of which may be null. + static List of(E e1, E e2) { + return ImmutableCollections.list(new Object[] {e1, e2}); + } + + /// An unmodifiable list of the given elements, none of which may be null. + static List of(E e1, E e2, E e3) { + return ImmutableCollections.list(new Object[] {e1, e2, e3}); + } + + /// An unmodifiable list of the given elements, none of which may be null. + static List of(E e1, E e2, E e3, E e4) { + return ImmutableCollections.list(new Object[] {e1, e2, e3, e4}); + } + + /// An unmodifiable list of the given elements, none of which may be null. + static List of(E e1, E e2, E e3, E e4, E e5) { + return ImmutableCollections.list(new Object[] {e1, e2, e3, e4, e5}); + } + + /// An unmodifiable list of the given elements, none of which may be null. + static List of(E e1, E e2, E e3, E e4, E e5, E e6) { + return ImmutableCollections.list(new Object[] {e1, e2, e3, e4, e5, e6}); + } + + /// An unmodifiable list of the given elements, none of which may be null. + static List of(E e1, E e2, E e3, E e4, E e5, E e6, E e7) { + return ImmutableCollections.list(new Object[] {e1, e2, e3, e4, e5, e6, e7}); + } + + /// An unmodifiable list of the given elements, none of which may be null. + static List of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8) { + return ImmutableCollections.list(new Object[] {e1, e2, e3, e4, e5, e6, e7, e8}); + } + + /// An unmodifiable list of the given elements, none of which may be null. + static List of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9) { + return ImmutableCollections.list(new Object[] {e1, e2, e3, e4, e5, e6, e7, e8, e9}); + } + + /// An unmodifiable list of the given elements, none of which may be null. + static List of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9, E e10) { + return ImmutableCollections.list(new Object[] {e1, e2, e3, e4, e5, e6, e7, e8, e9, e10}); + } + + /// An unmodifiable list of `elements`, none of which may be null. + @SafeVarargs + static List of(E... elements) { + return ImmutableCollections.listCopy(elements); + } + + /// An unmodifiable list of the elements of `collection`, which may not hold + /// null; `collection` itself when it is already such a list. + static List copyOf(Collection collection) { + return ImmutableCollections.listCopyOf(collection); + } + } diff --git a/Ports/CLDC11/src/java/util/ListResourceBundle.java b/Ports/CLDC11/src/java/util/ListResourceBundle.java new file mode 100644 index 00000000000..909ba5dbaa0 --- /dev/null +++ b/Ports/CLDC11/src/java/util/ListResourceBundle.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util; + + +/// A resource bundle whose content a subclass supplies as an array of key +/// and value pairs. +public abstract class ListResourceBundle extends ResourceBundle { + + private Map lookup; + + public ListResourceBundle() { + super(); + } + + /// The bundle's content: each element is a pair, the key (a `String`) + /// and its value. + protected abstract Object[][] getContents(); + + private Map lookup() { + if (lookup == null) { + Map built = new HashMap(); + Object[][] contents = getContents(); + for (int i = 0; i < contents.length; i++) { + Object key = contents[i][0]; + Object value = contents[i][1]; + if (!(key instanceof String) || value == null) { + throw new NullPointerException(); + } + built.put((String) key, value); + } + lookup = built; + } + return lookup; + } + + public final Object handleGetObject(String key) { + if (key == null) { + throw new NullPointerException(); + } + return lookup().get(key); + } + + public Enumeration getKeys() { + return new ResourceBundle.KeyEnumeration(keySet().iterator()); + } + + protected Set handleKeySet() { + return lookup().keySet(); + } +} diff --git a/Ports/CLDC11/src/java/util/Locale.java b/Ports/CLDC11/src/java/util/Locale.java index 82250dc9fce..e1a7fb5aaf5 100644 --- a/Ports/CLDC11/src/java/util/Locale.java +++ b/Ports/CLDC11/src/java/util/Locale.java @@ -22,13 +22,89 @@ */ package java.util; +/// A language, a country and a variant, with the JDK's value semantics: +/// two locales of the same codes are equal, hash alike and print alike +/// (`en_US`). The codes are normalized as the JDK normalizes them -- the +/// language to lower case, the country to upper case, with an ASCII fold, so +/// a device set to Turkish cannot turn `IT` into a dotless `it`. +/// +/// This runtime carries no locale data beyond the English names of the +/// common languages and countries, so the `getDisplay*` methods answer in +/// English whatever locale they are asked for, and fall back to the code. +/// A script and extensions are not kept: `getScript` is always empty. public class Locale { + /// English. + public static final Locale ENGLISH = new Locale("en", ""); + /// French. + public static final Locale FRENCH = new Locale("fr", ""); + /// German. + public static final Locale GERMAN = new Locale("de", ""); + /// Italian. + public static final Locale ITALIAN = new Locale("it", ""); + /// Japanese. + public static final Locale JAPANESE = new Locale("ja", ""); + /// Korean. + public static final Locale KOREAN = new Locale("ko", ""); + /// Chinese. + public static final Locale CHINESE = new Locale("zh", ""); + /// Chinese as written in China. + public static final Locale SIMPLIFIED_CHINESE = new Locale("zh", "CN"); + /// Chinese as written in Taiwan. + public static final Locale TRADITIONAL_CHINESE = new Locale("zh", "TW"); + /// French in France. + public static final Locale FRANCE = new Locale("fr", "FR"); + /// German in Germany. + public static final Locale GERMANY = new Locale("de", "DE"); + /// Italian in Italy. + public static final Locale ITALY = new Locale("it", "IT"); + /// Japanese in Japan. + public static final Locale JAPAN = new Locale("ja", "JP"); + /// Korean in Korea. + public static final Locale KOREA = new Locale("ko", "KR"); + /// Chinese in China; the same locale as `SIMPLIFIED_CHINESE`. + public static final Locale CHINA = SIMPLIFIED_CHINESE; + /// Chinese in China; the same locale as `SIMPLIFIED_CHINESE`. + public static final Locale PRC = SIMPLIFIED_CHINESE; + /// Chinese in Taiwan; the same locale as `TRADITIONAL_CHINESE`. + public static final Locale TAIWAN = TRADITIONAL_CHINESE; + /// English in the United Kingdom. + public static final Locale UK = new Locale("en", "GB"); + /// English in the United States. + public static final Locale US = new Locale("en", "US"); + /// English in Canada. + public static final Locale CANADA = new Locale("en", "CA"); + /// French in Canada. + public static final Locale CANADA_FRENCH = new Locale("fr", "CA"); + /// The root locale: no language, country or variant. + public static final Locale ROOT = new Locale("", ""); + private static Locale defaultLocale; private String language; private String country; - public Locale(String language, String locale) { - this.language = language; - this.country = locale; + private String variant; + + /// A locale of a language alone. + public Locale(String language) { + this(language, "", ""); + } + + /// A locale of a language and a country. + public Locale(String language, String country) { + this(language, country, ""); + } + + /// A locale of a language, a country and a variant. + /// + /// #### Throws + /// + /// - `NullPointerException`: if any of them is null + public Locale(String language, String country, String variant) { + if (language == null || country == null || variant == null) { + throw new NullPointerException(); + } + this.language = normalizeLanguage(language); + this.country = asciiUpper(country); + this.variant = variant; } Locale() { @@ -45,6 +121,24 @@ public Locale(String language, String locale) { if (country == null) { country = "US";//getOSCountry(); } + language = language == null ? "" : normalizeLanguage(language); + country = asciiUpper(country); + variant = ""; + } + + /// `new Locale(language)`. + public static Locale of(String language) { + return new Locale(language); + } + + /// `new Locale(language, country)`. + public static Locale of(String language, String country) { + return new Locale(language, country); + } + + /// `new Locale(language, country, variant)`. + public static Locale of(String language, String country, String variant) { + return new Locale(language, country, variant); } public static Locale getDefault() { @@ -55,6 +149,9 @@ public static Locale getDefault() { } public static void setDefault(Locale l) { + if (l == null) { + throw new NullPointerException("Can't set default locale to NULL"); + } defaultLocale = l; } @@ -66,5 +163,377 @@ public String getCountry() { return country; } + /// The variant, or the empty string. + public String getVariant() { + return variant; + } + + /// Always empty: this runtime does not keep a script. + public String getScript() { + return ""; + } + + /// Whether the locale carries extensions; never, here. + public boolean hasExtensions() { + return false; + } + + /// The locale without extensions, which is the locale itself here. + public Locale stripExtensions() { + return this; + } + + /// The locale an IETF BCP 47 language tag names: `fr`, `en-US`, + /// `zh-Hans-CN`. The first subtag is the language, a four letter subtag + /// after it a script (dropped), the next two letter or three digit subtag + /// the country and the subtags after it, five to eight characters or a + /// digit and three more, the variant. A single character subtag starts + /// the extensions, which are dropped. `und` and the empty tag are the + /// root locale. + public static Locale forLanguageTag(String languageTag) { + if (languageTag == null) { + throw new NullPointerException(); + } + String language = ""; + String country = ""; + StringBuilder variant = null; + int n = languageTag.length(); + int start = 0; + int index = 0; + for (int i = 0; i <= n; i++) { + if (i < n && languageTag.charAt(i) != '-' && languageTag.charAt(i) != '_') { + continue; + } + String part = languageTag.substring(start, i); + start = i + 1; + if (index == 0) { + if (!isLetters(part) || part.length() < 2 || part.length() > 8) { + break; + } + language = "und".equals(asciiLower(part)) ? "" : part; + } else if (part.length() == 1 || part.length() == 0) { + // "x-lvariant-" carries a variant that is not well formed BCP 47, + // which is how toLanguageTag writes one; the JDK reads it back. + String rest = i < n ? languageTag.substring(i + 1) : ""; + if (part.length() == 1 && (part.charAt(0) == 'x' || part.charAt(0) == 'X') + && rest.length() > 9 && rest.regionMatches(true, 0, "lvariant", 0, 8) + && (rest.charAt(8) == '-' || rest.charAt(8) == '_')) { + String lvariant = rest.substring(9).replace('-', '_'); + if (variant == null) { + variant = new StringBuilder(lvariant); + } else { + variant.append('_').append(lvariant); + } + } + break; + } else if (index == 1 && part.length() == 4 && isLetters(part)) { + // a script + index++; + continue; + } else if (country.length() == 0 && variant == null + && ((part.length() == 2 && isLetters(part)) || (part.length() == 3 && isDigits(part)))) { + country = part; + } else if (part.length() >= 5 && part.length() <= 8 && isAlphanumeric(part) + || part.length() == 4 && part.charAt(0) >= '0' && part.charAt(0) <= '9' && isAlphanumeric(part)) { + if (variant == null) { + variant = new StringBuilder(part); + } else { + variant.append('_').append(part); + } + } else { + break; + } + index++; + } + return new Locale(language, country, variant == null ? "" : variant.toString()); + } + + /// The IETF BCP 47 tag of the locale: `en-US`, `fr`, or `und` for the + /// root. + public String toLanguageTag() { + StringBuilder out = new StringBuilder(); + out.append(language.length() == 0 ? "und" : language); + if (country.length() > 0) { + out.append('-').append(country); + } + if (variant.length() > 0) { + String tagged = variant.replace('_', '-'); + if (isWellFormedVariant(tagged)) { + out.append('-').append(tagged); + } else { + out.append("-x-lvariant-").append(tagged); + } + } + return out.toString(); + } + + private static boolean isWellFormedVariant(String tagged) { + int start = 0; + int n = tagged.length(); + for (int i = 0; i <= n; i++) { + if (i < n && tagged.charAt(i) != '-') { + continue; + } + String part = tagged.substring(start, i); + start = i + 1; + boolean ok = part.length() >= 5 && part.length() <= 8 && isAlphanumeric(part) + || part.length() == 4 && part.charAt(0) >= '0' && part.charAt(0) <= '9' && isAlphanumeric(part); + if (!ok) { + return false; + } + } + return true; + } + + /// Whether `obj` is a locale of the same language, country and variant. + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Locale)) { + return false; + } + Locale other = (Locale) obj; + return language.equals(other.language) && country.equals(other.country) && variant.equals(other.variant); + } + + /// A hash of the language, country and variant. + public int hashCode() { + return (language.hashCode() * 31 + country.hashCode()) * 31 + variant.hashCode(); + } + + /// `en_US`, `fr`, `en_US_POSIX`, or the empty string for the root locale. + public String toString() { + boolean l = language.length() > 0; + boolean c = country.length() > 0; + boolean v = variant.length() > 0; + if (!c && !(l && v)) { + return language; + } + StringBuilder out = new StringBuilder(language); + out.append('_'); + if (c) { + out.append(country); + } + if (v && (l || c)) { + out.append('_').append(variant); + } + return out.toString(); + } + + /// The English name of the language, or its code when this runtime has + /// no name for it. + public String getDisplayLanguage() { + return displayLanguage(language); + } + + /// As `getDisplayLanguage()`: names are in English only. + public String getDisplayLanguage(Locale inLocale) { + if (inLocale == null) { + throw new NullPointerException(); + } + return getDisplayLanguage(); + } + + /// The English name of the country, or its code when this runtime has no + /// name for it. + public String getDisplayCountry() { + return displayCountry(country); + } + + /// As `getDisplayCountry()`: names are in English only. + public String getDisplayCountry(Locale inLocale) { + if (inLocale == null) { + throw new NullPointerException(); + } + return getDisplayCountry(); + } + /// The variant as it is. + public String getDisplayVariant() { + return variant; + } + + /// As `getDisplayVariant()`. + public String getDisplayVariant(Locale inLocale) { + if (inLocale == null) { + throw new NullPointerException(); + } + return getDisplayVariant(); + } + + /// Always empty: this runtime does not keep a script. + public String getDisplayScript() { + return ""; + } + + /// Always empty: this runtime does not keep a script. + public String getDisplayScript(Locale inLocale) { + if (inLocale == null) { + throw new NullPointerException(); + } + return ""; + } + + /// `English (United States)`: the language, then the country and the + /// variant in parentheses. + public String getDisplayName() { + String lang = getDisplayLanguage(); + StringBuilder qualifiers = new StringBuilder(); + if (country.length() > 0) { + qualifiers.append(getDisplayCountry()); + } + if (variant.length() > 0) { + if (qualifiers.length() > 0) { + qualifiers.append(", "); + } + qualifiers.append(variant); + } + if (lang.length() == 0) { + return qualifiers.toString(); + } + if (qualifiers.length() == 0) { + return lang; + } + return lang + " (" + qualifiers + ")"; + } + + /// As `getDisplayName()`: names are in English only. + public String getDisplayName(Locale inLocale) { + if (inLocale == null) { + throw new NullPointerException(); + } + return getDisplayName(); + } + + /// The default locale and the locales this class has constants for. + public static Locale[] getAvailableLocales() { + return new Locale[] { + getDefault(), ENGLISH, FRENCH, GERMAN, ITALIAN, JAPANESE, KOREAN, CHINESE, + SIMPLIFIED_CHINESE, TRADITIONAL_CHINESE, FRANCE, GERMANY, ITALY, JAPAN, KOREA, UK, US, CANADA, + CANADA_FRENCH + }; + } + + /// The English names, in a holder so that the tables are built the + /// first time a display name is asked for, not by every use of a + /// `Locale`. + private static final class Names { + private Names() { + } + + private static final String[] LANGUAGE_NAMES = { + "ar", "Arabic", "bg", "Bulgarian", "ca", "Catalan", "cs", "Czech", "da", "Danish", + "de", "German", "el", "Greek", "en", "English", "es", "Spanish", "et", "Estonian", + "fa", "Persian", "fi", "Finnish", "fr", "French", "he", "Hebrew", "hi", "Hindi", + "hr", "Croatian", "hu", "Hungarian", "id", "Indonesian", "is", "Icelandic", "it", "Italian", + "ja", "Japanese", "ko", "Korean", "lt", "Lithuanian", "lv", "Latvian", "ms", "Malay", + "nl", "Dutch", "no", "Norwegian", "pl", "Polish", "pt", "Portuguese", "ro", "Romanian", + "ru", "Russian", "sk", "Slovak", "sl", "Slovenian", "sr", "Serbian", "sv", "Swedish", + "th", "Thai", "tr", "Turkish", "uk", "Ukrainian", "vi", "Vietnamese", "zh", "Chinese" + }; + + private static final String[] COUNTRY_NAMES = { + "AR", "Argentina", "AT", "Austria", "AU", "Australia", "BE", "Belgium", "BR", "Brazil", + "CA", "Canada", "CH", "Switzerland", "CN", "China", "CZ", "Czechia", "DE", "Germany", + "DK", "Denmark", "ES", "Spain", "FI", "Finland", "FR", "France", "GB", "United Kingdom", + "GR", "Greece", "HK", "Hong Kong SAR China", "IE", "Ireland", "IL", "Israel", "IN", "India", + "IT", "Italy", "JP", "Japan", "KR", "South Korea", "MX", "Mexico", "NL", "Netherlands", + "NO", "Norway", "NZ", "New Zealand", "PL", "Poland", "PT", "Portugal", "RU", "Russia", + "SE", "Sweden", "TR", "Turkey", "TW", "Taiwan", "UA", "Ukraine", "US", "United States", + "ZA", "South Africa" + }; + } + + private static String displayLanguage(String code) { + return lookup(Names.LANGUAGE_NAMES, code); + } + + private static String displayCountry(String code) { + return lookup(Names.COUNTRY_NAMES, code); + } + + private static String lookup(String[] table, String code) { + for (int i = 0; i < table.length; i += 2) { + if (table[i].equals(code)) { + return table[i + 1]; + } + } + return code; + } + + private static String normalizeLanguage(String language) { + String lower = asciiLower(language); + // The JDK answers the current ISO 639 code for the three it renamed. + if ("iw".equals(lower)) { + return "he"; + } + if ("ji".equals(lower)) { + return "yi"; + } + if ("in".equals(lower)) { + return "id"; + } + return lower; + } + + private static boolean isLetters(String s) { + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))) { + return false; + } + } + return s.length() > 0; + } + + private static boolean isDigits(String s) { + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c < '0' || c > '9') { + return false; + } + } + return s.length() > 0; + } + + private static boolean isAlphanumeric(String s) { + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'))) { + return false; + } + } + return s.length() > 0; + } + + // Language and country codes are ASCII, and String.toLowerCase folds by + // the device's locale: under Turkish "IT" would not become "it". + private static String asciiLower(String s) { + StringBuilder out = null; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c >= 'A' && c <= 'Z') { + if (out == null) { + out = new StringBuilder(s); + } + out.setCharAt(i, (char) (c + ('a' - 'A'))); + } + } + return out == null ? s : out.toString(); + } + + private static String asciiUpper(String s) { + StringBuilder out = null; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c >= 'a' && c <= 'z') { + if (out == null) { + out = new StringBuilder(s); + } + out.setCharAt(i, (char) (c - ('a' - 'A'))); + } + } + return out == null ? s : out.toString(); + } } diff --git a/Ports/CLDC11/src/java/util/LongSummaryStatistics.java b/Ports/CLDC11/src/java/util/LongSummaryStatistics.java new file mode 100644 index 00000000000..ff3db599dc2 --- /dev/null +++ b/Ports/CLDC11/src/java/util/LongSummaryStatistics.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util; + +import java.util.function.LongConsumer; + +/// The count, +/// sum, minimum, maximum and average of the values it was given. +public class LongSummaryStatistics implements LongConsumer { + + private long count; + private long sum; + private long min = Long.MAX_VALUE; + private long max = Long.MIN_VALUE; + + public LongSummaryStatistics() { + // Nothing seen yet. + } + + @Override + public void accept(long value) { + count++; + sum += value; + min = Math.min(min, value); + max = Math.max(max, value); + } + + public void accept(int value) { + accept((long) value); + } + + public void combine(LongSummaryStatistics other) { + count += other.count; + sum += other.sum; + min = Math.min(min, other.min); + max = Math.max(max, other.max); + } + + public final long getCount() { + return count; + } + + public final long getSum() { + return sum; + } + + public final long getMin() { + return min; + } + + public final long getMax() { + return max; + } + + public final double getAverage() { + return count > 0 ? (double) getSum() / count : 0.0d; + } +} diff --git a/Ports/CLDC11/src/java/util/Map.java b/Ports/CLDC11/src/java/util/Map.java index 3343f2f3594..2c9159e3300 100644 --- a/Ports/CLDC11/src/java/util/Map.java +++ b/Ports/CLDC11/src/java/util/Map.java @@ -89,7 +89,53 @@ public static interface Entry { /// /// object the replaced value of this entry. public V setValue(V object); - }; + + /// Orders entries by the natural order of their keys. + static , V> Comparator> comparingByKey() { + return new Comparator>() { + @Override + public int compare(Map.Entry a, Map.Entry b) { + return a.getKey().compareTo(b.getKey()); + } + }; + } + + /// Orders entries by the natural order of their values. + static > Comparator> comparingByValue() { + return new Comparator>() { + @Override + public int compare(Map.Entry a, Map.Entry b) { + return a.getValue().compareTo(b.getValue()); + } + }; + } + + /// Orders entries by their keys, as `comparator` orders them. + static Comparator> comparingByKey(final Comparator comparator) { + if (comparator == null) { + throw new NullPointerException(); + } + return new Comparator>() { + @Override + public int compare(Map.Entry a, Map.Entry b) { + return comparator.compare(a.getKey(), b.getKey()); + } + }; + } + + /// Orders entries by their values, as `comparator` orders them. + static Comparator> comparingByValue(final Comparator comparator) { + if (comparator == null) { + throw new NullPointerException(); + } + return new Comparator>() { + @Override + public int compare(Map.Entry a, Map.Entry b) { + return comparator.compare(a.getValue(), b.getValue()); + } + }; + } + } /// Removes all elements from this `Map`, leaving it empty. /// @@ -373,4 +419,88 @@ default V merge(K key, V value, BiFunction remappingFunction) { return null; } + + /// An unmodifiable map of no mappings. + static Map of() { + return ImmutableCollections.map(new Object[0]); + } + + /// An unmodifiable map of the given keys and values, none of which may be + /// null, and no key given twice. + static Map of(K k1, V v1) { + return ImmutableCollections.map(new Object[] {k1, v1}); + } + + /// An unmodifiable map of the given keys and values, none of which may be + /// null, and no key given twice. + static Map of(K k1, V v1, K k2, V v2) { + return ImmutableCollections.map(new Object[] {k1, v1, k2, v2}); + } + + /// An unmodifiable map of the given keys and values, none of which may be + /// null, and no key given twice. + static Map of(K k1, V v1, K k2, V v2, K k3, V v3) { + return ImmutableCollections.map(new Object[] {k1, v1, k2, v2, k3, v3}); + } + + /// An unmodifiable map of the given keys and values, none of which may be + /// null, and no key given twice. + static Map of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4) { + return ImmutableCollections.map(new Object[] {k1, v1, k2, v2, k3, v3, k4, v4}); + } + + /// An unmodifiable map of the given keys and values, none of which may be + /// null, and no key given twice. + static Map of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5) { + return ImmutableCollections.map(new Object[] {k1, v1, k2, v2, k3, v3, k4, v4, k5, v5}); + } + + /// An unmodifiable map of the given keys and values, none of which may be + /// null, and no key given twice. + static Map of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6) { + return ImmutableCollections.map(new Object[] {k1, v1, k2, v2, k3, v3, k4, v4, k5, v5, k6, v6}); + } + + /// An unmodifiable map of the given keys and values, none of which may be + /// null, and no key given twice. + static Map of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7) { + return ImmutableCollections.map(new Object[] {k1, v1, k2, v2, k3, v3, k4, v4, k5, v5, k6, v6, k7, v7}); + } + + /// An unmodifiable map of the given keys and values, none of which may be + /// null, and no key given twice. + static Map of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7, K k8, V v8) { + return ImmutableCollections.map(new Object[] {k1, v1, k2, v2, k3, v3, k4, v4, k5, v5, k6, v6, k7, v7, k8, v8}); + } + + /// An unmodifiable map of the given keys and values, none of which may be + /// null, and no key given twice. + static Map of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7, K k8, V v8, K k9, V v9) { + return ImmutableCollections.map(new Object[] {k1, v1, k2, v2, k3, v3, k4, v4, k5, v5, k6, v6, k7, v7, k8, v8, k9, v9}); + } + + /// An unmodifiable map of the given keys and values, none of which may be + /// null, and no key given twice. + static Map of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7, K k8, V v8, K k9, V v9, K k10, V v10) { + return ImmutableCollections.map(new Object[] {k1, v1, k2, v2, k3, v3, k4, v4, k5, v5, k6, v6, k7, v7, k8, v8, k9, v9, k10, v10}); + } + + /// An unmodifiable map of the keys and values of `entries`, none of which + /// may be null, and no key given twice. + @SafeVarargs + static Map ofEntries(Map.Entry... entries) { + return ImmutableCollections.mapOfEntries(entries); + } + + /// An unmodifiable entry of `key` and `value`, neither of which may be null. + static Map.Entry entry(K key, V value) { + return new ImmutableCollections.ImmutableEntry(key, value); + } + + /// An unmodifiable map of the mappings of `map`, which may not hold null; + /// `map` itself when it is already such a map. + static Map copyOf(Map map) { + return ImmutableCollections.mapCopyOf(map); + } + } diff --git a/Ports/CLDC11/src/java/util/MissingResourceException.java b/Ports/CLDC11/src/java/util/MissingResourceException.java new file mode 100644 index 00000000000..3ee1805c22c --- /dev/null +++ b/Ports/CLDC11/src/java/util/MissingResourceException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util; + +/// A resource bundle, or a key in one, could not be found. +public class MissingResourceException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + private final String className; + private final String key; + + public MissingResourceException(String s, String className, String key) { + super(s); + this.className = className; + this.key = key; + } + + public String getClassName() { + return className; + } + + public String getKey() { + return key; + } +} diff --git a/Ports/CLDC11/src/java/util/Objects.java b/Ports/CLDC11/src/java/util/Objects.java index 3b692434b52..da20c332680 100644 --- a/Ports/CLDC11/src/java/util/Objects.java +++ b/Ports/CLDC11/src/java/util/Objects.java @@ -31,6 +31,9 @@ /// /// This class consists of static utility methods for operating on objects. These utilities include null-safe or null-tolerant methods for computing the hash code of an object, returning a string for an object, and comparing two objects. public final class Objects { + private Objects() { + } + /// Returns true if the arguments are equal to each other and false otherwise. Consequently, if both arguments are null, true is returned and if exactly one argument is null, false is returned. Otherwise, equality is determined by using the equals method of the first argument. /// /// #### Parameters @@ -128,5 +131,74 @@ public static boolean deepEquals(Object a, Object b) { public static int hash(Object... values) { return Arrays.hashCode(values); } + + /// Whether `obj` is null. + public static boolean isNull(Object obj) { + return false; + } + + /// `obj`, or a NullPointerException whose message `messageSupplier` + /// makes, only when it is needed. + public static T requireNonNull(T obj, java.util.function.Supplier messageSupplier) { + return null; + } + + /// `obj`, or `defaultObj` when `obj` is null. + /// + /// #### Throws + /// + /// - `NullPointerException`: if both are null + public static T requireNonNullElse(T obj, T defaultObj) { + return null; + } + + /// `obj`, or what `supplier` gives when `obj` is null. + /// + /// #### Throws + /// + /// - `NullPointerException`: if `obj` is null and the supplier is null or gives null + public static T requireNonNullElseGet(T obj, java.util.function.Supplier supplier) { + return null; + } + + /// `index`, if it lies in `[0, length)`. + /// + /// #### Throws + /// + /// - `IndexOutOfBoundsException`: otherwise + public static int checkIndex(int index, int length) { + return 0; + } + + /// `index`, if it lies in `[0, length)`. + public static long checkIndex(long index, long length) { + return 0; + } + + /// `fromIndex`, if `[fromIndex, toIndex)` lies in `[0, length)`. + public static int checkFromToIndex(int fromIndex, int toIndex, int length) { + return 0; + } + + /// `fromIndex`, if `[fromIndex, toIndex)` lies in `[0, length)`. + public static long checkFromToIndex(long fromIndex, long toIndex, long length) { + return 0; + } + + /// `fromIndex`, if `[fromIndex, fromIndex + size)` lies in `[0, length)`. + public static int checkFromIndexSize(int fromIndex, int size, int length) { + return 0; + } + + /// `fromIndex`, if `[fromIndex, fromIndex + size)` lies in `[0, length)`. + public static long checkFromIndexSize(long fromIndex, long size, long length) { + return 0; + } + + /// What `Object.toString` would answer for `o`, even when its class + /// overrides it. + public static String toIdentityString(Object o) { + return null; + } } diff --git a/Ports/CLDC11/src/java/util/Optional.java b/Ports/CLDC11/src/java/util/Optional.java new file mode 100644 index 00000000000..6d733990170 --- /dev/null +++ b/Ports/CLDC11/src/java/util/Optional.java @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util; + +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; + +/// A container that may or may not hold a non-null value (the Java 8 API). The +/// stream terminals that may find nothing answer one. +public final class Optional { + private static final Optional EMPTY = new Optional(null); + + private final T value; + + private Optional(T value) { + this.value = value; + } + + @SuppressWarnings("unchecked") + public static Optional empty() { + return (Optional) EMPTY; + } + + public static Optional of(T value) { + if (value == null) { + throw new NullPointerException(); + } + return new Optional(value); + } + + public static Optional ofNullable(T value) { + return value == null ? Optional.empty() : new Optional(value); + } + + public T get() { + if (value == null) { + throw new NoSuchElementException("No value present"); + } + return value; + } + + public boolean isPresent() { + return value != null; + } + + public boolean isEmpty() { + return value == null; + } + + public void ifPresent(Consumer action) { + if (value != null) { + action.accept(value); + } + } + + public void ifPresentOrElse(Consumer action, Runnable emptyAction) { + if (value != null) { + action.accept(value); + } else { + emptyAction.run(); + } + } + + public Optional filter(Predicate predicate) { + if (predicate == null) { + throw new NullPointerException(); + } + return value == null || predicate.test(value) ? this : Optional.empty(); + } + + public Optional map(Function mapper) { + if (mapper == null) { + throw new NullPointerException(); + } + return value == null ? Optional.empty() : Optional.ofNullable(mapper.apply(value)); + } + + @SuppressWarnings("unchecked") + public Optional flatMap(Function> mapper) { + if (mapper == null) { + throw new NullPointerException(); + } + if (value == null) { + return Optional.empty(); + } + Optional r = (Optional) mapper.apply(value); + if (r == null) { + throw new NullPointerException(); + } + return r; + } + + public Optional or(Supplier> supplier) { + // Checked first, as the JDK does: a null supplier is an error whether or not it is needed. + if (supplier == null) { + throw new NullPointerException(); + } + if (value != null) { + return this; + } + @SuppressWarnings("unchecked") + Optional r = (Optional) supplier.get(); + if (r == null) { + throw new NullPointerException(); + } + return r; + } + + /// A stream of the value, or an empty stream when there is none. + public java.util.stream.Stream stream() { + return value == null ? java.util.stream.Stream.empty() : java.util.stream.Stream.of(value); + } + + public T orElse(T other) { + return value != null ? value : other; + } + + public T orElseGet(Supplier supplier) { + return value != null ? value : supplier.get(); + } + + public T orElseThrow() { + return get(); + } + + public T orElseThrow(Supplier exceptionSupplier) throws X { + if (value == null) { + throw exceptionSupplier.get(); + } + return value; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Optional)) { + return false; + } + Object other = ((Optional) obj).value; + return value == null ? other == null : value.equals(other); + } + + @Override + public int hashCode() { + return value == null ? 0 : value.hashCode(); + } + + @Override + public String toString() { + return value == null ? "Optional.empty" : "Optional[" + value + "]"; + } +} diff --git a/Ports/CLDC11/src/java/util/OptionalDouble.java b/Ports/CLDC11/src/java/util/OptionalDouble.java new file mode 100644 index 00000000000..d69bfce021c --- /dev/null +++ b/Ports/CLDC11/src/java/util/OptionalDouble.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util; + +import java.util.function.DoubleConsumer; +import java.util.stream.DoubleStream; +import java.util.function.DoubleSupplier; + +import java.util.function.Supplier; + +/// A container that +/// holds one `double` or nothing. +public final class OptionalDouble { + + private static final OptionalDouble EMPTY = new OptionalDouble(false, 0d); + + private final boolean present; + private final double value; + + private OptionalDouble(boolean present, double value) { + this.present = present; + this.value = value; + } + + public static OptionalDouble empty() { + return EMPTY; + } + + public static OptionalDouble of(double value) { + return new OptionalDouble(true, value); + } + + public double getAsDouble() { + if (!present) { + throw new NoSuchElementException("No value present"); + } + return value; + } + + public boolean isPresent() { + return present; + } + + public boolean isEmpty() { + return !present; + } + + public void ifPresent(DoubleConsumer action) { + if (present) { + action.accept(value); + } + } + + public void ifPresentOrElse(DoubleConsumer action, Runnable emptyAction) { + if (present) { + action.accept(value); + } else { + emptyAction.run(); + } + } + + public DoubleStream stream() { + return present ? DoubleStream.of(value) : DoubleStream.empty(); + } + + public double orElse(double other) { + return present ? value : other; + } + + public double orElseGet(DoubleSupplier supplier) { + return present ? value : supplier.getAsDouble(); + } + + public double orElseThrow() { + return getAsDouble(); + } + + public double orElseThrow(Supplier exceptionSupplier) throws X { + if (present) { + return value; + } + throw exceptionSupplier.get(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof OptionalDouble)) { + return false; + } + OptionalDouble other = (OptionalDouble) obj; + if (present != other.present) { + return false; + } + return !present || Double.compare(value, other.value) == 0; + } + + @Override + public int hashCode() { + return present ? Double.valueOf(value).hashCode() : 0; + } + + @Override + public String toString() { + return present ? "OptionalDouble[" + value + "]" : "OptionalDouble.empty"; + } +} diff --git a/Ports/CLDC11/src/java/util/OptionalInt.java b/Ports/CLDC11/src/java/util/OptionalInt.java new file mode 100644 index 00000000000..910427fc02f --- /dev/null +++ b/Ports/CLDC11/src/java/util/OptionalInt.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util; + +import java.util.function.IntConsumer; +import java.util.stream.IntStream; +import java.util.function.IntSupplier; + +import java.util.function.Supplier; + +/// A container that +/// holds one `int` or nothing. +public final class OptionalInt { + + private static final OptionalInt EMPTY = new OptionalInt(false, 0); + + private final boolean present; + private final int value; + + private OptionalInt(boolean present, int value) { + this.present = present; + this.value = value; + } + + public static OptionalInt empty() { + return EMPTY; + } + + public static OptionalInt of(int value) { + return new OptionalInt(true, value); + } + + public int getAsInt() { + if (!present) { + throw new NoSuchElementException("No value present"); + } + return value; + } + + public boolean isPresent() { + return present; + } + + public boolean isEmpty() { + return !present; + } + + public void ifPresent(IntConsumer action) { + if (present) { + action.accept(value); + } + } + + public void ifPresentOrElse(IntConsumer action, Runnable emptyAction) { + if (present) { + action.accept(value); + } else { + emptyAction.run(); + } + } + + public IntStream stream() { + return present ? IntStream.of(value) : IntStream.empty(); + } + + public int orElse(int other) { + return present ? value : other; + } + + public int orElseGet(IntSupplier supplier) { + return present ? value : supplier.getAsInt(); + } + + public int orElseThrow() { + return getAsInt(); + } + + public int orElseThrow(Supplier exceptionSupplier) throws X { + if (present) { + return value; + } + throw exceptionSupplier.get(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof OptionalInt)) { + return false; + } + OptionalInt other = (OptionalInt) obj; + if (present != other.present) { + return false; + } + return !present || Integer.compare(value, other.value) == 0; + } + + @Override + public int hashCode() { + return present ? Integer.valueOf(value).hashCode() : 0; + } + + @Override + public String toString() { + return present ? "OptionalInt[" + value + "]" : "OptionalInt.empty"; + } +} diff --git a/Ports/CLDC11/src/java/util/OptionalLong.java b/Ports/CLDC11/src/java/util/OptionalLong.java new file mode 100644 index 00000000000..ab313afcf5a --- /dev/null +++ b/Ports/CLDC11/src/java/util/OptionalLong.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util; + +import java.util.function.LongConsumer; +import java.util.stream.LongStream; +import java.util.function.LongSupplier; + +import java.util.function.Supplier; + +/// A container that +/// holds one `long` or nothing. +public final class OptionalLong { + + private static final OptionalLong EMPTY = new OptionalLong(false, 0L); + + private final boolean present; + private final long value; + + private OptionalLong(boolean present, long value) { + this.present = present; + this.value = value; + } + + public static OptionalLong empty() { + return EMPTY; + } + + public static OptionalLong of(long value) { + return new OptionalLong(true, value); + } + + public long getAsLong() { + if (!present) { + throw new NoSuchElementException("No value present"); + } + return value; + } + + public boolean isPresent() { + return present; + } + + public boolean isEmpty() { + return !present; + } + + public void ifPresent(LongConsumer action) { + if (present) { + action.accept(value); + } + } + + public void ifPresentOrElse(LongConsumer action, Runnable emptyAction) { + if (present) { + action.accept(value); + } else { + emptyAction.run(); + } + } + + public LongStream stream() { + return present ? LongStream.of(value) : LongStream.empty(); + } + + public long orElse(long other) { + return present ? value : other; + } + + public long orElseGet(LongSupplier supplier) { + return present ? value : supplier.getAsLong(); + } + + public long orElseThrow() { + return getAsLong(); + } + + public long orElseThrow(Supplier exceptionSupplier) throws X { + if (present) { + return value; + } + throw exceptionSupplier.get(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof OptionalLong)) { + return false; + } + OptionalLong other = (OptionalLong) obj; + if (present != other.present) { + return false; + } + return !present || Long.compare(value, other.value) == 0; + } + + @Override + public int hashCode() { + return present ? Long.valueOf(value).hashCode() : 0; + } + + @Override + public String toString() { + return present ? "OptionalLong[" + value + "]" : "OptionalLong.empty"; + } +} diff --git a/Ports/CLDC11/src/java/util/PrimitiveIterator.java b/Ports/CLDC11/src/java/util/PrimitiveIterator.java new file mode 100644 index 00000000000..ed941198567 --- /dev/null +++ b/Ports/CLDC11/src/java/util/PrimitiveIterator.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util; + +import java.util.function.DoubleConsumer; +import java.util.function.IntConsumer; +import java.util.function.LongConsumer; + +import java.util.function.Consumer; + +/// An iterator +/// that can hand out its elements unboxed. +public interface PrimitiveIterator extends Iterator { + + void forEachRemaining(T_CONS action); + + /// An iterator of `int` values. + interface OfInt extends PrimitiveIterator { + + int nextInt(); + + @Override + default void forEachRemaining(IntConsumer action) { + if (action == null) { + throw new NullPointerException(); + } + while (hasNext()) { + action.accept(nextInt()); + } + } + + @Override + default Integer next() { + return Integer.valueOf(nextInt()); + } + + default void forEachRemaining(Consumer action) { + if (action == null) { + throw new NullPointerException(); + } + while (hasNext()) { + action.accept(Integer.valueOf(nextInt())); + } + } + } + + /// An iterator of `long` values. + interface OfLong extends PrimitiveIterator { + + long nextLong(); + + @Override + default void forEachRemaining(LongConsumer action) { + if (action == null) { + throw new NullPointerException(); + } + while (hasNext()) { + action.accept(nextLong()); + } + } + + @Override + default Long next() { + return Long.valueOf(nextLong()); + } + + default void forEachRemaining(Consumer action) { + if (action == null) { + throw new NullPointerException(); + } + while (hasNext()) { + action.accept(Long.valueOf(nextLong())); + } + } + } + + /// An iterator of `double` values. + interface OfDouble extends PrimitiveIterator { + + double nextDouble(); + + @Override + default void forEachRemaining(DoubleConsumer action) { + if (action == null) { + throw new NullPointerException(); + } + while (hasNext()) { + action.accept(nextDouble()); + } + } + + @Override + default Double next() { + return Double.valueOf(nextDouble()); + } + + default void forEachRemaining(Consumer action) { + if (action == null) { + throw new NullPointerException(); + } + while (hasNext()) { + action.accept(Double.valueOf(nextDouble())); + } + } + } +} diff --git a/Ports/CLDC11/src/java/util/Properties.java b/Ports/CLDC11/src/java/util/Properties.java new file mode 100644 index 00000000000..f4e9b3ccceb --- /dev/null +++ b/Ports/CLDC11/src/java/util/Properties.java @@ -0,0 +1,470 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; + +/// String keys and values, a second table of defaults behind them, and the +/// `.properties` text format. +/// +/// The format is read as the JDK reads it: `#` and `!` comments, `=`, `:` or +/// white space between key and value, a trailing backslash continuing a line +/// (the next line's indentation dropped), and the escapes `\t`, `\n`, `\r`, +/// `\f` and backslash-u followed by four hexadecimal digits; a backslash +/// before any other character stands for that character. A stream is +/// ISO 8859-1; a reader is whatever its characters are. [#store] writes text +/// that [#load] reads back to the same table, escaping what the format +/// requires and, on a stream, every character outside printable ASCII. +/// +/// The XML form (`loadFromXML`, `storeToXML`) and `list` are not provided. +public class Properties extends Hashtable { + + private static final long serialVersionUID = 4112578634029874840L; + + private static final String HEX = "0123456789ABCDEF"; + + /// The table consulted for a key this one does not have, or null. + protected Properties defaults; + + private static final int NONE = 0, SLASH = 1, UNICODE = 2, CONTINUE = 3, + KEY_DONE = 4, IGNORE = 5; + + /// An empty table with no defaults. + public Properties() { + } + + /// An empty table with `defaults` behind it. + public Properties(Properties defaults) { + this.defaults = defaults; + } + + /// The value for `name` here, else in the defaults, else null. A value + /// that is not a `String` counts as absent, as in the JDK. + public String getProperty(String name) { + Object result = get(name); + if (result instanceof String) { + return (String) result; + } + return defaults == null ? null : defaults.getProperty(name); + } + + /// The value for `name` as [#getProperty(String)] finds it, or + /// `defaultValue` when there is none. + public String getProperty(String name, String defaultValue) { + String property = getProperty(name); + return property == null ? defaultValue : property; + } + + /// Maps `name` to `value` in this table, answering the value it replaced. + public Object setProperty(String name, String value) { + return put(name, value); + } + + /// Reads ISO 8859-1 `.properties` text, adding to what is here. The + /// stream is left open. + public synchronized void load(InputStream in) throws IOException { + if (in == null) { + throw new NullPointerException("in == null"); + } + load(new Latin1Reader(in)); + } + + /// Reads `.properties` text, adding to what is here. The reader is left + /// open. + @SuppressWarnings("fallthrough") + public synchronized void load(Reader in) throws IOException { + if (in == null) { + throw new NullPointerException("in == null"); + } + int mode = NONE, unicode = 0, count = 0; + char nextChar, buf[] = new char[40]; + int offset = 0, keyLength = -1, intVal; + boolean firstChar = true; + + Reader br = in; + + while (true) { + intVal = br.read(); + if (intVal == -1) { + break; + } + nextChar = (char) intVal; + + if (offset == buf.length) { + char[] newBuf = new char[buf.length * 2]; + System.arraycopy(buf, 0, newBuf, 0, offset); + buf = newBuf; + } + if (mode == UNICODE) { + int digit = Character.digit(nextChar, 16); + if (digit >= 0) { + unicode = (unicode << 4) + digit; + if (++count < 4) { + continue; + } + } else if (count <= 4) { + throw new IllegalArgumentException("Malformed \\uxxxx encoding."); + } + mode = NONE; + buf[offset++] = (char) unicode; + if (nextChar != '\n') { + continue; + } + } + if (mode == SLASH) { + mode = NONE; + switch (nextChar) { + case '\r': + mode = CONTINUE; // Look for a following \n + continue; + case '\n': + mode = IGNORE; // Ignore whitespace on the next line + continue; + case 'f': + nextChar = '\f'; + break; + case 'n': + nextChar = '\n'; + break; + case 'r': + nextChar = '\r'; + break; + case 't': + nextChar = '\t'; + break; + case 'u': + mode = UNICODE; + unicode = count = 0; + continue; + default: + // Any other escaped character is that character, `\b` + // included: the JDK has no backspace escape. + break; + } + } else { + switch (nextChar) { + case '#': + case '!': + if (firstChar) { + while (true) { + intVal = br.read(); + if (intVal == -1) { + break; + } + nextChar = (char) intVal; + if (nextChar == '\r' || nextChar == '\n') { + break; + } + } + continue; + } + break; + case '\n': + if (mode == CONTINUE) { // Part of a \r\n sequence + mode = IGNORE; // Ignore whitespace on the next line + continue; + } + // fall into the next case + case '\r': + mode = NONE; + firstChar = true; + if (offset > 0 || (offset == 0 && keyLength == 0)) { + if (keyLength == -1) { + keyLength = offset; + } + String temp = new String(buf, 0, offset); + put(temp.substring(0, keyLength), temp.substring(keyLength)); + } + keyLength = -1; + offset = 0; + continue; + case '\\': + if (mode == KEY_DONE) { + keyLength = offset; + } + mode = SLASH; + continue; + case ':': + case '=': + if (keyLength == -1) { // if parsing the key + mode = NONE; + keyLength = offset; + continue; + } + break; + default: + break; + } + if (nextChar == ' ' || nextChar == '\n' || nextChar == '\r' || nextChar == '\t' + || nextChar == '\f') { + if (mode == CONTINUE) { + mode = IGNORE; + } + // if key length == 0 or value length == 0 + if (offset == 0 || offset == keyLength || mode == IGNORE) { + continue; + } + if (keyLength == -1) { // if parsing the key + mode = KEY_DONE; + continue; + } + } + if (mode == IGNORE || mode == CONTINUE) { + mode = NONE; + } + } + firstChar = false; + if (mode == KEY_DONE) { + keyLength = offset; + mode = NONE; + } + buf[offset++] = nextChar; + } + if (mode == UNICODE && count <= 4) { + throw new IllegalArgumentException("Malformed \\uxxxx encoding."); + } + if (keyLength == -1 && offset > 0) { + keyLength = offset; + } + if (keyLength >= 0) { + String temp = new String(buf, 0, offset); + put(temp.substring(0, keyLength), temp.substring(keyLength)); + } + } + + /// Every key of this table and of its defaults. + public Enumeration propertyNames() { + Hashtable all = new Hashtable(); + collect(all, false); + return all.keys(); + } + + /// The keys, of this table and of its defaults, whose key and value are + /// both strings. + public Set stringPropertyNames() { + Hashtable all = new Hashtable(); + collect(all, true); + Set names = new HashSet(); + for (Object key : all.keySet()) { + if (key instanceof String) { + names.add((String) key); + } + } + return Collections.unmodifiableSet(names); + } + + /// Defaults first, so that this table's own entries replace them. + private void collect(Hashtable into, boolean stringsOnly) { + if (defaults != null) { + defaults.collect(into, stringsOnly); + } + for (Map.Entry e : entrySet()) { + Object key = e.getKey(); + Object value = e.getValue(); + if (stringsOnly && !(key instanceof String && value instanceof String)) { + continue; + } + into.put(key, value); + } + } + + /// Writes this table as [#store(OutputStream, String)] does, ignoring any + /// `IOException`. + /// + /// #### Deprecated + /// + /// An error writing is lost; use [#store(OutputStream, String)]. + @Deprecated + public void save(OutputStream out, String comments) { + try { + store(out, comments); + } catch (IOException e) { + // As documented: save reports nothing. + } + } + + /// Writes this table (not its defaults) as ISO 8859-1 `.properties` + /// text: the comment lines, a line with the current date, then one + /// `key=value` line per entry. The stream is flushed and left open. + /// + /// #### Throws + /// + /// - `ClassCastException`: if a key or value is not a `String` + public void store(OutputStream out, String comments) throws IOException { + if (out == null) { + throw new NullPointerException(); + } + String text = render(comments, true); + byte[] bytes = new byte[text.length()]; + for (int i = 0; i < bytes.length; i++) { + // Everything outside ASCII was escaped by render. + bytes[i] = (byte) text.charAt(i); + } + out.write(bytes); + out.flush(); + } + + /// Writes this table as `.properties` text, leaving characters outside + /// ASCII as they are. The writer is flushed and left open. + /// + /// #### Throws + /// + /// - `ClassCastException`: if a key or value is not a `String` + public void store(Writer writer, String comments) throws IOException { + if (writer == null) { + throw new NullPointerException(); + } + writer.write(render(comments, false)); + writer.flush(); + } + + private synchronized String render(String comments, boolean escapeUnicode) { + StringBuilder out = new StringBuilder(); + if (comments != null) { + writeComments(out, comments, escapeUnicode); + } + out.append('#').append(new Date().toString()).append('\n'); + for (Map.Entry e : entrySet()) { + Object key = e.getKey(); + Object value = e.getValue(); + if (!(key instanceof String) || !(value instanceof String)) { + // The JDK fails here with a ClassCastException, which a + // device cannot be relied on to raise from a cast. + throw new ClassCastException("Properties holds a key or value that is not a String"); + } + escape(out, (String) key, true, escapeUnicode); + out.append('='); + escape(out, (String) value, false, escapeUnicode); + out.append('\n'); + } + return out.toString(); + } + + /// Every line of `comments` as a comment line: a line that does not + /// start one already gets `#`. + private static void writeComments(StringBuilder out, String comments, boolean escapeUnicode) { + out.append('#'); + int n = comments.length(); + for (int i = 0; i < n; i++) { + char c = comments.charAt(i); + if (c == '\n' || c == '\r') { + if (c == '\r' && i + 1 < n && comments.charAt(i + 1) == '\n') { + i++; + } + out.append('\n'); + if (i + 1 >= n || (comments.charAt(i + 1) != '#' && comments.charAt(i + 1) != '!')) { + out.append('#'); + } + } else if (escapeUnicode && c > 0xff) { + unicode(out, c); + } else { + out.append(c); + } + } + out.append('\n'); + } + + private static void unicode(StringBuilder out, char c) { + out.append('\\').append('u'); + out.append(HEX.charAt((c >> 12) & 0xf)).append(HEX.charAt((c >> 8) & 0xf)); + out.append(HEX.charAt((c >> 4) & 0xf)).append(HEX.charAt(c & 0xf)); + } + + private static void escape(StringBuilder out, String s, boolean key, boolean escapeUnicode) { + int n = s.length(); + for (int i = 0; i < n; i++) { + char c = s.charAt(i); + switch (c) { + case ' ': + // Only a leading space of a value would be dropped on + // reading; in a key every space ends the key. + if (key || i == 0) { + out.append('\\'); + } + out.append(' '); + break; + case '\t': + out.append("\\t"); + break; + case '\n': + out.append("\\n"); + break; + case '\r': + out.append("\\r"); + break; + case '\f': + out.append("\\f"); + break; + case '=': + case ':': + case '#': + case '!': + case '\\': + out.append('\\').append(c); + break; + default: + if (escapeUnicode && (c < 0x20 || c > 0x7e)) { + unicode(out, c); + } else { + out.append(c); + } + } + } + } + + /// A stream's bytes as ISO 8859-1 characters, one for one. + private static final class Latin1Reader extends Reader { + private final InputStream in; + + Latin1Reader(InputStream in) { + this.in = in; + } + + @Override + public int read() throws IOException { + return in.read(); + } + + @Override + public int read(char[] cbuf, int off, int len) throws IOException { + int n = 0; + while (n < len) { + int b = in.read(); + if (b < 0) { + return n == 0 ? -1 : n; + } + cbuf[off + n++] = (char) b; + } + return n; + } + + @Override + public void close() throws IOException { + // The caller's stream stays open. + } + } +} diff --git a/Ports/CLDC11/src/java/util/PropertyResourceBundle.java b/Ports/CLDC11/src/java/util/PropertyResourceBundle.java new file mode 100644 index 00000000000..d0931528d35 --- /dev/null +++ b/Ports/CLDC11/src/java/util/PropertyResourceBundle.java @@ -0,0 +1,239 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; + +/// A resource bundle read from a `.properties` file. +/// +/// The parser implements the whole `java.util.Properties` text format: `#` and `!` +/// comment lines, `=`, `:` or whitespace between key and value, a logical line +/// continued by a trailing backslash, and the escapes -- tab, newline, form +/// feed, carriage return, backslash-u followed by four hex digits, and a +/// backslash before any other character standing for that character. +/// +/// A stream is decoded as ISO-8859-1, one byte to one character, which is +/// what the JDK did for as long as `.properties` files have carried +/// backslash-u escapes for everything else. Pass a `Reader` to choose another +/// encoding. +public class PropertyResourceBundle extends ResourceBundle { + + private final Map lookup = new HashMap(); + + public PropertyResourceBundle(InputStream stream) throws IOException { + StringBuilder text = new StringBuilder(); + byte[] buffer = new byte[4096]; + int n = stream.read(buffer); + while (n >= 0) { + for (int i = 0; i < n; i++) { + text.append((char) (buffer[i] & 0xff)); + } + n = stream.read(buffer); + } + parse(text.toString(), lookup); + } + + public PropertyResourceBundle(Reader reader) throws IOException { + StringBuilder text = new StringBuilder(); + char[] buffer = new char[4096]; + int n = reader.read(buffer, 0, buffer.length); + while (n >= 0) { + text.append(buffer, 0, n); + n = reader.read(buffer, 0, buffer.length); + } + parse(text.toString(), lookup); + } + + public Object handleGetObject(String key) { + if (key == null) { + throw new NullPointerException(); + } + return lookup.get(key); + } + + public Enumeration getKeys() { + return new ResourceBundle.KeyEnumeration(keySet().iterator()); + } + + protected Set handleKeySet() { + return lookup.keySet(); + } + + private static boolean isBlank(char c) { + return c == ' ' || c == '\t' || c == '\f'; + } + + private static boolean isLineEnd(char c) { + return c == '\n' || c == '\r'; + } + + /// Reads `.properties` text into `out`, a later definition of a key + /// replacing an earlier one. + static void parse(String text, Map out) { + int n = text.length(); + int i = 0; + StringBuilder line = new StringBuilder(); + while (i < n) { + // Blank lines and the indentation of a logical line carry nothing. + while (i < n && (isBlank(text.charAt(i)) || isLineEnd(text.charAt(i)))) { + i++; + } + if (i >= n) { + break; + } + char c = text.charAt(i); + if (c == '#' || c == '!') { + // A comment runs to the end of its natural line and cannot be + // continued, whatever it ends with. + while (i < n && !isLineEnd(text.charAt(i))) { + i++; + } + continue; + } + line.setLength(0); + boolean escaped = false; + // A line that is nothing but a continuation is no entry, unless + // the input ends in the middle of it: then the JDK defines the + // empty key, and so does this. + boolean cutShort = false; + while (i < n) { + c = text.charAt(i); + if (isLineEnd(c)) { + if (!escaped) { + break; + } + // An odd run of backslashes before the terminator joins + // the next natural line, minus its indentation. + line.setLength(line.length() - 1); + i++; + cutShort = i >= n; + if (c == '\r' && i < n && text.charAt(i) == '\n') { + i++; + } + while (i < n && isBlank(text.charAt(i))) { + i++; + } + escaped = false; + continue; + } + line.append(c); + escaped = c == '\\' && !escaped; + i++; + } + if (escaped) { + // A lone backslash at the end of the input escapes nothing. + line.setLength(line.length() - 1); + cutShort = true; + } + if (line.length() > 0 || cutShort) { + store(line, out); + } + } + } + + private static void store(StringBuilder line, Map out) { + int len = line.length(); + int keyLen = 0; + int valueStart = len; + boolean hasSeparator = false; + boolean escaped = false; + while (keyLen < len) { + char c = line.charAt(keyLen); + if ((c == '=' || c == ':') && !escaped) { + valueStart = keyLen + 1; + hasSeparator = true; + break; + } + if (isBlank(c) && !escaped) { + valueStart = keyLen + 1; + break; + } + escaped = c == '\\' && !escaped; + keyLen++; + } + // Whitespace may surround the one separator character. + while (valueStart < len) { + char c = line.charAt(valueStart); + if (!isBlank(c)) { + if (!hasSeparator && (c == '=' || c == ':')) { + hasSeparator = true; + } else { + break; + } + } + valueStart++; + } + out.put(unescape(line, 0, keyLen), unescape(line, valueStart, len)); + } + + private static int hexDigit(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'a' && c <= 'f') { + return c - 'a' + 10; + } + if (c >= 'A' && c <= 'F') { + return c - 'A' + 10; + } + throw new IllegalArgumentException("Malformed unicode escape in properties text"); + } + + private static String unescape(StringBuilder in, int from, int to) { + StringBuilder sb = new StringBuilder(to - from); + int i = from; + while (i < to) { + char c = in.charAt(i++); + if (c != '\\' || i >= to) { + if (c != '\\') { + sb.append(c); + } + continue; + } + c = in.charAt(i++); + if (c == 'u') { + if (i + 4 > to) { + throw new IllegalArgumentException("Malformed unicode escape in properties text"); + } + int value = 0; + for (int k = 0; k < 4; k++) { + value = (value << 4) | hexDigit(in.charAt(i++)); + } + sb.append((char) value); + } else if (c == 't') { + sb.append('\t'); + } else if (c == 'r') { + sb.append('\r'); + } else if (c == 'n') { + sb.append('\n'); + } else if (c == 'f') { + sb.append('\f'); + } else { + sb.append(c); + } + } + return sb.toString(); + } +} diff --git a/Ports/CLDC11/src/java/util/Random.java b/Ports/CLDC11/src/java/util/Random.java index 5ba7aeb1de1..ee7b07a1251 100644 --- a/Ports/CLDC11/src/java/util/Random.java +++ b/Ports/CLDC11/src/java/util/Random.java @@ -94,9 +94,17 @@ public boolean nextBoolean() { return false; } + /// Fills `bytes` with random bytes. Four bytes come from each `nextInt()`, + /// low byte first, which is the algorithm the JDK documents: a seeded + /// generator must hand out the same bytes as on the JVM, and one `next(8)` + /// per byte does not. public void nextBytes(byte[] bytes) { - for (int i = 0; i < bytes.length; ++i) { - bytes[i] = (byte) next(8); + int len = bytes.length; + for (int i = 0; i < len;) { + int rnd = nextInt(); + for (int n = Math.min(len - i, 4); n-- > 0; rnd >>= 8) { + bytes[i++] = (byte) rnd; + } } } diff --git a/Ports/CLDC11/src/java/util/ResourceBundle.java b/Ports/CLDC11/src/java/util/ResourceBundle.java new file mode 100644 index 00000000000..81cb4bf6549 --- /dev/null +++ b/Ports/CLDC11/src/java/util/ResourceBundle.java @@ -0,0 +1,331 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util; + +import com.codename1.impl.JdkHost; + +import java.io.IOException; +import java.io.InputStream; + +/// Locale specific strings and objects, looked up by key, with a chain of +/// less specific bundles behind each one. +/// +/// `getBundle("com.example.Messages", locale)` looks for three bundles, most +/// specific first -- `Messages__`, `Messages_` +/// and `Messages` -- and chains the ones that exist, so a key missing from the +/// first is answered by the next. When nothing more specific than the base +/// bundle exists for the requested locale, the default locale is tried the +/// same way before the base bundle is settled for. Finding none at all throws +/// `MissingResourceException`. +/// +/// Each of the three is first created as a class of that name extending +/// `ResourceBundle`, then read as the resource +/// `com/example/Messages_.properties`. An application built by +/// Codename One carries a registry, generated with it, that creates the bundle +/// classes it has with `new`; no class is then created from its name, which a +/// device cannot be relied on to do. Without a registry the class is looked up +/// by name. The resource is read through the platform +/// (`com.codename1.impl.JdkHost`), which knows where the build put it. +/// +/// Bundles are cached for the life of the application, or until +/// `clearCache()`. `ResourceBundle.Control`, and the lookups that take one, +/// are not provided. +public abstract class ResourceBundle { + + /// Every bundle already loaded, by base name and suffix; MISSING for one known not to exist. + private static final Map CACHE = new HashMap(); + + private static final Object MISSING = new Object(); + + /// The bundle consulted for a key this one does not have, or null. + protected ResourceBundle parent; + + private Locale locale; + private String baseName; + + public ResourceBundle() { + } + + public static final ResourceBundle getBundle(String baseName) { + return getBundle(baseName, Locale.getDefault()); + } + + /// Looks the bundle up as getBundle(String, Locale) does. The class loader + /// is not used: an application has one. + public static ResourceBundle getBundle(String baseName, Locale locale, ClassLoader loader) { + return getBundle(baseName, locale); + } + + public static final ResourceBundle getBundle(String baseName, Locale locale) { + if (baseName == null || locale == null) { + throw new NullPointerException(); + } + String name = baseName.replace('/', '.'); + String language = nonNull(locale.getLanguage()); + String country = nonNull(locale.getCountry()); + ResourceBundle found; + synchronized (CACHE) { + found = findChain(name, language, country); + // A bundle found by the chain has had its locale set; one that + // has none is the base bundle, like one of the empty language. + Locale foundLocale = found == null ? null : found.locale; + if (foundLocale == null || nonNull(foundLocale.getLanguage()).length() == 0) { + // Nothing specific to the requested locale: the default + // locale's bundle is preferred to the base bundle. + Locale fallback = Locale.getDefault(); + String fallbackLanguage = nonNull(fallback.getLanguage()); + String fallbackCountry = nonNull(fallback.getCountry()); + boolean requestedRoot = language.length() == 0; + boolean sameLocale = fallbackLanguage.equals(language) && fallbackCountry.equals(country); + if (!sameLocale && !(requestedRoot && found != null)) { + ResourceBundle viaDefault = findChain(name, fallbackLanguage, fallbackCountry); + if (viaDefault != null) { + found = viaDefault; + } + } + } + } + if (found == null) { + String localeName = localeSuffix(language, country); + throw new MissingResourceException( + "Can't find bundle for base name " + baseName + ", locale " + + (localeName.length() == 0 ? "" : localeName.substring(1)), + baseName + localeName, ""); + } + return found; + } + + public static final void clearCache() { + synchronized (CACHE) { + CACHE.clear(); + } + } + + private static String nonNull(String s) { + return s == null ? "" : s; + } + + private static String localeSuffix(String language, String country) { + if (language.length() == 0) { + return ""; + } + return country.length() == 0 ? "_" + language : "_" + language + "_" + country; + } + + /// The most specific bundle that exists for the locale, with the less + /// specific ones chained behind it, or null when not even the base bundle + /// exists. + private static ResourceBundle findChain(String baseName, String language, String country) { + ResourceBundle result = load(baseName, "", "", "", null); + if (language.length() > 0) { + ResourceBundle forLanguage = load(baseName, "_" + language, language, "", result); + if (forLanguage != null) { + result = forLanguage; + } + if (country.length() > 0) { + ResourceBundle forCountry = + load(baseName, "_" + language + "_" + country, language, country, result); + if (forCountry != null) { + result = forCountry; + } + } + } + return result; + } + + private static ResourceBundle load(String baseName, String suffix, String language, String country, + ResourceBundle parent) { + String key = baseName + '\n' + suffix; + Object cached = CACHE.get(key); + if (cached instanceof ResourceBundle) { + return (ResourceBundle) cached; + } + if (cached != null) { + return null; + } + ResourceBundle bundle = create(baseName, suffix); + if (bundle == null) { + CACHE.put(key, MISSING); + return null; + } + if (bundle.parent == null) { + // The less specific bundle of a name is the same whichever locale + // led here, so the chain is fixed the first time it is built. + bundle.parent = parent; + } + bundle.locale = new Locale(language, country); + bundle.baseName = baseName; + CACHE.put(key, bundle); + return bundle; + } + + private static ResourceBundle create(String baseName, String suffix) { + String name = baseName + suffix; + JdkHost host = JdkHost.get(); + Object made = host.newInstance(name); + if (made instanceof ResourceBundle) { + return (ResourceBundle) made; + } + if (JdkHost.getRegistry() == null) { + // No registry was generated with the application, so nothing says + // which bundle classes it has: ask for the class by name. + ResourceBundle byName = createByName(name); + if (byName != null) { + return byName; + } + } + InputStream in = host.resource(baseName.replace('.', '/') + suffix + ".properties"); + if (in == null) { + return null; + } + try { + return new PropertyResourceBundle(in); + } catch (IOException e) { + // A bundle that cannot be read is a bundle that is not there. + return null; + } finally { + try { + in.close(); + } catch (IOException e) { + // Nothing to do about a failed close of a stream already read. + } + } + } + + private static ResourceBundle createByName(String name) { + try { + Object instance = Class.forName(name).newInstance(); + if (instance instanceof ResourceBundle) { + return (ResourceBundle) instance; + } + } catch (ClassNotFoundException e) { + // No class of this name: the bundle is a properties file, or absent. + } catch (InstantiationException e) { + // Not a class this lookup can create; the file may still be there. + } catch (IllegalAccessException e) { + // As above. + } + return null; + } + + public final String getString(String key) { + Object value = getObject(key); + if (value instanceof String) { + return (String) value; + } + throw new ClassCastException("Resource " + key + " is not a String"); + } + + public final String[] getStringArray(String key) { + Object value = getObject(key); + if (value instanceof String[]) { + return (String[]) value; + } + throw new ClassCastException("Resource " + key + " is not a String array"); + } + + public final Object getObject(String key) { + if (key == null) { + throw new NullPointerException(); + } + for (ResourceBundle b = this; b != null; b = b.parent) { + Object value = b.handleGetObject(key); + if (value != null) { + return value; + } + } + throw new MissingResourceException( + "Can't find resource for bundle " + getClass().getName() + ", key " + key, + getClass().getName(), key); + } + + /// The locale this bundle was found for, which may be less specific than + /// the one asked for. The base bundle's has an empty language. + public Locale getLocale() { + return locale; + } + + public String getBaseBundleName() { + return baseName; + } + + protected void setParent(ResourceBundle parent) { + this.parent = parent; + } + + public boolean containsKey(String key) { + if (key == null) { + throw new NullPointerException(); + } + for (ResourceBundle b = this; b != null; b = b.parent) { + if (b.handleKeySet().contains(key)) { + return true; + } + } + return false; + } + + /// Every key of this bundle and of the bundles behind it. + public Set keySet() { + Set keys = new HashSet(); + for (ResourceBundle b = this; b != null; b = b.parent) { + keys.addAll(b.handleKeySet()); + } + return keys; + } + + /// The keys this bundle itself defines. The default derives them from + /// getKeys(); a subclass that has them at hand should override it. + protected Set handleKeySet() { + Set keys = new HashSet(); + Enumeration all = getKeys(); + while (all.hasMoreElements()) { + String key = all.nextElement(); + if (handleGetObject(key) != null) { + keys.add(key); + } + } + return keys; + } + + protected abstract Object handleGetObject(String key); + + public abstract Enumeration getKeys(); + + /// An Enumeration over an iterator, for the getKeys implementations. + static final class KeyEnumeration implements Enumeration { + private final Iterator it; + + KeyEnumeration(Iterator it) { + this.it = it; + } + + public boolean hasMoreElements() { + return it.hasNext(); + } + + public String nextElement() { + return it.next(); + } + } +} diff --git a/Ports/CLDC11/src/java/util/ServiceConfigurationError.java b/Ports/CLDC11/src/java/util/ServiceConfigurationError.java new file mode 100644 index 00000000000..2e0e66b2c3f --- /dev/null +++ b/Ports/CLDC11/src/java/util/ServiceConfigurationError.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util; + +/// What a [ServiceLoader] throws for a provider that cannot be had. The +/// build leaves such a provider out with a warning, so an iteration never +/// throws it here; [ServiceLoader.Provider#get()] does, for a provider whose +/// class creates no instance of the service. +public class ServiceConfigurationError extends Error { + + private static final long serialVersionUID = 1L; + + public ServiceConfigurationError(String msg) { + super(msg); + } + + public ServiceConfigurationError(String msg, Throwable cause) { + super(msg); + initCause(cause); + } +} diff --git a/Ports/CLDC11/src/java/util/ServiceLoader.java b/Ports/CLDC11/src/java/util/ServiceLoader.java new file mode 100644 index 00000000000..fb28c624e5c --- /dev/null +++ b/Ports/CLDC11/src/java/util/ServiceLoader.java @@ -0,0 +1,395 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util; + +import com.codename1.impl.JdkHost; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Stream; + +/// The providers of a service, as the `META-INF/services` files of the +/// application and of the libraries bundled with it list them. +/// +/// #### Where the providers come from +/// +/// A device discovers nothing at run time and creates no class from its +/// name. The build reads every `META-INF/services/` file the +/// application ships and generates a registry with the application that +/// names the providers of each service, in class path order, and creates each +/// of them with `new` (`com.codename1.impl.JdkHost.Registry`). So the set of +/// providers is fixed when the application is built. A listed class that the +/// application does not ship, that is abstract or that has no public +/// constructor without parameters is left out by the build with a warning; +/// the JDK throws `ServiceConfigurationError` for such a provider when the +/// iteration reaches it, and here it is simply not there. +/// +/// Where no registry was generated -- a program translated without Codename +/// One, a test -- the services file is read through the platform +/// (`JdkHost.resource`), as the JDK reads it, and the classes are created +/// from their names. +/// +/// #### What is the same as the JDK +/// +/// Providers are created lazily, one at a time as an iteration reaches them, +/// and once per loader: a second `iterator()` answers the instances the first +/// one created and goes on from where it stopped. [#reload()] forgets them. +/// [#stream()] creates nothing until [Provider#get()] is called, which +/// answers a new instance on every call. +/// +/// #### What is not there +/// +/// The class loader a loader is asked for selects nothing: an application +/// has one. Module layers, and providers that are a static `provider()` +/// method of a named module, are not provided. +public final class ServiceLoader implements Iterable { + + private final Class service; + + /// The provider class names of this service, or null until an iteration asks. + private List entries; + + /// The instances created so far, in order. + private final List instances = new ArrayList(); + + /// The index in [#entries] of the next provider to create. + private int next; + + /// Counts the reloads, so that an iterator taken before one can tell. + private int generation; + + /// Whether this is the loader of the installed providers, which has none. + private final boolean installed; + + private ServiceLoader(Class service, boolean installed) { + if (service == null) { + throw new NullPointerException("Service interface cannot be null"); + } + this.service = service; + this.installed = installed; + } + + /// One provider of a service, not yet created: what [#stream()] answers. + public interface Provider extends Supplier { + /// The provider's class. + Class type(); + + /// A new instance of the provider. + S get(); + } + + /// A loader of the providers of `service`. Nothing is looked up or + /// created until it is iterated. + public static ServiceLoader load(Class service) { + return new ServiceLoader(service, false); + } + + /// As [#load(Class)]: an application has one class loader, so `loader`, + /// which may be null as in the JDK, selects nothing. + public static ServiceLoader load(Class service, ClassLoader loader) { + return new ServiceLoader(service, false); + } + + /// As [#load(Class)], for a layer that is not null. Nothing here hands + /// out a module layer -- [Module#getLayer()] answers null -- so code + /// reaches this only with the null the JDK refuses as well. + public static ServiceLoader load(ModuleLayer layer, Class service) { + if (layer == null) { + throw new NullPointerException(); + } + return new ServiceLoader(service, false); + } + + /// The providers installed in the platform: on a desktop, the ones the + /// platform class loader sees. An application's own providers are not + /// among those, and a device installs none, so the loader is empty. + public static ServiceLoader loadInstalled(Class service) { + return new ServiceLoader(service, true); + } + + private List entries() { + if (entries == null) { + entries = installed ? new ArrayList() : lookup(service.getName()); + } + return entries; + } + + private static List lookup(String name) { + List out = new ArrayList(); + JdkHost host = JdkHost.get(); + if (JdkHost.getRegistry() != null) { + // The build registered every provider the application has. + String[] registered = host.providers(name); + for (int i = 0; i < registered.length; i++) { + out.add(registered[i]); + } + return out; + } + String text = read(host.resource("META-INF/services/" + name)); + if (text != null) { + providerNames(text, out); + } + return out; + } + + /// The provider class names a services file lists, in order: each line + /// up to its first `#`, trimmed, when anything is left, and a name that + /// appears twice once. A line that is not a class name is left out; the + /// JDK refuses the whole file for one. + private static void providerNames(String text, List out) { + int n = text.length(); + int start = 0; + while (start <= n) { + int end = start; + while (end < n && text.charAt(end) != '\n' && text.charAt(end) != '\r') { + end++; + } + String line = text.substring(start, end); + int hash = line.indexOf('#'); + if (hash >= 0) { + line = line.substring(0, hash); + } + line = line.trim(); + if (line.length() > 0 && isClassName(line) && !out.contains(line)) { + out.add(line); + } + start = end + 1; + } + } + + private static boolean isClassName(String name) { + int n = name.length(); + for (int i = 0; i < n; i++) { + char c = name.charAt(i); + boolean letter = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || c == '$' || c >= 0x80; + boolean digit = c >= '0' && c <= '9'; + if (i == 0 ? !letter : !(letter || digit || c == '.')) { + return false; + } + } + return n > 0; + } + + private static String read(InputStream in) { + if (in == null) { + return null; + } + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + for (int n = in.read(buffer); n >= 0; n = in.read(buffer)) { + bytes.write(buffer, 0, n); + } + return new String(bytes.toByteArray(), "UTF-8"); + } catch (IOException e) { + // A file that cannot be read lists nothing. + return null; + } finally { + try { + in.close(); + } catch (IOException e) { + // Nothing to do about a failed close of a stream already read. + } + } + } + + /// The class of the provider `className`, or null when there is none. + private static Class typeOf(String className) { + if (JdkHost.getRegistry() != null) { + return JdkHost.get().type(className); + } + try { + return Class.forName(className); + } catch (ClassNotFoundException e) { + return null; + } + } + + /// A new instance of the provider `className`, or null when none can be made. + private static Object instantiate(String className) { + if (JdkHost.getRegistry() != null) { + return JdkHost.get().newInstance(className); + } + try { + return Class.forName(className).newInstance(); + } catch (ClassNotFoundException e) { + return null; + } catch (InstantiationException e) { + return null; + } catch (IllegalAccessException e) { + return null; + } + } + + /// The instance of `className` as this service's type, or null when it + /// could not be created or is not one. Tested rather than cast: a cast + /// that fails does not throw on every device. + @SuppressWarnings("unchecked") + private S create(String className) { + Object made = instantiate(className); + return service.isInstance(made) ? (S) made : null; + } + + /// Whether a provider is left to create. Skips what cannot be one -- a + /// class that is not there, or is not of the service's type -- without + /// creating anything: `hasNext()` creates no provider in the JDK either. + private boolean more() { + List all = entries(); + while (next < all.size()) { + Class type = typeOf(all.get(next)); + if (type != null && service.isAssignableFrom(type)) { + return true; + } + next++; + } + return false; + } + + /// The providers, each created when `next()` reaches it. The instances + /// an earlier iteration created come first, without being created again. + public Iterator iterator() { + return new ProviderIterator(this); + } + + /// The iterator [#iterator()] answers. A named class: the class library + /// is translated without lambdas, and an anonymous one reads no better. + private static final class ProviderIterator implements Iterator { + private final ServiceLoader loader; + private final int born; + private int index; + + ProviderIterator(ServiceLoader loader) { + this.loader = loader; + this.born = loader.generation; + } + + /// After a reload this iterator's place means nothing: the instances + /// it walked are gone and the providers are counted from the start + /// again. Going on would create a provider it has already handed + /// out, so it refuses, as the JDK's does. + private void check() { + if (born != loader.generation) { + throw new ConcurrentModificationException(); + } + } + + public boolean hasNext() { + check(); + return index < loader.instances.size() || loader.more(); + } + + public S next() { + check(); + if (index < loader.instances.size()) { + return loader.instances.get(index++); + } + while (loader.more()) { + S made = loader.create(loader.entries().get(loader.next++)); + if (made != null) { + loader.instances.add(made); + index = loader.instances.size(); + return made; + } + } + throw new NoSuchElementException(); + } + + public void remove() { + throw new UnsupportedOperationException(); + } + } + + public void forEach(Consumer action) { + if (action == null) { + throw new NullPointerException(); + } + Iterator it = iterator(); + while (it.hasNext()) { + action.accept(it.next()); + } + } + + /// The first provider, created if it has not been, or an empty optional + /// when the service has none. + public Optional findFirst() { + Iterator it = iterator(); + return it.hasNext() ? Optional.of(it.next()) : Optional.empty(); + } + + /// The providers, none of them created: [Provider#get()] creates one, + /// anew on every call. + public Stream> stream() { + List> out = new ArrayList>(); + List all = entries(); + for (int i = 0; i < all.size(); i++) { + String className = all.get(i); + Class type = typeOf(className); + if (type != null && service.isAssignableFrom(type)) { + out.add(new Lazy(this, className, type)); + } + } + return out.stream(); + } + + private static final class Lazy implements Provider { + private final ServiceLoader loader; + private final String className; + private final Class type; + + Lazy(ServiceLoader loader, String className, Class type) { + this.loader = loader; + this.className = className; + this.type = type; + } + + @SuppressWarnings("unchecked") + public Class type() { + return (Class) type; + } + + public S get() { + S made = loader.create(className); + if (made == null) { + throw new ServiceConfigurationError(loader.service.getName() + ": Provider " + type.getName() + + " could not be instantiated"); + } + return made; + } + } + + /// Forgets the providers created so far, so that the next iteration + /// creates them again. + public void reload() { + generation++; + instances.clear(); + next = 0; + entries = null; + } + + public String toString() { + return "java.util.ServiceLoader[" + service.getName() + "]"; + } +} diff --git a/Ports/CLDC11/src/java/util/Set.java b/Ports/CLDC11/src/java/util/Set.java index bee049dc4ee..41d6db404d3 100644 --- a/Ports/CLDC11/src/java/util/Set.java +++ b/Ports/CLDC11/src/java/util/Set.java @@ -241,4 +241,72 @@ public interface Set extends Collection { /// /// - Collection#toArray(Object[]) public T[] toArray(T[] array); + + /// An unmodifiable set of no elements. + static Set of() { + return ImmutableCollections.set(new Object[] {}, true); + } + + /// An unmodifiable set of the given elements, none of which may be null and no two equal. + static Set of(E e1) { + return ImmutableCollections.set(new Object[] {e1}, true); + } + + /// An unmodifiable set of the given elements, none of which may be null and no two equal. + static Set of(E e1, E e2) { + return ImmutableCollections.set(new Object[] {e1, e2}, true); + } + + /// An unmodifiable set of the given elements, none of which may be null and no two equal. + static Set of(E e1, E e2, E e3) { + return ImmutableCollections.set(new Object[] {e1, e2, e3}, true); + } + + /// An unmodifiable set of the given elements, none of which may be null and no two equal. + static Set of(E e1, E e2, E e3, E e4) { + return ImmutableCollections.set(new Object[] {e1, e2, e3, e4}, true); + } + + /// An unmodifiable set of the given elements, none of which may be null and no two equal. + static Set of(E e1, E e2, E e3, E e4, E e5) { + return ImmutableCollections.set(new Object[] {e1, e2, e3, e4, e5}, true); + } + + /// An unmodifiable set of the given elements, none of which may be null and no two equal. + static Set of(E e1, E e2, E e3, E e4, E e5, E e6) { + return ImmutableCollections.set(new Object[] {e1, e2, e3, e4, e5, e6}, true); + } + + /// An unmodifiable set of the given elements, none of which may be null and no two equal. + static Set of(E e1, E e2, E e3, E e4, E e5, E e6, E e7) { + return ImmutableCollections.set(new Object[] {e1, e2, e3, e4, e5, e6, e7}, true); + } + + /// An unmodifiable set of the given elements, none of which may be null and no two equal. + static Set of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8) { + return ImmutableCollections.set(new Object[] {e1, e2, e3, e4, e5, e6, e7, e8}, true); + } + + /// An unmodifiable set of the given elements, none of which may be null and no two equal. + static Set of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9) { + return ImmutableCollections.set(new Object[] {e1, e2, e3, e4, e5, e6, e7, e8, e9}, true); + } + + /// An unmodifiable set of the given elements, none of which may be null and no two equal. + static Set of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9, E e10) { + return ImmutableCollections.set(new Object[] {e1, e2, e3, e4, e5, e6, e7, e8, e9, e10}, true); + } + + /// An unmodifiable set of `elements`, none of which may be null and no two equal. + @SafeVarargs + static Set of(E... elements) { + return ImmutableCollections.set(elements, true); + } + + /// An unmodifiable set of the elements of `collection`, which may not hold + /// null; `collection` itself when it is already such a set. + static Set copyOf(Collection collection) { + return ImmutableCollections.setCopyOf(collection); + } + } diff --git a/Ports/CLDC11/src/java/util/Spliterator.java b/Ports/CLDC11/src/java/util/Spliterator.java new file mode 100644 index 00000000000..fda97ba2f96 --- /dev/null +++ b/Ports/CLDC11/src/java/util/Spliterator.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util; + +import java.util.function.Consumer; + +/// A source of elements that hands them out one at a time, which is what +/// `java.util.stream.StreamSupport` makes a stream of. +/// +/// Streams here are sequential, so nothing splits one: `trySplit` may +/// answer null, and the ones this library makes always do. +public interface Spliterator { + + /// The elements have a defined encounter order. + int ORDERED = 0x00000010; + + /// No two elements are equal. + int DISTINCT = 0x00000001; + + /// The elements are in the order [#getComparator()] gives. + int SORTED = 0x00000004; + + /// [#estimateSize()] is exact. + int SIZED = 0x00000040; + + /// No element is null. + int NONNULL = 0x00000100; + + /// The source cannot be changed. + int IMMUTABLE = 0x00000400; + + /// The source may be changed safely while it is read. + int CONCURRENT = 0x00001000; + + /// The parts a split makes are sized too. + int SUBSIZED = 0x00004000; + + /// Hands the next element to `action` and answers true, or answers false + /// when there is none. + boolean tryAdvance(Consumer action); + + /// Hands every remaining element to `action`, in order. + default void forEachRemaining(Consumer action) { + while (tryAdvance(action)) { + // each call handed one element on + } + } + + /// A part of the elements to be read separately, or null when this + /// source does not split. + Spliterator trySplit(); + + /// How many elements remain, `Long.MAX_VALUE` when that is not known. + long estimateSize(); + + /// [#estimateSize()] when the source is [#SIZED], -1 otherwise. + default long getExactSizeIfKnown() { + return (characteristics() & SIZED) == 0 ? -1L : estimateSize(); + } + + /// The characteristics of this source, as a set of the bits declared here. + int characteristics(); + + /// Whether every bit of `characteristics` is one of this source's. + default boolean hasCharacteristics(int characteristics) { + return (characteristics() & characteristics) == characteristics; + } + + /// The order of a [#SORTED] source, null for the natural order. + /// + /// #### Throws + /// + /// - `IllegalStateException`: when the source is not sorted + default Comparator getComparator() { + throw new IllegalStateException(); + } +} diff --git a/Ports/CLDC11/src/java/util/Spliterators.java b/Ports/CLDC11/src/java/util/Spliterators.java new file mode 100644 index 00000000000..615052437f0 --- /dev/null +++ b/Ports/CLDC11/src/java/util/Spliterators.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util; + +import java.util.function.Consumer; + +/// Ways to make a [Spliterator] of a collection or an iterator, and an +/// iterator of a spliterator. +public final class Spliterators { + + private Spliterators() { + } + + /// A spliterator of no elements. + public static Spliterator emptySpliterator() { + return new IteratorSpliterator(null, Collections.emptyList().iterator(), 0, + Spliterator.SIZED | Spliterator.SUBSIZED); + } + + /// The elements of `c`. Its iterator is asked for when the first element + /// is, not now, so elements added in between are seen, as the JDK's + /// collections do. + public static Spliterator spliterator(Collection c, int characteristics) { + if (c == null) { + throw new NullPointerException(); + } + return new IteratorSpliterator(c, null, -1, sized(characteristics)); + } + + /// The `size` elements of `iterator`. + public static Spliterator spliterator(Iterator iterator, long size, int characteristics) { + if (iterator == null) { + throw new NullPointerException(); + } + return new IteratorSpliterator(null, iterator, size, sized(characteristics)); + } + + /// The elements of `iterator`, however many there are. + public static Spliterator spliteratorUnknownSize(Iterator iterator, int characteristics) { + if (iterator == null) { + throw new NullPointerException(); + } + return new IteratorSpliterator(null, iterator, Long.MAX_VALUE, + characteristics & ~(Spliterator.SIZED | Spliterator.SUBSIZED)); + } + + private static int sized(int characteristics) { + return (characteristics & Spliterator.CONCURRENT) == 0 + ? characteristics | Spliterator.SIZED | Spliterator.SUBSIZED : characteristics; + } + + /// The elements of `spliterator`, as an iterator. + public static Iterator iterator(final Spliterator spliterator) { + if (spliterator == null) { + throw new NullPointerException(); + } + return new Unspliterated(spliterator); + } + + /// What [#iterator(Spliterator)] answers: an element taken ahead to + /// answer `hasNext`. + private static final class Unspliterated implements Iterator, Consumer { + private final Spliterator spliterator; + private boolean ready; + private T next; + + Unspliterated(Spliterator spliterator) { + this.spliterator = spliterator; + } + + @Override + public void accept(T t) { + ready = true; + next = t; + } + + @Override + public boolean hasNext() { + if (!ready) { + spliterator.tryAdvance(this); + } + return ready; + } + + @Override + public T next() { + if (!ready && !hasNext()) { + throw new NoSuchElementException(); + } + ready = false; + T out = next; + next = null; + return out; + } + } + + /// A spliterator over an iterator, or over the iterator a collection + /// gives when it is first read. + private static final class IteratorSpliterator implements Spliterator { + private final Collection collection; + private Iterator it; + private long size; + private final int characteristics; + + IteratorSpliterator(Collection collection, Iterator it, long size, + int characteristics) { + this.collection = collection; + this.it = it; + this.size = size; + this.characteristics = characteristics; + } + + private Iterator open() { + if (it == null) { + it = collection.iterator(); + size = collection.size(); + } + return it; + } + + @Override + public boolean tryAdvance(Consumer action) { + if (action == null) { + throw new NullPointerException(); + } + Iterator source = open(); + if (!source.hasNext()) { + return false; + } + action.accept(source.next()); + return true; + } + + @Override + public void forEachRemaining(Consumer action) { + if (action == null) { + throw new NullPointerException(); + } + Iterator source = open(); + while (source.hasNext()) { + action.accept(source.next()); + } + } + + @Override + public Spliterator trySplit() { + return null; + } + + @Override + public long estimateSize() { + if (it == null) { + return collection.size(); + } + return size; + } + + @Override + public int characteristics() { + return characteristics; + } + } +} diff --git a/Ports/CLDC11/src/java/util/StringJoiner.java b/Ports/CLDC11/src/java/util/StringJoiner.java new file mode 100644 index 00000000000..a889db758a5 --- /dev/null +++ b/Ports/CLDC11/src/java/util/StringJoiner.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util; + +/// `java.util.StringJoiner`: builds a sequence +/// of strings separated by a delimiter, optionally between a prefix and a +/// suffix. +public final class StringJoiner { + + private final String prefix; + private final String delimiter; + private final String suffix; + private StringBuilder value; + private String emptyValue; + + public StringJoiner(CharSequence delimiter) { + this(delimiter, "", ""); + } + + public StringJoiner(CharSequence delimiter, CharSequence prefix, CharSequence suffix) { + if (prefix == null) { + throw new NullPointerException("The prefix must not be null"); + } + if (delimiter == null) { + throw new NullPointerException("The delimiter must not be null"); + } + if (suffix == null) { + throw new NullPointerException("The suffix must not be null"); + } + this.prefix = prefix.toString(); + this.delimiter = delimiter.toString(); + this.suffix = suffix.toString(); + this.emptyValue = this.prefix + this.suffix; + } + + public StringJoiner setEmptyValue(CharSequence emptyValue) { + if (emptyValue == null) { + throw new NullPointerException("The empty value must not be null"); + } + this.emptyValue = emptyValue.toString(); + return this; + } + + @Override + public String toString() { + if (value == null) { + return emptyValue; + } + if (suffix.length() == 0) { + return value.toString(); + } + return value.toString() + suffix; + } + + public StringJoiner add(CharSequence newElement) { + prepareBuilder().append(String.valueOf(newElement)); + return this; + } + + public StringJoiner merge(StringJoiner other) { + if (other == null) { + throw new NullPointerException(); + } + if (other.value != null) { + // Taken before the builder is prepared, so that merging a joiner + // into itself appends its content once. + String content = other.value.toString().substring(other.prefix.length()); + prepareBuilder().append(content); + } + return this; + } + + private StringBuilder prepareBuilder() { + if (value != null) { + value.append(delimiter); + } else { + value = new StringBuilder().append(prefix); + } + return value; + } + + public int length() { + return value != null ? value.length() + suffix.length() : emptyValue.length(); + } +} diff --git a/Ports/CLDC11/src/java/util/UUID.java b/Ports/CLDC11/src/java/util/UUID.java new file mode 100644 index 00000000000..a59d7e85b51 --- /dev/null +++ b/Ports/CLDC11/src/java/util/UUID.java @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util; + +import com.codename1.impl.JdkHost; + +/// A 128 bit identifier: its canonical text form, random (version 4) +/// generation and name based (version 3) generation. +/// +/// [#randomUUID()] draws its bits from the platform's secure random source +/// through `JdkHost`. Where the platform installed none it falls back to a +/// `Random` seeded from the clock, which keeps identifiers unique for every +/// practical purpose but NOT unpredictable; an application that uses one as +/// a secret should check that its platform provides secure random bytes. +/// +/// [#nameUUIDFromBytes(byte\[\])] needs an MD5 digest from the platform and +/// throws `UnsupportedOperationException` where there is none. The time based +/// accessors (`timestamp`, `clockSequence`, `node`) are not provided. +public final class UUID implements Comparable, java.io.Serializable { + + private static final long serialVersionUID = -4856846361193249489L; + + private static final String HEX = "0123456789abcdef"; + + /// Made the first time the host has no secure source; see the class + /// description. + private static Random fallback; + + private final long mostSigBits; + private final long leastSigBits; + + /// An identifier from its two halves. + public UUID(long mostSigBits, long leastSigBits) { + this.mostSigBits = mostSigBits; + this.leastSigBits = leastSigBits; + } + + /// A new version 4 identifier; see the class description for where its + /// randomness comes from. + public static UUID randomUUID() { + byte[] bits = new byte[16]; + try { + JdkHost.get().randomBytes(bits); + } catch (UnsupportedOperationException noSecureSource) { + insecureBytes(bits); + } + // Version 4 in the high nibble of the seventh byte, the IETF variant + // in the top two bits of the ninth. + bits[6] = (byte) ((bits[6] & 0x0f) | 0x40); + bits[8] = (byte) ((bits[8] & 0x3f) | 0x80); + return fromBytes(bits); + } + + private static synchronized void insecureBytes(byte[] out) { + if (fallback == null) { + fallback = new Random(System.currentTimeMillis() ^ (System.nanoTime() << 21)); + } + for (int i = 0; i < out.length; i++) { + out[i] = (byte) fallback.nextInt(256); + } + } + + /// The version 3 identifier of `name`: its MD5 digest, with the version + /// and variant bits set. + /// + /// #### Throws + /// + /// - `UnsupportedOperationException`: if the platform has no MD5 digest + public static UUID nameUUIDFromBytes(byte[] name) { + JdkHost.Digest md5 = JdkHost.get().digest("MD5"); + if (md5 == null) { + throw new UnsupportedOperationException("MD5 not supported"); + } + md5.update(name, 0, name.length); + byte[] bits = md5.digest(); + bits[6] = (byte) ((bits[6] & 0x0f) | 0x30); + bits[8] = (byte) ((bits[8] & 0x3f) | 0x80); + return fromBytes(bits); + } + + private static UUID fromBytes(byte[] bits) { + long most = 0; + long least = 0; + for (int i = 0; i < 8; i++) { + most = (most << 8) | (bits[i] & 0xff); + } + for (int i = 8; i < 16; i++) { + least = (least << 8) | (bits[i] & 0xff); + } + return new UUID(most, least); + } + + /// Parses the text form [#toString()] writes: five groups of hexadecimal + /// digits separated by `-`. As in the JDK, a group may be shorter than + /// its full width, and one that is longer is not refused either: the + /// digits past its width are dropped, so `100000000-0-0-0-0` is the + /// nil UUID there and here. Only a group of more than sixteen digits, + /// which no `long` holds, is an error. + /// + /// #### Throws + /// + /// - `IllegalArgumentException`: if `name` is not in that form + public static UUID fromString(String name) { + long[] groups = new long[5]; + int group = 0; + int digits = 0; + long value = 0; + int n = name.length(); + for (int i = 0; i <= n; i++) { + char c = i < n ? name.charAt(i) : '-'; + if (c == '-') { + if (digits == 0 || group > 4) { + throw new IllegalArgumentException("Invalid UUID string: " + name); + } + groups[group++] = value; + value = 0; + digits = 0; + continue; + } + int d = hexDigit(c); + if (d < 0 || digits >= 16) { + throw new IllegalArgumentException("Invalid UUID string: " + name); + } + value = (value << 4) | d; + digits++; + } + if (group != 5) { + throw new IllegalArgumentException("Invalid UUID string: " + name); + } + long most = ((groups[0] & 0xffffffffL) << 32) | ((groups[1] & 0xffffL) << 16) | (groups[2] & 0xffffL); + long least = ((groups[3] & 0xffffL) << 48) | (groups[4] & 0xffffffffffffL); + return new UUID(most, least); + } + + private static int hexDigit(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'a' && c <= 'f') { + return c - 'a' + 10; + } + if (c >= 'A' && c <= 'F') { + return c - 'A' + 10; + } + return -1; + } + + /// The most significant 64 bits. + public long getMostSignificantBits() { + return mostSigBits; + } + + /// The least significant 64 bits. + public long getLeastSignificantBits() { + return leastSigBits; + } + + /// The version number: 4 for one made by [#randomUUID()], 3 for one made + /// by [#nameUUIDFromBytes(byte\[\])]. + public int version() { + return (int) ((mostSigBits >> 12) & 0x0f); + } + + /// The variant number: 2 for the IETF layout every identifier made here + /// has. + public int variant() { + return (int) ((leastSigBits >>> (64 - (leastSigBits >>> 62))) & (leastSigBits >> 63)); + } + + @Override + public String toString() { + StringBuilder out = new StringBuilder(36); + hex(out, mostSigBits >> 32, 8); + out.append('-'); + hex(out, mostSigBits >> 16, 4); + out.append('-'); + hex(out, mostSigBits, 4); + out.append('-'); + hex(out, leastSigBits >> 48, 4); + out.append('-'); + hex(out, leastSigBits, 12); + return out.toString(); + } + + private static void hex(StringBuilder out, long value, int digits) { + for (int shift = (digits - 1) * 4; shift >= 0; shift -= 4) { + out.append(HEX.charAt((int) ((value >> shift) & 0xf))); + } + } + + @Override + public int hashCode() { + long hilo = mostSigBits ^ leastSigBits; + return ((int) (hilo >> 32)) ^ (int) hilo; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof UUID)) { + return false; + } + UUID other = (UUID) obj; + return mostSigBits == other.mostSigBits && leastSigBits == other.leastSigBits; + } + + /// Orders by the most significant half, then the least, each as a signed + /// number -- the order the JDK defines. + @Override + public int compareTo(UUID val) { + if (mostSigBits != val.mostSigBits) { + return mostSigBits < val.mostSigBits ? -1 : 1; + } + if (leastSigBits != val.leastSigBits) { + return leastSigBits < val.leastSigBits ? -1 : 1; + } + return 0; + } +} diff --git a/Ports/CLDC11/src/java/util/WeakHashMap.java b/Ports/CLDC11/src/java/util/WeakHashMap.java new file mode 100644 index 00000000000..47b336dd589 --- /dev/null +++ b/Ports/CLDC11/src/java/util/WeakHashMap.java @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util; + +import java.lang.ref.WeakReference; + +/// `java.util.WeakHashMap`: a map that does not +/// keep its keys alive. An entry disappears once nothing else refers to its +/// key, which is what lets desktop code hang data off a component -- a layout +/// manager's cached constraints, say -- without ever removing it. +/// +/// Keys are held through the device's `WeakReference` and compared with +/// `equals`, as the JDK's are. Entries whose key is gone are dropped by the +/// next change to the map, by [#size()], and by reads too, in a sweep that +/// runs once per roughly as many reads as the map has keys -- so a cache that +/// is only ever read still lets go of its values, at an amortized constant +/// cost per read. There is no reference queue on a device, so nothing is +/// dropped in between, and [#size()] counts only the entries still alive. +/// +/// [#entrySet()], [#keySet()] and [#values()] are snapshots taken when they +/// are called: removing through them, or through their iterators, does not +/// change the map. The JDK's are live views; code that edits a weak map +/// through a view is rare enough that the simpler contract is kept. +/// +/// Like every shared JDK class here it is not synchronized, and neither is +/// the JDK's. +public class WeakHashMap extends AbstractMap { + + /// Stands in for the null key, which a weak reference cannot tell from a + /// key that was collected. + private static final Object NULL_KEY = new Object(); + + /// The entries by their key's hash code. + private final HashMap>> buckets = new HashMap>>(); + + /// Reads since the last sweep; see the class description. + private int readsSinceExpunge; + + private static final class Cell { + @SuppressWarnings("rawtypes") + private final WeakReference key; + private V value; + + @SuppressWarnings({"rawtypes", "unchecked"}) + Cell(Object key, V value) { + this.key = new WeakReference(key); + this.value = value; + } + } + + public WeakHashMap() { + // Nothing to size: the buckets grow as they are used. + } + + public WeakHashMap(int initialCapacity) { + if (initialCapacity < 0) { + throw new IllegalArgumentException("Illegal Initial Capacity: " + initialCapacity); + } + } + + public WeakHashMap(int initialCapacity, float loadFactor) { + this(initialCapacity); + if (loadFactor <= 0 || Float.isNaN(loadFactor)) { + throw new IllegalArgumentException("Illegal Load factor: " + loadFactor); + } + } + + public WeakHashMap(Map m) { + putAll(m); + } + + private static Object mask(Object key) { + return key == null ? NULL_KEY : key; + } + + /// The cell of `key` (already masked), or null. + private Cell find(Object key) { + List> bucket = buckets.get(Integer.valueOf(key.hashCode())); + if (bucket == null) { + return null; + } + for (int i = 0; i < bucket.size(); i++) { + Cell c = bucket.get(i); + Object k = c.key.get(); + if (k == key || (k != null && k.equals(key))) { + return c; + } + } + return null; + } + + /// Sweeps from a read once enough reads have passed to pay for it. + private void expungeFromRead() { + if (++readsSinceExpunge > buckets.size() + 16) { + expunge(); + } + } + + /// Drops every entry whose key has been collected. + private void expunge() { + readsSinceExpunge = 0; + Iterator>> all = buckets.values().iterator(); + while (all.hasNext()) { + List> bucket = all.next(); + for (int i = bucket.size() - 1; i >= 0; i--) { + if (bucket.get(i).key.get() == null) { + bucket.remove(i); + } + } + if (bucket.isEmpty()) { + all.remove(); + } + } + } + + @Override + public int size() { + expunge(); + int n = 0; + for (List> bucket : buckets.values()) { + n += bucket.size(); + } + return n; + } + + @Override + public boolean isEmpty() { + return size() == 0; + } + + @Override + public boolean containsKey(Object key) { + expungeFromRead(); + return find(mask(key)) != null; + } + + @Override + public V get(Object key) { + expungeFromRead(); + Cell c = find(mask(key)); + return c == null ? null : c.value; + } + + @Override + public V put(K key, V value) { + Object k = mask(key); + Cell c = find(k); + if (c != null) { + V old = c.value; + c.value = value; + return old; + } + expunge(); + Integer hash = Integer.valueOf(k.hashCode()); + List> bucket = buckets.get(hash); + if (bucket == null) { + bucket = new ArrayList>(1); + buckets.put(hash, bucket); + } + bucket.add(new Cell(k, value)); + return null; + } + + @Override + public V remove(Object key) { + Object k = mask(key); + Integer hash = Integer.valueOf(k.hashCode()); + List> bucket = buckets.get(hash); + if (bucket == null) { + return null; + } + for (int i = 0; i < bucket.size(); i++) { + Cell c = bucket.get(i); + Object held = c.key.get(); + if (held == k || (held != null && held.equals(k))) { + bucket.remove(i); + if (bucket.isEmpty()) { + buckets.remove(hash); + } + return c.value; + } + } + return null; + } + + @Override + public void clear() { + buckets.clear(); + } + + /// A snapshot of the live entries; see the class comment. + @Override + @SuppressWarnings("unchecked") + public Set> entrySet() { + Set> out = new HashSet>(); + for (List> bucket : buckets.values()) { + for (int i = 0; i < bucket.size(); i++) { + Cell c = bucket.get(i); + Object k = c.key.get(); + if (k != null) { + out.add(new AbstractMap.SimpleEntry(k == NULL_KEY ? null : (K) k, c.value)); + } + } + } + return out; + } +} diff --git a/Ports/CLDC11/src/java/util/concurrent/AbstractExecutorService.java b/Ports/CLDC11/src/java/util/concurrent/AbstractExecutorService.java new file mode 100644 index 00000000000..1553c7e7b25 --- /dev/null +++ b/Ports/CLDC11/src/java/util/concurrent/AbstractExecutorService.java @@ -0,0 +1,276 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.concurrent; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/// The [ExecutorService] methods that need nothing but `execute`: a +/// subclass provides execution and the life cycle, and gets `submit`, +/// `invokeAll` and `invokeAny` from here. +public abstract class AbstractExecutorService implements ExecutorService { + + public AbstractExecutorService() { + } + + protected RunnableFuture newTaskFor(Runnable runnable, T value) { + return new FutureTask(runnable, value); + } + + protected RunnableFuture newTaskFor(Callable callable) { + return new FutureTask(callable); + } + + public Future submit(Runnable task) { + if (task == null) { + throw new NullPointerException(); + } + RunnableFuture ftask = newTaskFor(task, null); + execute(ftask); + return ftask; + } + + public Future submit(Runnable task, T result) { + if (task == null) { + throw new NullPointerException(); + } + RunnableFuture ftask = newTaskFor(task, result); + execute(ftask); + return ftask; + } + + public Future submit(Callable task) { + if (task == null) { + throw new NullPointerException(); + } + RunnableFuture ftask = newTaskFor(task); + execute(ftask); + return ftask; + } + + public List> invokeAll(Collection> tasks) throws InterruptedException { + if (tasks == null) { + throw new NullPointerException(); + } + List> futures = new ArrayList>(tasks.size()); + boolean done = false; + try { + for (Callable t : tasks) { + RunnableFuture f = newTaskFor(t); + futures.add(f); + execute(f); + } + for (int i = 0; i < futures.size(); i++) { + Future f = futures.get(i); + if (!f.isDone()) { + try { + f.get(); + } catch (CancellationException ignore) { + // The future reports it to whoever asks. + } catch (ExecutionException ignore) { + // The future reports it to whoever asks. + } + } + } + done = true; + return futures; + } finally { + if (!done) { + cancelAll(futures); + } + } + } + + public List> invokeAll(Collection> tasks, long timeout, TimeUnit unit) + throws InterruptedException { + if (tasks == null || unit == null) { + throw new NullPointerException(); + } + long deadline = BlockingWait.deadline(unit.toMillis(timeout)); + List> futures = new ArrayList>(tasks.size()); + boolean done = false; + try { + for (Callable t : tasks) { + futures.add(newTaskFor(t)); + } + for (int i = 0; i < futures.size(); i++) { + if (System.currentTimeMillis() >= deadline) { + return futures; + } + execute((Runnable) futures.get(i)); + } + for (int i = 0; i < futures.size(); i++) { + Future f = futures.get(i); + if (!f.isDone()) { + long left = deadline - System.currentTimeMillis(); + if (left <= 0) { + return futures; + } + try { + f.get(left, TimeUnit.MILLISECONDS); + } catch (CancellationException ignore) { + // The future reports it to whoever asks. + } catch (ExecutionException ignore) { + // The future reports it to whoever asks. + } catch (TimeoutException timedOut) { + return futures; + } + } + } + done = true; + return futures; + } finally { + if (!done) { + cancelAll(futures); + } + } + } + + /// Cancels every future that is not done, the LAST first. + /// + /// The order matters. Workers take tasks in the order they were + /// submitted, so cancelling from the front frees a worker -- a task + /// cancelled before it started returns at once -- which then starts the + /// next task in line before this loop has reached it: a task the caller + /// was promised would not run, runs. Cancelling from the back means + /// every task behind the one being cancelled is already cancelled. + static void cancelAll(List> futures) { + for (int i = futures.size() - 1; i >= 0; i--) { + futures.get(i).cancel(true); + } + } + + public T invokeAny(Collection> tasks) throws InterruptedException, ExecutionException { + try { + return doInvokeAny(tasks, BlockingWait.FOREVER); + } catch (TimeoutException cannotHappen) { + throw new ExecutionException(cannotHappen); + } + } + + public T invokeAny(Collection> tasks, long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + if (unit == null) { + throw new NullPointerException(); + } + return doInvokeAny(tasks, BlockingWait.deadline(unit.toMillis(timeout))); + } + + private T doInvokeAny(Collection> tasks, long deadline) + throws InterruptedException, ExecutionException, TimeoutException { + if (tasks == null) { + throw new NullPointerException(); + } + int n = tasks.size(); + if (n == 0) { + throw new IllegalArgumentException(); + } + Finished finished = new Finished(); + List> futures = new ArrayList>(n); + try { + for (Callable t : tasks) { + if (t == null) { + throw new NullPointerException(); + } + AnyTask f = new AnyTask(t, finished); + futures.add(f); + execute(f); + } + ExecutionException last = null; + for (int seen = 0; seen < n; seen++) { + Future f = finished.next(deadline); + if (f == null) { + throw new TimeoutException(); + } + try { + @SuppressWarnings("unchecked") + T value = ((Future) f).get(); + return value; + } catch (ExecutionException e) { + last = e; + } catch (CancellationException e) { + last = new ExecutionException(e); + } + } + throw last; + } finally { + cancelAll(futures); + } + } + + /// The futures of an `invokeAny` in the order they completed. + private static final class Finished { + private final List> queue = new ArrayList>(); + + void add(Future f) { + synchronized (this) { + queue.add(f); + notifyAll(); + } + } + + /// The next completed future, or null when none completed by `deadline`. + Future next(long deadline) throws InterruptedException { + synchronized (this) { + if (!queue.isEmpty()) { + return queue.remove(0); + } + } + new Wait(this, deadline).await(); + synchronized (this) { + return queue.isEmpty() ? null : queue.remove(0); + } + } + + boolean isEmpty() { + return queue.isEmpty(); + } + } + + private static final class Wait extends BlockingWait { + private final Finished finished; + + Wait(Finished finished, long deadline) { + super(finished, deadline); + this.finished = finished; + } + + boolean ready() { + return !finished.isEmpty(); + } + } + + private static final class AnyTask extends FutureTask { + private final Finished finished; + + AnyTask(Callable c, Finished finished) { + super(c); + this.finished = finished; + } + + protected void done() { + finished.add(this); + } + } +} diff --git a/Ports/CLDC11/src/java/util/concurrent/AsyncPool.java b/Ports/CLDC11/src/java/util/concurrent/AsyncPool.java new file mode 100644 index 00000000000..91dafb16352 --- /dev/null +++ b/Ports/CLDC11/src/java/util/concurrent/AsyncPool.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.concurrent; + +/// The threads the `...Async` methods of [CompletableFuture] run on when +/// they are given no executor. +/// +/// A cached pool of daemon threads: a task goes to an idle worker or to a +/// new one, so a task that waits for another never holds that one up -- a +/// bounded pool deadlocks the moment every worker waits on work still in its +/// queue. A worker ends after a minute without work, and none keeps a +/// program from ending, as with the JDK's common pool. +final class AsyncPool { + + private static final Object LOCK = new Object(); + private static Executor pool; + + private AsyncPool() { + } + + /// The pool, made when it is first needed. + static Executor get() { + synchronized (LOCK) { + if (pool == null) { + pool = new Executors.Pool(Integer.MAX_VALUE, 60000L, new Executors.DefaultThreadFactory(true)); + } + return pool; + } + } +} diff --git a/Ports/CLDC11/src/java/util/concurrent/BlockingWait.java b/Ports/CLDC11/src/java/util/concurrent/BlockingWait.java new file mode 100644 index 00000000000..735bc0d0336 --- /dev/null +++ b/Ports/CLDC11/src/java/util/concurrent/BlockingWait.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.concurrent; + +import com.codename1.impl.JdkHost; + +/// A wait on a monitor until a condition holds or a deadline passes, for +/// every blocking call in this package: `Future.get`, `awaitTermination`, +/// `CountDownLatch.await` and the like. +/// +/// The UI thread must never block. Called there, the wait runs through +/// `JdkHost.blockUi`, which on a Codename One device is +/// `Display.invokeAndBlock`: events keep being dispatched while it waits. +/// Everywhere else -- a worker, the backend, a VM test -- it is a plain +/// `Object.wait` on the caller's thread and is interruptible. +/// +/// Whoever makes the condition true calls `notifyAll()` on the same monitor +/// while holding it. +abstract class BlockingWait implements Runnable { + + /// A deadline that never comes. + static final long FOREVER = Long.MAX_VALUE; + + private final Object monitor; + private final long deadline; + /// Set by [#run] when the wait was interrupted on the thread `blockUi` + /// lent it; read back by [#await] on the UI thread. Guarded by `this`. + private InterruptedException interrupted; + + /// A wait on `monitor` until `deadline` ([#FOREVER], or a time from + /// [#deadline]). + BlockingWait(Object monitor, long deadline) { + this.monitor = monitor; + this.deadline = deadline; + } + + /// The deadline `millis` from now, saturating at [#FOREVER]; a negative + /// duration is due now. + static long deadline(long millis) { + long now = System.currentTimeMillis(); + if (millis <= 0) { + return now; + } + return millis >= FOREVER - now ? FOREVER : now + millis; + } + + /// Whether the wait is over. Called holding the monitor. + abstract boolean ready(); + + /// Waits until [#ready] or the deadline, whichever is first. The caller + /// asks [#ready] again (or reads its own state) to learn which. + /// + /// A thread that is interrupted does not start to wait: the interrupt is + /// consumed by the `InterruptedException`, as in the JDK. + final void await() throws InterruptedException { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + JdkHost host = JdkHost.get(); + if (!host.isUiThread()) { + block(); + return; + } + host.blockUi(this); + InterruptedException e; + synchronized (this) { + e = interrupted; + interrupted = null; + } + if (e != null) { + throw e; + } + } + + /// The wait, on the thread `JdkHost.blockUi` runs it on. + @Override + public final void run() { + try { + block(); + } catch (InterruptedException e) { + synchronized (this) { + interrupted = e; + } + } + } + + private void block() throws InterruptedException { + synchronized (monitor) { + while (!ready()) { + if (deadline == FOREVER) { + monitor.wait(); + } else { + long left = deadline - System.currentTimeMillis(); + if (left <= 0) { + return; + } + monitor.wait(left); + } + } + } + } +} diff --git a/Ports/CLDC11/src/java/util/concurrent/Callable.java b/Ports/CLDC11/src/java/util/concurrent/Callable.java new file mode 100644 index 00000000000..12897a5c521 --- /dev/null +++ b/Ports/CLDC11/src/java/util/concurrent/Callable.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.concurrent; + +/// A task that answers a result and may throw. +/// +/// @param the type of the result +public interface Callable { + + /// Computes the result. + V call() throws Exception; +} diff --git a/Ports/CLDC11/src/java/util/concurrent/CancellationException.java b/Ports/CLDC11/src/java/util/concurrent/CancellationException.java new file mode 100644 index 00000000000..75b1711bb75 --- /dev/null +++ b/Ports/CLDC11/src/java/util/concurrent/CancellationException.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.concurrent; + +/// Thrown by [Future#get] when the work was cancelled before it ran. +public class CancellationException extends IllegalStateException { + /// An exception with no message. + public CancellationException() { } + + /// An exception with `message`. + /// + /// @param message the detail message + public CancellationException(String message) { super(message); } +} diff --git a/Ports/CLDC11/src/java/util/concurrent/CompletableFuture.java b/Ports/CLDC11/src/java/util/concurrent/CompletableFuture.java new file mode 100644 index 00000000000..eef231277bc --- /dev/null +++ b/Ports/CLDC11/src/java/util/concurrent/CompletableFuture.java @@ -0,0 +1,1144 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.concurrent; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/// `java.util.concurrent.CompletableFuture` for the Codename One runtime. +/// +/// A stage completes on the thread that completed the one before it, as in +/// the JDK. The `...Async` methods without an executor run on a pool of +/// daemon threads -- there is no fork-join pool here -- and never on the UI +/// thread; a continuation that touches the user interface passes an executor +/// that calls `Display.callSerially`, or calls it itself. +/// +/// The class is safe to use from several threads at once: its state is +/// kept in atomics, and every dependent action runs exactly once. +/// +/// `get()` and `join()` on the UI thread wait through `JdkHost.blockUi`, +/// which on a device is `Display.invokeAndBlock`: events keep being +/// dispatched. +/// +/// Exceptions travel as in the JDK: a stage that depends on a failed one +/// fails with a [CompletionException] whose cause is the original; `get()` +/// reports that cause in an [ExecutionException], `join()` throws the +/// `CompletionException`, and both throw [CancellationException] after +/// `cancel`. +/// +/// Not provided: `obtrudeValue`, `obtrudeException`, +/// `getNumberOfDependents`, `delayedExecutor` and `state()`. +public class CompletableFuture implements Future, CompletionStage { + + /// A null result; null in [#result] is "not completed". + private static final Object NIL = new Object(); + + private static final class Failure { + final Throwable cause; + + Failure(Throwable cause) { + this.cause = cause; + } + } + + /// One action waiting for this future, in a stack of them. + private static final class Waiting { + final Runnable action; + Waiting next; + + Waiting(Runnable action) { + this.action = action; + } + } + + /// What the stack of waiting actions holds once they have been run. + private static final Waiting RELEASED = new Waiting(null); + + private final AtomicReference result = new AtomicReference(); + private final AtomicReference waiting = new AtomicReference(); + + public CompletableFuture() { + } + + // ------------------------------------------------------------------ + // Completion + // ------------------------------------------------------------------ + + private boolean completeRaw(Object outcome) { + if (!result.compareAndSet(null, outcome)) { + return false; + } + Waiting head = waiting.getAndSet(RELEASED); + // The stack is newest first; run in the order they were added. + Waiting ordered = null; + while (head != null && head != RELEASED) { + Waiting next = head.next; + head.next = ordered; + ordered = head; + head = next; + } + while (ordered != null) { + ordered.action.run(); + ordered = ordered.next; + } + return true; + } + + /// Runs `action` when this future is complete: now if it already is. + private void whenDone(Runnable action) { + Waiting w = new Waiting(action); + while (true) { + Waiting head = waiting.get(); + if (head == RELEASED) { + action.run(); + return; + } + w.next = head; + if (waiting.compareAndSet(head, w)) { + return; + } + } + } + + private static CompletionException wrap(Throwable t) { + if (t instanceof CompletionException) { + return (CompletionException) t; + } + return new CompletionException(t); + } + + private boolean fail(Throwable t) { + return completeRaw(new Failure(t)); + } + + /// Takes the outcome of a stage this one depends on. + private boolean relay(Object outcome) { + if (outcome instanceof Failure) { + return completeRaw(new Failure(wrap(((Failure) outcome).cause))); + } + return completeRaw(outcome); + } + + @SuppressWarnings("unchecked") + private static V value(Object outcome) { + return outcome == NIL ? null : (V) outcome; + } + + private static Throwable failure(Object outcome) { + return outcome instanceof Failure ? ((Failure) outcome).cause : null; + } + + public boolean complete(T value) { + return completeRaw(value == null ? NIL : value); + } + + public boolean completeExceptionally(Throwable ex) { + if (ex == null) { + throw new NullPointerException(); + } + return fail(ex); + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + boolean cancelled = result.get() == null && fail(new CancellationException()); + return cancelled || isCancelled(); + } + + @Override + public boolean isCancelled() { + return failure(result.get()) instanceof CancellationException; + } + + @Override + public boolean isDone() { + return result.get() != null; + } + + public boolean isCompletedExceptionally() { + return result.get() instanceof Failure; + } + + // ------------------------------------------------------------------ + // Reading the result + // ------------------------------------------------------------------ + + /// Waits for completion, for `millis` at most when that is not negative. + /// On the UI thread the wait goes through `JdkHost.blockUi`, so events + /// keep being dispatched; anywhere else it is interruptible. + private Object await(long millis) throws InterruptedException, TimeoutException { + Object outcome = result.get(); + if (outcome != null) { + return outcome; + } + if (millis == 0) { + throw new TimeoutException(); + } + final Signal signal = new Signal(); + whenDone(signal); + new SignalWait(signal, millis < 0 ? BlockingWait.FOREVER : BlockingWait.deadline(millis)).await(); + outcome = result.get(); + if (outcome == null) { + throw new TimeoutException(); + } + return outcome; + } + + /// Raised when the future completes, for a thread waiting on it. + private static final class Signal implements Runnable { + private boolean raised; + + public void run() { + synchronized (this) { + raised = true; + notifyAll(); + } + } + } + + private static final class SignalWait extends BlockingWait { + private final Signal signal; + + SignalWait(Signal signal, long deadline) { + super(signal, deadline); + this.signal = signal; + } + + boolean ready() { + return signal.raised; + } + } + + private T reportGet(Object outcome) throws ExecutionException { + if (outcome instanceof Failure) { + Throwable t = ((Failure) outcome).cause; + if (t instanceof CancellationException) { + throw (CancellationException) t; + } + if (t instanceof CompletionException && t.getCause() != null) { + t = t.getCause(); + } + throw new ExecutionException(t); + } + return CompletableFuture.value(outcome); + } + + private T reportJoin(Object outcome) { + if (outcome instanceof Failure) { + Throwable t = ((Failure) outcome).cause; + if (t instanceof CancellationException) { + throw (CancellationException) t; + } + throw wrap(t); + } + return CompletableFuture.value(outcome); + } + + @Override + public T get() throws InterruptedException, ExecutionException { + try { + return reportGet(await(-1)); + } catch (TimeoutException cannotHappen) { + // A wait without a limit ends only once the future is done. + throw new IllegalStateException(cannotHappen.toString()); + } + } + + @Override + public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + return reportGet(await(Math.max(0, unit.toMillis(timeout)))); + } + + /// The result, waiting for it; a failure is a [CompletionException]. + public T join() { + Object outcome = result.get(); + boolean interrupted = false; + while (outcome == null) { + try { + outcome = await(-1); + } catch (InterruptedException e) { + // join does not give up; it asks again, and hands the + // interrupt back to the thread once it is done. + interrupted = true; + outcome = result.get(); + } catch (TimeoutException e) { + outcome = result.get(); + } + } + if (interrupted) { + Thread.currentThread().interrupt(); + } + return reportJoin(outcome); + } + + public T getNow(T valueIfAbsent) { + Object outcome = result.get(); + return outcome == null ? valueIfAbsent : reportJoin(outcome); + } + + /// The result of a future that completed normally. + public T resultNow() { + Object outcome = result.get(); + if (outcome == null) { + throw new IllegalStateException("Task has not completed"); + } + if (outcome instanceof Failure) { + throw new IllegalStateException(isCancelled() ? "Task was cancelled" + : "Task completed with exception"); + } + return CompletableFuture.value(outcome); + } + + /// The exception of a future that failed. + public Throwable exceptionNow() { + Object outcome = result.get(); + if (outcome == null) { + throw new IllegalStateException("Task has not completed"); + } + if (!(outcome instanceof Failure)) { + throw new IllegalStateException("Task completed with a result"); + } + if (isCancelled()) { + throw new IllegalStateException("Task was cancelled"); + } + Throwable t = ((Failure) outcome).cause; + return t instanceof CompletionException && t.getCause() != null ? t.getCause() : t; + } + + // ------------------------------------------------------------------ + // Factories + // ------------------------------------------------------------------ + + /// The executor the `...Async` methods use when given none. + public Executor defaultExecutor() { + return pool(); + } + + private static Executor pool() { + return AsyncPool.get(); + } + + public CompletableFuture newIncompleteFuture() { + return new CompletableFuture(); + } + + public static CompletableFuture completedFuture(U value) { + CompletableFuture f = new CompletableFuture(); + f.complete(value); + return f; + } + + public static CompletableFuture failedFuture(Throwable ex) { + if (ex == null) { + throw new NullPointerException(); + } + CompletableFuture f = new CompletableFuture(); + f.fail(ex); + return f; + } + + public static CompletionStage completedStage(U value) { + return completedFuture(value); + } + + public static CompletionStage failedStage(Throwable ex) { + return failedFuture(ex); + } + + public static CompletableFuture supplyAsync(Supplier supplier) { + return supplyAsync(supplier, pool()); + } + + public static CompletableFuture supplyAsync(final Supplier supplier, Executor executor) { + if (supplier == null || executor == null) { + throw new NullPointerException(); + } + final CompletableFuture f = new CompletableFuture(); + executor.execute(new Runnable() { + @Override + public void run() { + if (f.isDone()) { + return; + } + U value; + try { + value = supplier.get(); + } catch (Throwable t) { + f.fail(wrap(t)); + return; + } + f.complete(value); + } + }); + return f; + } + + public static CompletableFuture runAsync(Runnable runnable) { + return runAsync(runnable, pool()); + } + + public static CompletableFuture runAsync(final Runnable runnable, Executor executor) { + if (runnable == null) { + throw new NullPointerException(); + } + return supplyAsync(new Supplier() { + @Override + public Void get() { + runnable.run(); + return null; + } + }, executor); + } + + public CompletableFuture completeAsync(Supplier supplier) { + return completeAsync(supplier, pool()); + } + + public CompletableFuture completeAsync(final Supplier supplier, Executor executor) { + if (supplier == null || executor == null) { + throw new NullPointerException(); + } + executor.execute(new Runnable() { + @Override + public void run() { + if (isDone()) { + return; + } + T value; + try { + value = supplier.get(); + } catch (Throwable t) { + fail(wrap(t)); + return; + } + complete(value); + } + }); + return this; + } + + public static CompletableFuture allOf(final CompletableFuture... futures) { + final CompletableFuture all = new CompletableFuture(); + if (futures.length == 0) { + all.complete(null); + return all; + } + final AtomicInteger remaining = new AtomicInteger(futures.length); + Runnable one = new Runnable() { + @Override + public void run() { + if (remaining.decrementAndGet() != 0) { + return; + } + for (CompletableFuture f : futures) { + Object outcome = f.result.get(); + if (outcome instanceof Failure) { + all.relay(outcome); + return; + } + } + all.complete(null); + } + }; + for (CompletableFuture f : futures) { + f.whenDone(one); + } + return all; + } + + public static CompletableFuture anyOf(CompletableFuture... futures) { + final CompletableFuture any = new CompletableFuture(); + for (final CompletableFuture f : futures) { + f.whenDone(new Runnable() { + @Override + public void run() { + any.relay(f.result.get()); + } + }); + } + return any; + } + + // ------------------------------------------------------------------ + // Time + // ------------------------------------------------------------------ + + /// Runs `action` after `millis`, unless this future completes first: + /// then the wait is given up, and holds neither a thread nor the + /// future. See [Delays]. + private void later(long millis, Runnable action) { + final Delays.Handle wait = Delays.after(millis, action); + whenDone(new Runnable() { + @Override + public void run() { + wait.cancel(); + } + }); + } + + /// Fails this future with a [TimeoutException] if it is not complete + /// when the time has passed. + public CompletableFuture orTimeout(long timeout, TimeUnit unit) { + // The unit is required even when there is nothing left to time. + if (unit == null) { + throw new NullPointerException(); + } + if (!isDone()) { + later(unit.toMillis(timeout), new Runnable() { + @Override + public void run() { + fail(new TimeoutException()); + } + }); + } + return this; + } + + public CompletableFuture completeOnTimeout(final T value, long timeout, TimeUnit unit) { + // The unit is required even when there is nothing left to time. + if (unit == null) { + throw new NullPointerException(); + } + if (!isDone()) { + later(unit.toMillis(timeout), new Runnable() { + @Override + public void run() { + complete(value); + } + }); + } + return this; + } + + // ------------------------------------------------------------------ + // Dependent stages + // ------------------------------------------------------------------ + + /// What a dependent stage does with the outcome of this one. + private interface Step { + void run(Object outcome, CompletableFuture stage) throws Throwable; + } + + /// A new stage completed by `step` once this one is complete, on + /// `executor` when there is one and on the completing thread otherwise. + private CompletableFuture then(final Executor executor, final Step step) { + final CompletableFuture stage = newIncompleteFuture(); + whenDone(new Runnable() { + @Override + public void run() { + final Object outcome = result.get(); + Runnable body = new Runnable() { + @Override + public void run() { + try { + step.run(outcome, stage); + } catch (Throwable t) { + stage.fail(wrap(t)); + } + } + }; + if (executor == null) { + body.run(); + return; + } + try { + executor.execute(body); + } catch (Throwable t) { + stage.fail(wrap(t)); + } + } + }); + return stage; + } + + private static Executor required(Executor executor) { + if (executor == null) { + throw new NullPointerException(); + } + return executor; + } + + private CompletableFuture apply(Executor executor, final Function fn) { + if (fn == null) { + throw new NullPointerException(); + } + return then(executor, new Step() { + @Override + public void run(Object outcome, CompletableFuture stage) { + if (outcome instanceof Failure) { + stage.relay(outcome); + } else { + stage.complete(fn.apply(CompletableFuture.value(outcome))); + } + } + }); + } + + @Override + public CompletableFuture thenApply(Function fn) { + return apply(null, fn); + } + + @Override + public CompletableFuture thenApplyAsync(Function fn) { + return apply(pool(), fn); + } + + @Override + public CompletableFuture thenApplyAsync(Function fn, Executor executor) { + return apply(required(executor), fn); + } + + private CompletableFuture accept(Executor executor, final Consumer action) { + if (action == null) { + throw new NullPointerException(); + } + return apply(executor, new Function() { + @Override + public Void apply(T value) { + action.accept(value); + return null; + } + }); + } + + @Override + public CompletableFuture thenAccept(Consumer action) { + return accept(null, action); + } + + @Override + public CompletableFuture thenAcceptAsync(Consumer action) { + return accept(pool(), action); + } + + @Override + public CompletableFuture thenAcceptAsync(Consumer action, Executor executor) { + return accept(required(executor), action); + } + + private CompletableFuture run(Executor executor, final Runnable action) { + if (action == null) { + throw new NullPointerException(); + } + return apply(executor, new Function() { + @Override + public Void apply(T value) { + action.run(); + return null; + } + }); + } + + @Override + public CompletableFuture thenRun(Runnable action) { + return run(null, action); + } + + @Override + public CompletableFuture thenRunAsync(Runnable action) { + return run(pool(), action); + } + + @Override + public CompletableFuture thenRunAsync(Runnable action, Executor executor) { + return run(required(executor), action); + } + + private CompletableFuture compose(Executor executor, + final Function> fn) { + if (fn == null) { + throw new NullPointerException(); + } + return then(executor, new Step() { + @Override + public void run(Object outcome, final CompletableFuture stage) { + if (outcome instanceof Failure) { + stage.relay(outcome); + return; + } + final CompletableFuture inner = fn.apply(CompletableFuture.value(outcome)) + .toCompletableFuture(); + inner.whenDone(new Runnable() { + @Override + public void run() { + stage.relay(inner.result.get()); + } + }); + } + }); + } + + @Override + public CompletableFuture thenCompose(Function> fn) { + return compose(null, fn); + } + + @Override + public CompletableFuture thenComposeAsync(Function> fn) { + return compose(pool(), fn); + } + + @Override + public CompletableFuture thenComposeAsync(Function> fn, + Executor executor) { + return compose(required(executor), fn); + } + + private CompletableFuture handled(Executor executor, + final BiFunction fn) { + if (fn == null) { + throw new NullPointerException(); + } + return then(executor, new Step() { + @Override + public void run(Object outcome, CompletableFuture stage) { + if (outcome instanceof Failure) { + stage.complete(fn.apply(null, ((Failure) outcome).cause)); + } else { + stage.complete(fn.apply(CompletableFuture.value(outcome), null)); + } + } + }); + } + + @Override + public CompletableFuture handle(BiFunction fn) { + return handled(null, fn); + } + + @Override + public CompletableFuture handleAsync(BiFunction fn) { + return handled(pool(), fn); + } + + @Override + public CompletableFuture handleAsync(BiFunction fn, + Executor executor) { + return handled(required(executor), fn); + } + + private CompletableFuture completed(Executor executor, final BiConsumer action) { + if (action == null) { + throw new NullPointerException(); + } + return then(executor, new Step() { + @Override + public void run(Object outcome, CompletableFuture stage) throws Throwable { + if (outcome instanceof Failure) { + try { + action.accept(null, ((Failure) outcome).cause); + } catch (Throwable t) { + // The stage keeps the failure it was told about, and + // since JDK 9 the action's own failure rides along as + // a suppressed exception of it (JDK 8 dropped it). + Throwable cause = ((Failure) outcome).cause; + if (cause != null && cause != t) { + cause.addSuppressed(t); + } + stage.relay(outcome); + return; + } + stage.relay(outcome); + } else { + action.accept(CompletableFuture.value(outcome), null); + stage.completeRaw(outcome); + } + } + }); + } + + @Override + public CompletableFuture whenComplete(BiConsumer action) { + return completed(null, action); + } + + @Override + public CompletableFuture whenCompleteAsync(BiConsumer action) { + return completed(pool(), action); + } + + @Override + public CompletableFuture whenCompleteAsync(BiConsumer action, + Executor executor) { + return completed(required(executor), action); + } + + private CompletableFuture recovered(Executor executor, final Function fn) { + if (fn == null) { + throw new NullPointerException(); + } + return then(executor, new Step() { + @Override + public void run(Object outcome, CompletableFuture stage) { + if (outcome instanceof Failure) { + stage.complete(fn.apply(((Failure) outcome).cause)); + } else { + stage.completeRaw(outcome); + } + } + }); + } + + @Override + public CompletableFuture exceptionally(Function fn) { + return recovered(null, fn); + } + + public CompletableFuture exceptionallyAsync(Function fn) { + return recovered(pool(), fn); + } + + public CompletableFuture exceptionallyAsync(Function fn, Executor executor) { + return recovered(required(executor), fn); + } + + public CompletableFuture exceptionallyCompose(final Function> fn) { + if (fn == null) { + throw new NullPointerException(); + } + return then(null, new Step() { + @Override + public void run(Object outcome, final CompletableFuture stage) { + if (!(outcome instanceof Failure)) { + stage.completeRaw(outcome); + return; + } + final CompletableFuture inner = fn.apply(((Failure) outcome).cause).toCompletableFuture(); + inner.whenDone(new Runnable() { + @Override + public void run() { + stage.relay(inner.result.get()); + } + }); + } + }); + } + + private CompletableFuture both(final Executor executor, CompletionStage other, + final BiFunction fn) { + if (fn == null || other == null) { + throw new NullPointerException(); + } + final CompletableFuture second = other.toCompletableFuture(); + final CompletableFuture stage = newIncompleteFuture(); + final AtomicInteger remaining = new AtomicInteger(2); + Runnable one = new Runnable() { + @Override + public void run() { + if (remaining.decrementAndGet() != 0) { + return; + } + final Object a = result.get(); + final Object b = second.result.get(); + if (a instanceof Failure) { + stage.relay(a); + return; + } + if (b instanceof Failure) { + stage.relay(b); + return; + } + Runnable body = new Runnable() { + @Override + public void run() { + try { + stage.complete(fn.apply(CompletableFuture.value(a), CompletableFuture.value(b))); + } catch (Throwable t) { + stage.fail(wrap(t)); + } + } + }; + if (executor == null) { + body.run(); + return; + } + try { + executor.execute(body); + } catch (Throwable t) { + stage.fail(wrap(t)); + } + } + }; + whenDone(one); + second.whenDone(one); + return stage; + } + + @Override + public CompletableFuture thenCombine(CompletionStage other, + BiFunction fn) { + return both(null, other, fn); + } + + @Override + public CompletableFuture thenCombineAsync(CompletionStage other, + BiFunction fn) { + return both(pool(), other, fn); + } + + @Override + public CompletableFuture thenCombineAsync(CompletionStage other, + BiFunction fn, Executor executor) { + return both(required(executor), other, fn); + } + + private CompletableFuture acceptBoth(Executor executor, CompletionStage other, + final BiConsumer action) { + if (action == null) { + throw new NullPointerException(); + } + return both(executor, other, new BiFunction() { + @Override + public Void apply(T a, U b) { + action.accept(a, b); + return null; + } + }); + } + + @Override + public CompletableFuture thenAcceptBoth(CompletionStage other, + BiConsumer action) { + return acceptBoth(null, other, action); + } + + @Override + public CompletableFuture thenAcceptBothAsync(CompletionStage other, + BiConsumer action) { + return acceptBoth(pool(), other, action); + } + + @Override + public CompletableFuture thenAcceptBothAsync(CompletionStage other, + BiConsumer action, Executor executor) { + return acceptBoth(required(executor), other, action); + } + + private CompletableFuture runBoth(Executor executor, CompletionStage other, final Runnable action) { + if (action == null) { + throw new NullPointerException(); + } + return both(executor, other, new BiFunction() { + @Override + public Void apply(T a, Object b) { + action.run(); + return null; + } + }); + } + + @Override + public CompletableFuture runAfterBoth(CompletionStage other, Runnable action) { + return runBoth(null, other, action); + } + + @Override + public CompletableFuture runAfterBothAsync(CompletionStage other, Runnable action) { + return runBoth(pool(), other, action); + } + + @Override + public CompletableFuture runAfterBothAsync(CompletionStage other, Runnable action, Executor executor) { + return runBoth(required(executor), other, action); + } + + private CompletableFuture either(final Executor executor, CompletionStage other, + final Function fn) { + if (fn == null || other == null) { + throw new NullPointerException(); + } + final CompletableFuture second = other.toCompletableFuture(); + final CompletableFuture stage = newIncompleteFuture(); + final AtomicBoolean taken = new AtomicBoolean(); + final CompletableFuture self = this; + Runnable first = new Runnable() { + @Override + public void run() { + if (!taken.compareAndSet(false, true)) { + return; + } + Object mine = self.result.get(); + final Object outcome = mine != null ? mine : second.result.get(); + if (outcome instanceof Failure) { + stage.relay(outcome); + return; + } + Runnable body = new Runnable() { + @Override + public void run() { + try { + stage.complete(fn.apply(CompletableFuture.value(outcome))); + } catch (Throwable t) { + stage.fail(wrap(t)); + } + } + }; + if (executor == null) { + body.run(); + return; + } + try { + executor.execute(body); + } catch (Throwable t) { + stage.fail(wrap(t)); + } + } + }; + whenDone(first); + second.whenDone(first); + return stage; + } + + @Override + public CompletableFuture applyToEither(CompletionStage other, Function fn) { + return either(null, other, fn); + } + + @Override + public CompletableFuture applyToEitherAsync(CompletionStage other, + Function fn) { + return either(pool(), other, fn); + } + + @Override + public CompletableFuture applyToEitherAsync(CompletionStage other, Function fn, + Executor executor) { + return either(required(executor), other, fn); + } + + private CompletableFuture acceptFirst(Executor executor, CompletionStage other, + final Consumer action) { + if (action == null) { + throw new NullPointerException(); + } + return either(executor, other, new Function() { + @Override + public Void apply(T value) { + action.accept(value); + return null; + } + }); + } + + @Override + public CompletableFuture acceptEither(CompletionStage other, Consumer action) { + return acceptFirst(null, other, action); + } + + @Override + public CompletableFuture acceptEitherAsync(CompletionStage other, + Consumer action) { + return acceptFirst(pool(), other, action); + } + + @Override + public CompletableFuture acceptEitherAsync(CompletionStage other, Consumer action, + Executor executor) { + return acceptFirst(required(executor), other, action); + } + + @SuppressWarnings("unchecked") + private CompletableFuture runFirst(Executor executor, CompletionStage other, final Runnable action) { + if (action == null || other == null) { + throw new NullPointerException(); + } + // Only that one of the two completed matters, not with what. + CompletableFuture mine = thenApply(new Function() { + @Override + public Object apply(T value) { + return value; + } + }); + return mine.either(executor, (CompletionStage) other, new Function() { + @Override + public Void apply(Object value) { + action.run(); + return null; + } + }); + } + + @Override + public CompletableFuture runAfterEither(CompletionStage other, Runnable action) { + return runFirst(null, other, action); + } + + @Override + public CompletableFuture runAfterEitherAsync(CompletionStage other, Runnable action) { + return runFirst(pool(), other, action); + } + + @Override + public CompletableFuture runAfterEitherAsync(CompletionStage other, Runnable action, + Executor executor) { + return runFirst(required(executor), other, action); + } + + @Override + public CompletableFuture toCompletableFuture() { + return this; + } + + /// A new future that completes as this one does. + public CompletableFuture copy() { + return then(null, new Step() { + @Override + public void run(Object outcome, CompletableFuture stage) { + stage.relay(outcome); + } + }); + } + + /// A stage that completes as this one does, and that completing does + /// not complete this one: a [#copy]. + /// + /// **Not provided: the JDK's refusal.** There the stage answered is of + /// a class whose `complete`, `cancel`, `get` and the rest throw + /// `UnsupportedOperationException`, and whose `toCompletableFuture` + /// answers yet another copy. Here it is an ordinary future, as what + /// [#completedStage] and [#failedStage] answer is, so code that casts + /// the stage back to a `CompletableFuture`, or calls + /// `toCompletableFuture()` on it, and completes that, completes the + /// stage its dependents hang off instead of being refused. What the + /// method is called for holds either way: whoever is handed the stage + /// cannot complete, fail or cancel the future it was made from. + public CompletionStage minimalCompletionStage() { + return copy(); + } + + @Override + public String toString() { + Object outcome = result.get(); + String state = outcome == null ? "Not completed" + : outcome instanceof Failure ? "Completed exceptionally: " + ((Failure) outcome).cause + : "Completed normally"; + return "CompletableFuture[" + state + "]"; + } +} diff --git a/Ports/CLDC11/src/java/util/concurrent/CompletionException.java b/Ports/CLDC11/src/java/util/concurrent/CompletionException.java new file mode 100644 index 00000000000..f5dcae15bd3 --- /dev/null +++ b/Ports/CLDC11/src/java/util/concurrent/CompletionException.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.concurrent; + +/// `java.util.concurrent.CompletionException`: a stage failed, or the one +/// it depended on did; the cause is the original exception. +public class CompletionException extends RuntimeException { + + protected CompletionException() { + } + + protected CompletionException(String message) { + super(message); + } + + public CompletionException(String message, Throwable cause) { + super(message, cause); + } + + public CompletionException(Throwable cause) { + super(cause == null ? null : cause.toString(), cause); + } +} diff --git a/Ports/CLDC11/src/java/util/concurrent/CompletionStage.java b/Ports/CLDC11/src/java/util/concurrent/CompletionStage.java new file mode 100644 index 00000000000..2b8ac5588d7 --- /dev/null +++ b/Ports/CLDC11/src/java/util/concurrent/CompletionStage.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.concurrent; + +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; + +/// `java.util.concurrent.CompletionStage`: one step of a computation that +/// others can be attached to. [CompletableFuture] is the implementation. +public interface CompletionStage { + + CompletionStage thenApply(Function fn); + + CompletionStage thenApplyAsync(Function fn); + + CompletionStage thenApplyAsync(Function fn, Executor executor); + + CompletionStage thenAccept(Consumer action); + + CompletionStage thenAcceptAsync(Consumer action); + + CompletionStage thenAcceptAsync(Consumer action, Executor executor); + + CompletionStage thenRun(Runnable action); + + CompletionStage thenRunAsync(Runnable action); + + CompletionStage thenRunAsync(Runnable action, Executor executor); + + CompletionStage thenCombine(CompletionStage other, + BiFunction fn); + + CompletionStage thenCombineAsync(CompletionStage other, + BiFunction fn); + + CompletionStage thenCombineAsync(CompletionStage other, + BiFunction fn, Executor executor); + + CompletionStage thenAcceptBoth(CompletionStage other, + BiConsumer action); + + CompletionStage thenAcceptBothAsync(CompletionStage other, + BiConsumer action); + + CompletionStage thenAcceptBothAsync(CompletionStage other, + BiConsumer action, Executor executor); + + CompletionStage runAfterBoth(CompletionStage other, Runnable action); + + CompletionStage runAfterBothAsync(CompletionStage other, Runnable action); + + CompletionStage runAfterBothAsync(CompletionStage other, Runnable action, Executor executor); + + CompletionStage applyToEither(CompletionStage other, Function fn); + + CompletionStage applyToEitherAsync(CompletionStage other, Function fn); + + CompletionStage applyToEitherAsync(CompletionStage other, Function fn, + Executor executor); + + CompletionStage acceptEither(CompletionStage other, Consumer action); + + CompletionStage acceptEitherAsync(CompletionStage other, Consumer action); + + CompletionStage acceptEitherAsync(CompletionStage other, Consumer action, + Executor executor); + + CompletionStage runAfterEither(CompletionStage other, Runnable action); + + CompletionStage runAfterEitherAsync(CompletionStage other, Runnable action); + + CompletionStage runAfterEitherAsync(CompletionStage other, Runnable action, Executor executor); + + CompletionStage thenCompose(Function> fn); + + CompletionStage thenComposeAsync(Function> fn); + + CompletionStage thenComposeAsync(Function> fn, + Executor executor); + + CompletionStage handle(BiFunction fn); + + CompletionStage handleAsync(BiFunction fn); + + CompletionStage handleAsync(BiFunction fn, Executor executor); + + CompletionStage whenComplete(BiConsumer action); + + CompletionStage whenCompleteAsync(BiConsumer action); + + CompletionStage whenCompleteAsync(BiConsumer action, Executor executor); + + CompletionStage exceptionally(Function fn); + + CompletableFuture toCompletableFuture(); +} diff --git a/Ports/CLDC11/src/java/util/concurrent/ConcurrentHashMap.java b/Ports/CLDC11/src/java/util/concurrent/ConcurrentHashMap.java new file mode 100644 index 00000000000..74d31511a89 --- /dev/null +++ b/Ports/CLDC11/src/java/util/concurrent/ConcurrentHashMap.java @@ -0,0 +1,671 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.concurrent; + +import java.util.AbstractCollection; +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; + +/// `java.util.concurrent.ConcurrentHashMap` for the Codename One runtime, +/// over a `HashMap`. +/// +/// It is safe to use from several threads at once. Every operation holds +/// the map's monitor, so the compound operations (`putIfAbsent`, +/// `computeIfAbsent`, `compute`, `merge`, ...) are atomic, and a mapping +/// function runs while the monitor is held: it must not touch the map. Two +/// threads never read and write at the same moment, which a device's few +/// threads rarely notice and which keeps the class small. +/// +/// It also keeps from the JDK class: +/// +/// - `null` keys and values are rejected with `NullPointerException`. +/// - Iterators never throw `ConcurrentModificationException`. Each iterates +/// over the entries present when it was created, so a loop may add to and +/// remove from the map it is walking; `Iterator.remove` and +/// `Map.Entry.setValue` write through to the map. +/// +/// The bulk parallel operations (`forEach(long, ...)`, `search`, `reduce`) +/// and `forEachKey`/`forEachValue` are not provided. +public class ConcurrentHashMap extends AbstractMap implements ConcurrentMap, java.io.Serializable { + + private static final long serialVersionUID = 1L; + + private final HashMap map; + + public ConcurrentHashMap() { + map = new HashMap(); + } + + public ConcurrentHashMap(int initialCapacity) { + if (initialCapacity < 0) { + throw new IllegalArgumentException(); + } + map = new HashMap(initialCapacity); + } + + public ConcurrentHashMap(int initialCapacity, float loadFactor) { + this(initialCapacity, loadFactor, 1); + } + + public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) { + if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0) { + throw new IllegalArgumentException(); + } + map = new HashMap(initialCapacity); + } + + public ConcurrentHashMap(Map m) { + map = new HashMap(); + putAll(m); + } + + private static void requireNonNull(Object o) { + if (o == null) { + throw new NullPointerException(); + } + } + + @Override + public synchronized int size() { + return map.size(); + } + + public synchronized long mappingCount() { + return map.size(); + } + + @Override + public synchronized boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public synchronized V get(Object key) { + requireNonNull(key); + return map.get(key); + } + + @Override + public synchronized boolean containsKey(Object key) { + requireNonNull(key); + return map.containsKey(key); + } + + @Override + public synchronized boolean containsValue(Object value) { + requireNonNull(value); + return map.containsValue(value); + } + + public boolean contains(Object value) { + return containsValue(value); + } + + @Override + public synchronized V put(K key, V value) { + requireNonNull(key); + requireNonNull(value); + return map.put(key, value); + } + + @Override + public void putAll(Map m) { + for (Map.Entry e : m.entrySet()) { + put(e.getKey(), e.getValue()); + } + } + + @Override + public synchronized V remove(Object key) { + requireNonNull(key); + return map.remove(key); + } + + @Override + public synchronized void clear() { + map.clear(); + } + + @Override + public V getOrDefault(Object key, V defaultValue) { + V v = get(key); + return v == null ? defaultValue : v; + } + + @Override + public synchronized V putIfAbsent(K key, V value) { + requireNonNull(key); + requireNonNull(value); + V old = map.get(key); + if (old == null) { + map.put(key, value); + } + return old; + } + + @Override + public synchronized boolean remove(Object key, Object value) { + requireNonNull(key); + if (value == null) { + return false; + } + V old = map.get(key); + if (old != null && old.equals(value)) { + map.remove(key); + return true; + } + return false; + } + + @Override + public synchronized boolean replace(K key, V oldValue, V newValue) { + requireNonNull(key); + requireNonNull(oldValue); + requireNonNull(newValue); + V old = map.get(key); + if (old != null && old.equals(oldValue)) { + map.put(key, newValue); + return true; + } + return false; + } + + @Override + public synchronized V replace(K key, V value) { + requireNonNull(key); + requireNonNull(value); + V old = map.get(key); + if (old != null) { + map.put(key, value); + } + return old; + } + + @Override + public synchronized V computeIfAbsent(K key, Function mappingFunction) { + requireNonNull(key); + requireNonNull(mappingFunction); + V old = map.get(key); + if (old != null) { + return old; + } + V created = mappingFunction.apply(key); + if (created != null) { + map.put(key, created); + } + return created; + } + + public synchronized V computeIfPresent(K key, BiFunction remappingFunction) { + requireNonNull(key); + requireNonNull(remappingFunction); + V old = map.get(key); + if (old == null) { + return null; + } + V updated = remappingFunction.apply(key, old); + if (updated == null) { + map.remove(key); + } else { + map.put(key, updated); + } + return updated; + } + + public synchronized V compute(K key, BiFunction remappingFunction) { + requireNonNull(key); + requireNonNull(remappingFunction); + V old = map.get(key); + V updated = remappingFunction.apply(key, old); + if (updated == null) { + if (old != null) { + map.remove(key); + } + } else { + map.put(key, updated); + } + return updated; + } + + public synchronized V merge(K key, V value, BiFunction remappingFunction) { + requireNonNull(key); + requireNonNull(value); + requireNonNull(remappingFunction); + V old = map.get(key); + if (old == null) { + map.put(key, value); + return value; + } + V updated = remappingFunction.apply(old, value); + if (updated == null) { + map.remove(key); + } else { + map.put(key, updated); + } + return updated; + } + + @Override + public void forEach(BiConsumer action) { + requireNonNull(action); + for (Map.Entry e : snapshot()) { + action.accept(e.getKey(), e.getValue()); + } + } + + @Override + public synchronized void replaceAll(BiFunction function) { + requireNonNull(function); + for (Map.Entry e : snapshot()) { + V updated = function.apply(e.getKey(), e.getValue()); + requireNonNull(updated); + replace(e.getKey(), updated); + } + } + + /// The entries present now, as entries that write through to this map. + /// Every iterator walks one of these, which is what lets a caller modify + /// the map while iterating it. + private synchronized List> snapshot() { + List> out = new ArrayList>(map.size()); + for (Map.Entry e : map.entrySet()) { + out.add(new WriteThroughEntry(this, e.getKey(), e.getValue())); + } + return out; + } + + /// The keys, as a view of this map. Compiled code names the JDK's return + /// type, `ConcurrentHashMap.KeySetView`, so that is what this returns. + @Override + public KeySetView keySet() { + return new KeySetView(this, null); + } + + public KeySetView keySet(V mappedValue) { + requireNonNull(mappedValue); + return new KeySetView(this, mappedValue); + } + + public static KeySetView newKeySet() { + return new KeySetView(new ConcurrentHashMap(), Boolean.TRUE); + } + + public static KeySetView newKeySet(int initialCapacity) { + return new KeySetView(new ConcurrentHashMap(initialCapacity), Boolean.TRUE); + } + + @Override + public Collection values() { + return new ValuesView(this); + } + + @Override + public Set> entrySet() { + return new EntrySetView(this); + } + + public Enumeration keys() { + return new IteratorEnumeration(keySet().iterator()); + } + + public Enumeration elements() { + return new IteratorEnumeration(values().iterator()); + } + + private static final class WriteThroughEntry implements Map.Entry { + private final ConcurrentHashMap owner; + private final K key; + private V value; + + WriteThroughEntry(ConcurrentHashMap owner, K key, V value) { + this.owner = owner; + this.key = key; + this.value = value; + } + + @Override + public K getKey() { + return key; + } + + @Override + public V getValue() { + return value; + } + + @Override + public V setValue(V newValue) { + requireNonNull(newValue); + V old = value; + value = newValue; + owner.put(key, newValue); + return old; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Map.Entry)) { + return false; + } + Map.Entry other = (Map.Entry) o; + Object otherKey = other.getKey(); + Object otherValue = other.getValue(); + return key.equals(otherKey) && value.equals(otherValue); + } + + @Override + public int hashCode() { + return key.hashCode() ^ value.hashCode(); + } + + @Override + public String toString() { + return key + "=" + value; + } + } + + /// Walks a snapshot of the entries; `remove` removes the key from the + /// live map. + private static final class SnapshotIterator { + private final ConcurrentHashMap owner; + private final List> entries; + private int next; + private Map.Entry last; + + SnapshotIterator(ConcurrentHashMap owner) { + this.owner = owner; + this.entries = owner.snapshot(); + } + + boolean hasNext() { + return next < entries.size(); + } + + Map.Entry nextEntry() { + if (next >= entries.size()) { + throw new NoSuchElementException(); + } + last = entries.get(next++); + return last; + } + + void remove() { + if (last == null) { + throw new IllegalStateException(); + } + owner.remove(last.getKey()); + last = null; + } + } + + private static final class KeyIterator implements Iterator { + private final SnapshotIterator it; + + KeyIterator(ConcurrentHashMap owner) { + it = new SnapshotIterator(owner); + } + + @Override + public boolean hasNext() { + return it.hasNext(); + } + + @Override + public K next() { + return it.nextEntry().getKey(); + } + + @Override + public void remove() { + it.remove(); + } + } + + private static final class ValueIterator implements Iterator { + private final SnapshotIterator it; + + ValueIterator(ConcurrentHashMap owner) { + it = new SnapshotIterator(owner); + } + + @Override + public boolean hasNext() { + return it.hasNext(); + } + + @Override + public V next() { + return it.nextEntry().getValue(); + } + + @Override + public void remove() { + it.remove(); + } + } + + private static final class EntryIterator implements Iterator> { + private final SnapshotIterator it; + + EntryIterator(ConcurrentHashMap owner) { + it = new SnapshotIterator(owner); + } + + @Override + public boolean hasNext() { + return it.hasNext(); + } + + @Override + public Map.Entry next() { + return it.nextEntry(); + } + + @Override + public void remove() { + it.remove(); + } + } + + private static final class IteratorEnumeration implements Enumeration { + private final Iterator it; + + IteratorEnumeration(Iterator it) { + this.it = it; + } + + @Override + public boolean hasMoreElements() { + return it.hasNext(); + } + + @Override + public T nextElement() { + return it.next(); + } + } + + private static final class ValuesView extends AbstractCollection { + private final ConcurrentHashMap owner; + + ValuesView(ConcurrentHashMap owner) { + this.owner = owner; + } + + @Override + public Iterator iterator() { + return new ValueIterator(owner); + } + + @Override + public int size() { + return owner.size(); + } + + @Override + public boolean contains(Object o) { + return o != null && owner.containsValue(o); + } + + @Override + public void clear() { + owner.clear(); + } + } + + private static final class EntrySetView extends AbstractSet> { + private final ConcurrentHashMap owner; + + EntrySetView(ConcurrentHashMap owner) { + this.owner = owner; + } + + @Override + public Iterator> iterator() { + return new EntryIterator(owner); + } + + @Override + public int size() { + return owner.size(); + } + + @Override + public boolean contains(Object o) { + if (!(o instanceof Map.Entry)) { + return false; + } + Map.Entry e = (Map.Entry) o; + Object key = e.getKey(); + Object value = e.getValue(); + if (key == null || value == null) { + return false; + } + Object mine = owner.get(key); + return mine != null && mine.equals(value); + } + + @Override + public boolean remove(Object o) { + if (!(o instanceof Map.Entry)) { + return false; + } + Map.Entry e = (Map.Entry) o; + Object key = e.getKey(); + Object value = e.getValue(); + return key != null && value != null && owner.remove(key, value); + } + + @Override + public void clear() { + owner.clear(); + } + } + + /// `java.util.concurrent.ConcurrentHashMap.KeySetView`: the keys of a + /// map as a set. A view created with a mapped value (`keySet(V)`, + /// `newKeySet()`) also accepts `add`, which maps the key to that value. + public static class KeySetView extends AbstractSet implements java.io.Serializable { + + private static final long serialVersionUID = 1L; + + private final ConcurrentHashMap owner; + private final V value; + + KeySetView(ConcurrentHashMap owner, V value) { + this.owner = owner; + this.value = value; + } + + /// The map this is a view of. It is the live map, as in the JDK: + /// the view exists to share it. + public ConcurrentHashMap getMap() { + return owner; + } + + public V getMappedValue() { + return value; + } + + @Override + public Iterator iterator() { + return new KeyIterator(owner); + } + + @Override + public int size() { + return owner.size(); + } + + @Override + public boolean isEmpty() { + return owner.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return owner.containsKey(o); + } + + @Override + public boolean remove(Object o) { + return owner.remove(o) != null; + } + + @Override + public void clear() { + owner.clear(); + } + + @Override + public boolean add(K e) { + if (value == null) { + throw new UnsupportedOperationException(); + } + return owner.putIfAbsent(e, value) == null; + } + + @Override + public boolean addAll(Collection c) { + boolean added = false; + for (K e : c) { + if (add(e)) { + added = true; + } + } + return added; + } + } +} diff --git a/Ports/CLDC11/src/java/util/concurrent/ConcurrentMap.java b/Ports/CLDC11/src/java/util/concurrent/ConcurrentMap.java new file mode 100644 index 00000000000..d16859b9d4f --- /dev/null +++ b/Ports/CLDC11/src/java/util/concurrent/ConcurrentMap.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.concurrent; + +import java.util.Map; + +/// A map whose compound operations are atomic. The four methods it declares +/// are default methods of `Map` as well; an implementation makes them atomic. +public interface ConcurrentMap extends Map { + public V putIfAbsent(K key, V value); + public boolean remove(Object key, Object value); + public V replace(K key, V value); + public boolean replace(K key, V oldValue, V newValue); +} diff --git a/Ports/CLDC11/src/java/util/concurrent/CopyOnWriteArrayList.java b/Ports/CLDC11/src/java/util/concurrent/CopyOnWriteArrayList.java new file mode 100644 index 00000000000..dae26e4fbb0 --- /dev/null +++ b/Ports/CLDC11/src/java/util/concurrent/CopyOnWriteArrayList.java @@ -0,0 +1,572 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.concurrent; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.NoSuchElementException; +import java.util.RandomAccess; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; + +/// `java.util.concurrent.CopyOnWriteArrayList` for the Codename One runtime. +/// +/// Every mutation replaces the backing array with a modified copy, and an +/// iterator keeps the array that was current when it was created. That is the +/// property listener lists rely on, and it matters on a single thread as much +/// as across threads: a listener that removes itself, or adds another, while +/// the list is being walked to deliver an event neither disturbs the walk nor +/// throws `ConcurrentModificationException`. +/// +/// It is safe to use from several threads at once: mutations are made one +/// at a time under an internal lock, and reads take the current array. +/// +/// Iterators are read only, as in the JDK: `remove`, `set` and `add` on one +/// throw `UnsupportedOperationException`. `subList` differs from the JDK: it +/// returns an unmodifiable copy of the range instead of a live view. +public class CopyOnWriteArrayList implements List, RandomAccess, Cloneable, java.io.Serializable { + + private static final long serialVersionUID = 1L; + + private static final Object[] EMPTY = new Object[0]; + + private Object[] array; + /// Guards [#array]; every mutation runs whole under it, so two threads + /// never both copy the same array and lose one of their changes. + private final transient Object lock = new Object(); + + private Object[] getArray() { + synchronized (lock) { + return array; + } + } + + private void setArray(Object[] a) { + synchronized (lock) { + array = a; + } + } + + public CopyOnWriteArrayList() { + setArray(EMPTY); + } + + public CopyOnWriteArrayList(Collection c) { + setArray(c.toArray()); + } + + public CopyOnWriteArrayList(E[] toCopyIn) { + Object[] copy = new Object[toCopyIn.length]; + System.arraycopy(toCopyIn, 0, copy, 0, copy.length); + setArray(copy); + } + + private CopyOnWriteArrayList(Object[] shared, boolean marker) { + setArray(shared); + } + + private static boolean same(Object a, Object b) { + return a == null ? b == null : a.equals(b); + } + + private static int indexOf(Object o, Object[] elements, int from, int to) { + for (int i = from; i < to; i++) { + if (same(o, elements[i])) { + return i; + } + } + return -1; + } + + private static int lastIndexOf(Object o, Object[] elements, int from) { + for (int i = from; i >= 0; i--) { + if (same(o, elements[i])) { + return i; + } + } + return -1; + } + + @SuppressWarnings("unchecked") + private static T element(Object[] elements, int index) { + return (T) elements[index]; + } + + @Override + public int size() { + return getArray().length; + } + + @Override + public boolean isEmpty() { + return getArray().length == 0; + } + + @Override + public boolean contains(Object o) { + Object[] elements = getArray(); + return indexOf(o, elements, 0, elements.length) >= 0; + } + + @Override + public int indexOf(Object o) { + Object[] elements = getArray(); + return indexOf(o, elements, 0, elements.length); + } + + public int indexOf(E e, int index) { + Object[] elements = getArray(); + return indexOf(e, elements, index, elements.length); + } + + @Override + public int lastIndexOf(Object o) { + Object[] elements = getArray(); + return lastIndexOf(o, elements, elements.length - 1); + } + + public int lastIndexOf(E e, int index) { + return lastIndexOf(e, getArray(), index); + } + + /// A list sharing this one's current array. Neither list ever writes to + /// an array it has published, so the two are independent from here on, + /// which is all a copy has to be. + @Override + public Object clone() { + return new CopyOnWriteArrayList(getArray(), true); + } + + @Override + public Object[] toArray() { + Object[] elements = getArray(); + Object[] copy = new Object[elements.length]; + System.arraycopy(elements, 0, copy, 0, elements.length); + return copy; + } + + @Override + public T[] toArray(T[] a) { + Object[] elements = getArray(); + if (a.length < elements.length) { + // Only a collection can create an array of the caller's type + // without reflection. + return new ArrayList(Arrays.asList(elements)).toArray(a); + } + System.arraycopy(elements, 0, a, 0, elements.length); + if (a.length > elements.length) { + a[elements.length] = null; + } + return a; + } + + @Override + public E get(int index) { + return element(getArray(), index); + } + + @Override + public E set(int index, E element) { + synchronized (lock) { + Object[] elements = getArray(); + E old = element(elements, index); + if (old != element) { + Object[] copy = new Object[elements.length]; + System.arraycopy(elements, 0, copy, 0, elements.length); + copy[index] = element; + setArray(copy); + } + return old; + } + } + + @Override + public boolean add(E e) { + synchronized (lock) { + Object[] elements = getArray(); + Object[] copy = new Object[elements.length + 1]; + System.arraycopy(elements, 0, copy, 0, elements.length); + copy[elements.length] = e; + setArray(copy); + return true; + } + } + + @Override + public void add(int index, E element) { + synchronized (lock) { + Object[] elements = getArray(); + int len = elements.length; + if (index > len || index < 0) { + throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + len); + } + Object[] copy = new Object[len + 1]; + System.arraycopy(elements, 0, copy, 0, index); + System.arraycopy(elements, index, copy, index + 1, len - index); + copy[index] = element; + setArray(copy); + } + } + + @Override + public E remove(int index) { + synchronized (lock) { + Object[] elements = getArray(); + int len = elements.length; + E old = element(elements, index); + Object[] copy = new Object[len - 1]; + System.arraycopy(elements, 0, copy, 0, index); + System.arraycopy(elements, index + 1, copy, index, len - index - 1); + setArray(copy); + return old; + } + } + + @Override + public boolean remove(Object o) { + synchronized (lock) { + Object[] elements = getArray(); + int index = indexOf(o, elements, 0, elements.length); + if (index < 0) { + return false; + } + remove(index); + return true; + } + } + + public boolean addIfAbsent(E e) { + synchronized (lock) { + if (contains(e)) { + return false; + } + return add(e); + } + } + + @Override + public boolean containsAll(Collection c) { + Object[] elements = getArray(); + for (Object e : c) { + if (indexOf(e, elements, 0, elements.length) < 0) { + return false; + } + } + return true; + } + + /// Keeps the elements for which membership in `c` equals `keep`. + private boolean filter(Collection c, boolean keep) { + synchronized (lock) { + if (c == null) { + throw new NullPointerException(); + } + Object[] elements = getArray(); + Object[] kept = new Object[elements.length]; + int n = 0; + for (int i = 0; i < elements.length; i++) { + if (c.contains(elements[i]) == keep) { + kept[n++] = elements[i]; + } + } + if (n == elements.length) { + return false; + } + Object[] copy = new Object[n]; + System.arraycopy(kept, 0, copy, 0, n); + setArray(copy); + return true; + } + } + + @Override + public boolean removeAll(Collection c) { + return filter(c, false); + } + + @Override + public boolean retainAll(Collection c) { + return filter(c, true); + } + + public int addAllAbsent(Collection c) { + synchronized (lock) { + int added = 0; + for (E e : c) { + if (addIfAbsent(e)) { + added++; + } + } + return added; + } + } + + @Override + public void clear() { + synchronized (lock) { + setArray(EMPTY); + } + } + + @Override + public boolean addAll(Collection c) { + synchronized (lock) { + return addAll(getArray().length, c); + } + } + + @Override + public boolean addAll(int index, Collection c) { + synchronized (lock) { + Object[] incoming = c.toArray(); + Object[] elements = getArray(); + int len = elements.length; + if (index > len || index < 0) { + throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + len); + } + if (incoming.length == 0) { + return false; + } + Object[] copy = new Object[len + incoming.length]; + System.arraycopy(elements, 0, copy, 0, index); + System.arraycopy(incoming, 0, copy, index, incoming.length); + System.arraycopy(elements, index, copy, index + incoming.length, len - index); + setArray(copy); + return true; + } + } + + @Override + public void forEach(Consumer action) { + if (action == null) { + throw new NullPointerException(); + } + Object[] elements = getArray(); + for (int i = 0; i < elements.length; i++) { + action.accept(CopyOnWriteArrayList.element(elements, i)); + } + } + + @Override + public boolean removeIf(Predicate filter) { + synchronized (lock) { + if (filter == null) { + throw new NullPointerException(); + } + Object[] elements = getArray(); + Object[] kept = new Object[elements.length]; + int n = 0; + for (int i = 0; i < elements.length; i++) { + if (!filter.test(CopyOnWriteArrayList.element(elements, i))) { + kept[n++] = elements[i]; + } + } + if (n == elements.length) { + return false; + } + Object[] copy = new Object[n]; + System.arraycopy(kept, 0, copy, 0, n); + setArray(copy); + return true; + } + } + + @Override + public void replaceAll(UnaryOperator operator) { + synchronized (lock) { + if (operator == null) { + throw new NullPointerException(); + } + Object[] elements = getArray(); + Object[] copy = new Object[elements.length]; + for (int i = 0; i < elements.length; i++) { + copy[i] = operator.apply(CopyOnWriteArrayList.element(elements, i)); + } + setArray(copy); + } + } + + @Override + public void sort(Comparator c) { + synchronized (lock) { + Object[] copy = toArray(); + if (c == null) { + Arrays.sort(copy); + } else { + sortWith(copy, c); + } + setArray(copy); + } + } + + @SuppressWarnings("unchecked") + private static void sortWith(Object[] elements, Comparator c) { + // Every element was added as an E, which is what T stands for here. + Arrays.sort((T[]) elements, c); + } + + @Override + public String toString() { + Object[] elements = getArray(); + StringBuilder sb = new StringBuilder(); + sb.append('['); + for (int i = 0; i < elements.length; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(elements[i] == this ? "(this Collection)" : String.valueOf(elements[i])); + } + return sb.append(']').toString(); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof List)) { + return false; + } + Object[] elements = getArray(); + Iterator it = ((List) o).iterator(); + for (int i = 0; i < elements.length; i++) { + if (!it.hasNext() || !same(elements[i], it.next())) { + return false; + } + } + return !it.hasNext(); + } + + @Override + public int hashCode() { + int hash = 1; + Object[] elements = getArray(); + for (int i = 0; i < elements.length; i++) { + hash = 31 * hash + (elements[i] == null ? 0 : elements[i].hashCode()); + } + return hash; + } + + @Override + public Iterator iterator() { + return new SnapshotIterator(getArray(), 0); + } + + @Override + public ListIterator listIterator() { + return new SnapshotIterator(getArray(), 0); + } + + @Override + public ListIterator listIterator(int index) { + Object[] elements = getArray(); + if (index < 0 || index > elements.length) { + throw new IndexOutOfBoundsException("Index: " + index); + } + return new SnapshotIterator(elements, index); + } + + /// An unmodifiable copy of the range. The JDK returns a live view; code + /// that writes through a sublist of a copy-on-write list gets an + /// `UnsupportedOperationException` here instead of a lost update. + @Override + public List subList(int fromIndex, int toIndex) { + Object[] elements = getArray(); + if (fromIndex < 0 || toIndex > elements.length || fromIndex > toIndex) { + throw new IndexOutOfBoundsException(); + } + List out = new ArrayList(toIndex - fromIndex); + for (int i = fromIndex; i < toIndex; i++) { + out.add(CopyOnWriteArrayList.element(elements, i)); + } + return Collections.unmodifiableList(out); + } + + private static final class SnapshotIterator implements ListIterator { + private final Object[] snapshot; + private int cursor; + + SnapshotIterator(Object[] snapshot, int cursor) { + this.snapshot = snapshot; + this.cursor = cursor; + } + + @Override + public boolean hasNext() { + return cursor < snapshot.length; + } + + @Override + public boolean hasPrevious() { + return cursor > 0; + } + + @Override + public E next() { + if (cursor >= snapshot.length) { + throw new NoSuchElementException(); + } + return element(snapshot, cursor++); + } + + @Override + public E previous() { + if (cursor <= 0) { + throw new NoSuchElementException(); + } + return element(snapshot, --cursor); + } + + @Override + public int nextIndex() { + return cursor; + } + + @Override + public int previousIndex() { + return cursor - 1; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + @Override + public void set(E e) { + throw new UnsupportedOperationException(); + } + + @Override + public void add(E e) { + throw new UnsupportedOperationException(); + } + } +} diff --git a/Ports/CLDC11/src/java/util/concurrent/CopyOnWriteArraySet.java b/Ports/CLDC11/src/java/util/concurrent/CopyOnWriteArraySet.java new file mode 100644 index 00000000000..6baaee33ec2 --- /dev/null +++ b/Ports/CLDC11/src/java/util/concurrent/CopyOnWriteArraySet.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.concurrent; + +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Iterator; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/// `java.util.concurrent.CopyOnWriteArraySet` for the Codename One runtime: +/// a set kept in a [CopyOnWriteArrayList], in the order its elements were +/// added. +/// +/// Every mutation replaces the backing array with a modified copy, and an +/// iterator keeps the array that was current when it was created, so a +/// listener that removes itself while the set is being walked neither +/// disturbs the walk nor throws `ConcurrentModificationException`. Iterators +/// are read only, as in the JDK. +/// +/// It is as safe to use from several threads at once as the list it is +/// kept in. +public class CopyOnWriteArraySet extends AbstractSet implements java.io.Serializable { + + private static final long serialVersionUID = 1L; + + private final CopyOnWriteArrayList al; + + public CopyOnWriteArraySet() { + al = new CopyOnWriteArrayList(); + } + + public CopyOnWriteArraySet(Collection c) { + al = new CopyOnWriteArrayList(); + al.addAllAbsent(c); + } + + @Override + public int size() { + return al.size(); + } + + @Override + public boolean isEmpty() { + return al.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return al.contains(o); + } + + @Override + public Object[] toArray() { + return al.toArray(); + } + + @Override + public T[] toArray(T[] a) { + return al.toArray(a); + } + + @Override + public void clear() { + al.clear(); + } + + @Override + public boolean remove(Object o) { + return al.remove(o); + } + + @Override + public boolean add(E e) { + return al.addIfAbsent(e); + } + + @Override + public boolean containsAll(Collection c) { + return al.containsAll(c); + } + + @Override + public boolean addAll(Collection c) { + return al.addAllAbsent(c) > 0; + } + + @Override + public boolean removeAll(Collection c) { + return al.removeAll(c); + } + + @Override + public boolean retainAll(Collection c) { + return al.retainAll(c); + } + + /// The elements as they were when this was called, in the order they + /// were added. `remove` on the iterator throws + /// `UnsupportedOperationException`. + @Override + public Iterator iterator() { + return al.iterator(); + } + + public boolean removeIf(Predicate filter) { + return al.removeIf(filter); + } + + @Override + public void forEach(Consumer action) { + al.forEach(action); + } +} diff --git a/Ports/CLDC11/src/java/util/concurrent/CountDownLatch.java b/Ports/CLDC11/src/java/util/concurrent/CountDownLatch.java new file mode 100644 index 00000000000..7b16a74db90 --- /dev/null +++ b/Ports/CLDC11/src/java/util/concurrent/CountDownLatch.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.concurrent; + +import java.util.concurrent.atomic.AtomicLong; + +/// `java.util.concurrent.CountDownLatch`: threads wait until a count +/// reaches zero. It is safe to use from any number of threads. Waiting on +/// the UI thread goes through `JdkHost.blockUi`, which keeps events being +/// dispatched. +public class CountDownLatch { + + private final AtomicLong count; + private final CompletableFuture open = new CompletableFuture(); + + public CountDownLatch(int count) { + if (count < 0) { + throw new IllegalArgumentException("count < 0"); + } + this.count = new AtomicLong(count); + if (count == 0) { + open.complete(Boolean.TRUE); + } + } + + public void countDown() { + while (true) { + long c = count.get(); + if (c == 0) { + return; + } + if (count.compareAndSet(c, c - 1)) { + if (c == 1) { + open.complete(Boolean.TRUE); + } + return; + } + } + } + + public long getCount() { + return count.get(); + } + + public void await() throws InterruptedException { + try { + open.get(); + } catch (ExecutionException e) { + // The latch is only ever opened; there is no failure to report. + return; + } + } + + public boolean await(long timeout, TimeUnit unit) throws InterruptedException { + try { + open.get(timeout, unit); + return true; + } catch (ExecutionException e) { + return true; + } catch (TimeoutException e) { + return false; + } + } + + @Override + public String toString() { + return "CountDownLatch[Count = " + count.get() + "]"; + } +} diff --git a/Ports/CLDC11/src/java/util/concurrent/Delayed.java b/Ports/CLDC11/src/java/util/concurrent/Delayed.java new file mode 100644 index 00000000000..b533f4b684d --- /dev/null +++ b/Ports/CLDC11/src/java/util/concurrent/Delayed.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.concurrent; + +/// Something that is due after a delay, ordered by when it is due. +public interface Delayed extends Comparable { + + /// How long until this is due in `unit`; zero or negative once it is. + long getDelay(TimeUnit unit); +} diff --git a/Ports/CLDC11/src/java/util/concurrent/Delays.java b/Ports/CLDC11/src/java/util/concurrent/Delays.java new file mode 100644 index 00000000000..7ee508930a3 --- /dev/null +++ b/Ports/CLDC11/src/java/util/concurrent/Delays.java @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.concurrent; + +import com.codename1.impl.JdkHost; + +import java.util.ArrayList; +import java.util.List; + +/// Runs an action after a delay, for everything in this package that +/// waits: the timeouts of [CompletableFuture] and the work of a +/// [ScheduledPool]. One thread serves them all, and it ends when nothing +/// is waiting. +/// +/// #### Why not `java.util.Timer` +/// +/// A device's `Timer` starts a thread for every task it is given, which +/// sleeps for the whole delay; a cancelled task is not looked at again +/// until that sleep is over. A request that sets a long timeout on a +/// future which then completes at once -- the ordinary case of a timeout -- +/// left a thread asleep until the deadline, one per request, and enough of +/// them use up the threads a process may have. Here a cancelled wait is +/// taken out of the queue at once and holds nothing. +/// +/// An action runs on the waiting thread, so it must be short: hand real +/// work to a pool, as [ScheduledPool] does. +final class Delays { + + /// One wait: [#cancel] it to give it up. + static final class Handle { + private final long due; + private Runnable action; + + Handle(long due, Runnable action) { + this.due = due; + this.action = action; + } + + /// Gives the wait up. Does nothing once the action has started. + void cancel() { + synchronized (LOCK) { + action = null; + // Woken so that a thread with nothing left to wait for ends + // now and not at a deadline nobody is waiting on. + if (QUEUE.remove(this)) { + LOCK.notifyAll(); + } + } + } + } + + private static final Object LOCK = new Object(); + /// Ordered by the time each is due; equal times in the order given. + private static final List QUEUE = new ArrayList(); + private static boolean running; + + private Delays() { + } + + /// Runs `action` once `millis` have passed. + static Handle after(long millis, Runnable action) { + // A delay too long to add to the clock (Long.MAX_VALUE as "never", + // say) is due at the end of time rather than wrapping into the past. + long now = System.currentTimeMillis(); + long due = millis > Long.MAX_VALUE - now ? Long.MAX_VALUE : now + Math.max(0, millis); + Handle handle = new Handle(due, action); + synchronized (LOCK) { + int at = QUEUE.size(); + while (at > 0 && QUEUE.get(at - 1).due > handle.due) { + at--; + } + QUEUE.add(at, handle); + if (running) { + LOCK.notifyAll(); + } else { + running = true; + Thread t = new Thread(new Runnable() { + @Override + public void run() { + serve(); + } + }, "cn1-delays"); + // A wait nobody gives up (a timeout of Long.MAX_VALUE) must + // not keep a program from ending, as the JDK's own delay + // thread does not. + t.setDaemon(true); + t.start(); + } + } + return handle; + } + + /// How many waits are queued: what a test asks to see that a cancelled + /// one is gone. + static int pending() { + synchronized (LOCK) { + return QUEUE.size(); + } + } + + private static void serve() { + while (true) { + Runnable action = next(); + if (action == null) { + return; + } + try { + action.run(); + } catch (Throwable t) { + // One action that fails must not end the thread the others + // wait on. + JdkHost.get().log(JdkHost.LOG_ERROR, "Exception in a delayed action: " + t, t); + } + } + } + + /// The next action that is due, waiting for it; null when the queue is + /// empty, with the thread marked as gone under the same lock a new wait + /// would start one under. + private static Runnable next() { + synchronized (LOCK) { + while (true) { + if (QUEUE.isEmpty()) { + running = false; + return null; + } + Handle first = QUEUE.get(0); + long wait = first.due - System.currentTimeMillis(); + if (wait <= 0) { + QUEUE.remove(0); + Runnable action = first.action; + first.action = null; + if (action != null) { + return action; + } + continue; + } + try { + LOCK.wait(wait); + } catch (InterruptedException e) { + // Nothing interrupts this thread to stop it: the queue + // says when it is done. + continue; + } + } + } + } +} diff --git a/Ports/CLDC11/src/java/util/concurrent/ExecutionException.java b/Ports/CLDC11/src/java/util/concurrent/ExecutionException.java new file mode 100644 index 00000000000..904d3dec7fd --- /dev/null +++ b/Ports/CLDC11/src/java/util/concurrent/ExecutionException.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.concurrent; + +/// Thrown by [Future#get] when the work failed; the cause is what it threw. +public class ExecutionException extends Exception { + /// An exception with no message or cause. + public ExecutionException() { } + + /// An exception with `message`. + /// + /// @param message the detail message + public ExecutionException(String message) { super(message); } + + /// An exception with `message` and the failure that caused it. + /// + /// @param message the detail message + /// @param cause what the work threw + public ExecutionException(String message, Throwable cause) { super(message, cause); } + + /// An exception wrapping the failure that caused it. + /// + /// @param cause what the work threw + public ExecutionException(Throwable cause) { super(cause); } +} diff --git a/Ports/CLDC11/src/java/util/concurrent/Executor.java b/Ports/CLDC11/src/java/util/concurrent/Executor.java new file mode 100644 index 00000000000..eacd1807186 --- /dev/null +++ b/Ports/CLDC11/src/java/util/concurrent/Executor.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.concurrent; + +/// Runs submitted tasks, on a thread of its choosing. +public interface Executor { + + /// Runs `command` at some time in the future. + void execute(Runnable command); +} diff --git a/Ports/CLDC11/src/java/util/concurrent/ExecutorService.java b/Ports/CLDC11/src/java/util/concurrent/ExecutorService.java new file mode 100644 index 00000000000..10696bd4dba --- /dev/null +++ b/Ports/CLDC11/src/java/util/concurrent/ExecutorService.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.concurrent; + +import java.util.Collection; +import java.util.List; + +/// An [Executor] that answers a [Future] for a submitted task and can be +/// shut down. [Executors] creates the implementations; +/// [AbstractExecutorService] provides everything but the execution itself. +public interface ExecutorService extends Executor { + + /// Stops accepting tasks. The ones already submitted still run. + void shutdown(); + + /// Stops accepting tasks, drops the ones that have not started and + /// answers them, and interrupts the ones that are running. + List shutdownNow(); + + boolean isShutdown(); + + /// Whether the service was shut down and every task it accepted has + /// finished or been dropped. + boolean isTerminated(); + + /// Waits until the service has terminated or the time is up, and answers + /// which of the two it was. + boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException; + + Future submit(Callable task); + + Future submit(Runnable task, T result); + + Future submit(Runnable task); + + /// Runs every task and answers their futures, all of them done. + List> invokeAll(Collection> tasks) throws InterruptedException; + + /// Runs every task and answers their futures once all are done or the + /// time is up; the ones not done by then are cancelled. + List> invokeAll(Collection> tasks, long timeout, TimeUnit unit) + throws InterruptedException; + + /// The result of one of the tasks that completed without failing; the + /// others are cancelled. + T invokeAny(Collection> tasks) throws InterruptedException, ExecutionException; + + /// As [#invokeAny(Collection)], giving up with a [TimeoutException] when + /// no task has succeeded in time. + T invokeAny(Collection> tasks, long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException; +} diff --git a/Ports/CLDC11/src/java/util/concurrent/Executors.java b/Ports/CLDC11/src/java/util/concurrent/Executors.java new file mode 100644 index 00000000000..600d58c0ec5 --- /dev/null +++ b/Ports/CLDC11/src/java/util/concurrent/Executors.java @@ -0,0 +1,379 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.concurrent; + +import com.codename1.impl.JdkHost; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +/// Factories for the executor services. +/// +/// #### Where a task runs +/// +/// On the pool's own threads, never on the UI thread. A task that touches +/// the user interface hands that part back, the way desktop code does with +/// `SwingUtilities.invokeLater` or `Platform.runLater`. +/// +/// Workers are started when a task first needs one and share one queue: a +/// task goes to whichever worker is free. A fixed pool keeps its workers +/// until it is shut down; a cached pool starts one whenever none is free and +/// lets one go after a minute without work, as the JDK's do. The overloads +/// that take a [ThreadFactory] make their workers with it. +/// +/// #### Waiting +/// +/// `Future.get()` and `awaitTermination` on the UI thread wait through +/// `JdkHost.blockUi`, so events keep being dispatched. +/// +/// The work stealing pools are fixed pools. +public class Executors { + + /// How long a cached pool's idle worker waits for work before it ends. + private static final long CACHED_KEEP_ALIVE_MILLIS = 60000L; + + /// How many workers a work stealing pool without a size has. + private static final int WORK_STEALING_WORKERS = 4; + + private Executors() { + } + + public static ExecutorService newFixedThreadPool(int nThreads) { + return newFixedThreadPool(nThreads, defaultThreadFactory()); + } + + public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) { + if (nThreads <= 0) { + throw new IllegalArgumentException(); + } + return new Pool(nThreads, 0, requireFactory(threadFactory)); + } + + public static ExecutorService newSingleThreadExecutor() { + return newSingleThreadExecutor(defaultThreadFactory()); + } + + public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) { + return new Pool(1, 0, requireFactory(threadFactory)); + } + + public static ExecutorService newCachedThreadPool() { + return newCachedThreadPool(defaultThreadFactory()); + } + + public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) { + return new Pool(Integer.MAX_VALUE, CACHED_KEEP_ALIVE_MILLIS, requireFactory(threadFactory)); + } + + /// A pool for work that splits itself up: here, a fixed pool. + public static ExecutorService newWorkStealingPool() { + return newFixedThreadPool(WORK_STEALING_WORKERS); + } + + public static ExecutorService newWorkStealingPool(int parallelism) { + return newFixedThreadPool(parallelism); + } + + public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { + return newScheduledThreadPool(corePoolSize, defaultThreadFactory()); + } + + /// Zero is accepted, as the JDK accepts it: a zero sized scheduled pool + /// still starts a worker for the first task it is given. + public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory) { + if (corePoolSize < 0) { + throw new IllegalArgumentException(); + } + return new ScheduledPool(new Pool(Math.max(1, corePoolSize), 0, requireFactory(threadFactory))); + } + + public static ScheduledExecutorService newSingleThreadScheduledExecutor() { + return newScheduledThreadPool(1); + } + + public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) { + return newScheduledThreadPool(1, threadFactory); + } + + /// The factory the executors use by default: non-daemon threads named + /// `pool-N-thread-M`. + public static ThreadFactory defaultThreadFactory() { + return new DefaultThreadFactory(false); + } + + /// A `Callable` that runs `task` and answers `result`. + public static Callable callable(Runnable task, T result) { + if (task == null) { + throw new NullPointerException(); + } + return new RunnableAdapter(task, result); + } + + public static Callable callable(Runnable task) { + return callable(task, null); + } + + private static ThreadFactory requireFactory(ThreadFactory threadFactory) { + if (threadFactory == null) { + throw new NullPointerException(); + } + return threadFactory; + } + + static final class RunnableAdapter implements Callable { + private final Runnable task; + private final T result; + + RunnableAdapter(Runnable task, T result) { + this.task = task; + this.result = result; + } + + public T call() { + task.run(); + return result; + } + } + + static final class DefaultThreadFactory implements ThreadFactory { + private static final Object COUNT_LOCK = new Object(); + private static int pools; + + private final String prefix; + private final boolean daemon; + private int threads; + + DefaultThreadFactory(boolean daemon) { + int n; + synchronized (COUNT_LOCK) { + n = ++pools; + } + this.prefix = "pool-" + n + "-thread-"; + this.daemon = daemon; + } + + public Thread newThread(Runnable r) { + int n; + synchronized (this) { + n = ++threads; + } + Thread t = new Thread(r, prefix + n); + if (daemon) { + t.setDaemon(true); + } + return t; + } + } + + /// The thread pool every factory above hands out: workers started on + /// demand, up to `maxWorkers`, sharing one queue. Everything is guarded + /// by the queue's monitor; the backend submits from many threads at once. + static final class Pool extends AbstractExecutorService { + private final LinkedList queue = new LinkedList(); + private final List workers = new ArrayList(); + private final int maxWorkers; + /// How long an idle worker waits for work before ending; 0 is forever. + private final long keepAliveMillis; + private final ThreadFactory factory; + /// Workers waiting for work. + private int idle; + private boolean shutdown; + /// Set by shutdownNow: workers stop without draining the queue. + private boolean stop; + + Pool(int maxWorkers, long keepAliveMillis, ThreadFactory factory) { + this.maxWorkers = maxWorkers; + this.keepAliveMillis = keepAliveMillis; + this.factory = factory; + } + + public void execute(Runnable command) { + if (command == null) { + throw new NullPointerException(); + } + Thread started = null; + synchronized (queue) { + if (shutdown) { + throw new RejectedExecutionException("The executor has been shut down"); + } + queue.add(command); + if (queue.size() > idle && workers.size() < maxWorkers) { + Thread t = factory.newThread(new Worker()); + if (t == null) { + if (workers.isEmpty()) { + queue.removeLast(); + throw new RejectedExecutionException("The thread factory made no thread"); + } + } else { + workers.add(t); + started = t; + } + } else { + queue.notify(); + } + } + if (started != null) { + started.start(); + } + } + + public void shutdown() { + synchronized (queue) { + shutdown = true; + queue.notifyAll(); + } + } + + public List shutdownNow() { + List pending; + List running; + synchronized (queue) { + shutdown = true; + stop = true; + pending = new ArrayList(queue); + queue.clear(); + running = new ArrayList(workers); + queue.notifyAll(); + } + for (int i = 0; i < running.size(); i++) { + running.get(i).interrupt(); + } + return pending; + } + + public boolean isShutdown() { + synchronized (queue) { + return shutdown; + } + } + + public boolean isTerminated() { + synchronized (queue) { + return terminated(); + } + } + + /// Called holding the queue's monitor. + boolean terminated() { + return shutdown && workers.isEmpty() && queue.isEmpty(); + } + + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + if (unit == null) { + throw new NullPointerException(); + } + if (!isTerminated()) { + new Termination(this, BlockingWait.deadline(unit.toMillis(timeout))).await(); + } + return isTerminated(); + } + + /// The next task for the calling worker, or null when it is to end; + /// a worker that ends is taken off the list here, under the lock. + Runnable next(Thread self) { + synchronized (queue) { + while (true) { + if (stop || (queue.isEmpty() && shutdown)) { + retire(self); + return null; + } + if (!queue.isEmpty()) { + return queue.removeFirst(); + } + idle++; + boolean timedOut = false; + try { + if (keepAliveMillis > 0) { + long before = System.currentTimeMillis(); + queue.wait(keepAliveMillis); + timedOut = System.currentTimeMillis() - before >= keepAliveMillis; + } else { + queue.wait(); + } + } catch (InterruptedException e) { + // shutdownNow interrupts; the flags above say what to do. + timedOut = false; + } finally { + idle--; + } + if (timedOut && queue.isEmpty()) { + retire(self); + return null; + } + } + } + } + + private void retire(Thread self) { + workers.remove(self); + if (terminated()) { + queue.notifyAll(); + } + } + + /// Whether shutdownNow has been called; read by a worker between tasks. + boolean stopping() { + synchronized (queue) { + return stop; + } + } + + final class Worker implements Runnable { + public void run() { + Thread self = Thread.currentThread(); + while (true) { + Runnable task = next(self); + if (task == null) { + return; + } + // An interrupt meant for the last task, or for an idle + // wait, must not reach this one -- unless the pool is + // stopping, which is what the interrupt then says. + if (!stopping()) { + Thread.interrupted(); + } + try { + task.run(); + } catch (Throwable t) { + JdkHost.get().log(JdkHost.LOG_ERROR, + "Exception in thread \"" + self.getName() + "\" " + t, t); + } + } + } + } + + private static final class Termination extends BlockingWait { + private final Pool pool; + + Termination(Pool pool, long deadline) { + super(pool.queue, deadline); + this.pool = pool; + } + + boolean ready() { + return pool.terminated(); + } + } + } +} diff --git a/Ports/CLDC11/src/java/util/concurrent/Future.java b/Ports/CLDC11/src/java/util/concurrent/Future.java new file mode 100644 index 00000000000..5367df903c5 --- /dev/null +++ b/Ports/CLDC11/src/java/util/concurrent/Future.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.concurrent; + +/// The result of work that finishes later -- on the backend, what an `@Async` +/// method returns, completed when its body has run on its executor. +/// +/// @param the type of the result +public interface Future { + /// Stops the work if it has not started. Answers whether it was cancelled; + /// whether work already running is interrupted is up to the implementation + /// and to `mayInterruptIfRunning`. + /// + /// @param mayInterruptIfRunning whether a running task may be interrupted + /// @return true when this call cancelled it + boolean cancel(boolean mayInterruptIfRunning); + + /// Whether [#cancel] cancelled the work before it ran. + /// + /// @return true when cancelled + boolean isCancelled(); + + /// Whether the work has finished: completed, failed or cancelled. + /// + /// @return true once it will not change again + boolean isDone(); + + /// Waits for the work to finish and returns its result. + /// + /// @return the result + /// @throws InterruptedException when the waiting thread is interrupted + /// @throws ExecutionException when the work threw; [Throwable#getCause] is + /// what it threw + /// @throws CancellationException when the work was cancelled + V get() throws InterruptedException, ExecutionException; + + /// Waits up to `timeout` for the work to finish and returns its result. + /// + /// @param timeout how long to wait, in `unit` + /// @param unit the unit of `timeout` + /// @return the result + /// @throws InterruptedException when the waiting thread is interrupted + /// @throws ExecutionException when the work threw + /// @throws TimeoutException when it has not finished in time + /// @throws CancellationException when the work was cancelled + V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException; +} diff --git a/Ports/CLDC11/src/java/util/concurrent/FutureTask.java b/Ports/CLDC11/src/java/util/concurrent/FutureTask.java new file mode 100644 index 00000000000..853e074889c --- /dev/null +++ b/Ports/CLDC11/src/java/util/concurrent/FutureTask.java @@ -0,0 +1,252 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.concurrent; + +/// A cancellable computation whose result is read through [Future]. +/// +/// The state lives under this object's monitor, so it may be run, cancelled +/// and waited for from any number of threads. `get()` on the UI thread waits +/// through `JdkHost.blockUi`, which keeps events being dispatched. +/// +/// `cancel(true)` interrupts the thread running the task, as in the JDK. +public class FutureTask implements RunnableFuture { + private static final int NEW = 0; + private static final int NORMAL = 1; + private static final int EXCEPTIONAL = 2; + private static final int CANCELLED = 3; + + /// Null once the task has run, so that it does not hold what it captured. + private Callable callable; + private int state; + private Object outcome; + /// The thread inside [#run], so that `cancel(true)` can interrupt it. + private Thread runner; + + public FutureTask(Callable callable) { + if (callable == null) { + throw new NullPointerException(); + } + this.callable = callable; + } + + public FutureTask(Runnable runnable, V result) { + if (runnable == null) { + throw new NullPointerException(); + } + this.callable = Executors.callable(runnable, result); + } + + public boolean cancel(boolean mayInterruptIfRunning) { + synchronized (this) { + if (state != NEW) { + return false; + } + state = CANCELLED; + callable = null; + if (mayInterruptIfRunning && runner != null) { + runner.interrupt(); + } + notifyAll(); + } + done(); + return true; + } + + public boolean isCancelled() { + synchronized (this) { + return state == CANCELLED; + } + } + + public boolean isDone() { + synchronized (this) { + return state != NEW; + } + } + + /// Called once when the task becomes done, by whichever thread made it + /// so: completion, failure or cancellation. Does nothing here. + protected void done() { + } + + /// Completes the future with `v`, unless it is already done. + protected void set(V v) { + if (finish(NORMAL, v)) { + done(); + } + } + + /// Fails the future with `t`, unless it is already done. + protected void setException(Throwable t) { + if (finish(EXCEPTIONAL, t)) { + done(); + } + } + + private boolean finish(int to, Object value) { + synchronized (this) { + if (state != NEW) { + return false; + } + state = to; + outcome = value; + callable = null; + notifyAll(); + return true; + } + } + + public V get() throws InterruptedException, ExecutionException { + if (!isDone()) { + new Wait(this, BlockingWait.FOREVER).await(); + } + return report(); + } + + public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + if (unit == null) { + throw new NullPointerException(); + } + if (!isDone()) { + new Wait(this, BlockingWait.deadline(unit.toMillis(timeout))).await(); + if (!isDone()) { + throw new TimeoutException(); + } + } + return report(); + } + + @SuppressWarnings("unchecked") + private V report() throws ExecutionException { + int s; + Object o; + synchronized (this) { + s = state; + o = outcome; + } + if (s == CANCELLED) { + throw new CancellationException(); + } + if (s == EXCEPTIONAL) { + throw new ExecutionException((Throwable) o); + } + return (V) o; + } + + public void run() { + Callable c = claim(); + if (c == null) { + return; + } + V v; + try { + v = c.call(); + } catch (Throwable t) { + release(); + setException(t); + return; + } + release(); + set(v); + } + + /// Runs the task without setting a result, leaving the future ready to + /// run again: for work that repeats. False when the task failed, was + /// cancelled, or was not runnable. + protected boolean runAndReset() { + Callable c = claim(); + if (c == null) { + return false; + } + try { + c.call(); + } catch (Throwable t) { + release(); + setException(t); + return false; + } + release(); + return !isDone(); + } + + /// The callable, with this thread recorded as its runner; null when the + /// task is done or another thread is running it. + private Callable claim() { + synchronized (this) { + if (state != NEW || runner != null) { + return null; + } + runner = Thread.currentThread(); + return callable; + } + } + + /// Forgets the runner. A cancel(true) that arrived during the run has + /// interrupted this thread; that interrupt is meant for the task, so it + /// is consumed here rather than leaking into the next task the thread runs. + private void release() { + boolean cancelled; + synchronized (this) { + runner = null; + cancelled = state == CANCELLED; + } + if (cancelled) { + Thread.interrupted(); + } + } + + public String toString() { + String status; + synchronized (this) { + switch (state) { + case NORMAL: + status = "[Completed normally]"; + break; + case EXCEPTIONAL: + status = "[Completed exceptionally: " + outcome + "]"; + break; + case CANCELLED: + status = "[Cancelled]"; + break; + default: + status = "[Not completed]"; + break; + } + } + return super.toString() + status; + } + + /// Waits for the task to be done. + private static final class Wait extends BlockingWait { + private final FutureTask task; + + Wait(FutureTask task, long deadline) { + super(task, deadline); + this.task = task; + } + + boolean ready() { + return task.state != NEW; + } + } +} diff --git a/Ports/CLDC11/src/java/util/concurrent/RejectedExecutionException.java b/Ports/CLDC11/src/java/util/concurrent/RejectedExecutionException.java new file mode 100644 index 00000000000..044724fb498 --- /dev/null +++ b/Ports/CLDC11/src/java/util/concurrent/RejectedExecutionException.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.concurrent; + +/// Thrown by an [Executor] that does not accept a task, typically because +/// it has been shut down. +public class RejectedExecutionException extends RuntimeException { + + public RejectedExecutionException() { + } + + public RejectedExecutionException(String message) { + super(message); + } + + public RejectedExecutionException(String message, Throwable cause) { + super(message, cause); + } + + public RejectedExecutionException(Throwable cause) { + super(cause); + } +} diff --git a/Ports/CLDC11/src/java/util/concurrent/RunnableFuture.java b/Ports/CLDC11/src/java/util/concurrent/RunnableFuture.java new file mode 100644 index 00000000000..70567ad1b59 --- /dev/null +++ b/Ports/CLDC11/src/java/util/concurrent/RunnableFuture.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.concurrent; + +/// A [Future] that is also the task computing it: running it completes it. +public interface RunnableFuture extends Runnable, Future { + + /// Computes the result and completes this future with it, unless it was + /// cancelled. + void run(); +} diff --git a/Ports/CLDC11/src/java/util/concurrent/ScheduledExecutorService.java b/Ports/CLDC11/src/java/util/concurrent/ScheduledExecutorService.java new file mode 100644 index 00000000000..b4f01364311 --- /dev/null +++ b/Ports/CLDC11/src/java/util/concurrent/ScheduledExecutorService.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.concurrent; + +/// `java.util.concurrent.ScheduledExecutorService`: an executor that also +/// runs work after a delay, or again and again. +/// +/// The work runs on the executor's threads, never on the UI thread. +public interface ScheduledExecutorService extends ExecutorService { + + ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit); + + ScheduledFuture schedule(Callable callable, long delay, TimeUnit unit); + + ScheduledFuture scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit); + + ScheduledFuture scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit); +} diff --git a/Ports/CLDC11/src/java/util/concurrent/ScheduledFuture.java b/Ports/CLDC11/src/java/util/concurrent/ScheduledFuture.java new file mode 100644 index 00000000000..29d60302da2 --- /dev/null +++ b/Ports/CLDC11/src/java/util/concurrent/ScheduledFuture.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.concurrent; + +/// `java.util.concurrent.ScheduledFuture`: the result of work an executor +/// was asked to run later. +public interface ScheduledFuture extends Delayed, Future { +} diff --git a/Ports/CLDC11/src/java/util/concurrent/ScheduledPool.java b/Ports/CLDC11/src/java/util/concurrent/ScheduledPool.java new file mode 100644 index 00000000000..6ccdb525ba5 --- /dev/null +++ b/Ports/CLDC11/src/java/util/concurrent/ScheduledPool.java @@ -0,0 +1,444 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.concurrent; + +import java.util.Collection; +import java.util.List; +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; + +/// The [ScheduledExecutorService] that [Executors] hands out: a timer that +/// passes each piece of work, when it is due, to an ordinary executor. +/// +/// #### After `shutdown()` +/// +/// As a JDK `ScheduledThreadPoolExecutor` with its default policies: work +/// already scheduled to run once still runs when it is due, and its future +/// completes; work that repeats is cancelled; nothing new is accepted. The +/// executor is terminated once the last of the accepted work has run. +/// `shutdownNow()` drops what has not started. +/// +/// Not provided: the two policies themselves +/// (`setExecuteExistingDelayedTasksAfterShutdownPolicy` and its periodic +/// twin are methods of the JDK's class, not of the interface this is handed +/// out as). +final class ScheduledPool extends AbstractExecutorService implements ScheduledExecutorService { + + private static final Scheduled[] NONE = new Scheduled[0]; + + private final ExecutorService pool; + private final AtomicBoolean shutdown = new AtomicBoolean(); + /// Set once the pool has been told to stop and nothing waits any more. + private final AtomicBoolean stopped = new AtomicBoolean(); + /// The scheduled work that has not completed, which is what an orderly + /// shutdown waits for (the work that runs once) or cancels (the work + /// that repeats). Replaced, never changed: this is read and written by + /// the timer's thread, the workers and the caller's. + private final AtomicReference[]> live = new AtomicReference[]>(NONE); + + ScheduledPool(ExecutorService pool) { + this.pool = pool; + } + + private final class Scheduled implements Runnable, ScheduledFuture { + private final Callable work; + private final boolean periodic; + /// The gap to keep after a run ends, or zero for a fixed rate. + private final long delayAfter; + /// The period of a fixed rate, or zero. + private final long rate; + /// The wait this is in, in [Delays], or null: see [#arm]. + private final AtomicReference wait = new AtomicReference(); + private final AtomicLong due; + private final AtomicBoolean running = new AtomicBoolean(); + /// A fixed rate's firings that have come and not yet run, counting + /// the one running now. See [#run]. + private final AtomicInteger owed = new AtomicInteger(); + private final CompletableFuture outcome = new CompletableFuture(); + + Scheduled(Callable work, long dueAt, boolean periodic, long delayAfter, long rate) { + this.work = work; + this.periodic = periodic; + this.delayAfter = delayAfter; + this.rate = rate; + this.due = new AtomicLong(dueAt); + } + + @Override + public void run() { + // Work that runs once and was accepted before a shutdown still + // runs; work that repeats does not. + if (outcome.isDone() || (periodic && shutdown.get()) || stopped.get()) { + disarm(); + return; + } + if (rate > 0) { + // A fixed rate keeps its beat whatever a run takes. + due.set(due.get() + rate); + arm(due.get() - System.currentTimeMillis()); + // A firing that comes while a run is under way is owed, not + // dropped: as with the JDK's scheduleAtFixedRate, an overrun + // makes the next runs start late and one after another, + // never at once. The worker already running takes it. + if (owed.getAndIncrement() > 0) { + return; + } + } else if (!running.compareAndSet(false, true)) { + return; + } + try { + pool.execute(new Runnable() { + @Override + public void run() { + once(); + } + }); + } catch (RuntimeException e) { + running.set(false); + owed.set(0); + outcome.completeExceptionally(e); + disarm(); + } + } + + /// Waits `millis` and then runs. The handle is kept so that a + /// cancelled task leaves the queue at once. + void arm(long millis) { + Delays.Handle next = Delays.after(millis, this); + Delays.Handle before = wait.getAndSet(next); + if (before != null) { + before.cancel(); + } + if (outcome.isDone() || stopped.get()) { + // Cancelled, or stopped, while this was being armed. + disarm(); + } + } + + void disarm() { + Delays.Handle before = wait.getAndSet(null); + if (before != null) { + before.cancel(); + } + } + + /// A fixed rate runs every firing it owes, in turn, on this worker. + private void once() { + if (rate == 0) { + step(); + return; + } + do { + running.set(true); + if (!step()) { + return; + } + } while (owed.decrementAndGet() > 0); + } + + /// One run; false when the work is not to run again. + private boolean step() { + // Cancelled, or stopped, while it waited for a worker: a future + // that answers isCancelled() must never start its work after. + if (outcome.isDone() || (periodic && shutdown.get()) || stopped.get()) { + running.set(false); + disarm(); + return false; + } + boolean rearm = false; + try { + V value; + try { + value = work.call(); + } catch (Throwable t) { + // Work that fails is not run again. + outcome.completeExceptionally(t); + disarm(); + return false; + } + if (!periodic) { + outcome.complete(value); + } else if (delayAfter > 0 && !outcome.isDone() && !shutdown.get()) { + rearm = true; + } + } finally { + running.set(false); + } + if (rearm) { + // Only once the flag is down. Armed before it, a short delay + // let the timer fire while this run still held the flag: the + // next run saw a run in progress and returned, nothing armed + // another, and the task stopped for good with its future + // never completing. + again(); + } + return !outcome.isDone(); + } + + private void again() { + due.set(System.currentTimeMillis() + delayAfter); + arm(delayAfter); + } + + @Override + public long getDelay(TimeUnit unit) { + return unit.convert(due.get() - System.currentTimeMillis(), TimeUnit.MILLISECONDS); + } + + @Override + public int compareTo(Delayed other) { + if (other == this) { + return 0; + } + long mine = getDelay(TimeUnit.MILLISECONDS); + long theirs = other.getDelay(TimeUnit.MILLISECONDS); + return mine < theirs ? -1 : (mine == theirs ? 0 : 1); + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + disarm(); + return outcome.cancel(mayInterruptIfRunning); + } + + @Override + public boolean isCancelled() { + return outcome.isCancelled(); + } + + @Override + public boolean isDone() { + return outcome.isDone(); + } + + @Override + public V get() throws InterruptedException, ExecutionException { + return outcome.get(); + } + + @Override + public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + return outcome.get(timeout, unit); + } + } + + /// Keeps `task` until it completes, and runs it after `millis`. + private Scheduled accept(final Scheduled task, long millis) { + checkOpen(); + while (true) { + Scheduled[] before = live.get(); + Scheduled[] after = new Scheduled[before.length + 1]; + System.arraycopy(before, 0, after, 0, before.length); + after[before.length] = task; + if (live.compareAndSet(before, after)) { + break; + } + } + task.outcome.whenComplete(new BiConsumer() { + @Override + public void accept(V value, Throwable failure) { + forget(task); + } + }); + if (shutdown.get()) { + // A shutdown came in between: this one was not accepted. + task.cancel(false); + throw new RejectedExecutionException("The executor has been shut down"); + } + task.arm(millis); + return task; + } + + private void forget(Scheduled task) { + while (true) { + Scheduled[] before = live.get(); + int at = -1; + for (int i = 0; i < before.length; i++) { + if (before[i] == task) { + at = i; + } + } + if (at < 0) { + break; + } + Scheduled[] after = new Scheduled[before.length - 1]; + System.arraycopy(before, 0, after, 0, at); + System.arraycopy(before, at + 1, after, at, after.length - at); + if (live.compareAndSet(before, after.length == 0 ? NONE : after)) { + break; + } + } + stopWhenIdle(); + } + + /// After a shutdown, stops the timer and the pool once nothing accepted + /// is left to run. + private void stopWhenIdle() { + if (shutdown.get() && live.get().length == 0 && stopped.compareAndSet(false, true)) { + pool.shutdown(); + } + } + + private void checkOpen() { + if (shutdown.get()) { + throw new RejectedExecutionException("The executor has been shut down"); + } + } + + @Override + public ScheduledFuture schedule(Callable callable, long delay, TimeUnit unit) { + if (callable == null || unit == null) { + throw new NullPointerException(); + } + final long millis = Math.max(0, unit.toMillis(delay)); + final Scheduled task = new Scheduled(callable, System.currentTimeMillis() + millis, false, 0, 0); + return accept(task, millis); + } + + @Override + public ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit) { + return schedule(Executors.callable(command, null), delay, unit); + } + + @Override + public ScheduledFuture scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) { + if (unit == null) { + throw new NullPointerException(); + } + if (period <= 0) { + throw new IllegalArgumentException(); + } + final long millis = Math.max(0, unit.toMillis(initialDelay)); + final long every = Math.max(1, unit.toMillis(period)); + final Scheduled task = new Scheduled(Executors.callable(command, null), + System.currentTimeMillis() + millis, true, 0, every); + return accept(task, millis); + } + + @Override + public ScheduledFuture scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, + TimeUnit unit) { + if (unit == null) { + throw new NullPointerException(); + } + if (delay <= 0) { + throw new IllegalArgumentException(); + } + final long millis = Math.max(0, unit.toMillis(initialDelay)); + final Scheduled task = new Scheduled(Executors.callable(command, null), + System.currentTimeMillis() + millis, true, Math.max(1, unit.toMillis(delay)), 0); + return accept(task, millis); + } + + @Override + public void execute(Runnable command) { + // The pool itself stays open after a shutdown for as long as + // delayed work is due to be handed to it, so it cannot be the one + // to refuse. + checkOpen(); + pool.execute(command); + } + + @Override + public void shutdown() { + shutdown.set(true); + for (Scheduled task : live.get()) { + if (task.periodic) { + task.cancel(false); + } + } + stopWhenIdle(); + } + + /// Nothing scheduled that has not started runs after this. The futures + /// of the dropped work stay as they are, neither done nor cancelled, + /// as the JDK leaves them. + /// + /// What is answered is all of it, as the contract of the interface + /// has it: the work the pool had been handed and had not begun, and + /// the work that was still waiting for its time, which the pool never + /// saw. The JDK answers its delayed tasks the same way, as the futures + /// `schedule` returned. + @Override + public List shutdownNow() { + shutdown.set(true); + stopped.set(true); + List dropped = new ArrayList(pool.shutdownNow()); + // Taken out of `live` here: a dropped task's future never completes, + // so the forget() its completion would run never comes, and the + // pool would hold every dropped task (and what its work holds) for + // as long as the pool itself is reachable. Nothing reads `live` + // after this -- `stopped` is set, so stopWhenIdle() is done with it + // -- and a task still running forgets itself into an empty array. + for (Scheduled task : live.getAndSet(NONE)) { + task.disarm(); + if (!task.outcome.isDone() && !task.running.get()) { + dropped.add(task); + } + } + return dropped; + } + + @Override + public boolean isShutdown() { + return shutdown.get(); + } + + @Override + public boolean isTerminated() { + return pool.isTerminated(); + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + return pool.awaitTermination(timeout, unit); + } + + @Override + public Future submit(Callable task) { + checkOpen(); + return pool.submit(task); + } + + @Override + public Future submit(Runnable task, T result) { + checkOpen(); + return pool.submit(task, result); + } + + @Override + public Future submit(Runnable task) { + checkOpen(); + return pool.submit(task); + } + + @Override + public List> invokeAll(Collection> tasks) throws InterruptedException { + checkOpen(); + return pool.invokeAll(tasks); + } +} diff --git a/Ports/CLDC11/src/java/util/concurrent/ThreadFactory.java b/Ports/CLDC11/src/java/util/concurrent/ThreadFactory.java new file mode 100644 index 00000000000..2be3cd81893 --- /dev/null +++ b/Ports/CLDC11/src/java/util/concurrent/ThreadFactory.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.concurrent; + +/// Makes the threads an executor runs its tasks on. +public interface ThreadFactory { + + /// A new, unstarted thread that runs `r`; null to refuse. + Thread newThread(Runnable r); +} diff --git a/Ports/CLDC11/src/java/util/concurrent/TimeoutException.java b/Ports/CLDC11/src/java/util/concurrent/TimeoutException.java new file mode 100644 index 00000000000..69be4c33b8d --- /dev/null +++ b/Ports/CLDC11/src/java/util/concurrent/TimeoutException.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.concurrent; + +/// Thrown by [Future#get(long, TimeUnit)] when the work has not finished in time. +public class TimeoutException extends Exception { + /// An exception with no message. + public TimeoutException() { } + + /// An exception with `message`. + /// + /// @param message the detail message + public TimeoutException(String message) { super(message); } +} diff --git a/Ports/CLDC11/src/java/util/function/BiConsumer.java b/Ports/CLDC11/src/java/util/function/BiConsumer.java index f27cf084ac6..52c0356ef5b 100644 --- a/Ports/CLDC11/src/java/util/function/BiConsumer.java +++ b/Ports/CLDC11/src/java/util/function/BiConsumer.java @@ -1,5 +1,55 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ package java.util.function; +import java.util.Objects; + +/// An operation on two values that answers nothing. +/// +/// @param the first input type +/// @param the second input type +@FunctionalInterface public interface BiConsumer { + + /// Performs this operation. + /// + /// @param t the first input + /// @param u the second input void accept(T t, U u); + + /// This operation followed by `after`, both given the same values. If + /// this operation throws, `after` does not run. + /// + /// @param after the operation to run next + /// @return the composed operation + default BiConsumer andThen(final BiConsumer after) { + Objects.requireNonNull(after); + final BiConsumer self = this; + return new BiConsumer() { + public void accept(T t, U u) { + self.accept(t, u); + after.accept(t, u); + } + }; + } } diff --git a/Ports/CLDC11/src/java/util/function/BiFunction.java b/Ports/CLDC11/src/java/util/function/BiFunction.java index 92c2bccaf71..cdfa42856e3 100644 --- a/Ports/CLDC11/src/java/util/function/BiFunction.java +++ b/Ports/CLDC11/src/java/util/function/BiFunction.java @@ -1,5 +1,56 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ package java.util.function; +import java.util.Objects; + +/// A function of two arguments. +/// +/// @param the first input type +/// @param the second input type +/// @param the result type +@FunctionalInterface public interface BiFunction { + + /// Applies this function. + /// + /// @param t the first input + /// @param u the second input + /// @return the result R apply(T t, U u); + + /// This function, then `after` on its result. + /// + /// @param after the function applied next + /// @param the result type of `after` + /// @return the composed function + default BiFunction andThen(final Function after) { + Objects.requireNonNull(after); + final BiFunction self = this; + return new BiFunction() { + public V apply(T t, U u) { + return after.apply(self.apply(t, u)); + } + }; + } } diff --git a/Ports/CLDC11/src/java/util/function/BiPredicate.java b/Ports/CLDC11/src/java/util/function/BiPredicate.java new file mode 100644 index 00000000000..9f4585b89ad --- /dev/null +++ b/Ports/CLDC11/src/java/util/function/BiPredicate.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.function; + +import java.util.Objects; + +/// A test of two arguments; the two-argument form of [Predicate]. +/// +/// @param the first input type +/// @param the second input type +@FunctionalInterface +public interface BiPredicate { + + /// Whether `t` and `u` pass. + /// + /// @param t the first input + /// @param u the second input + /// @return true when they pass + boolean test(T t, U u); + + /// Passes when this and `other` both pass; `other` is not asked when this + /// fails. + /// + /// @param other the second test + /// @return the composed test + default BiPredicate and(final BiPredicate other) { + Objects.requireNonNull(other); + final BiPredicate self = this; + return new BiPredicate() { + public boolean test(T t, U u) { + return self.test(t, u) && other.test(t, u); + } + }; + } + + /// Passes when this fails. + /// + /// @return the negated test + default BiPredicate negate() { + final BiPredicate self = this; + return new BiPredicate() { + public boolean test(T t, U u) { + return !self.test(t, u); + } + }; + } + + /// Passes when this or `other` passes; `other` is not asked when this + /// passes. + /// + /// @param other the second test + /// @return the composed test + default BiPredicate or(final BiPredicate other) { + Objects.requireNonNull(other); + final BiPredicate self = this; + return new BiPredicate() { + public boolean test(T t, U u) { + return self.test(t, u) || other.test(t, u); + } + }; + } +} diff --git a/Ports/CLDC11/src/java/util/function/BinaryOperator.java b/Ports/CLDC11/src/java/util/function/BinaryOperator.java index 6bb43e2efd4..2755a698c25 100644 --- a/Ports/CLDC11/src/java/util/function/BinaryOperator.java +++ b/Ports/CLDC11/src/java/util/function/BinaryOperator.java @@ -1,5 +1,61 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ package java.util.function; -public interface BinaryOperator { - T apply(T a, T b); +import java.util.Comparator; +import java.util.Objects; + +/// An operation on two values of one type that answers that type. +/// +/// @param the operand and result type +@FunctionalInterface +public interface BinaryOperator extends BiFunction { + + /// The smaller of two values by `comparator`; the first on a tie. + /// + /// @param comparator the order + /// @param the type + /// @return the operator + static BinaryOperator minBy(final Comparator comparator) { + Objects.requireNonNull(comparator); + return new BinaryOperator() { + public T apply(T a, T b) { + return comparator.compare(a, b) <= 0 ? a : b; + } + }; + } + + /// The larger of two values by `comparator`; the first on a tie. + /// + /// @param comparator the order + /// @param the type + /// @return the operator + static BinaryOperator maxBy(final Comparator comparator) { + Objects.requireNonNull(comparator); + return new BinaryOperator() { + public T apply(T a, T b) { + return comparator.compare(a, b) >= 0 ? a : b; + } + }; + } } diff --git a/Ports/CLDC11/src/java/util/function/BooleanSupplier.java b/Ports/CLDC11/src/java/util/function/BooleanSupplier.java new file mode 100644 index 00000000000..43fc08737e8 --- /dev/null +++ b/Ports/CLDC11/src/java/util/function/BooleanSupplier.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.function; + +/// A source of `boolean` values; the `boolean` form of [Supplier]. +@FunctionalInterface +public interface BooleanSupplier { + + /// A value. + /// + /// @return the value + boolean getAsBoolean(); +} diff --git a/Ports/CLDC11/src/java/util/function/Consumer.java b/Ports/CLDC11/src/java/util/function/Consumer.java index a000a2c474e..7ee4c4c190a 100644 --- a/Ports/CLDC11/src/java/util/function/Consumer.java +++ b/Ports/CLDC11/src/java/util/function/Consumer.java @@ -1,5 +1,53 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ package java.util.function; +import java.util.Objects; + +/// An operation on one value that answers nothing. +/// +/// @param the input type +@FunctionalInterface public interface Consumer { + + /// Performs this operation on `t`. + /// + /// @param t the input void accept(T t); + + /// This operation followed by `after`, both given the same value. If this + /// operation throws, `after` does not run. + /// + /// @param after the operation to run next + /// @return the composed operation + default Consumer andThen(final Consumer after) { + Objects.requireNonNull(after); + final Consumer self = this; + return new Consumer() { + public void accept(T t) { + self.accept(t); + after.accept(t); + } + }; + } } diff --git a/Ports/CLDC11/src/java/util/function/DoubleBinaryOperator.java b/Ports/CLDC11/src/java/util/function/DoubleBinaryOperator.java new file mode 100644 index 00000000000..bfa1d4e0513 --- /dev/null +++ b/Ports/CLDC11/src/java/util/function/DoubleBinaryOperator.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.function; + +/// An operation on two `double` values that answers a `double`; the `double` form of [BinaryOperator]. +@FunctionalInterface +public interface DoubleBinaryOperator { + + /// Applies this operator. + /// + /// @param left the first operand + /// @param right the second operand + /// @return the result + double applyAsDouble(double left, double right); +} diff --git a/Ports/CLDC11/src/java/util/function/DoubleConsumer.java b/Ports/CLDC11/src/java/util/function/DoubleConsumer.java new file mode 100644 index 00000000000..1d44bd4bd42 --- /dev/null +++ b/Ports/CLDC11/src/java/util/function/DoubleConsumer.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.function; + +import java.util.Objects; + +/// An operation on one `double` that answers nothing; the `double` form of [Consumer]. +@FunctionalInterface +public interface DoubleConsumer { + + /// Performs this operation on `value`. + /// + /// @param value the input + void accept(double value); + + /// This operation followed by `after`, both given the same value. If this + /// operation throws, `after` does not run. + /// + /// @param after the operation to run next + /// @return the composed operation + default DoubleConsumer andThen(final DoubleConsumer after) { + Objects.requireNonNull(after); + final DoubleConsumer self = this; + return new DoubleConsumer() { + public void accept(double value) { + self.accept(value); + after.accept(value); + } + }; + } +} diff --git a/Ports/CLDC11/src/java/util/function/DoubleFunction.java b/Ports/CLDC11/src/java/util/function/DoubleFunction.java new file mode 100644 index 00000000000..85cbd3632dc --- /dev/null +++ b/Ports/CLDC11/src/java/util/function/DoubleFunction.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.function; + +/// A function of one `double`; the `double` form of [Function]. +/// +/// @param the result type +@FunctionalInterface +public interface DoubleFunction { + + /// Applies this function. + /// + /// @param value the input + /// @return the result + R apply(double value); +} diff --git a/Ports/CLDC11/src/java/util/function/DoublePredicate.java b/Ports/CLDC11/src/java/util/function/DoublePredicate.java new file mode 100644 index 00000000000..125d220f3b6 --- /dev/null +++ b/Ports/CLDC11/src/java/util/function/DoublePredicate.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.function; + +import java.util.Objects; + +/// A test of one `double`; the `double` form of [Predicate]. +@FunctionalInterface +public interface DoublePredicate { + + /// Whether `value` passes. + /// + /// @param value the input + /// @return true when it passes + boolean test(double value); + + /// Passes when this and `other` both pass; `other` is not asked when this + /// fails. + /// + /// @param other the second test + /// @return the composed test + default DoublePredicate and(final DoublePredicate other) { + Objects.requireNonNull(other); + final DoublePredicate self = this; + return new DoublePredicate() { + public boolean test(double value) { + return self.test(value) && other.test(value); + } + }; + } + + /// Passes when this fails. + /// + /// @return the negated test + default DoublePredicate negate() { + final DoublePredicate self = this; + return new DoublePredicate() { + public boolean test(double value) { + return !self.test(value); + } + }; + } + + /// Passes when this or `other` passes; `other` is not asked when this + /// passes. + /// + /// @param other the second test + /// @return the composed test + default DoublePredicate or(final DoublePredicate other) { + Objects.requireNonNull(other); + final DoublePredicate self = this; + return new DoublePredicate() { + public boolean test(double value) { + return self.test(value) || other.test(value); + } + }; + } +} diff --git a/Ports/CLDC11/src/java/util/function/DoubleSupplier.java b/Ports/CLDC11/src/java/util/function/DoubleSupplier.java new file mode 100644 index 00000000000..de3fe0d5a37 --- /dev/null +++ b/Ports/CLDC11/src/java/util/function/DoubleSupplier.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.function; + +/// A source of `double` values; the `double` form of [Supplier]. +@FunctionalInterface +public interface DoubleSupplier { + + /// A value. + /// + /// @return the value + double getAsDouble(); +} diff --git a/Ports/CLDC11/src/java/util/function/DoubleToIntFunction.java b/Ports/CLDC11/src/java/util/function/DoubleToIntFunction.java new file mode 100644 index 00000000000..fe3d5a98fad --- /dev/null +++ b/Ports/CLDC11/src/java/util/function/DoubleToIntFunction.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.function; + +/// A function of one `double` that answers a `int`. +@FunctionalInterface +public interface DoubleToIntFunction { + + /// Applies this function. + /// + /// @param value the input + /// @return the result + int applyAsInt(double value); +} diff --git a/Ports/CLDC11/src/java/util/function/DoubleToLongFunction.java b/Ports/CLDC11/src/java/util/function/DoubleToLongFunction.java new file mode 100644 index 00000000000..949d967ce35 --- /dev/null +++ b/Ports/CLDC11/src/java/util/function/DoubleToLongFunction.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.function; + +/// A function of one `double` that answers a `long`. +@FunctionalInterface +public interface DoubleToLongFunction { + + /// Applies this function. + /// + /// @param value the input + /// @return the result + long applyAsLong(double value); +} diff --git a/Ports/CLDC11/src/java/util/function/DoubleUnaryOperator.java b/Ports/CLDC11/src/java/util/function/DoubleUnaryOperator.java new file mode 100644 index 00000000000..16e51536be4 --- /dev/null +++ b/Ports/CLDC11/src/java/util/function/DoubleUnaryOperator.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.function; + +import java.util.Objects; + +/// An operation on one `double` that answers a `double`; the `double` form of [UnaryOperator]. +@FunctionalInterface +public interface DoubleUnaryOperator { + + /// Applies this operator. + /// + /// @param operand the input + /// @return the result + double applyAsDouble(double operand); + + /// `before`, then this operator on its result. + /// + /// @param before the operator applied first + /// @return the composed operator + default DoubleUnaryOperator compose(final DoubleUnaryOperator before) { + Objects.requireNonNull(before); + final DoubleUnaryOperator self = this; + return new DoubleUnaryOperator() { + public double applyAsDouble(double value) { + return self.applyAsDouble(before.applyAsDouble(value)); + } + }; + } + + /// This operator, then `after` on its result. + /// + /// @param after the operator applied next + /// @return the composed operator + default DoubleUnaryOperator andThen(final DoubleUnaryOperator after) { + Objects.requireNonNull(after); + final DoubleUnaryOperator self = this; + return new DoubleUnaryOperator() { + public double applyAsDouble(double value) { + return after.applyAsDouble(self.applyAsDouble(value)); + } + }; + } + + /// The operator that answers its operand. + /// + /// @return the identity operator + static DoubleUnaryOperator identity() { + return new DoubleUnaryOperator() { + public double applyAsDouble(double value) { + return value; + } + }; + } +} diff --git a/Ports/CLDC11/src/java/util/function/Function.java b/Ports/CLDC11/src/java/util/function/Function.java index ad779d44781..1c16bc90b84 100644 --- a/Ports/CLDC11/src/java/util/function/Function.java +++ b/Ports/CLDC11/src/java/util/function/Function.java @@ -1,5 +1,81 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ package java.util.function; +import java.util.Objects; + +/// A function of one argument. +/// +/// @param the input type +/// @param the result type +@FunctionalInterface public interface Function { + + /// Applies this function. + /// + /// @param t the input + /// @return the result R apply(T t); + + /// `before`, then this function on its result. + /// + /// @param before the function applied first + /// @param the input type of `before` + /// @return the composed function + default Function compose(final Function before) { + Objects.requireNonNull(before); + final Function self = this; + return new Function() { + public R apply(V v) { + return self.apply(before.apply(v)); + } + }; + } + + /// This function, then `after` on its result. + /// + /// @param after the function applied next + /// @param the result type of `after` + /// @return the composed function + default Function andThen(final Function after) { + Objects.requireNonNull(after); + final Function self = this; + return new Function() { + public V apply(T t) { + return after.apply(self.apply(t)); + } + }; + } + + /// The function that answers its argument. + /// + /// @param the type + /// @return the identity function + static Function identity() { + return new Function() { + public T apply(T t) { + return t; + } + }; + } } diff --git a/Ports/CLDC11/src/java/util/function/IntBinaryOperator.java b/Ports/CLDC11/src/java/util/function/IntBinaryOperator.java new file mode 100644 index 00000000000..dccd2f21f3c --- /dev/null +++ b/Ports/CLDC11/src/java/util/function/IntBinaryOperator.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.function; + +/// An operation on two `int` values that answers a `int`; the `int` form of [BinaryOperator]. +@FunctionalInterface +public interface IntBinaryOperator { + + /// Applies this operator. + /// + /// @param left the first operand + /// @param right the second operand + /// @return the result + int applyAsInt(int left, int right); +} diff --git a/Ports/CLDC11/src/java/util/function/IntConsumer.java b/Ports/CLDC11/src/java/util/function/IntConsumer.java new file mode 100644 index 00000000000..80a74ca841c --- /dev/null +++ b/Ports/CLDC11/src/java/util/function/IntConsumer.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.function; + +import java.util.Objects; + +/// An operation on one `int` that answers nothing; the `int` form of [Consumer]. +@FunctionalInterface +public interface IntConsumer { + + /// Performs this operation on `value`. + /// + /// @param value the input + void accept(int value); + + /// This operation followed by `after`, both given the same value. If this + /// operation throws, `after` does not run. + /// + /// @param after the operation to run next + /// @return the composed operation + default IntConsumer andThen(final IntConsumer after) { + Objects.requireNonNull(after); + final IntConsumer self = this; + return new IntConsumer() { + public void accept(int value) { + self.accept(value); + after.accept(value); + } + }; + } +} diff --git a/Ports/CLDC11/src/java/util/function/IntFunction.java b/Ports/CLDC11/src/java/util/function/IntFunction.java new file mode 100644 index 00000000000..a148d448cb8 --- /dev/null +++ b/Ports/CLDC11/src/java/util/function/IntFunction.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.function; + +/// A function of one `int`; the `int` form of [Function]. +/// +/// @param the result type +@FunctionalInterface +public interface IntFunction { + + /// Applies this function. + /// + /// @param value the input + /// @return the result + R apply(int value); +} diff --git a/Ports/CLDC11/src/java/util/function/IntPredicate.java b/Ports/CLDC11/src/java/util/function/IntPredicate.java new file mode 100644 index 00000000000..3c538be9255 --- /dev/null +++ b/Ports/CLDC11/src/java/util/function/IntPredicate.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.function; + +import java.util.Objects; + +/// A test of one `int`; the `int` form of [Predicate]. +@FunctionalInterface +public interface IntPredicate { + + /// Whether `value` passes. + /// + /// @param value the input + /// @return true when it passes + boolean test(int value); + + /// Passes when this and `other` both pass; `other` is not asked when this + /// fails. + /// + /// @param other the second test + /// @return the composed test + default IntPredicate and(final IntPredicate other) { + Objects.requireNonNull(other); + final IntPredicate self = this; + return new IntPredicate() { + public boolean test(int value) { + return self.test(value) && other.test(value); + } + }; + } + + /// Passes when this fails. + /// + /// @return the negated test + default IntPredicate negate() { + final IntPredicate self = this; + return new IntPredicate() { + public boolean test(int value) { + return !self.test(value); + } + }; + } + + /// Passes when this or `other` passes; `other` is not asked when this + /// passes. + /// + /// @param other the second test + /// @return the composed test + default IntPredicate or(final IntPredicate other) { + Objects.requireNonNull(other); + final IntPredicate self = this; + return new IntPredicate() { + public boolean test(int value) { + return self.test(value) || other.test(value); + } + }; + } +} diff --git a/Ports/CLDC11/src/java/util/function/IntSupplier.java b/Ports/CLDC11/src/java/util/function/IntSupplier.java new file mode 100644 index 00000000000..630ef2d22d6 --- /dev/null +++ b/Ports/CLDC11/src/java/util/function/IntSupplier.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.function; + +/// A source of `int` values; the `int` form of [Supplier]. +@FunctionalInterface +public interface IntSupplier { + + /// A value. + /// + /// @return the value + int getAsInt(); +} diff --git a/Ports/CLDC11/src/java/util/function/IntToDoubleFunction.java b/Ports/CLDC11/src/java/util/function/IntToDoubleFunction.java new file mode 100644 index 00000000000..9a5f753bc75 --- /dev/null +++ b/Ports/CLDC11/src/java/util/function/IntToDoubleFunction.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.function; + +/// A function of one `int` that answers a `double`. +@FunctionalInterface +public interface IntToDoubleFunction { + + /// Applies this function. + /// + /// @param value the input + /// @return the result + double applyAsDouble(int value); +} diff --git a/Ports/CLDC11/src/java/util/function/IntToLongFunction.java b/Ports/CLDC11/src/java/util/function/IntToLongFunction.java new file mode 100644 index 00000000000..c31acba27c5 --- /dev/null +++ b/Ports/CLDC11/src/java/util/function/IntToLongFunction.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.function; + +/// A function of one `int` that answers a `long`. +@FunctionalInterface +public interface IntToLongFunction { + + /// Applies this function. + /// + /// @param value the input + /// @return the result + long applyAsLong(int value); +} diff --git a/Ports/CLDC11/src/java/util/function/IntUnaryOperator.java b/Ports/CLDC11/src/java/util/function/IntUnaryOperator.java new file mode 100644 index 00000000000..ce97b7a0b9e --- /dev/null +++ b/Ports/CLDC11/src/java/util/function/IntUnaryOperator.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.function; + +import java.util.Objects; + +/// An operation on one `int` that answers a `int`; the `int` form of [UnaryOperator]. +@FunctionalInterface +public interface IntUnaryOperator { + + /// Applies this operator. + /// + /// @param operand the input + /// @return the result + int applyAsInt(int operand); + + /// `before`, then this operator on its result. + /// + /// @param before the operator applied first + /// @return the composed operator + default IntUnaryOperator compose(final IntUnaryOperator before) { + Objects.requireNonNull(before); + final IntUnaryOperator self = this; + return new IntUnaryOperator() { + public int applyAsInt(int value) { + return self.applyAsInt(before.applyAsInt(value)); + } + }; + } + + /// This operator, then `after` on its result. + /// + /// @param after the operator applied next + /// @return the composed operator + default IntUnaryOperator andThen(final IntUnaryOperator after) { + Objects.requireNonNull(after); + final IntUnaryOperator self = this; + return new IntUnaryOperator() { + public int applyAsInt(int value) { + return after.applyAsInt(self.applyAsInt(value)); + } + }; + } + + /// The operator that answers its operand. + /// + /// @return the identity operator + static IntUnaryOperator identity() { + return new IntUnaryOperator() { + public int applyAsInt(int value) { + return value; + } + }; + } +} diff --git a/Ports/CLDC11/src/java/util/function/LongBinaryOperator.java b/Ports/CLDC11/src/java/util/function/LongBinaryOperator.java new file mode 100644 index 00000000000..3b6faeacc4e --- /dev/null +++ b/Ports/CLDC11/src/java/util/function/LongBinaryOperator.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.function; + +/// An operation on two `long` values that answers a `long`; the `long` form of [BinaryOperator]. +@FunctionalInterface +public interface LongBinaryOperator { + + /// Applies this operator. + /// + /// @param left the first operand + /// @param right the second operand + /// @return the result + long applyAsLong(long left, long right); +} diff --git a/Ports/CLDC11/src/java/util/function/LongConsumer.java b/Ports/CLDC11/src/java/util/function/LongConsumer.java new file mode 100644 index 00000000000..cdb2b02e5d8 --- /dev/null +++ b/Ports/CLDC11/src/java/util/function/LongConsumer.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.function; + +import java.util.Objects; + +/// An operation on one `long` that answers nothing; the `long` form of [Consumer]. +@FunctionalInterface +public interface LongConsumer { + + /// Performs this operation on `value`. + /// + /// @param value the input + void accept(long value); + + /// This operation followed by `after`, both given the same value. If this + /// operation throws, `after` does not run. + /// + /// @param after the operation to run next + /// @return the composed operation + default LongConsumer andThen(final LongConsumer after) { + Objects.requireNonNull(after); + final LongConsumer self = this; + return new LongConsumer() { + public void accept(long value) { + self.accept(value); + after.accept(value); + } + }; + } +} diff --git a/Ports/CLDC11/src/java/util/function/LongFunction.java b/Ports/CLDC11/src/java/util/function/LongFunction.java new file mode 100644 index 00000000000..f1458845b13 --- /dev/null +++ b/Ports/CLDC11/src/java/util/function/LongFunction.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.function; + +/// A function of one `long`; the `long` form of [Function]. +/// +/// @param the result type +@FunctionalInterface +public interface LongFunction { + + /// Applies this function. + /// + /// @param value the input + /// @return the result + R apply(long value); +} diff --git a/Ports/CLDC11/src/java/util/function/LongPredicate.java b/Ports/CLDC11/src/java/util/function/LongPredicate.java new file mode 100644 index 00000000000..f010b7bc72b --- /dev/null +++ b/Ports/CLDC11/src/java/util/function/LongPredicate.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.function; + +import java.util.Objects; + +/// A test of one `long`; the `long` form of [Predicate]. +@FunctionalInterface +public interface LongPredicate { + + /// Whether `value` passes. + /// + /// @param value the input + /// @return true when it passes + boolean test(long value); + + /// Passes when this and `other` both pass; `other` is not asked when this + /// fails. + /// + /// @param other the second test + /// @return the composed test + default LongPredicate and(final LongPredicate other) { + Objects.requireNonNull(other); + final LongPredicate self = this; + return new LongPredicate() { + public boolean test(long value) { + return self.test(value) && other.test(value); + } + }; + } + + /// Passes when this fails. + /// + /// @return the negated test + default LongPredicate negate() { + final LongPredicate self = this; + return new LongPredicate() { + public boolean test(long value) { + return !self.test(value); + } + }; + } + + /// Passes when this or `other` passes; `other` is not asked when this + /// passes. + /// + /// @param other the second test + /// @return the composed test + default LongPredicate or(final LongPredicate other) { + Objects.requireNonNull(other); + final LongPredicate self = this; + return new LongPredicate() { + public boolean test(long value) { + return self.test(value) || other.test(value); + } + }; + } +} diff --git a/Ports/CLDC11/src/java/util/function/LongSupplier.java b/Ports/CLDC11/src/java/util/function/LongSupplier.java new file mode 100644 index 00000000000..133d768e0fd --- /dev/null +++ b/Ports/CLDC11/src/java/util/function/LongSupplier.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.function; + +/// A source of `long` values; the `long` form of [Supplier]. +@FunctionalInterface +public interface LongSupplier { + + /// A value. + /// + /// @return the value + long getAsLong(); +} diff --git a/Ports/CLDC11/src/java/util/function/LongToDoubleFunction.java b/Ports/CLDC11/src/java/util/function/LongToDoubleFunction.java new file mode 100644 index 00000000000..ec0ad15bf49 --- /dev/null +++ b/Ports/CLDC11/src/java/util/function/LongToDoubleFunction.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.function; + +/// A function of one `long` that answers a `double`. +@FunctionalInterface +public interface LongToDoubleFunction { + + /// Applies this function. + /// + /// @param value the input + /// @return the result + double applyAsDouble(long value); +} diff --git a/Ports/CLDC11/src/java/util/function/LongToIntFunction.java b/Ports/CLDC11/src/java/util/function/LongToIntFunction.java new file mode 100644 index 00000000000..1ae1a3c9ce5 --- /dev/null +++ b/Ports/CLDC11/src/java/util/function/LongToIntFunction.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.function; + +/// A function of one `long` that answers a `int`. +@FunctionalInterface +public interface LongToIntFunction { + + /// Applies this function. + /// + /// @param value the input + /// @return the result + int applyAsInt(long value); +} diff --git a/Ports/CLDC11/src/java/util/function/LongUnaryOperator.java b/Ports/CLDC11/src/java/util/function/LongUnaryOperator.java new file mode 100644 index 00000000000..e70cbd9fa22 --- /dev/null +++ b/Ports/CLDC11/src/java/util/function/LongUnaryOperator.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.function; + +import java.util.Objects; + +/// An operation on one `long` that answers a `long`; the `long` form of [UnaryOperator]. +@FunctionalInterface +public interface LongUnaryOperator { + + /// Applies this operator. + /// + /// @param operand the input + /// @return the result + long applyAsLong(long operand); + + /// `before`, then this operator on its result. + /// + /// @param before the operator applied first + /// @return the composed operator + default LongUnaryOperator compose(final LongUnaryOperator before) { + Objects.requireNonNull(before); + final LongUnaryOperator self = this; + return new LongUnaryOperator() { + public long applyAsLong(long value) { + return self.applyAsLong(before.applyAsLong(value)); + } + }; + } + + /// This operator, then `after` on its result. + /// + /// @param after the operator applied next + /// @return the composed operator + default LongUnaryOperator andThen(final LongUnaryOperator after) { + Objects.requireNonNull(after); + final LongUnaryOperator self = this; + return new LongUnaryOperator() { + public long applyAsLong(long value) { + return after.applyAsLong(self.applyAsLong(value)); + } + }; + } + + /// The operator that answers its operand. + /// + /// @return the identity operator + static LongUnaryOperator identity() { + return new LongUnaryOperator() { + public long applyAsLong(long value) { + return value; + } + }; + } +} diff --git a/Ports/CLDC11/src/java/util/function/ObjDoubleConsumer.java b/Ports/CLDC11/src/java/util/function/ObjDoubleConsumer.java new file mode 100644 index 00000000000..9a90ee68e89 --- /dev/null +++ b/Ports/CLDC11/src/java/util/function/ObjDoubleConsumer.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.function; + +/// An operation on an object and a `double` that answers nothing; the `double` form of [BiConsumer]. +/// +/// @param the object type +@FunctionalInterface +public interface ObjDoubleConsumer { + + /// Performs this operation. + /// + /// @param t the object + /// @param value the `double` + void accept(T t, double value); +} diff --git a/Ports/CLDC11/src/java/util/function/ObjIntConsumer.java b/Ports/CLDC11/src/java/util/function/ObjIntConsumer.java new file mode 100644 index 00000000000..d6a5fe85d0b --- /dev/null +++ b/Ports/CLDC11/src/java/util/function/ObjIntConsumer.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.function; + +/// An operation on an object and a `int` that answers nothing; the `int` form of [BiConsumer]. +/// +/// @param the object type +@FunctionalInterface +public interface ObjIntConsumer { + + /// Performs this operation. + /// + /// @param t the object + /// @param value the `int` + void accept(T t, int value); +} diff --git a/Ports/CLDC11/src/java/util/function/ObjLongConsumer.java b/Ports/CLDC11/src/java/util/function/ObjLongConsumer.java new file mode 100644 index 00000000000..6b4735ae72f --- /dev/null +++ b/Ports/CLDC11/src/java/util/function/ObjLongConsumer.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.function; + +/// An operation on an object and a `long` that answers nothing; the `long` form of [BiConsumer]. +/// +/// @param the object type +@FunctionalInterface +public interface ObjLongConsumer { + + /// Performs this operation. + /// + /// @param t the object + /// @param value the `long` + void accept(T t, long value); +} diff --git a/Ports/CLDC11/src/java/util/function/Predicate.java b/Ports/CLDC11/src/java/util/function/Predicate.java index 4bb9d60f478..4c0067d26d4 100644 --- a/Ports/CLDC11/src/java/util/function/Predicate.java +++ b/Ports/CLDC11/src/java/util/function/Predicate.java @@ -1,5 +1,105 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ package java.util.function; +import java.util.Objects; + +/// A test of one value. +/// +/// @param the input type +@FunctionalInterface public interface Predicate { + + /// Whether `t` passes. + /// + /// @param t the input + /// @return true when it passes boolean test(T t); + + /// Passes when this and `other` both pass; `other` is not asked when this + /// fails. + /// + /// @param other the second test + /// @return the composed test + default Predicate and(final Predicate other) { + Objects.requireNonNull(other); + final Predicate self = this; + return new Predicate() { + public boolean test(T t) { + return self.test(t) && other.test(t); + } + }; + } + + /// Passes when this fails. + /// + /// @return the negated test + default Predicate negate() { + final Predicate self = this; + return new Predicate() { + public boolean test(T t) { + return !self.test(t); + } + }; + } + + /// Passes when this or `other` passes; `other` is not asked when this + /// passes. + /// + /// @param other the second test + /// @return the composed test + default Predicate or(final Predicate other) { + Objects.requireNonNull(other); + final Predicate self = this; + return new Predicate() { + public boolean test(T t) { + return self.test(t) || other.test(t); + } + }; + } + + /// Passes for values equal to `targetRef` by [Object#equals], or for null + /// when `targetRef` is null. + /// + /// @param targetRef the value to compare with + /// @param the input type + /// @return the test + static Predicate isEqual(final Object targetRef) { + return new Predicate() { + public boolean test(T t) { + return targetRef == null ? t == null : targetRef.equals(t); + } + }; + } + + /// The negation of `target`. + /// + /// @param target the test to negate + /// @param the input type + /// @return the negated test + @SuppressWarnings("unchecked") + static Predicate not(Predicate target) { + Objects.requireNonNull(target); + return (Predicate) target.negate(); + } } diff --git a/Ports/CLDC11/src/java/util/function/Supplier.java b/Ports/CLDC11/src/java/util/function/Supplier.java index e45d9f5aeef..499a06e4965 100644 --- a/Ports/CLDC11/src/java/util/function/Supplier.java +++ b/Ports/CLDC11/src/java/util/function/Supplier.java @@ -1,5 +1,35 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ package java.util.function; +/// A source of values. +/// +/// @param the type supplied +@FunctionalInterface public interface Supplier { + + /// A value. + /// + /// @return the value T get(); } diff --git a/Ports/CLDC11/src/java/util/function/ToDoubleBiFunction.java b/Ports/CLDC11/src/java/util/function/ToDoubleBiFunction.java new file mode 100644 index 00000000000..bedef3df575 --- /dev/null +++ b/Ports/CLDC11/src/java/util/function/ToDoubleBiFunction.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.function; + +/// A function of two arguments that answers a `double`; the `double` form of [BiFunction]. +/// +/// @param the first input type +/// @param the second input type +@FunctionalInterface +public interface ToDoubleBiFunction { + + /// Applies this function. + /// + /// @param t the first input + /// @param u the second input + /// @return the result + double applyAsDouble(T t, U u); +} diff --git a/Ports/CLDC11/src/java/util/function/ToDoubleFunction.java b/Ports/CLDC11/src/java/util/function/ToDoubleFunction.java new file mode 100644 index 00000000000..57d5d343ec1 --- /dev/null +++ b/Ports/CLDC11/src/java/util/function/ToDoubleFunction.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.function; + +/// A function that answers a `double`; the `double` form of [Function]. +/// +/// @param the input type +@FunctionalInterface +public interface ToDoubleFunction { + + /// Applies this function. + /// + /// @param value the input + /// @return the result + double applyAsDouble(T value); +} diff --git a/Ports/CLDC11/src/java/util/function/ToIntBiFunction.java b/Ports/CLDC11/src/java/util/function/ToIntBiFunction.java new file mode 100644 index 00000000000..b9aa9c17d86 --- /dev/null +++ b/Ports/CLDC11/src/java/util/function/ToIntBiFunction.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.function; + +/// A function of two arguments that answers a `int`; the `int` form of [BiFunction]. +/// +/// @param the first input type +/// @param the second input type +@FunctionalInterface +public interface ToIntBiFunction { + + /// Applies this function. + /// + /// @param t the first input + /// @param u the second input + /// @return the result + int applyAsInt(T t, U u); +} diff --git a/Ports/CLDC11/src/java/util/function/ToIntFunction.java b/Ports/CLDC11/src/java/util/function/ToIntFunction.java new file mode 100644 index 00000000000..8d03908f875 --- /dev/null +++ b/Ports/CLDC11/src/java/util/function/ToIntFunction.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.function; + +/// A function that answers a `int`; the `int` form of [Function]. +/// +/// @param the input type +@FunctionalInterface +public interface ToIntFunction { + + /// Applies this function. + /// + /// @param value the input + /// @return the result + int applyAsInt(T value); +} diff --git a/Ports/CLDC11/src/java/util/function/ToLongBiFunction.java b/Ports/CLDC11/src/java/util/function/ToLongBiFunction.java new file mode 100644 index 00000000000..a751c610c2e --- /dev/null +++ b/Ports/CLDC11/src/java/util/function/ToLongBiFunction.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.function; + +/// A function of two arguments that answers a `long`; the `long` form of [BiFunction]. +/// +/// @param the first input type +/// @param the second input type +@FunctionalInterface +public interface ToLongBiFunction { + + /// Applies this function. + /// + /// @param t the first input + /// @param u the second input + /// @return the result + long applyAsLong(T t, U u); +} diff --git a/Ports/CLDC11/src/java/util/function/ToLongFunction.java b/Ports/CLDC11/src/java/util/function/ToLongFunction.java new file mode 100644 index 00000000000..45dc0fedb37 --- /dev/null +++ b/Ports/CLDC11/src/java/util/function/ToLongFunction.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.function; + +/// A function that answers a `long`; the `long` form of [Function]. +/// +/// @param the input type +@FunctionalInterface +public interface ToLongFunction { + + /// Applies this function. + /// + /// @param value the input + /// @return the result + long applyAsLong(T value); +} diff --git a/Ports/CLDC11/src/java/util/function/UnaryOperator.java b/Ports/CLDC11/src/java/util/function/UnaryOperator.java index 0a15d04019a..e58ddc6cdb3 100644 --- a/Ports/CLDC11/src/java/util/function/UnaryOperator.java +++ b/Ports/CLDC11/src/java/util/function/UnaryOperator.java @@ -1,4 +1,42 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ package java.util.function; +/// An operation on one value that answers the same type. +/// +/// @param the operand and result type +@FunctionalInterface public interface UnaryOperator extends Function { + + /// The operator that answers its operand. + /// + /// @param the type + /// @return the identity operator + static UnaryOperator identity() { + return new UnaryOperator() { + public T apply(T t) { + return t; + } + }; + } } diff --git a/Ports/CLDC11/src/java/util/logging/Level.java b/Ports/CLDC11/src/java/util/logging/Level.java new file mode 100644 index 00000000000..032099d8ad8 --- /dev/null +++ b/Ports/CLDC11/src/java/util/logging/Level.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.logging; + +/// A logging level: the standard +/// levels, and room for an application's own -- the constructor is +/// protected, as the JDK's is, so a class may extend this one to add them. +/// +/// [Logger] writes through the platform log, which has four levels; see +/// there for how these map onto them. +public class Level implements java.io.Serializable { + + private static final long serialVersionUID = 1L; + + public static final Level OFF = new Level("OFF", Integer.MAX_VALUE); + public static final Level SEVERE = new Level("SEVERE", 1000); + public static final Level WARNING = new Level("WARNING", 900); + public static final Level INFO = new Level("INFO", 800); + public static final Level CONFIG = new Level("CONFIG", 700); + public static final Level FINE = new Level("FINE", 500); + public static final Level FINER = new Level("FINER", 400); + public static final Level FINEST = new Level("FINEST", 300); + public static final Level ALL = new Level("ALL", Integer.MIN_VALUE); + + private final String name; + private final int value; + private final String resourceBundleName; + + protected Level(String name, int value) { + this(name, value, null); + } + + protected Level(String name, int value, String resourceBundleName) { + if (name == null) { + throw new NullPointerException(); + } + this.name = name; + this.value = value; + this.resourceBundleName = resourceBundleName; + } + + public String getName() { + return name; + } + + /// The name: a device carries no translations of the level names. + public String getLocalizedName() { + return name; + } + + public String getResourceBundleName() { + return resourceBundleName; + } + + public final int intValue() { + return value; + } + + private static Level[] standard() { + return new Level[] {OFF, SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST, ALL}; + } + + /// The standard level called `name`, in any case, or whose number + /// `name` spells. A number that belongs to no standard level makes a + /// level of its own, named by the number. + public static Level parse(String name) { + if (name == null) { + throw new NullPointerException(); + } + String trimmed = name.trim(); + Level[] all = standard(); + for (Level level : all) { + if (level.name.equalsIgnoreCase(trimmed)) { + return level; + } + } + int number; + try { + number = Integer.parseInt(trimmed); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Bad level \"" + name + "\""); + } + for (Level level : all) { + if (level.value == number) { + return level; + } + } + return new Level(trimmed, number); + } + + @Override + public final String toString() { + return name; + } + + @Override + public boolean equals(Object other) { + return other instanceof Level && ((Level) other).value == value; + } + + @Override + public int hashCode() { + return value; + } +} diff --git a/Ports/CLDC11/src/java/util/logging/Logger.java b/Ports/CLDC11/src/java/util/logging/Logger.java new file mode 100644 index 00000000000..bbf1cbd401e --- /dev/null +++ b/Ports/CLDC11/src/java/util/logging/Logger.java @@ -0,0 +1,301 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.logging; + +import com.codename1.impl.JdkHost; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +/// A logger writing through the platform's log, which +/// `com.codename1.impl.JdkHost` reaches: `com.codename1.io.Log` in a Codename +/// One application, standard error elsewhere. +/// +/// The platform log has four levels where the JDK has seven, so they fold: +/// `SEVERE` is an error, `WARNING` a warning, `INFO` and `CONFIG` are +/// information, and the three `FINE` levels are debug output. A record +/// carries its logger's name in front of the message, and an attached +/// exception is logged with it, which is what prints a stack trace on a +/// device. +/// +/// A logger with no level of its own logs `INFO` and above, the JDK's +/// default; the platform log's own level may then filter once more. There are no +/// handlers, filters, formatters or parents to configure: a call that would +/// install one has no method here, and the build reports it. +public class Logger { + + public static final String GLOBAL_LOGGER_NAME = "global"; + + private static final Map LOGGERS = new HashMap(); + + private final String name; + private Level level; + + protected Logger(String name, String resourceBundleName) { + this.name = name; + } + + public static Logger getLogger(String name) { + if (name == null) { + throw new NullPointerException(); + } + synchronized (LOGGERS) { + Logger logger = LOGGERS.get(name); + if (logger == null) { + logger = new Logger(name, null); + LOGGERS.put(name, logger); + } + return logger; + } + } + + /// The bundle is not consulted: messages are logged as they are given. + public static Logger getLogger(String name, String resourceBundleName) { + return getLogger(name); + } + + public static Logger getGlobal() { + return getLogger(GLOBAL_LOGGER_NAME); + } + + public static Logger getAnonymousLogger() { + return new Logger(null, null); + } + + public String getName() { + return name; + } + + public Level getLevel() { + return level; + } + + public void setLevel(Level newLevel) { + this.level = newLevel; + } + + public Logger getParent() { + return null; + } + + public void setUseParentHandlers(boolean useParentHandlers) { + // There are no handlers; see the class description. + } + + public boolean getUseParentHandlers() { + return false; + } + + public boolean isLoggable(Level l) { + int floor = (level == null ? Level.INFO : level).intValue(); + return l.intValue() >= floor && floor != Level.OFF.intValue(); + } + + private static int logLevel(Level l) { + int value = l.intValue(); + if (value >= Level.SEVERE.intValue()) { + return JdkHost.LOG_ERROR; + } + if (value >= Level.WARNING.intValue()) { + return JdkHost.LOG_WARNING; + } + if (value >= Level.CONFIG.intValue()) { + return JdkHost.LOG_INFO; + } + return JdkHost.LOG_DEBUG; + } + + public void log(Level l, String msg) { + if (isLoggable(l)) { + JdkHost.get().log(logLevel(l), name == null ? String.valueOf(msg) : name + ": " + msg, null); + } + } + + public void log(Level l, Supplier msgSupplier) { + if (isLoggable(l)) { + log(l, msgSupplier.get()); + } + } + + public void log(Level l, String msg, Object param1) { + log(l, msg, new Object[] {param1}); + } + + /// Logs `msg` with each `{n}` replaced by parameter `n`, as the JDK's + /// formatter does. + public void log(Level l, String msg, Object[] params) { + if (!isLoggable(l)) { + return; + } + String text = msg; + if (text != null && params != null && params.length > 0 && text.indexOf('{') >= 0) { + text = substitute(text, params); + } + log(l, text); + } + + /// `msg` with each `{n}` for which there is a parameter replaced by it. + /// The JDK formats with `MessageFormat`; this covers the plain index + /// form, which is what a log message uses, and leaves anything else -- + /// a format type, quoting, a brace with no parameter -- as written. + private static String substitute(String msg, Object[] params) { + StringBuilder out = new StringBuilder(msg.length() + 16); + int i = 0; + while (i < msg.length()) { + char c = msg.charAt(i); + if (c == '{') { + int close = msg.indexOf('}', i + 1); + int index = close < 0 ? -1 : index(msg, i + 1, close); + if (index >= 0 && index < params.length) { + out.append(String.valueOf(params[index])); + i = close + 1; + continue; + } + } + out.append(c); + i++; + } + return out.toString(); + } + + /// The decimal number spelled from `start` to `end`, or -1. + private static int index(String text, int start, int end) { + if (start == end || end - start > 4) { + return -1; + } + int value = 0; + for (int i = start; i < end; i++) { + char c = text.charAt(i); + if (c < '0' || c > '9') { + return -1; + } + value = value * 10 + (c - '0'); + } + return value; + } + + public void log(Level l, String msg, Throwable thrown) { + if (isLoggable(l)) { + JdkHost.get().log(logLevel(l), name == null ? String.valueOf(msg) : name + ": " + msg, thrown); + } + } + + public void log(Level l, Throwable thrown, Supplier msgSupplier) { + if (isLoggable(l)) { + log(l, msgSupplier.get(), thrown); + } + } + + public void logp(Level l, String sourceClass, String sourceMethod, String msg) { + log(l, msg); + } + + public void logp(Level l, String sourceClass, String sourceMethod, String msg, Object param1) { + log(l, msg, param1); + } + + public void logp(Level l, String sourceClass, String sourceMethod, String msg, Object[] params) { + log(l, msg, params); + } + + public void logp(Level l, String sourceClass, String sourceMethod, String msg, Throwable thrown) { + log(l, msg, thrown); + } + + public void entering(String sourceClass, String sourceMethod) { + log(Level.FINER, "ENTRY " + sourceClass + "." + sourceMethod); + } + + public void entering(String sourceClass, String sourceMethod, Object param1) { + log(Level.FINER, "ENTRY " + sourceClass + "." + sourceMethod + " " + param1); + } + + public void exiting(String sourceClass, String sourceMethod) { + log(Level.FINER, "RETURN " + sourceClass + "." + sourceMethod); + } + + public void exiting(String sourceClass, String sourceMethod, Object result) { + log(Level.FINER, "RETURN " + sourceClass + "." + sourceMethod + " " + result); + } + + public void throwing(String sourceClass, String sourceMethod, Throwable thrown) { + log(Level.FINER, "THROW " + sourceClass + "." + sourceMethod, thrown); + } + + public void severe(String msg) { + log(Level.SEVERE, msg); + } + + public void warning(String msg) { + log(Level.WARNING, msg); + } + + public void info(String msg) { + log(Level.INFO, msg); + } + + public void config(String msg) { + log(Level.CONFIG, msg); + } + + public void fine(String msg) { + log(Level.FINE, msg); + } + + public void finer(String msg) { + log(Level.FINER, msg); + } + + public void finest(String msg) { + log(Level.FINEST, msg); + } + + public void severe(Supplier msgSupplier) { + log(Level.SEVERE, msgSupplier); + } + + public void warning(Supplier msgSupplier) { + log(Level.WARNING, msgSupplier); + } + + public void info(Supplier msgSupplier) { + log(Level.INFO, msgSupplier); + } + + public void config(Supplier msgSupplier) { + log(Level.CONFIG, msgSupplier); + } + + public void fine(Supplier msgSupplier) { + log(Level.FINE, msgSupplier); + } + + public void finer(Supplier msgSupplier) { + log(Level.FINER, msgSupplier); + } + + public void finest(Supplier msgSupplier) { + log(Level.FINEST, msgSupplier); + } +} diff --git a/Ports/CLDC11/src/java/util/prefs/BackingStoreException.java b/Ports/CLDC11/src/java/util/prefs/BackingStoreException.java new file mode 100644 index 00000000000..819fd8a4867 --- /dev/null +++ b/Ports/CLDC11/src/java/util/prefs/BackingStoreException.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.prefs; + +/// A failure of the store under the preferences. +/// [Preferences] declares it where the JDK does and never throws it: the +/// store it writes to does not fail that way. +public class BackingStoreException extends Exception { + + private static final long serialVersionUID = 1L; + + public BackingStoreException(String s) { + super(s); + } + + public BackingStoreException(Throwable cause) { + super(cause); + } +} diff --git a/Ports/CLDC11/src/java/util/prefs/Preferences.java b/Ports/CLDC11/src/java/util/prefs/Preferences.java new file mode 100644 index 00000000000..7f446fe2749 --- /dev/null +++ b/Ports/CLDC11/src/java/util/prefs/Preferences.java @@ -0,0 +1,529 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.prefs; + +import com.codename1.impl.JdkHost; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/// Preferences kept in the platform's preference store, which +/// `com.codename1.impl.JdkHost` reaches: `com.codename1.io.Preferences` in a +/// Codename One application, memory for this run elsewhere. +/// +/// The store is one flat table of the application's own, so a node is a +/// prefix: the value of `key` in the node `/com/acme` is stored under the +/// node's path and the key. The user and the system trees are the same +/// tree -- an application on a device has no machine-wide settings to write. +/// Every value is stored as text, as the JDK stores it, so `getInt` of +/// something written with `put` reads it back. +/// +/// Each node keeps the list of its keys beside them, which is what `keys` +/// and `clear` answer from; the store cannot be asked for it. A node also +/// keeps the names of its children that hold a key, or have a descendant +/// that does: that is what `childrenNames` answers and what `removeNode` +/// walks to remove a whole subtree. A node that only ever was asked for, +/// and never written to, is not listed. +/// +/// Writes are saved as they are made. `flush` and `sync` do nothing, and +/// there are no change listeners. +public abstract class Preferences { + + public static final int MAX_KEY_LENGTH = 80; + public static final int MAX_VALUE_LENGTH = 8192; + public static final int MAX_NAME_LENGTH = 80; + + private final String path; + + /// [#removalCount] as it stood when this instance was made: a removal + /// of this node, or of one above it, numbered higher happened to it. + private final long born; + + /// For the JDK's shape; the nodes this class hands out are its own. + protected Preferences() { + this("/"); + } + + private Preferences(String path) { + this.path = path; + synchronized (STORE_LOCK) { + born = removalCount; + } + } + + public static Preferences userRoot() { + return new Node("/"); + } + + public static Preferences systemRoot() { + return new Node("/"); + } + + public static Preferences userNodeForPackage(Class c) { + return new Node(packagePath(c)); + } + + public static Preferences systemNodeForPackage(Class c) { + return new Node(packagePath(c)); + } + + private static String packagePath(Class c) { + if (c.isArray()) { + throw new IllegalArgumentException("Arrays have no associated preferences node."); + } + String name = c.getName(); + int dot = name.lastIndexOf('.'); + return dot < 0 ? "/" : "/" + name.substring(0, dot).replace('.', '/'); + } + + /// The node at `pathName`: absolute when it starts with a slash, else + /// below this one. + public Preferences node(String pathName) { + checkLive(); + if (pathName.length() == 0) { + return this; + } + if (pathName.charAt(0) == '/') { + return new Node(pathName); + } + return new Node("/".equals(path) ? "/" + pathName : path + "/" + pathName); + } + + public String name() { + return "/".equals(path) ? "" : path.substring(path.lastIndexOf('/') + 1); + } + + public String absolutePath() { + return path; + } + + public Preferences parent() { + checkLive(); + if ("/".equals(path)) { + return null; + } + int slash = path.lastIndexOf('/'); + return new Node(slash <= 0 ? "/" : path.substring(0, slash)); + } + + public boolean isUserNode() { + return true; + } + + /// Where `key` of this node is kept in Codename One's flat store: the + /// node's path and the key, each escaped, around one line break. + /// + /// A key may hold a `/`, so joining the two with one aliased entries: + /// `node("a").put("b/c", ...)` and `node("a/b").put("c", ...)` both + /// wrote `/a/b/c`. A node name may hold anything but `/`, a line break + /// too, so both halves are escaped and the one raw line break left is + /// where they meet. The index ([#indexKey()]) ends in a second raw line + /// break, which no escaped key can be. + private String storeKey(String key) { + if (key == null) { + throw new NullPointerException("Null key"); + } + return escape(path) + '\n' + escape(key); + } + + private String indexKey() { + return escape(path) + "\n\n"; + } + + /// Where the names of the node's children are kept: three raw line + /// breaks, which neither a key ([#storeKey]) nor the index can end in. + private String childrenKey() { + return childrenKey(path); + } + + private static String childrenKey(String nodePath) { + return escape(nodePath) + "\n\n\n"; + } + + private static String parentPath(String nodePath) { + int slash = nodePath.lastIndexOf('/'); + return slash <= 0 ? "/" : nodePath.substring(0, slash); + } + + private static String escape(String text) { + if (text.indexOf('\\') < 0 && text.indexOf('\n') < 0) { + return text; + } + StringBuilder out = new StringBuilder(text.length() + 8); + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (c == '\\') { + out.append("\\\\"); + } else if (c == '\n') { + out.append("\\n"); + } else { + out.append(c); + } + } + return out.toString(); + } + + /// Guards every use of the store, from every node. Codename One's + /// Preferences, and the Storage under it, are written for its one + /// thread: two workers saving at once threw inside Storage, which logs + /// the failure and goes on, so a value could be lost without a word. + /// It also makes each read-modify-write of an index whole: two threads + /// adding different keys to one node each read the same index, and the + /// one that saved last dropped the other's key for good. Static, since + /// two instances can stand for one node. + private static final Object STORE_LOCK = new Object(); + + /// The nodes removed in this run, each with the number of its removal. + /// Nodes are not cached -- two calls to `node` hand out two instances + /// for one path -- so an instance cannot be told it was removed; it + /// asks here instead, for its path and every one above it. + private static final Map REMOVALS = new HashMap(); + + private static long removalCount; + + /// Throws as the JDK does for any use of a removed node. + private void checkLive() { + synchronized (STORE_LOCK) { + if (isRemoved()) { + throw new IllegalStateException("Node has been removed."); + } + } + } + + /// Called under [#STORE_LOCK]. + private boolean isRemoved() { + if (REMOVALS.isEmpty()) { + return false; + } + String at = path; + while (true) { + Long when = REMOVALS.get(at); + if (when != null && when.longValue() > born) { + return true; + } + if ("/".equals(at)) { + return false; + } + at = parentPath(at); + } + } + + /// The node's keys, one per line. A key may hold any character, a line + /// break too, so a backslash and a line break inside one are written as + /// `\\` and `\n`; see [#saveIndex]. + private List index() { + return readList(indexKey()); + } + + private void saveIndex(List keys) { + saveList(indexKey(), keys); + } + + private static List readList(String storeKey) { + List keys = new ArrayList(); + String all = stored(storeKey, ""); + StringBuilder key = new StringBuilder(); + for (int i = 0; i < all.length(); i++) { + char c = all.charAt(i); + if (c == '\\' && i + 1 < all.length()) { + i++; + key.append(all.charAt(i) == 'n' ? '\n' : all.charAt(i)); + } else if (c == '\n') { + keys.add(key.toString()); + key.setLength(0); + } else { + key.append(c); + } + } + if (key.length() > 0) { + keys.add(key.toString()); + } + return keys; + } + + private static void saveList(String storeKey, List keys) { + if (keys.isEmpty()) { + JdkHost.get().deletePreference(storeKey); + return; + } + StringBuilder all = new StringBuilder(); + for (String key : keys) { + for (int i = 0; i < key.length(); i++) { + char c = key.charAt(i); + if (c == '\\') { + all.append("\\\\"); + } else if (c == '\n') { + all.append("\\n"); + } else { + all.append(c); + } + } + all.append('\n'); + } + JdkHost.get().setPreference(storeKey, all.toString()); + } + + /// Lists this node with its parent, and so on up, so that removing an + /// ancestor finds it. Stops at the first one already listed: a node is + /// only ever listed after everything above it. + private void register() { + String at = path; + while (!"/".equals(at)) { + String parent = parentPath(at); + String name = at.substring(at.lastIndexOf('/') + 1); + String key = childrenKey(parent); + List names = readList(key); + if (names.contains(name)) { + return; + } + names.add(name); + saveList(key, names); + at = parent; + } + } + + /// Deletes every entry of `nodePath` and of every node listed below it. + private static void removeTree(String nodePath) { + for (String child : readList(childrenKey(nodePath))) { + removeTree("/".equals(nodePath) ? "/" + child : nodePath + "/" + child); + } + String prefix = escape(nodePath) + '\n'; + for (String key : readList(prefix + '\n')) { + JdkHost.get().deletePreference(prefix + escape(key)); + } + JdkHost.get().deletePreference(prefix + '\n'); + JdkHost.get().deletePreference(childrenKey(nodePath)); + } + + public void put(String key, String value) { + if (value == null) { + throw new NullPointerException("Null value"); + } + synchronized (STORE_LOCK) { + checkLive(); + JdkHost.get().setPreference(storeKey(key), value); + List keys = index(); + if (!keys.contains(key)) { + keys.add(key); + saveIndex(keys); + } + register(); + } + } + + public String get(String key, String def) { + synchronized (STORE_LOCK) { + checkLive(); + return stored(storeKey(key), def); + } + } + + public void remove(String key) { + synchronized (STORE_LOCK) { + checkLive(); + JdkHost.get().deletePreference(storeKey(key)); + List keys = index(); + if (keys.remove(key)) { + saveIndex(keys); + } + } + } + + public void clear() throws BackingStoreException { + synchronized (STORE_LOCK) { + checkLive(); + for (String key : index()) { + JdkHost.get().deletePreference(storeKey(key)); + } + saveIndex(new ArrayList()); + } + } + + public String[] keys() throws BackingStoreException { + synchronized (STORE_LOCK) { + checkLive(); + List keys = index(); + return keys.toArray(new String[keys.size()]); + } + } + + /// The children that hold a key, or have a descendant that does; see + /// the class description. + public String[] childrenNames() throws BackingStoreException { + synchronized (STORE_LOCK) { + checkLive(); + List names = readList(childrenKey()); + return names.toArray(new String[names.size()]); + } + } + + /// As the JDK: `nodeExists("")` answers whether this node is still + /// there, and is the one question a removed node may still be asked. + public boolean nodeExists(String pathName) throws BackingStoreException { + synchronized (STORE_LOCK) { + if (pathName.length() == 0) { + return !isRemoved(); + } + Preferences node = node(pathName); + return !node.index().isEmpty() || !readList(node.childrenKey()).isEmpty(); + } + } + + /// Removes this node and everything below it, as the JDK does: their + /// entries are deleted, this instance and every other one for the + /// removed nodes throw `IllegalStateException` from then on, and a node + /// asked for again by its path afterwards is a new, empty one. The root + /// cannot be removed. + public void removeNode() throws BackingStoreException { + if ("/".equals(path)) { + throw new UnsupportedOperationException("Can't remove the root!"); + } + synchronized (STORE_LOCK) { + if (isRemoved()) { + throw new IllegalStateException("Node already removed."); + } + removeTree(path); + String parentKey = childrenKey(parentPath(path)); + List siblings = readList(parentKey); + if (siblings.remove(path.substring(path.lastIndexOf('/') + 1))) { + saveList(parentKey, siblings); + } + recordRemoval(path); + } + } + + /// Called under [#STORE_LOCK]. + private static void recordRemoval(String nodePath) { + removalCount++; + REMOVALS.put(nodePath, Long.valueOf(removalCount)); + } + + public void putInt(String key, int value) { + put(key, Integer.toString(value)); + } + + public int getInt(String key, int def) { + String value = get(key, null); + if (value != null) { + try { + return Integer.parseInt(value.trim()); + } catch (NumberFormatException e) { + return def; + } + } + return def; + } + + public void putLong(String key, long value) { + put(key, Long.toString(value)); + } + + public long getLong(String key, long def) { + String value = get(key, null); + if (value != null) { + try { + return Long.parseLong(value.trim()); + } catch (NumberFormatException e) { + return def; + } + } + return def; + } + + public void putBoolean(String key, boolean value) { + put(key, value ? "true" : "false"); + } + + public boolean getBoolean(String key, boolean def) { + String value = get(key, null); + if ("true".equalsIgnoreCase(value)) { + return true; + } + if ("false".equalsIgnoreCase(value)) { + return false; + } + return def; + } + + public void putFloat(String key, float value) { + put(key, Float.toString(value)); + } + + public float getFloat(String key, float def) { + String value = get(key, null); + if (value != null) { + try { + return Float.parseFloat(value.trim()); + } catch (NumberFormatException e) { + return def; + } + } + return def; + } + + public void putDouble(String key, double value) { + put(key, Double.toString(value)); + } + + public double getDouble(String key, double def) { + String value = get(key, null); + if (value != null) { + try { + return Double.parseDouble(value.trim()); + } catch (NumberFormatException e) { + return def; + } + } + return def; + } + + public void flush() throws BackingStoreException { + // Saved as written; see the class description. + } + + public void sync() throws BackingStoreException { + // Saved as written; see the class description. A removed node + // cannot be synced, as with the JDK (flushing one is allowed). + checkLive(); + } + + @Override + public String toString() { + return "User Preference Node: " + path; + } + + /// The stored value of `storeKey`, or `def`. + private static String stored(String storeKey, String def) { + String value = JdkHost.get().preference(storeKey); + return value == null ? def : value; + } + + /// The one concrete node. + private static final class Node extends Preferences { + Node(String path) { + super(path); + } + } +} diff --git a/Ports/CLDC11/src/java/util/stream/BaseStream.java b/Ports/CLDC11/src/java/util/stream/BaseStream.java index 63e61583529..7310abdb7bb 100644 --- a/Ports/CLDC11/src/java/util/stream/BaseStream.java +++ b/Ports/CLDC11/src/java/util/stream/BaseStream.java @@ -24,14 +24,25 @@ import java.util.Iterator; +/// What the +/// object stream and the three primitive ones have in common. +/// +/// Streams here are sequential. `parallel()` exists because code calls it, +/// and answers the stream it was called on. public interface BaseStream> extends AutoCloseable { + Iterator iterator(); + boolean isParallel(); + S sequential(); S parallel(); + S unordered(); + S onClose(Runnable closeHandler); + @Override void close(); } diff --git a/Ports/CLDC11/src/java/util/stream/Collector.java b/Ports/CLDC11/src/java/util/stream/Collector.java index 67a7519bb39..767b5541de6 100644 --- a/Ports/CLDC11/src/java/util/stream/Collector.java +++ b/Ports/CLDC11/src/java/util/stream/Collector.java @@ -1,11 +1,41 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ package java.util.stream; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; import java.util.function.BiConsumer; import java.util.function.BinaryOperator; import java.util.function.Function; import java.util.function.Supplier; +/// How [Stream#collect(Collector)] folds the elements of a stream into a +/// result: a container to start from, a step per element, a way to merge two +/// containers and a last step that turns the container into the result. public interface Collector { + Supplier supplier(); BiConsumer accumulator(); @@ -13,4 +43,37 @@ public interface Collector { BinaryOperator combiner(); Function finisher(); + + Set characteristics(); + + static Collector of(Supplier supplier, BiConsumer accumulator, + BinaryOperator combiner, Characteristics... characteristics) { + if (supplier == null || accumulator == null || combiner == null || characteristics == null) { + throw new NullPointerException(); + } + Set all = new HashSet(Arrays.asList(characteristics)); + all.add(Characteristics.IDENTITY_FINISH); + return new Collectors.Of(supplier, accumulator, combiner, Function.identity(), + Collections.unmodifiableSet(all)); + } + + static Collector of(Supplier supplier, BiConsumer accumulator, + BinaryOperator combiner, Function finisher, + Characteristics... characteristics) { + if (supplier == null || accumulator == null || combiner == null || finisher == null + || characteristics == null) { + throw new NullPointerException(); + } + Set all = new HashSet(Arrays.asList(characteristics)); + return new Collectors.Of(supplier, accumulator, combiner, finisher, + Collections.unmodifiableSet(all)); + } + + /// What a collector says about itself. Nothing here acts on them: a + /// sequential stream runs every collector the same way. + enum Characteristics { + CONCURRENT, + UNORDERED, + IDENTITY_FINISH + } } diff --git a/Ports/CLDC11/src/java/util/stream/Collectors.java b/Ports/CLDC11/src/java/util/stream/Collectors.java index d1a34e20db1..3ef7bb0e244 100644 --- a/Ports/CLDC11/src/java/util/stream/Collectors.java +++ b/Ports/CLDC11/src/java/util/stream/Collectors.java @@ -1,14 +1,785 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ package java.util.stream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.DoubleSummaryStatistics; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IntSummaryStatistics; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.LongSummaryStatistics; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.function.ToDoubleFunction; +import java.util.function.ToIntFunction; +import java.util.function.ToLongFunction; + +/// The common [Collector]s: into collections, maps, strings, groups and +/// numbers. +/// +/// The results are the JDK's: `groupingBy` answers a `HashMap` of +/// `ArrayList`s unless told otherwise, `toMap` refuses a second value for a +/// key unless given a function to merge them, and the unmodifiable variants +/// refuse null. +/// +/// Every function here is a class of its own, made only by the method that +/// needs it, so an application carries the collectors it uses and no others. public final class Collectors { + private Collectors() { } - public static Collector> toList() { - return null; + /// A collector made of its four functions. + static final class Of implements Collector { + + private final Supplier supplier; + private final BiConsumer accumulator; + private final BinaryOperator combiner; + private final Function finisher; + private final Set characteristics; + + Of(Supplier supplier, BiConsumer accumulator, BinaryOperator combiner, Function finisher, + Set characteristics) { + this.supplier = supplier; + this.accumulator = accumulator; + this.combiner = combiner; + this.finisher = finisher; + this.characteristics = characteristics; + } + + @Override + public Supplier supplier() { + return supplier; + } + + @Override + public BiConsumer accumulator() { + return accumulator; + } + + @Override + public BinaryOperator combiner() { + return combiner; + } + + @Override + public Function finisher() { + return finisher; + } + + @Override + public Set characteristics() { + return characteristics; + } + } + + @SuppressWarnings("unchecked") + private static Collector of(Supplier supplier, BiConsumer accumulator, + BinaryOperator combiner, Function finisher) { + return new Of(supplier, (BiConsumer) accumulator, combiner, finisher, + Collections.emptySet()); + } + + @SuppressWarnings("unchecked") + private static Function castingIdentity() { + return (Function) Function.identity(); + } + + /// One value being folded, and whether any element reached it yet. + private static final class Box { + T value; + boolean present; + + Box(T value, boolean present) { + this.value = value; + this.present = present; + } + } + + // ---- into collections ---- + + private static > BiConsumer adder() { + return new BiConsumer() { + @Override + public void accept(C c, T t) { + c.add(t); + } + }; + } + + private static > BinaryOperator addAll() { + return new BinaryOperator() { + @Override + public C apply(C a, C b) { + a.addAll(b); + return a; + } + }; + } + + private static Supplier> newArrayList() { + return new Supplier>() { + @Override + public List get() { + return new ArrayList(); + } + }; + } + + private static Supplier> newHashSet() { + return new Supplier>() { + @Override + public Set get() { + return new HashSet(); + } + }; + } + + public static > Collector toCollection(Supplier collectionFactory) { + Objects.requireNonNull(collectionFactory); + return Collectors.of(collectionFactory, Collectors.adder(), Collectors.addAll(), + Collectors.castingIdentity()); + } + + public static Collector> toList() { + return Collectors., List>of(Collectors.newArrayList(), Collectors.>adder(), + Collectors.>addAll(), Collectors., List>castingIdentity()); + } + + public static Collector> toUnmodifiableList() { + return Collectors., List>of(Collectors.newArrayList(), Collectors.>adder(), + Collectors.>addAll(), new Function, List>() { + @Override + public List apply(List c) { + return List.copyOf(c); + } + }); + } + + public static Collector> toSet() { + return Collectors., Set>of(Collectors.newHashSet(), Collectors.>adder(), + Collectors.>addAll(), Collectors., Set>castingIdentity()); + } + + public static Collector> toUnmodifiableSet() { + return Collectors., Set>of(Collectors.newHashSet(), Collectors.>adder(), + Collectors.>addAll(), new Function, Set>() { + @Override + public Set apply(Set c) { + return Set.copyOf(c); + } + }); + } + + // ---- into a string ---- + + /// What `java.util.StringJoiner` does for [#joining(CharSequence, + /// CharSequence, CharSequence)]. + private static final class Joiner { + private final String delimiter; + private final String prefix; + private final String suffix; + private StringBuilder value; + + Joiner(CharSequence delimiter, CharSequence prefix, CharSequence suffix) { + this.delimiter = delimiter.toString(); + this.prefix = prefix.toString(); + this.suffix = suffix.toString(); + } + + private StringBuilder next() { + if (value == null) { + value = new StringBuilder().append(prefix); + } else { + value.append(delimiter); + } + return value; + } + + void add(CharSequence s) { + next().append(String.valueOf(s)); + } + + /// Adds what `other` holds, without its prefix and suffix, as one element. + void merge(Joiner other) { + if (other.value != null) { + next().append(other.value, other.prefix.length(), other.value.length()); + } + } + + @Override + public String toString() { + return value == null ? prefix + suffix : value.toString() + suffix; + } } public static Collector joining() { - return null; + return Collectors.of(new Supplier() { + @Override + public StringBuilder get() { + return new StringBuilder(); + } + }, new BiConsumer() { + @Override + public void accept(StringBuilder sb, CharSequence s) { + sb.append(String.valueOf(s)); + } + }, new BinaryOperator() { + @Override + public StringBuilder apply(StringBuilder a, StringBuilder b) { + return a.append(b.toString()); + } + }, new Function() { + @Override + public String apply(StringBuilder sb) { + return sb.toString(); + } + }); + } + + public static Collector joining(CharSequence delimiter) { + return joining(delimiter, "", ""); + } + + public static Collector joining(final CharSequence delimiter, + final CharSequence prefix, final CharSequence suffix) { + if (delimiter == null || prefix == null || suffix == null) { + throw new NullPointerException(); + } + return Collectors.of(new Supplier() { + @Override + public Joiner get() { + return new Joiner(delimiter, prefix, suffix); + } + }, new BiConsumer() { + @Override + public void accept(Joiner j, CharSequence s) { + j.add(s); + } + }, new BinaryOperator() { + @Override + public Joiner apply(Joiner a, Joiner b) { + a.merge(b); + return a; + } + }, new Function() { + @Override + public String apply(Joiner j) { + return j.toString(); + } + }); + } + + // ---- adapting another collector ---- + + public static Collector mapping(final Function mapper, + Collector downstream) { + Objects.requireNonNull(mapper); + final BiConsumer inner = downstream.accumulator(); + return Collectors.of(downstream.supplier(), new BiConsumer() { + @Override + public void accept(A a, T t) { + inner.accept(a, mapper.apply(t)); + } + }, downstream.combiner(), downstream.finisher()); + } + + public static Collector filtering(final Predicate predicate, + Collector downstream) { + Objects.requireNonNull(predicate); + final BiConsumer inner = downstream.accumulator(); + return Collectors.of(downstream.supplier(), new BiConsumer() { + @Override + public void accept(A a, T t) { + if (predicate.test(t)) { + inner.accept(a, t); + } + } + }, downstream.combiner(), downstream.finisher()); + } + + public static Collector flatMapping( + final Function> mapper, Collector downstream) { + Objects.requireNonNull(mapper); + final BiConsumer inner = downstream.accumulator(); + return Collectors.of(downstream.supplier(), new BiConsumer() { + @Override + public void accept(A a, T t) { + Stream mapped = mapper.apply(t); + if (mapped != null) { + try { + Iterator it = mapped.iterator(); + while (it.hasNext()) { + inner.accept(a, it.next()); + } + } finally { + mapped.close(); + } + } + } + }, downstream.combiner(), downstream.finisher()); + } + + public static Collector collectingAndThen(Collector downstream, + final Function finisher) { + Objects.requireNonNull(finisher); + final Function first = downstream.finisher(); + return Collectors.of(downstream.supplier(), downstream.accumulator(), downstream.combiner(), + new Function() { + @Override + public RR apply(A a) { + return finisher.apply(first.apply(a)); + } + }); + } + + // ---- numbers ---- + + /// Folds the `long` values `mapper` answers into a [LongSummaryStatistics]; + /// `mapper` null counts the elements instead. + private static Collector longs( + final ToLongFunction mapper) { + return Collectors.of( + new Supplier() { + @Override + public LongSummaryStatistics get() { + return new LongSummaryStatistics(); + } + }, new BiConsumer() { + @Override + public void accept(LongSummaryStatistics a, T t) { + a.accept(mapper == null ? 1L : mapper.applyAsLong(t)); + } + }, new BinaryOperator() { + @Override + public LongSummaryStatistics apply(LongSummaryStatistics a, LongSummaryStatistics b) { + a.combine(b); + return a; + } + }, Collectors.castingIdentity()); + } + + private static Collector ints( + final ToIntFunction mapper) { + Objects.requireNonNull(mapper); + return Collectors.of( + new Supplier() { + @Override + public IntSummaryStatistics get() { + return new IntSummaryStatistics(); + } + }, new BiConsumer() { + @Override + public void accept(IntSummaryStatistics a, T t) { + a.accept(mapper.applyAsInt(t)); + } + }, new BinaryOperator() { + @Override + public IntSummaryStatistics apply(IntSummaryStatistics a, IntSummaryStatistics b) { + a.combine(b); + return a; + } + }, Collectors.castingIdentity()); + } + + private static Collector doubles( + final ToDoubleFunction mapper) { + Objects.requireNonNull(mapper); + return Collectors.of( + new Supplier() { + @Override + public DoubleSummaryStatistics get() { + return new DoubleSummaryStatistics(); + } + }, new BiConsumer() { + @Override + public void accept(DoubleSummaryStatistics a, T t) { + a.accept(mapper.applyAsDouble(t)); + } + }, new BinaryOperator() { + @Override + public DoubleSummaryStatistics apply(DoubleSummaryStatistics a, DoubleSummaryStatistics b) { + a.combine(b); + return a; + } + }, Collectors.castingIdentity()); + } + + /// Statistics a collector folds into, answering one of their figures. + private static final int SUM = 0; + private static final int AVERAGE = 1; + private static final int COUNT = 2; + + private static Function figure(final int which) { + return new Function() { + @Override + @SuppressWarnings("unchecked") + public R apply(S s) { + Object out; + if (s instanceof IntSummaryStatistics) { + IntSummaryStatistics i = (IntSummaryStatistics) s; + out = which == SUM ? (Object) Integer.valueOf((int) i.getSum()) : Double.valueOf(i.getAverage()); + } else if (s instanceof LongSummaryStatistics) { + LongSummaryStatistics l = (LongSummaryStatistics) s; + out = which == AVERAGE ? (Object) Double.valueOf(l.getAverage()) + : Long.valueOf(which == SUM ? l.getSum() : l.getCount()); + } else { + DoubleSummaryStatistics d = (DoubleSummaryStatistics) s; + out = Double.valueOf(which == SUM ? d.getSum() : d.getAverage()); + } + return (R) out; + } + }; + } + + private static Collector finishing(Collector folded, int which) { + return Collectors.of(folded.supplier(), folded.accumulator(), folded.combiner(), + Collectors.figure(which)); + } + + public static Collector counting() { + return Collectors.finishing(Collectors.longs(null), COUNT); + } + + public static Collector summingInt(ToIntFunction mapper) { + return Collectors.finishing(ints(mapper), SUM); + } + + public static Collector summingLong(ToLongFunction mapper) { + Objects.requireNonNull(mapper); + return Collectors.finishing(longs(mapper), SUM); + } + + public static Collector summingDouble(ToDoubleFunction mapper) { + return Collectors.finishing(doubles(mapper), SUM); + } + + public static Collector averagingInt(ToIntFunction mapper) { + return Collectors.finishing(ints(mapper), AVERAGE); + } + + public static Collector averagingLong(ToLongFunction mapper) { + Objects.requireNonNull(mapper); + return Collectors.finishing(longs(mapper), AVERAGE); + } + + public static Collector averagingDouble(ToDoubleFunction mapper) { + return Collectors.finishing(doubles(mapper), AVERAGE); + } + + public static Collector summarizingInt(ToIntFunction mapper) { + return ints(mapper); + } + + public static Collector summarizingLong(ToLongFunction mapper) { + Objects.requireNonNull(mapper); + return longs(mapper); + } + + public static Collector summarizingDouble( + ToDoubleFunction mapper) { + return doubles(mapper); + } + + // ---- reductions ---- + + public static Collector reducing(T identity, BinaryOperator op) { + return Collectors.reducing(identity, Function.identity(), op); + } + + public static Collector> reducing(final BinaryOperator op) { + Objects.requireNonNull(op); + return Collectors., Optional>of(new Supplier>() { + @Override + public Box get() { + return new Box(null, false); + } + }, new BiConsumer, T>() { + @Override + public void accept(Box a, T t) { + a.value = a.present ? op.apply(a.value, t) : t; + a.present = true; + } + }, new BinaryOperator>() { + @Override + public Box apply(Box a, Box b) { + if (b.present) { + a.value = a.present ? op.apply(a.value, b.value) : b.value; + a.present = true; + } + return a; + } + }, new Function, Optional>() { + @Override + public Optional apply(Box a) { + return Optional.ofNullable(a.value); + } + }); + } + + public static Collector reducing(final U identity, final Function mapper, + final BinaryOperator op) { + Objects.requireNonNull(mapper); + Objects.requireNonNull(op); + return Collectors., U>of(new Supplier>() { + @Override + public Box get() { + return new Box(identity, true); + } + }, new BiConsumer, T>() { + @Override + public void accept(Box a, T t) { + a.value = op.apply(a.value, mapper.apply(t)); + } + }, new BinaryOperator>() { + @Override + public Box apply(Box a, Box b) { + a.value = op.apply(a.value, b.value); + return a; + } + }, new Function, U>() { + @Override + public U apply(Box a) { + return a.value; + } + }); + } + + public static Collector> minBy(Comparator comparator) { + return reducing(BinaryOperator.minBy(comparator)); + } + + public static Collector> maxBy(Comparator comparator) { + return reducing(BinaryOperator.maxBy(comparator)); + } + + // ---- groups ---- + + public static Collector>> groupingBy(Function classifier) { + return groupingBy(classifier, Collectors.toList()); + } + + public static Collector> groupingBy(Function classifier, + Collector downstream) { + return Collectors.>groupingBy(classifier, new Supplier>() { + @Override + public Map get() { + return new HashMap(); + } + }, downstream); + } + + @SuppressWarnings("unchecked") + public static > Collector groupingBy( + final Function classifier, final Supplier mapFactory, + Collector downstream) { + Objects.requireNonNull(classifier); + Objects.requireNonNull(mapFactory); + final Supplier innerSupplier = downstream.supplier(); + final BiConsumer innerAccumulator = downstream.accumulator(); + final BinaryOperator innerCombiner = downstream.combiner(); + final Function innerFinisher = downstream.finisher(); + // While elements arrive the map holds each group's container, and + // the finisher puts the group's result in its place: one map, of + // the kind the caller asked for. + return Collectors., M>of(new Supplier>() { + @Override + public Map get() { + return (Map) mapFactory.get(); + } + }, new BiConsumer, T>() { + @Override + public void accept(Map m, T t) { + K key = Objects.requireNonNull(classifier.apply(t), "element cannot be mapped to a null key"); + A container = m.get(key); + if (container == null) { + container = innerSupplier.get(); + m.put(key, container); + } + innerAccumulator.accept(container, t); + } + }, new BinaryOperator>() { + @Override + public Map apply(Map a, Map b) { + for (Map.Entry e : b.entrySet()) { + A mine = a.get(e.getKey()); + a.put(e.getKey(), mine == null ? e.getValue() : innerCombiner.apply(mine, e.getValue())); + } + return a; + } + }, new Function, M>() { + @Override + public M apply(Map m) { + Map out = (Map) m; + for (K key : new ArrayList(m.keySet())) { + out.put(key, innerFinisher.apply(m.get(key))); + } + return (M) out; + } + }); + } + + public static Collector>> partitioningBy(Predicate predicate) { + return partitioningBy(predicate, Collectors.toList()); + } + + @SuppressWarnings("unchecked") + public static Collector> partitioningBy(final Predicate predicate, + Collector downstream) { + Objects.requireNonNull(predicate); + final Supplier innerSupplier = downstream.supplier(); + final BiConsumer innerAccumulator = downstream.accumulator(); + final BinaryOperator innerCombiner = downstream.combiner(); + final Function innerFinisher = downstream.finisher(); + return Collectors.>of(new Supplier() { + @Override + public Object[] get() { + return new Object[] {innerSupplier.get(), innerSupplier.get()}; + } + }, new BiConsumer() { + @Override + public void accept(Object[] a, T t) { + innerAccumulator.accept((A) a[predicate.test(t) ? 1 : 0], t); + } + }, new BinaryOperator() { + @Override + public Object[] apply(Object[] a, Object[] b) { + a[0] = innerCombiner.apply((A) a[0], (A) b[0]); + a[1] = innerCombiner.apply((A) a[1], (A) b[1]); + return a; + } + }, new Function>() { + @Override + public Map apply(Object[] a) { + // Both keys are always there, false first. + Map out = new LinkedHashMap(); + out.put(Boolean.FALSE, innerFinisher.apply((A) a[0])); + out.put(Boolean.TRUE, innerFinisher.apply((A) a[1])); + return out; + } + }); + } + + // ---- maps ---- + + /// Puts `value` under `key`; a second value for a key is an error when + /// `mergeFunction` is null and merged with the first otherwise, a merged + /// null removing the key. + static void put(Map map, K key, U value, BinaryOperator mergeFunction) { + Objects.requireNonNull(value); + U old = map.get(key); + if (old == null) { + map.put(key, value); + return; + } + if (mergeFunction == null) { + throw new IllegalStateException("Duplicate key " + key + " (attempted merging values " + old + " and " + + value + ")"); + } + U merged = mergeFunction.apply(old, value); + if (merged == null) { + map.remove(key); + } else { + map.put(key, merged); + } + } + + public static Collector> toMap(Function keyMapper, + Function valueMapper) { + return Collectors.>toMap(keyMapper, valueMapper, null, Collectors.newHashMap()); + } + + public static Collector> toMap(Function keyMapper, + Function valueMapper, + BinaryOperator mergeFunction) { + Objects.requireNonNull(mergeFunction); + return Collectors.>toMap(keyMapper, valueMapper, mergeFunction, + Collectors.newHashMap()); + } + + private static Supplier> newHashMap() { + return new Supplier>() { + @Override + public Map get() { + return new HashMap(); + } + }; + } + + public static > Collector toMap( + final Function keyMapper, final Function valueMapper, + final BinaryOperator mergeFunction, Supplier mapFactory) { + Objects.requireNonNull(keyMapper); + Objects.requireNonNull(valueMapper); + Objects.requireNonNull(mapFactory); + return Collectors.of(mapFactory, new BiConsumer() { + @Override + public void accept(M m, T t) { + put(m, keyMapper.apply(t), valueMapper.apply(t), mergeFunction); + } + }, new BinaryOperator() { + @Override + public M apply(M a, M b) { + for (Map.Entry e : b.entrySet()) { + put(a, e.getKey(), e.getValue(), mergeFunction); + } + return a; + } + }, Collectors.castingIdentity()); + } + + private static Function, Map> unmodifiableMap() { + return new Function, Map>() { + @Override + public Map apply(Map m) { + return Map.copyOf(m); + } + }; + } + + public static Collector> toUnmodifiableMap( + Function keyMapper, Function valueMapper) { + return collectingAndThen(Collectors.toMap(keyMapper, valueMapper), + Collectors.unmodifiableMap()); + } + + public static Collector> toUnmodifiableMap( + Function keyMapper, Function valueMapper, + BinaryOperator mergeFunction) { + return collectingAndThen(Collectors.toMap(keyMapper, valueMapper, mergeFunction), + Collectors.unmodifiableMap()); } } diff --git a/Ports/CLDC11/src/java/util/stream/DoublePipeline.java b/Ports/CLDC11/src/java/util/stream/DoublePipeline.java new file mode 100644 index 00000000000..0d269c7688a --- /dev/null +++ b/Ports/CLDC11/src/java/util/stream/DoublePipeline.java @@ -0,0 +1,484 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.stream; + +import java.util.ArrayList; +import java.util.DoubleSummaryStatistics; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.PrimitiveIterator; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.DoubleBinaryOperator; +import java.util.function.DoubleConsumer; +import java.util.function.DoubleFunction; +import java.util.function.DoublePredicate; +import java.util.function.DoubleSupplier; +import java.util.function.DoubleToIntFunction; +import java.util.function.DoubleToLongFunction; +import java.util.function.DoubleUnaryOperator; +import java.util.function.Function; +import java.util.function.ObjDoubleConsumer; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.function.UnaryOperator; + +/// The one implementation of [DoubleStream]: a view of a stream of boxed values, +/// which does the work. Boxing costs little on ParparVM, where a boxed +/// `double` from `valueOf` is usually an immediate rather than an object. +final class DoublePipeline implements DoubleStream { + + private final Stream boxed; + + DoublePipeline(Stream boxed) { + this.boxed = boxed; + } + + /// The elements `from` (inclusive) to `to` (exclusive) of `values`. + static DoubleStream ofArray(final double[] values, final int from, final int to) { + if (from < 0 || to > values.length || from > to) { + throw new ArrayIndexOutOfBoundsException("origin(" + from + ") > fence(" + to + ")"); + } + return new DoublePipeline(ObjPipeline.over(new PullIterator() { + private int at = from; + + @Override + boolean advance() { + if (at >= to) { + return false; + } + emit(Double.valueOf(values[at++])); + return true; + } + })); + } + + static DoubleStream iterate(double seed, DoublePredicate hasNext, DoubleUnaryOperator next) { + Objects.requireNonNull(next); + UnaryOperator step = Fn.function(Fn.MAP, next); + if (hasNext == null) { + return new DoublePipeline(Stream.iterate(Double.valueOf(seed), step)); + } + return new DoublePipeline(Stream.iterate(Double.valueOf(seed), Fn.predicate(hasNext), step)); + } + + @SuppressWarnings("unchecked") + static DoubleStream generate(DoubleSupplier s) { + Objects.requireNonNull(s); + return new DoublePipeline(Stream.generate((Supplier) new Fn(Fn.GET, s))); + } + + /// Each primitive function this stream is handed, as the object function + /// the boxed stream underneath takes. One class for every operation keeps + /// an application that uses this stream from carrying a class per method. + @SuppressWarnings({"rawtypes", "unchecked"}) + static final class Fn implements Predicate, UnaryOperator, Consumer, Supplier { + static final int TEST = 0; + static final int MAP = 1; + static final int TO_OBJ = 2; + static final int TO_INT = 3; + static final int TO_LONG = 4; + static final int FLAT = 5; + static final int BOX = 6; + static final int ACCEPT = 7; + static final int GET = 8; + + private final int kind; + private final Object fn; + + Fn(int kind, Object fn) { + this.kind = kind; + this.fn = fn; + } + + static UnaryOperator function(int kind, Object fn) { + return new Fn(kind, fn); + } + + static Predicate predicate(DoublePredicate fn) { + return new Fn(TEST, fn); + } + + static Consumer consumer(DoubleConsumer fn) { + return new Fn(ACCEPT, fn); + } + + @Override + public boolean test(Object o) { + return ((DoublePredicate) fn).test(((Double) o).doubleValue()); + } + + @Override + public Object apply(Object o) { + if (kind == BOX) { + return o; + } + double v = ((Double) o).doubleValue(); + switch (kind) { + case MAP: + return Double.valueOf(((DoubleUnaryOperator) fn).applyAsDouble(v)); + case TO_OBJ: + return ((DoubleFunction) fn).apply(v); + case TO_INT: + return Integer.valueOf(((DoubleToIntFunction) fn).applyAsInt(v)); + case TO_LONG: + return Long.valueOf(((DoubleToLongFunction) fn).applyAsLong(v)); + default: { + DoubleStream inner = (DoubleStream) ((DoubleFunction) fn).apply(v); + return inner == null ? null : inner.boxed(); + } + } + } + + @Override + public void accept(Object o) { + ((DoubleConsumer) fn).accept(((Double) o).doubleValue()); + } + + @Override + public Object get() { + return Double.valueOf(((DoubleSupplier) fn).getAsDouble()); + } + } + + @Override + public DoubleStream filter(DoublePredicate predicate) { + Objects.requireNonNull(predicate); + return new DoublePipeline(boxed.filter(Fn.predicate(predicate))); + } + + @Override + @SuppressWarnings("unchecked") + public DoubleStream map(DoubleUnaryOperator mapper) { + Objects.requireNonNull(mapper); + return new DoublePipeline(boxed.map(Fn.function(Fn.MAP, mapper))); + } + + @Override + @SuppressWarnings("unchecked") + public Stream mapToObj(DoubleFunction mapper) { + Objects.requireNonNull(mapper); + return boxed.map(Fn.function(Fn.TO_OBJ, mapper)); + } + + @Override + public IntStream mapToInt(DoubleToIntFunction mapper) { + Objects.requireNonNull(mapper); + return new IntPipeline(boxed.map(Fn.function(Fn.TO_INT, mapper))); + } + + @Override + public LongStream mapToLong(DoubleToLongFunction mapper) { + Objects.requireNonNull(mapper); + return new LongPipeline(boxed.map(Fn.function(Fn.TO_LONG, mapper))); + } + + @Override + @SuppressWarnings("unchecked") + public DoubleStream flatMap(DoubleFunction mapper) { + Objects.requireNonNull(mapper); + return new DoublePipeline(boxed.flatMap(Fn.>function(Fn.FLAT, mapper))); + } + + @Override + public DoubleStream distinct() { + return new DoublePipeline(boxed.distinct()); + } + + @Override + public DoubleStream sorted() { + return new DoublePipeline(boxed.sorted()); + } + + @Override + public DoubleStream peek(DoubleConsumer action) { + Objects.requireNonNull(action); + return new DoublePipeline(boxed.peek(Fn.consumer(action))); + } + + @Override + public DoubleStream limit(long maxSize) { + return new DoublePipeline(boxed.limit(maxSize)); + } + + @Override + public DoubleStream skip(long n) { + return new DoublePipeline(boxed.skip(n)); + } + + @Override + public DoubleStream takeWhile(DoublePredicate predicate) { + Objects.requireNonNull(predicate); + return new DoublePipeline(boxed.takeWhile(Fn.predicate(predicate))); + } + + @Override + public DoubleStream dropWhile(DoublePredicate predicate) { + Objects.requireNonNull(predicate); + return new DoublePipeline(boxed.dropWhile(Fn.predicate(predicate))); + } + + @Override + public void forEach(DoubleConsumer action) { + Objects.requireNonNull(action); + Iterator it = boxed.iterator(); + try { + while (it.hasNext()) { + action.accept(it.next().doubleValue()); + } + } catch (Throwable e) { + throw ObjPipeline.fail(boxed, e); + } + } + + @Override + public void forEachOrdered(DoubleConsumer action) { + forEach(action); + } + + @Override + public double[] toArray() { + List all = new ArrayList(); + Iterator it = boxed.iterator(); + try { + while (it.hasNext()) { + all.add(it.next()); + } + double[] out = new double[all.size()]; + for (int i = 0; i < out.length; i++) { + out[i] = all.get(i).doubleValue(); + } + return out; + } catch (Throwable e) { + throw ObjPipeline.fail(boxed, e); + } + } + + @Override + public double reduce(double identity, DoubleBinaryOperator op) { + Objects.requireNonNull(op); + double result = identity; + Iterator it = boxed.iterator(); + try { + while (it.hasNext()) { + result = op.applyAsDouble(result, it.next().doubleValue()); + } + return result; + } catch (Throwable e) { + throw ObjPipeline.fail(boxed, e); + } + } + + @Override + public OptionalDouble reduce(DoubleBinaryOperator op) { + Objects.requireNonNull(op); + Iterator it = boxed.iterator(); + try { + if (!it.hasNext()) { + return OptionalDouble.empty(); + } + double result = it.next().doubleValue(); + while (it.hasNext()) { + result = op.applyAsDouble(result, it.next().doubleValue()); + } + return OptionalDouble.of(result); + } catch (Throwable e) { + throw ObjPipeline.fail(boxed, e); + } + } + + @Override + public R collect(Supplier supplier, ObjDoubleConsumer accumulator, BiConsumer combiner) { + Objects.requireNonNull(supplier); + Objects.requireNonNull(accumulator); + Objects.requireNonNull(combiner); + R result = supplier.get(); + Iterator it = boxed.iterator(); + try { + while (it.hasNext()) { + accumulator.accept(result, it.next().doubleValue()); + } + return result; + } catch (Throwable e) { + throw ObjPipeline.fail(boxed, e); + } + } + + @Override + public double sum() { + return summaryStatistics().getSum(); + } + + /// The smallest (`max` false) or largest value, as `Math.min` and + /// `Math.max` choose, which is what the JDK's reduction uses. + private OptionalDouble extreme(boolean max) { + Iterator it = boxed.iterator(); + try { + if (!it.hasNext()) { + return OptionalDouble.empty(); + } + double result = it.next().doubleValue(); + while (it.hasNext()) { + double v = it.next().doubleValue(); + result = max ? Math.max(result, v) : Math.min(result, v); + } + return OptionalDouble.of(result); + } catch (Throwable e) { + throw ObjPipeline.fail(boxed, e); + } + } + + @Override + public OptionalDouble min() { + return extreme(false); + } + + @Override + public OptionalDouble max() { + return extreme(true); + } + + @Override + public long count() { + return boxed.count(); + } + + @Override + public OptionalDouble average() { + DoubleSummaryStatistics all = summaryStatistics(); + return all.getCount() == 0 ? OptionalDouble.empty() : OptionalDouble.of(all.getAverage()); + } + + @Override + public DoubleSummaryStatistics summaryStatistics() { + DoubleSummaryStatistics all = new DoubleSummaryStatistics(); + Iterator it = boxed.iterator(); + try { + while (it.hasNext()) { + all.accept(it.next().doubleValue()); + } + return all; + } catch (Throwable e) { + throw ObjPipeline.fail(boxed, e); + } + } + + @Override + public boolean anyMatch(DoublePredicate predicate) { + Objects.requireNonNull(predicate); + return boxed.anyMatch(Fn.predicate(predicate)); + } + + @Override + public boolean allMatch(DoublePredicate predicate) { + Objects.requireNonNull(predicate); + return boxed.allMatch(Fn.predicate(predicate)); + } + + @Override + public boolean noneMatch(DoublePredicate predicate) { + Objects.requireNonNull(predicate); + return boxed.noneMatch(Fn.predicate(predicate)); + } + + @Override + public OptionalDouble findFirst() { + // Through the boxed stream's own, which says that the rest is not + // going to be read: a mapped stream left part read is closed. + Optional first = boxed.findFirst(); + return first.isPresent() ? OptionalDouble.of(first.get().doubleValue()) : OptionalDouble.empty(); + } + + @Override + public OptionalDouble findAny() { + return findFirst(); + } + + @Override + @SuppressWarnings("unchecked") + public Stream boxed() { + // A stage of its own, so that this stream counts as used like any + // other that had an operation applied to it. + return boxed.map(Fn.function(Fn.BOX, null)); + } + + @Override + public PrimitiveIterator.OfDouble iterator() { + return new Unboxing(boxed.iterator()); + } + + /// The elements of the boxed pipeline, one primitive at a time. + private static final class Unboxing implements PrimitiveIterator.OfDouble { + private final Iterator it; + + Unboxing(Iterator it) { + this.it = it; + } + + @Override + public boolean hasNext() { + return it.hasNext(); + } + + @Override + public double nextDouble() { + return it.next().doubleValue(); + } + } + + @Override + public boolean isParallel() { + return boxed.isParallel(); + } + + @Override + public DoubleStream sequential() { + boxed.sequential(); + return this; + } + + @Override + public DoubleStream parallel() { + boxed.parallel(); + return this; + } + + @Override + public DoubleStream unordered() { + return this; + } + + @Override + public DoubleStream onClose(Runnable closeHandler) { + boxed.onClose(closeHandler); + return this; + } + + @Override + public void close() { + boxed.close(); + } +} diff --git a/Ports/CLDC11/src/java/util/stream/DoubleStream.java b/Ports/CLDC11/src/java/util/stream/DoubleStream.java new file mode 100644 index 00000000000..907d9f74769 --- /dev/null +++ b/Ports/CLDC11/src/java/util/stream/DoubleStream.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.stream; + +import java.util.DoubleSummaryStatistics; +import java.util.OptionalDouble; +import java.util.PrimitiveIterator; +import java.util.function.BiConsumer; +import java.util.function.DoubleBinaryOperator; +import java.util.function.DoubleConsumer; +import java.util.function.DoubleFunction; +import java.util.function.DoublePredicate; +import java.util.function.DoubleSupplier; +import java.util.function.DoubleToIntFunction; +import java.util.function.DoubleToLongFunction; +import java.util.function.DoubleUnaryOperator; +import java.util.function.ObjDoubleConsumer; +import java.util.function.Supplier; + +/// A sequence of `double` values to which operations are applied one stage at a +/// time, evaluated lazily, one element at a time, when a terminal operation +/// asks for them. +/// +/// Streams here are sequential: `parallel()` answers the same stream, which +/// the JDK allows a parallel stream to be. +public interface DoubleStream extends BaseStream { + + /// Declared again with this type, as the JDK does: a call compiled + /// against the JDK names `DoubleStream.sequential()` answering `DoubleStream`, + /// and the method inherited from [BaseStream] answers its type + /// variable, which is another descriptor. + @Override + DoubleStream sequential(); + + /// Declared again with this type; see [#sequential()]. + @Override + DoubleStream parallel(); + + DoubleStream filter(DoublePredicate predicate); + + DoubleStream map(DoubleUnaryOperator mapper); + + Stream mapToObj(DoubleFunction mapper); + + IntStream mapToInt(DoubleToIntFunction mapper); + + LongStream mapToLong(DoubleToLongFunction mapper); + + DoubleStream flatMap(DoubleFunction mapper); + + DoubleStream distinct(); + + DoubleStream sorted(); + + DoubleStream peek(DoubleConsumer action); + + DoubleStream limit(long maxSize); + + DoubleStream skip(long n); + + DoubleStream takeWhile(DoublePredicate predicate); + + DoubleStream dropWhile(DoublePredicate predicate); + + void forEach(DoubleConsumer action); + + void forEachOrdered(DoubleConsumer action); + + double[] toArray(); + + double reduce(double identity, DoubleBinaryOperator op); + + OptionalDouble reduce(DoubleBinaryOperator op); + + R collect(Supplier supplier, ObjDoubleConsumer accumulator, BiConsumer combiner); + + double sum(); + + OptionalDouble min(); + + OptionalDouble max(); + + long count(); + + OptionalDouble average(); + + DoubleSummaryStatistics summaryStatistics(); + + boolean anyMatch(DoublePredicate predicate); + + boolean allMatch(DoublePredicate predicate); + + boolean noneMatch(DoublePredicate predicate); + + OptionalDouble findFirst(); + + OptionalDouble findAny(); + + Stream boxed(); + + @Override + PrimitiveIterator.OfDouble iterator(); + + static DoubleStream empty() { + return DoublePipeline.ofArray(new double[0], 0, 0); + } + + static DoubleStream of(double value) { + return DoublePipeline.ofArray(new double[] {value}, 0, 1); + } + + static DoubleStream of(double... values) { + return DoublePipeline.ofArray(values, 0, values.length); + } + + /// `seed`, then `f` applied to it, then `f` applied to that, without end. + static DoubleStream iterate(double seed, DoubleUnaryOperator f) { + return DoublePipeline.iterate(seed, null, f); + } + + /// As [#iterate(double, DoubleUnaryOperator)], ending before the first value + /// `hasNext` refuses. + static DoubleStream iterate(double seed, DoublePredicate hasNext, DoubleUnaryOperator next) { + if (hasNext == null) { + throw new NullPointerException(); + } + return DoublePipeline.iterate(seed, hasNext, next); + } + + /// The values `s` answers, one call per element, without end. + static DoubleStream generate(DoubleSupplier s) { + return DoublePipeline.generate(s); + } + + /// The elements of `a`, then those of `b`. Closing the result closes both. + static DoubleStream concat(DoubleStream a, DoubleStream b) { + return new DoublePipeline(Stream.concat(a.boxed(), b.boxed())); + } +} diff --git a/Ports/CLDC11/src/java/util/stream/IntPipeline.java b/Ports/CLDC11/src/java/util/stream/IntPipeline.java new file mode 100644 index 00000000000..c0dec4cd833 --- /dev/null +++ b/Ports/CLDC11/src/java/util/stream/IntPipeline.java @@ -0,0 +1,536 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.stream; + +import java.util.ArrayList; +import java.util.IntSummaryStatistics; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.PrimitiveIterator; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.IntBinaryOperator; +import java.util.function.IntConsumer; +import java.util.function.IntFunction; +import java.util.function.IntPredicate; +import java.util.function.IntSupplier; +import java.util.function.IntToDoubleFunction; +import java.util.function.IntToLongFunction; +import java.util.function.IntUnaryOperator; +import java.util.function.ObjIntConsumer; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.function.UnaryOperator; + +/// The one implementation of [IntStream]: a view of a stream of boxed values, +/// which does the work. Boxing costs little on ParparVM, where a boxed +/// `int` from `valueOf` is usually an immediate rather than an object. +final class IntPipeline implements IntStream { + + private final Stream boxed; + + IntPipeline(Stream boxed) { + this.boxed = boxed; + } + + /// The elements `from` (inclusive) to `to` (exclusive) of `values`. + static IntStream ofArray(final int[] values, final int from, final int to) { + if (from < 0 || to > values.length || from > to) { + throw new ArrayIndexOutOfBoundsException("origin(" + from + ") > fence(" + to + ")"); + } + return new IntPipeline(ObjPipeline.over(new PullIterator() { + private int at = from; + + @Override + boolean advance() { + if (at >= to) { + return false; + } + emit(Integer.valueOf(values[at++])); + return true; + } + })); + } + + /// The values from `start` up to `end`, which is part of them when + /// `closed`. An empty stream when `start` is past `end`. + static IntStream ofRange(final int start, final int end, final boolean closed) { + return new IntPipeline(ObjPipeline.over(new PullIterator() { + private int next = start; + /// Whether `next` is still to be handed out. Counting towards + /// `end` with a flag, not past it, is what keeps a closed range + /// that ends at the largest value from wrapping around. + private boolean more = closed ? start <= end : start < end; + + @Override + boolean advance() { + if (!more) { + return false; + } + emit(Integer.valueOf(next)); + if (closed ? next < end : next < end - 1) { + next++; + } else { + more = false; + } + return true; + } + })); + } + + static IntStream iterate(int seed, IntPredicate hasNext, IntUnaryOperator next) { + Objects.requireNonNull(next); + UnaryOperator step = Fn.function(Fn.MAP, next); + if (hasNext == null) { + return new IntPipeline(Stream.iterate(Integer.valueOf(seed), step)); + } + return new IntPipeline(Stream.iterate(Integer.valueOf(seed), Fn.predicate(hasNext), step)); + } + + @SuppressWarnings("unchecked") + static IntStream generate(IntSupplier s) { + Objects.requireNonNull(s); + return new IntPipeline(Stream.generate((Supplier) new Fn(Fn.GET, s))); + } + + /// Each primitive function this stream is handed, as the object function + /// the boxed stream underneath takes. One class for every operation keeps + /// an application that uses this stream from carrying a class per method. + @SuppressWarnings({"rawtypes", "unchecked"}) + static final class Fn implements Predicate, UnaryOperator, Consumer, Supplier { + static final int TEST = 0; + static final int MAP = 1; + static final int TO_OBJ = 2; + static final int TO_LONG = 3; + static final int TO_DOUBLE = 4; + static final int WIDEN_LONG = 5; + static final int WIDEN_DOUBLE = 6; + static final int FLAT = 7; + static final int BOX = 8; + static final int ACCEPT = 9; + static final int GET = 10; + + private final int kind; + private final Object fn; + + Fn(int kind, Object fn) { + this.kind = kind; + this.fn = fn; + } + + static UnaryOperator function(int kind, Object fn) { + return new Fn(kind, fn); + } + + static Predicate predicate(IntPredicate fn) { + return new Fn(TEST, fn); + } + + static Consumer consumer(IntConsumer fn) { + return new Fn(ACCEPT, fn); + } + + @Override + public boolean test(Object o) { + return ((IntPredicate) fn).test(((Integer) o).intValue()); + } + + @Override + public Object apply(Object o) { + if (kind == BOX) { + return o; + } + int v = ((Integer) o).intValue(); + switch (kind) { + case MAP: + return Integer.valueOf(((IntUnaryOperator) fn).applyAsInt(v)); + case TO_OBJ: + return ((IntFunction) fn).apply(v); + case TO_LONG: + return Long.valueOf(((IntToLongFunction) fn).applyAsLong(v)); + case TO_DOUBLE: + return Double.valueOf(((IntToDoubleFunction) fn).applyAsDouble(v)); + case WIDEN_LONG: + return Long.valueOf(v); + case WIDEN_DOUBLE: + return Double.valueOf(v); + default: { + IntStream inner = (IntStream) ((IntFunction) fn).apply(v); + return inner == null ? null : inner.boxed(); + } + } + } + + @Override + public void accept(Object o) { + ((IntConsumer) fn).accept(((Integer) o).intValue()); + } + + @Override + public Object get() { + return Integer.valueOf(((IntSupplier) fn).getAsInt()); + } + } + + @Override + public IntStream filter(IntPredicate predicate) { + Objects.requireNonNull(predicate); + return new IntPipeline(boxed.filter(Fn.predicate(predicate))); + } + + @Override + @SuppressWarnings("unchecked") + public IntStream map(IntUnaryOperator mapper) { + Objects.requireNonNull(mapper); + return new IntPipeline(boxed.map(Fn.function(Fn.MAP, mapper))); + } + + @Override + @SuppressWarnings("unchecked") + public Stream mapToObj(IntFunction mapper) { + Objects.requireNonNull(mapper); + return boxed.map(Fn.function(Fn.TO_OBJ, mapper)); + } + + @Override + public LongStream mapToLong(IntToLongFunction mapper) { + Objects.requireNonNull(mapper); + return new LongPipeline(boxed.map(Fn.function(Fn.TO_LONG, mapper))); + } + + @Override + public DoubleStream mapToDouble(IntToDoubleFunction mapper) { + Objects.requireNonNull(mapper); + return new DoublePipeline(boxed.map(Fn.function(Fn.TO_DOUBLE, mapper))); + } + + @Override + public LongStream asLongStream() { + return new LongPipeline(boxed.map(Fn.function(Fn.WIDEN_LONG, null))); + } + + @Override + public DoubleStream asDoubleStream() { + return new DoublePipeline(boxed.map(Fn.function(Fn.WIDEN_DOUBLE, null))); + } + + @Override + @SuppressWarnings("unchecked") + public IntStream flatMap(IntFunction mapper) { + Objects.requireNonNull(mapper); + return new IntPipeline(boxed.flatMap(Fn.>function(Fn.FLAT, mapper))); + } + + @Override + public IntStream distinct() { + return new IntPipeline(boxed.distinct()); + } + + @Override + public IntStream sorted() { + return new IntPipeline(boxed.sorted()); + } + + @Override + public IntStream peek(IntConsumer action) { + Objects.requireNonNull(action); + return new IntPipeline(boxed.peek(Fn.consumer(action))); + } + + @Override + public IntStream limit(long maxSize) { + return new IntPipeline(boxed.limit(maxSize)); + } + + @Override + public IntStream skip(long n) { + return new IntPipeline(boxed.skip(n)); + } + + @Override + public IntStream takeWhile(IntPredicate predicate) { + Objects.requireNonNull(predicate); + return new IntPipeline(boxed.takeWhile(Fn.predicate(predicate))); + } + + @Override + public IntStream dropWhile(IntPredicate predicate) { + Objects.requireNonNull(predicate); + return new IntPipeline(boxed.dropWhile(Fn.predicate(predicate))); + } + + @Override + public void forEach(IntConsumer action) { + Objects.requireNonNull(action); + Iterator it = boxed.iterator(); + try { + while (it.hasNext()) { + action.accept(it.next().intValue()); + } + } catch (Throwable e) { + throw ObjPipeline.fail(boxed, e); + } + } + + @Override + public void forEachOrdered(IntConsumer action) { + forEach(action); + } + + @Override + public int[] toArray() { + List all = new ArrayList(); + Iterator it = boxed.iterator(); + try { + while (it.hasNext()) { + all.add(it.next()); + } + int[] out = new int[all.size()]; + for (int i = 0; i < out.length; i++) { + out[i] = all.get(i).intValue(); + } + return out; + } catch (Throwable e) { + throw ObjPipeline.fail(boxed, e); + } + } + + @Override + public int reduce(int identity, IntBinaryOperator op) { + Objects.requireNonNull(op); + int result = identity; + Iterator it = boxed.iterator(); + try { + while (it.hasNext()) { + result = op.applyAsInt(result, it.next().intValue()); + } + return result; + } catch (Throwable e) { + throw ObjPipeline.fail(boxed, e); + } + } + + @Override + public OptionalInt reduce(IntBinaryOperator op) { + Objects.requireNonNull(op); + Iterator it = boxed.iterator(); + try { + if (!it.hasNext()) { + return OptionalInt.empty(); + } + int result = it.next().intValue(); + while (it.hasNext()) { + result = op.applyAsInt(result, it.next().intValue()); + } + return OptionalInt.of(result); + } catch (Throwable e) { + throw ObjPipeline.fail(boxed, e); + } + } + + @Override + public R collect(Supplier supplier, ObjIntConsumer accumulator, BiConsumer combiner) { + Objects.requireNonNull(supplier); + Objects.requireNonNull(accumulator); + Objects.requireNonNull(combiner); + R result = supplier.get(); + Iterator it = boxed.iterator(); + try { + while (it.hasNext()) { + accumulator.accept(result, it.next().intValue()); + } + return result; + } catch (Throwable e) { + throw ObjPipeline.fail(boxed, e); + } + } + + @Override + public int sum() { + int total = 0; + Iterator it = boxed.iterator(); + try { + while (it.hasNext()) { + total += it.next().intValue(); + } + return total; + } catch (Throwable e) { + throw ObjPipeline.fail(boxed, e); + } + } + + /// The smallest (`max` false) or largest value, as `Math.min` and + /// `Math.max` choose, which is what the JDK's reduction uses. + private OptionalInt extreme(boolean max) { + Iterator it = boxed.iterator(); + try { + if (!it.hasNext()) { + return OptionalInt.empty(); + } + int result = it.next().intValue(); + while (it.hasNext()) { + int v = it.next().intValue(); + result = max ? Math.max(result, v) : Math.min(result, v); + } + return OptionalInt.of(result); + } catch (Throwable e) { + throw ObjPipeline.fail(boxed, e); + } + } + + @Override + public OptionalInt min() { + return extreme(false); + } + + @Override + public OptionalInt max() { + return extreme(true); + } + + @Override + public long count() { + return boxed.count(); + } + + @Override + public OptionalDouble average() { + IntSummaryStatistics all = summaryStatistics(); + return all.getCount() == 0 ? OptionalDouble.empty() : OptionalDouble.of(all.getAverage()); + } + + @Override + public IntSummaryStatistics summaryStatistics() { + IntSummaryStatistics all = new IntSummaryStatistics(); + Iterator it = boxed.iterator(); + try { + while (it.hasNext()) { + all.accept(it.next().intValue()); + } + return all; + } catch (Throwable e) { + throw ObjPipeline.fail(boxed, e); + } + } + + @Override + public boolean anyMatch(IntPredicate predicate) { + Objects.requireNonNull(predicate); + return boxed.anyMatch(Fn.predicate(predicate)); + } + + @Override + public boolean allMatch(IntPredicate predicate) { + Objects.requireNonNull(predicate); + return boxed.allMatch(Fn.predicate(predicate)); + } + + @Override + public boolean noneMatch(IntPredicate predicate) { + Objects.requireNonNull(predicate); + return boxed.noneMatch(Fn.predicate(predicate)); + } + + @Override + public OptionalInt findFirst() { + // Through the boxed stream's own, which says that the rest is not + // going to be read: a mapped stream left part read is closed. + Optional first = boxed.findFirst(); + return first.isPresent() ? OptionalInt.of(first.get().intValue()) : OptionalInt.empty(); + } + + @Override + public OptionalInt findAny() { + return findFirst(); + } + + @Override + @SuppressWarnings("unchecked") + public Stream boxed() { + // A stage of its own, so that this stream counts as used like any + // other that had an operation applied to it. + return boxed.map(Fn.function(Fn.BOX, null)); + } + + @Override + public PrimitiveIterator.OfInt iterator() { + return new Unboxing(boxed.iterator()); + } + + /// The elements of the boxed pipeline, one primitive at a time. + private static final class Unboxing implements PrimitiveIterator.OfInt { + private final Iterator it; + + Unboxing(Iterator it) { + this.it = it; + } + + @Override + public boolean hasNext() { + return it.hasNext(); + } + + @Override + public int nextInt() { + return it.next().intValue(); + } + } + + @Override + public boolean isParallel() { + return boxed.isParallel(); + } + + @Override + public IntStream sequential() { + boxed.sequential(); + return this; + } + + @Override + public IntStream parallel() { + boxed.parallel(); + return this; + } + + @Override + public IntStream unordered() { + return this; + } + + @Override + public IntStream onClose(Runnable closeHandler) { + boxed.onClose(closeHandler); + return this; + } + + @Override + public void close() { + boxed.close(); + } +} diff --git a/Ports/CLDC11/src/java/util/stream/IntStream.java b/Ports/CLDC11/src/java/util/stream/IntStream.java new file mode 100644 index 00000000000..c5edbe6ad4f --- /dev/null +++ b/Ports/CLDC11/src/java/util/stream/IntStream.java @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.stream; + +import java.util.IntSummaryStatistics; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.PrimitiveIterator; +import java.util.function.BiConsumer; +import java.util.function.IntBinaryOperator; +import java.util.function.IntConsumer; +import java.util.function.IntFunction; +import java.util.function.IntPredicate; +import java.util.function.IntSupplier; +import java.util.function.IntToDoubleFunction; +import java.util.function.IntToLongFunction; +import java.util.function.IntUnaryOperator; +import java.util.function.ObjIntConsumer; +import java.util.function.Supplier; + +/// A sequence of `int` values to which operations are applied one stage at a +/// time, evaluated lazily, one element at a time, when a terminal operation +/// asks for them. +/// +/// Streams here are sequential: `parallel()` answers the same stream, which +/// the JDK allows a parallel stream to be. +public interface IntStream extends BaseStream { + + /// Declared again with this type, as the JDK does: a call compiled + /// against the JDK names `IntStream.sequential()` answering `IntStream`, + /// and the method inherited from [BaseStream] answers its type + /// variable, which is another descriptor. + @Override + IntStream sequential(); + + /// Declared again with this type; see [#sequential()]. + @Override + IntStream parallel(); + + IntStream filter(IntPredicate predicate); + + IntStream map(IntUnaryOperator mapper); + + Stream mapToObj(IntFunction mapper); + + LongStream mapToLong(IntToLongFunction mapper); + + DoubleStream mapToDouble(IntToDoubleFunction mapper); + + IntStream flatMap(IntFunction mapper); + + IntStream distinct(); + + IntStream sorted(); + + IntStream peek(IntConsumer action); + + IntStream limit(long maxSize); + + IntStream skip(long n); + + IntStream takeWhile(IntPredicate predicate); + + IntStream dropWhile(IntPredicate predicate); + + void forEach(IntConsumer action); + + void forEachOrdered(IntConsumer action); + + int[] toArray(); + + int reduce(int identity, IntBinaryOperator op); + + OptionalInt reduce(IntBinaryOperator op); + + R collect(Supplier supplier, ObjIntConsumer accumulator, BiConsumer combiner); + + int sum(); + + OptionalInt min(); + + OptionalInt max(); + + long count(); + + OptionalDouble average(); + + IntSummaryStatistics summaryStatistics(); + + boolean anyMatch(IntPredicate predicate); + + boolean allMatch(IntPredicate predicate); + + boolean noneMatch(IntPredicate predicate); + + OptionalInt findFirst(); + + OptionalInt findAny(); + + LongStream asLongStream(); + + DoubleStream asDoubleStream(); + + Stream boxed(); + + @Override + PrimitiveIterator.OfInt iterator(); + + static IntStream empty() { + return IntPipeline.ofArray(new int[0], 0, 0); + } + + static IntStream of(int value) { + return IntPipeline.ofArray(new int[] {value}, 0, 1); + } + + static IntStream of(int... values) { + return IntPipeline.ofArray(values, 0, values.length); + } + + /// `seed`, then `f` applied to it, then `f` applied to that, without end. + static IntStream iterate(int seed, IntUnaryOperator f) { + return IntPipeline.iterate(seed, null, f); + } + + /// As [#iterate(int, IntUnaryOperator)], ending before the first value + /// `hasNext` refuses. + static IntStream iterate(int seed, IntPredicate hasNext, IntUnaryOperator next) { + if (hasNext == null) { + throw new NullPointerException(); + } + return IntPipeline.iterate(seed, hasNext, next); + } + + /// The values `s` answers, one call per element, without end. + static IntStream generate(IntSupplier s) { + return IntPipeline.generate(s); + } + + /// The values from `startInclusive` up to, not including, `endExclusive`, + /// in steps of one. Empty when `startInclusive` is not below `endExclusive`. + static IntStream range(int startInclusive, int endExclusive) { + return IntPipeline.ofRange(startInclusive, endExclusive, false); + } + + /// The values from `startInclusive` up to and including `endInclusive`, in + /// steps of one. Empty when `startInclusive` is above `endInclusive`. + static IntStream rangeClosed(int startInclusive, int endInclusive) { + return IntPipeline.ofRange(startInclusive, endInclusive, true); + } + + /// The elements of `a`, then those of `b`. Closing the result closes both. + static IntStream concat(IntStream a, IntStream b) { + return new IntPipeline(Stream.concat(a.boxed(), b.boxed())); + } +} diff --git a/Ports/CLDC11/src/java/util/stream/LongPipeline.java b/Ports/CLDC11/src/java/util/stream/LongPipeline.java new file mode 100644 index 00000000000..b58c5921d4d --- /dev/null +++ b/Ports/CLDC11/src/java/util/stream/LongPipeline.java @@ -0,0 +1,528 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.stream; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.LongSummaryStatistics; +import java.util.Objects; +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalLong; +import java.util.PrimitiveIterator; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.LongBinaryOperator; +import java.util.function.LongConsumer; +import java.util.function.LongFunction; +import java.util.function.LongPredicate; +import java.util.function.LongSupplier; +import java.util.function.LongToDoubleFunction; +import java.util.function.LongToIntFunction; +import java.util.function.LongUnaryOperator; +import java.util.function.ObjLongConsumer; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.function.UnaryOperator; + +/// The one implementation of [LongStream]: a view of a stream of boxed values, +/// which does the work. Boxing costs little on ParparVM, where a boxed +/// `long` from `valueOf` is usually an immediate rather than an object. +final class LongPipeline implements LongStream { + + private final Stream boxed; + + LongPipeline(Stream boxed) { + this.boxed = boxed; + } + + /// The elements `from` (inclusive) to `to` (exclusive) of `values`. + static LongStream ofArray(final long[] values, final int from, final int to) { + if (from < 0 || to > values.length || from > to) { + throw new ArrayIndexOutOfBoundsException("origin(" + from + ") > fence(" + to + ")"); + } + return new LongPipeline(ObjPipeline.over(new PullIterator() { + private int at = from; + + @Override + boolean advance() { + if (at >= to) { + return false; + } + emit(Long.valueOf(values[at++])); + return true; + } + })); + } + + /// The values from `start` up to `end`, which is part of them when + /// `closed`. An empty stream when `start` is past `end`. + static LongStream ofRange(final long start, final long end, final boolean closed) { + return new LongPipeline(ObjPipeline.over(new PullIterator() { + private long next = start; + /// Whether `next` is still to be handed out. Counting towards + /// `end` with a flag, not past it, is what keeps a closed range + /// that ends at the largest value from wrapping around. + private boolean more = closed ? start <= end : start < end; + + @Override + boolean advance() { + if (!more) { + return false; + } + emit(Long.valueOf(next)); + if (closed ? next < end : next < end - 1) { + next++; + } else { + more = false; + } + return true; + } + })); + } + + static LongStream iterate(long seed, LongPredicate hasNext, LongUnaryOperator next) { + Objects.requireNonNull(next); + UnaryOperator step = Fn.function(Fn.MAP, next); + if (hasNext == null) { + return new LongPipeline(Stream.iterate(Long.valueOf(seed), step)); + } + return new LongPipeline(Stream.iterate(Long.valueOf(seed), Fn.predicate(hasNext), step)); + } + + @SuppressWarnings("unchecked") + static LongStream generate(LongSupplier s) { + Objects.requireNonNull(s); + return new LongPipeline(Stream.generate((Supplier) new Fn(Fn.GET, s))); + } + + /// Each primitive function this stream is handed, as the object function + /// the boxed stream underneath takes. One class for every operation keeps + /// an application that uses this stream from carrying a class per method. + @SuppressWarnings({"rawtypes", "unchecked"}) + static final class Fn implements Predicate, UnaryOperator, Consumer, Supplier { + static final int TEST = 0; + static final int MAP = 1; + static final int TO_OBJ = 2; + static final int TO_INT = 3; + static final int TO_DOUBLE = 4; + static final int WIDEN_DOUBLE = 5; + static final int FLAT = 6; + static final int BOX = 7; + static final int ACCEPT = 8; + static final int GET = 9; + + private final int kind; + private final Object fn; + + Fn(int kind, Object fn) { + this.kind = kind; + this.fn = fn; + } + + static UnaryOperator function(int kind, Object fn) { + return new Fn(kind, fn); + } + + static Predicate predicate(LongPredicate fn) { + return new Fn(TEST, fn); + } + + static Consumer consumer(LongConsumer fn) { + return new Fn(ACCEPT, fn); + } + + @Override + public boolean test(Object o) { + return ((LongPredicate) fn).test(((Long) o).longValue()); + } + + @Override + public Object apply(Object o) { + if (kind == BOX) { + return o; + } + long v = ((Long) o).longValue(); + switch (kind) { + case MAP: + return Long.valueOf(((LongUnaryOperator) fn).applyAsLong(v)); + case TO_OBJ: + return ((LongFunction) fn).apply(v); + case TO_INT: + return Integer.valueOf(((LongToIntFunction) fn).applyAsInt(v)); + case TO_DOUBLE: + return Double.valueOf(((LongToDoubleFunction) fn).applyAsDouble(v)); + case WIDEN_DOUBLE: + return Double.valueOf(v); + default: { + LongStream inner = (LongStream) ((LongFunction) fn).apply(v); + return inner == null ? null : inner.boxed(); + } + } + } + + @Override + public void accept(Object o) { + ((LongConsumer) fn).accept(((Long) o).longValue()); + } + + @Override + public Object get() { + return Long.valueOf(((LongSupplier) fn).getAsLong()); + } + } + + @Override + public LongStream filter(LongPredicate predicate) { + Objects.requireNonNull(predicate); + return new LongPipeline(boxed.filter(Fn.predicate(predicate))); + } + + @Override + @SuppressWarnings("unchecked") + public LongStream map(LongUnaryOperator mapper) { + Objects.requireNonNull(mapper); + return new LongPipeline(boxed.map(Fn.function(Fn.MAP, mapper))); + } + + @Override + @SuppressWarnings("unchecked") + public Stream mapToObj(LongFunction mapper) { + Objects.requireNonNull(mapper); + return boxed.map(Fn.function(Fn.TO_OBJ, mapper)); + } + + @Override + public IntStream mapToInt(LongToIntFunction mapper) { + Objects.requireNonNull(mapper); + return new IntPipeline(boxed.map(Fn.function(Fn.TO_INT, mapper))); + } + + @Override + public DoubleStream mapToDouble(LongToDoubleFunction mapper) { + Objects.requireNonNull(mapper); + return new DoublePipeline(boxed.map(Fn.function(Fn.TO_DOUBLE, mapper))); + } + + @Override + public DoubleStream asDoubleStream() { + return new DoublePipeline(boxed.map(Fn.function(Fn.WIDEN_DOUBLE, null))); + } + + @Override + @SuppressWarnings("unchecked") + public LongStream flatMap(LongFunction mapper) { + Objects.requireNonNull(mapper); + return new LongPipeline(boxed.flatMap(Fn.>function(Fn.FLAT, mapper))); + } + + @Override + public LongStream distinct() { + return new LongPipeline(boxed.distinct()); + } + + @Override + public LongStream sorted() { + return new LongPipeline(boxed.sorted()); + } + + @Override + public LongStream peek(LongConsumer action) { + Objects.requireNonNull(action); + return new LongPipeline(boxed.peek(Fn.consumer(action))); + } + + @Override + public LongStream limit(long maxSize) { + return new LongPipeline(boxed.limit(maxSize)); + } + + @Override + public LongStream skip(long n) { + return new LongPipeline(boxed.skip(n)); + } + + @Override + public LongStream takeWhile(LongPredicate predicate) { + Objects.requireNonNull(predicate); + return new LongPipeline(boxed.takeWhile(Fn.predicate(predicate))); + } + + @Override + public LongStream dropWhile(LongPredicate predicate) { + Objects.requireNonNull(predicate); + return new LongPipeline(boxed.dropWhile(Fn.predicate(predicate))); + } + + @Override + public void forEach(LongConsumer action) { + Objects.requireNonNull(action); + Iterator it = boxed.iterator(); + try { + while (it.hasNext()) { + action.accept(it.next().longValue()); + } + } catch (Throwable e) { + throw ObjPipeline.fail(boxed, e); + } + } + + @Override + public void forEachOrdered(LongConsumer action) { + forEach(action); + } + + @Override + public long[] toArray() { + List all = new ArrayList(); + Iterator it = boxed.iterator(); + try { + while (it.hasNext()) { + all.add(it.next()); + } + long[] out = new long[all.size()]; + for (int i = 0; i < out.length; i++) { + out[i] = all.get(i).longValue(); + } + return out; + } catch (Throwable e) { + throw ObjPipeline.fail(boxed, e); + } + } + + @Override + public long reduce(long identity, LongBinaryOperator op) { + Objects.requireNonNull(op); + long result = identity; + Iterator it = boxed.iterator(); + try { + while (it.hasNext()) { + result = op.applyAsLong(result, it.next().longValue()); + } + return result; + } catch (Throwable e) { + throw ObjPipeline.fail(boxed, e); + } + } + + @Override + public OptionalLong reduce(LongBinaryOperator op) { + Objects.requireNonNull(op); + Iterator it = boxed.iterator(); + try { + if (!it.hasNext()) { + return OptionalLong.empty(); + } + long result = it.next().longValue(); + while (it.hasNext()) { + result = op.applyAsLong(result, it.next().longValue()); + } + return OptionalLong.of(result); + } catch (Throwable e) { + throw ObjPipeline.fail(boxed, e); + } + } + + @Override + public R collect(Supplier supplier, ObjLongConsumer accumulator, BiConsumer combiner) { + Objects.requireNonNull(supplier); + Objects.requireNonNull(accumulator); + Objects.requireNonNull(combiner); + R result = supplier.get(); + Iterator it = boxed.iterator(); + try { + while (it.hasNext()) { + accumulator.accept(result, it.next().longValue()); + } + return result; + } catch (Throwable e) { + throw ObjPipeline.fail(boxed, e); + } + } + + @Override + public long sum() { + long total = 0; + Iterator it = boxed.iterator(); + try { + while (it.hasNext()) { + total += it.next().longValue(); + } + return total; + } catch (Throwable e) { + throw ObjPipeline.fail(boxed, e); + } + } + + /// The smallest (`max` false) or largest value, as `Math.min` and + /// `Math.max` choose, which is what the JDK's reduction uses. + private OptionalLong extreme(boolean max) { + Iterator it = boxed.iterator(); + try { + if (!it.hasNext()) { + return OptionalLong.empty(); + } + long result = it.next().longValue(); + while (it.hasNext()) { + long v = it.next().longValue(); + result = max ? Math.max(result, v) : Math.min(result, v); + } + return OptionalLong.of(result); + } catch (Throwable e) { + throw ObjPipeline.fail(boxed, e); + } + } + + @Override + public OptionalLong min() { + return extreme(false); + } + + @Override + public OptionalLong max() { + return extreme(true); + } + + @Override + public long count() { + return boxed.count(); + } + + @Override + public OptionalDouble average() { + LongSummaryStatistics all = summaryStatistics(); + return all.getCount() == 0 ? OptionalDouble.empty() : OptionalDouble.of(all.getAverage()); + } + + @Override + public LongSummaryStatistics summaryStatistics() { + LongSummaryStatistics all = new LongSummaryStatistics(); + Iterator it = boxed.iterator(); + try { + while (it.hasNext()) { + all.accept(it.next().longValue()); + } + return all; + } catch (Throwable e) { + throw ObjPipeline.fail(boxed, e); + } + } + + @Override + public boolean anyMatch(LongPredicate predicate) { + Objects.requireNonNull(predicate); + return boxed.anyMatch(Fn.predicate(predicate)); + } + + @Override + public boolean allMatch(LongPredicate predicate) { + Objects.requireNonNull(predicate); + return boxed.allMatch(Fn.predicate(predicate)); + } + + @Override + public boolean noneMatch(LongPredicate predicate) { + Objects.requireNonNull(predicate); + return boxed.noneMatch(Fn.predicate(predicate)); + } + + @Override + public OptionalLong findFirst() { + // Through the boxed stream's own, which says that the rest is not + // going to be read: a mapped stream left part read is closed. + Optional first = boxed.findFirst(); + return first.isPresent() ? OptionalLong.of(first.get().longValue()) : OptionalLong.empty(); + } + + @Override + public OptionalLong findAny() { + return findFirst(); + } + + @Override + @SuppressWarnings("unchecked") + public Stream boxed() { + // A stage of its own, so that this stream counts as used like any + // other that had an operation applied to it. + return boxed.map(Fn.function(Fn.BOX, null)); + } + + @Override + public PrimitiveIterator.OfLong iterator() { + return new Unboxing(boxed.iterator()); + } + + /// The elements of the boxed pipeline, one primitive at a time. + private static final class Unboxing implements PrimitiveIterator.OfLong { + private final Iterator it; + + Unboxing(Iterator it) { + this.it = it; + } + + @Override + public boolean hasNext() { + return it.hasNext(); + } + + @Override + public long nextLong() { + return it.next().longValue(); + } + } + + @Override + public boolean isParallel() { + return boxed.isParallel(); + } + + @Override + public LongStream sequential() { + boxed.sequential(); + return this; + } + + @Override + public LongStream parallel() { + boxed.parallel(); + return this; + } + + @Override + public LongStream unordered() { + return this; + } + + @Override + public LongStream onClose(Runnable closeHandler) { + boxed.onClose(closeHandler); + return this; + } + + @Override + public void close() { + boxed.close(); + } +} diff --git a/Ports/CLDC11/src/java/util/stream/LongStream.java b/Ports/CLDC11/src/java/util/stream/LongStream.java new file mode 100644 index 00000000000..6cef52daf12 --- /dev/null +++ b/Ports/CLDC11/src/java/util/stream/LongStream.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.stream; + +import java.util.LongSummaryStatistics; +import java.util.OptionalDouble; +import java.util.OptionalLong; +import java.util.PrimitiveIterator; +import java.util.function.BiConsumer; +import java.util.function.LongBinaryOperator; +import java.util.function.LongConsumer; +import java.util.function.LongFunction; +import java.util.function.LongPredicate; +import java.util.function.LongSupplier; +import java.util.function.LongToDoubleFunction; +import java.util.function.LongToIntFunction; +import java.util.function.LongUnaryOperator; +import java.util.function.ObjLongConsumer; +import java.util.function.Supplier; + +/// A sequence of `long` values to which operations are applied one stage at a +/// time, evaluated lazily, one element at a time, when a terminal operation +/// asks for them. +/// +/// Streams here are sequential: `parallel()` answers the same stream, which +/// the JDK allows a parallel stream to be. +public interface LongStream extends BaseStream { + + /// Declared again with this type, as the JDK does: a call compiled + /// against the JDK names `LongStream.sequential()` answering `LongStream`, + /// and the method inherited from [BaseStream] answers its type + /// variable, which is another descriptor. + @Override + LongStream sequential(); + + /// Declared again with this type; see [#sequential()]. + @Override + LongStream parallel(); + + LongStream filter(LongPredicate predicate); + + LongStream map(LongUnaryOperator mapper); + + Stream mapToObj(LongFunction mapper); + + IntStream mapToInt(LongToIntFunction mapper); + + DoubleStream mapToDouble(LongToDoubleFunction mapper); + + LongStream flatMap(LongFunction mapper); + + LongStream distinct(); + + LongStream sorted(); + + LongStream peek(LongConsumer action); + + LongStream limit(long maxSize); + + LongStream skip(long n); + + LongStream takeWhile(LongPredicate predicate); + + LongStream dropWhile(LongPredicate predicate); + + void forEach(LongConsumer action); + + void forEachOrdered(LongConsumer action); + + long[] toArray(); + + long reduce(long identity, LongBinaryOperator op); + + OptionalLong reduce(LongBinaryOperator op); + + R collect(Supplier supplier, ObjLongConsumer accumulator, BiConsumer combiner); + + long sum(); + + OptionalLong min(); + + OptionalLong max(); + + long count(); + + OptionalDouble average(); + + LongSummaryStatistics summaryStatistics(); + + boolean anyMatch(LongPredicate predicate); + + boolean allMatch(LongPredicate predicate); + + boolean noneMatch(LongPredicate predicate); + + OptionalLong findFirst(); + + OptionalLong findAny(); + + DoubleStream asDoubleStream(); + + Stream boxed(); + + @Override + PrimitiveIterator.OfLong iterator(); + + static LongStream empty() { + return LongPipeline.ofArray(new long[0], 0, 0); + } + + static LongStream of(long value) { + return LongPipeline.ofArray(new long[] {value}, 0, 1); + } + + static LongStream of(long... values) { + return LongPipeline.ofArray(values, 0, values.length); + } + + /// `seed`, then `f` applied to it, then `f` applied to that, without end. + static LongStream iterate(long seed, LongUnaryOperator f) { + return LongPipeline.iterate(seed, null, f); + } + + /// As [#iterate(long, LongUnaryOperator)], ending before the first value + /// `hasNext` refuses. + static LongStream iterate(long seed, LongPredicate hasNext, LongUnaryOperator next) { + if (hasNext == null) { + throw new NullPointerException(); + } + return LongPipeline.iterate(seed, hasNext, next); + } + + /// The values `s` answers, one call per element, without end. + static LongStream generate(LongSupplier s) { + return LongPipeline.generate(s); + } + + /// The values from `startInclusive` up to, not including, `endExclusive`, + /// in steps of one. Empty when `startInclusive` is not below `endExclusive`. + static LongStream range(long startInclusive, long endExclusive) { + return LongPipeline.ofRange(startInclusive, endExclusive, false); + } + + /// The values from `startInclusive` up to and including `endInclusive`, in + /// steps of one. Empty when `startInclusive` is above `endInclusive`. + static LongStream rangeClosed(long startInclusive, long endInclusive) { + return LongPipeline.ofRange(startInclusive, endInclusive, true); + } + + /// The elements of `a`, then those of `b`. Closing the result closes both. + static LongStream concat(LongStream a, LongStream b) { + return new LongPipeline(Stream.concat(a.boxed(), b.boxed())); + } +} diff --git a/Ports/CLDC11/src/java/util/stream/ObjPipeline.java b/Ports/CLDC11/src/java/util/stream/ObjPipeline.java new file mode 100644 index 00000000000..0ccf2d1b428 --- /dev/null +++ b/Ports/CLDC11/src/java/util/stream/ObjPipeline.java @@ -0,0 +1,821 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.stream; + +import java.util.function.IntFunction; +import java.util.Optional; +import java.util.function.ToDoubleFunction; +import java.util.function.ToIntFunction; +import java.util.function.ToLongFunction; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.BinaryOperator; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; + +/// The one implementation of [Stream]: a stage holding the iterator that +/// produces its elements. An intermediate operation wraps that iterator in +/// another and answers a new stage; a terminal one drains it. +/// +/// Nothing runs until a terminal operation pulls, and then one element at a +/// time travels the whole chain before the next is asked for. +final class ObjPipeline implements Stream { + + private static final String USED = "stream has already been operated upon or closed"; + + private final Iterator source; + private final StreamState state; + /// A stage takes one operation, like the JDK's. + private boolean used; + + private ObjPipeline(Iterator source, StreamState state) { + this.source = source; + this.state = state; + } + + static ObjPipeline over(Iterator source) { + return new ObjPipeline(source, new StreamState()); + } + + /// A stream of what `source` holds when the terminal operation starts, + /// not when the stream was made: elements added in between are seen. + static ObjPipeline over(Iterable source) { + return over(new PullIterator() { + private Iterator it; + + @Override + boolean advance() { + if (it == null) { + it = source.iterator(); + } + if (!it.hasNext()) { + return false; + } + emit(it.next()); + return true; + } + }); + } + + /// The elements `from` (inclusive) to `to` (exclusive) of `values`. + static ObjPipeline ofArray(T[] values, int from, int to) { + if (from < 0 || to > values.length || from > to) { + throw new ArrayIndexOutOfBoundsException("origin(" + from + ") > fence(" + to + ")"); + } + return over(new PullIterator() { + private int at = from; + + @Override + boolean advance() { + if (at >= to) { + return false; + } + emit(values[at++]); + return true; + } + }); + } + + static ObjPipeline concat(Stream a, Stream b) { + Iterator first = a.iterator(); + Iterator second = b.iterator(); + ObjPipeline out = over(new PullIterator() { + @Override + boolean advance() { + if (first.hasNext()) { + emit(first.next()); + return true; + } + if (second.hasNext()) { + emit(second.next()); + return true; + } + return false; + } + }); + // Parallel when either input is, as the JDK's concatenation reports. + out.state.parallel = a.isParallel() || b.isParallel(); + // Closing the concatenation closes both inputs, the second even + // when the first one's handler throws. + out.state.onClose(new Runnable() { + @Override + public void run() { + a.close(); + } + }); + out.state.onClose(new Runnable() { + @Override + public void run() { + b.close(); + } + }); + return out; + } + + /// The iterator of this stage, which the caller now owns. + /// Rethrows `failure` from a terminal operation of a primitive stream + /// over `stream`, first closing what its pipeline still holds open -- the + /// inner stream of a `flatMap` -- as this class's own terminals do. + static RuntimeException fail(Stream stream, Throwable failure) { + if (stream instanceof ObjPipeline) { + return ((ObjPipeline) stream).state.fail(failure); + } + return StreamState.unchecked(failure); + } + + private Iterator take() { + // Closing any stage closes the pipeline, so a stage made before an + // earlier one was closed is refused as well as the closed one. + if (used || state.isClosed()) { + throw new IllegalStateException(USED); + } + used = true; + return source; + } + + private ObjPipeline stage(Iterator next) { + return new ObjPipeline(next, state); + } + + @Override + public Stream filter(Predicate predicate) { + Objects.requireNonNull(predicate); + Iterator up = take(); + return stage(new PullIterator() { + @Override + boolean advance() { + while (up.hasNext()) { + T value = up.next(); + if (predicate.test(value)) { + emit(value); + return true; + } + } + return false; + } + }); + } + + @Override + public Stream map(Function mapper) { + Objects.requireNonNull(mapper); + Iterator up = take(); + return stage(new PullIterator() { + @Override + boolean advance() { + if (!up.hasNext()) { + return false; + } + emit(mapper.apply(up.next())); + return true; + } + }); + } + + @Override + public IntStream mapToInt(ToIntFunction mapper) { + Objects.requireNonNull(mapper); + final ToIntFunction f = mapper; + return new IntPipeline(this.map(new Function() { + @Override + public Integer apply(T v) { + return Integer.valueOf(f.applyAsInt(v)); + } + })); + } + + @Override + public LongStream mapToLong(ToLongFunction mapper) { + Objects.requireNonNull(mapper); + final ToLongFunction f = mapper; + return new LongPipeline(this.map(new Function() { + @Override + public Long apply(T v) { + return Long.valueOf(f.applyAsLong(v)); + } + })); + } + + @Override + public DoubleStream mapToDouble(ToDoubleFunction mapper) { + Objects.requireNonNull(mapper); + final ToDoubleFunction f = mapper; + return new DoublePipeline(this.map(new Function() { + @Override + public Double apply(T v) { + return Double.valueOf(f.applyAsDouble(v)); + } + })); + } + + @Override + public Stream flatMap(Function> mapper) { + Objects.requireNonNull(mapper); + FlatMapped flat = new FlatMapped(take(), mapper); + // The mapped stream being read when the pipeline stops asking is + // closed too, as the JDK closes it: see StreamState#onAbandon. + state.onAbandon(flat); + return stage(flat); + } + + /// The elements of each stream a mapper answers, one mapped stream at a + /// time. A mapped stream is read lazily, so the one in hand can be left + /// part read: by a `limit` or a `takeWhile` after this stage, or by a + /// terminal operation that has its answer. Running this closes it. + private static final class FlatMapped extends PullIterator implements Runnable { + private final Iterator up; + private final Function> mapper; + private Stream inner; + private Iterator it; + + FlatMapped(Iterator up, Function> mapper) { + this.up = up; + this.mapper = mapper; + } + + @Override + boolean advance() { + while (true) { + if (it != null) { + if (it.hasNext()) { + emit(it.next()); + return true; + } + // A mapped stream is closed once its elements have + // been taken. + run(); + } + if (!up.hasNext()) { + return false; + } + Stream mapped = mapper.apply(up.next()); + if (mapped != null) { + inner = mapped; + it = mapped.iterator(); + } + } + } + + @Override + public void run() { + Stream open = inner; + inner = null; + it = null; + if (open != null) { + open.close(); + } + } + } + + @Override + public IntStream flatMapToInt(Function mapper) { + Objects.requireNonNull(mapper); + final Function f = mapper; + return new IntPipeline(this.flatMap(new Function>() { + @Override + public Stream apply(T v) { + IntStream mapped = f.apply(v); + return mapped == null ? null : mapped.boxed(); + } + })); + } + + @Override + public LongStream flatMapToLong(Function mapper) { + Objects.requireNonNull(mapper); + final Function f = mapper; + return new LongPipeline(this.flatMap(new Function>() { + @Override + public Stream apply(T v) { + LongStream mapped = f.apply(v); + return mapped == null ? null : mapped.boxed(); + } + })); + } + + @Override + public DoubleStream flatMapToDouble(Function mapper) { + Objects.requireNonNull(mapper); + final Function f = mapper; + return new DoublePipeline(this.flatMap(new Function>() { + @Override + public Stream apply(T v) { + DoubleStream mapped = f.apply(v); + return mapped == null ? null : mapped.boxed(); + } + })); + } + + @Override + public Stream distinct() { + Iterator up = take(); + return stage(new PullIterator() { + private final Set seen = new HashSet(); + + @Override + boolean advance() { + while (up.hasNext()) { + T value = up.next(); + if (seen.add(value)) { + emit(value); + return true; + } + } + return false; + } + }); + } + + @Override + public Stream sorted() { + // The natural order of whatever the elements are; an element that is not + // Comparable fails in the comparison, as on the JDK. + @SuppressWarnings({"unchecked", "rawtypes"}) + Comparator natural = (Comparator) Comparator.naturalOrder(); + return sorted(natural); + } + + @Override + public Stream sorted(Comparator comparator) { + // The JDK refuses a null comparator here; only sorted() means the + // natural order, and it passes that order itself. + if (comparator == null) { + throw new NullPointerException(); + } + final Comparator order = comparator; + Iterator up = take(); + return stage(new PullIterator() { + private Iterator it; + + @Override + boolean advance() { + if (it == null) { + // Sorting needs every element, so the first pull takes + // the whole of what is upstream. + List all = new ArrayList(); + while (up.hasNext()) { + all.add(up.next()); + } + Collections.sort(all, order); + it = all.iterator(); + } + if (!it.hasNext()) { + return false; + } + emit(it.next()); + return true; + } + }); + } + + @Override + public Stream peek(Consumer action) { + Objects.requireNonNull(action); + Iterator up = take(); + return stage(new PullIterator() { + @Override + boolean advance() { + if (!up.hasNext()) { + return false; + } + T value = up.next(); + action.accept(value); + emit(value); + return true; + } + }); + } + + @Override + public Stream limit(long maxSize) { + if (maxSize < 0) { + throw new IllegalArgumentException(Long.toString(maxSize)); + } + Iterator up = take(); + int before = state.abandonable(); + return stage(new PullIterator() { + private long left = maxSize; + + @Override + boolean advance() { + // Checked before asking upstream: the element after the + // last one wanted is never produced. + if (left <= 0) { + // Nothing before this stage is asked again. + state.abandon(before); + return false; + } + if (!up.hasNext()) { + return false; + } + left--; + emit(up.next()); + return true; + } + }); + } + + @Override + public Stream skip(long n) { + if (n < 0) { + throw new IllegalArgumentException(Long.toString(n)); + } + Iterator up = take(); + return stage(new PullIterator() { + private long left = n; + + @Override + boolean advance() { + while (left > 0) { + if (!up.hasNext()) { + return false; + } + up.next(); + left--; + } + if (!up.hasNext()) { + return false; + } + emit(up.next()); + return true; + } + }); + } + + @Override + public Stream takeWhile(Predicate predicate) { + Objects.requireNonNull(predicate); + Iterator up = take(); + int before = state.abandonable(); + return stage(new PullIterator() { + @Override + boolean advance() { + if (!up.hasNext()) { + return false; + } + T value = up.next(); + if (!predicate.test(value)) { + state.abandon(before); + return false; + } + emit(value); + return true; + } + }); + } + + @Override + public Stream dropWhile(Predicate predicate) { + Objects.requireNonNull(predicate); + Iterator up = take(); + return stage(new PullIterator() { + private boolean dropping = true; + + @Override + boolean advance() { + while (up.hasNext()) { + T value = up.next(); + if (dropping && predicate.test(value)) { + continue; + } + dropping = false; + emit(value); + return true; + } + return false; + } + }); + } + + @Override + public void forEach(Consumer action) { + Objects.requireNonNull(action); + Iterator it = take(); + try { + while (it.hasNext()) { + action.accept(it.next()); + } + } catch (Throwable e) { + throw state.fail(e); + } + } + + @Override + public void forEachOrdered(Consumer action) { + forEach(action); + } + + private List drain() { + List all = new ArrayList(); + Iterator it = take(); + try { + while (it.hasNext()) { + all.add(it.next()); + } + return all; + } catch (Throwable e) { + throw state.fail(e); + } + } + + @Override + public Object[] toArray() { + return drain().toArray(); + } + + @Override + public A[] toArray(IntFunction generator) { + Objects.requireNonNull(generator); + List all = drain(); + A[] out = generator.apply(all.size()); + if (out.length != all.size()) { + throw new IllegalStateException("The generator answered an array of " + out.length + " for " + + all.size() + " elements"); + } + // Copied through an Object[] view: storing an element of the wrong + // type fails as an ArrayStoreException, like the JDK's. + Object[] target = out; + for (int i = 0; i < target.length; i++) { + target[i] = all.get(i); + } + return out; + } + + @Override + public T reduce(T identity, BinaryOperator accumulator) { + Objects.requireNonNull(accumulator); + T result = identity; + Iterator it = take(); + try { + while (it.hasNext()) { + result = accumulator.apply(result, it.next()); + } + return result; + } catch (Throwable e) { + throw state.fail(e); + } + } + + @Override + public Optional reduce(BinaryOperator accumulator) { + Objects.requireNonNull(accumulator); + Iterator it = take(); + try { + if (!it.hasNext()) { + return Optional.empty(); + } + T result = it.next(); + while (it.hasNext()) { + result = accumulator.apply(result, it.next()); + } + return Optional.of(result); + } catch (Throwable e) { + throw state.fail(e); + } + } + + @Override + public U reduce(U identity, BiFunction accumulator, BinaryOperator combiner) { + Objects.requireNonNull(accumulator); + Objects.requireNonNull(combiner); + U result = identity; + Iterator it = take(); + try { + while (it.hasNext()) { + result = accumulator.apply(result, it.next()); + } + return result; + } catch (Throwable e) { + throw state.fail(e); + } + } + + @Override + public R collect(Supplier supplier, BiConsumer accumulator, BiConsumer combiner) { + Objects.requireNonNull(supplier); + Objects.requireNonNull(accumulator); + Objects.requireNonNull(combiner); + R result = supplier.get(); + Iterator it = take(); + try { + while (it.hasNext()) { + accumulator.accept(result, it.next()); + } + return result; + } catch (Throwable e) { + throw state.fail(e); + } + } + + @Override + public R collect(Collector collector) { + Objects.requireNonNull(collector); + A container = collector.supplier().get(); + BiConsumer accumulator = collector.accumulator(); + Iterator it = take(); + try { + while (it.hasNext()) { + accumulator.accept(container, it.next()); + } + return collector.finisher().apply(container); + } catch (Throwable e) { + throw state.fail(e); + } + } + + @Override + public Optional min(Comparator comparator) { + Objects.requireNonNull(comparator); + return reduce(BinaryOperator.minBy(comparator)); + } + + @Override + public Optional max(Comparator comparator) { + Objects.requireNonNull(comparator); + return reduce(BinaryOperator.maxBy(comparator)); + } + + @Override + public long count() { + long n = 0; + Iterator it = take(); + try { + while (it.hasNext()) { + it.next(); + n++; + } + return n; + } catch (Throwable e) { + throw state.fail(e); + } + } + + @Override + public boolean anyMatch(Predicate predicate) { + Objects.requireNonNull(predicate); + Iterator it = take(); + try { + while (it.hasNext()) { + if (predicate.test(it.next())) { + state.abandon(state.abandonable()); + return true; + } + } + return false; + } catch (Throwable e) { + throw state.fail(e); + } + } + + @Override + public boolean allMatch(Predicate predicate) { + Objects.requireNonNull(predicate); + Iterator it = take(); + try { + while (it.hasNext()) { + if (!predicate.test(it.next())) { + state.abandon(state.abandonable()); + return false; + } + } + return true; + } catch (Throwable e) { + throw state.fail(e); + } + } + + @Override + public boolean noneMatch(Predicate predicate) { + Objects.requireNonNull(predicate); + Iterator it = take(); + try { + while (it.hasNext()) { + if (predicate.test(it.next())) { + state.abandon(state.abandonable()); + return false; + } + } + return true; + } catch (Throwable e) { + throw state.fail(e); + } + } + + @Override + public Optional findFirst() { + Iterator it = take(); + try { + if (!it.hasNext()) { + return Optional.empty(); + } + Optional first = Optional.of(it.next()); + // The answer is in hand with the rest unread. + state.abandon(state.abandonable()); + return first; + } catch (Throwable e) { + throw state.fail(e); + } + } + + @Override + public Optional findAny() { + return findFirst(); + } + + @Override + public Iterator iterator() { + return take(); + } + + @Override + public boolean isParallel() { + return state.parallel; + } + + @Override + public Stream sequential() { + state.parallel = false; + return this; + } + + @Override + public Stream parallel() { + state.parallel = true; + return this; + } + + @Override + public Stream unordered() { + return this; + } + + @Override + public Stream onClose(Runnable closeHandler) { + if (used || state.isClosed()) { + throw new IllegalStateException(USED); + } + state.onClose(closeHandler); + return this; + } + + @Override + public void close() { + used = true; + state.close(); + } + + /// What [Stream#builder] answers. + static final class ListBuilder implements Stream.Builder { + + private List elements = new ArrayList(); + + @Override + public void accept(T t) { + if (elements == null) { + throw new IllegalStateException(); + } + elements.add(t); + } + + @Override + public Stream build() { + if (elements == null) { + throw new IllegalStateException(); + } + List built = elements; + elements = null; + return over(built); + } + } +} diff --git a/Ports/CLDC11/src/java/util/stream/PullIterator.java b/Ports/CLDC11/src/java/util/stream/PullIterator.java new file mode 100644 index 00000000000..2a8eed18320 --- /dev/null +++ b/Ports/CLDC11/src/java/util/stream/PullIterator.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.stream; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +/// The iterator every stream stage is: it produces its next element only +/// when asked whether there is one, which is what makes a pipeline lazy -- +/// `limit` stops asking the stage before it, and nothing further upstream +/// runs. +abstract class PullIterator implements Iterator { + + private boolean ready; + private boolean done; + private T value; + + /// Produces the next element by calling [#emit], or answers false when + /// there is none. Not called again once it answered false. + abstract boolean advance(); + + final void emit(T next) { + value = next; + } + + @Override + public final boolean hasNext() { + if (!ready && !done) { + if (advance()) { + ready = true; + } else { + done = true; + } + } + return ready; + } + + @Override + public final T next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + ready = false; + T out = value; + value = null; + return out; + } +} diff --git a/Ports/CLDC11/src/java/util/stream/Stream.java b/Ports/CLDC11/src/java/util/stream/Stream.java index 6d907368a8b..f6815ce98e9 100644 --- a/Ports/CLDC11/src/java/util/stream/Stream.java +++ b/Ports/CLDC11/src/java/util/stream/Stream.java @@ -22,31 +22,97 @@ */ package java.util.stream; +import java.util.function.IntFunction; +import java.util.Optional; +import java.util.function.ToDoubleFunction; +import java.util.function.ToIntFunction; +import java.util.function.ToLongFunction; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; import java.util.function.BinaryOperator; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; - +import java.util.function.Supplier; +import java.util.function.UnaryOperator; + +/// A stream here is sequential and lazy: each stage pulls one element at a +/// time from the one before it, and only when a terminal operation asks. So +/// an infinite `iterate` or `generate` behind a `limit` ends, `findFirst` +/// and the `anyMatch` family stop at the element that decides them, and the +/// side effects of `peek` happen element by element, in encounter order. +/// +/// What is not here: spliterators, and with them parallel evaluation. +/// `parallel()` answers the stream it was called on. public interface Stream extends BaseStream> { + Stream filter(Predicate predicate); Stream map(Function mapper); - Stream sorted(); + IntStream mapToInt(ToIntFunction mapper); + + LongStream mapToLong(ToLongFunction mapper); + + DoubleStream mapToDouble(ToDoubleFunction mapper); + + Stream flatMap(Function> mapper); + + IntStream flatMapToInt(Function mapper); + + LongStream flatMapToLong(Function mapper); + + DoubleStream flatMapToDouble(Function mapper); Stream distinct(); + Stream sorted(); + + Stream sorted(Comparator comparator); + + Stream peek(Consumer action); + Stream limit(long maxSize); Stream skip(long n); + Stream takeWhile(Predicate predicate); + + Stream dropWhile(Predicate predicate); + void forEach(Consumer action); + void forEachOrdered(Consumer action); + Object[] toArray(); + A[] toArray(IntFunction generator); + T reduce(T identity, BinaryOperator accumulator); - R collect(Collector collector); + Optional reduce(BinaryOperator accumulator); + + U reduce(U identity, BiFunction accumulator, BinaryOperator combiner); + + R collect(Supplier supplier, BiConsumer accumulator, BiConsumer combiner); + + R collect(Collector collector); + + /// An unmodifiable list of the elements, which may be null. + @SuppressWarnings("unchecked") + default List toList() { + return (List) Collections.unmodifiableList(new ArrayList(Arrays.asList(toArray()))); + } + + Optional min(Comparator comparator); + + Optional max(Comparator comparator); long count(); @@ -56,15 +122,103 @@ public interface Stream extends BaseStream> { boolean noneMatch(Predicate predicate); + Optional findFirst(); + + Optional findAny(); + + static Builder builder() { + return new ObjPipeline.ListBuilder(); + } + static Stream empty() { - return null; + return ObjPipeline.over(Collections.emptyList()); } - static Stream of(T value) { - return null; + static Stream of(T t) { + return ObjPipeline.over(Collections.singletonList(t)); } + static Stream ofNullable(T t) { + return t == null ? Stream.empty() : Stream.of(t); + } + + @SafeVarargs + @SuppressWarnings("varargs") static Stream of(T... values) { - return null; + return ObjPipeline.ofArray(values, 0, values.length); + } + + static Stream iterate(T seed, UnaryOperator f) { + if (f == null) { + throw new NullPointerException(); + } + return ObjPipeline.over(new PullIterator() { + private boolean started; + private T current; + + @Override + boolean advance() { + current = started ? f.apply(current) : seed; + started = true; + emit(current); + return true; + } + }); + } + + static Stream iterate(T seed, Predicate hasNext, UnaryOperator next) { + if (hasNext == null || next == null) { + throw new NullPointerException(); + } + return ObjPipeline.over(new PullIterator() { + private boolean started; + private T current; + + @Override + boolean advance() { + current = started ? next.apply(current) : seed; + started = true; + if (!hasNext.test(current)) { + return false; + } + emit(current); + return true; + } + }); + } + + static Stream generate(Supplier s) { + if (s == null) { + throw new NullPointerException(); + } + return ObjPipeline.over(new PullIterator() { + @Override + boolean advance() { + emit(s.get()); + return true; + } + }); + } + + static Stream concat(Stream a, Stream b) { + if (a == null || b == null) { + throw new NullPointerException(); + } + return ObjPipeline.concat(a, b); + } + + /// `java.util.stream.Stream.Builder`: collects elements, then answers + /// them as a stream, once. + interface Builder extends Consumer { + + @Override + void accept(T t); + + default Builder add(T t) { + accept(t); + return this; + } + + Stream build(); } } diff --git a/Ports/CLDC11/src/java/util/stream/StreamState.java b/Ports/CLDC11/src/java/util/stream/StreamState.java new file mode 100644 index 00000000000..1be432ad226 --- /dev/null +++ b/Ports/CLDC11/src/java/util/stream/StreamState.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.stream; + +import java.util.ArrayList; +import java.util.List; + +/// What the stages of one pipeline share: the handlers `onClose` registered, +/// which `close()` on any stage runs, once. +final class StreamState { + + private List handlers; + + /// What `parallel()` and `sequential()` last asked for, which + /// `isParallel()` reports. The pipeline runs sequentially either way, + /// which the JDK allows a parallel stream to do. + boolean parallel; + private boolean closed; + + /// What closes a resource a stage holds while it is part read: the + /// mapped stream a `flatMap` is in the middle of. Kept in the order the + /// stages were added, which is the order the elements travel. + /// + /// The JDK closes a mapped stream whether or not all of it was wanted + /// (`flatMap(...).findFirst()` closes the one it took its answer from). + /// A pulled pipeline has to be told that nobody will ask again, and + /// three things know: a `limit` that has handed out its last element + /// and a `takeWhile` that met its first refusal, about the stages + /// before them, and a short-circuiting terminal operation or `close`, + /// about every stage. + /// + /// A terminal operation that ends by throwing releases them all too + /// ([#fail(Throwable)]). Not covered, and left to `close` as for any + /// stream holding a resource: an `iterator()` its caller stops reading. + private List abandoned; + + void onAbandon(Runnable release) { + if (abandoned == null) { + abandoned = new ArrayList(); + } + abandoned.add(release); + } + + /// How many stages so far hold something to release: what a stage keeps + /// to say later that everything before it is no longer read. + int abandonable() { + return abandoned == null ? 0 : abandoned.size(); + } + + /// Releases what the first `count` such stages hold, the latest stage + /// first as the JDK's nested `flatMap`s close, even when one throws; the + /// first exception is thrown once all have run. + void abandon(int count) { + List held = abandoned; + if (held == null) { + return; + } + RuntimeException first = null; + for (int i = Math.min(count, held.size()) - 1; i >= 0; i--) { + Runnable release = held.get(i); + try { + release.run(); + } catch (RuntimeException e) { + if (first == null) { + first = e; + } + } + } + if (first != null) { + throw first; + } + } + + void onClose(Runnable handler) { + if (handler == null) { + throw new NullPointerException(); + } + if (handlers == null) { + handlers = new ArrayList(); + } + handlers.add(handler); + } + + /// Whether some stage of the pipeline was closed. The stages share this + /// state, so each of them can tell. + boolean isClosed() { + return closed; + } + + /// Runs every handler, in the order they were added, even when one + /// throws, an Error included; the first failure is thrown once all have + /// run, with each later one added to it as suppressed, as the JDK's + /// stream does. + void close() { + if (closed) { + return; + } + closed = true; + Throwable first = null; + try { + abandon(abandonable()); + } catch (Throwable e) { + first = e; + } + if (handlers != null) { + for (int i = 0; i < handlers.size(); i++) { + // Read outside the try: the cast a generic get compiles to must + // not sit under a handler that would swallow its failure. + Runnable handler = handlers.get(i); + try { + handler.run(); + } catch (Throwable e) { + first = suppress(first, e); + } + } + } + if (first != null) { + // Thrown as it is, a sneakily thrown checked exception included, + // which the JDK does not wrap either. + StreamState.rethrow(first); + } + } + + /// What a terminal operation that ended by throwing `failure` calls: + /// every mapped stream still open is closed, the latest first, and what + /// closing one throws rides along as suppressed, as the JDK's + /// try-with-resources around each mapped stream does. Always throws + /// `failure`; declared to answer an exception so a caller can write + /// `throw state.fail(e)`. + RuntimeException fail(Throwable failure) { + List held = abandoned; + if (held != null) { + for (int i = held.size() - 1; i >= 0; i--) { + Runnable release = held.get(i); + try { + release.run(); + } catch (Throwable e) { + suppress(failure, e); + } + } + } + StreamState.rethrow(failure); + return null; + } + + /// Throws `t` unchanged, checked or not: the erased cast is to + /// `Throwable`, so nothing is checked at run time. + @SuppressWarnings("unchecked") + /// Throws `failure` as it is, checked or not. + static RuntimeException unchecked(Throwable failure) { + StreamState.rethrow(failure); + return null; + } + + private static void rethrow(Throwable t) throws E { + throw (E) t; + } + + private static Throwable suppress(Throwable first, Throwable next) { + if (first == null) { + return next; + } + if (first != next) { + first.addSuppressed(next); + } + return first; + } +} diff --git a/Ports/CLDC11/src/java/util/stream/StreamSupport.java b/Ports/CLDC11/src/java/util/stream/StreamSupport.java new file mode 100644 index 00000000000..3f1cfbd854d --- /dev/null +++ b/Ports/CLDC11/src/java/util/stream/StreamSupport.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.stream; + +import java.util.Spliterator; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/// Makes a [Stream] of a [Spliterator]: what `Collection.stream()` does, and +/// how a library turns any `Iterable` into a stream +/// (`StreamSupport.stream(iterable.spliterator(), false)`). +/// +/// Streams here run sequentially; `parallel` is what `isParallel()` +/// reports and changes nothing else, which the JDK allows. +public final class StreamSupport { + + private StreamSupport() { + } + + /// The elements of `spliterator`, read when a terminal operation asks + /// for them. + public static Stream stream(Spliterator spliterator, boolean parallel) { + if (spliterator == null) { + throw new NullPointerException(); + } + Stream out = ObjPipeline.over(new Source(spliterator, null)); + return parallel ? out.parallel() : out; + } + + /// The elements of the spliterator `supplier` answers when a terminal + /// operation first asks for one. + public static Stream stream(Supplier> supplier, int characteristics, + boolean parallel) { + if (supplier == null) { + throw new NullPointerException(); + } + Stream out = ObjPipeline.over(new Source(null, supplier)); + return parallel ? out.parallel() : out; + } + + /// The first stage of such a stream: one `tryAdvance` per element. + private static final class Source extends PullIterator implements Consumer { + private Spliterator spliterator; + private final Supplier> supplier; + + Source(Spliterator spliterator, Supplier> supplier) { + this.spliterator = spliterator; + this.supplier = supplier; + } + + @Override + public void accept(T t) { + emit(t); + } + + @Override + boolean advance() { + if (spliterator == null) { + spliterator = supplier.get(); + } + return spliterator.tryAdvance(this); + } + } +} diff --git a/Ports/JavaSE/src/com/codename1/impl/JdkHost.java b/Ports/JavaSE/src/com/codename1/impl/JdkHost.java new file mode 100644 index 00000000000..ae07bd506c4 --- /dev/null +++ b/Ports/JavaSE/src/com/codename1/impl/JdkHost.java @@ -0,0 +1,480 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.Map; + +/// The services the Java class library asks of the platform it runs on. +/// +/// Internal API. The `java.*` classes of the ParparVM runtime (`vm/JavaAPI`) +/// need a file system, preferences, the network, a log and an event thread, +/// and on a device those belong to Codename One, which the class library +/// cannot name: it is translated alone for the translator itself, the backend +/// and the VM tests. So the class library calls this class, and whoever runs +/// it installs the implementation that fits: +/// +/// - a Codename One application gets `com.codename1.impl.CoreJdkHost`, which +/// delegates to `FileSystemStorage`, `Preferences`, `NetworkManager`, `Log` +/// and `Display`. The ParparVM ports install it from their `init`. +/// - a program translated without Codename One (the self-hosted translator, +/// the backend, a VM test) gets `PosixJdkHost`, which reaches the POSIX file +/// natives through `java.io.File`. +/// - anything else gets the defaults below, which are plain Java and touch no +/// native method, so an application target that never installs a host +/// cannot link the file natives through this class. +/// +/// On Java SE and Android the real JDK runs and nothing calls this class. +/// Copies of it ship in those ports only so that `CoreJdkHost`, which is part +/// of the core sources, always has its superclass. +/// +/// The file is kept byte for byte identical in `vm/JavaAPI`, `Ports/CLDC11`, +/// `Ports/JavaSE` and `Ports/Android`; `CldcJavaApiParityTest` holds them +/// to that. It may use only API all four class libraries have. +/// +/// Paths are in the host's own form: absolute, as [#appHome] answers them. +/// Turning a relative path into one is the caller's job. +public abstract class JdkHost { + /// [#log] level of a debug message. + public static final int LOG_DEBUG = 1; + /// [#log] level of an informational message. + public static final int LOG_INFO = 2; + /// [#log] level of a warning. + public static final int LOG_WARNING = 3; + /// [#log] level of an error. + public static final int LOG_ERROR = 4; + + private static final JdkHost DEFAULTS = new Defaults(); + private static JdkHost current; + private static Registry registry; + private static Map memoryPreferences; + + /// Subclasses are installed with [#install]. + protected JdkHost() { + } + + /// The installed host, or the plain Java defaults when none was installed. + public static JdkHost get() { + JdkHost h = current; + return h == null ? DEFAULTS : h; + } + + /// Makes `host` the one [#get] answers; null restores the defaults. + /// Called once, while the program starts and before it starts a thread + /// of its own. + public static void install(JdkHost host) { + current = host; + } + + /// Makes `r` the registry [#providers] and [#newInstance] read. An + /// application's generated start-up code installs it. + public static void setRegistry(Registry r) { + registry = r; + } + + /// The registry [#setRegistry] installed, or null. + public static Registry getRegistry() { + return registry; + } + + // ------------------------------------------------------------------ + // Files + + /// What is known of `path`, or null when nothing is there. + public FileStat fileStat(String path) { + return null; + } + + /// The names of the entries of the directory `path`, or null when it is + /// not a directory. + public String[] fileList(String path) throws IOException { + return null; + } + + /// Opens the file `path` for reading. + public InputStream fileRead(String path) throws IOException { + throw new IOException("No file system: " + path); + } + + /// Opens the file `path` for writing, at its end when `append` is true + /// and from empty otherwise, creating it when it is not there. + public OutputStream fileWrite(String path, boolean append) throws IOException { + throw new IOException("No file system: " + path); + } + + /// Creates the directory `path`; false when it was not created, also + /// when something was there already. + public boolean fileMkdir(String path) { + return false; + } + + /// Deletes the file or empty directory `path`; false when it is still + /// there or never was. + public boolean fileDelete(String path) { + return false; + } + + /// Moves `from` to `to`, which must not exist. False when it did not + /// happen; a host may refuse a move to another directory, and the + /// caller then copies. + public boolean fileRename(String from, String to) { + return false; + } + + /// The application's home directory, where a relative path resolves. + public String appHome() { + return "/"; + } + + /// A directory for disposable files, or null when the platform has no + /// separate one and [#appHome] serves. + public String cachesDir() { + return null; + } + + /// The roots of the file system. + public String[] fileRoots() { + return new String[] {appHome()}; + } + + /// The size in bytes of the volume holding `path`, or 0 when unknown. + public long fileTotalSpace(String path) { + return 0L; + } + + /// The bytes still free for this program on the volume holding `path`, + /// or 0 when unknown. + public long fileUsableSpace(String path) { + return 0L; + } + + /// The bytes free on the volume holding `path`, counting what only a + /// privileged user may use; [#fileUsableSpace] unless the host knows + /// better. + public long fileFreeSpace(String path) { + return fileUsableSpace(path); + } + + /// [#fileTest]: something is at the path. + public static final int FILE_EXISTS = 1; + /// [#fileTest]: the path is a directory. + public static final int FILE_DIRECTORY = 2; + /// [#fileTest]: the path is a file that is not a directory. + public static final int FILE_REGULAR = 3; + /// [#fileTest] and [#fileSet]: the path may be read. + public static final int FILE_READABLE = 4; + /// [#fileTest] and [#fileSet]: the path may be written. + public static final int FILE_WRITABLE = 5; + /// [#fileTest] and [#fileSet]: the path may be executed, or for a + /// directory, entered. + public static final int FILE_EXECUTABLE = 6; + /// [#fileTest]: the path is hidden. + public static final int FILE_HIDDEN = 7; + + /// Whether `path` has the property `what`, one of the `FILE_` constants. + /// + /// The defaults read [#fileStat]: a file system without permissions lets + /// everything that exists be read and written, a directory be entered and + /// nothing be run, and hides a name that starts with a dot. + public boolean fileTest(String path, int what) { + if (what == FILE_HIDDEN) { + int slash = path.lastIndexOf('/'); + return path.startsWith(".", slash + 1); + } + FileStat st = fileStat(path); + if (st == null) { + return false; + } + if (what == FILE_DIRECTORY || what == FILE_EXECUTABLE) { + return st.isDirectory(); + } + if (what == FILE_REGULAR) { + return !st.isDirectory(); + } + return true; + } + + /// Grants (`on`) or takes away the permission `what` on `path`, one of + /// [#FILE_READABLE], [#FILE_WRITABLE] and [#FILE_EXECUTABLE]; false when + /// the path is not left that way. + /// + /// The defaults change nothing, so they answer whether the path already is + /// as asked, by the rules of [#fileTest]. + public boolean fileSet(String path, int what, boolean on) { + FileStat st = fileStat(path); + if (st == null) { + return false; + } + if (what == FILE_EXECUTABLE) { + return on == st.isDirectory(); + } + return on; + } + + /// The length in bytes of the file `path`; 0 for a directory or when + /// nothing is there. + public long fileLength(String path) { + FileStat st = fileStat(path); + return st == null || st.isDirectory() ? 0L : st.length(); + } + + /// When `path` was last modified, in milliseconds since the epoch; 0 when + /// nothing is there. + public long fileLastModified(String path) { + FileStat st = fileStat(path); + return st == null ? 0L : st.lastModified(); + } + + /// Creates the empty file `path` when nothing is there; false when + /// something was. + /// + /// The default checks and then creates, which is not atomic: two threads + /// racing on one path can both be answered true. + public boolean fileCreate(String path) throws IOException { + if (fileStat(path) != null) { + return false; + } + fileWrite(path, false).close(); + return true; + } + + /// Moves `from` over `to`, replacing the file there. False when it did not + /// happen, which is all the defaults answer; the caller then copies. + public boolean fileReplace(String from, String to) { + return false; + } + + /// The absolute `path` with symbolic links, `.` and `..` resolved, or + /// null when the host cannot resolve links and the caller should resolve + /// the dots by itself. + public String fileCanonicalPath(String path) throws IOException { + return null; + } + + // ------------------------------------------------------------------ + // Preferences + + /// The stored preference `key`, or null. The defaults keep preferences + /// in memory, for this run only. + public String preference(String key) { + return memoryPreference(key, null, false); + } + + /// Stores `value` under `key`. + public void setPreference(String key, String value) { + memoryPreference(key, value, true); + } + + /// Removes the preference `key`. + public void deletePreference(String key) { + memoryPreference(key, null, true); + } + + /// Reads `key` from the in-memory preferences, or, when `write` is true, + /// stores `value` there (null removes it). One lock: the class library + /// runs on several threads where a host leaves this to the defaults. + private static synchronized String memoryPreference(String key, String value, boolean write) { + if (memoryPreferences == null) { + memoryPreferences = new HashMap(); + } + if (!write) { + return memoryPreferences.get(key); + } + if (value == null) { + return memoryPreferences.remove(key); + } + return memoryPreferences.put(key, value); + } + + // ------------------------------------------------------------------ + // Network, log and process + + /// Reads `url` with an HTTP GET. A missing resource (404, 410) throws + /// `java.io.FileNotFoundException`, any other failure an `IOException`. + public InputStream openUrl(String url) throws IOException { + throw new IOException("No network: " + url); + } + + /// Logs `message` and `thrown`, either of which may be null, at `level` + /// ([#LOG_DEBUG] to [#LOG_ERROR]). + public void log(int level, String message, Throwable thrown) { + if (message != null) { + System.err.println(message); + } + if (thrown != null) { + thrown.printStackTrace(); + } + } + + /// The platform's answer for the system property `key`, or null. The + /// class library answers the properties it defines itself. + public String property(String key) { + return null; + } + + /// Asks the platform to close the application, after the shutdown hooks + /// ran; `Runtime.exit` ends the process itself if this returns. The + /// defaults leave it to that. + public void exit(int status) { + } + + /// Called when the program installs a default uncaught exception + /// handler, so that a host whose UI thread catches what its tasks throw + /// can hand that to `Thread.getDefaultUncaughtExceptionHandler()` too. + /// Called on every such install; the defaults do nothing. + public void watchUncaughtExceptions() { + } + + // ------------------------------------------------------------------ + // Threads + + /// Whether the caller is the UI thread, which must not block. + public boolean isUiThread() { + return false; + } + + /// Runs `task`, which blocks, where blocking is allowed: off the UI + /// thread while that thread keeps serving events, or right here. + public void blockUi(Runnable task) { + task.run(); + } + + // ------------------------------------------------------------------ + // Resources, services and locale + + /// Opens the application resource `name` (a path such as + /// `META-INF/services/x`, without a leading slash), or null. + public InputStream resource(String name) { + return null; + } + + /// The class names of the providers of `service` the application + /// declares, from the [Registry]; empty when there is none. + public String[] providers(String service) { + Registry r = registry; + String[] names = r == null ? null : r.providers(service); + return names == null ? new String[0] : names; + } + + /// A new instance of the application class `className`, made by the + /// [Registry] without reflection; null when it cannot make one. + public Object newInstance(String className) { + Registry r = registry; + return r == null ? null : r.newInstance(className); + } + + /// The class of the application class `className`, as the [Registry] + /// knows it, so that its type can be tested without creating one; null + /// when it does not know the class. + public Class type(String className) { + Registry r = registry; + return r == null ? null : r.type(className); + } + + /// The decimal separator, grouping separator and currency symbol of the + /// device's locale, any of them null when unknown; or null when the + /// platform cannot tell. + public String[] localeNumberSymbols() { + return null; + } + + // ------------------------------------------------------------------ + // Security + + /// Fills `out` with cryptographically secure random bytes. + public void randomBytes(byte[] out) { + throw new UnsupportedOperationException("No secure random source"); + } + + /// A new digest for `algorithm` (`MD5`, `SHA-1`, `SHA-256` and the like), + /// or null when the platform has none. + public Digest digest(String algorithm) { + return null; + } + + /// The attributes [#fileStat] reports. + public static final class FileStat { + private final boolean directory; + private final long length; + private final long lastModified; + + /// A file or, when `directory` is true, a directory. + public FileStat(boolean directory, long length, long lastModified) { + this.directory = directory; + this.length = length; + this.lastModified = lastModified; + } + + /// Whether it is a directory. + public boolean isDirectory() { + return directory; + } + + /// The length in bytes of a file; 0 for a directory. + public long length() { + return length; + } + + /// When it was last modified, in milliseconds since the epoch, or 0. + public long lastModified() { + return lastModified; + } + } + + /// A running message digest, as [#digest] makes it. + public interface Digest { + /// Feeds `length` bytes of `data` from `offset`. + void update(byte[] data, int offset, int length); + + /// Finishes the digest and resets it. + byte[] digest(); + + /// Discards what was fed. + void reset(); + + /// The length of the digest in bytes. + int length(); + } + + /// What an application declares for `java.util.ServiceLoader` and + /// `java.util.ResourceBundle`, generated with the application. Once one + /// is installed it is the whole answer: the class library creates no + /// class from its name and reads no services file. + public interface Registry { + /// The provider class names declared for `service`, or null. + String[] providers(String service); + + /// A new instance of `className`, or null. + Object newInstance(String className); + + /// The class `className`, or null when the registry does not know it. + Class type(String className); + } + + /// The host [#get] answers when none was installed. + private static final class Defaults extends JdkHost { + } +} diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/CompatResourceSupport.java b/Ports/JavaSE/src/com/codename1/impl/javase/CompatResourceSupport.java new file mode 100644 index 00000000000..3751795e5e8 --- /dev/null +++ b/Ports/JavaSE/src/com/codename1/impl/javase/CompatResourceSupport.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.javase; + +import com.codename1.impl.compat.Resources; + +import java.io.ByteArrayInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.util.Collections; +import java.util.Enumeration; + +/// What an application built from desktop sources needs of the JDK this port +/// runs on to find the resources its build flattened +/// ([com.codename1.impl.compat.Resources]). +/// +/// - The JDK's `URL` does not know the `cn1res:` scheme, so the URLs +/// `Resources` answers are made here with a handler that reads them back +/// through it. +/// - The JDK's `ResourceBundle` and `ServiceLoader` look a nested name up +/// through a class loader, so the loader installed here asks the +/// application's own loader first and `Resources` after it. +/// +/// The Android port carries the same class for the same reasons. +final class CompatResourceSupport { + private static final String SCHEME = "cn1res"; + + private CompatResourceSupport() { + } + + /// Installs both into `Resources`. Idempotent. + static void install() { + Resources.cn1SetUrlFactory(new Factory()); + if (Resources.cn1ClassLoader() == null) { + ClassLoader parent = Resources.class.getClassLoader(); + Resources.cn1SetClassLoader(new FlatResourceLoader(parent)); + } + } + + private static URL create(String encodedPath) throws MalformedURLException { + return new URL(SCHEME, null, -1, encodedPath, Handler.INSTANCE); + } + + private static final class Factory implements Resources.UrlFactory { + @Override + public URL create(String encodedPath) throws IOException { + return CompatResourceSupport.create(encodedPath); + } + } + + private static final class Handler extends URLStreamHandler { + static final Handler INSTANCE = new Handler(); + + @Override + protected URLConnection openConnection(URL u) { + return new Connection(u); + } + } + + private static final class Connection extends URLConnection { + Connection(URL url) { + super(url); + } + + @Override + public void connect() { + connected = true; + } + + @Override + public InputStream getInputStream() throws IOException { + InputStream in = Resources.openUrlPath(getURL().getPath()); + if (in == null) { + throw new FileNotFoundException(getURL().toExternalForm()); + } + return in; + } + } + + /// The application's loader, then the resources the build flattened. + private static final class FlatResourceLoader extends ClassLoader { + FlatResourceLoader(ClassLoader parent) { + super(parent); + } + + @Override + protected URL findResource(String name) { + if (name == null || name.startsWith("/") || !Resources.exists(name)) { + return null; + } + return Resources.getSystemResource(name); + } + + @Override + protected Enumeration findResources(String name) { + URL url = findResource(name); + if (url == null) { + return Collections.emptyEnumeration(); + } + return Collections.enumeration(Collections.singletonList(url)); + } + + /// A services file is the generated registry's list once one is + /// installed, and nothing else: the files the build flattened would + /// shadow one another, and would name providers that do not ship. + @Override + public Enumeration getResources(String name) throws IOException { + if (name != null && name.startsWith(Resources.SERVICES)) { + String file = Resources.cn1ServicesFile(name.substring(Resources.SERVICES.length())); + if (file != null) { + URL url = new URL(SCHEME, null, -1, "/" + name, new TextHandler(file)); + return Collections.enumeration(Collections.singletonList(url)); + } + } + return super.getResources(name); + } + } + + /// Serves one text, as UTF-8, whatever the URL. + private static final class TextHandler extends URLStreamHandler { + private final String text; + + TextHandler(String text) { + this.text = text; + } + + @Override + protected URLConnection openConnection(URL u) { + return new URLConnection(u) { + @Override + public void connect() { + connected = true; + } + + @Override + public InputStream getInputStream() throws IOException { + return new ByteArrayInputStream(text.getBytes("UTF-8")); + } + }; + } + } +} diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java index 3f5bfb49eca..20054fa8453 100644 --- a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java +++ b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java @@ -11306,6 +11306,9 @@ private static void installGeneratedSvgRegistry() { public void init(Object m) { inInit = true; installGeneratedSvgRegistry(); + // cn1res: URLs and flattened resources for applications built from + // desktop sources, which this JDK cannot resolve on its own. + CompatResourceSupport.install(); // Make the desktop stdio and loopback socket MCP transports available to // com.codename1.mcp.MCP. @@ -12435,7 +12438,60 @@ public void run() { //} checkEDT(); - + + // The editor is a Swing component, so it is built, added and focused on the + // AWT event thread and nowhere else. Built here, on the Codename One EDT, it + // raced the AWT thread that was already painting it: setCaretPosition off the + // AWT thread makes DefaultCaret queue its repaint there, and that repaint read + // the text view while setFont on this thread was replacing it -- an + // intermittent NullPointerException under DefaultCaret.repaintNewCaret. + // + // The hop waits, as flushGraphics and requestFullScreen do, so the editor is + // in place and isEditingText() answers true by the time this method returns, + // exactly as before. Nothing in the body waits for this thread in return: + // what the editor sends back goes through callSerially. + // + // One line of the body is not Swing's: the field's own text is set from the + // editor's when editing starts with a key. That fires the application's + // listeners, so it is handed back and done here, on the thread they expect. + final String[] initialText = new String[1]; + if (EventQueue.isDispatchThread()) { + editStringOnAwtThread(cmp, constraint, text, keyCode, initialText); + } else { + final String fText = text; + try { + EventQueue.invokeAndWait(new Runnable() { + public void run() { + editStringOnAwtThread(cmp, constraint, fText, keyCode, initialText); + } + }); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + return; + } catch (java.lang.reflect.InvocationTargetException ex) { + Throwable cause = ex.getCause(); + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } + if (cause instanceof Error) { + throw (Error) cause; + } + throw new RuntimeException(cause); + } + } + if (initialText[0] != null) { + ((com.codename1.ui.TextField) cmp).setText(initialText[0]); + } + } + + /** + * The Swing half of editString: builds the native editor over the field and + * starts editing. Runs on the AWT event thread only. + * + * @param initialText receives the text the field itself is to be given, when + * editing starts with a key; the caller sets it on the Codename One EDT + */ + private void editStringOnAwtThread(final Component cmp, final int constraint, String text, final int keyCode, String[] initialText) { class Repainter { JComponent jcmp; javax.swing.border.Border origBorder; @@ -12646,7 +12702,7 @@ public void keyReleased(KeyEvent e) { setText(tf, text); setCaretPosition(tf, text.length()); if(cmp instanceof com.codename1.ui.TextField) { - ((com.codename1.ui.TextField) cmp).setText(getText(tf)); + initialText[0] = getText(tf); } } else { setText(tf, text); diff --git a/Ports/JavaSE/src/com/codename1/impl/process/JavaSEProcessSupport.java b/Ports/JavaSE/src/com/codename1/impl/process/JavaSEProcessSupport.java index 46b53629fab..9ae2e8f61a8 100644 --- a/Ports/JavaSE/src/com/codename1/impl/process/JavaSEProcessSupport.java +++ b/Ports/JavaSE/src/com/codename1/impl/process/JavaSEProcessSupport.java @@ -80,5 +80,29 @@ public Process destroyForcibly() { child.destroyForcibly(); return this; } + @Override + public long pid() { + // Process.pid() is Java 9 API and this class compiles against + // Java 8. A Java 8 runtime has no id to give, and answers as + // JavaSE's own default does. + Object id; + try { + id = java.lang.Process.class.getMethod("pid").invoke(child); + } catch (NoSuchMethodException ex) { + throw new UnsupportedOperationException("pid() needs Java 9 or newer"); + } catch (IllegalAccessException ex) { + throw new UnsupportedOperationException(ex.toString()); + } catch (java.lang.reflect.InvocationTargetException ex) { + Throwable cause = ex.getCause(); + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } + throw new UnsupportedOperationException(String.valueOf(cause)); + } + if (id instanceof Long) { + return ((Long) id).longValue(); + } + throw new UnsupportedOperationException("pid() answered " + id); + } } } diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java index abbf5bdddbb..6e5625746e4 100644 --- a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java +++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java @@ -5116,6 +5116,9 @@ public MouseEvent getLastMouseEvent() { @Override public void init(Object m) { + // The class library (vm/JavaAPI) reaches files, preferences, the + // network and the EDT through Codename One from here on. + com.codename1.impl.CoreJdkHostInstaller.install(); //__init(); if (m instanceof Runnable) { //((Runnable)m).run(); diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_desktopwindow.c b/Ports/LinuxPort/nativeSources/cn1_linux_desktopwindow.c index 95526c2f3ef..acc232e49ef 100644 --- a/Ports/LinuxPort/nativeSources/cn1_linux_desktopwindow.c +++ b/Ports/LinuxPort/nativeSources/cn1_linux_desktopwindow.c @@ -1181,6 +1181,136 @@ JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_mainWindowGetBounds___int_1ARR return JAVA_TRUE; } +/* + * Resizing the application's own window. + * + * The main window opens at the size initDisplay is given and, until this existed, + * could only be resized by the user: Display.setWindowSize reached the default + * implementation, which does nothing, so an application that asked for a 350x550 + * window drew into the 800x600 one it was born with. + * + * The size is the OUTER one, frame included, which is what the JavaSE port's + * setWindowSize (JFrame.setSize) and this port's secondary windows both mean. GTK + * resizes the client area, so the chrome has to be subtracted, and the chrome is + * not measurable until the window manager has reparented the window. The request + * is therefore kept pending and applied from configure-event, exactly as + * cn1DesktopApplyPendingOuterSize does for a secondary window. + */ +static int cn1MainOuterPending; +static int cn1MainRequestedOuterW; +static int cn1MainRequestedOuterH; +static int cn1MainConfigureHooked; + +static void cn1MainApplyPendingOuterSize(GtkWidget* main) { + int chromeW; + int chromeH; + int clientW; + int clientH; + if (main == 0 || !cn1MainOuterPending) { + return; + } + if (!cn1DesktopChromeInsets(main, &chromeW, &chromeH)) { + return; + } + clientW = cn1MainRequestedOuterW - chromeW; + clientH = cn1MainRequestedOuterH - chromeH; + if (clientW < 1) { + clientW = 1; + } + if (clientH < 1) { + clientH = 1; + } + /* Cleared before the resize: the resize comes back through configure-event. */ + cn1MainOuterPending = 0; + gtk_window_resize(GTK_WINDOW(main), clientW, clientH); +} + +static gboolean cn1MainOnWindowConfigure(GtkWidget* widget, GdkEventConfigure* e, gpointer data) { + (void) e; + (void) data; + cn1MainApplyPendingOuterSize(widget); + return FALSE; +} + +static void cn1MainWindowSetSizeOnMain(void* arg) { + CN1DesktopOp* op = (CN1DesktopOp*) arg; + GtkWidget* main = cn1LinuxWindowWidget(); + if (main == 0) { + /* Headless: the surface size is the screenshot's, and is not ours to change. */ + return; + } + if (!cn1MainConfigureHooked) { + cn1MainConfigureHooked = 1; + g_signal_connect(main, "configure-event", G_CALLBACK(cn1MainOnWindowConfigure), 0); + } + cn1MainRequestedOuterW = op->a > 0 ? op->a : 1; + cn1MainRequestedOuterH = op->b > 0 ? op->b : 1; + cn1MainOuterPending = 1; + cn1MainApplyPendingOuterSize(main); + if (cn1MainOuterPending) { + /* No frame to measure yet -- or never, with no window manager, where the + * client area is the outer size. Resize to the figure asked for and let + * configure-event correct it once there is chrome. */ + gtk_window_resize(GTK_WINDOW(main), cn1MainRequestedOuterW, cn1MainRequestedOuterH); + } +} + +JAVA_VOID com_codename1_impl_linux_LinuxNative_mainWindowSetSize___int_int( + CODENAME_ONE_THREAD_STATE, JAVA_INT width, JAVA_INT height) { + CN1DesktopOp op; + memset(&op, 0, sizeof(op)); + op.a = width; + op.b = height; + cn1LinuxRunOnMainAndWait(cn1MainWindowSetSizeOnMain, &op); +} + +/* The main window's outer rectangle: what mainWindowSetSize accepts, so that a + * size read back is the size that was set. mainWindowGetBounds answers the client + * area, which is what centring over a Form wants and is smaller by the chrome. */ +static void cn1MainWindowGetFrameOnMain(void* arg) { + CN1DesktopOp* op = (CN1DesktopOp*) arg; + GtkWidget* main = cn1LinuxWindowWidget(); + int chromeW; + int chromeH; + op->a = 0; + if (main == 0) { + return; + } + gtk_window_get_position(GTK_WINDOW(main), &op->out[0], &op->out[1]); + gtk_window_get_size(GTK_WINDOW(main), &op->out[2], &op->out[3]); + if (cn1DesktopChromeInsets(main, &chromeW, &chromeH)) { + op->out[2] += chromeW; + op->out[3] += chromeH; + } else if (cn1MainOuterPending) { + op->out[2] = cn1MainRequestedOuterW; + op->out[3] = cn1MainRequestedOuterH; + } + op->a = 1; +} + +JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_mainWindowGetFrame___int_1ARRAY_R_boolean( + CODENAME_ONE_THREAD_STATE, JAVA_OBJECT out) { + CN1DesktopOp op; + JAVA_INT* arr; + if (out == JAVA_NULL) { + return JAVA_FALSE; + } + if ((int) (*(JAVA_ARRAY) out).length < 4) { + return JAVA_FALSE; + } + memset(&op, 0, sizeof(op)); + cn1LinuxRunOnMainAndWait(cn1MainWindowGetFrameOnMain, &op); + if (!op.a) { + return JAVA_FALSE; + } + arr = (JAVA_INT*) CN1_ARRAY_DATA(out); + arr[0] = op.out[0]; + arr[1] = op.out[1]; + arr[2] = op.out[2]; + arr[3] = op.out[3]; + return JAVA_TRUE; +} + JAVA_INT com_codename1_impl_linux_LinuxNative_desktopWindowGetWidth___int_R_int( CODENAME_ONE_THREAD_STATE, JAVA_INT slot) { CN1LinuxWindow* w = slotAt(slot); diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_edit.c b/Ports/LinuxPort/nativeSources/cn1_linux_edit.c index 25b73c87035..5ec2fcdb305 100644 --- a/Ports/LinuxPort/nativeSources/cn1_linux_edit.c +++ b/Ports/LinuxPort/nativeSources/cn1_linux_edit.c @@ -44,6 +44,8 @@ typedef struct { GtkTextBuffer* buffer; /* GtkTextView buffer, or NULL for single-line */ int singleLine; volatile int done; + /* How the edit ended when Tab ended it: 1 forwards, 2 backwards, else 0. */ + volatile int tab; char* text; /* latest committed/changed text (heap) */ /* The window whose overlay hosts this editor, so teardown removes it from the * same place it was added. */ @@ -102,6 +104,28 @@ static gboolean cn1EditFocusOut(GtkWidget* w, GdkEvent* ev, gpointer data) { return FALSE; } +/* + * Tab in a single-line field ends the edit and is reported, so the port hands the + * key to the form and the focus moves on. Left to GTK it moved GTK's own focus: + * the entry is the only focusable widget of the window, so the edit ended on the + * focus-out and the focus of the form stayed where it was -- the next field was + * out of the keyboard's reach. + */ +static gboolean cn1EditKeyPress(GtkWidget* w, GdkEventKey* ev, gpointer data) { + CN1Edit* e = (CN1Edit*) data; + (void) w; + if ((ev->state & (GDK_CONTROL_MASK | GDK_MOD1_MASK)) != 0) { + return FALSE; + } + if (ev->keyval == GDK_KEY_Tab || ev->keyval == GDK_KEY_KP_Tab || ev->keyval == GDK_KEY_ISO_Left_Tab) { + cn1EditCaptureText(e); + e->tab = (ev->keyval == GDK_KEY_ISO_Left_Tab || (ev->state & GDK_SHIFT_MASK) != 0) ? 2 : 1; + e->done = 1; + return TRUE; + } + return FALSE; +} + static void cn1EditApplyStyle(GtkWidget* w, CN1Font* font, int fg, int bg) { if (font && font->desc) { gtk_widget_override_font(w, font->desc); @@ -143,6 +167,7 @@ static void cn1EditCreateOnMain(void* p) { cn1EditApplyStyle(e->entry, req->font, req->fg, req->bg); g_signal_connect(e->entry, "changed", G_CALLBACK(cn1EditChanged), e); g_signal_connect(e->entry, "activate", G_CALLBACK(cn1EditActivate), e); + g_signal_connect(e->entry, "key-press-event", G_CALLBACK(cn1EditKeyPress), e); g_signal_connect(e->entry, "focus-out-event", G_CALLBACK(cn1EditFocusOut), e); } else { /* Use the GtkTextView directly as the overlay widget rather than wrapping it @@ -171,7 +196,15 @@ static void cn1EditCreateOnMain(void* p) { g_object_ref_sink(e->container); cn1LinuxOverlayAdd(req->slot, e->container, req->x, req->y, req->w, req->h); - gtk_widget_grab_focus(e->container); + if (e->entry) { + /* An entry given the focus selects all of its text, so the first key typed + * replaced what the field held. The caret goes after the text instead, as + * it does in the field the editor stands in for. */ + gtk_entry_grab_focus_without_selecting(GTK_ENTRY(e->entry)); + gtk_editable_set_position(GTK_EDITABLE(e->entry), -1); + } else { + gtk_widget_grab_focus(e->container); + } req->result = e; } @@ -202,6 +235,11 @@ JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_editIsDone___long_R_boolean(CO return (e && e->done) ? JAVA_TRUE : JAVA_FALSE; } +JAVA_INT com_codename1_impl_linux_LinuxNative_editTabbed___long_R_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + CN1Edit* e = (CN1Edit*) (intptr_t) peer; + return e ? (JAVA_INT) e->tab : 0; +} + JAVA_OBJECT com_codename1_impl_linux_LinuxNative_editGetText___long_R_java_lang_String(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { CN1Edit* e = (CN1Edit*) (intptr_t) peer; JAVA_OBJECT result; diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_image.c b/Ports/LinuxPort/nativeSources/cn1_linux_image.c index 6b8eea1d168..cff7cd2feba 100644 --- a/Ports/LinuxPort/nativeSources/cn1_linux_image.c +++ b/Ports/LinuxPort/nativeSources/cn1_linux_image.c @@ -34,8 +34,15 @@ */ #include "cn1_linux_gfx.h" +#include +#include +#include #include #include +#include +#ifdef __GLIBC__ +#include +#endif extern const char* stringToUTF8(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT str); extern JAVA_OBJECT allocArray(CODENAME_ONE_THREAD_STATE, int length, struct clazz* type, int primitiveSize, int dim); @@ -44,11 +51,61 @@ int cn1LinuxSurfaceToPng(cairo_surface_t* surface, unsigned char** outData, int* #define CN1I(p) ((CN1Image*) (intptr_t) (p)) +/* + * How many images are alive and how many bytes their surfaces hold. An image is + * made on whatever thread asks for one and destroyed on the drawing thread, hence + * atomics. The numbers answer the question a resident-set figure cannot: whether + * memory is in pictures the port still holds, or somewhere else. + * + * CN1_LOG_IMAGES in the environment prints them to stderr, at most every 50 ms, + * as "[IMAGES] live= bytes= made= freed=" -- the same + * kind of switch as the collector's CN1_GC_LOG_CYCLES, and like it one cached + * getenv when it is off. + */ +static _Atomic long cn1ImagesLive = 0; +static _Atomic long cn1ImageBytesLive = 0; +static _Atomic long cn1ImagesMade = 0; +static _Atomic long cn1ImagesFreed = 0; + +static void cn1LogImages(void) { + static _Atomic int logImages = -1; + static _Atomic long long lastLogged = 0; + struct timespec now; + long long millis; + int on = atomic_load_explicit(&logImages, memory_order_relaxed); + if (on < 0) { + on = getenv("CN1_LOG_IMAGES") ? 1 : 0; + atomic_store_explicit(&logImages, on, memory_order_relaxed); + } + if (!on) { + return; + } + clock_gettime(CLOCK_MONOTONIC, &now); + millis = (long long) now.tv_sec * 1000 + now.tv_nsec / 1000000; + if (millis - atomic_load_explicit(&lastLogged, memory_order_relaxed) < 50) { + return; + } + atomic_store_explicit(&lastLogged, millis, memory_order_relaxed); + fprintf(stderr, "[IMAGES] live=%ld bytes=%ld made=%ld freed=%ld\n", + atomic_load_explicit(&cn1ImagesLive, memory_order_relaxed), + atomic_load_explicit(&cn1ImageBytesLive, memory_order_relaxed), + atomic_load_explicit(&cn1ImagesMade, memory_order_relaxed), + atomic_load_explicit(&cn1ImagesFreed, memory_order_relaxed)); +} + +static long cn1SurfaceBytes(cairo_surface_t* surface) { + return (long) cairo_image_surface_get_stride(surface) * cairo_image_surface_get_height(surface); +} + static CN1Image* cn1WrapSurface(cairo_surface_t* surface) { CN1Image* img = (CN1Image*) calloc(1, sizeof(CN1Image)); img->surface = surface; img->width = cairo_image_surface_get_width(surface); img->height = cairo_image_surface_get_height(surface); + atomic_fetch_add_explicit(&cn1ImagesLive, 1, memory_order_relaxed); + atomic_fetch_add_explicit(&cn1ImageBytesLive, cn1SurfaceBytes(surface), memory_order_relaxed); + atomic_fetch_add_explicit(&cn1ImagesMade, 1, memory_order_relaxed); + cn1LogImages(); return img; } @@ -127,6 +184,10 @@ JAVA_LONG com_codename1_impl_linux_LinuxNative_createImageFromBytes___byte_1ARRA if (data == JAVA_NULL || length <= 0) { return 0; } + /* Never read outside the array, whatever the caller passed. */ + if (offset < 0 || length > ((JAVA_ARRAY) data)->length - offset) { + return 0; + } bytes = (unsigned char*) CN1_ARRAY_DATA(data); stream = g_memory_input_stream_new_from_data(bytes + offset, length, 0); pix = gdk_pixbuf_new_from_stream(stream, 0, 0); @@ -235,6 +296,98 @@ JAVA_LONG com_codename1_impl_linux_LinuxNative_getImageGraphics___long_R_long(CO return (JAVA_LONG) (intptr_t) g; } +/* + * Disposal. + * + * An image handle's finalizer runs on the collector's thread while the event + * dispatch thread may be inside cairo with the very surface, so the finalizer only + * queues the pointer (releaseImage) and the drawing thread frees what is queued + * between frames (nextReleasedImage / disposeImage). Until these existed nothing + * in the port destroyed an image surface at all. + */ +/* + * Freeing is not giving back. glibc keeps what free() hands it for the next + * malloc, and a surface is 1.9 MB at 800x600 -- below the size it maps on its + * own once the threshold has adapted, so the pages stay in the process. An + * application that draws a picture per frame freed every one of them and was + * still measured at 340 MB resident with 58 MB in use; malloc_trim(0) took it to + * 188 MB. The pages are returned once this many bytes of surfaces were freed + * since the last time, which is about once per collection. Other C libraries + * have no such call and keep their own counsel. + */ +#define CN1_TRIM_AFTER_BYTES (32L * 1024 * 1024) +static long cn1FreedSinceTrim = 0; + +static pthread_mutex_t cn1ReleasedLock = PTHREAD_MUTEX_INITIALIZER; +static CN1Image** cn1Released = 0; +static int cn1ReleasedCount = 0; +static int cn1ReleasedCapacity = 0; + +JAVA_VOID com_codename1_impl_linux_LinuxNative_releaseImage___long(CODENAME_ONE_THREAD_STATE, JAVA_LONG image) { + CN1Image* img = CN1I(image); + if (!img) { + return; + } + pthread_mutex_lock(&cn1ReleasedLock); + if (cn1ReleasedCount == cn1ReleasedCapacity) { + int capacity = cn1ReleasedCapacity == 0 ? 64 : cn1ReleasedCapacity * 2; + CN1Image** grown = (CN1Image**) realloc(cn1Released, sizeof(CN1Image*) * (size_t) capacity); + if (grown == 0) { + /* Out of memory for the queue itself: the image stays allocated, which + * is what happened to every image before this queue existed. */ + pthread_mutex_unlock(&cn1ReleasedLock); + return; + } + cn1Released = grown; + cn1ReleasedCapacity = capacity; + } + cn1Released[cn1ReleasedCount++] = img; + pthread_mutex_unlock(&cn1ReleasedLock); +} + +JAVA_LONG com_codename1_impl_linux_LinuxNative_nextReleasedImage___R_long(CODENAME_ONE_THREAD_STATE) { + CN1Image* img = 0; + pthread_mutex_lock(&cn1ReleasedLock); + if (cn1ReleasedCount > 0) { + img = cn1Released[--cn1ReleasedCount]; + } + pthread_mutex_unlock(&cn1ReleasedLock); + return (JAVA_LONG) (intptr_t) img; +} + +JAVA_LONG com_codename1_impl_linux_LinuxNative_disposeImage___long_R_long(CODENAME_ONE_THREAD_STATE, JAVA_LONG image) { + CN1Image* img = CN1I(image); + CN1Graphics* g; + if (!img) { + return 0; + } + g = img->mutableGraphics; + if (g != 0) { + if (g->cr != 0) { + cairo_destroy(g->cr); + } + cn1LinuxFreeClipShape(g); + free(g); + } + if (img->surface != 0) { + long bytes = cn1SurfaceBytes(img->surface); + cn1FreedSinceTrim += bytes; + atomic_fetch_sub_explicit(&cn1ImageBytesLive, bytes, memory_order_relaxed); + cairo_surface_destroy(img->surface); + } + free(img); + atomic_fetch_sub_explicit(&cn1ImagesLive, 1, memory_order_relaxed); + atomic_fetch_add_explicit(&cn1ImagesFreed, 1, memory_order_relaxed); + cn1LogImages(); +#ifdef __GLIBC__ + if (cn1FreedSinceTrim >= CN1_TRIM_AFTER_BYTES) { + cn1FreedSinceTrim = 0; + malloc_trim(0); + } +#endif + return (JAVA_LONG) (intptr_t) g; +} + /* drawImage / drawImageScaled live here (image ownership unit). They blit the * CN1Image surface onto the target graphics honouring its clip, affine and * alpha. */ diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_io.c b/Ports/LinuxPort/nativeSources/cn1_linux_io.c index e703ba70f8b..4a0ea775634 100644 --- a/Ports/LinuxPort/nativeSources/cn1_linux_io.c +++ b/Ports/LinuxPort/nativeSources/cn1_linux_io.c @@ -467,6 +467,32 @@ JAVA_OBJECT com_codename1_impl_linux_LinuxNative_storageDir___R_java_lang_String return newStringFromCString(threadStateData, cn1StorageDir()); } +/* + * A moment written the way the user's locale writes a date (what == 0, strftime's %x) + * or a time of day (what == 1, %X), in the local time zone. An empty string when the + * moment cannot be broken down. + * + * The locale is the one GTK selected from the environment when it started + * (setlocale(LC_ALL, "")), so LC_TIME decides: 02/29/2024 for en_US, 29.02.2024 for + * de_DE, 02/29/24 with no locale set at all. + */ +JAVA_OBJECT com_codename1_impl_linux_LinuxNative_formatLocalTime___long_int_R_java_lang_String(CODENAME_ONE_THREAD_STATE, JAVA_LONG millis, JAVA_INT what) { + char buf[256]; + struct tm parts; + time_t seconds = (time_t) (millis / 1000); + if (millis < 0 && millis % 1000 != 0) { + /* Division truncates towards zero; a moment before 1970 belongs to the second below. */ + seconds--; + } + buf[0] = 0; + if (localtime_r(&seconds, &parts) != 0) { + if (strftime(buf, sizeof(buf), what == 1 ? "%X" : "%x", &parts) == 0) { + buf[0] = 0; + } + } + return newStringFromCString(threadStateData, buf); +} + JAVA_OBJECT com_codename1_impl_linux_LinuxNative_executableDir___R_java_lang_String(CODENAME_ONE_THREAD_STATE) { char buf[4096]; ssize_t n = readlink("/proc/self/exe", buf, sizeof(buf) - 1); diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_text.c b/Ports/LinuxPort/nativeSources/cn1_linux_text.c index 1817e317cd9..cdd0629dd58 100644 --- a/Ports/LinuxPort/nativeSources/cn1_linux_text.c +++ b/Ports/LinuxPort/nativeSources/cn1_linux_text.c @@ -86,11 +86,66 @@ static CN1Font* cn1MakeFont(const char* family, int weight, int italic, int pixe pango_font_description_set_weight(f->desc, weight); pango_font_description_set_style(f->desc, italic ? PANGO_STYLE_ITALIC : PANGO_STYLE_NORMAL); pango_font_description_set_absolute_size(f->desc, pixelSize * PANGO_SCALE); + /* + * A weight under regular is kept only when the family has a face near it. + * Pango answers with the nearest face it has, and for a family with no Light + * that is its ExtraLight when it has one (DejaVu Sans does): the titles and + * buttons a theme styles with native:MainLight came out as hairlines that read + * as disabled. Regular is what the other ports fall back to. + */ + if (weight < PANGO_WEIGHT_NORMAL) { + PangoFont* loaded = pango_context_load_font(cn1LinuxPangoContext(), f->desc); + int near = 0; + if (loaded != 0) { + PangoFontDescription* actual = pango_font_describe(loaded); + if (actual != 0) { + int delta = (int) pango_font_description_get_weight(actual) - weight; + near = delta > -75 && delta < 75; + pango_font_description_free(actual); + } + g_object_unref(loaded); + } + if (!near) { + pango_font_description_set_weight(f->desc, PANGO_WEIGHT_NORMAL); + } + } f->pixelSize = pixelSize; cn1FontMetrics(f); return f; } +/* + * The weight and the slant a "native:" face asks for. The names are Codename One's: + * Main or Italic, then Thin, Light, Regular, Bold or Black ("native:ItalicBold"). + * + * Every one of them used to load as the regular upright face: the name was reduced + * to the family and the rest of it dropped. A label a theme styles with + * native:MainBold drew like the text around it, and text an application asked for + * in bold or italic -- which the Swing and JavaFX layers ask for under these names + * -- was neither. deriveTrueTypeFont copies the description it is handed, so a font + * derived from one of these keeps what is set here. + */ +static void cn1NativeFace(const char* face, int* weight, int* italic) { + const char* rest = face; + *weight = PANGO_WEIGHT_NORMAL; + *italic = 0; + if (strncmp(face, "Italic", 6) == 0) { + *italic = 1; + rest = face + 6; + } else if (strncmp(face, "Main", 4) == 0) { + rest = face + 4; + } + if (strcmp(rest, "Thin") == 0) { + *weight = PANGO_WEIGHT_THIN; + } else if (strcmp(rest, "Light") == 0) { + *weight = PANGO_WEIGHT_LIGHT; + } else if (strcmp(rest, "Bold") == 0) { + *weight = PANGO_WEIGHT_BOLD; + } else if (strcmp(rest, "Black") == 0) { + *weight = PANGO_WEIGHT_HEAVY; + } +} + JAVA_LONG com_codename1_impl_linux_LinuxNative_createFont___int_int_int_R_long(CODENAME_ONE_THREAD_STATE, JAVA_INT face, JAVA_INT style, JAVA_INT size) { const char* family = "Sans"; int pixelSize = 18; @@ -237,12 +292,15 @@ JAVA_INT com_codename1_impl_linux_LinuxNative_fontHeight___long_R_int(CODENAME_O JAVA_LONG com_codename1_impl_linux_LinuxNative_loadTrueTypeFont___java_lang_String_java_lang_String_R_long(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT fontName, JAVA_OBJECT fileName) { const char* name = fontName == JAVA_NULL ? "Sans" : stringToUTF8(threadStateData, fontName); const char* family = name; + int weight = PANGO_WEIGHT_NORMAL; + int italic = 0; (void) fileName; - /* "native:" scheme maps to the system UI family. */ + /* "native:" scheme maps to the system UI family, in the weight and slant the name asks for. */ if (strncmp(name, "native:", 7) == 0) { + cn1NativeFace(name + 7, &weight, &italic); family = "Sans"; } - return (JAVA_LONG) (intptr_t) cn1MakeFont(family, PANGO_WEIGHT_NORMAL, 0, 18); + return (JAVA_LONG) (intptr_t) cn1MakeFont(family, weight, italic, 18); } JAVA_LONG com_codename1_impl_linux_LinuxNative_loadTrueTypeFontFromMemory___java_lang_String_byte_1ARRAY_R_long(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT fontName, JAVA_OBJECT data) { @@ -260,10 +318,13 @@ JAVA_LONG com_codename1_impl_linux_LinuxNative_loadTrueTypeFontFromMemory___java FT_Face face = 0; family[0] = 0; if (data == JAVA_NULL || (len = (int) (*(JAVA_ARRAY) data).length) <= 0) { + int weight = PANGO_WEIGHT_NORMAL; + int italic = 0; if (strncmp(name, "native:", 7) == 0) { + cn1NativeFace(name + 7, &weight, &italic); name = "Sans"; } - return (JAVA_LONG) (intptr_t) cn1MakeFont(name, PANGO_WEIGHT_NORMAL, 0, 18); + return (JAVA_LONG) (intptr_t) cn1MakeFont(name, weight, italic, 18); } bytes = (unsigned char*) CN1_ARRAY_DATA(data); diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_window.c b/Ports/LinuxPort/nativeSources/cn1_linux_window.c index 6c1a7283e7e..b27dfd1bb1d 100644 --- a/Ports/LinuxPort/nativeSources/cn1_linux_window.c +++ b/Ports/LinuxPort/nativeSources/cn1_linux_window.c @@ -711,7 +711,13 @@ static gboolean cn1OnKey(GtkWidget* widget, GdkEventKey* e, gpointer data) { * is torn down GTK clears the toplevel focus to NULL, so CN1 keys resume. */ if (cn1Window != 0) { GtkWidget* focus = gtk_window_get_focus(GTK_WINDOW(cn1Window)); - if (focus != 0 && focus != cn1DrawingArea && focus != cn1TitleCanvas) { + /* The accessibility mirror is not a peer. Its widgets describe Codename One + * components to an assistive technology and have no way to hand text back, + * so a keystroke that arrives while one of them holds the GTK focus is still + * Codename One's. Forwarding it typed into an entry drawn at 1% opacity: + * the text went nowhere the user could see. */ + if (focus != 0 && focus != cn1DrawingArea && focus != cn1TitleCanvas + && !(cn1AccessibilityFixed != 0 && gtk_widget_is_ancestor(focus, cn1AccessibilityFixed))) { return FALSE; } } @@ -1731,9 +1737,45 @@ JAVA_VOID com_codename1_impl_linux_LinuxNative_accessibilityAction___long_java_l cn1LinuxRunOnMainAndWait(cn1A11yActionMain, action); } +/* Takes a window and everything under it out of pointer delivery. + * + * The mirror is made of real GTK widgets -- entries, toggle buttons, scales -- laid + * exactly over the Codename One components they describe, and each of them owns + * GdkWindows that take pointer input. gtk_overlay_set_overlay_pass_through only + * covers the overlay child's own window: GDK says in so many words that a + * pass-through window "can still have a subwindow without pass through". So a + * click on a text field landed on the invisible GtkEntry above it, which took the + * GTK focus, and everything typed afterwards went into that entry instead of the + * application. An assistive technology drives these widgets through ATK actions, + * never through the pointer, so nothing is lost by letting the pointer through. */ +static void cn1A11yPassPointerThrough(GdkWindow* window) { + GList* item; + if (window == 0) { + return; + } + gdk_window_set_pass_through(window, TRUE); + for (item = gdk_window_peek_children(window); item; item = item->next) { + cn1A11yPassPointerThrough((GdkWindow*) item->data); + } +} + static void cn1A11yEndMain(void* pointer) { (void) pointer; - if (cn1AccessibilityFixed) gtk_widget_show_all(cn1AccessibilityFixed); + if (cn1AccessibilityFixed) { + gtk_widget_show_all(cn1AccessibilityFixed); + if (gtk_widget_get_realized(cn1AccessibilityFixed)) { + /* A GtkFixed has no window of its own: this is the child window the + * overlay made for it, and the mirror's windows are all below it. */ + GdkWindow* mirror = gtk_widget_get_window(cn1AccessibilityFixed); + /* Never the window the drawing area lives in: passing THAT through + * would take the pointer away from the whole application. */ + if (mirror != 0 && mirror != gtk_widget_get_window(cn1Overlay) + && mirror != gtk_widget_get_window(cn1DrawingArea) + && mirror != gtk_widget_get_window(cn1Window)) { + cn1A11yPassPointerThrough(mirror); + } + } + } } JAVA_VOID com_codename1_impl_linux_LinuxNative_accessibilityEnd___int(CODENAME_ONE_THREAD_STATE, JAVA_INT changeType) { diff --git a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java index 04f40467d12..b483bad9063 100644 --- a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java +++ b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java @@ -53,6 +53,7 @@ import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; /** * Native Linux (GTK3, desktop) implementation of the Codename One platform @@ -81,6 +82,9 @@ public class LinuxImplementation extends CodenameOneImplementation { /// editor and browser hosts. Matches CN1_MAIN_WINDOW_SLOT in cn1_linux_gfx.h. static final int MAIN_WINDOW_SLOT = -1; + /// The key code of Tab, the one `Form` moves the focus on. + private static final int KEY_CODE_TAB = 9; + @Override public boolean isHighContrastEnabled() { @@ -180,7 +184,109 @@ public static LinuxImplementation getInstance() { /* -------------------------------------------------------------- helpers */ private static long peer(Object o) { - return o == null ? 0L : ((Long) o).longValue(); + if (o instanceof Long) { + return ((Long) o).longValue(); + } + if (o instanceof NativeImage) { + return ((NativeImage) o).peer; + } + if (o instanceof ImageGraphics) { + return ((ImageGraphics) o).peer; + } + return 0L; + } + + /// The native half of an image: a Cairo surface, and the graphics context drawing + /// into it once one was asked for. + /// + /// An image used to be a boxed pointer, and nothing ever gave the surface back: + /// there was no native to do it. An application that made a picture per frame -- + /// 800x600 is 1.9 MB -- grew by that much per frame for as long as it ran, with + /// the collector reclaiming every Java object on schedule. The handle is what + /// tells the port the image is gone. + /// + /// `finalize` runs on the collector's thread, where a surface the event dispatch + /// thread may be part way through blitting must not be destroyed. It only queues + /// the pointer; `#disposeReleasedImages()` frees it between frames. + static final class NativeImage { + final long peer; + + NativeImage(long peer) { + this.peer = peer; + } + + @Override + protected void finalize() { + if (peer != 0) { + LinuxNative.releaseImage(peer); + } + } + } + + /// The graphics of a mutable image. It keeps the image: the context draws into + /// the image's surface and is freed with it, so the image must outlive every + /// graphics object still drawing there. + static final class ImageGraphics { + final long peer; + final Object image; + + ImageGraphics(long peer, Object image) { + this.peer = peer; + this.image = image; + } + } + + /// Surface bytes handed out since a collection was last asked for. A surface is + /// invisible to the collector -- the Java side of a 2 MB picture is 16 bytes -- + /// so a program can allocate images for a long time without the heap ever + /// reaching the size that starts a cycle. Plain, not atomic: a lost update only + /// postpones the request by one image. + private static long imageBytesSinceGc; + + private static final long IMAGE_BYTES_PER_GC = 32L * 1024 * 1024; + + /// When an image was last made, and how long none must have been made before + /// an idle event dispatch thread asks for the collection that the count above + /// did not reach. An application that draws a picture per frame and then stops + /// leaves the images of its last frames behind, short of the count for good: + /// nothing allocates any more, so nothing ever starts a cycle. While images + /// are still being made the count is what asks. + private static long lastImageMillis; + + private static final long IMAGE_QUIET_MILLIS = 500; + + static Object wrapImage(long peer, int width, int height) { + if (peer != 0) { + if (width <= 0 || height <= 0) { + width = LinuxNative.imageWidth(peer); + height = LinuxNative.imageHeight(peer); + } + imageBytesSinceGc += 4L * width * height; + lastImageMillis = System.currentTimeMillis(); + if (imageBytesSinceGc > IMAGE_BYTES_PER_GC) { + imageBytesSinceGc = 0; + System.gc(); + } + } + return new NativeImage(peer); + } + + /// Frees the images whose handles the collector finalized. Event dispatch thread + /// only, between frames: it is the one moment no native draw can be holding one. + /// The graphics context freed with an image takes its entries in the transform + /// and clip-stack tables with it; a context allocated later at the same address + /// would otherwise start life under a dead image's transform. + private void disposeReleasedImages() { + long image = LinuxNative.nextReleasedImage(); + while (image != 0) { + long graphics = LinuxNative.disposeImage(image); + if (graphics != 0) { + Long key = Long.valueOf(graphics); + graphicsTransforms.remove(key); + clipStacks.remove(key); + } + image = LinuxNative.nextReleasedImage(); + } } private String storagePath(String name) { @@ -221,6 +327,9 @@ private static byte[] readFully(InputStream i) throws IOException { @Override public void init(Object m) { + // The class library (vm/JavaAPI) reaches files, preferences, the + // network and the EDT through Codename One from here on. + com.codename1.impl.CoreJdkHostInstaller.install(); LinuxNative.initDisplay("Codename One", 800, 600); windowGraphicsPeer = LinuxNative.getWindowGraphics(); windowGraphics = Long.valueOf(windowGraphicsPeer); @@ -645,8 +754,64 @@ public int getDisplayHeight() { return LinuxNative.getDisplayHeight(); } + /// Resizes the application window. The size is the outer one, frame included, + /// as on the JavaSE port; the display follows through the size-changed event + /// the window's new allocation produces, so nothing is laid out here. + /// + /// The window still OPENS at 800x600: it exists before any application code + /// runs, and that default is what every screenshot baseline was taken at. + @Override + public void setWindowSize(int width, int height) { + if (width > 0 && height > 0) { + LinuxNative.mainWindowSetSize(width, height); + } + } + + @Override + public com.codename1.ui.geom.Rectangle getWindowBounds() { + int[] frame = new int[4]; + if (!LinuxNative.mainWindowGetFrame(frame)) { + return super.getWindowBounds(); + } + return new com.codename1.ui.geom.Rectangle(frame[0], frame[1], frame[2], frame[3]); + } + + /// The monitor the application window is on, which is the desktop a + /// percentage of it is taken from. + @Override + public com.codename1.ui.geom.Dimension getDesktopSize() { + int[] bounds = new int[4]; + if (!LinuxNative.mainWindowGetFrame(bounds)) { + return super.getDesktopSize(); + } + LinuxNative.monitorBounds(LinuxNative.monitorForMainWindow(), false, bounds); + if (bounds[2] <= 0 || bounds[3] <= 0) { + return super.getDesktopSize(); + } + return new com.codename1.ui.geom.Dimension(bounds[2], bounds[3]); + } + + /// The hint is a size for the window the first form opens in. Here that window + /// already exists, so the hint is applied as it arrives and consumed, the way + /// the JavaSE port consumes it when it builds its frame. + @Override + public void setInitialWindowSizeHintPercent(com.codename1.ui.geom.Dimension hint) { + super.setInitialWindowSizeHintPercent(hint); + com.codename1.ui.geom.Dimension desktop = hint == null ? null : getDesktopSize(); + if (desktop == null) { + return; + } + int width = Math.min(desktop.getWidth(), + Math.max(1, Math.round(desktop.getWidth() * (hint.getWidth() / 100f)))); + int height = Math.min(desktop.getHeight(), + Math.max(1, Math.round(desktop.getHeight() * (hint.getHeight() / 100f)))); + super.setInitialWindowSizeHintPercent(null); + setWindowSize(width, height); + } + @Override public void flushGraphics() { + disposeReleasedImages(); LinuxNative.flushGraphics(windowGraphicsPeer, 0, 0, getDisplayWidth(), getDisplayHeight()); } @@ -721,6 +886,8 @@ public void run() { @Override public void flushGraphics(int x, int y, int width, int height) { + // Most frames repaint a part of the window and come through here. + disposeReleasedImages(); LinuxNative.flushGraphics(windowGraphicsPeer, x, y, width, height); } @@ -764,11 +931,85 @@ public void screenshot(com.codename1.util.SuccessCallback callback) { @Override public void edtIdle(boolean enter) { - // Intentionally empty: the Win32 message pump and input dispatch run on the + // No pumping here: the message pump and input dispatch run on the // main thread (see runMainEventLoop), and the EDT is woken by the Display // lock notifications those dispatches trigger. The EDT therefore idles by // sleeping on the lock like every other Codename One platform -- it must // not pump or drain on its own (it is not the window's owning thread). + // + // What it does do on its way to sleep is free the images the collector + // let go of. A frame frees them too, but an application that computed a + // picture and then sat still painted no further frame: the images of + // its last collection stayed queued, 70 to 200 MB of them in one + // measured run, for as long as nothing was repainted. + if (enter) { + disposeReleasedImages(); + if (imageBytesSinceGc > 0) { + long quiet = System.currentTimeMillis() - lastImageMillis; + if (quiet > IMAGE_QUIET_MILLIS) { + // See lastImageMillis. Nothing is counted until an image is made. + imageBytesSinceGc = 0; + System.gc(); + releaseWakes = 2; + wakeEdtIn(IMAGE_RELEASE_MILLIS); + } else { + // Too soon to call it quiet, and this thread may not wake again + // on its own: an application that stops moving stops for good. + wakeEdtIn(IMAGE_QUIET_MILLIS - quiet + 1); + } + } else if (releaseWakes > 0) { + releaseWakes--; + if (releaseWakes > 0) { + wakeEdtIn(4 * IMAGE_RELEASE_MILLIS); + } + } + } + } + + /// How long after asking for the quiet collection this thread comes back to free + /// what it let go of, and how many times it still will: once soon, once later for + /// a cycle that took its time. + private static final long IMAGE_RELEASE_MILLIS = 250; + + private int releaseWakes; + + private static final AtomicBoolean WAKE_PENDING = new AtomicBoolean(); + + private static final Runnable NOTHING = new Runnable() { + @Override + public void run() { + } + }; + + /// Has the event dispatch thread pass through `edtIdle` once more, some time + /// from now. The quiet collection above is asked for by a thread on its way to + /// sleep, and the collector answers on its own thread a moment later: the images + /// it released sat in the queue until the next frame, which a still application + /// never paints. A Mandelbrot picture that had finished rendering held 115 + /// surfaces, 220 MB, for as long as nobody touched the window; and when the + /// thread went to sleep less than `IMAGE_QUIET_MILLIS` after the last image, + /// the collection was never asked for at all. + /// + /// One wake-up is pending at a time, hence the atomic: the timer thread clears + /// it and this thread sets it. + private static void wakeEdtIn(final long millis) { + if (!WAKE_PENDING.compareAndSet(false, true)) { + return; + } + Thread t = new Thread(new Runnable() { + @Override + public void run() { + try { + Thread.sleep(millis); + } catch (InterruptedException ex) { + // Woken early is still woken: the pass it asks for is harmless. + } + WAKE_PENDING.set(false); + Display.getInstance().callSerially(NOTHING); + } + }, "cn1-linux-image-release"); + t.setDaemon(true); + t.start(); } // High bits in the native key flag touch, pen, or eraser input (see cn1_linux.h); @@ -882,10 +1123,10 @@ private void drainInput() { dispatchPointerHover(windowId, x, y, key); break; case EVENT_KEY_PRESSED: - windowKeyPressed(windowId, key); + windowKeyPressed(windowId, keyCodeOf(key)); break; case EVENT_KEY_RELEASED: - windowKeyReleased(windowId, key); + windowKeyReleased(windowId, keyCodeOf(key)); break; case EVENT_SIZE_CHANGED: if (windowId == 0) { @@ -1185,7 +1426,7 @@ public Object getNativeGraphics() { @Override public Object getNativeGraphics(Object image) { - return Long.valueOf(LinuxNative.getImageGraphics(peer(image))); + return new ImageGraphics(LinuxNative.getImageGraphics(peer(image)), image); } /* ------------------------------------------------------------- transforms @@ -1931,6 +2172,25 @@ public void drawImage(Object graphics, Object img, int x, int y) { LinuxNative.drawImage(peer(graphics), peer(img), x, y); } + /// Cairo scales an image as it draws it, and `drawImageScaled` has done so + /// since the port was written -- but nothing called it. Without this answer the + /// core scales for the port: `Image.scaled` makes a second surface of the size + /// asked for and keeps it in the image's scale cache for as long as the image + /// lives, one per size. An application that zooms a picture draws it at a new + /// size every frame, so every frame left a surface behind, each larger than the + /// last: a JavaFX canvas zooming an 800x600 snapshot was measured holding 156 + /// surfaces, 1.8 GB, by the end of a one second animation, none of them garbage + /// until the snapshot itself was replaced. + @Override + public boolean isScaledImageDrawingSupported() { + return true; + } + + @Override + public void drawImage(Object graphics, Object img, int x, int y, int w, int h) { + LinuxNative.drawImageScaled(peer(graphics), peer(img), x, y, w, h); + } + @Override public void drawRGB(Object graphics, int[] rgbData, int offset, int x, int y, int w, int h, boolean processAlpha) { LinuxNative.drawRGB(peer(graphics), rgbData, offset, x, y, w, h, processAlpha); @@ -2049,28 +2309,34 @@ public Object deriveTrueTypeFont(Object font, float size, int weight) { @Override public Object createImage(int[] rgb, int width, int height) { - return Long.valueOf(LinuxNative.createImageFromARGB(rgb, width, height)); + return wrapImage(LinuxNative.createImageFromARGB(rgb, width, height), width, height); } @Override public Object createImage(String path) throws IOException { - return Long.valueOf(LinuxNative.createImageFromFile(stripFileUrl(path))); + return wrapImage(LinuxNative.createImageFromFile(stripFileUrl(path)), 0, 0); } @Override public Object createImage(InputStream i) throws IOException { byte[] data = readFully(i); - return Long.valueOf(LinuxNative.createImageFromBytes(data, 0, data.length)); + return wrapImage(LinuxNative.createImageFromBytes(data, 0, data.length), 0, 0); } @Override public Object createImage(byte[] bytes, int offset, int len) { - return Long.valueOf(LinuxNative.createImageFromBytes(bytes, offset, len)); + // The native decoder reads the range as it is given; a range past + // the array is refused here, as Java would, before it is read. + if (bytes != null && (offset < 0 || len < 0 || offset > bytes.length - len)) { + throw new ArrayIndexOutOfBoundsException("offset " + offset + ", length " + len + + ", array " + bytes.length); + } + return wrapImage(LinuxNative.createImageFromBytes(bytes, offset, len), 0, 0); } @Override public Object createMutableImage(int width, int height, int fillColor) { - return Long.valueOf(LinuxNative.createMutableImage(width, height, fillColor)); + return wrapImage(LinuxNative.createMutableImage(width, height, fillColor), width, height); } @Override @@ -2085,7 +2351,7 @@ public int getImageHeight(Object i) { @Override public Object scale(Object nativeImage, int width, int height) { - return Long.valueOf(LinuxNative.scaleImage(peer(nativeImage), width, height)); + return wrapImage(LinuxNative.scaleImage(peer(nativeImage), width, height), width, height); } @Override @@ -2163,6 +2429,14 @@ public void editString(final Component cmp, int maxSize, int constraint, String // The editor goes into the overlay of the window the field lives in; without // the slot it appeared over the main window while the window being typed into // showed nothing. + // The key that started the edit is part of what was typed. The editor + // opens after it was pressed and never sees it, so it is added here; + // without it the first character typed into a field was lost. + if (initiatingKeycode >= ' ' && initiatingKeycode != 127 && initiatingKeycode <= 0xffff + && getGameAction(initiatingKeycode) == 0 + && (maxSize <= 0 || text == null || text.length() < maxSize)) { + text = (text == null ? "" : text) + (char) initiatingKeycode; + } long peer = LinuxNative.editStringAt(x, y, w, h, text == null ? "" : text, singleLine, maxSize, fontPeer, s.getFgColor(), s.getBgColor(), 0, LinuxWindowManager.slotForComponent(cmp)); @@ -2186,7 +2460,21 @@ public void run() { return; } if (LinuxNative.editIsDone(editPeer)) { + // Tab ended it: the editor kept the key from the form, so + // the form is given it now and moves its focus on. In the + // main window only; a second window keeps what it did. + int tabbed = LinuxNative.editTabbed(editPeer); + boolean tab = tabbed != 0 + && LinuxWindowManager.slotForComponent(cmp) == MAIN_WINDOW_SLOT; hideTextEditor(); + if (tab) { + // The editor consumed the key, so the direction travels + // with it: 2 is Shift-Tab. See #isShiftKeyDown. + tabDirection = tabbed; + tabDirectionUntil = System.currentTimeMillis() + 1000; + keyPressed(KEY_CODE_TAB); + keyReleased(KEY_CODE_TAB); + } return; } // Mirror the native control's text into the field as the user @@ -2563,12 +2851,71 @@ public int getBackKeyCode() { @Override public int getGameAction(int keyCode) { - return 0; + switch (keyCode) { + case GAME_KEY_CODE_UP: + return Display.GAME_UP; + case GAME_KEY_CODE_DOWN: + return Display.GAME_DOWN; + case GAME_KEY_CODE_LEFT: + return Display.GAME_LEFT; + case GAME_KEY_CODE_RIGHT: + return Display.GAME_RIGHT; + default: + return 0; + } } @Override public int getKeyCode(int gameAction) { - return 0; + switch (gameAction) { + case Display.GAME_UP: + return GAME_KEY_CODE_UP; + case Display.GAME_DOWN: + return GAME_KEY_CODE_DOWN; + case Display.GAME_LEFT: + return GAME_KEY_CODE_LEFT; + case Display.GAME_RIGHT: + return GAME_KEY_CODE_RIGHT; + default: + return 0; + } + } + + // The codes the arrow keys are handed to Codename One as. They are + // negative, as on the JavaSE port: a positive code is a character, and + // a form, a list and everything else that moves on an arrow asks + // getGameAction what a negative one means. + private static final int GAME_KEY_CODE_UP = -91; + private static final int GAME_KEY_CODE_DOWN = -92; + private static final int GAME_KEY_CODE_LEFT = -93; + private static final int GAME_KEY_CODE_RIGHT = -94; + + /// The Codename One code of a key the window reported. + /// + /// The window hands over the character a key types, and for a key + /// that types none its GDK key value. The arrows are such keys, and + /// their values (0xff51 to 0xff54, and the keypad's 0xff96 to 0xff99) + /// were passed on as they came: getGameAction answered 0 for + /// everything, so no arrow key did anything in any application -- a + /// form did not move its focus, and a game played with the arrows did + /// not play -- while a text component read the value as a character. + static int keyCodeOf(int key) { + switch (key) { + case 0xff51: + case 0xff96: + return GAME_KEY_CODE_LEFT; + case 0xff52: + case 0xff97: + return GAME_KEY_CODE_UP; + case 0xff53: + case 0xff98: + return GAME_KEY_CODE_RIGHT; + case 0xff54: + case 0xff99: + return GAME_KEY_CODE_DOWN; + default: + return key; + } } /* ------------------------------------------------------------ network */ @@ -3289,6 +3636,17 @@ public boolean isDesktop() { /// JavaSEPort and the macOS port reported modifier state before this. @Override public boolean isShiftKeyDown() { + if (tabDirection != 0) { + // Answered once, for the Tab a native editor ended its edit with: the + // form asks this when it handles that key, and by then the modifiers + // the window recorded belong to whatever key came last -- a Tab + // released after Shift reads as a plain Tab and moved focus forwards. + int direction = tabDirection; + tabDirection = 0; + if (System.currentTimeMillis() <= tabDirectionUntil) { + return direction == 2; + } + } return (LinuxNative.currentModifiers() & 1) != 0; } @@ -3308,6 +3666,12 @@ public boolean isAltKeyDown() { return (LinuxNative.currentModifiers() & 4) != 0; } + /// The direction of the Tab that ended a native edit -- 1 forward, 2 back + /// (Shift-Tab), 0 none -- for the one [#isShiftKeyDown] that asks about it, + /// and the time past which it is stale. Both on the EDT only. + private int tabDirection; + private long tabDirectionUntil; + @Override public int[] getNativeWindowTitleBarInsets(com.codename1.ui.Component toolbar) { if (!(toolbar.getTopLevelContainer() instanceof com.codename1.ui.Form) @@ -3741,6 +4105,27 @@ public String getLongMonthName(Date date) { } return LONG_MONTHS[m]; } + + /* + * The short date and the time of day are the locale's own. The + * base class answers Date.toString() for both, which is neither + * short nor a date alone: a date Picker -- and so a JavaFX + * DatePicker or anything else that shows a day -- read + * "February 29, 2024 12:00:00 PM" where every other port shows + * 2/29/2024. An empty answer means the C library could not + * write the moment, and the old text is better than none. + */ + @Override + public String formatDateShortStyle(Date d) { + String s = LinuxNative.formatLocalTime(d.getTime(), 0); + return s == null || s.length() == 0 ? super.formatDateShortStyle(d) : s; + } + + @Override + public String formatTime(Date d) { + String s = LinuxNative.formatLocalTime(d.getTime(), 1); + return s == null || s.length() == 0 ? super.formatTime(d) : s; + } }; } return l10n; diff --git a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxNative.java b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxNative.java index 361da314a11..a2f56d496c5 100644 --- a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxNative.java +++ b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxNative.java @@ -241,6 +241,15 @@ public static native int desktopWindowCreate(int windowId, String title, int x, public static native boolean mainWindowGetBounds(int[] out); + /// Resizes the application's MAIN window to an outer size, frame included. + /// A no-op when headless, where the surface is the size of the screenshot. + public static native void mainWindowSetSize(int width, int height); + + /// Fills `out` with the main window's outer rectangle, the one + /// `#mainWindowSetSize(int, int)` sets; `#mainWindowGetBounds(int[])` answers + /// the client area. False when there is no window. + public static native boolean mainWindowGetFrame(int[] out); + public static native int desktopWindowGetWidth(int slot); public static native int desktopWindowGetHeight(int slot); @@ -488,6 +497,18 @@ public static native void accessibilityNode(long id, long parentId, String role, public static native long getImageGraphics(long image); + /// Queues an image for disposal. Safe from any thread: this is what an image + /// handle's finalizer calls, on the collector's thread. Nothing is freed here. + public static native void releaseImage(long image); + + /// The next queued image, or 0 when the queue is empty. + public static native long nextReleasedImage(); + + /// Destroys an image's surface and the graphics context cached for it, and + /// returns that context's pointer (0 if it never had one) so the caller can + /// forget what it kept under it. Drawing thread only. + public static native long disposeImage(long image); + /* ------------------------------------------------------- filesystem */ /** @@ -517,6 +538,9 @@ public static native long editStringAt(int x, int y, int w, int h, String text, /** True once the user has committed the native edit (Enter / focus loss). */ public static native boolean editIsDone(long peer); + /** How Tab ended the native edit: 1 forwards, 2 backwards, 0 when it did not. */ + public static native int editTabbed(long peer); + /** The native EDIT control's current text. */ public static native String editGetText(long peer); @@ -530,6 +554,13 @@ public static native long editStringAt(int x, int y, int w, int h, String text, /** Why the most recent open failed, for the exception the port raises. */ public static native String lastIoError(); + /** + * A moment as the user's locale writes a date ({@code what == 0}) or a time + * of day ({@code what == 1}), in the local time zone; empty when it cannot + * be written. + */ + public static native String formatLocalTime(long millis, int what); + /* ---------------------------------------------------------- crypto */ /** Fills {@code out} with fresh entropy; false when the platform RNG failed. */ diff --git a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxWindowManager.java b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxWindowManager.java index 14468e52ca2..8864a9477d2 100644 --- a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxWindowManager.java +++ b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxWindowManager.java @@ -463,7 +463,7 @@ public Object capture(Object peer) { if (img == 0) { return null; } - return Long.valueOf(img); + return LinuxImplementation.wrapImage(img, 0, 0); } @Override diff --git a/Ports/WindowsPort/nativeSources/cn1_windows.h b/Ports/WindowsPort/nativeSources/cn1_windows.h index 90216898f93..1050d7a39d8 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows.h +++ b/Ports/WindowsPort/nativeSources/cn1_windows.h @@ -179,6 +179,13 @@ typedef struct CN1Graphics { JAVA_BOOLEAN clipIsRect; void* clipGeom; /* ID2D1PathGeometry* when clip is a shape, else NULL */ void* clipLayer; /* ID2D1Layer* pushed for a shape clip, else NULL */ + JAVA_BOOLEAN clipGeomHasRect; /* a clipRect narrowed the shape clip: clipX/Y/W/H is + * then a rectangle the shape is ALSO cut to. False + * right after setClipShape, when clipX/Y/W/H is still + * the rectangle that was the clip before the shape + * and is not part of the clip at all */ + JAVA_BOOLEAN clipRectPushed; /* that rectangle was pushed under the shape's layer + * and cn1WinPopClip has to pop it as well */ D2D1_MATRIX_3X2_F clipMaskTransform; /* world transform captured at setClipShape time; * applied to the (raw) clip geometry as the layer * maskTransform so a shape clip lands exactly where diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_graphics.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_graphics.cpp index 3c610c22797..b9569326b8f 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows_graphics.cpp +++ b/Ports/WindowsPort/nativeSources/cn1_windows_graphics.cpp @@ -84,6 +84,8 @@ CN1Graphics* cn1WinCreateGraphics(ID2D1RenderTarget* target) { g->clipIsRect = JAVA_TRUE; g->clipGeom = NULL; g->clipLayer = NULL; + g->clipGeomHasRect = JAVA_FALSE; + g->clipRectPushed = JAVA_FALSE; g->clipMaskTransform = D2D1::Matrix3x2F::Identity(); g->font = cn1Win.defaultFont; g->inFrame = JAVA_FALSE; @@ -179,6 +181,22 @@ extern "C" void cn1WinPushClip(CN1Graphics* g) { * the fill) -- so the clip stays fixed while the fill transforms within it. */ if (!g->clipIsRect && g->clipGeom != NULL) { ID2D1Layer* layer = NULL; + /* A clipRect that came after the shape: the clip is the shape AND that + * rectangle (see clipRect below). The rectangle goes on first, under + * identity for the reason given at the end of this function, and the + * shape's layer inside it; cn1WinPopClip takes them off in reverse. */ + if (g->clipGeomHasRect) { + D2D1_RECT_F rc; + D2D1_MATRIX_3X2_F idr = D2D1::Matrix3x2F::Identity(); + rc.left = (FLOAT)g->clipX; + rc.top = (FLOAT)g->clipY; + rc.right = (FLOAT)(g->clipX + g->clipW); + rc.bottom = (FLOAT)(g->clipY + g->clipH); + ID2D1RenderTarget_SetTransform(g->target, &idr); + ID2D1RenderTarget_PushAxisAlignedClip(g->target, &rc, D2D1_ANTIALIAS_MODE_PER_PRIMITIVE); + ID2D1RenderTarget_SetTransform(g->target, &g->transform); + g->clipRectPushed = JAVA_TRUE; + } if (SUCCEEDED(ID2D1RenderTarget_CreateLayer(g->target, NULL, &layer)) && layer != NULL) { D2D1_LAYER_PARAMETERS lp; lp.contentBounds = D2D1::InfiniteRect(); @@ -253,6 +271,20 @@ extern "C" void cn1WinPopClip(CN1Graphics* g) { ID2D1RenderTarget_PopLayer(g->target); ID2D1Layer_Release((ID2D1Layer*) g->clipLayer); g->clipLayer = NULL; + if (g->clipRectPushed) { + ID2D1RenderTarget_PopAxisAlignedClip(g->target); + g->clipRectPushed = JAVA_FALSE; + } + return; + } + if (g->clipRectPushed) { + /* The shape's layer could not be made; its rectangle was still pushed. */ + ID2D1RenderTarget_PopAxisAlignedClip(g->target); + g->clipRectPushed = JAVA_FALSE; + return; + } + if (!g->clipIsRect && g->clipGeom != NULL) { + /* A shape clip whose layer could not be made pushed nothing at all. */ return; } ID2D1RenderTarget_PopAxisAlignedClip(g->target); @@ -335,6 +367,7 @@ JAVA_VOID com_codename1_impl_windows_WindowsNative_setClip___long_int_int_int_in g->clipW = __cn1Arg4; g->clipH = __cn1Arg5; g->clipIsRect = JAVA_TRUE; + g->clipGeomHasRect = JAVA_FALSE; cn1WinReleaseClipGeom(g); } @@ -375,12 +408,33 @@ JAVA_VOID com_codename1_impl_windows_WindowsNative_setClipShape___long_float_1AR g->clipGeom = cn1WinBuildPathGeometry(coords, types, __cn1Arg4, __cn1Arg5); g->clipMaskTransform = g->transform; /* freeze the world transform at setClip time */ g->clipIsRect = (g->clipGeom == NULL) ? JAVA_TRUE : JAVA_FALSE; - /* Keep clipX/Y/W/H as the geometry's bounding box so getClip* stays sane. */ + /* clipX/Y/W/H is left as it was: the rectangle that was the clip BEFORE the + * shape replaced it. It is what getClip* goes on answering, and it is not + * part of the clip until a clipRect says so. */ + g->clipGeomHasRect = JAVA_FALSE; } JAVA_VOID com_codename1_impl_windows_WindowsNative_clipRect___long_int_int_int_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG __cn1Arg1, JAVA_INT __cn1Arg2, JAVA_INT __cn1Arg3, JAVA_INT __cn1Arg4, JAVA_INT __cn1Arg5) { /* Intersect the requested rectangle with the current clip. */ CN1Graphics* g = (CN1Graphics*)__cn1Arg1; + /* A shape clip is narrowed, not thrown away. This used to end in + * clipIsRect = JAVA_TRUE and a released geometry whatever the clip was, so + * "setClip(shape); clipRect(the clip that was there)" -- which is how + * Graphics.fillShape confines a Paint to the shape it fills, and how any + * caller narrows the clip to a shape without widening it -- left only the + * rectangle that had been the clip BEFORE the shape: a gradient meant for a + * small rounded rectangle covered everything from the panel's corner to the + * end of the gradient's band. The first clipRect after the shape supplies + * the rectangle outright, since clipX/Y/W/H still holds the replaced clip + * then; later ones intersect with it as they always did. */ + if (!g->clipIsRect && g->clipGeom != NULL && !g->clipGeomHasRect) { + g->clipX = __cn1Arg2; + g->clipY = __cn1Arg3; + g->clipW = (__cn1Arg4 > 0) ? __cn1Arg4 : 0; + g->clipH = (__cn1Arg5 > 0) ? __cn1Arg5 : 0; + g->clipGeomHasRect = JAVA_TRUE; + return; + } JAVA_INT curRight = g->clipX + g->clipW; JAVA_INT curBottom = g->clipY + g->clipH; JAVA_INT reqRight = __cn1Arg2 + __cn1Arg4; @@ -395,6 +449,9 @@ JAVA_VOID com_codename1_impl_windows_WindowsNative_clipRect___long_int_int_int_i g->clipY = newY; g->clipW = (newRight > newX) ? (newRight - newX) : 0; g->clipH = (newBottom > newY) ? (newBottom - newY) : 0; + if (!g->clipIsRect && g->clipGeom != NULL) { + return; /* the shape stays, cut to the narrower rectangle */ + } g->clipIsRect = JAVA_TRUE; cn1WinReleaseClipGeom(g); } diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_text.c b/Ports/WindowsPort/nativeSources/cn1_windows_text.c index 8bf6d5f91b5..c82bf1f18fe 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows_text.c +++ b/Ports/WindowsPort/nativeSources/cn1_windows_text.c @@ -324,13 +324,19 @@ JAVA_LONG com_codename1_impl_windows_WindowsNative_loadTrueTypeFontFromMemory___ /* * deriveTrueTypeFont produces a new peer at the requested pixel size and weight, * reusing the base font's family. weight carries the CN1 STYLE_* bits. + * + * The base font's own bold and slant are kept. A "native:MainBold" font is + * bold by its name, and every caller that only wants another size of it asks + * with a plain weight: taking the weight alone gave back regular Segoe UI, so + * no sized native font was ever bold or italic. */ JAVA_LONG com_codename1_impl_windows_WindowsNative_deriveTrueTypeFont___long_float_int_R_long( CODENAME_ONE_THREAD_STATE, JAVA_LONG __cn1Arg1, JAVA_FLOAT __cn1Arg2, JAVA_INT __cn1Arg3) { CN1Font* base = (CN1Font*) (intptr_t) __cn1Arg1; const wchar_t* family = (base != NULL && base->family != NULL) ? base->family : L"Segoe UI"; float px = __cn1Arg2 > 0.0f ? __cn1Arg2 : 15.0f; - CN1Font* font = cn1WinMakeFont(family, px, 0, __cn1Arg3); + int style = __cn1Arg3 | (base != NULL ? base->style : 0); + CN1Font* font = cn1WinMakeFont(family, px, 0, style); return (JAVA_LONG) (intptr_t) font; } diff --git a/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java b/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java index 4c955bcac31..d4e3f32dc32 100644 --- a/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java +++ b/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java @@ -189,9 +189,18 @@ private String storagePath(String name) { return WindowsNative.storageDir() + getFileSystemSeparator() + name; } + /// The native path of a `file://` URL. This port writes `file://C:/x`, + /// but the standard form of the same URL is `file:///C:/x` -- which is + /// what `java.io.File` hands over for an absolute path -- and its leading + /// slash makes `/C:/x`, a name Windows refuses (error 123). private static String stripFileUrl(String path) { if (path.startsWith("file://")) { - return path.substring("file://".length()); + String p = path.substring("file://".length()); + if (p.length() > 2 && p.charAt(0) == '/' && p.charAt(2) == ':' + && Character.isLetter(p.charAt(1))) { + return p.substring(1); + } + return p; } return path; } @@ -223,6 +232,9 @@ private static byte[] readFully(InputStream i) throws IOException { @Override public void init(Object m) { + // The class library (vm/JavaAPI) reaches files, preferences, the + // network and the EDT through Codename One from here on. + com.codename1.impl.CoreJdkHostInstaller.install(); WindowsNative.initDisplay("Codename One", 800, 600); windowGraphicsPeer = WindowsNative.getWindowGraphics(); windowGraphics = Long.valueOf(windowGraphicsPeer); diff --git a/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java b/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java index e4a2a2abdf4..a702b0fe607 100644 --- a/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java +++ b/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java @@ -256,6 +256,9 @@ protected void initDefaultUserAgent() { public void init(Object m) { + // The class library (vm/JavaAPI) reaches files, preferences, the + // network and the EDT through Codename One from here on. + com.codename1.impl.CoreJdkHostInstaller.install(); instance = this; // Set the metalRendering static gate as early as possible -- before // any NativeImage / NativeGraphics is constructed -- so mutable-image diff --git a/docs/demos/common/src/main/snippets/developer-guide/desktop-interop.properties b/docs/demos/common/src/main/snippets/developer-guide/desktop-interop.properties new file mode 100644 index 00000000000..49e7e44b0ba --- /dev/null +++ b/docs/demos/common/src/main/snippets/developer-guide/desktop-interop.properties @@ -0,0 +1,5 @@ +// Generated from docs/developer-guide source blocks. Edit the guide snippets here, not inline. + +// tag::desktop-interop-properties-003[] +codename1.arg.desktop.themeMode=native +// end::desktop-interop-properties-003[] diff --git a/docs/demos/common/src/main/snippets/developer-guide/desktop-interop.sh b/docs/demos/common/src/main/snippets/developer-guide/desktop-interop.sh new file mode 100644 index 00000000000..238c7dbd0e9 --- /dev/null +++ b/docs/demos/common/src/main/snippets/developer-guide/desktop-interop.sh @@ -0,0 +1,66 @@ +// Generated from docs/developer-guide source blocks. Edit the guide snippets here, not inline. + +// tag::desktop-interop-bash-001[] +mvn cn1:import-desktop-project -Dcn1.desktop.import=/path/to/MyDesktopApp +// end::desktop-interop-bash-001[] + +// tag::desktop-interop-bash-002[] +mvn cn1:import-desktop-project -Dcn1.desktop.import=/path/to/MyDesktopApp \ + -Dcn1.desktop.module=app \ + -Dcn1.desktop.mainClass=com.example.Main +// end::desktop-interop-bash-002[] + +// tag::desktop-interop-bash-003[] +mvn cn1:run +mvn cn1:build -Dcodename1.platform=ios +// end::desktop-interop-bash-003[] + +// tag::desktop-interop-bash-004[] +./gradlew run # the simulator +./gradlew buildIos # an iOS build on the build server +./gradlew buildLinuxDevice +// end::desktop-interop-bash-004[] + +// tag::desktop-interop-bash-005[] +mvn cn1:run # the simulator +mvn cn1:buildIos # iOS, on the build server +mvn cn1:buildAndroid # Android, on the build server +mvn cn1:buildJavascript # the JavaScript port +mvn package -Dcodename1.platform=ios -Dcodename1.buildTarget=ios-source # an Xcode project +mvn package -Dcodename1.platform=android -Dcodename1.buildTarget=android-source # an Android Studio project +// end::desktop-interop-bash-005[] + +// tag::desktop-interop-bash-006[] +# Linux: a native executable, built on a Linux machine +mvn package -Dcodename1.platform=linux -Dcodename1.buildTarget=local-linux-device + +# macOS: a native .app, built on a Mac with Xcode +mvn package -Dcodename1.platform=ios -Dcodename1.buildTarget=local-mac-device + +# Windows: a native .exe, built on Windows or cross-compiled +mvn package -Dcodename1.platform=win -Dcodename1.buildTarget=local-windows-device +// end::desktop-interop-bash-006[] + +// tag::desktop-interop-bash-007[] +mvn package -Dcn1.desktop.report=port.md # Maven +./gradlew build -Pcn1.desktop.report=port.md # Gradle +// end::desktop-interop-bash-007[] + +// tag::desktop-interop-bash-008[] +# From a checkout of the Codename One sources, against a built application: +xvfb-run -a maven/integration-tests/desktop-compat-realport/run.sh \ + /path/to/MyApp/common /path/to/orders.txt /path/to/output +// end::desktop-interop-bash-008[] + +// tag::desktop-interop-bash-009[] +scripts/desktop-compat-benchmarks/bench.sh --app /path/to/MyDesktopApp --name myapp \ + --kind swing --main com.example.Main --work /path/to/work +scripts/desktop-compat-benchmarks/report.py /path/to/work/myapp/result.json +// end::desktop-interop-bash-009[] + +// tag::desktop-interop-bash-010[] +scripts/desktop-compat-benchmarks/mobile-size.sh --target ios \ + --project /path/to/work/myapp/cn1/app --work /path/to/work/myapp-ios +scripts/desktop-compat-benchmarks/mobile-size.sh --target android \ + --project /path/to/work/myapp/cn1/app --work /path/to/work/myapp-android +// end::desktop-interop-bash-010[] diff --git a/docs/demos/common/src/main/snippets/developer-guide/desktop-interop.xml b/docs/demos/common/src/main/snippets/developer-guide/desktop-interop.xml new file mode 100644 index 00000000000..fc6f11c2241 --- /dev/null +++ b/docs/demos/common/src/main/snippets/developer-guide/desktop-interop.xml @@ -0,0 +1,10 @@ +// Generated from docs/developer-guide source blocks. Edit the guide snippets here, not inline. + +// tag::desktop-interop-xml-001[] + + com.example + swing-widgets + 1.0 + compile + +// end::desktop-interop-xml-001[] diff --git a/docs/demos/desktop-interop/pom.xml b/docs/demos/desktop-interop/pom.xml new file mode 100644 index 00000000000..6debdbe2051 --- /dev/null +++ b/docs/demos/desktop-interop/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + com.codenameone.developerguide + democode + 1.0-SNAPSHOT + + desktopinteropsnippets + desktopinteropsnippets + + + + com.codenameone + codenameone-core + ${cn1.version} + provided + + + com.codenameone + codenameone-swing-compat + ${cn1.version} + provided + + + com.codenameone + codenameone-javafx-compat + ${cn1.version} + provided + + + diff --git a/docs/demos/desktop-interop/src/main/java/com/codenameone/developerguide/desktopinterop/javafx/CounterApp.java b/docs/demos/desktop-interop/src/main/java/com/codenameone/developerguide/desktopinterop/javafx/CounterApp.java new file mode 100644 index 00000000000..9b613d6d98d --- /dev/null +++ b/docs/demos/desktop-interop/src/main/java/com/codenameone/developerguide/desktopinterop/javafx/CounterApp.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codenameone.developerguide.desktopinterop.javafx; + +// tag::desktopInteropJavaFxApp[] +import javafx.application.Application; +import javafx.beans.binding.Bindings; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.VBox; +import javafx.stage.Stage; + +public class CounterApp extends Application { + private final IntegerProperty count = new SimpleIntegerProperty(); + + @Override + public void start(Stage stage) { + Label label = new Label(); + label.textProperty().bind(Bindings.concat("Pressed ", count.asString(), " times")); + + Button press = new Button("Press"); + press.setOnAction(e -> count.set(count.get() + 1)); + + VBox root = new VBox(12, label, press); + root.setAlignment(Pos.CENTER); + root.setPadding(new Insets(16)); + + stage.setTitle("Counter"); + stage.setScene(new Scene(root, 320, 200)); + stage.show(); + } + + public static void main(String[] args) { + launch(args); + } +} +// end::desktopInteropJavaFxApp[] diff --git a/docs/demos/desktop-interop/src/main/java/com/codenameone/developerguide/desktopinterop/javafx/EmbeddedChart.java b/docs/demos/desktop-interop/src/main/java/com/codenameone/developerguide/desktopinterop/javafx/EmbeddedChart.java new file mode 100644 index 00000000000..d6e76e857e8 --- /dev/null +++ b/docs/demos/desktop-interop/src/main/java/com/codenameone/developerguide/desktopinterop/javafx/EmbeddedChart.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codenameone.developerguide.desktopinterop.javafx; + +import com.codename1.fxcompat.FxInterop; +import com.codename1.ui.Form; +import com.codename1.ui.layouts.BorderLayout; + +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.VBox; +import javafx.stage.Stage; + +/// A Codename One screen that shows a JavaFX scene graph, and a stage opened +/// from a Codename One application. +public class EmbeddedChart { + + public void show() { + // tag::desktopInteropEmbedFx[] + VBox root = new VBox(8, new Label("Orders this week"), new Button("Refresh")); + Scene scene = new Scene(root); + scene.getStylesheets().add("/com/example/orders.css"); + + Form form = new Form("Report", new BorderLayout()); + form.add(BorderLayout.CENTER, FxInterop.asComponent(scene)); + form.show(); + // end::desktopInteropEmbedFx[] + } + + public void openEditor(VBox editor) { + // tag::desktopInteropFxWindows[] + Stage stage = new Stage(); + stage.setTitle("Edit order"); + stage.setScene(new Scene(editor)); + stage.show(); // closing it returns to the form that was showing + // end::desktopInteropFxWindows[] + } +} diff --git a/docs/demos/desktop-interop/src/main/java/com/codenameone/developerguide/desktopinterop/javafx/GreeterController.java b/docs/demos/desktop-interop/src/main/java/com/codenameone/developerguide/desktopinterop/javafx/GreeterController.java new file mode 100644 index 00000000000..e2cec28fa40 --- /dev/null +++ b/docs/demos/desktop-interop/src/main/java/com/codenameone/developerguide/desktopinterop/javafx/GreeterController.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codenameone.developerguide.desktopinterop.javafx; + +// tag::desktopInteropFxmlController[] +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; + +public class GreeterController { + @FXML + private Label status; + + @FXML + private TextField name; + + @FXML + private Button greet; + + @FXML + private void initialize() { + greet.disableProperty().bind(name.textProperty().isEmpty()); + } + + @FXML + private void greet(ActionEvent event) { + status.setText("Hello, " + name.getText() + "!"); + } +} +// end::desktopInteropFxmlController[] diff --git a/docs/demos/desktop-interop/src/main/java/com/codenameone/developerguide/desktopinterop/javafx/GreeterFxApp.java b/docs/demos/desktop-interop/src/main/java/com/codenameone/developerguide/desktopinterop/javafx/GreeterFxApp.java new file mode 100644 index 00000000000..a268ef531bd --- /dev/null +++ b/docs/demos/desktop-interop/src/main/java/com/codenameone/developerguide/desktopinterop/javafx/GreeterFxApp.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codenameone.developerguide.desktopinterop.javafx; + +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.stage.Stage; + +/// A JavaFX application whose screen is an FXML document styled by a +/// style sheet. +public class GreeterFxApp extends Application { + + // tag::desktopInteropFxmlLoad[] + @Override + public void start(Stage stage) throws Exception { + Parent root = FXMLLoader.load(getClass().getResource("greeter.fxml")); + Scene scene = new Scene(root); + scene.getStylesheets().add(getClass().getResource("greeter.css").toExternalForm()); + stage.setTitle("Greeter"); + stage.setScene(scene); + stage.show(); + } + // end::desktopInteropFxmlLoad[] + + public static void main(String[] args) { + launch(args); + } +} diff --git a/docs/demos/desktop-interop/src/main/java/com/codenameone/developerguide/desktopinterop/swing/BadgePanel.java b/docs/demos/desktop-interop/src/main/java/com/codenameone/developerguide/desktopinterop/swing/BadgePanel.java new file mode 100644 index 00000000000..e49582dac07 --- /dev/null +++ b/docs/demos/desktop-interop/src/main/java/com/codenameone/developerguide/desktopinterop/swing/BadgePanel.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codenameone.developerguide.desktopinterop.swing; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; + +import javax.swing.JPanel; + +/// A panel that paints itself: a disc with the first letter of a name. +// tag::desktopInteropSwingPaint[] +public class BadgePanel extends JPanel { + private String initial = "?"; + + public void setInitial(String name) { + initial = name == null || name.length() == 0 ? "?" : name.substring(0, 1); + repaint(); + } + + @Override + public Dimension getPreferredSize() { + return new Dimension(120, 120); + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + Graphics2D g2 = (Graphics2D) g; + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + int size = Math.min(getWidth(), getHeight()) - 16; + int x = (getWidth() - size) / 2; + int y = (getHeight() - size) / 2; + g2.setColor(new Color(0x2f6fed)); + g2.fillOval(x, y, size, size); + g2.setColor(Color.WHITE); + g2.setFont(getFont().deriveFont(size / 2f)); + FontMetrics metrics = g2.getFontMetrics(); + g2.drawString(initial, (getWidth() - metrics.stringWidth(initial)) / 2, + (getHeight() - metrics.getHeight()) / 2 + metrics.getAscent()); + } +} +// end::desktopInteropSwingPaint[] diff --git a/docs/demos/desktop-interop/src/main/java/com/codenameone/developerguide/desktopinterop/swing/CompactLayout.java b/docs/demos/desktop-interop/src/main/java/com/codenameone/developerguide/desktopinterop/swing/CompactLayout.java new file mode 100644 index 00000000000..85cd58b62b4 --- /dev/null +++ b/docs/demos/desktop-interop/src/main/java/com/codenameone/developerguide/desktopinterop/swing/CompactLayout.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codenameone.developerguide.desktopinterop.swing; + +import javax.swing.JSplitPane; + +import com.codename1.ui.CN; + +/// Swing code that asks Codename One what kind of device it is running on. +public class CompactLayout { + + public void apply(JSplitPane split) { + // tag::desktopInteropCodenameOneApi[] + if (!CN.isDesktop() && !CN.isTablet()) { + // A phone: stack the two halves instead of placing them side by side. + split.setOrientation(JSplitPane.VERTICAL_SPLIT); + } + // end::desktopInteropCodenameOneApi[] + } +} diff --git a/docs/demos/desktop-interop/src/main/java/com/codenameone/developerguide/desktopinterop/swing/DeviceSettings.java b/docs/demos/desktop-interop/src/main/java/com/codenameone/developerguide/desktopinterop/swing/DeviceSettings.java new file mode 100644 index 00000000000..1b2f841d696 --- /dev/null +++ b/docs/demos/desktop-interop/src/main/java/com/codenameone/developerguide/desktopinterop/swing/DeviceSettings.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codenameone.developerguide.desktopinterop.swing; + +import javax.swing.JFrame; +import javax.swing.JLabel; + +import com.codename1.ui.CN; + +/// A Swing entry point that adjusts how the layer maps it onto the device +/// before it shows anything. +public class DeviceSettings { + + public static void main(String[] args) { + // tag::desktopInteropDisplayProperties[] + if (!CN.isDesktop()) { + // Fit a little more of a wide form on a phone. + CN.setProperty("desktopcompat.scale", "1.6"); + // Keep the menu bar in the window instead of the overflow menu. + CN.setProperty("desktopcompat.menuBar", "window"); + } + // A fixed width font the application ships among its resources. + CN.setProperty("desktopcompat.monospaced", "DejaVu Sans Mono|DejaVuSansMono.ttf"); + + JFrame frame = new JFrame("Orders"); + // end::desktopInteropDisplayProperties[] + frame.add(new JLabel("Orders")); + frame.setVisible(true); + } +} diff --git a/docs/demos/desktop-interop/src/main/java/com/codenameone/developerguide/desktopinterop/swing/EmbeddedReport.java b/docs/demos/desktop-interop/src/main/java/com/codenameone/developerguide/desktopinterop/swing/EmbeddedReport.java new file mode 100644 index 00000000000..b26a8353b27 --- /dev/null +++ b/docs/demos/desktop-interop/src/main/java/com/codenameone/developerguide/desktopinterop/swing/EmbeddedReport.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codenameone.developerguide.desktopinterop.swing; + +import java.awt.BorderLayout; + +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; + +import com.codename1.desktopcompat.SwingInterop; +import com.codename1.ui.Form; + +/// A Codename One screen that shows a Swing panel, and Swing windows opened +/// from a Codename One application. +public class EmbeddedReport { + + public void show() { + // tag::desktopInteropEmbedSwing[] + JPanel report = new JPanel(new BorderLayout()); + report.add(new JLabel("Orders this week"), BorderLayout.NORTH); + report.add(new JScrollPane(new JTable(new Object[][] {{"Mon", 12}, {"Tue", 9}}, + new Object[] {"Day", "Orders"})), BorderLayout.CENTER); + + Form form = new Form("Report", new com.codename1.ui.layouts.BorderLayout()); + form.add(com.codename1.ui.layouts.BorderLayout.CENTER, SwingInterop.asComponent(report)); + form.show(); + // end::desktopInteropEmbedSwing[] + } + + public boolean openEditor(JPanel editor) { + // tag::desktopInteropSwingWindows[] + JFrame frame = new JFrame("Edit order"); + frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + frame.setContentPane(editor); + frame.setVisible(true); // closing it returns to the form that was showing + + int answer = JOptionPane.showConfirmDialog(frame, "Save the order?", "Save", + JOptionPane.YES_NO_OPTION); + // end::desktopInteropSwingWindows[] + return answer == JOptionPane.YES_OPTION; + } +} diff --git a/docs/demos/desktop-interop/src/main/java/com/codenameone/developerguide/desktopinterop/swing/ExternalTools.java b/docs/demos/desktop-interop/src/main/java/com/codenameone/developerguide/desktopinterop/swing/ExternalTools.java new file mode 100644 index 00000000000..e5be50b9245 --- /dev/null +++ b/docs/demos/desktop-interop/src/main/java/com/codenameone/developerguide/desktopinterop/swing/ExternalTools.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codenameone.developerguide.desktopinterop.swing; + +import com.codename1.ui.CN; + +/// A feature the desktop application implemented by starting processes, moved +/// behind an interface so that the user interface no longer knows how it is done. +public class ExternalTools { + + // tag::desktopInteropIsolate[] + /// What the screens ask for. They hold one of these and nothing else. + public interface Tools { + /// Whether the menu item that opens a terminal should be there at all. + boolean canOpenTerminal(); + + void openTerminal(String directory); + + void showInFileManager(String path); + } + + /// The implementation the Codename One build uses. The desktop project + /// keeps its own, written with ProcessBuilder, in a class this build + /// doesn't compile. + public static class DeviceTools implements Tools { + @Override + public boolean canOpenTerminal() { + return false; + } + + @Override + public void openTerminal(String directory) { + throw new UnsupportedOperationException("No terminal on this device"); + } + + @Override + public void showInFileManager(String path) { + String url = path.startsWith("file:") ? path : "file://" + path; + Boolean can = CN.canExecute(url); + if (can == null || can.booleanValue()) { + CN.execute(url); + } + } + } + // end::desktopInteropIsolate[] +} diff --git a/docs/demos/desktop-interop/src/main/java/com/codenameone/developerguide/desktopinterop/swing/GreeterApp.java b/docs/demos/desktop-interop/src/main/java/com/codenameone/developerguide/desktopinterop/swing/GreeterApp.java new file mode 100644 index 00000000000..a6804cdfec4 --- /dev/null +++ b/docs/demos/desktop-interop/src/main/java/com/codenameone/developerguide/desktopinterop/swing/GreeterApp.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codenameone.developerguide.desktopinterop.swing; + +// tag::desktopInteropSwingApp[] +import java.awt.BorderLayout; +import java.awt.GridLayout; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.SwingUtilities; + +public class GreeterApp { + + public static void main(String[] args) { + SwingUtilities.invokeLater(GreeterApp::showFrame); + } + + private static void showFrame() { + JFrame frame = new JFrame("Greeter"); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + JTextField name = new JTextField(16); + JPanel form = new JPanel(new GridLayout(0, 2, 8, 8)); + form.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12)); + form.add(new JLabel("Name")); + form.add(name); + + BadgePanel badge = new BadgePanel(); + JButton greet = new JButton("Greet"); + greet.addActionListener(e -> { + badge.setInitial(name.getText()); + JOptionPane.showMessageDialog(frame, "Hello, " + name.getText() + "!"); + }); + + frame.getContentPane().add(form, BorderLayout.NORTH); + frame.getContentPane().add(badge, BorderLayout.CENTER); + frame.getContentPane().add(greet, BorderLayout.SOUTH); + frame.pack(); + frame.setVisible(true); + } +} +// end::desktopInteropSwingApp[] diff --git a/docs/demos/desktop-interop/src/main/java/com/codenameone/developerguide/desktopinterop/swing/LoadNames.java b/docs/demos/desktop-interop/src/main/java/com/codenameone/developerguide/desktopinterop/swing/LoadNames.java new file mode 100644 index 00000000000..9ba8ef48c61 --- /dev/null +++ b/docs/demos/desktop-interop/src/main/java/com/codenameone/developerguide/desktopinterop/swing/LoadNames.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codenameone.developerguide.desktopinterop.swing; + +import java.util.ArrayList; +import java.util.List; + +import javax.swing.DefaultListModel; +import javax.swing.JLabel; +import javax.swing.SwingWorker; + +/// Work off the event dispatch thread, with the results delivered on it. +public class LoadNames { + + public void load(DefaultListModel model, JLabel status) { + // tag::desktopInteropSwingWorker[] + new SwingWorker, String>() { + @Override + protected List doInBackground() { + List names = new ArrayList(); + for (String name : new String[] {"Ada", "Grace", "Linus"}) { + names.add(name); + publish(name); + } + return names; + } + + @Override + protected void process(List chunks) { + for (String name : chunks) { + model.addElement(name); + } + } + + @Override + protected void done() { + status.setText(model.getSize() + " names loaded"); + } + }.execute(); + // end::desktopInteropSwingWorker[] + } +} diff --git a/docs/demos/desktop-interop/src/main/resources/com/codenameone/developerguide/desktopinterop/javafx/greeter.css b/docs/demos/desktop-interop/src/main/resources/com/codenameone/developerguide/desktopinterop/javafx/greeter.css new file mode 100644 index 00000000000..5ee5879dc74 --- /dev/null +++ b/docs/demos/desktop-interop/src/main/resources/com/codenameone/developerguide/desktopinterop/javafx/greeter.css @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +/* tag::desktopInteropFxCss[] */ +.root { + -app-accent: #2f6fed; +} + +.card { + -fx-background-color: white; + -fx-border-color: -app-accent; + -fx-border-width: 2; + -fx-border-radius: 8; + -fx-background-radius: 8; +} + +#status { + -fx-text-fill: -app-accent; + -fx-font-size: 18; + -fx-font-weight: bold; +} + +.card > .button:hover { + -fx-opacity: 0.85; +} +/* end::desktopInteropFxCss[] */ diff --git a/docs/demos/desktop-interop/src/main/resources/com/codenameone/developerguide/desktopinterop/javafx/greeter.fxml b/docs/demos/desktop-interop/src/main/resources/com/codenameone/developerguide/desktopinterop/javafx/greeter.fxml new file mode 100644 index 00000000000..ca37a8c2c23 --- /dev/null +++ b/docs/demos/desktop-interop/src/main/resources/com/codenameone/developerguide/desktopinterop/javafx/greeter.fxml @@ -0,0 +1,18 @@ + + + + + + + + + + + + +