diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c5e2992 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,24 @@ +name: CI + +on: + push: + branches: ["**"] + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + java-version: [11, 21] + steps: + - uses: actions/checkout@v4 + - name: Set up JDK ${{ matrix.java-version }} + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: ${{ matrix.java-version }} + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Test with Gradle + run: ./gradlew test --no-daemon diff --git a/build.gradle b/build.gradle index 33c2c64..57c474d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,27 +1,29 @@ +// enable intellij integration & load vmf version to be used buildscript { + + ext.commonProps = new Properties() + file(new File(rootProject.getRootDir(),"config/common.properties")). + withInputStream { {commonProps.load(it) }} + + ext.vmfPluginIntelliJIntegration = true + repositories { mavenCentral() - jcenter() - } - - dependencies { - // } } plugins { - id "com.github.hierynomus.license" version "0.13.1" + id "com.github.hierynomus.license" version "0.16.1" id 'maven-publish' - id 'net.nemerosa.versioning' version '2.4.0' - id 'com.jfrog.bintray' version '1.8.4' - id 'com.github.ben-manes.versions' version '0.13.0' + id 'net.nemerosa.versioning' version '3.1.0' + id 'com.github.ben-manes.versions' version '0.52.0' } apply plugin: 'java' apply from: 'gradle/publishing.gradle' wrapper { - gradleVersion = '5.3' + gradleVersion = '8.14.2' } sourceCompatibility = '1.8' @@ -40,13 +42,12 @@ tasks.withType(Javadoc) { } repositories { - jcenter() mavenCentral() mavenLocal() } dependencies { - testCompile group: 'junit', name: 'junit', version: '4.10' + testImplementation group: 'junit', name: 'junit', version: '4.13.2' } license { diff --git a/config/common.properties b/config/common.properties new file mode 100644 index 0000000..c6da23c --- /dev/null +++ b/config/common.properties @@ -0,0 +1 @@ +publication.version = 0.3.4 \ No newline at end of file diff --git a/gradle/project-info.gradle b/gradle/project-info.gradle index e6d0ca9..a1b518e 100644 --- a/gradle/project-info.gradle +++ b/gradle/project-info.gradle @@ -1,25 +1,23 @@ // ----------------------------------------------------------------------------- // publishing information // ----------------------------------------------------------------------------- -ext.publishing.artifactId = project.name.toLowerCase() -ext.publishing.groupId = 'eu.mihosoft.vcollections' -ext.publishing.versionId = '0.3.3' +ext.publishingInfo.artifactId = project.name.toLowerCase() +ext.publishingInfo.groupId = 'eu.mihosoft.vcollections' +ext.publishingInfo.versionId = ext.commonProps.get("publication.version") -ext.publishing.developerName = 'Michael Hoffer' -ext.publishing.developerAlias = 'miho' -ext.publishing.developerEmail = 'info@michaelhoffer.de' -ext.publishing.inceptionYear = '2017' +ext.publishingInfo.developerName = 'Michael Hoffer' +ext.publishingInfo.developerAlias = 'miho' +ext.publishingInfo.developerEmail = 'info@michaelhoffer.de' +ext.publishingInfo.inceptionYear = '2017' -ext.publishing.bintray.repo = 'VCollections' -ext.publishing.bintray.userOrg = 'miho' -ext.publishing.bintray.name = project.name +ext.publishingInfo.desc = 'Lightweight observable collections (used by VMF and VRL)' +ext.publishingInfo.license = 'Apache-2.0' +ext.publishingInfo.licenseUrl = 'https://github.com/miho/VCollections/blob/master/LICENSE.txt' +ext.publishingInfo.labels = ['collections', 'observable', 'vmf', 'modeling', 'data model', 'vrl', 'modeling framework'] +ext.publishingInfo.websiteUrl = 'https://github.com/miho/VCollections' +ext.publishingInfo.issueTrackerUrl = 'https://github.com/miho/VCollections/issues' +ext.publishingInfo.vcsUrl = 'https://github.com/miho/VCollections.git' -ext.publishing.desc = 'Lightweight observable collections (used by VMF and VRL)' -ext.publishing.license = 'BSD 2-Clause' -ext.publishing.licenseUrl = 'https://github.com/miho/VCollections/blob/master/LICENSE.txt' -ext.publishing.labels = ['collections', 'observable', 'vmf', 'modeling', 'data model', 'vrl', 'modeling framework'] -ext.publishing.websiteUrl = 'https://github.com/miho/VCollections' -ext.publishing.issueTrackerUrl = 'https://github.com/miho/VCollections/issues' -ext.publishing.vcsUrl = 'https://github.com/miho/VCollections.git' +ext.publishingInfo.gitHubMavenRepo = "https://maven.pkg.github.com/miho/VCollections" -ext.publishing.pomName = ext.publishing.artifactId +ext.publishingInfo.pomName = ext.publishingInfo.artifactId diff --git a/gradle/publishing.gradle b/gradle/publishing.gradle index 4d8d648..73aa523 100644 --- a/gradle/publishing.gradle +++ b/gradle/publishing.gradle @@ -1,25 +1,25 @@ // ----------------------------------------------------------------------------- // Collect publishing information // ----------------------------------------------------------------------------- -ext.publishing = [:] -ext.publishing.bintray = [:] +ext.publishingInfo = [:] -ext.publishing.pomName = ext.publishing.artifactId +ext.publishingInfo.pomName = ext.publishingInfo.artifactId apply from: "gradle/project-info.gradle" +apply plugin: 'signing' // ----------------------------------------------------------------------------- // Performs publishing // ----------------------------------------------------------------------------- task javadocJar(type: Jar, dependsOn: javadoc) { - classifier = 'javadoc' + archiveClassifier = 'javadoc' from javadoc.destinationDir } // create one jar for the source files task sourcesJar(type: Jar, dependsOn: classes) { - classifier = 'sources' + archiveClassifier = 'sources' from sourceSets.main.allSource } @@ -28,6 +28,7 @@ artifacts { archives javadocJar archives sourcesJar } + Date buildTimeAndDate = new Date() ext { buildDate = new java.text.SimpleDateFormat('yyyy-MM-dd').format(buildTimeAndDate) @@ -37,86 +38,136 @@ ext { jar { manifest { attributes( - 'Built-By': System.properties['user.name'], - 'Created-By': System.properties['java.version'] + " (" + System.properties['java.vendor'] + " " + System.properties['java.vm.version'] + ")", - 'Build-Date': project.buildDate, - 'Build-Time': project.buildTime, - 'Build-Revision': versioning.info.commit, - 'Specification-Title': project.name, - 'Specification-Version': project.version, - 'Implementation-Title': project.name, - 'Implementation-Version': project.version + 'Built-By': System.properties['user.name'], + 'Created-By': System.properties['java.version'] + " (" + System.properties['java.vendor'] + " " + System.properties['java.vm.version'] + ")", + 'Build-Date': project.buildDate, + 'Build-Time': project.buildTime, + 'Build-Revision': versioning.info.commit, + 'Specification-Title': project.name, + 'Specification-Version': project.version, + 'Implementation-Title': project.name, + 'Implementation-Version': project.version ) } } def pomConfig = { - name ext.publishing.pomName - description ext.publishing.desc - url ext.publishing.websiteUrl - inceptionYear ext.publishing.inceptionYear + name ext.publishingInfo.pomName + description ext.publishingInfo.desc + url ext.publishingInfo.websiteUrl + inceptionYear ext.publishingInfo.inceptionYear licenses { license([:]) { - name ext.publishing.license - url ext.publishing.licenseUrl + name ext.publishingInfo.license + url ext.publishingInfo.licenseUrl distribution 'repo' } } scm { - url ext.publishing.vcsUrl - connection ext.publishing.vcsUrl - developerConnection ext.publishing.vcsUrl + url ext.publishingInfo.vcsUrl + connection ext.publishingInfo.vcsUrl + developerConnection ext.publishingInfo.vcsUrl } developers { developer { - id ext.publishing.developerNameAlias - name ext.publishing.developerName + id ext.publishingInfo.developerNameAlias + name ext.publishingInfo.developerName } } } + + publishing { + + repositories { + // -------------------------------------------------------------------------------- + // Destination Repository 'GitHubPackages' + // -> call task 'publishMavenJavaPublicationToGitHubPackagesRepository' to publish + // -------------------------------------------------------------------------------- + maven { + name = "GitHubPackages" + // see https://levelup.gitconnected.com/publish-a-maven-package-to-github-with-gradle-fabc6de24d6 + // Replace OWNER and REPOSITORY with your GitHub username/repository + // (must be both lowercase according to the documenations) + // url = uri("https://maven.pkg.github.com/OWNER/REPOSITORY") + url = uri(project.findProperty('publishingInfo').gitHubMavenRepo) + credentials { + // Make sure to generate a token with write-packages and read-packages permission: + // https://github.com/settings/tokens/new + // You can either store the username and token in + // ~/.gradle/gradle.properties (use the gpr.user and gpr.key keys) + // Or you can store them as environment variables e.g. in ~/.bash_profile or ~/.zsh + // depending on your shell (GITHUB_USERNAME and GITHUB_TOKEN keys) + // Or you pass them via CLI: gradle publish -Pgpr.user=username -Pgpr.key=token + // See at EOF for examples on how to store the credentials + username = project.findProperty("gpr.user") ?: System.getenv("GITHUB_USERNAME") + password = project.findProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN") + } + } + + // -------------------------------------------------------------------------------- + // Destination Repository 'OSSRH' + // telling gradle to publish artifact to local directory + // -> call task 'publishMavenJavaPublicationToOSSRHRepository' to publish + // -> go to https://oss.sonatype.org/#stagingRepositories, click 'close', drink coffee and then click 'release' + // if closing was successful + // -------------------------------------------------------------------------------- + maven { + name = "OSSRH" + url "https://oss.sonatype.org/service/local/staging/deploy/maven2" + credentials { + username = project.findProperty("oss.user") ?: System.getenv("OSS_USERNAME") + password = project.findProperty("oss.pwd") ?: System.getenv("OSS_PWD") + } + } + + // -------------------------------------------------------------------------------- + // Destination Repository 'BuildDir' + // telling gradle to publish artifact to local directory + // -> call task 'publishMavenJavaPublicationToBuildDirRepository' to publish + // -------------------------------------------------------------------------------- + maven { + name = "BuildDir" + url "file:/${buildDir}/artifacts" + } + } + + publications { - mavenCustom(MavenPublication) { - groupId publishing.groupId - artifactId publishing.artifactId - version publishing.versionId + mavenJava(MavenPublication) { + groupId publishingInfo.groupId + artifactId publishingInfo.artifactId + version publishingInfo.versionId from components.java artifact sourcesJar artifact javadocJar pom.withXml { def root = asNode() - root.appendNode 'description', publishing.desc + root.appendNode 'description', publishingInfo.desc root.children().last() + pomConfig } } } } -if (!project.hasProperty('bintrayUsername')) ext.bintrayUsername = '' -if (!project.hasProperty('bintrayApiKey')) ext.bintrayApiKey = '' - -bintray { - user = project.bintrayUsername - key = project.bintrayApiKey - publications = ['mavenCustom'] - pkg { - repo = publishing.bintray.repo - userOrg = publishing.bintray.userOrg - name = publishing.bintray.name - desc = publishing.desc - licenses = [publishing.license] - labels = publishing.labels - websiteUrl = publishing.websiteUrl - issueTrackerUrl = publishing.issueTrackerUrl - vcsUrl = publishing.vcsUrl - publicDownloadNumbers = true - - version { - name = publishing.versionId - vcsTag = 'v' + publishing.versionId - } + +if( + project.findProperty("signing.secretKeyRingFile") + && project.findProperty("signing.password") + && project.findProperty("signing.keyId") +) { + + signing { + sign publishing.publications } + +} else { + println "> skipping signing, provide\n" + + " - 'signing.secretKeyRingFile'\n" + + " - 'signing.password'\n" + + " - 'signing.keyId'\n" + + " to activate it\n" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 87b738c..1b33c55 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 838e6bc..ff23a68 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index af6708f..23d15a9 100755 --- a/gradlew +++ b/gradlew @@ -1,78 +1,129 @@ -#!/usr/bin/env sh +#!/bin/sh + +# +# Copyright © 2015-2021 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 UN*X -## +# +# 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 -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +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 -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` -# 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"' +# 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" +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 - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" + # 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" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -81,92 +132,120 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + 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" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +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 -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -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" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + 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 - i=$((i+1)) + # 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 - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" +# 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" \ + -classpath "$CLASSPATH" \ + -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 index 0f8d593..5eed7ee 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,4 +1,22 @@ -@if "%DEBUG%" == "" @echo off +@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 @@ -9,25 +27,29 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +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" +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%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +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 @@ -35,48 +57,36 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +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 -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +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! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +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 diff --git a/src/main/java/eu/mihosoft/vcollections/EventUtil.java b/src/main/java/eu/mihosoft/vcollections/EventUtil.java index 6440170..cb415d5 100644 --- a/src/main/java/eu/mihosoft/vcollections/EventUtil.java +++ b/src/main/java/eu/mihosoft/vcollections/EventUtil.java @@ -63,6 +63,8 @@ private EventUtil() { public static String toStringWithDetails(CollectionChangeEvent evt) { if (evt instanceof VListChangeEvent) { return ((VListChangeEvent) evt).toStringWithDetails(); + } else if (evt instanceof VMapChangeEvent) { + return ((VMapChangeEvent) evt).toStringWithDetails(); } else { return evt.toString(); } diff --git a/src/main/java/eu/mihosoft/vcollections/VList.java b/src/main/java/eu/mihosoft/vcollections/VList.java index d0e86fa..7591f4a 100644 --- a/src/main/java/eu/mihosoft/vcollections/VList.java +++ b/src/main/java/eu/mihosoft/vcollections/VList.java @@ -293,7 +293,7 @@ public boolean addAll(int index, Collection c) { public boolean addAll(int[] indices, Collection c) { Objects.requireNonNull(indices, "Indices must not be null"); - Objects.requireNonNull(indices, "collection must not be null"); + Objects.requireNonNull(c, "collection must not be null"); if(indices.length!=c.size()) { throw new RuntimeException("The number of indices must match the number of elements to add"); @@ -370,11 +370,11 @@ public Collection setAll(int index, Collection elements) { i++; } - List oldElements = new ArrayList<>(originalList.subList(index, toIndex)); + List oldElements = new ArrayList<>(previousElements); if (hasListeners()) { _vmf_fireChangeEvent(VListChangeEvent.getSetEvent( - this, indices, oldElements, new ArrayList(elements),getEventInfo() + this, indices, oldElements, new ArrayList(elements), getEventInfo() )); } diff --git a/src/main/java/eu/mihosoft/vcollections/VListChangeSupport.java b/src/main/java/eu/mihosoft/vcollections/VListChangeSupport.java index d0a88cb..5ef8596 100644 --- a/src/main/java/eu/mihosoft/vcollections/VListChangeSupport.java +++ b/src/main/java/eu/mihosoft/vcollections/VListChangeSupport.java @@ -74,7 +74,7 @@ public void fireEvent(CollectionChangeEvent, ? super VListCh public boolean hasListeners() { - return true; + return !listeners.isEmpty(); } } diff --git a/src/main/java/eu/mihosoft/vcollections/VMap.java b/src/main/java/eu/mihosoft/vcollections/VMap.java new file mode 100644 index 0000000..af94b1f --- /dev/null +++ b/src/main/java/eu/mihosoft/vcollections/VMap.java @@ -0,0 +1,280 @@ +/* + * Copyright 2017-2019 Michael Hoffer . All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * If you use this software for scientific research then please cite the following publication(s): + * + * M. Hoffer, C. Poliwoda, & G. Wittum. (2013). Visual reflection library: + * a framework for declarative GUI programming on the Java platform. + * Computing and Visualization in Science, 2013, 16(4), + * 181–192. http://doi.org/10.1007/s00791-014-0230-y + * + * THIS SOFTWARE IS PROVIDED BY Michael Hoffer "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Michael Hoffer OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN 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. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of Michael Hoffer . + */ +package eu.mihosoft.vcollections; + +import java.util.Map; +import vjavax.observer.Subscription; +import eu.mihosoft.vcollections.VMapChangeListener; + +/** + * Observable map. + * + * @author Michael Hoffer + * + * @param key type + * @param value type + */ +public interface VMap extends Map, VMapObservable { + + /** + * Creates a new wrapper around the specified map. Modifying the wrapper + * will modify the wrapped map. + * + * @param key type + * @param value type + * @param map map to wrap + * @return new {@link VMap} that wraps the specified map + */ + static VMap newInstance(Map map) { + return VMapImpl.newInstance(map); + } + + /** + * Returns an unmodifiable view of this map (see + * {@link java.util.Collections#unmodifiableMap(java.util.Map)} ). + * + * @return an unmodifiable view of this map + */ + VMap asUnmodifiable(); + + /** + * Removes all elements with the specified keys. + * + * @param keys keys to remove + * @return {@code true} if this map changed as a result of the call; {@code false} otherwise + */ + boolean removeAll(@SuppressWarnings("unchecked") K... keys); + + /** + * Sets the event info to be used for event generation. + * + * @param evtInfo event info to set + */ + void setEventInfo(String evtInfo); + + /** + * Returns the event info used for event generation. + * + * @return event info used for event generation + */ + String getEventInfo(); +} + +/** + * Observable map implementation. + */ +final class VMapImpl extends java.util.AbstractMap implements VMap { + + private final Map originalMap; + private VMapChangeSupport mapChangeSupport; + private VMapImpl unmodifiableInstance; + private String evtInfo = ""; + + private VMapChangeSupport getMapChangeSupport() { + if (mapChangeSupport == null) { + mapChangeSupport = new VMapChangeSupport<>(); + } + return mapChangeSupport; + } + + private boolean hasListeners() { + return mapChangeSupport != null && mapChangeSupport.hasListeners(); + } + + private VMapImpl(Map originalMap) { + this.originalMap = originalMap; + } + + static VMapImpl newInstance(Map map) { + return new VMapImpl<>(map); + } + + @Override + public void setEventInfo(String evtInfo) { + if (evtInfo == null) { + this.evtInfo = ""; + } else { + this.evtInfo = evtInfo; + } + } + + @Override + public String getEventInfo() { + return this.evtInfo; + } + + private void fireChangeEvent(VMapChangeEvent evt) { + if (hasListeners()) { + mapChangeSupport.fireEvent(evt); + } + } + + @Override + public int size() { + return originalMap.size(); + } + + @Override + public boolean isEmpty() { + return originalMap.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return originalMap.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return originalMap.containsValue(value); + } + + @Override + public V get(Object key) { + return originalMap.get(key); + } + + @Override + public V put(K key, V value) { + boolean hadKey = originalMap.containsKey(key); + V previous = originalMap.put(key, value); + + if (hasListeners()) { + if (hadKey) { + fireChangeEvent(VMapChangeEvent.getSetEvent(this, + java.util.Collections.singletonMap(key, previous), + java.util.Collections.singletonMap(key, value), getEventInfo())); + } else { + fireChangeEvent(VMapChangeEvent.getAddedEvent(this, + java.util.Collections.singletonMap(key, value), getEventInfo())); + } + } + return previous; + } + + @Override + public V remove(Object key) { + if (!originalMap.containsKey(key)) { + return null; + } + V removed = originalMap.remove(key); + if (hasListeners()) { + fireChangeEvent(VMapChangeEvent.getRemovedEvent(this, + java.util.Collections.singletonMap((K) key, removed), getEventInfo())); + } + return removed; + } + + @Override + public void clear() { + if (originalMap.isEmpty()) { + return; + } + Map removed = new java.util.LinkedHashMap<>(originalMap); + originalMap.clear(); + if (hasListeners()) { + fireChangeEvent(VMapChangeEvent.getRemovedEvent(this, removed, getEventInfo())); + } + } + + @Override + public void putAll(Map m) { + if (m.isEmpty()) return; + Map added = new java.util.LinkedHashMap<>(); + Map removed = new java.util.LinkedHashMap<>(); + for (Entry e : m.entrySet()) { + K k = e.getKey(); + V v = e.getValue(); + if (originalMap.containsKey(k)) { + V prev = originalMap.put(k, v); + removed.put(k, prev); + added.put(k, v); + } else { + originalMap.put(k, v); + added.put(k, v); + } + } + if (hasListeners()) { + fireChangeEvent(VMapChangeEvent.getSetEvent(this, removed, added, getEventInfo())); + } + } + + @Override + public java.util.Set> entrySet() { + return originalMap.entrySet(); + } + + @Override + public VMap asUnmodifiable() { + if (unmodifiableInstance == null) { + unmodifiableInstance = new VMapImpl<>(java.util.Collections.unmodifiableMap(originalMap)); + syncMaps(this, unmodifiableInstance); + } + return unmodifiableInstance; + } + + private void syncMaps(VMapImpl src, VMapImpl target) { + src.addChangeListener(target::fireChangeEvent); + } + + @Override + public boolean removeAll(K... keys) { + if (keys.length == 0) return true; + Map removed = new java.util.LinkedHashMap<>(); + for (K k : keys) { + if (originalMap.containsKey(k)) { + removed.put(k, originalMap.remove(k)); + } + } + if (hasListeners() && !removed.isEmpty()) { + fireChangeEvent(VMapChangeEvent.getRemovedEvent(this, removed, getEventInfo())); + } + return !removed.isEmpty(); + } + + @Override + public Subscription addChangeListener(VMapChangeListener l) { + return getMapChangeSupport().addChangeListener(l); + } + + @Override + public boolean removeChangeListener(VMapChangeListener l) { + boolean result = getMapChangeSupport().removeChangeListener(l); + if (!getMapChangeSupport().hasListeners()) { + mapChangeSupport = null; + } + return result; + } +} diff --git a/src/main/java/eu/mihosoft/vcollections/VMapChange.java b/src/main/java/eu/mihosoft/vcollections/VMapChange.java new file mode 100644 index 0000000..22d52b4 --- /dev/null +++ b/src/main/java/eu/mihosoft/vcollections/VMapChange.java @@ -0,0 +1,121 @@ +/* + * Copyright 2017-2019 Michael Hoffer . All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * If you use this software for scientific research then please cite the following publication(s): + * + * M. Hoffer, C. Poliwoda, & G. Wittum. (2013). Visual reflection library: + * a framework for declarative GUI programming on the Java platform. + * Computing and Visualization in Science, 2013, 16(4), + * 181–192. http://doi.org/10.1007/s00791-014-0230-y + * + * THIS SOFTWARE IS PROVIDED BY Michael Hoffer "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Michael Hoffer OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN 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. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of Michael Hoffer . + */ +package eu.mihosoft.vcollections; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Map.Entry; +import vjavax.observer.collection.CollectionChange; + +/** + * Represents a map change. + * + * @author Michael Hoffer + */ +public interface VMapChange extends CollectionChange> { + + /** + * @return changed entries + */ + Map entries(); + + /** + * @return keys of changed entries + */ + default Collection keys() { + return entries().keySet(); + } + + @Override + default List> elements() { + return new ArrayList<>(entries().entrySet()); + } + + /** + * Creates a new map change. + * + * @param key type + * @param value type + * @param entries changed entries + * @return new map change object + */ + static VMapChange newInstance(Map entries) { + Objects.requireNonNull(entries); + return new VMapChangeImpl<>(entries); + } + + /** + * Creates an empty map change object. + * + * @param key type + * @param value type + * @return an empty map change object + */ + @SuppressWarnings("unchecked") + static VMapChange empty() { + return (VMapChange) VMapChangeImpl.EMPTY; + } + + /** + * Indicates whether this object contains map changes. + * + * @return {@code true} if this object contains changes; {@code false} + * otherwise + */ + default boolean hasChanges() { + return !entries().isEmpty(); + } +} + +class VMapChangeImpl implements VMapChange { + + private final Map entries; + + @SuppressWarnings("unchecked") + static final VMapChange EMPTY = new VMapChangeImpl<>(Collections.emptyMap()); + + VMapChangeImpl(Map entries) { + this.entries = entries; + } + + @Override + public Map entries() { + return entries; + } +} diff --git a/src/main/java/eu/mihosoft/vcollections/VMapChangeEvent.java b/src/main/java/eu/mihosoft/vcollections/VMapChangeEvent.java new file mode 100644 index 0000000..23fa76a --- /dev/null +++ b/src/main/java/eu/mihosoft/vcollections/VMapChangeEvent.java @@ -0,0 +1,165 @@ +/* + * Copyright 2017-2019 Michael Hoffer . All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * If you use this software for scientific research then please cite the following publication(s): + * + * M. Hoffer, C. Poliwoda, & G. Wittum. (2013). Visual reflection library: + * a framework for declarative GUI programming on the Java platform. + * Computing and Visualization in Science, 2013, 16(4), + * 181–192. http://doi.org/10.1007/s00791-014-0230-y + * + * THIS SOFTWARE IS PROVIDED BY Michael Hoffer "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Michael Hoffer OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN 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. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of Michael Hoffer . + */ +package eu.mihosoft.vcollections; + +import java.util.Map; +import java.util.Map.Entry; + +/** + * Map change event. An event contains information about all changes made by the + * action that fired the event. + * + * @param key type + * @param value type + */ +public interface VMapChangeEvent { + + default boolean wasAdded() { + return added().hasChanges(); + } + + default boolean wasRemoved() { + return removed().hasChanges(); + } + + default boolean wasSet() { + return added().hasChanges() && removed().hasChanges(); + } + + VMapChange added(); + + VMapChange removed(); + + VMap source(); + + default String eventInfo() { return ""; } + + static VMapChangeEvent getAddedEvent(VMap source, Map added) { + return new VMapChangeEventImpl<>(source, VMapChange.newInstance(added), VMapChange.empty()); + } + + static VMapChangeEvent getAddedEvent(VMap source, Map added, String evtInfo) { + return new VMapChangeEventImpl<>(source, VMapChange.newInstance(added), VMapChange.empty(), evtInfo); + } + + static VMapChangeEvent getRemovedEvent(VMap source, Map removed) { + return new VMapChangeEventImpl<>(source, VMapChange.empty(), VMapChange.newInstance(removed)); + } + + static VMapChangeEvent getRemovedEvent(VMap source, Map removed, String evtInfo) { + return new VMapChangeEventImpl<>(source, VMapChange.empty(), VMapChange.newInstance(removed), evtInfo); + } + + static VMapChangeEvent getSetEvent(VMap source, Map removed, Map added) { + return new VMapChangeEventImpl<>(source, VMapChange.newInstance(added), VMapChange.newInstance(removed)); + } + + static VMapChangeEvent getSetEvent(VMap source, Map removed, Map added, String evtInfo) { + return new VMapChangeEventImpl<>(source, VMapChange.newInstance(added), VMapChange.newInstance(removed), evtInfo); + } + + String toStringWithDetails(); +} + +class VMapChangeEventImpl implements VMapChangeEvent { + + private final VMap source; + private final VMapChange added; + private final VMapChange removed; + private final String evtInfo; + + VMapChangeEventImpl(VMap source, VMapChange added, VMapChange removed) { + this.source = source; + this.added = added; + this.removed = removed; + this.evtInfo = ""; + } + + VMapChangeEventImpl(VMap source, VMapChange added, VMapChange removed, String evtInfo) { + this.source = source; + this.added = added; + this.removed = removed; + this.evtInfo = evtInfo; + } + + @Override + public boolean wasAdded() { + return added.hasChanges(); + } + + @Override + public boolean wasRemoved() { + return removed.hasChanges(); + } + + @Override + public boolean wasSet() { + return added.hasChanges() && removed.hasChanges(); + } + + @Override + public VMapChange added() { + return added; + } + + @Override + public VMapChange removed() { + return removed; + } + + @Override + public VMap source() { + return source; + } + + @Override + public String eventInfo() { + return evtInfo; + } + + @Override + public String toString() { + return "event: #added=" + added().entries().size() + ", #removed=" + removed().entries().size(); + } + + @Override + public String toStringWithDetails() { + StringBuilder sb = new StringBuilder(); + sb.append("event: [#removed: ").append(removed.entries().size()).append(", #added: ").append(added.entries().size()).append("]\n"); + sb.append("removed-keys = ").append(removed.keys()).append("\n"); + sb.append("added-keys = ").append(added.keys()).append("\n"); + return sb.toString(); + } +} diff --git a/src/main/java/eu/mihosoft/vcollections/VMapChangeListener.java b/src/main/java/eu/mihosoft/vcollections/VMapChangeListener.java new file mode 100644 index 0000000..cbba3ce --- /dev/null +++ b/src/main/java/eu/mihosoft/vcollections/VMapChangeListener.java @@ -0,0 +1,12 @@ +/* + * Copyright 2017-2019 Michael Hoffer . All rights reserved. + */ +package eu.mihosoft.vcollections; + +/** + * Map change listener. This listener is called whenever a map change occurs. + */ +@FunctionalInterface +public interface VMapChangeListener { + void onChange(VMapChangeEvent evt); +} diff --git a/src/main/java/eu/mihosoft/vcollections/VMapChangeSupport.java b/src/main/java/eu/mihosoft/vcollections/VMapChangeSupport.java new file mode 100644 index 0000000..51022b3 --- /dev/null +++ b/src/main/java/eu/mihosoft/vcollections/VMapChangeSupport.java @@ -0,0 +1,75 @@ +/* + * Copyright 2017-2019 Michael Hoffer . All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * If you use this software for scientific research then please cite the following publication(s): + * + * M. Hoffer, C. Poliwoda, & G. Wittum. (2013). Visual reflection library: + * a framework for declarative GUI programming on the Java platform. + * Computing and Visualization in Science, 2013, 16(4), + * 181–192. http://doi.org/10.1007/s00791-014-0230-y + * + * THIS SOFTWARE IS PROVIDED BY Michael Hoffer "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Michael Hoffer OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN 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. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of Michael Hoffer . + */ +package eu.mihosoft.vcollections; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map.Entry; +import vjavax.observer.Subscription; + +import eu.mihosoft.vcollections.VMapChangeListener; +import eu.mihosoft.vcollections.VMapChangeEvent; + +/** + * Map change support for managing and notifying listeners. + * + * @author Michael Hoffer + */ +public final class VMapChangeSupport implements VMapObservable { + + private final List> listeners = new ArrayList<>(); + + @Override + public Subscription addChangeListener(VMapChangeListener l) { + listeners.add(l); + return () -> listeners.remove(l); + } + + @Override + public boolean removeChangeListener(VMapChangeListener l) { + return listeners.remove(l); + } + + public void fireEvent(VMapChangeEvent evt) { + List> toNotify = new ArrayList<>(listeners); + for (VMapChangeListener l : toNotify) { + l.onChange(evt); + } + } + + public boolean hasListeners() { + return !listeners.isEmpty(); + } +} diff --git a/src/main/java/eu/mihosoft/vcollections/VMapObservable.java b/src/main/java/eu/mihosoft/vcollections/VMapObservable.java new file mode 100644 index 0000000..efa71aa --- /dev/null +++ b/src/main/java/eu/mihosoft/vcollections/VMapObservable.java @@ -0,0 +1,102 @@ +/* + * Copyright 2017-2019 Michael Hoffer . All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * If you use this software for scientific research then please cite the following publication(s): + * + * M. Hoffer, C. Poliwoda, & G. Wittum. (2013). Visual reflection library: + * a framework for declarative GUI programming on the Java platform. + * Computing and Visualization in Science, 2013, 16(4), + * 181–192. http://doi.org/10.1007/s00791-014-0230-y + * + * THIS SOFTWARE IS PROVIDED BY Michael Hoffer "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Michael Hoffer OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN 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. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of Michael Hoffer . + */ +package eu.mihosoft.vcollections; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; +import java.util.HashMap; +import vjavax.observer.Subscription; +import eu.mihosoft.vcollections.VMapChangeListener; + +/** + * Map observable. + * + * @author Michael Hoffer + */ +public interface VMapObservable { + + @SafeVarargs + static VMapObservable of(VMap... maps) { + return new VMapObservableImpl<>(Arrays.asList(maps)); + } + + static VMapObservable of(Collection> maps) { + return new VMapObservableImpl<>(maps); + } + + Subscription addChangeListener(VMapChangeListener l); + + boolean removeChangeListener(VMapChangeListener l); +} + +class VMapObservableImpl implements VMapObservable { + + private final Collection> maps; + private final Map> subscriptions = new HashMap<>(); + + VMapObservableImpl(Collection> maps) { + this.maps = maps; + } + + @Override + public Subscription addChangeListener(VMapChangeListener l) { + Collection subsOfL = subscriptions.get(l); + if (subsOfL == null) { + subsOfL = new java.util.ArrayList<>(); + subscriptions.put(l, subsOfL); + } + for (VMap m : maps) { + Subscription s = m.addChangeListener(l); + subsOfL.add(s); + } + return () -> { + Collection subs = subscriptions.get(l); + if (subs != null) { + subs.forEach(Subscription::unsubscribe); + } + }; + } + + @Override + public boolean removeChangeListener(VMapChangeListener l) { + Collection subs = subscriptions.get(l); + if (subscriptions.containsKey(l) && subs != null) { + subs.forEach(Subscription::unsubscribe); + subscriptions.remove(l); + } + return subs != null; + } +} diff --git a/src/main/java/eu/mihosoft/vcollections/VMappedList.java b/src/main/java/eu/mihosoft/vcollections/VMappedList.java index 3da38e6..ca6c399 100644 --- a/src/main/java/eu/mihosoft/vcollections/VMappedList.java +++ b/src/main/java/eu/mihosoft/vcollections/VMappedList.java @@ -206,13 +206,14 @@ public Subscription addChangeListener(CollectionChangeListener(VMappedList.this, VListChange.newInstance(evt.added().indices(), - evt.added().elements().stream(). - map(fromOrigToThis). - collect(Collectors.toList())), + evt.added().elements().stream() + .map(fromOrigToThis) + .collect(Collectors.toList())), VListChange.newInstance(evt.removed().indices(), - evt.removed().elements().stream(). - map(fromOrigToThis). - collect(Collectors.toList())) + evt.removed().elements().stream() + .map(fromOrigToThis) + .collect(Collectors.toList())), + evt.eventInfo() ); l.onChange(e); diff --git a/src/main/java/eu/mihosoft/vcollections/VMappedMap.java b/src/main/java/eu/mihosoft/vcollections/VMappedMap.java new file mode 100644 index 0000000..e7b5166 --- /dev/null +++ b/src/main/java/eu/mihosoft/vcollections/VMappedMap.java @@ -0,0 +1,173 @@ +/* + * Copyright 2017-2019 Michael Hoffer . All rights reserved. + */ +package eu.mihosoft.vcollections; + +import java.util.AbstractMap; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import vjavax.observer.Subscription; + +/** + * Mapped observable map that reflects changes from and to an underlying map. + * + * @param key type of this map + * @param value type of this map + * @param key type of the original map + * @param value type of the original map + */ +public final class VMappedMap extends AbstractMap implements VMap { + + private final VMap originalMap; + private final Function fromOrigKey; + private final Function fromThisKey; + private final Function fromOrigValue; + private final Function fromThisValue; + + private final Map, VMapChangeListener> listenerMap = new HashMap<>(); + + public static VMap newInstance(Map srcMap, + Function fromOrigKey, + Function fromThisKey, + Function fromOrigValue, + Function fromThisValue) { + return new VMappedMap<>(srcMap, fromOrigKey, fromThisKey, fromOrigValue, fromThisValue); + } + + public static VMap newUnmodifiableInstance(VMap srcMap, + Function fromOrigKey, + Function fromOrigValue) { + return new VMappedMap<>(srcMap.asUnmodifiable(), fromOrigKey, + k -> { throw new UnsupportedOperationException("Cannot modify an unmodifiable map."); }, + fromOrigValue, + v -> { throw new UnsupportedOperationException("Cannot modify an unmodifiable map."); }); + } + + @SuppressWarnings("unchecked") + private VMappedMap(Map srcMap, + Function fromOrigKey, + Function fromThisKey, + Function fromOrigValue, + Function fromThisValue) { + if (srcMap instanceof VMap) { + this.originalMap = (VMap) srcMap; + } else { + this.originalMap = VMap.newInstance(srcMap); + } + this.fromOrigKey = fromOrigKey; + this.fromThisKey = fromThisKey; + this.fromOrigValue = fromOrigValue; + this.fromThisValue = fromThisValue; + } + + @Override + public void setEventInfo(String evtInfo) { + originalMap.setEventInfo(evtInfo); + } + + @Override + public String getEventInfo() { + return originalMap.getEventInfo(); + } + + @Override + public int size() { + return originalMap.size(); + } + + @Override + public boolean isEmpty() { + return originalMap.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return originalMap.containsKey(fromThisKey.apply((K) key)); + } + + @Override + public boolean containsValue(Object value) { + return originalMap.containsValue(fromThisValue.apply((V) value)); + } + + @Override + public V get(Object key) { + OV v = originalMap.get(fromThisKey.apply((K) key)); + return v != null ? fromOrigValue.apply(v) : null; + } + + @Override + public V put(K key, V value) { + OV prev = originalMap.put(fromThisKey.apply(key), fromThisValue.apply(value)); + return prev != null ? fromOrigValue.apply(prev) : null; + } + + @Override + public V remove(Object key) { + OV prev = originalMap.remove(fromThisKey.apply((K) key)); + return prev != null ? fromOrigValue.apply(prev) : null; + } + + @Override + public void clear() { + originalMap.clear(); + } + + @Override + public void putAll(Map m) { + Map mapped = new HashMap<>(); + for (Entry e : m.entrySet()) { + mapped.put(fromThisKey.apply(e.getKey()), fromThisValue.apply(e.getValue())); + } + originalMap.putAll(mapped); + } + + @Override + public Set> entrySet() { + return originalMap.entrySet().stream() + .map(e -> new SimpleEntry<>(fromOrigKey.apply(e.getKey()), fromOrigValue.apply(e.getValue()))) + .collect(Collectors.toSet()); + } + + @Override + public VMap asUnmodifiable() { + throw new UnsupportedOperationException("Unsupported operation: use 'newUnmodifiableInstance(...)' instead."); + } + + @Override + public boolean removeAll(K... keys) { + OK[] mapped = java.util.Arrays.stream(keys) + .map(fromThisKey) + .toArray(size -> (OK[]) new Object[size]); + return originalMap.removeAll(mapped); + } + + @Override + public Subscription addChangeListener(VMapChangeListener l) { + VMapChangeListener mappedListener = evt -> { + Map added = evt.added().entries().entrySet().stream() + .collect(Collectors.toMap(e -> fromOrigKey.apply(e.getKey()), e -> fromOrigValue.apply(e.getValue()))); + Map removed = evt.removed().entries().entrySet().stream() + .collect(Collectors.toMap(e -> fromOrigKey.apply(e.getKey()), e -> fromOrigValue.apply(e.getValue()))); + VMapChangeEvent e = new VMapChangeEventImpl<>(VMappedMap.this, + VMapChange.newInstance(added), + VMapChange.newInstance(removed), + evt.eventInfo()); + l.onChange(e); + }; + listenerMap.put(l, mappedListener); + return originalMap.addChangeListener(mappedListener); + } + + @Override + public boolean removeChangeListener(VMapChangeListener l) { + VMapChangeListener ml = listenerMap.remove(l); + if (ml != null) { + return originalMap.removeChangeListener(ml); + } + return false; + } +} diff --git a/src/test/java/eu/mihosoft/vcollections/VListTest.java b/src/test/java/eu/mihosoft/vcollections/VListTest.java index 3725570..0f4f4e5 100644 --- a/src/test/java/eu/mihosoft/vcollections/VListTest.java +++ b/src/test/java/eu/mihosoft/vcollections/VListTest.java @@ -38,6 +38,7 @@ import org.junit.Test; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.ListIterator; @@ -45,6 +46,8 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; +import eu.mihosoft.vcollections.VListChangeEvent; + /** * Created by miho on 16.01.2017. */ @@ -491,6 +494,33 @@ public void iteratorTest() { } } + @Test + public void setAllEventContainsPreviousElements() { + VList vList = VList.newInstance(new ArrayList<>(Arrays.asList(0,1,2,3,4))); + + List> events = new ArrayList<>(); + vList.addChangeListener(evt -> events.add((VListChangeEvent) evt)); + + vList.setAll(1, Arrays.asList(10, 11)); + + Assert.assertEquals(1, events.size()); + VListChangeEvent evt = events.get(0); + Assert.assertEquals(Arrays.asList(1, 2), evt.removed().elements()); + Assert.assertEquals(Arrays.asList(10, 11), evt.added().elements()); + } + + @Test + public void removeChangeListenerResetsSupport() throws Exception { + VList vList = VList.newInstance(new ArrayList()); + VListChangeListener l = evt -> {}; + vList.addChangeListener(l); + vList.removeChangeListener(l); + + java.lang.reflect.Field f = vList.getClass().getDeclaredField("listChangeSupport"); + f.setAccessible(true); + Assert.assertNull(f.get(vList)); + } + private void createIteratorAddTest(int size) { // creates a list with size random integers List aList = new ArrayList<>(); diff --git a/src/test/java/eu/mihosoft/vcollections/VMapTest.java b/src/test/java/eu/mihosoft/vcollections/VMapTest.java new file mode 100644 index 0000000..8e07c7d --- /dev/null +++ b/src/test/java/eu/mihosoft/vcollections/VMapTest.java @@ -0,0 +1,235 @@ +/* + * Copyright 2017-2019 Michael Hoffer . All rights reserved. + */ +package eu.mihosoft.vcollections; + +import org.junit.Assert; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.stream.Collectors; + +/** + * Tests for {@link VMap}. + */ +public class VMapTest { + + @Test + public void eventInfoTest() { + VMap map = VMap.newInstance(new HashMap()); + String info = "info"; + map.setEventInfo(info); + List> events = new ArrayList<>(); + map.addChangeListener(e -> events.add((VMapChangeEvent) e)); + map.put("a", 1); + Assert.assertEquals(1, events.size()); + Assert.assertEquals(info, events.get(0).eventInfo()); + } + + @Test + public void wrapMapEqualsTest() { + Map base = new HashMap<>(); + addRandomEntries(10, base); + VMap map = VMap.newInstance(base); + + Assert.assertEquals(base, map); + + // modify map + addRandomEntries(3, map); + Assert.assertEquals(base, map); + + String keyToRemove = map.keySet().iterator().next(); + map.remove(keyToRemove); + Assert.assertEquals(base, map); + } + + @Test + public void vMapNotEqualsTest() { + Map base = new HashMap<>(); + addRandomEntries(5, base); + VMap m1 = VMap.newInstance(base); + VMap m2 = VMap.newInstance(new HashMap<>(base)); + + String key = base.keySet().iterator().next(); + m2.put(key, m2.get(key) + 1); + Assert.assertNotEquals(m1, m2); + } + + @Test + public void changeOnPutNotificationTest() { + for (int i = 0; i < 50; i++) { + int size = Math.max(1, getRandom().nextInt(20)); + createPutEntryTest(size); + createPutAllTest(size, Math.max(1, getRandom().nextInt(5))); + createUpdateEntryTest(size); + } + } + + private void createPutEntryTest(int size) { + Map base = new HashMap<>(); + addRandomEntries(size, base); + VMap map = VMap.newInstance(base); + + List> events = new ArrayList<>(); + map.addChangeListener(e -> events.add((VMapChangeEvent) e)); + + String k = randomKey(); + int v = getRandom().nextInt(); + map.put(k, v); + + Assert.assertEquals(1, events.size()); + VMapChangeEvent evt = events.get(0); + Assert.assertTrue(evt.wasAdded()); + Assert.assertFalse(evt.wasRemoved()); + Assert.assertEquals(v, evt.added().entries().get(k).intValue()); + } + + private void createUpdateEntryTest(int size) { + Map base = new HashMap<>(); + addRandomEntries(size, base); + VMap map = VMap.newInstance(base); + + List> events = new ArrayList<>(); + map.addChangeListener(e -> events.add((VMapChangeEvent) e)); + + String k = base.keySet().iterator().next(); + int newVal = getRandom().nextInt(); + int oldVal = base.get(k); + map.put(k, newVal); + + Assert.assertEquals(1, events.size()); + VMapChangeEvent evt = events.get(0); + Assert.assertTrue(evt.wasSet()); + Assert.assertEquals(oldVal, evt.removed().entries().get(k).intValue()); + Assert.assertEquals(newVal, evt.added().entries().get(k).intValue()); + } + + private void createPutAllTest(int baseSize, int addSize) { + Map base = new HashMap<>(); + addRandomEntries(baseSize, base); + VMap map = VMap.newInstance(base); + + Map toAdd = new HashMap<>(); + addRandomEntries(addSize, toAdd); + + List> events = new ArrayList<>(); + map.addChangeListener(e -> events.add((VMapChangeEvent) e)); + + map.putAll(toAdd); + + Assert.assertEquals(1, events.size()); + VMapChangeEvent evt = events.get(0); + Assert.assertTrue(evt.wasAdded()); + Assert.assertEquals(toAdd, evt.added().entries()); + } + + @Test + public void changeOnRemoveNotificationTest() { + for (int i = 0; i < 50; i++) { + int size = Math.max(2, getRandom().nextInt(20)); + createRemoveEntryTest(size); + createRemoveAllEntriesTest(size, Math.max(1, size - 1)); + } + } + + private void createRemoveEntryTest(int size) { + Map base = new HashMap<>(); + addRandomEntries(size, base); + VMap map = VMap.newInstance(base); + + String key = base.keySet().iterator().next(); + int val = base.get(key); + + List> events = new ArrayList<>(); + map.addChangeListener(e -> events.add((VMapChangeEvent) e)); + + map.remove(key); + + Assert.assertEquals(1, events.size()); + VMapChangeEvent evt = events.get(0); + Assert.assertTrue(evt.wasRemoved()); + Assert.assertEquals(val, evt.removed().entries().get(key).intValue()); + } + + private void createRemoveAllEntriesTest(int size, int removeCount) { + Map base = new HashMap<>(); + addRandomEntries(size, base); + VMap map = VMap.newInstance(base); + + List keys = new ArrayList<>(base.keySet()).subList(0, removeCount); + Map expected = keys.stream().collect(Collectors.toMap(k -> k, base::get)); + + List> events = new ArrayList<>(); + map.addChangeListener(e -> events.add((VMapChangeEvent) e)); + + map.removeAll(keys.toArray(new String[0])); + + Assert.assertEquals(1, events.size()); + VMapChangeEvent evt = events.get(0); + Assert.assertTrue(evt.wasRemoved()); + Assert.assertEquals(expected, evt.removed().entries()); + } + + @Test + public void clearEventTest() { + Map base = new HashMap<>(); + addRandomEntries(5, base); + VMap map = VMap.newInstance(base); + + List> events = new ArrayList<>(); + map.addChangeListener(e -> events.add((VMapChangeEvent) e)); + + int sizeBefore = base.size(); + map.clear(); + Assert.assertEquals(1, events.size()); + Assert.assertEquals(sizeBefore, events.get(0).removed().entries().size()); + Assert.assertTrue(map.isEmpty()); + } + + @Test + public void removeChangeListenerResetsSupport() throws Exception { + VMap map = VMap.newInstance(new HashMap()); + VMapChangeListener l = evt -> {}; + map.addChangeListener(l); + map.removeChangeListener(l); + + Field f = map.getClass().getDeclaredField("mapChangeSupport"); + f.setAccessible(true); + Assert.assertNull(f.get(map)); + } + + @Test + public void observableOfAggregatesEvents() { + VMap m1 = VMap.newInstance(new HashMap()); + VMap m2 = VMap.newInstance(new HashMap()); + VMapObservable obs = VMapObservable.of(m1, m2); + + List> events = new ArrayList<>(); + obs.addChangeListener(e -> events.add((VMapChangeEvent) e)); + + m1.put("a", 1); + m2.put("b", 2); + + Assert.assertEquals(2, events.size()); + } + + private static String randomKey() { + return "k" + getRandom().nextInt(); + } + + private static void addRandomEntries(int length, Map map) { + Random r = getRandom(); + for (int i = 0; i < length; i++) { + map.put(randomKey(), r.nextInt()); + } + } + + public static Random getRandom() { + return VListTest.getRandom(); + } +} diff --git a/src/test/java/eu/mihosoft/vcollections/VMappedListTest.java b/src/test/java/eu/mihosoft/vcollections/VMappedListTest.java new file mode 100644 index 0000000..4a03de2 --- /dev/null +++ b/src/test/java/eu/mihosoft/vcollections/VMappedListTest.java @@ -0,0 +1,74 @@ +package eu.mihosoft.vcollections; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +/** + * Tests for {@link VMappedList}. + */ +public class VMappedListTest { + + private final Function fromOrig = i -> "n" + i; + private final Function toOrig = s -> Integer.parseInt(s.substring(1)); + + @Test + public void addPropagatesToSourceAndFiresEvent() { + VList src = VList.newInstance(new ArrayList<>()); + VList list = VMappedList.newInstance(src, fromOrig, toOrig); + + List> events = new ArrayList<>(); + list.addChangeListener(e -> events.add((VListChangeEvent) e)); + + list.add("n5"); + + Assert.assertEquals(Integer.valueOf(5), src.get(0)); + Assert.assertEquals(1, events.size()); + Assert.assertTrue(events.get(0).wasAdded()); + Assert.assertEquals("n5", events.get(0).added().elements().get(0)); + } + + @Test + public void sourceChangesReflected() { + VList src = VList.newInstance(new ArrayList<>()); + VList list = VMappedList.newInstance(src, fromOrig, toOrig); + + src.add(3); + + Assert.assertEquals("n3", list.get(0)); + } + + @Test + public void removePropagates() { + VList src = VList.newInstance(new ArrayList<>()); + src.add(7); + VList list = VMappedList.newInstance(src, fromOrig, toOrig); + + List> events = new ArrayList<>(); + list.addChangeListener(e -> events.add((VListChangeEvent) e)); + + String removed = list.remove(0); + + Assert.assertEquals("n7", removed); + Assert.assertTrue(src.isEmpty()); + Assert.assertEquals(1, events.size()); + Assert.assertTrue(events.get(0).wasRemoved()); + Assert.assertEquals("n7", events.get(0).removed().elements().get(0)); + } + + @Test + public void eventInfoPropagated() { + VList src = VList.newInstance(new ArrayList<>()); + VList list = VMappedList.newInstance(src, fromOrig, toOrig); + list.setEventInfo("info"); + + List> events = new ArrayList<>(); + list.addChangeListener(e -> events.add((VListChangeEvent) e)); + list.add("n2"); + + Assert.assertEquals("info", events.get(0).eventInfo()); + } +} diff --git a/src/test/java/eu/mihosoft/vcollections/VMappedMapTest.java b/src/test/java/eu/mihosoft/vcollections/VMappedMapTest.java new file mode 100644 index 0000000..6e6e702 --- /dev/null +++ b/src/test/java/eu/mihosoft/vcollections/VMappedMapTest.java @@ -0,0 +1,78 @@ +package eu.mihosoft.vcollections; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +/** + * Tests for {@link VMappedMap}. + */ +public class VMappedMapTest { + + private final Function kFromOrig = i -> "k" + i; + private final Function kToOrig = s -> Integer.parseInt(s.substring(1)); + private final Function vFromOrig = i -> "v" + i; + private final Function vToOrig = s -> Integer.parseInt(s.substring(1)); + + @Test + public void putPropagatesToSourceAndFiresEvent() { + VMap src = VMap.newInstance(new HashMap<>()); + VMap map = VMappedMap.newInstance(src, kFromOrig, kToOrig, vFromOrig, vToOrig); + + List> events = new ArrayList<>(); + map.addChangeListener(e -> events.add((VMapChangeEvent) e)); + + map.put("k1", "v5"); + + Assert.assertEquals(Integer.valueOf(5), src.get(1)); + Assert.assertEquals(1, events.size()); + Assert.assertTrue(events.get(0).wasAdded()); + Assert.assertEquals("v5", events.get(0).added().entries().get("k1")); + } + + @Test + public void sourceChangesReflected() { + VMap src = VMap.newInstance(new HashMap<>()); + VMap map = VMappedMap.newInstance(src, kFromOrig, kToOrig, vFromOrig, vToOrig); + + src.put(2, 8); + + Assert.assertEquals("v8", map.get("k2")); + } + + @Test + public void removePropagates() { + VMap src = VMap.newInstance(new HashMap<>()); + src.put(3, 9); + VMap map = VMappedMap.newInstance(src, kFromOrig, kToOrig, vFromOrig, vToOrig); + + List> events = new ArrayList<>(); + map.addChangeListener(e -> events.add((VMapChangeEvent) e)); + + String val = map.remove("k3"); + + Assert.assertEquals("v9", val); + Assert.assertFalse(src.containsKey(3)); + Assert.assertEquals(1, events.size()); + Assert.assertTrue(events.get(0).wasRemoved()); + Assert.assertEquals("v9", events.get(0).removed().entries().get("k3")); + } + + @Test + public void eventInfoPropagated() { + VMap src = VMap.newInstance(new HashMap<>()); + VMap map = VMappedMap.newInstance(src, kFromOrig, kToOrig, vFromOrig, vToOrig); + map.setEventInfo("info"); + + List> events = new ArrayList<>(); + map.addChangeListener(e -> events.add((VMapChangeEvent) e)); + map.put("k4", "v1"); + + Assert.assertEquals("info", events.get(0).eventInfo()); + } +}