diff --git a/.gitignore b/.gitignore index 524f096..78fef29 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,95 @@ +# Created by https://www.toptal.com/developers/gitignore/api/intellij+all,java,gradle +# Edit at https://www.toptal.com/developers/gitignore?templates=intellij+all,java,gradle + +### Intellij+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij+all Patch ### +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. + +.idea/* + +!.idea/codeStyles +!.idea/runConfigurations + +### Java ### # Compiled class file *.class @@ -22,3 +114,32 @@ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* replay_pid* + +### Gradle ### +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +### Gradle Patch ### +# Java heap dump +*.hprof + +# End of https://www.toptal.com/developers/gitignore/api/intellij+all,java,gradle diff --git a/build-logic/README.md b/build-logic/README.md new file mode 100644 index 0000000..f85c92a --- /dev/null +++ b/build-logic/README.md @@ -0,0 +1,4 @@ +# JabRef build logic + +This directory contains gradle instructions for the build. +Initially, it was created by `gradle init` using gradle 8.13. diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts new file mode 100644 index 0000000..d23cc25 --- /dev/null +++ b/build-logic/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + `kotlin-dsl` +} + +repositories { + gradlePluginPortal() +} + +dependencies { + implementation("com.adarshr:gradle-test-logger-plugin:4.0.0") + implementation("com.github.andygoossens:gradle-modernizer-plugin:1.12.0") + implementation("org.gradlex:extra-java-module-info:1.13.1") + implementation("org.gradlex:java-module-dependencies:1.11") + implementation("org.gradlex:java-module-packaging:1.2") + implementation("org.gradlex:java-module-testing:1.8") + implementation("org.gradlex:jvm-dependency-conflict-resolution:2.4") + implementation("org.gradle.toolchains:foojay-resolver:1.0.0") +} diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts new file mode 100644 index 0000000..7fbbd44 --- /dev/null +++ b/build-logic/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "build-logic" diff --git a/build-logic/src/main/kotlin/org.jabref.javafx.controls.gradle.base.dependency-rules.gradle.kts b/build-logic/src/main/kotlin/org.jabref.javafx.controls.gradle.base.dependency-rules.gradle.kts new file mode 100644 index 0000000..69662cc --- /dev/null +++ b/build-logic/src/main/kotlin/org.jabref.javafx.controls.gradle.base.dependency-rules.gradle.kts @@ -0,0 +1,71 @@ +plugins { + id("org.gradlex.extra-java-module-info") + id("org.gradlex.jvm-dependency-conflict-resolution") + id("org.gradlex.java-module-dependencies") // only for mappings at the moment +} + +javaModuleDependencies { + // TODO remove to translate 'requires' from 'module-info.java' to Gradle dependencies + // and remove 'dependencies {}' block from build.gradle files + analyseOnly = true // makes no difference +} + +jvmDependencyConflicts { + consistentResolution { + platform(":versions") + } +} + +// Tell gradle which jar to use for which platform +// Source: https://github.com/jjohannes/java-module-system/blob/be19f6c088dca511b6d9a7487dacf0b715dbadc1/gradle/plugins/src/main/kotlin/metadata-patch.gradle.kts#L14-L22 +jvmDependencyConflicts.patch { + listOf( + "base", + ).forEach { jfxModule -> + module( + "org.openjfx:javafx-$jfxModule" + ) { + addTargetPlatformVariant( + "", + "none", + "none" + ) // matches the empty Jars: to get better errors + addTargetPlatformVariant( + "linux", + OperatingSystemFamily.LINUX, + MachineArchitecture.X86_64 + ) + addTargetPlatformVariant( + "linux-aarch64", + OperatingSystemFamily.LINUX, + MachineArchitecture.ARM64 + ) + addTargetPlatformVariant( + "mac", + OperatingSystemFamily.MACOS, + MachineArchitecture.X86_64 + ) + addTargetPlatformVariant( + "mac-aarch64", + OperatingSystemFamily.MACOS, + MachineArchitecture.ARM64 + ) + addTargetPlatformVariant( + "win", + OperatingSystemFamily.WINDOWS, + MachineArchitecture.X86_64 + ) + } + } +} +extraJavaModuleInfo { + failOnAutomaticModules = true + failOnModifiedDerivedModuleNames = true + skipLocalJars = true + + module("org.openjfx:javafx-base", "javafx.base") { + patchRealModule() + exportAllPackages() + } + +} diff --git a/build-logic/src/main/kotlin/org.jabref.javafx.controls.gradle.base.repositories.gradle.kts b/build-logic/src/main/kotlin/org.jabref.javafx.controls.gradle.base.repositories.gradle.kts new file mode 100644 index 0000000..e98886e --- /dev/null +++ b/build-logic/src/main/kotlin/org.jabref.javafx.controls.gradle.base.repositories.gradle.kts @@ -0,0 +1,13 @@ +repositories { + mavenCentral() + + maven { url = uri("https://central.sonatype.com/repository/maven-snapshots/") } + + // Required for https://github.com/sialcasa/mvvmFX + maven { url = uri("https://jitpack.io") } + + // Required for one.jpro.jproutils:tree-showing + maven { url = uri("https://sandec.jfrog.io/artifactory/repo") } + + maven { url = rootDir.resolve("jablib/lib").toURI() } +} diff --git a/build-logic/src/main/kotlin/org.jabref.javafx.controls.gradle.build.settings.gradle.kts b/build-logic/src/main/kotlin/org.jabref.javafx.controls.gradle.build.settings.gradle.kts new file mode 100644 index 0000000..d07a822 --- /dev/null +++ b/build-logic/src/main/kotlin/org.jabref.javafx.controls.gradle.build.settings.gradle.kts @@ -0,0 +1,7 @@ +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") + id("org.gradlex.java-module-dependencies") +} + +// https://github.com/gradlex-org/java-module-dependencies#plugin-dependency +includeBuild(".") diff --git a/build-logic/src/main/kotlin/org.jabref.javafx.controls.gradle.feature.compile.gradle.kts b/build-logic/src/main/kotlin/org.jabref.javafx.controls.gradle.feature.compile.gradle.kts new file mode 100644 index 0000000..090f125 --- /dev/null +++ b/build-logic/src/main/kotlin/org.jabref.javafx.controls.gradle.feature.compile.gradle.kts @@ -0,0 +1,31 @@ +plugins { + id("java") +} + +java { + toolchain { + // If this is updated, also update + // - build.gradle -> jacoco -> toolVersion (because JaCoCo does not support newest JDK out of the box. Check versions at https://www.jacoco.org/jacoco/trunk/doc/changes.html) + // - jitpack.yml + // - .sdkmanrc + // - .devcontainer/devcontainer.json#L34 - there, also check if the gradleVersion matches the one of gradle/wrapper/gradle-wrapper.properties + // - .moderne/moderne.yml + // - .github/workflows/binaries*.yml + // - .github/workflows/publish.yml + // - .github/workflows/tests*.yml + // - .github/workflows/update-gradle-wrapper.yml + // - .jbang/Jab*.java + // - docs/getting-into-the-code/guidelines-for-setting-up-a-local-workspace/intellij-12-build.md + // - jablib-examples/jbang/*.java + // - jablib-examples/maven3/*/pom.xml + languageVersion = JavaLanguageVersion.of(24) + // See https://docs.gradle.org/current/javadoc/org/gradle/jvm/toolchain/JvmVendorSpec.html for a full list + // Temurin does not ship jmods, thus we need to use another JDK -- see https://github.com/actions/setup-java/issues/804 + // We also need a JDK without JavaFX, because we patch JavaFX due to modularity issues + vendor = JvmVendorSpec.AMAZON + } +} + +tasks.withType().configureEach { + options.release = 24 +} diff --git a/build-logic/src/main/kotlin/org.jabref.javafx.controls.gradle.feature.test.gradle.kts b/build-logic/src/main/kotlin/org.jabref.javafx.controls.gradle.feature.test.gradle.kts new file mode 100644 index 0000000..5d212a6 --- /dev/null +++ b/build-logic/src/main/kotlin/org.jabref.javafx.controls.gradle.feature.test.gradle.kts @@ -0,0 +1,39 @@ +import com.adarshr.gradle.testlogger.theme.ThemeType + +plugins { + id("java") + id("org.gradlex.java-module-testing") + // Hint from https://stackoverflow.com/a/46533151/873282 + id("com.adarshr.test-logger") +} + +testing { + @Suppress("UnstableApiUsage") + suites.named("test") { + useJUnitJupiter() + } +} + +tasks.withType().configureEach { + // Enable parallel tests (on desktop). + // See https://docs.gradle.org/8.1/userguide/performance.html#execute_tests_in_parallel for details. + if (!providers.environmentVariable("CI").isPresent) { + maxParallelForks = maxOf(Runtime.getRuntime().availableProcessors() - 1, 1) + } +} + +testlogger { + // See https://github.com/radarsh/gradle-test-logger-plugin#configuration for configuration options + + theme = ThemeType.STANDARD + + showPassed = false + showSkipped = false + + showCauses = true + showStackTraces = true +} + +configurations.testCompileOnly { + extendsFrom(configurations.compileOnly.get()) +} diff --git a/build-logic/src/main/kotlin/org.jabref.javafx.controls.gradle.module.gradle.kts b/build-logic/src/main/kotlin/org.jabref.javafx.controls.gradle.module.gradle.kts new file mode 100644 index 0000000..9704866 --- /dev/null +++ b/build-logic/src/main/kotlin/org.jabref.javafx.controls.gradle.module.gradle.kts @@ -0,0 +1,8 @@ +plugins { + id("java") + id("project-report") + id("org.jabref.javafx.controls.gradle.base.dependency-rules") + id("org.jabref.javafx.controls.gradle.base.repositories") + id("org.jabref.javafx.controls.gradle.feature.compile") + id("org.jabref.javafx.controls.gradle.feature.test") +} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..60620a9 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,4 @@ +plugins { + id("org.jabref.javafx.controls.gradle.base.repositories") + id("org.jabref.javafx.controls.gradle.feature.compile") +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..f8e1ee3 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..bad7c24 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..adff685 --- /dev/null +++ b/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..c4bdd3a --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/mvvmfx-validation/build.gradle.kts b/mvvmfx-validation/build.gradle.kts new file mode 100644 index 0000000..d0085ed --- /dev/null +++ b/mvvmfx-validation/build.gradle.kts @@ -0,0 +1,64 @@ +import com.vanniktech.maven.publish.JavaLibrary +import com.vanniktech.maven.publish.JavadocJar + +plugins { + id("org.jabref.javafx.controls.gradle.module") + id("java-library") + id("com.vanniktech.maven.publish") version "0.35.0" +} + +var version: String = project.findProperty("projVersion")?.toString() ?: "0.1.0" +if (project.findProperty("tagbuild")?.toString() != "true") { + version += "-SNAPSHOT" +} + + + +dependencies { + implementation("org.openjfx:javafx-base") +} + + +mavenPublishing { + configure( + JavaLibrary( + // configures the -javadoc artifact, possible values: + // - `JavadocJar.None()` don't publish this artifact + // - `JavadocJar.Empty()` publish an emprt jar + // - `JavadocJar.Javadoc()` to publish standard javadocs + javadocJar = JavadocJar.Javadoc(), + // whether to publish a sources jar + sourcesJar = true, + ) + ) + + publishToMavenCentral() + signAllPublications() + + coordinates("org.jabref", "controls", version) + + pom { + name.set("jablib") + description.set("JabRef's Java library to work with BibTeX") + inceptionYear.set("2025") + url.set("https://github.com/JabRef/jabref/") + licenses { + license { + name.set("MIT") + url.set("https://github.com/JabRef/jabref/blob/main/LICENSE") + } + } + developers { + developer { + id.set("jabref") + name.set("JabRef Developers") + url.set("https://github.com/JabRef/") + } + } + scm { + url.set("https://github.com/JabRef/jabref") + connection.set("scm:git:https://github.com/JabRef/jabref") + developerConnection.set("scm:git:git@github.com:JabRef/jabref.git") + } + } +} diff --git a/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/CompositeValidationStatus.java b/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/CompositeValidationStatus.java new file mode 100644 index 0000000..8fedb5e --- /dev/null +++ b/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/CompositeValidationStatus.java @@ -0,0 +1,171 @@ +/******************************************************************************* + * Copyright 2015 Alexander Casall, Manuel Mauky + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ +package de.saxsys.mvvmfx.utils.validation; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * This class is used as {@link ValidationStatus} for {@link CompositeValidator}. + * + * In contrast to the basic {@link ValidationStatus} this class not only tracks + * {@link ValidationMessage} alone but also keeps track of the {@link Validator}s that + * the messages belong to. This is needed to be able to remove only those messages for + * a specific validator. + * + * @author manuel.mauky + */ +class CompositeValidationStatus extends ValidationStatus { + + /** + * The CompositeValidator needs to be able to add and remove {@link ValidationMessage}s for specific validators only. + * + * Because {@link ValidationMessage} is an immutable value type (overrides equals/hashCode for value equality) + * two message instances will be considered to be "equal" when they have the same values even if they are belonging + * to different validators. Simply putting the messages into the message list ({@link #getMessagesInternal()}) + * doesn't work because if a message is removed for one validator, messages with the same values for other validators would be removed too. + *

+ * For this reason we need a special logic here. + * To get this working we will maintain a Map that keeps track of all messages for each validator. + * Instead of using the actual instances of validator as key and messages as values we will use {@link System#identityHashCode(Object)} + * for both. This way we can distinguish between different instances of {@link ValidationMessage} even if + * they are considered to be "equal" by equals/hashCode methods. + * A second benefit of using identityHashCode is that it minimizes the changes of memory leaks because no references to + * actual objects are stored. This is especially important for the validator instance. + *

+ * + * Key: {@link System#identityHashCode(Object)} of the validator + * Values: A list of {@link System#identityHashCode(Object)} of the validation messages. + */ + private final Map> validatorToMessagesMap = new HashMap<>(); + + + /** + * This class is package private and only used in the {@link CompositeValidator}. + * For this use case adding and removing messages is only done in combination with a validator instance. + * For this reason the normal methods to add/remove messages are overridden ad no-op methods. + */ + @Override + void addMessage(ValidationMessage message) { + } + + @Override + void addMessage(Collection messages) { + } + + @Override + void removeMessage(ValidationMessage message) { + } + + @Override + void removeMessage(Collection messages) { + } + + @Override + void clearMessages() { + } + + /** + * Add a list of validation messages for the specified validator. + */ + void addMessage(Validator validator, List messages) { + if(messages.isEmpty()) { + return; + } + + + final int validatorHash = System.identityHashCode(validator); + + if(!validatorToMessagesMap.containsKey(validatorHash)){ + validatorToMessagesMap.put(validatorHash, new ArrayList<>()); + } + + + final List messageHashesOfThisValidator = validatorToMessagesMap.get(validatorHash); + + // add the hashCodes of the messages to the internal map + messages.stream() + .map(System::identityHashCode) + .forEach(messageHashesOfThisValidator::add); + + // add the actual messages to the message list so that they are accessible by the user. + getMessagesInternal().addAll(messages); + } + + /* + Remove all given messages for the given validator. + */ + void removeMessage(final Validator validator, final List messages) { + if(messages.isEmpty()) { + return; + } + + final int validatorHash = System.identityHashCode(validator); + + // if the validator is unknown by the map we haven't stored any messages for it yet that could be removed + if(validatorToMessagesMap.containsKey(validatorHash)){ + final List messageHashesOfThisValidator = validatorToMessagesMap.get(validatorHash); + + + final List hashesOfMessagesToRemove = messages.stream() + .filter(m -> { // only those messages that are stored for this validator + int hash = System.identityHashCode(m); + return messageHashesOfThisValidator.contains(hash); + }) + .map(System::identityHashCode) // we only need the hashCode here + .collect(Collectors.toList()); + + // only remove those messages that we have the hashCode stored + getMessagesInternal().removeIf(message -> { + int hash = System.identityHashCode(message); + return hashesOfMessagesToRemove.contains(hash); + }); + + + // we need to cleanup our internal map + messageHashesOfThisValidator.removeAll(hashesOfMessagesToRemove); + + if(messageHashesOfThisValidator.isEmpty()) { + validatorToMessagesMap.remove(validatorHash); + } + } + } + + /* + * Remove all messages for this particular validator. + */ + void removeMessage(final Validator validator) { + final int validatorHash = System.identityHashCode(validator); + + if(validatorToMessagesMap.containsKey(validatorHash)){ + + final List messageHashesOfThisValidator = validatorToMessagesMap.get(validatorHash); + + getMessagesInternal().removeIf(message -> { + int hash = System.identityHashCode(message); + + return messageHashesOfThisValidator.contains(hash); + }); + + validatorToMessagesMap.remove(validatorHash); + } + } + +} diff --git a/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/CompositeValidator.java b/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/CompositeValidator.java new file mode 100644 index 0000000..9fab2cd --- /dev/null +++ b/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/CompositeValidator.java @@ -0,0 +1,113 @@ +/******************************************************************************* + * Copyright 2015 Alexander Casall, Manuel Mauky + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ +package de.saxsys.mvvmfx.utils.validation; + +import javafx.beans.property.ListProperty; +import javafx.beans.property.SimpleListProperty; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; + +import java.util.HashMap; +import java.util.Map; + +/** + * This {@link Validator} implementation is used to compose multiple other validators. + *

+ * The {@link ValidationStatus} of this validator will contain all messages of all registered validators. + * + * @author manuel.mauky + */ +public class CompositeValidator implements Validator { + + private CompositeValidationStatus status = new CompositeValidationStatus(); + + private ListProperty validators = new SimpleListProperty<>(FXCollections.observableArrayList()); + private Map> listenerMap = new HashMap<>(); + + + public CompositeValidator() { + + validators.addListener(new ListChangeListener() { + @Override + public void onChanged(Change c) { + while (c.next()) { + + // When validators are added... + c.getAddedSubList().forEach(validator -> { + + ObservableList messages = validator.getValidationStatus().getMessages(); + // ... we first add all existing messages to our own validator messages. + status.addMessage(validator, messages); + + final ListChangeListener changeListener = change -> { + while (change.next()) { + // add/remove messages for this particular validator + status.addMessage(validator, change.getAddedSubList()); + status.removeMessage(validator, change.getRemoved()); + } + }; + + validator.getValidationStatus().getMessages().addListener(changeListener); + + // keep a reference to the listener for a specific validator so we can later use + // this reference to remove the listener + listenerMap.put(validator, changeListener); + }); + + + c.getRemoved().forEach(validator -> { + status.removeMessage(validator); + + if (listenerMap.containsKey(validator)) { + ListChangeListener changeListener = listenerMap.get(validator); + + validator.getValidationStatus().getMessages().removeListener(changeListener); + listenerMap.remove(validator); + } + }); + } + } + }); + + } + + public CompositeValidator(Validator... validators) { + this(); // before adding the validators we need to setup the listeners in the default constructor + addValidators(validators); + } + + + /** + * @return an unmodifiable observable list of validators composed by this CompositeValidator. + */ + public ObservableList getValidators() { + return FXCollections.unmodifiableObservableList(this.validators); + } + + public void addValidators(Validator... validators) { + this.validators.addAll(validators); + } + + public void removeValidators(Validator... validators) { + this.validators.removeAll(validators); + } + + @Override + public ValidationStatus getValidationStatus() { + return status; + } +} diff --git a/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/FunctionBasedValidator.java b/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/FunctionBasedValidator.java new file mode 100644 index 0000000..3564b5b --- /dev/null +++ b/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/FunctionBasedValidator.java @@ -0,0 +1,110 @@ +/******************************************************************************* + * Copyright 2015 Alexander Casall, Manuel Mauky + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ +package de.saxsys.mvvmfx.utils.validation; + +import javafx.beans.value.ObservableValue; + +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; + + +/** + * This {@link Validator} implementation uses functions to validate the values of an observable. You need to define a + * observable value as source that contains the value that should be validated. + *

+ * There are two "flavours" of using this validator: Using a {@link Predicate} or a {@link Function} for validation. + *

+ * The variant with Predicate is used for simple use cases where you provide a predicate that simply tells the + * validator, if the given value is valid or not. If it is invalid, the given {@link ValidationMessage} will be present + * in the {@link ValidationStatus} of this validator. + *

+ * The variant with Function is used for use cases where different messages should be shown under specific conditions. + * Instead of only returning true or false the function has to return a + * {@link ValidationMessage} for a given input value if it is invalid. The returned message will then be present in the + * validation status. If the input value is valid and therefore no validation message should be shown, the function has + * to return null instead. + * + *

+ *
+ * For more complex validation logic like cross field validation you can use the {@link ObservableRuleBasedValidator} as + * an alternative. + * + * @param + * the generic value of the source observable. + */ +public class FunctionBasedValidator implements Validator { + + private ValidationStatus validationStatus = new ValidationStatus(); + + private Function> validateFunction; + + private FunctionBasedValidator(ObservableValue source) { + source.addListener((observable, oldValue, newValue) -> { + validate(newValue); + }); + } + + + /** + * Creates a validator that uses a {@link Function} for validation. The function has to return a + * {@link ValidationMessage} for a given input value or null if the value is valid. + * + * @param source + * the observable value that will be validated. + * @param function + * the validation function. + */ + public FunctionBasedValidator(ObservableValue source, Function function) { + this(source); + + validateFunction = value -> Optional.ofNullable(function.apply(value)); + + validate(source.getValue()); + } + + /** + * Creates a validator that uses a {@link Predicate} for validation. The predicate has to return true + * if the input value is valid. Otherwise false. + *

+ * When the predicate returns false, the given validation message will be used. + * + * @param source + * the observable value that will be validated. + * @param predicate + * the validation predicate. + * @param message + * the message that will be used when the predicate doesn't match + */ + public FunctionBasedValidator(ObservableValue source, Predicate predicate, ValidationMessage message) { + this(source); + + validateFunction = value -> Optional.ofNullable(predicate.test(value) ? null : message); + + validate(source.getValue()); + } + + private void validate(T newValue) { + validationStatus.clearMessages(); + Optional message = validateFunction.apply(newValue); + message.ifPresent(validationStatus::addMessage); + } + + @Override + public ValidationStatus getValidationStatus() { + return validationStatus; + } +} diff --git a/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/ObservableRuleBasedValidator.java b/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/ObservableRuleBasedValidator.java new file mode 100644 index 0000000..20ae6cb --- /dev/null +++ b/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/ObservableRuleBasedValidator.java @@ -0,0 +1,165 @@ +/******************************************************************************* + * Copyright 2015 Alexander Casall, Manuel Mauky + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ +package de.saxsys.mvvmfx.utils.validation; + +import javafx.beans.value.ObservableValue; + +import java.util.ArrayList; +import java.util.List; + + +/** + * This {@link Validator} implementation uses observable values as rules. Compared to the {@link FunctionBasedValidator} + * this allows more complex validation logic. + *

+ * There are two variants of rules possible: + *

    + *
  • "boolean rules" of type ObservableValue<Boolean>
  • + *
  • "complex rules" of type ObservableValue<ValidationMessage>
  • + *
+ * + *

Boolean Rules of type ObservableValue<Boolean>

+ * + * The first variant uses an {@link ObservableValue} together with a static message. If this observable + * has a value of false the validation status will be "invalid" and the given message will be present in the {@link ValidationStatus} + * of this validator. + * If the observable has a value of true it is considered to be valid. + * + * + *

Complex Rules of type ObservableValue<ValidationMessage>

+ * + * The second variant allows more complex rules. It uses a {@link ObservableValue} as rule. + * If this observable has a value other then null it is considered to be invalid. The {@link ValidationMessage} + * value will be present in the {@link ValidationStatus} of this validator. + * + *

+ * + * You can add multiple rules via the {@link #addRule(ObservableValue, ValidationMessage)} and {@link #addRule(ObservableValue)} method. + * If multiple rules are violated, each message will be present. + */ +public class ObservableRuleBasedValidator implements Validator { + + /* + These lists are used to store the rules that this validator is working with. + The reason for this is to prevent problems with garbage collection. + If the validator wouldn't keep references to all given rules it would be possible that they are + removed by the garbage collector which would result in wrong validation results. + To prevent this we store references to all given rules here. + Don't get confused by the fact that no values are only added to these lists but not obtained. + */ + private List> booleanRules = new ArrayList<>(); + private List> complexRules = new ArrayList<>(); + + private ValidationStatus validationStatus = new ValidationStatus(); + + /** + * Creates an instance of the Validator without any rules predefined. + */ + public ObservableRuleBasedValidator() {} + + /** + * Creates an instance of the Validator with the given rule predefined. + * It's a shortcut for creating an empty validator and + * adding a single boolean rule with {@link #addRule(ObservableValue, ValidationMessage)}. + * + * @param rule + * @param message + */ + public ObservableRuleBasedValidator(ObservableValue rule, ValidationMessage message) { + addRule(rule, message); + } + + /** + * Creates an instance of the Validator with the given complex rules predefined. + * It's a shortcut for creating an empty validator and + * adding one or multiple complex rules with {@link #addRule(ObservableValue)}. + * @param rules + */ + public ObservableRuleBasedValidator(ObservableValue ... rules) { + for (ObservableValue rule : rules) { + addRule(rule); + } + } + + /** + * Add a rule for this validator. + *

+ * The rule defines a condition that has to be fulfilled. + *

+ * A rule is defined by an observable boolean value. If the rule has a value of true the rule is + * "fulfilled". If the rule has a value of false the rule is violated. In this case the given message + * object will be added to the status of this validator. + *

+ * There are some predefined rules for common use cases in the {@link ObservableRules} class that can be used. + * + * @param rule + * @param message + */ + public void addRule(ObservableValue rule, ValidationMessage message) { + booleanRules.add(rule); + + rule.addListener((observable, oldValue, newValue) -> { + validateBooleanRule(newValue, message); + }); + + validateBooleanRule(rule.getValue(), message); + } + + private void validateBooleanRule(boolean isValid, ValidationMessage message) { + if (isValid) { + validationStatus.removeMessage(message); + } else { + validationStatus.addMessage(message); + } + } + + @Override + public ValidationStatus getValidationStatus() { + return validationStatus; + } + + /** + * Add a complex rule for this validator. + *

+ * The rule is defined by an {@link ObservableValue}. If this observable contains a {@link ValidationMessage} + * object the rule is considered to be violated and the {@link ValidationStatus} of this validator will contain + * the validation message object contained in the observable value. + * If the observable doesn't contain a value (in other words it contains null) the rule is considered to + * be fulfilled and the validation status of this validator will be valid (given that no other rule is violated). + *

+ * + * This method allows some more complex rules compared to {@link #addRule(ObservableValue, ValidationMessage)}. + * This way you can define rules that have different messages for specific cases. + * + * @param rule + */ + public void addRule(ObservableValue rule) { + complexRules.add(rule); + + rule.addListener((observable, oldValue, newValue) -> { + if(oldValue != null) { + validationStatus.removeMessage(oldValue); + } + if(newValue != null) { + validationStatus.addMessage(newValue); + } + }); + + if(rule.getValue() != null) { + validationStatus.addMessage(rule.getValue()); + } + } +} diff --git a/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/ObservableRules.java b/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/ObservableRules.java new file mode 100644 index 0000000..b67f2fa --- /dev/null +++ b/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/ObservableRules.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * Copyright 2015 Alexander Casall, Manuel Mauky + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ +package de.saxsys.mvvmfx.utils.validation; + +import javafx.beans.binding.Bindings; +import javafx.beans.value.ObservableBooleanValue; +import javafx.beans.value.ObservableValue; + +import java.util.function.Predicate; +import java.util.regex.Pattern; + +/** + * A collection of observable boolean constructors that can be used as rules for the + * {@link ObservableRuleBasedValidator}. + */ +public class ObservableRules { + + + public static ObservableBooleanValue fromPredicate(ObservableValue source, Predicate predicate) { + return Bindings.createBooleanBinding(() -> predicate.test(source.getValue()), source); + } + + public static ObservableBooleanValue notEmpty(ObservableValue source) { + return Bindings.createBooleanBinding(() -> { + final String s = source.getValue(); + + return s != null && !s.trim().isEmpty(); + }, source); + } + + public static ObservableBooleanValue matches(ObservableValue source, Pattern pattern) { + return Bindings.createBooleanBinding(() -> { + final String s = source.getValue(); + return s != null && pattern.matcher(s).matches(); + }, source); + } + +} diff --git a/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/Severity.java b/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/Severity.java new file mode 100644 index 0000000..bfc08d3 --- /dev/null +++ b/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/Severity.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright 2015 Alexander Casall, Manuel Mauky + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ +package de.saxsys.mvvmfx.utils.validation; + +/** + * The severity of validation messages. + * + * @author manuel.mauky + */ +public enum Severity { + WARNING, + ERROR +} diff --git a/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/ValidationMessage.java b/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/ValidationMessage.java new file mode 100644 index 0000000..842a6e2 --- /dev/null +++ b/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/ValidationMessage.java @@ -0,0 +1,81 @@ +/******************************************************************************* + * Copyright 2015 Alexander Casall, Manuel Mauky + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ +package de.saxsys.mvvmfx.utils.validation; + +import java.util.Objects; + +/** + * This class represents a single validation message for an error or a warning. It consists of a string message and a + * {@link Severity}. + * + * @author manuel.mauky + */ +public class ValidationMessage { + + private final String message; + + private final Severity severity; + + public ValidationMessage(Severity severity, String message) { + this.severity = Objects.requireNonNull(severity); + this.message = Objects.requireNonNull(message); + } + + + public static ValidationMessage warning(String message) { + return new ValidationMessage(Severity.WARNING, message); + } + + public static ValidationMessage error(String message) { + return new ValidationMessage(Severity.ERROR, message); + } + + public String getMessage() { + return message; + } + + public Severity getSeverity() { + return severity; + } + + @Override + public String toString() { + return "ValidationMessage{" + + "message='" + message + '\'' + + ", severity=" + severity + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || !(o instanceof ValidationMessage)) + return false; + + ValidationMessage that = (ValidationMessage) o; + + return message.equals(that.message) && severity == that.severity; + + } + + @Override + public int hashCode() { + int result = message.hashCode(); + result = 31 * result + severity.hashCode(); + return result; + } +} diff --git a/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/ValidationStatus.java b/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/ValidationStatus.java new file mode 100644 index 0000000..72fe104 --- /dev/null +++ b/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/ValidationStatus.java @@ -0,0 +1,137 @@ +/******************************************************************************* + * Copyright 2015 Alexander Casall, Manuel Mauky + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ +package de.saxsys.mvvmfx.utils.validation; + +import javafx.beans.binding.Bindings; +import javafx.beans.property.ListProperty; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyBooleanWrapper; +import javafx.beans.property.SimpleListProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.transformation.FilteredList; + +import java.util.Collection; +import java.util.Optional; + + +/** + * This class represents the state of a {@link Validator}. + *

+ * This class is reactive, which means that it's values will represent the current validation status. When the + * validation status changes the observable lists for the messages will be updated automatically. + * + * + * @author manuel.mauky + */ +public class ValidationStatus { + + private ListProperty messages = new SimpleListProperty<>(FXCollections.observableArrayList()); + + private ObservableList unmodifiableMessages = FXCollections.unmodifiableObservableList(messages); + private ObservableList errorMessages = new FilteredList<>(unmodifiableMessages, message -> message.getSeverity().equals(Severity.ERROR)); + private ObservableList warningMessages = new FilteredList<>(unmodifiableMessages, message -> message.getSeverity().equals(Severity.WARNING)); + + /** + * Intended for subclasses used by special validator implementations. + * Such subclasses can access the message list with writing accesses (unlike the {@link #getMessages()} + * which is read-only. + * + * An example for a subclass is {@link CompositeValidationStatus}. + */ + protected ObservableList getMessagesInternal() { + return messages; + } + + + void addMessage(ValidationMessage message) { + getMessagesInternal().add(message); + } + + void addMessage(Collection messages) { + getMessagesInternal().addAll(messages); + } + + void removeMessage(ValidationMessage message) { + getMessagesInternal().remove(message); + } + + void removeMessage(Collection messages) { + getMessagesInternal().removeAll(messages); + } + + void clearMessages() { + getMessagesInternal().clear(); + } + + + /** + * @return an unmodifiable observable list of all messages. + */ + public ObservableList getMessages() { + return unmodifiableMessages; + } + + + /** + * @return an unmodifiable observable list of all messages of severity {@link Severity#ERROR}. + */ + public ObservableList getErrorMessages() { + return errorMessages; + } + + /** + * @return an unmodifiable observable list of all messages of severity {@link Severity#WARNING}. + */ + public ObservableList getWarningMessages() { + return warningMessages; + } + + /** + * @return true if there are no validation messages present. + */ + public ReadOnlyBooleanProperty validProperty() { + return messages.emptyProperty(); + } + + public boolean isValid() { + return validProperty().get(); + } + + /** + * Returns the message with the highest priority using the following algorithm: - if there are messages with + * {@link Severity#ERROR}, take the first one. - otherwise, if there are messages with {@link Severity#WARNING}, + * take the first one. - otherwise, an empty Optional is returned. + * + * @return an Optional containing the ValidationMessage or an empty Optional. + */ + public Optional getHighestMessage() { + final Optional error = getMessages().stream() + .filter(message -> message.getSeverity().equals(Severity.ERROR)) + .findFirst(); + + if (error.isPresent()) { + return error; + } else { + final Optional warning = getMessages().stream() + .filter(message -> message.getSeverity().equals(Severity.WARNING)) + .findFirst(); + + return warning; + } + } + +} diff --git a/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/Validator.java b/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/Validator.java new file mode 100644 index 0000000..3e9a7e9 --- /dev/null +++ b/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/Validator.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * Copyright 2015 Alexander Casall, Manuel Mauky + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ +package de.saxsys.mvvmfx.utils.validation; + +/** + * This interface is implemented by specific validators. Each validator has to provide a reactive + * {@link ValidationStatus} that's is updated by the validator implementation when the validation is executed (f.e. when + * the user has changed an input value). + * + * @author manuel.mauky + */ +public interface Validator { + + /** + * Returns the validation status of this validator. The status will be updated when the validator re-validates the + * inputs of the user. + * + * @return the state. + */ + ValidationStatus getValidationStatus(); + +} diff --git a/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/visualization/ControlsFxVisualizer.java b/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/visualization/ControlsFxVisualizer.java new file mode 100644 index 0000000..39e50c7 --- /dev/null +++ b/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/visualization/ControlsFxVisualizer.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * Copyright 2015 Alexander Casall, Manuel Mauky + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ +package de.saxsys.mvvmfx.utils.validation.visualization; + +import de.saxsys.mvvmfx.utils.validation.Severity; +import de.saxsys.mvvmfx.utils.validation.ValidationMessage; +import javafx.scene.control.Control; +import org.controlsfx.validation.ValidationSupport; +import org.controlsfx.validation.decoration.GraphicValidationDecoration; +import org.controlsfx.validation.decoration.ValidationDecoration; + +import java.util.Optional; + +/** + * An implementation of {@link ValidationVisualizer} that uses the third-party library ControlsFX to visualize validation messages. + *

+ * Please Note: The library ControlsFX is not delivered with the mvvmFX library. If you like to use + * this visualization you have to add the ControlsFX library to your classpath, otherwise you will get + * {@link NoClassDefFoundError}s and {@link ClassNotFoundException}s. If you are using a build management system like + * maven or gradle you simply have to add the library as dependency. + * + * + * @author manuel.mauky + */ +public class ControlsFxVisualizer extends ValidationVisualizerBase { + + private ValidationDecoration decoration = new GraphicValidationDecoration(); + + /** + * Define a custom ControlsFX {@link ValidationVisualizer} that is used to visualize the validation results. + *

+ * By default the {@link GraphicValidationDecoration} is used. + */ + public void setDecoration(ValidationDecoration decoration) { + this.decoration = decoration; + } + + + @Override + protected void applyRequiredVisualization(Control control, boolean required) { + ValidationSupport.setRequired(control, required); + if (required) { + decoration.applyRequiredDecoration(control); + } + } + + @Override + protected void applyVisualization(Control control, Optional messageOptional, boolean required) { + + if (messageOptional.isPresent()) { + final ValidationMessage message = messageOptional.get(); + + decoration.removeDecorations(control); + + if (Severity.ERROR.equals(message.getSeverity())) { + decoration.applyValidationDecoration(org.controlsfx.validation.ValidationMessage.error(control, + message.getMessage())); + } else if (Severity.WARNING.equals(message.getSeverity())) { + decoration.applyValidationDecoration(org.controlsfx.validation.ValidationMessage.warning(control, + message.getMessage())); + } + + } else { + decoration.removeDecorations(control); + } + + if (required) { + decoration.applyRequiredDecoration(control); + } + } + +} diff --git a/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/visualization/ValidationVisualizer.java b/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/visualization/ValidationVisualizer.java new file mode 100644 index 0000000..292035c --- /dev/null +++ b/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/visualization/ValidationVisualizer.java @@ -0,0 +1,67 @@ +/******************************************************************************* + * Copyright 2015 Alexander Casall, Manuel Mauky + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ +package de.saxsys.mvvmfx.utils.validation.visualization; + +import de.saxsys.mvvmfx.utils.validation.ValidationStatus; +import javafx.scene.control.Control; + +/** + * Common interface for all implementations of validation visualizers. + *

+ * A single instance of a visualizer is connected to a single {@link ValidationStatus} that it visualizes. When the + * state of the {@link ValidationStatus} changes the visualizer has to react to these changes and update it's decoration + * accordingly. + *

+ * Besides showing validation messages the job of the visualizer is to mark an input control as mandatory. Note that + * this mark is only a visual effect and has no effect to the actual validation logic. + *

+ * + * Instead of directly implementing this interface implementors of custom visualizers should consider to extend from the + * base class {@link ValidationVisualizerBase}. This base class handles the life cycle of the {@link ValidationStatus} + * (i.e. listeners on the observable lists of validation messages). The implementor only needs to implement on how a + * single message should be shown and how a control is marked as mandatory. + * + * @author manuel.mauky + */ +public interface ValidationVisualizer { + + /** + * Initialize this visualization so that it visualizes the given {@link ValidationStatus} on the given input + * control. + * + * @param status + * the status that is visualized. + * @param control + * the control that will be decorated. + */ + default void initVisualization(ValidationStatus status, Control control) { + initVisualization(status, control, false); + } + + /** + * Initialize this visualization so that it visualizes the given {@link ValidationStatus} on the given input + * control. + * + * @param status + * the status that is visualized. + * @param control + * the control that will be decorated. + * @param mandatory + * a boolean flag indicating whether this input value is mandatory or not. + */ + void initVisualization(ValidationStatus status, Control control, boolean mandatory); + +} diff --git a/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/visualization/ValidationVisualizerBase.java b/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/visualization/ValidationVisualizerBase.java new file mode 100644 index 0000000..6ccfca6 --- /dev/null +++ b/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/visualization/ValidationVisualizerBase.java @@ -0,0 +1,96 @@ +/******************************************************************************* + * Copyright 2015 Alexander Casall, Manuel Mauky + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ +package de.saxsys.mvvmfx.utils.validation.visualization; + +import de.saxsys.mvvmfx.utils.validation.Severity; +import de.saxsys.mvvmfx.utils.validation.ValidationMessage; +import de.saxsys.mvvmfx.utils.validation.ValidationStatus; +import javafx.application.Platform; +import javafx.collections.ListChangeListener; +import javafx.scene.control.Control; + +import java.util.Optional; + +/** + * A base class for implementations of {@link ValidationVisualizer}s. + *

+ * Implementors using this base class only need to implement logic on how to visualize a single + * {@link ValidationMessage} (see {@link #applyVisualization(Control, Optional, boolean)}) and the required flag (see + * {@link #applyRequiredVisualization(Control, boolean)}). + *

+ * This base class takes care for the handling of the {@link ValidationStatus} and the reaction to it's changing message + * lists. + * + * @author manuel.mauky + */ +public abstract class ValidationVisualizerBase implements ValidationVisualizer { + + + @Override + public void initVisualization(final ValidationStatus result, final Control control, boolean required) { + Platform.runLater(() -> { + if (required) { + applyRequiredVisualization(control, required); + } + + applyVisualization(control, result.getHighestMessage(), required); + + // Monitor the message list and always display the highest message. + // Note: there could be more than one change on the message list, but only the highest + // message is of interest in this case. + result.getMessages().addListener((ListChangeListener) c -> { + Platform.runLater(() -> applyVisualization(control, result.getHighestMessage(), required)); + }); + }); + } + + /** + * Apply a visualization to the given control that indicates that it is a mandatory field. + *

+ * This method is called when the validator is initialized. + * + * @param control + * the controls that has to be decorated. + * @param required + * a boolean indicating whether the given control is mandatory or not. + */ + protected abstract void applyRequiredVisualization(Control control, boolean required); + + /** + * Apply a visualization to the given control that shows a validation message. + *

+ * This method will be called every time the validation state changes. If the given {@link Optional} for the + * {@link ValidationMessage} is empty, no validation rule is violated at the moment and therefore no error/warning + * should be shown. + *

+ * A visualizer can handle the {@link Severity} that is provided in the visualization message ( + * {@link ValidationMessage#getSeverity()}). + *

+ * The given boolean parameter indicates whether this controls is mandatory or not. It can be used if a violation + * for a mandatory field should be visualized differently than a non-mandatory field. + * + * + * @param control + * the control that will be decorated. + * @param messageOptional + * an optional containing the validation message with the highest priority, or an empty Optional if no + * validation rule is violated at the moment. + * @param required + * a boolean flag indicating whether this control is mandatory or not. + */ + protected abstract void applyVisualization(Control control, Optional messageOptional, boolean required); + +} diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..8acdc38 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,20 @@ +pluginManagement { + includeBuild("build-logic") + + + repositories { + gradlePluginPortal() + } +} +plugins { + id("org.jabref.javafx.controls.gradle.build") +} + +rootProject.name = "jabref.controls" + +javaModules { + directory(".") + versions("versions") + // include("jablib", "jabkit", "jabgui", "jabsrv", "jabsrv-cli", "test-support", "versions") +} + diff --git a/versions/build.gradle.kts b/versions/build.gradle.kts new file mode 100644 index 0000000..fbee2d2 --- /dev/null +++ b/versions/build.gradle.kts @@ -0,0 +1,32 @@ +import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform + +plugins { + id("java-platform") +} + +javaPlatform { + allowDependencies() +} + +// Based on https://stackoverflow.com/questions/11235614/how-to-detect-the-current-os-from-gradle +val os = + DefaultNativePlatform.getCurrentOperatingSystem() +val arch = + DefaultNativePlatform.getCurrentArchitecture() +val javafx = 24 + +dependencies { + +} + +dependencies.constraints { + api("org.openjfx:javafx-base:$javafx") + // api("org.openjfx:javafx-controls:$javafx") + // api("org.openjfx:javafx-fxml:$javafx") + // api("org.openjfx:javafx-graphics:${javafx}") + // api("org.openjfx:javafx-swing:$javafx") + // api("org.openjfx:javafx-web:$javafx") + // from JavaFX25 onwards + // api("org.openjfx:jdk-jsobject:$javafx") + +}